Here’s an inefficiency the lab lived with for months: standing at
site A, opening vault.example.dev, my request left the building,
traveled to the VPS, and came back through the overlay to a server
ten feet away. Round trip, city-scale. Useful distance, three meters.
Worse than the latency is the dependency. That hairpin means local services need the internet working. The day the site A connection wobbled, I couldn’t reach things running in the same building, which offends me on a level I can’t fully articulate. Post 2 promised this itch would get scratched with smarter DNS rather than riskier records. Here’s the smarter DNS.
The trick: different answers for different askers
Split-horizon DNS: the same name resolves differently depending on where you ask from. Outside, public DNS answers with url, as always. At site A, the LAN’s resolver lies, helpfully, and hands back hermes’s LAN address.
My LAN resolver is AdGuard Home on the firewall (it was already there eating ads). The feature you want is DNS rewrites, one per public name worth short-pathing:
vault.example.dev -> 192.168.x.10 # hermes's LAN address
auth.example.dev -> 192.168.x.10
home.example.dev -> 192.168.x.10
requests.example.tv -> 192.168.x.10
Devices on site A wifi get the short answer automatically. The same devices on cellular get the public answer. Nobody’s bookmarks know the difference, which is the entire point.
Step 1: the internal proxy holds up its end
Hermes’s Traefik is about to receive browsers asking for PUBLIC hostnames. Two things must be true first.
It needs the public wildcard cert too. It’s been minting
*.hermes.zt.example.dev (post 6); browsers will now ask it for
vault.example.dev. DNS-01 doesn’t care that the host is private.
Add a second domain to the entrypoint:
entryPoints:
websecure:
http:
tls:
domains:
- main: "*.hermes.zt.example.dev"
- main: "*.example.dev" # split-horizon names
It needs routers for those names, with the SAME auth posture as the edge. One “split” router per service, mirroring the public route:
labels:
- traefik.http.routers.vault-split.rule=Host(`vault.${PRIMARY_DOMAIN}`)
- traefik.http.routers.vault-split.entrypoints=websecure
- traefik.http.routers.vault-split.tls=true
# if the edge protects it with SSO, so does this router:
- traefik.http.routers.vault-split.middlewares=sso@file
That last line is a security rule, not a style note. A name behind forward-auth at the edge but naked on the internal path means being on the wifi is an auth bypass. Guests are on the wifi. IoT devices of dubious provenance are on the wifi. Mirror the middleware list, every time.
Check it worked, from a site A device:
dig +short vault.example.dev # should be 192.168.x.10 on the LAN
curl -sI https://vault.example.dev # 200/302 with a valid cert
Steady state, the tax is small: a new public service costs one rewrite plus one split-router label on top of what posts 6 and 7 already required.
Why not just use internal names at home?
The tempting alternative: skip all this and type
vault.hermes.zt.example.dev when you’re at site A. It fails on
sessions and muscle memory. Post 8’s cookies are scoped to the
registrable domain, so two names for one service means two sessions,
OAuth redirect URIs that match only one of them, a password manager
that fills on one and shrugs at the other, and a phone app configured
against whichever URL you typed at setup. One name everywhere, with
DNS choosing the path, keeps every client config true in both worlds.
The name is the interface. Don’t fork the interface.
The quirk the shortcut exposed
One app’s login broke only at site A, only sometimes, with a TLS error mid-OAuth-dance. I’ll spare you the wrong theories I committed to first (the cert, then AdGuard, then “the phone is being weird”) and give you the real one: during the redirect chain, one hop sent an SNI that didn’t match its Host header. The edge had been absorbing this quietly, because its routing for that name happened to match anyway. The internal proxy’s stricter Host-based router missed, and the request fell into the 404 void. Only on the internal path. Only at home. Sometimes.
The patch, once diagnosed, is a high-priority fallback router that catches the auth path BY PATH when the Host header is unhelpful (priorities explicit, because post 6’s footgun 2 never stops being true):
- traefik.http.routers.sso-fallback.rule=PathPrefix(`/application/o/`)
- traefik.http.routers.sso-fallback.priority=2000
- traefik.http.routers.sso-fallback.service=sso-core
The transferable lesson outranks the patch: split-horizon doesn’t create bugs, it reveals assumptions the single-path setup was silently absorbing. When something works remotely and fails locally, or vice versa, stop debugging the app and start diffing the paths. The app is fine. The paths differ. They always differed; now it matters.
Next, and last on the main line: the boss fight. Self-hosted email that actually delivers.