This lab replaces the OPNsense default ruleset with a
deliberate, auditable baseline. The post-wizard ruleset
(“allow everything from LAN, block everything from WAN”) is
functional, but it is not a policy. A policy says what is
permitted, who is permitted to do it, and how the rest is
handled. This lab implements that policy and verifies the
runtime ruleset with pfctl so the operator can prove the GUI
matches what the kernel is enforcing.
The lab also introduces the most important habit in firewall
operations: route the verification through the same tools the
attacker would use, not the GUI. The GUI is the input; pfctl
is the truth.
Objective
By the end of this lab you can:
- Read the live PF ruleset with
pfctl -sr. - Replace the wizard’s default-allow LAN rule with a service-by-service allow list.
- Add a default-deny WAN inbound rule with logging.
- Trigger a block and locate it in the firewall log.
- Reload OPNsense safely and confirm the on-disk ruleset matches the runtime.
Requirements
- A working OPNsense instance from the previous lab (the baseline backup is the rollback target).
- A LAN-side workstation with
curl,dig, andping. - Web UI access to the firewall from the LAN side.
- Optional: a workstation on the WAN side or a second NIC for testing the inbound deny.
Tasks
Task 1: Capture the current ruleset
Before any change, capture what is running. The GUI is the
input; pfctl -sr is the truth:
configctl filter show rules | tee /tmp/opnsense-rules-before.txt
pfctl -sr | tee /tmp/opnsense-pf-before.txt
The two views should agree. The first is the OPNsense model (filter rules); the second is the compiled PF ruleset the kernel is enforcing. If they differ, the GUI is showing a staged change that has not been applied, or a service has injected a rule the model does not know about.
Count the rules:
pfctl -sr | wc -l
The post-wizard ruleset is small — fewer than twenty rules. Note the count; the comparison after the change is the evidence that the ruleset changed.
Task 2: Disable the LAN default-allow rule
The wizard adds an IPv4 LAN net to any rule on LAN that
allows everything. The rule is the reason a freshly installed
OPNsense passes the “does the Internet work?” smoke test from
the LAN. It is also the reason a compromised LAN host can
exfiltrate on any port.
In the GUI, Firewall → Rules → LAN, edit the default rule and disable it (the green enable toggle on the right). Do not delete it yet — the rollback path is the green toggle back. Click Apply changes.
Re-check the runtime:
pfctl -sr | grep -E 'LAN|em1'
The default-allow rule should still be in the model but no
longer in the compiled ruleset. The discrepancy is the
warning sign: the GUI is showing the model, not the kernel
view. The production check is pfctl -sr.
Task 3: Add the deliberate outbound allow list
Add four rules on the LAN interface, in order, top down:
- Allow LAN to WAN: DNS (UDP 53). Without this, name resolution fails and the “the Internet is down” page arrives without DNS working.
- Allow LAN to WAN: NTP (UDP 123). Hosts need time sync to validate TLS.
- Allow LAN to WAN: HTTPS (TCP 443). The majority of modern traffic.
- Allow LAN to WAN: ICMP echo (ping). For diagnostics.
For each rule:
- Action: Pass
- Interface: LAN
- Protocol: as listed
- Source: LAN net
- Destination: any
- Log: enabled
The log flag is critical. Without it the lab cannot prove the rules worked — only that traffic passed.
Task 4: Add the default-deny rule at the bottom
Below the four allow rules on LAN, add:
- Action: Block
- Interface: LAN
- Direction: in
- Protocol: any
- Source: any
- Destination: any
- Log: enabled
This rule is the policy: anything the four allow rules do not cover is blocked and logged. Apply.
Task 5: Add the WAN-side default-deny with logging
On the WAN interface, the wizard already adds one IPv4 * to * block rule, but it is silent. Replace or edit it so logging
is enabled. The verbosity should be the default, not “full
packet” — full-packet logs grow quickly and obscure the
signals.
In the GUI: Firewall → Rules → WAN, edit the block rule (or add a new one at the bottom of the list) with logging enabled. Apply.
Verify:
pfctl -sr | grep -E 'block|deny'
Expect a block (or deny) rule on WAN that names the interface and logs.
Task 6: Reproduce a block and find it in the log
From the LAN workstation, try something that should be blocked:
# SMTP outbound - deliberately not in the allow list
curl -v --connect-timeout 5 telnet://smtp.example.com:25
Expect a connection timeout. Then from the firewall:
# Most recent firewall log entries
configctl filter show log | tail -20
# Or via the GUI: Firewall → Log Files → Live View
The block should be visible with the source IP, destination IP
and port, and the rule name. The rule name is the auditor’s
proof — a rule named Default deny LAN to any makes the
intent unambiguous.
Task 7: Verify the live ruleset with packet capture
pfctl -sr is the contract. One more check — confirm the
rules actually fire:
# On the LAN side, start a TCP connection that the rules SHOULD allow
tcpdump -ni em1 'tcp port 443' -c 2 &
# From the LAN workstation
curl -sI https://example.com
The tcpdump output should show the SYN, SYN-ACK, and ACK
trio for the connection. If the SYN appears but the SYN-ACK
does not, the firewall passed the SYN outbound but is blocking
the return — a state-tracking problem, not a rule problem.
Task 8: Reload cleanly and confirm
Apply the ruleset, then verify the kernel view:
configctl filter reload
pfctl -sr | wc -l
The new count should be larger than the pre-change count by the number of rules added (four LAN allows + one LAN deny + log edits on WAN = ~5–7 additional rules, depending on whether the wizard add was replaced or extended).
Validation
pfctl -srshows the four explicit LAN allow rules followed by the default deny.pfctl -srshows a logging-enabled block rule on WAN.- The SMTP connection attempt in Task 6 produces a log entry under the deny rule.
- HTTPS from LAN to a public host works and shows traffic in
the LAN-side
tcpdumpcapture. - The rule count increased by the expected number of rules.
Expected Result
You have a baseline ruleset that follows the discipline “allow what is needed, log what is blocked, deny the rest, and verify the runtime against the model.” The wizard’s default-allow rule is gone. The LAN workstation can reach the Internet for DNS, NTP, HTTPS, and ping; anything else is blocked and logged.
Troubleshooting
- All traffic from LAN is blocked. The four allow rules
did not apply. Check
pfctl -sr | grep LAN— the entries should be there. If they are not, the GUI’s Apply did not commit. Repeat the Apply and reload. - Logging shows nothing on the block. The log flag is not set on the rule. Edit the rule, enable “Log packets that are handled by this rule”, save, Apply.
- HTTPS works from the LAN but the connection log is
empty. The TCP fast-open or HTTP/3 path is masking the
log. Try a plain
curl --http1.1to force the TCP handshake through the firewall. pfctl -srshows rules that are not in the GUI. A plugin (Suricata, Zenarmor) is injecting its own rules. These are normal, but document them — they are part of the effective policy.
Cleanup
The baseline ruleset is the new starting state for the course. Snapshot it before the next lab:
configctl backup download
# Save as opnsense-baseline-rules.xml
To roll back to the post-wizard ruleset:
# Substitute your own values before running:
# Basename of the backup taken at the end of the previous lab;
# list the candidates with: ls -1 /conf/backup/
PREVIOUS_BASELINE=config-2026-08-13T18:22:41
# Restore the backup from the previous lab
configctl restore "/conf/backup/$PREVIOUS_BASELINE.xml"
# Or in the GUI: System → Configuration → Backups → Restore
What you learned
- The GUI is the input;
pfctl -sris the truth. Verify the runtime against the model. - A default-allow LAN rule is convenient but not a policy. The first change in any production deployment is replacing it with a default-deny plus an explicit allow list.
- Logging is part of the rule, not an afterthought. A rule without log is invisible to the next operator.
- The four-service allow list (DNS, NTP, HTTPS, ICMP) is the minimum a modern LAN needs. Anything else is candidate for explicit allow or an explicit deny-and-log.