HTTPS for services the internet can't reach
Let's Encrypt's usual challenge asks you to be publicly reachable. A home lab isn't. DNS-01 and a wildcard certificate solve it in one line of Caddy — plus the gotcha that breaks renewal without telling you.
Forty-six subdomains in the house, and not one of them is reachable from the internet. My connection sits behind CGNAT, so even if I wanted to open a port I couldn’t. Browsers still want HTTPS, and the usual answer — a self-signed certificate and a warning to click through on every device, forever — isn’t an answer.
HTTP-01 needs a door
The standard route to a Let’s Encrypt certificate is HTTP-01: the CA requests http://monitor.example.com/.well-known/acme-challenge/… and you serve back the token. It proves you control the name because you answered on it. It also means the name has to be publicly reachable on port 80. Mine aren’t, and can’t be. That’s the whole blocker.
DNS-01 proves the domain instead
DNS-01 writes a TXT record under _acme-challenge and lets the CA read it. Nothing has to be reachable — the proof lives in the zone, not on the host. And because it proves the domain rather than one hostname, you can ask for *.example.com: a single certificate covering every subdomain, including the ones that don’t exist yet.
In Caddy that’s one line in the global block:
{
acme_dns infomaniak {env.INFOMANIAK_API_TOKEN}
}
A new service is a new subdomain, and the certificate already covers it. I haven’t thought about a certificate in months.
One snippet, forty-six services
With TLS handled once, routing is just repetition — so it should cost four lines per service:
(proxy) {
reverse_proxy {args[0]} {
header_up X-Real-IP {client_ip}
}
}
@grafana host monitor.example.com
handle @grafana {
import proxy 10.0.0.5:3000
}
Adding a service is a copy-paste and a reload.
The gotcha that breaks renewal quietly
My own resolver is authoritative for example.com on the LAN — that’s rather the point of running DNS at home. So when Caddy checks whether the TXT record has propagated, it asks that resolver, and my internal zone answers “no such record”, authoritatively. The check never clears.
One line fixes it:
tls_resolvers 1.1.1.1 9.9.9.9
The propagation check now goes to public resolvers and the internal zone stops shadowing it. Worth knowing that the failure is silent: the certificate you already hold is good for 90 days, so nothing breaks on the day you misconfigure it. You find out weeks later, when the renewal that was supposed to happen didn’t — unless something is watching the clock for you. Uptime Kuma sends a certificate-expiry notification for any HTTPS monitor without extra setup, which is enough to turn a silent failure into a warning with days of runway left.
What it costs
The Infomaniak DNS plugin isn’t in the stock Caddy build, so mine is a custom xcaddy build with the package held back so apt can’t quietly swap it for the official one. Ten minutes at install, then a rebuild at every upgrade — small, but it’s a thing you have to remember exists. Whichever provider you’re on, check whether it needs a custom build before you commit to it.
The takeaway
HTTP-01 asks you to be reachable. DNS-01 only asks you to own the domain — which is exactly why it fits a home lab, where being unreachable is the design rather than a limitation. Pair it with a wildcard and certificates stop being a per-service chore entirely. Just point the propagation check somewhere outside the house, or your own resolver will cheerfully lie to you about your own domain 🔒.