Skip to main content
RunBook Academy

OPNsenseIX · Firewall RulesFirewall Rules

Rule processing order

Foundation⏱ ~14 minpfctltcpdumppf

What you'll learn

  • Explain PF evaluation order: top-down, first match wins, quick modifier
  • Read the compiled ruleset with pfctl and verify rule order
  • Identify the order-induced traps that cause production incidents
  • Construct a deliberate, reviewable rule ordering for a production estate

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.

PF evaluates rules top-to-bottom. The first rule whose match criteria all match a packet decides the packet fate — pass, block, match — and PF stops evaluating. Every rule below the matching one is irrelevant for that packet. This is the “first match wins” model, and it is the rule-ordering model that every production firewall operator must understand.

This lesson covers how PF walks the ruleset, what quick means, how to read the compiled ruleset OPNsense generates, the production patterns that make ordering deliberate, and the order-induced traps that cause incidents.

First match wins

For each packet that has no matching state, PF walks the ruleset from top to bottom. The first rule whose source, destination, port, protocol, and other match criteria all match the packet is the one that decides. PF applies the rule action (pass, block, match, rdr, nat) and stops.

Three consequences:

  • A broad rule above a specific rule shadows it. The most common rule-ordering trap. The operator writes a broad “permit LAN → any” rule at the top, then writes a more specific “block LAN → 10.0.0.0/8” rule below. The block never matches because the permit above it always wins for matching packets.
  • Action types are not priorities. Pass, block, match, rdr, nat — they are all rules. PF does not prefer pass over block. Order is everything.
  • The default-deny is a rule, not a PF default. PF’s own behaviour for a packet that matches no rule is to pass it. What blocks unmatched traffic on OPNsense is a rule the filter generator writes: a non-quick block matching everything, which wins by last-match whenever no quick rule has claimed the packet. It is in pfctl -sr and it can be removed — at which point PF passes what nothing matched.
Read-only / Safepfctl -s rules
$ pfctl -s rules
@87 pass in quick on igb0 inet proto tcp from 192.0.2.0/24 to any port = https
@88 pass in quick on igb0 inet proto tcp from 192.0.2.0/24 to any port = http
@89 pass in quick on igb0 inet proto tcp from 192.0.2.0/24 to any port = 53
@92 pass in quick on igb0 inet proto tcp from 192.0.2.0/24 to any port = 22
@100 block drop in quick on igb0 inet from 10.0.0.0/8 to any
@110 block drop in on igb1 inet from any to any
@115 pass out all

Illustrative output

The quick modifier

Every OPNsense-generated rule has the quick keyword. The keyword tells PF to stop evaluating as soon as the rule matches. Without quick, PF continues evaluating after a match, and the last matching rule wins.

The quick modifier is what most operators expect: first match wins, just like the GUI rule ordering suggests. But it has a subtle consequence: rules below a quick rule never see packets that the quick rule matches. So if a rule above silently captures packets the operator meant to handle with a rule below, those packets will not appear in the log lines of the rule below.

If you genuinely need last-match-wins semantics — rare in OPNsense — you can omit quick. Be aware that logging the evaluating rules becomes harder because every matching rule fires its log line, not just the last one.

How OPNsense generates the ruleset

The GUI rules are not the rules PF runs. OPNsense compiles the GUI rules into a filter.conf and feeds it to pfctl -f. The compiled ruleset has additional elements the GUI does not show:

  • Rule numbers (@N) assigned by the filter generator for ordering and reference.
  • quick on every GUI rule.
  • Anchor rules that load additional rulesets from subdirectories (OpenVPN, IPsec, captive portal).
  • Default block rules at the bottom that catch everything the GUI did not explicitly permit.

To read what PF is actually doing, always use pfctl -s rules. Do not assume the GUI rule ordering matches the compiled ordering in edge cases — floating rules, interface-group rules, and NAT hooks can reorder the compiled ruleset.

Reading the rule order from the GUI

OPNsense GUI displays the rule list in the order they were added or moved. The order can be changed with the up/down arrows in the GUI or by dragging rules. The display order is what gets compiled into the ruleset — but as noted above, the compiled order can be affected by other features.

To see the actual order PF is using:

  1. SSH to the firewall.
  2. Run pfctl -s rules. The output is top-to-bottom.
  3. Read the output and confirm the rules are in the expected order.

The pfctl -s rules output also shows the @N rule numbers. These are useful for referencing specific rules in incident review and for cross-referencing with the GUI rule IDs.

Production ordering patterns

Three ruleset orderings cover most production estates well.

Default-allow-then-specific-deny (least safe, most common)

1. Permit LAN -> any
2. Permit DMZ -> specific destinations
3. Permit specific block on a sub-pattern
4. Block all

This is what the OPNsense wizard generates. It is easy to write but it means the operator cannot easily add a deny rule for specific traffic — every deny rule must be added above the default-allow.

1. Permit LAN -> Internet (specific)
2. Permit DMZ -> specific internal services
3. Permit VPN -> internal
4. Permit management -> specific
5. Block all

Every permit rule is explicit. Every block above the default catches specific traffic that should not be permitted. This is the ordering the OPNsense hardening guide recommends for production estates.

Tiered ruleset (most reviewable)

1. Anti-spoofing blocks (RFC 1918 from WAN, bogons)
2. Permit established/related traffic
3. Permit specific inbound services (each on its own line)
4. Permit specific outbound services (each on its own line)
5. Block all

Each tier has a purpose; rules within a tier can be reordered without affecting the overall behaviour. This is the most reviewable ordering and the one the production-first checklist promotes.

Order-induced production incidents

Three patterns recur:

  1. The permit-any above a specific deny. A “permit LAN to any” rule sits above a “deny LAN to 10.0.0.0/8” rule. The deny never matches. The fix is to swap their order or to rewrite the deny as a more specific permit elsewhere.
  2. The floating rule that matches everywhere. A floating rule that permits LAN-to-anything on every interface shadows every interface-specific rule. The fix is to bind the floating rule to a specific interface or to reorder.
  3. The logging rule above the real rule. A “log all” rule for debugging, left in place, captures every packet that should have matched a more-specific rule below. The fix is to remove the debug rule after diagnosis.

Verifying a ruleset

The verification sequence for any ruleset change:

  1. Edit the GUI rule.
  2. Click Apply.
  3. Run pfctl -s rules | grep -A 1 -B 1 <unique text> to confirm the rule compiled at the expected position.
  4. Send test traffic and confirm the rule matches as expected.
  5. If the rule does not behave as expected, check pfctl -s state for the flow; the state table may have cached the old decision.

The state-cache point is critical: PF caches decisions in the state table. If a packet matches a state entry from before the rule change, it does not re-evaluate against the new ruleset. To verify the new ruleset for an existing flow, clear the relevant state with pfctl -k state.

Summary

  • PF evaluates top-down. First match wins. Implicit default is block.
  • quick stops evaluation at the first match. OPNsense uses quick everywhere.
  • The GUI rules are not the rules PF runs. Read pfctl -s rules to verify.
  • Order matters: a broad permit above a specific deny shadows the deny.
  • State caches decisions. Verify new rules by clearing state.

Knowledge check · 4 questions

  1. Q1. You add a rule that permits HTTPS from the LAN to a specific server, but the rule never matches. The GUI shows the rule in the right place. What is the most likely cause?

  2. Q2. OPNsense generates every GUI rule with the quick modifier by default.

  3. Q3. Which of the following statements about PF rule evaluation are correct? Select all that apply.

  4. Q4. You change a firewall rule but a live TCP session continues to match the old rule behaviour. PF state shows an entry for the session. What is the most likely explanation?

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