Skip to main content

Beyond Docker + ufw: Using iptables DOCKER-USER Chain for IP Whitelisting

Background

After deploying PostgreSQL with Docker on a server, I noticed frequent login attempts from unknown IPs in the logs. I wanted to use ufw to restrict access, but discovered that Docker's -p parameter directly manipulates iptables, bypassing ufw rules.

Solution Comparison

SolutionProsCons
Bind 127.0.0.1 + SSH tunnelMost secure, port not exposed to publicNeed to open tunnel for each connection
pg_hba.conf whitelistDatabase-level controlNeed to modify config and restart when IP changes
Disable Docker iptablesufw works normallyNeed to manually manage container networking
DOCKER-USER chainDoesn't affect existing deployment, flexibleRequires iptables knowledge
Tailscale/WireGuardZero-config VPN, stable IPAll nodes need installation

This article uses the DOCKER-USER chain approach, suitable for existing deployments with fixed server IPs.

Steps (Debian/Ubuntu)

1. Confirm DOCKER-USER Chain Exists

iptables -L DOCKER-USER -n -v

Normal output shows a RETURN rule, indicating Docker has created this chain.

tip

If you see iptables-legacy tables present warning, run iptables-legacy -L DOCKER-USER -n -v to check. If legacy doesn't have DOCKER-USER chain, Docker is using nftables backend - continue using iptables command.

2. Add Whitelist Rules

Assuming you need to allow these IPs to access PostgreSQL (port 5432):

  • k3s nodes: 203.0.113.10, 203.0.113.11
  • Other servers: 198.51.100.20, 198.51.100.21
  • Home network: 192.0.2.100
  • VPN exit: 192.0.2.200

Add ACCEPT rules one by one:

iptables -I DOCKER-USER -p tcp --dport 5432 -s 203.0.113.10 -j ACCEPT
iptables -I DOCKER-USER -p tcp --dport 5432 -s 203.0.113.11 -j ACCEPT
iptables -I DOCKER-USER -p tcp --dport 5432 -s 198.51.100.20 -j ACCEPT
iptables -I DOCKER-USER -p tcp --dport 5432 -s 198.51.100.21 -j ACCEPT
iptables -I DOCKER-USER -p tcp --dport 5432 -s 192.0.2.100 -j ACCEPT
iptables -I DOCKER-USER -p tcp --dport 5432 -s 192.0.2.200 -j ACCEPT

3. Add DROP Rule

iptables -A DOCKER-USER -p tcp --dport 5432 -j DROP
Note the Order
  • ACCEPT rules use -I (insert at beginning)
  • DROP rule uses -A (append to end)

This ensures ACCEPT comes before DROP.

4. Verify Rule Order

iptables -L DOCKER-USER -n -v --line-numbers

Correct output should be:

num   pkts bytes target     prot opt in     out     source               destination
1 0 0 ACCEPT tcp -- * * 192.0.2.200 0.0.0.0/0 tcp dpt:5432
2 0 0 ACCEPT tcp -- * * 192.0.2.100 0.0.0.0/0 tcp dpt:5432
3 0 0 ACCEPT tcp -- * * 198.51.100.21 0.0.0.0/0 tcp dpt:5432
4 0 0 ACCEPT tcp -- * * 198.51.100.20 0.0.0.0/0 tcp dpt:5432
5 0 0 ACCEPT tcp -- * * 203.0.113.11 0.0.0.0/0 tcp dpt:5432
6 0 0 ACCEPT tcp -- * * 203.0.113.10 0.0.0.0/0 tcp dpt:5432
7 0 0 DROP tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:5432
8 xxx xxxxx RETURN all -- * * 0.0.0.0/0 0.0.0.0/0

Key point: DROP must be before RETURN, otherwise it won't take effect.

If DROP is after RETURN, adjust:

# Delete incorrectly positioned DROP
iptables -D DOCKER-USER -p tcp --dport 5432 -j DROP

# Insert before RETURN (assuming RETURN is at position 7)
iptables -I DOCKER-USER 7 -p tcp --dport 5432 -j DROP

5. Persist Rules

apt install iptables-persistent -y

During installation, you'll be prompted to save current rules - select Yes.

Manual save:

netfilter-persistent save

Rules are saved in /etc/iptables/rules.v4.

6. Verify Persistence

grep -A 10 "DOCKER-USER" /etc/iptables/rules.v4

You should see your added rules.

Daily Maintenance

Add New IP

# Insert before DROP rule (check DROP's line number and subtract 1)
iptables -I DOCKER-USER 7 -p tcp --dport 5432 -s NEW_IP -j ACCEPT
netfilter-persistent save

Remove an IP

# First check line numbers
iptables -L DOCKER-USER -n --line-numbers

# Delete specific line
iptables -D DOCKER-USER LINE_NUMBER
netfilter-persistent save

View Blocked Connections

iptables -L DOCKER-USER -n -v

The pkts and bytes columns of the DROP rule show blocked packet counts.

What About Dynamic IPs?

If your home network IP changes frequently, use SSH tunnel:

# Run locally
ssh -L 5432:127.0.0.1:5432 user@SERVER_IP

# Then connect client to local 127.0.0.1:5432

Or configure in ~/.ssh/config:

Host mydb
HostName SERVER_IP
User YOUR_USERNAME
LocalForward 5432 127.0.0.1:5432

Then ssh mydb automatically establishes the tunnel.

Other Ports

The same method works for other Docker-exposed ports - just modify the --dport parameter:

# Example: restrict Redis port 6379
iptables -I DOCKER-USER -p tcp --dport 6379 -s ALLOWED_IP -j ACCEPT
iptables -A DOCKER-USER -p tcp --dport 6379 -j DROP