TLS in a Homelab with cert-manager¶
I wanted grafana.ruiz.sh to load with a green padlock in the browser. No self-signed certificates, no browser warnings. Real TLS, issued by Let's Encrypt, on a homelab that isn't exposed to the internet.
cert-manager makes this possible.
The problem¶
To get a valid TLS certificate, you need to prove you own the domain. Let's Encrypt supports two methods:
| Method | How it works | Needs internet exposure? |
|---|---|---|
| HTTP-01 | Let's Encrypt hits http://yourdomain/.well-known/acme-challenge/token |
Yes, port 80 open |
| DNS-01 | Let's Encrypt checks a TXT record in your DNS | No, just API access to DNS provider |
Since the homelab isn't exposed to the internet, HTTP-01 is out. DNS-01 is the only option. It also happens to be the only method that supports wildcard certificates.
How it works¶
The flow involves three players: cert-manager (in the cluster), Let's Encrypt (on the internet), and Cloudflare (DNS for ruiz.sh).
cert-manager: "I want a cert for *.ruiz.sh"
│
▼
Let's Encrypt: "Prove you own ruiz.sh.
Create a TXT record at _acme-challenge.ruiz.sh"
│
▼
cert-manager ──> Cloudflare API ──> creates TXT record
│
▼
Let's Encrypt checks DNS, finds the TXT record
│
▼
Let's Encrypt: "Here's your certificate"
│
▼
cert-manager saves it as a Kubernetes Secret
cert-manager removes the TXT record from Cloudflare
The whole thing is automated. cert-manager also renews the certificate 30 days before it expires (Let's Encrypt certs last 90 days). No manual intervention needed.
The Kubernetes resources¶
Four pieces make this work:
ExternalSecret. Pulls the Cloudflare API token from Doppler into a Kubernetes Secret. cert-manager needs this to create DNS records.
ClusterIssuer. Tells cert-manager how to get certificates: use Let's Encrypt production, prove ownership via DNS-01 on Cloudflare.
Certificate. The actual request. A wildcard certificate for *.ruiz.sh and ruiz.sh, stored as a Secret called wildcard-ruiz-sh-tls in the envoy-gateway-system namespace.
Gateway. The Envoy Gateway listener on port 443 references the TLS Secret. It terminates HTTPS and forwards plain HTTP to backend services inside the cluster.
The result¶
Browser (grafana.ruiz.sh)
│
│ DNS resolves to 192.168.68.201
▼
Envoy Gateway (port 443)
│
│ TLS termination with wildcard-ruiz-sh-tls
▼
HTTPRoute (grafana)
│
▼
Grafana (port 80, plain HTTP inside the cluster)
Every service behind the Gateway gets HTTPS for free. grafana.ruiz.sh, argocd.ruiz.sh, thanos.ruiz.sh. One wildcard certificate covers them all.
DNS-01 is the right choice for homelabs. No ports to open, no exposure to the internet. The only requirement is API access to your DNS provider, and Cloudflare's free tier works fine.
The staging vs production distinction in Let's Encrypt matters. Staging has much higher rate limits but issues certificates that browsers don't trust. Always test with staging first to avoid hitting the production limit of 50 certificates per week per domain.
cert-manager is one of those "set it up once and forget about it" tools. Once the ClusterIssuer and Certificate are in place, renewals happen automatically. The Grafana dashboard I added shows certificate expiry and renewal timelines, so I can see at a glance if something is off.