Let's Encrypt HTTPS/SSL for Microk8s

Let’s Encrypt is an excellent way to have https for free. The tutorial at Microk8s addon: Cert Manager lists all the steps but doesn’t explain much. Here’s what I learned.

Go with the tutorial at Microk8s addon: Cert Manager; I’ll explain everything below.

The first step is to create the ClusterIssuer resource. Such resources are not bound to namespaces, and therefore all apps are able to access that resource. Once you create the ClusterIssuer resource, CertManager will know how to issue the certificate. The resource tells CertManager to use the ACME protocol which is supported by Let’s Encrypt: that’s the acme: part. The server: part points to the REST endpoint which implements ACME; in this case the endpoint points towards Let’s Encrypt. CertManager will generate a certificate authority key to letsencrypt-account-key and you can view it in the Kubernetes Dashboard, under “Secrets”.

The ACME protocol goes like this: the Certificate Authority (generated above) will produce a certificate for your DNS. It will then ask Let’s Encrypt to sign the certificate, in order for all browsers to trust that certificate. Let’s Encrypt will talk to certbot running on your DNS, to prove that you own the DNS. CertManager will run certbot automatically during certificate obtain/renewal process. CertManager will then store the signed certificate locally, and will provide it to the browser when a https communication is attempted.

This is where the Ingress configuration comes to play. Ingress is annotated with cert-manager.io/cluster-issuer: lets-encrypt which tells CertManager that we are going to issue a certificate for this site, and we’ll use the lets-encrypt ClusterIssuer. Certbot will take a look at the tls/secretName name (say it’s microbot-ingress-tls), and will run the ACME certificate signing for all hosts mentioned, and will store the certificate to given secret microbot-ingress-tls.

The rules/host is also important: it then tells Ingress https engine which certificate for which host to use. If you omit this, Ingress will use a default certificate (by default self-signed Kubernetes certificate but can be changed, see below) which will be rejected by the browsers.

Multiple Ingress configurations pointing towards the same DNS

If you have multiple Ingress configuration resources for the same host split across multiple files, the question is, which parts of the Ingress configuration should be repeated, and which parts should not. There are two parts in question:

  • The cert-manager.io/cluster-issuer: lets-encrypt annotation which activates CertManager auto-renewal; and
  • spec/tls which configures Ingress HTTPS support for this resource.

I think that the cert-manager.io/cluster-issuer: lets-encrypt annotation may or may not be repeated; if repeated, in the worst case CertManager may ask for signing too many times. The spec/tls needs to be repeated otherwise Ingress HTTPS won’t activate.

When Annotation is not repeated

I was thinking of creating a very simple nginx static backend service; the sole purpose for it would be to activate CertManager auto-renewal for my-dns.com.

The problem is that Secrets are namespaced and tied to that namespace; an app from a different namespace can not obtain access to a secret from another namespace: #8083 and #2170. But there’s a way: Syncing Secrets Across Namespaces.

Looks like the Nginx default certificate is the simplest solution, but you can obviously make only one DNS the default one.

Troubleshooting

Q: I have Error from server (InternalError): error when creating "letsencrypt.yaml": Internal error occurred: failed calling webhook "webhook.cert-manager.io": failed to call webhook: Post "https://cert-manager-webhook.cert-manager.svc:443/mutate?timeout=10s": dial tcp 10.152.183.130:443: connect: connection refused

A: If you just installed cert-manager addon, it may still be initializing/downloading images for pods. For me, the issue resolved itself in a minute or two. If that doesn’t work, try to completely uninstall cert-manager.

Q: The secret name has 5 alphanumeric characters appended in Kubernetes Dashboard (e.g. v-herd-eu-ingress-tls-reya6 instead of v-herd-eu-ingress-tls)

A: cert-manager is in the process of refreshing that secret. Wait 10 seconds or so; check the pods list for cm-acme-http-solver to find the certbot running. Once certbot is done, it will rename the secret back to v-herd-eu-ingress-tls. If that doesn’t work, cert-manager could be stuck. Try completely uninstalling cert-manager. You can check the cert-manager-* pod for logs. Alternative is to simply uninstall microk8s via snap remove --purge microk8s, then install it back.

Note that cert-manager works with microk8s even when not in ha-cluster mode.

Getting The Certificates

Cert Manager stores the SSL certificate and private key in the kubernetes secret as a pair of keys: the tls.crt and tls.key. To see the SSL certificate from command-line, enter:

$ microk8s kubectl get secret v-herd-eu-ingress-tls  --namespace v-herd-eu-welcome-page -o jsonpath='{.data.tls\.crt}'|base64 --decode

To obtain the private key:

$ microk8s kubectl get secret v-herd-eu-ingress-tls  --namespace v-herd-eu-welcome-page -o jsonpath='{.data.tls\.key}'|base64 --decode

nginx

If you store those files on a filesystem, you can then feed them for example to an external nginx:

server {
	listen 9443 ssl default_server;
	listen [::]:9443 ssl default_server;
	ssl_certificate /etc/nginx/secret/tls.crt;
	ssl_certificate_key /etc/nginx/secret/tls.key;
    location / {
      proxy_pass http://localhost:8080/;
      # proxy_cookie_path / /foo; # use this if you mount your app to `location /foo/`
      proxy_cookie_domain localhost $host;
    }
}

You can periodically update the certificates, e.g. once per month. Create a file named /etc/cron.monthly/nginx-update-tls with these contents:

#!/bin/bash
set -e
microk8s kubectl get secret v-herd-eu-ingress-tls  --namespace v-herd-eu-welcome-page -o jsonpath='{.data.tls\.crt}'|base64 --decode >/etc/nginx/secret/tls.crt
microk8s kubectl get secret v-herd-eu-ingress-tls  --namespace v-herd-eu-welcome-page -o jsonpath='{.data.tls\.key}'|base64 --decode >/etc/nginx/secret/tls.key
systemctl reload nginx

Don’t forget to chmod a+x /etc/cron.monthly/nginx-update-tls.

This way, you can easily expose services, which are running outside kubernetes, yet they will still be protected by https. The only disadvantage is that they’ll run on a different port, in this case 9443.

Written on February 9, 2023