TLS in a Homelab with cert-manager
I wanted grafana.prd.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).
sequenceDiagram
participant CM as cert-manager
participant LE as Let's Encrypt
participant CF as Cloudflare
CM->>LE: I want a cert for *.prd.ruiz.sh
LE-->>CM: Prove it. Create a TXT record at _acme-challenge.prd.ruiz.sh
CM->>CF: create TXT record (API)
LE->>CF: check DNS, find the TXT record
LE-->>CM: here's your certificate
CM->>CM: save it as a Kubernetes Secret
CM->>CF: remove the TXT record
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:
DopplerSecret. Pulls the Cloudflare API token from Doppler into a Kubernetes Secret (cloudflare-api-token in the cert-manager namespace, key api-token). cert-manager needs this to create DNS records.
ClusterIssuer. Tells cert-manager how to get certificates: use Let's Encrypt production (letsencrypt-prod), prove ownership via DNS-01 on Cloudflare.
Certificate. The actual request. A wildcard certificate for *.prd.ruiz.sh and prd.ruiz.sh, stored as a Secret called wildcard-prd-ruiz-sh-tls in the istio-system namespace.
Gateway. The Istio Gateway listener on port 443 references the TLS Secret. It terminates HTTPS and forwards plain HTTP to backend services inside the cluster.
The result
flowchart TD
B["Browser (grafana.prd.ruiz.sh)"] -->|"DNS resolves to 192.168.*.*"| GW["Istio Gateway (port 443)"]
GW -->|"TLS termination with wildcard-prd-ruiz-sh-tls"| HR["HTTPRoute (grafana)"]
HR --> G["Grafana (port 80, plain HTTP inside the cluster)"]
Every service behind the Gateway gets HTTPS for free. grafana.prd.ruiz.sh, argo.prd.ruiz.sh, thanos.prd.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.