How an incoming packet goes through INPUT
A connection to the server itself enters the INPUT chain of the filter table. Rules are checked in order: the first match with ACCEPT, DROP or REJECT decides, LOG just writes to the log and moves on. A -j jump into a user chain (ufw-user-input, f2b-sshd, DOCKER-USER) works like a function call: if nothing decides there or RETURN fires, checking resumes with the next rule. If nothing matches by the end of INPUT, the chain policy applies — the :INPUT DROP or ACCEPT line.
The analyzer replays this path for a new connection from an arbitrary address to every port from the rules and to common service ports. ESTABLISHED,RELATED-only rules do not open ports to new connections, -i lo only concerns the server itself, and a rule with -s 10.0.0.0/8 opens the port to that network only — such ports are marked “only from”. Conditions it cannot parse are not guessed: the rule is marked “may not apply”.
Why Docker ports bypass ufw
docker run -p 5432:5432 adds a DNAT rule to the nat table: a packet to port 5432 of the server is redirected to the container address, e.g. 172.17.0.2, already in PREROUTING. To the kernel this is transit traffic, so it goes through FORWARD, where Docker adds its own accept rules. INPUT and ufw rules (ufw deny 5432) never see it — the database is open even though ufw status says otherwise.
What to do: if only the server or other containers need the port, publish it on 127.0.0.1 or do not publish it at all — services on a Compose network reach each other by name. If specific addresses need access, add rules to the DOCKER-USER chain: it is checked first and Docker does not overwrite it. Note that packets there already carry the container port; match the original host port with -m conntrack --ctorigdstport. More: Docker and the firewall
How to read iptables-save
The output is split into tables: *filter starts a table, COMMIT ends it. Lines with a colon declare chains: :INPUT DROP [0:0] is a built-in chain with a DROP policy and packet/byte counters, :ufw-user-input - [0:0] is a user chain without a policy. Then come the rules in check order: -A INPUT -p tcp -m tcp --dport 22 -j ACCEPT means “append to INPUT: TCP to port 22 — accept”. An exclamation mark negates a condition: ! -i docker0 means “not from interface docker0”.
nft list ruleset writes the same in blocks: table inet filter { chain input { type filter hook input priority filter; policy drop; … } }. If you see “# Warning: table ip filter is managed by iptables-nft”, those rules were created through iptables (ufw, Docker) and are best edited with the same tools. All the commands are in the wiki.
FAQ
Why doesn't ufw deny close a container port?
Docker publishes a port with a DNAT rule in the nat table. The destination is rewritten before filtering, and the packet reaches the container through the FORWARD chain, so ufw and regular INPUT rules never see it. Restrict access in the DOCKER-USER chain or publish on 127.0.0.1 only (ports: "127.0.0.1:5432:5432").
Why iptables-save and not iptables -L?
iptables -L shows only the filter table by default, hides interfaces (-i lo looks like “accept everything”) and resolves addresses to names. iptables-save prints every table, policy and rule exactly as loaded — it can be parsed and restored with iptables-restore.
Do I need to paste ip6tables-save too?
Yes, if the server has an IPv6 address (check ip -6 addr show scope global). iptables and ip6tables are two independent rule sets: a port closed over IPv4 may be open over IPv6. In nftables an inet table covers both at once.
What does “shadowed” or “never fires” mean?
Rules are checked top to bottom, and the first match with ACCEPT, DROP or REJECT ends the check. If a broader rule comes first — say ufw allow 8080 before ufw deny from 198.51.100.23 to any port 8080 — packets never reach the narrow rule. Move it up (iptables -I, ufw insert 1) or delete it.
Is it safe to paste my server's rules here?
The analysis runs in your browser: rules are not sent to a server, not stored and not put into the page URL. Still treat addresses and chain names as internal information and do not post the output in public chats.
