proxmox-k8s · cert-manager + Let's Encrypt DNS-01 via DuckDNS
TLS terminates at the NGINX data plane pod. Traffic between NGINX and backend services stays plain HTTP inside the cluster network. The certificate is stored as a K8s Secret (homelab-tls) and auto-renewed by cert-manager before expiry.
The DNS-01 challenge proves domain ownership without opening any inbound ports — critical for a homelab behind NAT. The entire flow is outbound from your cluster.
The whole process takes 1–3 minutes. cert-manager auto-renews 30 days before expiry. No port 80 or 443 needs to be open inbound — the DuckDNS webhook calls the DuckDNS API outbound over HTTPS, and Let's Encrypt checks DNS records from its own infrastructure. The dns01-recursive-nameservers flag on cert-manager ensures it uses public DNS (8.8.8.8, 1.1.1.1) to verify propagation, not your local AdGuard resolver.
Proves domain ownership by setting a DNS TXT record. Works behind NAT, no open ports needed. Supports wildcard certs. The DuckDNS webhook automates the TXT record via the DuckDNS API.
Proves ownership by serving a file on port 80. Requires the Gateway to be publicly reachable from Let's Encrypt servers. Most residential ISPs block inbound port 80, and NAT makes this unreliable. No wildcard cert support.
Controller that watches for Certificate CRs and Gateway annotations. Creates ACME orders, delegates DNS-01 challenges to the webhook, and stores issued certs as K8s Secrets. Deployed via ArgoCD helm chart.
Kubernetes API extension server that handles DNS-01 challenges for *.duckdns.org domains. When cert-manager needs to prove domain ownership, it calls this webhook, which calls the DuckDNS API to set the _acme-challenge TXT record. Reads the DuckDNS token from a K8s Secret.
Named letsencrypt-prod. Configures the ACME server URL, solver type (DNS-01 via DuckDNS webhook), and private key storage. Referenced by the Gateway annotation cert-manager.io/cluster-issuer.
The homelab Gateway's HTTPS listener on port 443. References the homelab-tls Secret for the certificate. NGF provisions an NGINX data plane pod that terminates TLS using this cert and proxies to upstream services.
| File | Purpose | Component |
|---|---|---|
| gitops/apps/cert-manager.yaml | ArgoCD app — cert-manager helm chart | cert-manager |
| gitops/values/cert-manager-values.yaml | Helm values — CRDs, DNS01 recursive nameservers | cert-manager |
| gitops/apps/cert-manager-webhook-duckdns.yaml | ArgoCD app — DuckDNS webhook helm chart | webhook |
| gitops/values/cert-manager-webhook-duckdns-values.yaml | Helm values — group name, existing secret ref | webhook |
| gitops/manifests/letsencrypt-issuer.yaml | ClusterIssuer — ACME server + DuckDNS solver | issuer |
| gitops/manifests/gateway/gateway.yaml | Gateway — HTTPS listener + cert-manager annotation | gateway |
| secrets/duckdns-token.txt | DuckDNS API token (gitignored) | secret |
Secrets are never committed. Run make duckdns-token to create the K8s Secret from the local file. ArgoCD syncs everything else automatically when you push.
make duckdns-token — creates the DuckDNS API token as a K8s Secret in the cert-manager namespace.cert-manager namespace.kubectl get clusterissuer letsencrypt-prod — should show Ready: True.cert-manager.io/cluster-issuer annotation and creates a Certificate CR automatically.homelab-tls. NGF picks it up and the HTTPS listener goes live. Verify with kubectl get certificate -n default homelab-tls.