You set up a Linux server, configure UFW (Uncomplicated Firewall) to allow only SSH on port 22 and HTTP/HTTPS on ports 80 and 443, and verify that all other ports are blocked. Then you deploy a Docker container that publishes port 5432 for PostgreSQL. You assume the firewall will block external access to port 5432 — but it does not. Your database is accessible to the entire internet.
This is one of the most dangerous and least understood security issues in Docker deployments. Docker modifies iptables rules directly, creating DOCKER chains that take priority over UFW's rules. Your firewall is technically still running, but Docker's iptables rules are evaluated before UFW's rules, effectively making your firewall invisible for any port published by Docker.
Why This Happens: iptables Chain Priority
Linux uses iptables (or its successor nftables) for packet filtering. Iptables has several built-in chains that packets traverse in a specific order: PREROUTING, INPUT, FORWARD, OUTPUT, and POSTROUTING. UFW adds rules to the INPUT chain, which handles traffic destined for the host machine.
Docker, however, routes container traffic through the FORWARD chain, not the INPUT chain. When you publish a port with -p 5432:5432, Docker creates NAT rules in the PREROUTING chain that redirect incoming traffic on port 5432 to the container, and FORWARD rules that allow this traffic. The packets never reach the INPUT chain where UFW's rules live.
# See Docker's iptables rules
sudo iptables -L -n -v
sudo iptables -t nat -L -n -v
# Notice the DOCKER chain and DOCKER-USER chain
sudo iptables -L DOCKER -n -v
sudo iptables -L DOCKER-USER -n -v
This is by design — Docker needs to manage its own networking. But the result is that any security administrator who relies solely on UFW for access control has a massive blind spot.
How Bad Is This Problem?
To demonstrate the severity, here is what happens on a typical server:
# Configure UFW to deny everything except SSH and HTTP
sudo ufw default deny incoming
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status # Shows only 22, 80, 443 allowed
# Start a container with a published port
docker run -d -p 5432:5432 postgres:16
# From another machine, try to connect
nmap -p 5432 your-server-ip
# Result: Port 5432 is OPEN, despite UFW denying it
Now imagine this with Redis on port 6379 (default: no password), MongoDB on port 27017 (default: no authentication), or an admin panel on port 8080. This is not a theoretical risk — Shodan searches reveal millions of Docker-exposed databases and services on the internet.
Solution 1: Never Publish Ports to 0.0.0.0
The simplest fix is to never publish container ports to all interfaces. Instead, bind to localhost (127.0.0.1) so the port is only accessible from the host machine:
# BAD: Accessible from anywhere (bypasses UFW)
ports:
- "5432:5432"
# GOOD: Only accessible from localhost
ports:
- "127.0.0.1:5432:5432"
For services that need to be accessed externally (like a web application), use Nginx as a reverse proxy. Nginx listens on ports 80/443 (allowed by UFW), and the application container only publishes to localhost:
# docker-compose.yml
services:
web:
image: myapp:latest
ports:
- "127.0.0.1:3000:3000" # Only accessible via Nginx, not directly
db:
image: postgres:16
ports:
- "127.0.0.1:5432:5432" # Only accessible from the host
This approach is the most reliable because it does not depend on iptables rules — the port is simply not listening on external interfaces.
Solution 2: Use Docker Networks Instead of Published Ports
An even better approach for inter-container communication is to not publish ports at all. Use Docker networks so containers can communicate with each other by service name without exposing any ports to the host:
services:
web:
image: myapp:latest
ports:
- "127.0.0.1:3000:3000" # Only this service needs a published port
networks:
- app-network
db:
image: postgres:16
# NO ports published — only accessible from other containers on the same network
networks:
- app-network
redis:
image: redis:7-alpine
# NO ports published
networks:
- app-network
networks:
app-network:
driver: bridge
In this configuration, the web container can connect to PostgreSQL at db:5432 and Redis at redis:6379 using the service names as hostnames. But neither PostgreSQL nor Redis is accessible from outside the Docker network — not even from the host machine.
Solution 3: The DOCKER-USER Chain
Docker provides the DOCKER-USER iptables chain specifically for user-defined firewall rules. Rules in DOCKER-USER are evaluated before Docker's own rules, giving you a way to filter traffic to containers:
# Allow established connections (important!)
sudo iptables -I DOCKER-USER -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN
# Allow traffic from the internal Docker network
sudo iptables -I DOCKER-USER -s 172.16.0.0/12 -j RETURN
# Allow traffic from your trusted IP
sudo iptables -I DOCKER-USER -s YOUR_OFFICE_IP -j RETURN
# Allow traffic to web ports (80, 443)
sudo iptables -I DOCKER-USER -p tcp --dport 80 -j RETURN
sudo iptables -I DOCKER-USER -p tcp --dport 443 -j RETURN
# Drop everything else to containers
sudo iptables -A DOCKER-USER -j DROP
The DOCKER-USER rules persist across Docker restarts (Docker recreates its own chains but does not touch DOCKER-USER). However, they do not persist across system reboots unless you save them.
Save the rules so they survive reboots:
# Install iptables-persistent
sudo apt install iptables-persistent -y
# Save current rules
sudo netfilter-persistent save
Solution 4: Disable Docker's iptables Management
You can tell Docker to stop managing iptables entirely by adding "iptables": false to /etc/docker/daemon.json:
{
"iptables": false
}
After restarting Docker, it will no longer create any iptables rules. This means UFW will work as expected, but you must manually configure networking for containers. Container-to-container communication, port publishing, and internet access from containers will need manual iptables rules. This approach is only recommended for experienced administrators who are comfortable managing iptables directly.
Solution 5: Use the ufw-docker Utility
The ufw-docker project provides a wrapper that makes UFW and Docker work together correctly. It modifies UFW's after.rules to properly filter Docker traffic:
# Install ufw-docker
sudo wget -O /usr/local/bin/ufw-docker https://github.com/chaifeng/ufw-docker/raw/master/ufw-docker
sudo chmod +x /usr/local/bin/ufw-docker
# Install the UFW integration
sudo ufw-docker install
# Restart UFW
sudo systemctl restart ufw
# Now manage Docker container access through ufw-docker
sudo ufw-docker allow mycontainer 80/tcp
sudo ufw-docker allow mycontainer 443/tcp
Verification
After implementing any of these solutions, verify from an external machine that only the intended ports are accessible:
# From another machine or your local computer
nmap -p 1-65535 your-server-ip
# Or use an online port scanner
# https://www.yougetsignal.com/tools/open-ports/
Only ports 22, 80, and 443 should be open. If you see other ports open (especially database ports like 3306, 5432, 27017, or 6379), your firewall is being bypassed and immediate action is required.
ZeonEdge provides server security hardening, firewall configuration, and Docker security audits. Learn more about our security services.
Sarah Chen
Senior Cybersecurity Engineer with 12+ years of experience in penetration testing and security architecture.