Ramblings of an aging IT geek
← Ramblings of an aging IT geek
homelab

tls on the home network without the faff

How I run a single reverse proxy at home that fronts every internal service with real Let's Encrypt certificates, no port 80 exposed.

A server rack with cabling

For years my homelab ran on self-signed certificates and a browser full of red padlocks. Every new service meant another exception to click through, and worse, it trained me to ignore the one warning I should never ignore. The fix was not to expose anything to the internet. It was to put one reverse proxy in front of everything and let it do the certificate dance over DNS.

The key realisation: you do not need inbound port 80 to get a Let's Encrypt certificate. The DNS-01 challenge proves you control a domain by writing a TXT record, not by answering an HTTP request. That means I can have a real, trusted certificate for jellyfin.internal.i0.pm even though that name only ever resolves on my LAN and is never reachable from outside.

The shape of it

Everything internal sits behind a single Caddy instance. Caddy talks to my DNS provider's API to solve the challenge, fetches the certificate, and renews it without me thinking about it. My internal split-horizon DNS points *.internal.i0.pm at the proxy's address, so a browser asks for the service, hits Caddy, and Caddy routes it on to the real backend.

A homelab setup with networking gear

A Caddyfile for one service is almost embarrassingly short:

jellyfin.internal.i0.pm {
    tls {
        dns cloudflare {env.CF_API_TOKEN}
    }
    reverse_proxy 10.0.30.12:8096
}

That is the whole thing. Trusted certificate, automatic renewal, clean routing. The CF_API_TOKEN is scoped to edit exactly one zone and nothing else, which matters because a leaked token that can rewrite all your DNS is a very bad afternoon.

What this bought me

The obvious win is the green padlock, but the real win is that adding a service is now three lines and a reload. No openssl incantations I have to look up every time, no copying a self-signed CA into every device on the network, no teaching the family laptop to trust my homemade certificate authority.

There are sharp edges. Split-horizon DNS is one more thing that can break, and when it breaks the failure mode is confusing because the name resolves to something, just the wrong something. I keep the proxy config and the DNS records in the same git repo so they move together, which has saved me from at least one self-inflicted outage where the two drifted apart.

The other thing worth doing early is wildcard certificates. Rather than fetch a separate certificate per hostname, I ask for one *.internal.i0.pm and let Caddy reuse it across every backend. That keeps the number of certificate requests down, which matters because Let's Encrypt rate-limits issuance and you will hit those limits faster than you expect the first time you script something into a renewal loop by accident. A wildcard also means a brand new service inherits a valid certificate the moment I add its route, with no waiting on a fresh challenge.

One habit I would press on anyone copying this: monitor the renewal, not just the certificate. The certificate being valid today tells you nothing about whether the automated renewal will succeed in sixty days when the DNS token has quietly expired. I have a tiny check that alerts if any certificate drops below thirty days of validity, so a broken renewal becomes an email rather than a browser full of red padlocks on a Sunday morning.

It is not glamorous infrastructure. It is the kind of thing you set up once and then forget exists, which is exactly the point. The best homelab plumbing is the plumbing you stop noticing.