If you want to offer your website not only with HTTP, but also with the secure HTTPS protocol, you need a signed SSL certificate. Here are some reasons why you need HTTPS….

  1. Security: HTTPS helps secure the connection between a client and a server by encrypting the data transmitted between them.

  2. Trust: HTTPS gives your website a certain level of trust and credibility. Today’s browsers usually display a warning if a website only offers HTTP or the certificate is not signed by a certification authority such as Let’s Encrypt.

  3. SEO: Search engines use SSL/TLS certificates as a ranking factor. This means that an SSL/TLS certificate can help improve your website’s search engine rankings.

Roughly speaking, HTTPS is the state of the art that every website should support.

You get the signed certificate from a certification authority (CA). In this article we use Let’s Encrypt.

Theory

To be honest, you could ignore the theory and jump straight to practice because the cert manager and the ingress controller do everything for you. But for possible troubleshooting here is the theory.

What is Let’s Encrypt

Let’s Encrypt is a free and open Certificate Authority (CA). It offers Domain Validation (DV) SSL/TLS certificates used to secure and encrypt data sent between a client (e.g. a web browser) and a server (e.g. a website).

Its big advantage is that the certificate obtaining process with Let’s Encrypt is completely automatic. This means that you only have to set up the system once and then never have to worry about it again.

You don’t need an account with Let’s Encrypt because all communication between your Kubernetes cluster and the Let’s Encrypt website is handled automatically.

How does the certificate obtaining process work with Let’s Encrypt

In order to obtain an SSL/TLS certificate from Let’s Encrypt, your system must go through two steps: 1. Domain Validation and 2. Certificate Issuance.

These certificates are usually valid for 90 days and are then automatically renewed.

Domain Validation

In the first step, your Kubernetes cluster must prove that it owns the domain for which it wants the certificate. This works like this:

  1. Kubernetes asks Lets Encrypt for domain validation.
  2. Let’s Encrypt issues a challenge* and sends a nonce to sign.
  3. Kubernetes solves the challenge and signs the nonce with its private key.
  4. Let’s Encrypt checks if the challenges have been solved and verifies the signature on the nonce.
  5. If everything worked, the key pair used by Kubernetes is now an authorized key pair.

* A challenge can be providing a DNS record, or simply hosting a file and path chosen by Let’s Encrypt.

Certificate issuance

With the authorized key pair, your Kubernetes cluster can request, renew, and revoke certificates for your domain. It works like this:

  1. Kubernetes issues a “PKCS#10” certificate signing request (CSR). This CSR is signed by the authorized key pair.
  2. Let’s Encrypt verifies both signatures. If everything looks good, they issue a certificate for the requested domain with the CSR’s public key and send it back.
  3. Kubernetes can now use this certificate for SSL communication.

Practice

I’m using Azure Kubernetes Service (AKS) version 1.23. It should work the same way on a self-hosted Kubernetes cluster, please give me feedback!

The goal of this exercise is that you can apply for a certificate using ingress rules.

In principle, your Kubernetes cluster needs 3 things:

  • the Cert-Manager
  • a ClusterIssuer resource
  • an ingress controller

Install Cert-Manager

The Cert-Manager adds certificates and certificate issuers as resource types to Kubernetes and ensures that certificates are valid and up-to-date, and tries to renew certificates before they expire.

In principle, it can communicate with different systems such as Hashicorp Vault or Venafi. But we just need it for Let’s Encrypt.

I am currently using the Cert Manager in the latest version 1.10.1. This version supports the Kubernetes versions 1.20 to 1.26. Check version incompatibilities here.

You can easily roll it out on your Kubernetes server using the Helm chart from the Jetstack Repository.

helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install cert-manager jetstack/cert-manager --namespace cert-manager --create-namespace --version v1.10.1 --set installCRDs=true

These commands create a new namespace called “cert-manager”. Due to the --set installCRDs=true flag, additional custom resource definitions are created.

You will notice that there are 3 new pods running.

Install ClusterIssuer

Now that Cert-Manager is installed, we need to tell it that we want to use Let’s Encrypt as the Certificate Authority (CA). This can be done by creating a ClusterIssuer resource. Simply create it with a .yaml file.

clusterissuer.yaml

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: YOUR@EMAIL.COM
    privateKeySecretRef:
      name: letsencrypt
    solvers:
      - http01:
          ingress:
            class: nginx

Replace YOUR@EMAIL.COM with your email address. Letsencrypt uses it to warn you about expiring certificates if the automatic renewal did not work. You don’t need an account with Let’s Encrypt or anything like that.

Deploy this YAML as usual on your K8s cluster.

kubectl apply -f clusterissuer.yaml

Install Ingress Controller

If you don’t have an ingress controller, now is the ideal time to install one. We will use the NGINX ingress controller.

An ingress controller is the system that receives incoming requests and forwards them to your internal services based on ingress rules.

Here’s how to install it with Helm into the ingress-basic namespace.

helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm install ingress-nginx ingress-nginx/ingress-nginx --create-namespace --namespace ingress-basic --set controller.service.annotations."service\.beta\.kubernetes\.io/azure-load-balancer-health-probe-request-path"=/healthz --set controller.replicaCount=3 --set controller.service.externalTrafficPolicy=Local

Note that I set “replicaCount=3” here. The larger the replica number, the more requests your cluster can accept at the same time. However, each replica also needs some CPU and RAM. A good value is 1 replica per node.

With controller.service.externalTrafficPolicy=Local the IP addresses are forwarded to your pods. You may not need this in your setup. Usually this shouldn’t cause any problems.

This launches a few pods aswel.

Ingress Rules: Practical Example

Since last week I’m hosting my german blog on Azure Blob as Static Website instead of WordPress.

There are two reasons for this. On the one hand, WordPress was quite slow with the growing number of plugins and, on the other hand, I always have to pay the complete managed disk, regardless on how many gigabytes I used. With Blob Storages I only pay for what I actually use.

The Blob Storage is located on www.quisl.de using a DNS C entry. But I would like the blog to be accessible directly on quisl.de (without www). This is where the Kubernetes server comes into play as a forwarder.

First I create a service that points as ExternalName from www.quisl.de.

service.yaml

apiVersion: v1
kind: Service
metadata:
  name: blog
spec:
  type: ExternalName
  externalName: www.quisl.de
  ports:
    - port: 80
      targetPort: 80
      name: http
      protocol: TCP
    - port: 443
      targetPort: 443
      name: https
      protocol: TCP

And now I can write an ingress rule that forwards all traffic from quisl.de to this service.

ingress.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: blog-ingress
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/rewrite-target: /$1
    nginx.ingress.kubernetes.io/use-regex: "true"
    cert-manager.io/cluster-issuer: letsencrypt
    nginx.ingress.kubernetes.io/backend-protocol: https
    nginx.ingress.kubernetes.io/upstream-vhost: www.quisl.de
spec:
  tls:
    - hosts:
        - quisl.de
      secretName: tls-secret
  rules:
    - host: quisl.de
      http:
        paths:
          - backend:
              service:
                name: blog
                port:
                  number: 443
            path: /(.*)
            pathType: ImplementationSpecific

I pushed the whole thing to my cluster.

kubectl apply -f .\service.yaml
kubectl apply -f .\ingress.yaml

Cert-Manager takes care of the certificate in the background and quisl.de was reachable within a minute.

Conclusion

Now everything should work. If not, I can recommend the Troubleshooting Website from Cert-Manager.

With the help of ingress rules, you can request a certificate for every URL. Of course, the URL must each have an A record that points to the IP of your Kubernetes cluster. This way the Cert Manager can complete the challenge and receive a Let’s Encrypt SSL certificate.


Could I help? Buy me a drink ! 💙