The thing that finally pushed me to sort out certificates properly was the browser warning. Every internal service in the homelab, the dashboard, the media box, the various admin panels, greeted me with a full-page "your connection is not private" interstitial, and I'd trained myself to click through it without reading. That's a terrible habit to build, because the entire point of that warning is to be alarming, and I'd sanded all the alarm off it.
So: real, trusted certificates on internal services that are never exposed to the internet. The trick is that you don't need to expose anything. You need DNS-01.
The usual Let's Encrypt flow is HTTP-01: the CA hits your domain over port 80 to prove you control it, which means your service has to be reachable from the internet. No thanks. DNS-01 proves control a different way: you put a specific TXT record in your domain's DNS, the CA checks the record, and you've proven you own the domain without exposing a single service. The validation happens entirely in DNS, so the box being certified can sit on a private network and never see a packet from outside.
I run everything through one reverse proxy, Caddy in my case, which terminates TLS at the edge of the LAN and forwards to each backend over the internal network. That gives me one place to hold certificates instead of wrangling them on every individual service, most of which have their own fiddly and incompatible ideas about TLS. The proxy talks ACME with DNS-01, my DNS provider has an API, and Caddy handles the TXT record dance automatically:
*.lan.i0.pm {
tls {
dns cloudflare {env.CF_API_TOKEN}
}
@dashboard host dashboard.lan.i0.pm
handle @dashboard {
reverse_proxy 10.0.10.20:3000
}
}
A wildcard certificate on the internal subdomain means I provision one cert and every service under it is covered. New service, new internal hostname, no new certificate ceremony. The proxy renews automatically, well ahead of expiry, and I genuinely don't think about it anymore, which is the correct amount of thinking to do about certificate renewal.
The one piece of homework is the DNS. The internal names need to resolve, which my own resolver handles with local records pointing the lan.i0.pm names at the proxy. So internally, dashboard.lan.i0.pm resolves to the proxy on the LAN, the proxy presents a certificate that's genuinely trusted because it's a genuine Let's Encrypt certificate for that name, and the browser is satisfied. Nothing is exposed, everything is encrypted, and the certificate is real rather than a self-signed thing I've taught my browser to ignore.
The payoff is small and enormous at once. Small, because it's just green padlocks on internal admin panels. Enormous, because I've stopped reflexively clicking through security warnings, which was a habit quietly waiting to bite me the day a warning actually meant something. Trusted certs on private services, no exposure, no manual renewal. That's the whole win.