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.