OPNsenseIX · Firewall RulesFirewall Rules
Rule processing order
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
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
blockmatching everything, which wins by last-match whenever noquickrule has claimed the packet. It is inpfctl -srand it can be removed — at which point PF passes what nothing matched.
$ 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 allIllustrative 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. quickon 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:
- SSH to the firewall.
- Run
pfctl -s rules. The output is top-to-bottom. - 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.
Specific-permit-then-default-deny (safest, recommended)
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:
- 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.
- 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.
- 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:
- Edit the GUI rule.
- Click Apply.
- Run
pfctl -s rules | grep -A 1 -B 1 <unique text>to confirm the rule compiled at the expected position. - Send test traffic and confirm the rule matches as expected.
- If the rule does not behave as expected, check
pfctl -s statefor 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.
quickstops evaluation at the first match. OPNsense usesquickeverywhere.- The GUI rules are not the rules PF runs. Read
pfctl -s rulesto 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
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?
Q2. OPNsense generates every GUI rule with the quick modifier by default.
Q3. Which of the following statements about PF rule evaluation are correct? Select all that apply.
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.