Skip to main content
RunBook Academy

OPNsenseIII · Stateful Firewalling and PFStateful firewalling and PF

PF internals and the generated configuration

Advanced⏱ ~16 minpfctlfindgrep

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

Not yet marked complete on this device.

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:

  1. Save the configuration. The PHP backend writes the new configuration to /conf/config.xml (and config.cache.xml for the active running state).
  2. Trigger the filter generator. A signal causes the filter_generate.inc script to run. This script reads the configuration and produces the PF ruleset.
  3. Write filter.conf. The generator writes the compiled ruleset to /conf/backup/filter-<timestamp>.conf and /conf/backup/filter/filter.conf for the active state.
  4. 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.
  5. Reload PF. pfctl -f /conf/backup/filter/filter.conf swaps 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.

Read-only / Safels /conf/backup/filter/
$ 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.conf

Illustrative 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.

Read-only / Safefilter.conf excerpt
$ 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 all

Illustrative 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 featureGenerated 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:

  1. Generated files are overwritten. Every apply or reboot regenerates filter.conf. Your edits are gone.
  2. 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.
  3. 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

  1. 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?

  2. Q2. The /conf/backup/filter/filter.conf file is the source of truth for OPNsense firewall configuration.

  3. Q3. Which of the following statements about OPNsense ruleset generation are correct? Select all that apply.

  4. 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.