k3s HTTPS with Let’s Encrypt

This post was updated in November 2021 to be compatible with the latest K3s (v1.21.5+k3s2) and cert-manager (1.6.0) at the time of writing. Previously this post used Helm but this is no longer necessary.

K3s is a Certified Kubernetes distribution designed for production workloads in unattended, resource-constrained, remote locations or inside IoT appliances. It provides a ready to go Kubernetes instance packaged as a single binary.

This guide will show you how to easily set up k3s with HTTPS via Let’s Encrypt.

K3s comes with Traefik out of the box. Traefik itself supports automatic HTTPS via Let’s Encrypt, but setting this up with k3s turned out to be pretty cumbersome. Instead using the excellent cert-manager add-on, it's a breeze!

0: Setup k3s.

This guide assumes you have a working k3s instance and kubectl configured to talk to your k3s instance. If you haven't already just follow the docs and you'll be up and running in minutes.

1. Install cert-manager

cert-manager is a Kubernetes add-on to automate the management and issuance of TLS certificates from various issuing sources, amongst which Let’s Encrypt.

cert-manager can be installed in several ways. The most straightforward way is via kubectl apply:

kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.6.0/cert-manager.yaml

Note: 1.6.0 is the latest version of cert-manager at the time of writing. Be sure to check the latest version, but take into account that newer versions might not be fully compatible with instructions in this post.

It might take a minute or so for all components to be installed and up running. Verify the installation by checking all cert-manager pods are running:

kubectl get all -n cert-manager

2. Configure cert-manager

With cert-manager installed we need to configure it to use Let’s Encrypt, by creating a certificate issuer:

cat <<EOF > letsencrypt-prod-issuer.yaml
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: letsencrypt-prod
spec:
acme:
# The ACME server URL
server: https://acme-v02.api.letsencrypt.org/directory
# Email address used for ACME registration, update to your own.
email: user@example.com
# Name of a secret used to store the ACME account private key
privateKeySecretRef:
name: letsencrypt-prod
# Enable the HTTP-01 challenge provider
solvers:
# An empty 'selector' means that this solver matches all domains
- selector: {}
http01:
ingress: {}
EOF

⚠️ Make sure to update the email address to your own.

And apply it:

kubectl apply -f letsencrypt-prod-issuer.yaml

By now, cert-manager is ready to provision Let’s Encrypt certificates as we need it.

3. Deploy a service with automatic HTTPS

To try this out let's deploy a simple service. You should have a DNS name pointed to your k3s cluster for this to work. The example below uses bootcamp.k3s.example.org, be sure to update this to your own hostname!

Create the manifest:

cat <<EOF > k8s-bootcamp.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: k8s-bootcamp
spec:
replicas: 1
selector:
matchLabels:
app: k8s-bootcamp
template:
metadata:
labels:
app: k8s-bootcamp
spec:
containers:
- name: k8s-bootcamp
image: gcr.io/google-samples/kubernetes-bootcamp:v1
---
apiVersion: v1
kind: Service
metadata:
name: k8s-bootcamp
spec:
ports:
- name: http
targetPort: 8080
port: 80
selector:
app: k8s-bootcamp
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: k8s-bootcamp
annotations:
kubernetes.io/ingress.class: "traefik"
cert-manager.io/issuer: "letsencrypt-prod"
spec:
tls:
- hosts:
# Change this to your own hostname
- bootcamp.k3s.example.org
secretName: bootcamp-k3s-example-org-tls
rules:
# Change this to your own hostname
- host: bootcamp.k3s.example.org
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: k8s-bootcamp
port:
name: http
EOF

And apply it:

kubectl apply -f k8s-bootcamp.yaml

Wait a minute or so and your endpoint should become available with a Let’s Encrypt certificate! 🎉

Note that the Traefik ingress doesn't automatically redirect HTTP to HTTPS, like the Nginx ingress does. I haven't dug into how to enable this with Traefik, as I've been using the Nginx ingress myself.