Here’s the thing: your firewall might be completely open and you don’t even know it. I’ve walked into production environments where someone added a broad ALLOW rule “just to test something” and never removed it, sitting above all the restrictive rules. By the time the audit rolled around, they’d forgotten it was there.
The fundamental principle is this: firewall rules are evaluated top-down, and the first match wins. Every rule below a matching rule is ignored. That one fact will save you from shooting yourself in the foot.
The Classic Mistake
Here’s what someone inexperienced usually does:
$ sudo ufw allow from 10.0.0.0/8$ sudo ufw deny from 10.0.1.100They think: “Allow the whole subnet, then deny one bad actor.” Nice try. The second rule never fires because the first rule already matched. Anyone from 10.0.0.0/8 gets through, including 10.0.1.100.
The correct order:
$ sudo ufw deny from 10.0.1.100$ sudo ufw allow from 10.0.0.0/8Now the specific denial happens first, then the broader allow. The bad actor is blocked, everything else from that subnet gets through.
Why Order Matters: iptables Under the Hood
UFW is a wrapper around iptables, so let’s think about what’s actually happening. When a packet arrives, iptables walks down the rule list in order and stops at the first match. If you have:
# Rule 1 (top)iptables -A INPUT -p tcp -m multiport --dports 22,80,443 -j ACCEPT
# Rule 2iptables -A INPUT -p tcp --dport 22 -s 192.168.1.50 -j DROP
# Rule 3iptables -A INPUT -j DROPA connection from 192.168.1.50:22 hits Rule 1 first (port 22 is in the multiport list) and gets ACCEPT’d immediately. Rule 2 never sees it. Your attempt to block SSH from that IP is completely ignored.
The fix: be specific before being broad.
# Rule 1 (top) — specific denials firstiptables -A INPUT -p tcp --dport 22 -s 192.168.1.50 -j DROP
# Rule 2 — then the broad allowiptables -A INPUT -p tcp -m multiport --dports 22,80,443 -j ACCEPT
# Rule 3 — default denyiptables -A INPUT -j DROPChecking Your Current Rules (UFW)
List all rules in order with:
$ sudo ufw status numberedYou’ll get something like:
To Action From -- ------ ----[ 1] 22 ALLOW Anywhere[ 2] 443 ALLOW Anywhere[ 3] 80 ALLOW Anywhere[ 4] 22 DENY 192.168.1.50See that? Rule 4 is useless because Rule 1 already matched port 22 from anywhere, including 192.168.1.50. You need to delete and reinsert it:
$ sudo ufw delete 4$ sudo ufw insert 1 deny from 192.168.1.50 to any port 22The insert 1 puts it at the top. Now it looks like:
[ 1] 22 DENY 192.168.1.50[ 2] 22 ALLOW Anywhere[ 3] 443 ALLOW Anywhere[ 4] 80 ALLOW AnywhereMuch better.
Real-World Pattern: Deny-First Rules
Here’s the pattern your production firewall should follow:
# 1. Deny specific bad actors / subnetssudo ufw deny from 203.0.113.0/24 # sketchy IP rangesudo ufw deny from 192.0.2.50 # compromised internal box
# 2. Allow specific internal subnets to SSHsudo ufw allow from 10.0.1.0/24 to any port 22sudo ufw allow from 10.0.2.0/24 to any port 22
# 3. Allow public servicessudo ufw allow 80/tcpsudo ufw allow 443/tcp
# 4. Default deny everything elsesudo ufw default deny incomingNotice: specific denials, then specific allows, then broad allows, then default deny. That’s your hierarchy.
One More Gotcha: In vs Out
By default, UFW defaults to denying incoming traffic. But your outbound rules are separate. You might be allowing SSH in, but if you’ve also set default deny on outgoing and never allowed traffic out, your syslog is full of dropped packets going from your machine:
$ sudo ufw default deny outgoing$ sudo ufw allow out 53 # DNS$ sudo ufw allow out 443 # HTTPSNow your box can’t talk to anything except DNS and HTTPS. Double-check both directions:
$ sudo ufw show addedThe Audit Moment
Every few months, export your rules and actually read them:
$ sudo ufw show added > /tmp/firewall_rules.txt$ cat /tmp/firewall_rules.txtSearch for broad ALLOW rules sitting above denials. Search for commented rules you forgot you added. Search for test rules from 6 months ago.
Your 2 AM self (or your auditor) will thank you.