[ Ionut Dumitru ]
SystemsJul 28, 20257 min read

A reverse proxy is the front door you keep forgetting to lock

The reverse proxy is the most important security boundary in a homelab and the one people configure last.

The reverse proxy gets treated like plumbing. You stand up a stack — Jellyfin, Nextcloud, a Git server, some dashboard you found on a Saturday — and the proxy is the last thing you touch, a routing detail you bolt on once everything else works. So it inherits whatever defaults shipped in the example config. But the proxy is not plumbing. It is the one process that every external request passes through before it reaches anything you care about. It is the front door, and most homelabs leave it unlocked because locking it was never on the checklist.

The mistake is thinking of the proxy as a router when it is actually a gate. A router asks "where does this go." A gate asks "should this be allowed in at all." When you only configure the first question, you have published every service you own to the internet with the security posture of whichever app is the most carelessly written — and in a homelab, that is always some container you spun up to try for an evening and forgot to take down.

Default routing publishes your worst app

Here is the failure that bites people. You set up nginx or Caddy or Traefik with a wildcard rule: anything on *.home.example.com routes to the matching container. Convenient. It means a new service is reachable the moment you name it. It also means a service you never meant to expose — the admin panel with the hardcoded password, the dev build with auth disabled "just for now" — is reachable the instant it starts listening, with no further action from you.

The proxy did exactly what you told it. The problem is you told it to forward, not to guard. A gate that opens for everyone is a wall with a hole in it.

  • Route explicitly. One entry per service you actually intend to publish, never a wildcard that catches whatever appears.
  • Default to deny. Anything not named in the config returns a 404 from the proxy itself, before it ever touches an upstream.
  • Separate internal from external. Most of your services have no business being reachable from outside your network at all; the proxy is where you draw that line, not the firewall alone.

A gate that opens for everyone is a wall with a hole in it.

The proxy is the only honest place to put authentication

Every self-hosted app has its own idea of what a login is. Some have real session management. Some have a single shared password in an environment variable. Some have a "security" mode that is off by default and a settings page that warns you not to expose it. You cannot fix all of them, and you should not try. Per-app auth is a patchwork, and a patchwork has seams, and seams are where attackers live.

The proxy collapses that variance into one decision made in one place. Put an authentication layer — forward auth to an identity provider, mutual TLS, even basic auth for the genuinely low-stakes stuff — in front of everything, and the question "is this request allowed to reach an app at all" gets answered before any app's idiosyncratic login logic runs. The app's own auth becomes a second layer, not the only one.

This is the part people skip because it feels like overkill for a movie server. It is not overkill. It is the difference between one boundary you can reason about and a dozen boundaries you cannot.

Configure it like it's load-bearing, because it is

Treat the proxy config as the most security-sensitive file you own, because it is. A typo here does not crash a service — it quietly exposes one. The defaults are tuned for getting started, not for standing between the internet and your data.

A few rules that have saved me real trouble:

  • Terminate TLS at the proxy and refuse plain HTTP entirely. A redirect from HTTP to HTTPS still accepts the first request in the clear; prefer HSTS and a closed port 80 for anything sensitive.
  • Set timeouts and request size limits. The defaults are generous in ways that help an attacker hold connections open or push oversized bodies at an upstream that won't survive it.
  • Strip and set headers deliberately. Pass through the real client IP, add the security headers your apps forget, and drop anything an upstream might leak about its internals.
  • Log what gets denied, not just what gets served. The denials are the early warning. If nothing is ever blocked, your rules are probably too loose to be doing anything.

The shape of a deny-by-default check is simple enough that there's no excuse for not having it. Match the host, confirm it's on the list, and refuse everything else before a single byte reaches an upstream.

proxy.conf

server {
server_name _;            # catch-all
return 404;               # unmatched host stops here
}
server {
server_name jellyfin.home.example.com;
auth_request /forward-auth;
location / { proxy_pass http://jellyfin:8096; }
}

Deny by default: an unlisted host never reaches an upstream.

None of this is exotic. It is the boring, deliberate configuration of a thing you already run. The reason it gets skipped is that the proxy works without it — services resolve, pages load, everything looks done. Security is the one property that does not announce its own absence. The door swings open just as smoothly whether or not it locks.

So go look at your proxy config the way an attacker would: not "what does this route" but "what does this let in." The most important boundary in your setup is the one you configured while thinking about something else. Lock the front door first, then worry about the rooms.

#Systems#Networking#SecurityShare ↗
→ / AUTHOR
Ionut Dumitru
Ionut Dumitru

Full-stack engineer and product designer. Writes about building products where the engineering and the design are the same job.

→ / NEXT
AIJul 21, 2025
Fine-tuning is the answer to a question you probably don't have
← All writingionutdumitru.com