Keep the edge boring
Your internet-facing edge should be the least clever part of the whole stack. Everything interesting belongs behind it.
The edge is the one part of my infrastructure I want to be able to forget about. Not ignore — forget. It should be the component I touch least, understand most completely, and never feel clever about. The cleverness lives behind it, where a mistake costs me a service rather than the whole front door.
This is a position I arrived at the slow way, by making the opposite choice first. For a while my reverse proxy was where I expressed myself: header rewrites for three different apps, a hand-rolled rate limiter, two TLS termination points because one of them needed mutual auth and I never untangled it, a sprinkling of map directives whose purpose I could no longer reconstruct. It worked, mostly. Then it didn't, on a Sunday, and I spent an afternoon discovering that I had built something only my past self understood — and he was unavailable for comment.
So here is the thesis, stated plainly: the edge is plumbing, not a platform. Treat it the way you treat the water main. You want it boring, standard, and replaceable, and you want to remember where the shutoff is.
Terminate TLS in exactly one place
The single most calming decision I made was to terminate TLS once, at the edge, and let everything behind it speak plain HTTP over a network I already trust. Not because end-to-end encryption is wrong — it isn't — but because every termination point is a place certificates can expire, ciphers can drift, and a 2 a.m. renewal can fail silently. One termination point is one thing to monitor, one cert store, one clock to watch.
Internally I let services talk over the bridge network or the LAN in the clear. An attacker who is already sniffing my Docker bridge has won a more important fight than the one TLS would settle. Spending operational complexity to defend that segment is paying real money to mitigate an imaginary threat, and the bill comes due exactly when you can least afford it.
Automate certificates, then stop thinking about them
A certificate you renew by hand is a certificate that will expire on a holiday. The only acceptable renewal strategy is one where the proxy fetches and rotates certs on its own, on a schedule it manages, with no cron job of mine in the loop and no calendar reminder I will eventually snooze.
This is the strongest argument for choosing your proxy on operational grounds rather than feature count. The question that matters is not can it do mTLS and gRPC and request mirroring — most can. The question is: when a cert is sixty days old, does it renew without me noticing? If the answer is yes, you have bought back a category of 3 a.m. pages for the price of reading one config format. That is the best trade in self-hosting.
An edge you have to think about is an edge that is already failing; it just hasn't told you when yet.
scrawled in the runbook, after the holiday in question
Keep the config small and declarative
The whole edge configuration should fit on one screen and read like a statement of intent, not a program. Declarative beats imperative here for a reason that has nothing to do with taste: a declarative config is something you can diff, review, and reason about by reading it. An imperative one — full of conditionals and rewrites and computed variables — is something you have to execute in your head to understand, and human beings are poor interpreters at the best of times, worse at 3 a.m.
Here is roughly the shape I aim for. One static site served from disk, one internal application reverse-proxied, and a gate so the internal app only answers to my private network. The syntax below is Caddy-flavoured, but the shape is the point, not the dialect.
# The public face: a static site. TLS is fetched and renewed
# automatically — there is no cert path here on purpose.
blog.example.com {
root * /srv/blog
file_server
encode zstd gzip
}
# An internal app, exposed over TLS but gated to the LAN.
# Anything off-net gets a flat 403 before the app is touched.
grafana.example.com {
@private remote_ip 192.168.10.0/24 10.20.0.0/16
handle @private {
reverse_proxy grafana:3000
}
respond "forbidden" 403
}
That is the entire edge for two services. There is no certificate management because the proxy owns it. There is no rate-limit logic because the static site doesn't need it and the internal app isn't reachable from the open internet to begin with. The IP gate does more for me than any web application firewall I have run, and I can explain every line to a colleague in under a minute. When this file grows a clause, that growth is a decision I have to defend, not a default I drifted into.
Gate internal services by source IP
Most of what I run has no business being reachable from the public internet. Dashboards, metrics, the things I poke at while debugging — these want to exist on the proxy for the convenience of a clean hostname and automatic TLS, but they want to answer only to my own networks. A source-IP allow rule is the bluntest possible instrument for this, which is exactly why I trust it.
The blunt instrument is the point. An IP gate has no auth flow to misconfigure, no session to fixate, no token to leak, no library to keep patched. It fails closed: if the rule doesn't match, the request dies with a flat 403 before the application code ever runs. Defence in depth still applies — the app keeps its own login — but the gate means a zero-day in that app is reachable only by someone already inside the perimeter, and that is a very different incident from one the whole internet can dial.
One honest caveat, because the edge is the wrong place for comfortable lies: trust the source IP only as far as you trust the path in front of it. If a CDN or another proxy sits ahead of yours, the apparent remote address is theirs, and you must read the real client from a forwarded header you have explicitly chosen to trust — never one a stranger can simply set. Get that boundary wrong and the gate is decorative. Get it right and it is the cheapest real security control you own.
Prefer the proxy that renews without you
I have run the venerable options and I will run them again. There is nothing wrong with a mature proxy and a separate ACME client wired together with care. But for a homelab, or any small fleet where I am the entire operations team, the calculus is different. The proxy that fetches and renews certificates on its own, from a config I can read in one sitting, removes an entire class of failure that the more powerful tools leave on my plate. I would rather give up a feature I use twice a year than keep a 3 a.m. page I get every spring.
It comes down to where you want your cleverness to live. Put it in the application, the data model, the backup strategy, the place where it earns its keep and where a failure is contained. Keep it out of the one component every request passes through, because that is the component whose failures are never contained — they are total, and they are public.
Rules for a boring edge
- One TLS termination point. Terminate at the edge; trust your internal network or fix your internal network — do not paper over it with more certificates.
- Certificates renew themselves. If a human is in the renewal loop, the renewal will eventually be missed. Remove the human.
- The config fits on one screen. If it doesn't, something interesting has crept into the boring layer. Evict it.
- Declarative over imperative. You should be able to understand the edge by reading it, not by running it in your head.
- Default to deny. Internal services answer to internal networks; everything else gets a flat refusal before the app runs.
- Trust the source IP only as far as the path in front of it. Read the real client from a header you chose to trust, or not at all.
- No clever feature you use twice a year is worth a recurring page. Push cleverness inward, where failures stay small.
None of this is sophisticated, and that is the entire argument. The edge is the part of the system where sophistication is a liability and dullness is a feature. Make it boring, write it down, and go spend your cleverness somewhere it can fail quietly.