~/notes / reverse-proxy-split-dns
Internal HTTPS with split DNS and a reverse proxy
Every service in my homelab has its own https://<service>.doof.quest address with a valid certificate, and none of those names exist on the public internet. Pi-hole points them at Nginx Proxy Manager, which holds one wildcard certificate and routes by hostname.
$ cat goal
- Open every self-hosted service at
https://<service>.doof.questwith a valid certificate, instead of an IP and a port number. - Keep it all internal. No inbound ports, and nothing in public DNS that points at my house.
- Use the same URLs at home and away.
$ cat design
The certificate. Nginx Proxy Manager requests one wildcard certificate for *.doof.quest from Let's Encrypt using the DNS-01 challenge. Let's Encrypt never has to reach my network. It checks a TXT record that NPM creates through the Cloudflare API, which works fine behind CGNAT.
The names. Pi-hole has one local DNS record per service, each pointing at the proxy. Nothing about these names exists in public DNS.
The routing. NPM has a proxy host for each name that forwards to the service's real address and port, all using the same wildcard certificate.
Away from home. Tailscale hands out Pi-hole as the DNS server, so the same names resolve the same way on mobile data. The remote access note covers that side.
$ cat decisions
- DNS-01, not HTTP-01. HTTP-01 needs port 80 open to the internet. DNS-01 needs nothing open, and it's the only way to get a wildcard certificate.
- A scoped API token. The token NPM uses can edit DNS on this one zone and read it, and that's all. If it leaks, it can't touch the rest of the account.
- One record per service. I started with a single dnsmasq wildcard. It was less typing, but it also caught names I didn't mean it to, so I replaced it with exact records.
$ cat gotchas
- Pi-hole behind the proxy returned 502. I pointed NPM at Pi-hole's HTTPS port, and Pi-hole's own certificate is self-signed, so the proxy refused the connection. Forwarding to plain HTTP on port 80 fixed it. The proxy still handles TLS for the browser.
- The Proxmox console needs WebSockets. A record that sent the Proxmox name straight to the host, skipping the proxy, broke every console. The proxy host needs "Websockets Support" turned on. Full story in the console postmortem.
- Wildcards catch more than you think. My old dnsmasq wildcard matched the bare domain too, which bit me the day this site went live. See the launch-day postmortem.
- A fallback DNS server defeats split DNS. When the router also advertised a public resolver, macOS could skip Pi-hole entirely. The router now hands out Pi-hole only.
$ cat verify
dig +short grafana.doof.quest # the proxy's address
dig +short grafana.doof.quest @1.1.1.1 # nothing
curl -sI https://grafana.doof.quest | head -1
$ cat lessons
- Internal HTTPS doesn't need open ports. DNS-01 gets a real certificate for names the internet can't even resolve.
- Exact records are easier to reason about. When something resolves wrong, the Pi-hole UI shows every record that could be responsible.