Serving Every k3s Ingress over HTTPS with One Default Certificate
With a wildcard certificate in the cluster, I don't want TLS settings in every Ingress. Traefik can serve one certificate as its default, and the Traefik that ships with k3s already turns on TLS for every route on its HTTPS port.
Together, those mean an Ingress with a *.lab.example.com host is served over HTTPS without a tls: section at all.
Placeholders: example.com is your domain, and <app> is any app behind Traefik.
How it works
Two pieces of Traefik configuration combine:
- The
websecureentrypoint has TLS on. In the Traefik Helm chart that k3s ships,ports.websecure.http.tls.enableddefaults totrue. Every router on port 443 terminates TLS, including routers created from plain Ingresses. - A router without its own certificate uses the default one. Traefik takes it from a
TLSStorenameddefault. Without one, it serves a self-signed certificate it generates at startup.
So the only missing piece is that TLSStore.
Setting the default certificate
The certificate from the Let's Encrypt post is stored in the wildcard-lab-tls Secret in kube-system, where k3s runs Traefik. The TLSStore points at it:
apiVersion: traefik.io/v1alpha1
kind: TLSStore
metadata:
name: default
namespace: kube-system
spec:
defaultCertificate:
secretName: wildcard-lab-tlsTwo details are easy to miss:
- The name has to be
default. Traefik only uses the store with that name for the default certificate. - The Secret has to be in the same namespace as the
TLSStore. That's why the certificate is issued intokube-systemrather than cert-manager's own namespace.
In my repo this file sits in infra/traefik/manifests/, next to the HelmChartConfig that already customizes k3s's Traefik.
Checking the certificate
From a device on the tailnet:
openssl s_client -connect forgejo.lab.example.com:443 \
-servername forgejo.lab.example.com </dev/null 2>/dev/null \
| openssl x509 -noout -issuer -ext subjectAltNameThe issuer should be Let's Encrypt, and the names should include *.lab.example.com. This works before any app has moved, because Traefik serves the default certificate for every name it receives.
Redirecting HTTP to HTTPS
k3s customizes its packaged Traefik through a HelmChartConfig named traefik in kube-system. The redirect is one chart value:
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
name: traefik
namespace: kube-system
spec:
valuesContent: |-
nodeSelector:
node-role.kubernetes.io/control-plane: "true"
ports:
web:
http:
redirections:
entryPoint:
to: websecure
scheme: https
permanent: trueThe nodeSelector was already there. Traefik can read every Secret in the cluster, so it stays off the node where CI runs.
Turn the redirect on last, after every app has moved to its lab.example.com name. It applies to every request on port 80. An app still reached by an old LAN name would be sent to HTTPS with a certificate that doesn't match that name, and the browser would refuse it.
Taking Traefik off the LAN
Once nothing uses the old LAN names, Traefik doesn't need to listen on the nodes at all. k3s's built-in ServiceLB only exposes LoadBalancer Services, so one more value removes the listeners:
service:
spec:
type: ClusterIPThe Tailscale proxy and in-cluster clients still reach Traefik through its Service, so nothing else changes. Every device that uses the apps now needs Tailscale, even at home.
If Tailscale is ever down, kubectl port-forward to an app's Service still works, because the Kubernetes API is a separate listener.
Adding a new app
Once this is in place, a new app needs one Ingress:
spec:
ingressClassName: traefik
rules:
- host: <app>.lab.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: <app>
port:
number: 8080No tls: block, no new certificate and no DNS change. The tailnet grant from the proxy post already covers it.
Files
The configs from this post
The real files, with private values replaced by placeholders. Fill in your own before using them.