OPNsenseIII · Stateful Firewalling and PFStateful firewalling and PF
PF internals and the generated configuration
What you'll learn
- Trace the path from a GUI rule change to the running PF ruleset
- Locate the generated ruleset files on the firewall filesystem
- Read the generated ruleset and map sections back to GUI features
- Recognise why editing generated files directly is an anti-pattern
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
Every OPNsense operator eventually asks “where does the ruleset
PF is actually running come from?”. The honest answer is: a PHP
script on the firewall takes the GUI configuration and compiles
it into a text file, which is then loaded by pfctl. The
operator who understands this path can debug any “the GUI shows
one thing, PF does another” discrepancy; the operator who does
not eventually edits a generated file directly and breaks the
firewall.
This lesson traces the path from GUI to running ruleset, shows how to inspect the generated files, and explains why the GUI should always be the source of truth.
The path from GUI to running ruleset
When the operator clicks “Apply” in the GUI, OPNsense runs a chain of events:
- Save the configuration. The PHP backend writes the new
configuration to
/conf/config.xml(andconfig.cache.xmlfor the active running state). - Trigger the filter generator. A signal causes the
filter_generate.incscript to run. This script reads the configuration and produces the PF ruleset. - Write
filter.conf. The generator writes the compiled ruleset to/conf/backup/filter-<timestamp>.confand/conf/backup/filter/filter.conffor the active state. - Validate. The generator calls
pfctl -nf(no-load syntax check) on the new ruleset. If the ruleset has syntax errors, the apply fails and the previous ruleset stays active. - Reload PF.
pfctl -f /conf/backup/filter/filter.confswaps in the new ruleset. State is preserved where possible (existing states whose rule is unchanged continue to match).
The whole path takes seconds. The operator sees the GUI apply complete; the new ruleset is live.
$ ls -la /conf/backup/filter/total 148
drwxr-xr-x 2 root wheel 512 Aug 14 03:27 .
drwxr-xr-x 9 root wheel 512 Aug 14 03:27 ..
-rw-r--r-- 1 root wheel 4321 Aug 14 03:27 filter.conf
-rw-r--r-- 1 root wheel 4488 Aug 14 02:14 filter-20260814-021453.conf
-rw-r--r-- 1 root wheel 4387 Aug 13 23:51 filter-20260813-235129.conf
-rw-r--r-- 1 root wheel 4390 Aug 13 22:30 filter-20260813-223007.confIllustrative output
Reading the generated ruleset
The generated filter.conf is a plain-text PF ruleset. It is
the ruleset that PF would parse if you ran pfctl -f filter.conf manually. Every feature OPNsense supports has a
corresponding section in the file:
- Header with version and timestamp.
- Macros and tables (aliases).
- Global options (timeouts, limits).
- Per-interface rules.
- Floating rules.
- NAT rules (rdr, nat).
- Anchor rules for OpenVPN, IPsec, Captive Portal.
- Default blocks at the bottom.
Reading the file directly is the most authoritative way to verify what PF is running.
$ head -40 /conf/backup/filter/filter.conf# OPNsense generated ruleset
# Generated: 2026-08-14 03:27:18
# Source config: /conf/config.xml
# --- Tables (aliases) ---
table <bogons> persist file "/etc/bogons"
table <sshlockout> persist
table <wan_net> { 198.51.100.0/24 }
table <lan_net> { 192.0.2.0/24 }
table <allowed_admin_ips> { 203.0.113.10, 203.0.113.20, 198.51.100.0/24 }
# --- Global options ---
set limit states 100000
set limit src-nodes 10000
set timeout tcp.established 86400
set timeout tcp.first 30
set timeout udp.first 60
set timeout icmp.first 20
# --- Interface 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 = 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 rules in the file use the same syntax PF understands
directly. You can read them without translation. The line
numbers in the file (@87, @88) are arbitrary labels assigned
by the generator; they are useful for referencing specific
rules in conversation and for cross-referencing with the GUI’s
rule IDs.
Mapping GUI features to ruleset sections
Each OPNsense GUI feature has a corresponding section in the generated ruleset:
| GUI feature | Generated ruleset location |
|---|---|
Aliases (Firewall → Aliases) | table <alias_name> declarations |
Per-interface rules (Firewall → Rules → LAN) | @N rules under “Interface rules” |
Floating rules (Firewall → Rules → Floating) | @N rules under “Floating rules” (multiple interface matches) |
Outbound NAT (Firewall → NAT → Outbound) | nat rules (with -> syntax) |
Port forwards (Firewall → NAT → Port Forward) | rdr rules |
OpenVPN (VPN → OpenVPN) | anchor "openvpn/*" rules |
IPsec (VPN → IPsec) | anchor "ipsec/*" rules |
Captive Portal (Services → Captive Portal) | anchor "captiveportal/*" rules |
Global firewall settings (System → Firewall → Advanced) | set limit, set timeout options |
Knowing where each GUI feature appears in the ruleset makes debugging faster. If the operator enables a feature in the GUI but does not see the expected behaviour, read the corresponding ruleset section to confirm the feature actually compiled.
Why you should never edit generated files directly
Three reasons:
- Generated files are overwritten. Every apply or reboot
regenerates
filter.conf. Your edits are gone. - GUI state and PF state diverge. If the GUI shows a rule that does not match what PF is running, the operator will trust the GUI and miss your edits. Future operators inheriting the firewall will be confused.
- Audit trail is lost. The config XML is the auditable record of the firewall. Generated files are derived artefacts and not part of the change-control flow. Editing generated files bypasses the audit trail.
For operators who genuinely need behaviour the GUI cannot
express, the supported mechanism is custom snippets via
System → Advanced → Firewall → Generated Rules. Snippets
are still overwritten on every apply, but they are visible in
the config XML (under <filter>...<customrules>...</customrules>...</filter>)
and the GUI surfaces them with a clear “this is a custom
snippet” indicator.
Production patterns
Three patterns make the generated ruleset tractable in a production estate.
Diffing rulesets across applies
OPNsense keeps timestamped backups in /conf/backup/filter/.
To see what changed between two applies:
diff /conf/backup/filter/filter-20260813-235129.conf \
/conf/backup/filter/filter-20260814-021453.conf
The diff shows every rule that was added, removed, or modified. For change-control audits, this diff is the evidence that the intended change actually compiled.
Capturing rulesets for incident review
When investigating a firewall incident, save the running ruleset:
pfctl -s rules > /tmp/rules-20260814-0314.txt
pfctl -s state > /tmp/state-20260814-0314.txt
pfctl -s Anchors > /tmp/anchors-20260814-0314.txt
These captures are evidence. Attach them to the incident ticket so the next operator can reconstruct what the firewall was doing.
Auditing for stale rules
Production firewalls accumulate rules over years. Some rules no longer match any traffic (the destination service was decommissioned). To find stale rules, capture rulesets over time and look for rules whose state-table counters never advance:
pfctl -s state -v | grep -A 1 <rule_pattern>
A rule that has matched no traffic in months is a candidate for review and removal.
Summary
- The GUI configuration compiles to a generated ruleset via
filter_generate.inc, written to/conf/backup/filter/. - Every apply regenerates the ruleset. Editing the file directly is an anti-pattern.
- Each GUI feature maps to a specific section in the generated ruleset. Read the file to verify what PF is actually doing.
- For behaviour the GUI cannot express, use custom snippets.
- Diffing rulesets across applies is the change-control evidence for firewall configuration.
Knowledge check · 4 questions
Q1. You need a firewall rule that matches on a specific TCP flag combination that the OPNsense GUI does not expose. Which is the supported way to add this rule?
Q2. The /conf/backup/filter/filter.conf file is the source of truth for OPNsense firewall configuration.
Q3. Which of the following statements about OPNsense ruleset generation are correct? Select all that apply.
Q4. You change a GUI firewall rule but pfctl -s rules does not show the change. The system log shows no errors. What is the most likely cause?
Passing score: 75%. Answers are checked in this browser.