Skip to main content
RunBook Academy

OPNsenseXLIV · Change Management and Rule ReviewRule review

Shadowed and redundant rules — finding the rules that are doing nothing

Intermediate⏱ ~15 minpfctlconfigctllogread

What you'll learn

  • Identify a shadowed rule — a rule that is never reached because an earlier rule matches first
  • Identify a redundant rule — a rule that matches the same traffic as another rule
  • Identify a dead rule — a rule that has not matched in months
  • Remove a shadowed, redundant, or dead rule with the safe rule change procedure

Prerequisites

Verified against OPNsense 25.x · FreeBSD 14.x · PF (FreeBSD packet filter) FreeBSD 14.x · Unbound 1.20+ · Kea DHCP OPNsense 25.x plugin · WireGuard in-kernel + OPNsense plugin · strongSwan (IPsec plugin) OPNsense 25.x plugin · OpenVPN 2.6.x · Suricata 7.x · 2026-08-14

Not yet marked complete on this device.

A rule that is never reached is a rule that does not exist — except that the operator who reads the ruleset thinks it does. The shadowed rule is a ruleset landmine: it sits in the ruleset, the operator assumes it is in effect, the next incident reveals that the rule was never matching. The redundant rule is worse — it is in effect, but the operator’s mental model of the ruleset is wrong because two rules do the same thing. The dead rule is the simplest: a rule that was added for a service that no longer runs.

This lesson covers the three failure modes of rules that are not earning their place: shadowed, redundant, dead. How to find each, why PF’s evaluation order makes shadowing possible, and how to remove them safely.

The three failure modes

A rule in the ruleset either matches traffic or it does not. If it matches, it is doing work. If it does not match, it is failing one of three tests:

  • Shadowed. An earlier rule (in PF evaluation order) matches the same traffic with a quick action. The shadowed rule is never reached.
  • Redundant. Another rule matches the same traffic. Both rules produce the same effect; one is duplicate.
  • Dead. No traffic has matched the rule in a long time. The service the rule was meant to allow is no longer running, or the source/destination has changed.

Each failure mode has a different cause and a different fix. The operator who confuses them makes the wrong removal — they remove a rule they thought was redundant but was actually critical, or they keep a shadowed rule thinking it provides defence in depth.

Read-only / SafeRuleset with @-prefixed rule numbers
$ pfctl -s rules | grep -v '^@' | head -5 && echo '---' && pfctl -s rules | grep -c '^@'
pass in quick on igb0 inet proto tcp from 192.0.2.0/24 to any port = https
@0 pass in quick on igb0 inet proto tcp from 192.0.2.0/24 to any port = https
@1 pass in quick on igb0 inet proto tcp from 192.0.2.0/24 to any port = dns
@2 pass in quick on igb0 inet proto tcp from 192.0.2.0/24 to any port = ssh
@3 pass in quick on igb0 inet proto tcp from 192.0.2.0/24 to any port = https
847

Illustrative output

How PF evaluation order creates shadowing

PF evaluates rules in order. The first rule that matches a packet decides the action — unless an earlier rule uses quick, in which case the earlier rule decides and PF stops. Rules without quick continue to be evaluated; the last matching rule decides.

OPNsense’s filter generator adds quick to most rules. The standard pattern is:

@0 pass in quick on igb0 inet proto tcp from 192.0.2.0/24 to any port = https
@1 pass in quick on igb0 inet proto tcp from 192.0.2.0/24 to any port = dns
@2 pass in quick on igb0 inet proto tcp from 192.0.2.0/24 to any port = ssh
@3 block in quick on igb0 inet from 192.0.2.99 to any

Rules @0, @1, @2 are quick-pass rules that allow specific traffic from the LAN. Rule @3 is a quick-block rule that blocks a specific source (192.0.2.99) from any traffic.

Now consider this addition:

@4 pass in quick on igb0 inet proto tcp from 192.0.2.0/24 to any port = https

Rule @4 is a duplicate of @0. Both rules allow TCP from 192.0.2.0/24 to any port 443. Rule @4 is redundant — the same traffic is already allowed by @0.

Now consider this addition, placed at @4:

@4 pass in quick on igb0 inet proto tcp from 192.0.2.0/24 to any port = https
@5 pass in quick on igb0 inet proto tcp from 192.0.2.0/24 to any port = 8443

Rule @5 is the only rule that allows port 8443, so it is doing real work. Rule @4 is still redundant.

Now consider this addition:

@4 block in quick on igb0 inet proto tcp from 192.0.2.0/24 to any port = https
@5 pass in quick on igb0 inet proto tcp from 192.0.2.0/24 to any port = https

Rule @4 blocks traffic that rule @5 allows. With quick on both, rule @4 wins because it is evaluated first. Rule @5 is shadowed — it is never reached.

The operator who reads the ruleset sees rule @5 and thinks “HTTPS from LAN is allowed”. They are wrong. Rule @4 blocks first; rule @5 is never reached.

Finding shadowed rules

A shadowed rule is found by reading the compiled ruleset and looking for rules that can never be reached. Three common patterns:

  1. A block rule before a pass rule with overlapping criteria. If a block rule matches everything the pass rule would match, the pass rule is shadowed.
  2. A quick pass rule before another quick pass rule with the same criteria. The earlier rule matches first; the later rule is redundant (a special case of shadowing).
  3. A quick rule with broader criteria before a more specific rule. If the broader rule’s action is the same as the specific rule’s action, the specific rule is shadowed. If the broader rule’s action differs, the specific rule might still be reached for the narrower traffic.

The operator reads pfctl -s rules and walks through each rule, asking: “for the traffic this rule would match, is there an earlier rule that matches it first?” If yes, the rule is shadowed.

Finding redundant rules

A redundant rule is found by looking for two or more rules that match the same traffic and produce the same effect. The operator reads pfctl -s rules and groups rules by:

  • Interface (on igb0)
  • Direction (in / out)
  • Protocol (inet proto tcp / udp / icmp)
  • Source
  • Destination
  • Port
  • Action

Two rules that match the same traffic on all these dimensions are redundant. The operator chooses one to keep and removes the other.

A subtle case: a rule with broader criteria and a rule with narrower criteria, where the narrower rule’s action is the same as the broader rule’s action. If the broader rule’s action is pass, the narrower rule is redundant (its traffic is already allowed by the broader rule).

A more subtle case: a pass rule with quick and a later pass rule without quick. The earlier rule matches first and stops evaluation. The later rule is shadowed for the matching traffic but might still be reached for non-matching traffic. The operator must read both rules carefully to decide whether the later rule is doing work for some traffic pattern.

Finding dead rules

A dead rule is a rule that has not matched in months. The operator finds dead rules by checking the state table and the rule hit counters.

OPNsense maintains per-rule statistics on the firewall. The operator queries the state table for the rule:

# Substitute your own values before running:
# Rule number from the @N prefix in pfctl -sr -v output
RULE_NUMBER=12

pfctl -s state -v | grep -A2 "$RULE_NUMBER"

Or, more directly, queries the rules with state:

pfctl -s state | awk '{print $1, $2, $3}' | sort -u

A rule that has not appeared in pfctl -s state output for 60-90 days is a candidate dead rule.

A more rigorous test: enable per-rule statistics on each interface and check the hit count. A rule with zero hits over 60 days is dead.

The dead rule is the simplest failure mode to identify, but the most dangerous to remove. The rule was added for a reason; the reason may be dormant (a service that runs quarterly, a backup that runs weekly, an audit that runs annually). The operator who removes the rule without checking the dormant use case breaks the dormant flow.

Removing shadowed, redundant, or dead rules

The removal procedure is the safe rule change procedure applied in reverse. The operator:

  1. Identifies the rule to remove. Records the rule number, the interface, the action, the criteria.
  2. Takes a backup of the current configuration.
  3. Peer-reviews the removal. The peer confirms the rule is shadowed, redundant, or dead, and that the removal will not affect production traffic.
  4. Verifies the recovery path (console, break-glass SSH, backup restore).
  5. Applies the removal from one session.
  6. Tests from a second session — verifies the traffic patterns the rule was supposed to handle still work (or, for shadowed rules, verifies that no traffic depended on the shadowed rule).
  7. Documents the removal: ticket, rationale (“rule was shadowed by @N — see prior review”), before/after evidence.

The removal is a change. The change gets the same procedure as any other change, with the same risk classification. A high-risk removal (the rule was the only allow for a critical path) gets the same precautions as a high-risk addition.

Summary

  • Three failure modes of rules not earning their place: shadowed (earlier rule matches first), redundant (duplicate traffic), dead (no traffic matches in months).
  • PF’s evaluation order creates shadowing. The first matching rule with quick wins; later rules with overlapping criteria are shadowed.
  • Finding shadowed rules: read pfctl -s rules in evaluation order; ask “for this rule’s traffic, is there an earlier rule that matches first?”
  • Finding redundant rules: group rules by interface, direction, protocol, source, destination, port, action; two rules that match the same traffic on all dimensions are redundant.
  • Finding dead rules: per-rule statistics, hit counts, state-table analysis over 60-90 days.
  • Removing rules uses the safe rule change procedure in reverse. The removal is a change; it gets the same procedure as any other change.

Knowledge check · 3 questions

  1. Q1. Reading `pfctl -s rules` in evaluation order, you see: `@0 pass in quick on igb0 inet proto tcp from 192.0.2.0/24 to any port = https` and `@1 pass in quick on igb0 inet proto tcp from 192.0.2.0/24 to any port = https`. What is the failure mode of rule @1?

  2. Q2. A shadowed rule that matches the same traffic as an earlier rule is a form of defence in depth that should be kept in the ruleset for documentation purposes.

  3. Q3. Which of the following are reliable ways to find dead rules? Select all that apply.

Passing score: 75%. Answers are checked in this browser.