Get one wildcard certificate for your lab domain via a DNS-01 challenge, terminate it at the reverse proxy, and stop thinking about TLS. No port 80 open to the internet, no per-service certificates leaking your hostnames into public transparency logs, one renewal to monitor instead of fifteen. The whole setup is about twenty lines of Caddy config or one Traefik certresolver block, and it works for services that are never reachable from outside your LAN.
DNS-01 beats HTTP-01 at home
HTTP-01 requires Let's Encrypt to reach port 80 on the hostname being certified — which forces you to expose services just to encrypt them, and fails outright for internal-only hosts. DNS-01 proves domain control by writing a TXT record through your DNS provider's API, so the server requesting the certificate can sit behind NAT, a VPN, or an airgap-ish VLAN and still get publicly-trusted certs. It's also the only challenge type that can issue wildcards. The requirements: a real domain (a few dollars a year) and DNS hosted somewhere with a decent API — Cloudflare, deSEC, Hetzner, and Porkbun all work well. If your registrar's DNS has no API, delegate just the challenge with a CNAME from _acme-challenge.example.com to a provider that does (the acme-dns pattern). Challenge types are documented at letsencrypt.org.
Wildcards keep your hostnames out of CT logs
Every certificate a public CA issues is published to certificate transparency logs. Search your domain on crt.sh and you'll see every hostname you've ever requested a cert for — which means per-service certificates (vaultwarden.example.com, paperless.example.com) publish a neat inventory of your attack surface for anyone who looks. A wildcard for *.lab.example.com publishes exactly one entry and reveals nothing about what's behind it. This, more than convenience, is the argument for wildcard-plus-DNS-01 at home.
The whole thing in one Caddyfile
Caddy with the Cloudflare DNS plugin (use the builder image or xcaddy to include it):
*.lab.example.com {
tls {
dns cloudflare {env.CF_API_TOKEN}
}
@paperless host paperless.lab.example.com
handle @paperless {
reverse_proxy 192.168.1.20:8000
}
@jellyfin host jellyfin.lab.example.com
handle @jellyfin {
reverse_proxy 192.168.1.21:8096
}
}
Scope the API token to DNS-edit on that one zone and nothing else — this token can get certificates issued for your domain, so treat it like a password. Traefik does the same with a dnsChallenge certresolver and a wildcard in the router's TLS domains. The one prerequisite either way: your LAN must resolve *.lab.example.com to the proxy's internal address, which is split-horizon DNS — covered here.
Renewals fail silently now — monitor them
Let's Encrypt stopped sending expiry notification emails in June 2025, so nobody warns you anymore; the first sign of a broken renewal is an outage. Certificates last 90 days and every sane client renews around day 60, which means an expiring cert equals roughly 30 days of failed renewal attempts nobody noticed. Add one check in Gatus or Uptime Kuma against your proxy with a certificate-expiry condition — in Gatus, [CERTIFICATE_EXPIRATION] > 336h alerts when under 14 days remain, which still leaves two weeks to fix the API token that expired or the plugin that broke. This is a five-minute task that catches the single most common TLS failure.
Shorter lifetimes are coming: automate like it's already true
The CA/Browser Forum ratified a schedule in 2025 stepping the maximum certificate lifetime down in stages to 47 days by March 2029, and Let's Encrypt already offers a short-lived (roughly six-day) certificate profile. The practical test for your setup: could it renew weekly without you noticing? Caddy, Traefik, and certbot-in-a-timer all pass. A certificate you manually export and upload into a NAS admin panel every quarter does not — put that device behind the reverse proxy instead of giving it its own cert, and the problem disappears along with the quarterly chore.
Cert pinning myths, briefly
Things not worth doing at home: pinning your server's certificate fingerprint in client apps (it changes every 60 days now — you've built a scheduled outage), HPKP (removed from browsers back in 2018), and running a private CA purely for TLS (you'll spend weekends shipping the root cert to every phone, TV, and guest device, and one missed device gets scary warnings forever; private CAs earn their keep only when you need mTLS). The cheap thing that is worth doing: a CAA record, which tells every other certificate authority not to issue for your domain:
example.com. IN CAA 0 issue "letsencrypt.org"
One DNS record, near-zero cost, and mis-issuance through some other CA now fails closed.
What I'd do
Domain on Cloudflare or deSEC, wildcard *.lab.example.com via DNS-01 in Caddy or Traefik, split-horizon DNS pointing the wildcard at the proxy, a Gatus expiry check at 14 days, and a CAA record. Roughly an hour of setup, and the result is every internal service on HTTPS with a padlock, no exposed ports, no hostname inventory in CT logs, and no human in the renewal loop — which is the only kind of renewal loop that survives 47-day certificates.