I'm always poking around my servers trying to understand how the moving parts interact. Recently, while trying to audit the security of my main server—which runs a mix of Odoo instances, n8n, and Ollama—I stumbled onto a surprising realization.
I had carefully configured ufw to only allow ports 80 and 443. I thought my internal services were locked down.
I was wrong.
The Docker Bypass Discovery
While running some network scans with an AI agent to map out the topology, I noticed something alarming. Even though ufw said my backend ports (like 18069 for Odoo) were blocked, they were still accessible from the outside.
It turns out that because my docker-compose files used explicit port bindings like 0.0.0.0:18069->8069, Docker was silently injecting its own routing rules directly into iptables. It completely bypassed UFW.
The Architectural Vulnerability: Docker manipulates iptables directly to manage container networking. If you rely solely on UFW (which is just a frontend for iptables) and use 0.0.0.0 bindings, Docker's rules take precedence, exposing your containers globally.
Experimenting with iptables
I'm no networking guru, but I knew I had to figure out how to seal this gap. Tearing down the live docker-compose files would break my routing, so I started researching how Docker interacts with iptables.
Through a lot of trial and error, and with some help from the agent, I learned about the DOCKER-USER chain. This chain is evaluated before Docker's own forwarding rules, making it the perfect place to inject a kill switch for external traffic.
Instead of just theorizing, here is the exact execution command I implemented to drop all external traffic attempting to hit the containers, while allowing traffic from my Caddy proxy:
# Drop all incoming traffic to the DOCKER-USER chain coming from the external interface (eth0)
iptables -I DOCKER-USER -i eth0 -j DROP
# Allow established connections (return traffic)
iptables -I DOCKER-USER -i eth0 -m state --state ESTABLISHED,RELATED -j ACCEPT
When the rules finally clicked into place, the containers vanished from the port scanners, but the reverse proxy kept routing traffic perfectly.
I'm still exploring the nuances of Linux networking, but finding and fixing this vulnerability taught me a crucial lesson: never assume a tool works the way you think it does until you test it yourself.
💡 Tips for Testing Docker Firewalls
If you are running Docker on a public server, you should test this yourself:
- Run a Port Scan: Use an external machine (or an online port scanner tool) to scan your server's IP address. See if your "blocked" Docker ports are responding.
- Use the DOCKER-USER Chain: Never manually edit the raw
DOCKERiptables chain, as Docker will overwrite it on restart. Always inject your custom rules into theDOCKER-USERchain. - Bind to Localhost: As a best practice, if you are using a reverse proxy, bind your docker ports strictly to localhost in your compose file (e.g.,
127.0.0.1:8069:8069) instead of0.0.0.0. This prevents external routing natively.
Trust, but Verify
A Systems Architect doesn't just read the documentation; they scan their own ports to prove it.
🔗 Related Resources
- Internal Link: Wondering how I give my AI agents the context they need to understand my iptables rules? Check out my post on Building a Server Orchestration Brain.
- External Link: The official Docker documentation on Docker and iptables is required reading if you run containers in production.
Discovering the Invisible Vulnerability: Docker and UFW