Finding shadowed, redundant, and unreachable firewall rules
Every long-lived firewall accumulates rules that never fire. Some are harmless bloat; one kind quietly undoes your security policy. How to find each — and why you can't do it with a text diff.
Every firewall that's been in production for a few years accumulates rules that never fire. Some are harmless bloat. Others are actively dangerous — a permissive rule sitting above a restrictive one means the restriction never applies, and nobody notices because the config looks fine. Here's how to tell the categories apart and find each one.
Four things people call the same thing
These get used interchangeably and they shouldn't. Firewalls evaluate top-down, first match wins, so all of this is about a rule's position relative to the rules above it.
- Shadowed — an earlier, broader rule matches everything a later rule would, so the later rule never triggers. Rule 5 permits any → any : 443; rule 40 permits host-A → server : 443. Rule 40 is shadowed — rule 5 already caught that traffic.
- Redundant — two rules produce the same effect; removing one changes nothing. Often adjacent and identical in action, versus shadowing where a broad rule hides a narrower one.
- Unreachable — the umbrella term for a rule that can never match, whether from shadowing, ordering, or sitting below a catch-all.
- Unused — a valid, reachable rule that simply gets no traffic. Different from the above: it could match, nothing currently hits it. You only know from traffic logs, not from logic.
Why you can't find these with a text diff
This is the part that makes it hard by hand. Shadowing isn't about rules that look alike — it's about whether one rule's match set is a superset of another's. And rules reference object groups, not raw addresses. Two rules can share zero object names and still shadow each other, because the groups resolve to overlapping IP ranges.
So the only reliable comparison is to expand every rule to the actual sets it matches — resolve address groups to IP ranges, service groups to protocol/port sets — and compare those sets across the full tuple: source zone, source address, destination zone, destination address, service/application, and action.
Rule B (lower) is shadowed by an earlier rule A when, with objects expanded: A's sources ⊇ B's sources, A's destinations ⊇ B's destinations, A's services ⊇ B's services, the zones line up, and A's action applies to the traffic. If all hold, B never sees a packet.
The shadow that changes your security posture
Most shadowed rules are just clutter. The dangerous case is an action conflict: a broad allow sitting above a specific deny.
Say someone added a permissive allow any → any for a troubleshooting session and never removed it. Every deny rule below it is now shadowed — the traffic you meant to block is allowed, because the allow matched first. The config still contains your deny rules; they just never execute. This is the finding that turns a hygiene exercise into a security one, and it's invisible unless you're checking superset relationships against action.
What hit counters do and don't tell you
PAN-OS tracks rule hit count and last-hit time, and it's genuinely useful — a rule with zero hits over a long, representative window is a candidate for removal. But hit count is a blunt instrument:
- A shadowed rule shows zero hits — but so does a perfectly valid rule for traffic that simply hasn't occurred yet (DR failover, a seasonal service). Zero hits alone doesn't distinguish "dead" from "dormant."
- Hit counters can't tell you why a rule is unused — only logical analysis reveals that it's unreachable because something above it matches first.
- A short measurement window makes legitimate low-frequency rules look dead.
So use both: logic to find rules that can't match (shadowed, unreachable — safe to fix on structure alone), and hit counters to flag rules that can match but don't (unused — investigate before removing).
A practical pass
- Find any broad allow high in the rulebase and check what it shadows — especially deny rules below it.
- Expand objects to sets; compare each rule against the ones above it for superset relationships. Don't compare names.
- Separate structural findings (shadowed/unreachable — provable from the config) from usage findings (unused — needs logs).
- Treat a migration or a major review as the moment to do this — you're touching every rule anyway, and a clean rulebase is far easier to reason about than one carrying a decade of additions.
A firewall policy is one of the few places where "we never remove anything" is a security problem, not just a tidiness one. The rules that never fire are exactly the ones nobody's looked at — which is why that's where the surprises live.
Written by Roman Dmytriv — network security engineer (CISSP), two decades securing and modernizing enterprise networks. More writing at blog.dmytriv.com, and I'm on LinkedIn.