Over time, it’s common for the same service to be allowed by more than one rule. An older broad rule may still match traffic first, while newer, more restrictive rules below it are never evaluated.
If you already know UFW, the challenge is not writing rules. It’s auditing existing UFW rules to identify which still define exposure, which no longer serve a purpose, and which don’t affect traffic due to rule order, IPv6 parity, or underlying iptables behavior.
This guide focuses on auditing. You’ll use ufw status numbered to inventory active rules, ufw show raw to confirm enforcement order, and validation steps to reconcile what UFW reports with what the host is actually listening on.
Most searches that lead here start with a mismatch. ufw status says a port is blocked, but it’s still reachable. The rules look strict, yet the host is exposed in ways that aren’t obvious from the summary output. That’s the condition you’re auditing.
On long-lived servers, it’s often unclear which rules are actually controlling access. The same service often ends up allowed in more than one way. Rules remain after a service changes ports or is no longer running. Temporary access added for debugging or migrations never gets cleaned up. A policy can look locked down in ufw status, while an early Anywhere rule still defines access. IPv4 gets reviewed and tightened while IPv6 remains permissive.
Another common issue is incomplete visibility. Not all logic comes from the UFW CLI. Older hosts often include custom rules in /etc/ufw/before.rules, such as NAT handling or ICMP filtering, that run before user-defined UFW rules. If you don’t check that file, you’re not seeing the full enforcement path.
A UFW ruleset is ready for restrictive changes only when you can answer two questions without digging or guessing. What the host is currently exposed to, and which specific rule allows a given connection. If you can’t point to a line number and explain why it matches, tightening rules is premature.
For long-lived hosts, auditing needs to happen before any restrictive changes. Until you identify which rules actually allow inbound traffic, changing the policy usually adds complexity instead of reducing it.
Starting with restrictive changes before understanding exposure usually creates more rules, not less clarity. Without knowing which rules actually define access, tightening tends to pile exceptions on top of exceptions, making the ruleset harder to reason about, not safer.
The audit in this guide follows the same order UFW uses to evaluate traffic. Each step answers a specific question about how your UFW rules behave on the host, and each one depends on the previous step.
The order matters. Audit first, prove enforcement, then harden. If you reverse it, you usually don’t reduce exposure. You add new rules while the old ones still match, and the ruleset gets harder to explain.
ufw status numbered This step answers one question only. What is this host exposed to right now, based on the declared UFW rules? You are not proving enforcement yet. You are inventorying what the ruleset claims is allowed.
Start by confirming UFW is active:
sudo ufw statusThen pull the numbered ruleset. The line numbers matter later when you start removing or consolidating rules.
sudo ufw status numberedDuring audits, it’s also common to add context so you can see defaults, logging state, and policy direction:
sudo ufw status verboseAt this stage, resist the urge to fix anything. You’re reading for shape and volume, not correctness.
Don’t overthink this. Run ufw status numbered and just get a feel for the mess.
Anywhere? Those are usually the ones that matter.If the ruleset is huge and half of it is Anywhere, assume this host has had “temporary” access added over the years, and nobody ever came back to remove it.
Don’t read this top to bottom like it’s a story. Treat it like you’re triaging.
Group it in your head by what it’s protecting:
This is where stale access shows up fast. If SSH is ALLOW Anywhere, that’s your real exposure. Treat it as public SSH and move on.
Broad allows usually define exposure. A single Anywhere rule often matters more than several narrow ones below it. Duplicate rules usually point to an old intent that was never removed. These are the rules you’ll validate and harden later.
[ 1] 22/tcp ALLOW IN Anywhere
[ 2] 22/tcp ALLOW IN 203.0.113.10
[ 3] 80/tcp ALLOW IN Anywhere
[ 4] 443/tcp ALLOW IN Anywhere
[ 5] 9100/tcp ALLOW IN 10.0.0.0/8
[ 6] 9100/tcp ALLOW IN Anywhere
[ 7] 3306/tcp ALLOW IN 10.0.5.0/24
[ 8] 22/tcp (v6) ALLOW IN Anywhere (v6)In this ruleset, SSH traffic hits line 1 first. That rule allows SSH from anywhere, so the connection is accepted immediately. Line 2 never gets a chance to apply, even though it looks more restrictive.
The same thing happens with the monitoring port. Line 6 allows port 9100 from Anywhere. Because it appears above, nothing more restrictive for that port defines access. Line 5 does not reduce exposure.
Line 8 shows the same pattern on IPv6. SSH is allowed from Anywhere over IPv6, even though IPv4 access looks more controlled.
That’s why multiple rules can exist for the same service, but only the first matching one affects access. The first rule that allows the traffic is the one that defines what the host is exposed to.
Sometimes you can’t tell whether a rule is still needed just by reading it. Use temporary UFW logging during a short audit window, then turn it back down.
Increase logging temporarily during an audit window:
sudo ufw logging highVerify the logging state:
sudo ufw status verboseWatch UFW logs in real time:
sudo journalctl -f | grep -i ufwIf your environment logs to a file instead:
sudo tail -f /var/log/ufw.logYou’re not doing deep log analysis here. You’re confirming whether traffic still matches a rule you’re considering removing. Once you’ve answered that, logging should go back to its previous level before you move on.
ufw show raw Step 1 shows what the ruleset claims is allowed. That’s not the same as what actually decides a connection. This step answers a narrower question. Which rule wins when traffic hits the host?
UFW doesn’t enforce packets by itself. It translates your policy into iptables rules, and that’s what actually gets evaluated. Order matters there, and the first match decides the outcome. If you don’t look at the generated rules, it’s easy to trust a summary that doesn’t reflect enforcement.
Show the generated rules exactly as UFW loads them:
sudo ufw show rawThink of UFW rules as a series of gates. If the first gate is wide open, the packet walks through immediately. Anything below it, no matter how strict, never even gets evaluated. This is why an audit has to prove which rule matches first, not just which rules exist.
This output is the enforcement path. Rules are evaluated top to bottom. A broad rule early in the chain will accept traffic before a narrower rule below it ever gets a chance to apply.
This is a common reason the summary output doesn’t reflect actual behavior. The policy looks tight in ufw status numbered. The raw output shows a permissive match earlier that defines access.
Consider a simplified example from ufw show raw:
-A ufw-user-input -p tcp --dport 22 -j ACCEPT
-A ufw-user-input -p tcp -s 203.0.113.10 --dport 22 -j ACCEPTBoth rules allow SSH. The second one is narrower. It never matters. Any SSH connection that matches the first rule is accepted immediately. The presence of the restricted rule below it does not reduce exposure.
This is why rule count and intent don’t tell you much on their own. Order does.
To separate intent from enforcement, it also helps to see which rules were added explicitly through the UFW CLI:
sudo ufw show addedThis view is useful later when you clean up rules. It shows what was added deliberately, not what exists after rendering and defaults are applied.
Finally, validate the firewall view against what the host is actually listening on:
sudo ufw show listeningMany admins also cross-check directly at the socket level:
sudo ss -tulpenAt this point, you should be able to reconcile three things. What services are listening, what UFW rules claim to allow them, and which raw rule actually decides the connection. If you are still unsure, check out our UFW Troubleshooting blog.
On long-lived hosts, Docker is one of the most common reasons UFW policy and real exposure diverge. Docker programs iptables directly. That includes NAT and forwarding paths that sit outside the usual UFW flow.
Most admins assume UFW is deciding whether a port is reachable. With Docker, that’s often not true. When you run docker run -p 80:80, Docker inserts NAT and forwarding rules (including PREROUTING and DOCKER-related FORWARD logic) that can make a port reachable even if ufw status looks strict. If you don’t inspect the underlying chains, it’s easy to mistake declared UFW policy for actual exposure.
If Docker is present, don’t stop at UFW output. Verify the underlying rule path.
Start with the full ruleset and Docker-specific chains:
sudo iptables -S
sudo iptables -S DOCKER-USER
sudo iptables -S FORWARD
sudo iptables -t nat -SIf you prefer counters and packet visibility:
sudo iptables -L DOCKER-USER -n -v
sudo iptables -t nat -L -n -vIf ufw status numbered says a port is blocked, but Docker published it, the host may still be reachable on that port. The audit step here is confirming the actual reachable surface, not relying on UFW’s declared intent alone.
At this point, you’ve verified declared policy, enforcement order, and actual listeners. You now know what the rules say, how they’re enforced, and where UFW may not be the deciding layer. Hardening only makes sense after that picture is clear.
After auditing the ruleset and confirming the enforcement order, the next task is deciding which UFW rules actually matter. Not every rule contributes to exposure. A small number of permissive rules usually define what the host allows.
Work through the ruleset service by service. For each exposed service, apply the same checks.

Look for the rule with the widest scope. Anywhere sources, unrestricted interfaces, or broad subnets. That rule usually defines exposure, regardless of how many narrower rules exist below it.
Once you identify it, treat it as the rule that represents the service’s real exposure. If you had to describe access in one sentence, this is the rule you’d use.
After you find the most permissive rule, look for other rules that allow the same traffic but with narrower conditions. If a rule matches, iptables stops and applies that target. Rules below it won’t change exposure. They often reflect older access paths, temporary exceptions, or later attempts to tighten access without removing the original rule.
During an audit, the goal is not to decide what to delete. It’s to identify which rules actually affect traffic and which ones don’t change behavior at all.
For each exposed port or service, you should be able to write a single line that describes access as enforced:
Anywhere (IPv4), Anywhere (v6)10.0.5.0/24Anywhere because of rule 6If you can’t summarize exposure without rereading the ruleset, you don’t yet know which rule is actually controlling access.
At the end of this step, you should be able to identify the dominant rule (the first rule that actually allows the traffic) for each exposed service and explain why it wins.
Don’t leave this as “the rules look fine.” The goal was to prove what’s actually happening on this host. You started with what UFW says (status), checked what it really loads (show raw), reconciled that against what’s listening (show listening / ss), and used logging when the behavior still didn’t line up. If you can’t point to the first rule that matches and explain why it wins, you’re not done yet.
If you want the bigger picture behind why UFW summaries drift from enforcement on long-lived hosts, how UFW fits into Linux firewalls covers where UFW sits in the stack and what it abstracts away.
If you’re ready to tighten access without breaking the box, UFW Hardening Patterns for Long-Lived Linux Servers picks up from here with add → test → remove, rollback safety, and validation checks so changes stay boring.