Own Your Stack · Part 2
The internal zone that was public

In part 1 I promised it: the story of the security hole we built for ourselves. Here it is — complete, including the part that hurts.
The plan was good
My setup’s architecture separates public from private cleanly: a few services must be reachable from the open internet (the password manager, the login). Everything else — admin interfaces, the user directory, the internal dashboard — should only be reachable through the WireGuard VPN.
For that there was an “internal zone”: a wildcard DNS entry points to the server’s private VPN address — a dead end from outside. And as a second line of defense, the reverse proxy checks the source IP of every request: anything not from the VPN address range gets rejected.
Belt and suspenders. What could go wrong?
The test that flipped everything
Our process includes a rule that earned its keep that day: every block gets tested from the outside, against ourselves. Not “check that it works from inside” — actively try to break through your own wall, from a machine outside the VPN, against the server’s public IP.
The result: the internal zone answered. Publicly. To anyone.
The cause: Docker means well
The IP filter in the proxy was configured correctly — and still useless. The reason is behaviour Docker documents officially, but you have to know it: when a container runs behind Docker’s network bridge with published ports, Docker replaces the request’s source address with its internal bridge address (source NAT).
So the proxy never saw the real source IP. For every request — from the VPN or from the open internet — it saw the same internal Docker address. And that address was on the allow list. The filter didn’t fail; it simply checked a signal that no longer existed at that point in the chain.
The fix
Two changes, both small, both fundamental:
- The proxy now runs on the host network instead of behind the bridge — it sees real source addresses again.
- The backends publish their ports locally only (bound to the loopback address) — they are no longer reachable from the network at all, only through the proxy.
Then: the same attack test from outside, this time with the expected result — connection refused. Only then was the whole thing marked done in the decision log, with an addendum documenting the hole. That, too, is part of honest infrastructure: the mistake lives in the same document as the decision.
The actual lesson
The technical lesson — “IP filters behind Docker NAT are worthless” — you can google. The process lesson is worth more:
A security measure that has never been tested from the outside is not a security measure. It is an assumption.
An AI agent builds an internal zone like this in minutes. Which is exactly why the counterweights matter: documented assumptions, and a test that attacks the assumption instead of confirming it. Speed is only an advantage when the review loop keeps up — that holds for my home setup exactly as it holds for every company currently rebuilding its infrastructure “quickly, with AI”.
The next part gets more fundamental: why my notebook runs Arch Linux — and not macOS or Windows.
Questions, objections, war stories of your own? Write me — I answer personally.