firewall-appliance
Firewall Appliance: OPNsense & pfSense Management
Manage, troubleshoot, and harden OPNsense and pfSense firewalls via SSH. Both are FreeBSD-based, pf-powered firewall distributions - most concepts, commands, and patterns apply to both.
Target versions (May 2026):
- OPNsense: 26.4.0 (26.4 series; 26.1.6_2 still supported on the 26.1 stable branch)
- pfSense CE: 2.8.1 / pfSense Plus: 26.03
- CrowdSec: v1.7.7
When to use
- Managing or troubleshooting OPNsense and pfSense firewalls over SSH
- Reviewing pf rules, NAT, CARP, Unbound, WireGuard, CrowdSec, or pfBlockerNG on these appliances
- Configuring VLANs, CARP HA failover, or interface assignments on firewall appliances
- Debugging connectivity between networks or VLANs routed through OPNsense/pfSense
- Hardening BSD firewall appliances and validating safe remote-change workflows
When NOT to use
- Linux networking, reverse proxies, VPN setup, or nftables work outside firewall appliances - use networking
- General shell scripting or local shell behavior outside the BSD firewall context - use command-prompt
- Fleet-wide configuration management via playbooks - use ansible
- Offensive testing, exploitation, or post-exploitation - use lockpick
- Application-level security review or dependency scanning - use security-audit
AI Self-Check
Before returning any firewall commands, verify:
- Platform confirmed (OPNsense vs pfSense) - commands differ between them
- No commands that could lock out SSH or management access
- Config backup taken (or reminded) before destructive changes
-
pfctlrules tested with-n(dry run) before applying - Service names correct for the target platform (
configctlvsservice) - Plugin names use correct prefix (
os-*for OPNsense, unprefixed for pfSense) - CARP changes target the master node, not the backup
- Shell syntax is POSIX sh (heredoc), not bash/zsh (csh/tcsh is the default shell on both)
- No firmware or plugin updates without explicit user confirmation
- Blast radius stated for any change affecting network connectivity
- DNS impact considered - changes to Unbound, DHCP, or firewall rules on port 53 can break name resolution for all clients on affected VLANs
- CrowdSec/pfBlockerNG checked when diagnosing blocks - bans look identical to firewall drops from the client side
- VLAN interface assigned before adding rules - unassigned VLANs pass no traffic through the firewall even if the trunk is tagged correctly
- Current source checked: dated versions, CLI flags, API names, and support windows are verified against primary docs before repeating them
- Hidden state identified: local config, credentials, caches, contexts, branches, cluster targets, or previous runs are made explicit before acting
- Verification is real: final checks exercise the actual runtime, parser, service, or integration point instead of only linting prose or happy paths
- Platform/version checked: OPNsense, pfSense, FreeBSD, pf, and plugin commands match the appliance version
- Lockout path prevented: remote firewall changes preserve management access and rollback
Performance
- Prefer rule ordering that rejects high-volume unwanted traffic early and keeps expensive inspection scoped.
- Use aliases/tables for large address sets instead of expanding repetitive rules.
- Check state table, DNSBL, IDS/IPS, and plugin load before blaming WAN latency.
Best Practices
- Export configuration before rule, NAT, VPN, CARP, or package changes.
- Make HA changes one node at a time and verify CARP state before touching the peer.
- Keep emergency console or out-of-band access available for management-plane changes.
Workflow
Step 1: Detect platform
If the platform is not obvious from context, ask the user which one they're running before issuing commands. Identify the target device explicitly - never assume which firewall you're talking to. Key differences at a glance:
| OPNsense | pfSense | |
|---|---|---|
| Base OS | FreeBSD (migrated from HardenedBSD in 2021) | FreeBSD |
| Config path | /conf/config.xml |
/cf/conf/config.xml |
| Service control | configctl service restart <svc> |
pfSsh.php playback svc restart <svc> or service <svc> restart |
| Plugin prefix | os-<name> (e.g., os-wireguard) |
No prefix (e.g., pfSense-pkg-WireGuard) |
| PHP shell | N/A | pfSsh.php (interactive PHP shell) |
| Quick rule add | N/A | easyrule pass wan tcp <src> <dst> <port> |
| IP blocking | CrowdSec (os-crowdsec) |
pfBlockerNG |
| Root shell | csh |
tcsh (same heredoc workaround applies) |
| IDS/IPS config | /tmp/suricata_*.log, eve.json |
/var/log/suricata/suricata.log, eve.json |
| Firmware CLI | configctl firmware check/status |
pkg-static update + GUI |
| Template engine | configd + configctl template |
PHP-generated configs |
| REST API | Yes (/api/, key/secret auth) |
Yes (similar, different endpoints) |
| Licensing | Free, open source | CE: free but slower updates; Plus: $129/yr on non-Netgate HW |
| Release cadence | Bi-weekly, fixed schedule | Irregular, Netgate hardware prioritized |
2026 status: OPNsense is the clear choice for new deployments. pfSense CE gets slower updates and zero priority; Plus costs $129/year on non-Netgate hardware. Migration from pfSense to OPNsense has no automated path - expect ~60% clean config transfer, manual rebuild for NAT rules, VPN, and DNS forwarder settings.
When platform is unknown, these commands work on both:
pfctl -sr # firewall rules
pfctl -ss # state table
ifconfig # interfaces
pkg info # installed packages
netstat -rn # routing table
sockstat -4l # listening sockets
Step 2: Back up config
Before any change that modifies rules, services, plugins, or firmware:
- OPNsense:
configctl firmware backupor GUI export (System > Configuration > Backups) - pfSense: GUI export (Diagnostics > Backup & Restore) or copy
/cf/conf/config.xml - For major upgrades on virtualized firewalls, pair config backup with a hypervisor snapshot
Skip this step only for read-only operations (diagnostics, log review, status checks).
Step 3: Execute the task
Apply changes using the platform-appropriate commands. Refer to the domain sections below and the reference files for specifics. For any change that affects connectivity:
- Test
pfctlrules with-n(dry run) before applying - State the blast radius ("this will drop all VPN tunnels for ~30s")
- On HA pairs, always change on the master node and let XMLRPC sync propagate
Step 4: Verify
After every change, confirm the firewall is healthy:
- Connectivity: can you still reach the device? Can clients reach the internet?
- Logs: check
/var/log/filter.log, service logs, and CrowdSec/Suricata if active - Service status:
configctl service list(OPNsense) orservice -e(pfSense) - State table:
pfctl -si | grep entries- watch for unexpected drops or state exhaustion
Quick Task Procedures
Creating a firewall rule (VLAN to server)
- Identify the interface the traffic originates from (e.g.,
opt1for VLAN 50) - Confirm the VLAN interface is assigned:
ifconfigmust show the VLAN interface UP. If the VLAN is not assigned to an OPNsense/pfSense interface yet (Interfaces > Assignments), it cannot have rules - assign it first. - Check existing rules:
pfctl -sr | grep <iface>- new interfaces have no rules (implicit deny all). Confirm the baseline before adding anything so you know exactly what you're changing. - Create aliases for source subnet and destination server (keeps rules readable):
- OPNsense API:
curl -X POST -u key:secret https://<fw>/api/firewall/alias/addItem -d '{"alias":{"name":"WebServer","type":"host","content":"10.0.1.100"}}' - OPNsense CLI:
configctl template reload OPNsense/Filter(after editing alias via API or XML). To verify the alias was created:configctl template list | grep Alias, then confirm withpfctl -t WebServer -T show. - pfSense:
easyruledoesn't support aliases - use GUI or edit/cf/conf/config.xmldirectly
- OPNsense API:
- Add a pass rule on the VLAN interface (not WAN - pf evaluates rules on the interface where traffic enters): source = alias, destination = server alias, port = 443
- Place the allow rule above any block-all rule for that interface (rule ordering matters - pf evaluates last match, not first match, so a later block overrides an earlier pass)
- Test:
pfctl -n -f /tmp/rules.debug(OPNsense) to dry-run before applying - Apply:
configctl filter reload(OPNsense) orpfSsh.php playback svc restart filter(pfSense) - Verify:
pfctl -sr | grep <alias>to confirm the rule is active
Creating a block rule (isolate IoT VLAN)
Block IoT devices from reaching internal networks while allowing internet access:
- Identify the IoT VLAN interface (e.g.,
opt3for VLAN 30) - Create an alias for RFC1918 ranges: name
RFC1918, typeNetwork, content10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 - Add a block rule on the IoT interface: source = IoT subnet, destination =
RFC1918alias, action = block. This prevents IoT from reaching any internal network. - Add a pass rule below it: source = IoT subnet, destination = any, ports = 53 (DNS), 443 (HTTPS). This allows internet access for the permitted services.
- Rule order matters: the block rule for RFC1918 must come before the pass rule for any, since pf uses last-match semantics - the block must not be overridden by a later pass.
- Test and apply as above:
pfctl -n -f /tmp/rules.debug, thenconfigctl filter reload
Troubleshooting connectivity after VLAN changes
Work through these steps in order. Do not skip ahead or assume the root cause - each step eliminates one layer. The most common failure is a missing outbound NAT rule, not a firewall rule. Also check CrowdSec (cscli decisions list) and Suricata early - their blocks look identical to firewall drops from the client side.
Prerequisite: the VLAN must be assigned to a firewall interface before it can have rules, DHCP, or NAT. In OPNsense: Interfaces > Assignments > add the VLAN, then enable it and set its IP. In pfSense: Interfaces > Interface Assignments. An unassigned VLAN passes no traffic through the firewall even if the parent trunk is tagged correctly.
- Interface assigned and UP?
ifconfig- is the VLAN interface listed and UP? If not: assign it (see prerequisite above). If listed but DOWN: enable it in the GUI or check the parent interface. - Services running?
configctl service list(OPNsense) orservice -e(pfSense) - confirm DHCP, DNS (Unbound), and the packet filter are running. A stopped DHCP server on the new VLAN means clients never get an IP. - Rules present?
pfctl -sr- any pass rules on the new VLAN interface? New interfaces have no rules by default (deny all). - NAT configured? Check outbound NAT rules include the new VLAN subnet. On OPNsense: Firewall > NAT > Outbound. Missing outbound NAT is the #1 cause of "VLAN can't reach internet."
- DNS working?
drill google.com @<firewall-ip>from a VLAN client. If this fails but ping to 8.8.8.8 works, it's a DNS issue, not a firewall rule. - Packet capture:
tcpdump -ni <vlan-iface> host <client-ip>- are packets arriving at the firewall?- Reading tcpdump output: each line shows
timestamp src > dst: proto. Look for: (a) request packets from the client arriving on the VLAN interface, (b) reply packets going back. If you see requests but no replies, the firewall is blocking or NAT is missing. If you see no packets at all, the issue is below the firewall - check VLAN tagging, trunk config, and switch ports. Use-vfor header details or-Xfor payload hex when deeper inspection is needed.
- Reading tcpdump output: each line shows
- If packets arrive but no response: the rule or NAT is the problem. If no packets: the VLAN trunk, switch tagging, or interface assignment is wrong - check the physical/virtual layer before touching firewall config.
FreeBSD Mental Model
Read references/platform-and-operations.md for the detailed FreeBSD shell model, key commands,
config system, REST API, IPv6 gotchas, SOPs, and recovery procedures.
- Treat both platforms as FreeBSD appliances, not Linux hosts.
- For anything beyond trivial SSH one-liners, prefer piping a POSIX
shheredoc instead of fightingcshortcsh. - Guard non-zero informational commands with
; truewhen running checks in parallel.
Operations and Common Tasks
- Identify the target device explicitly before changing anything.
- Back up config before risky changes or upgrades.
- Check plugin or package layers early because they often explain traffic behavior that looks like a firewall-rule problem.
- Treat firmware, plugin, backup, and HA work as operational procedures, not casual single commands.
- Use
references/plugins.mdfor plugin specifics andreferences/hardening.mdfor hardening and CARP guidance.
Reference Files
references/platform-and-operations.md- FreeBSD shell model, key commands, config system, REST API, IPv6 gotchas, SOPs, and recovery procedures (both platforms)references/plugins.md- operational guidance for common OPNsense plugins (CrowdSec, WireGuard, Suricata, HAProxy, ACME, FRR, etc.). For pfSense package equivalents, map concepts using the platform comparison table above.references/hardening.md- comprehensive hardening checklist. OPNsense-focused but most items apply to pfSense with equivalent settings in its GUI/config.
Output Contract
See skills/_shared/output-contract.md for the full contract.
- Skill name: FIREWALL-APPLIANCE
- Deliverable bucket:
audits - Mode: conditional. When invoked to analyze, review, audit, or improve existing repo content, emit the full contract -- boxed inline header, body summary inline plus per-finding detail in the deliverable file, boxed conclusion, conclusion table -- and write the deliverable to
docs/local/audits/firewall-appliance/<YYYY-MM-DD>-<slug>.md. When invoked to answer a question, teach a concept, build a new artifact, or generate content, respond freely without the contract. - Severity scale:
P0 | P1 | P2 | P3 | info(see shared contract; only used in audit/review mode).
Related Skills
- networking - for Linux reverse proxies, VPNs, DNS, and nftables work outside BSD firewall appliances
- command-prompt - for general shell scripting and local shell behavior; this skill covers the FreeBSD firewall context
- security-audit - for defensive security review of application code and supply chain, rather than firewall administration
- lockpick - for authorized offensive testing and post-exploitation, not defensive firewall operations
- ansible - for fleet-wide firewall automation or playbook-based configuration management
Rules
These exist because bricking a firewall remotely means driving to wherever it is.
- Never modify rules that could lock out SSH access. If the change touches the SSH port or the management interface, triple-check the rule order and confirm with the user.
- Never disable the LAN interface or change its IP without explicit confirmation and a rollback plan.
- Never apply firmware or plugin updates without asking first - updates can reboot the device and may require physical console access if something goes wrong.
- Always confirm destructive changes: rule deletions, service disables, plugin removals,
state table flushes (
pfctl -Fa). - Estimate blast radius: if a change could cause network disruption beyond the target device, warn the user with specifics (e.g., "this will drop all VPN tunnels for ~30s").
- OPNsense CrowdSec: don't delete decisions or bouncers without understanding why they exist. A ban that looks wrong might be catching a real attack.
- pfSense pfBlockerNG: don't disable feed lists without understanding what they block. Review the deny logs before removing feeds.
- HA/CARP (both platforms): never make config changes directly on the backup node - XMLRPC sync from master will overwrite them. Always change on master and let sync propagate.
- pfSense
easyrule: convenient but creates rules without descriptions. Document what you added and why. Consider using the GUI or config.xml for permanent rules instead.