
docker silently punches holes through ufw
ufw-dockerthe ufw rule you trusted is lying to you
you set up ufw, you locked down every port except ssh and maybe 443, you feel good about it. then you run a docker container with -p 8080:8080 and suddenly that port is wide open to the internet, ufw rules or no ufw rules. this isn't a bug you introduced. it's just how docker works, and almost nobody finds out until a scan or a bad day tells them.
why docker skips ufw entirely
ufw is a friendly wrapper around iptables. it manages one set of chains and assumes it's the only thing writing rules. docker doesn't play by that assumption. when you publish a port, docker inserts its own rules directly into the nat table and into the DOCKER and FORWARD chains, and it does this at a priority that lands above ufw's rules in the chain order.
the result: traffic hitting a published container port gets forwarded by docker's rules before ufw ever gets a say. ufw shows the port as denied in its config, but the traffic doesn't care what ufw's config says, because ufw's deny rule never gets evaluated for that traffic. you can have "deny incoming" as your default policy and still be fully exposed on every port you've published with docker.
check it yourself:
sudo iptables -L DOCKER -n -v
sudo iptables -L FORWARD -n -v
if you see accept rules tied to your container's ports sitting there, that's docker doing its own thing, completely separate from ufw's tables.
fix one: the ufw-docker helper
the cleanest fix for most people is a small script called ufw-docker. it patches ufw so it actually understands docker's chains and can enforce rules against them instead of getting silently overridden.
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
sudo ufw-docker install
the install step rewrites ufw's after.rules file so docker traffic actually gets checked against ufw policy instead of bypassing it. after that, you allow container ports through ufw specifically, using the helper's syntax instead of the normal ufw allow command:
sudo ufw-docker allow container_name 8080
this tells ufw to allow traffic to that specific container's published port, and nothing else gets a free pass. restart docker after installing so the rule ordering actually takes effect:
sudo systemctl restart docker
fix two: lock down the DOCKER-USER chain manually
if you don't want a third party script touching your firewall, docker actually gives you an escape hatch on purpose. it created the DOCKER-USER chain specifically so admins have a spot to insert rules that get evaluated before docker's own automatic rules. anything you put here actually gets respected.
sudo iptables -I DOCKER-USER -i eth0 ! -s 10.0.0.0/8 -j DROP
that example drops forwarded traffic coming in on your public interface unless it's from your internal range, adjust the interface name and cidr to match your setup. the key idea is that DOCKER-USER rules are evaluated first, so this is the one place you can genuinely override docker's default "expose everything you publish" behavior without fighting the tool. just remember these rules aren't persistent by default, save them with your normal iptables-persistent setup or a systemd unit that reapplies them on boot.
confirm it from the outside, not from your own head
don't trust the config file, trust a scan. run this from a different machine or a cloud vps, not from inside your own network, because internal scans will lie to you thanks to routing differences.
nmap -p 1-65535 your.public.ip
anything showing open that you didn't explicitly allow through ufw-docker or DOCKER-USER is your real exposure, not the theoretical one in your ufw status output. run this after every fix, and honestly run it periodically anyway since new containers with new published ports are exactly how this problem creeps back in.
the takeaway
ufw was never wrong, it just never had a chance to enforce anything against docker's traffic. if you run containers with published ports, assume ufw is doing nothing for them until you prove otherwise. install ufw-docker or take direct control of DOCKER-USER, then verify with an external scan, not a status command. the fix takes ten minutes, the exposure it closes can last for years if nobody checks.