Synopsis #
Restrict administrator access, protect daemon keys, validate firewall changes, and keep systems patched. This chapter combines PF source checks, SSH and doas restrictions, protected secret files, and rollback procedures. Keep console access and a working configuration available before changing a remote system.
Design Considerations #
- Threat model. Protect the management plane first. Restrict administration origins, prefer keys over passwords, and default-deny all east–west traffic not required by policy.
- Spoofing and path symmetry. Enforce source validity with interface-based anti-spoofing and unicast RPF checks.
- Rule safety. Validate and stage PF changes with syntax checks and temporary roll-back plans. Maintain a failsafe anchor that preserves out-of-band access during rollouts.
- Secrets and ownership. Keep keys and PSKs readable by root only. Prefer daemon-provided chroots and privilege separation.
- Patches and change tracking. Apply errata promptly. Track configuration in version control and explain changes in commit messages.
- Logging. Forward logs to another system, configure local rotation, and synchronize clocks before comparing events.
Configuration #
1) Hardened PF skeleton with anti-spoofing, uRPF, and bogon tables #
The example below is a conservative baseline. It assumes em0 is WAN and em1/em2 are inside interfaces. It rejects obviously forged sources and blocks packets that fail a reverse-path check on the receiving interface.
## /etc/pf.conf — hardened baseline with spoofing controls
set skip on lo
# Interfaces
wan = "em0"
inside= "{ em1, em2 }"
# -------------------------
# Tables
# -------------------------
# Commonly spoofed IPv4 sources (adjust as needed; keep current in ops docs)
table <bogons4> persist { \
0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, \
169.254.0.0/16, 172.16.0.0/12, 192.0.2.0/24, 192.168.0.0/16, \
198.18.0.0/15, 198.51.100.0/24, 203.0.113.0/24, 224.0.0.0/4, 240.0.0.0/4 }
table <bogons6> persist { \
::/128, ::1/128, ::ffff:0:0/96, \
100::/64, 2001:db8::/32, fc00::/7, fe80::/10, ff00::/8 }
# Inside networks (adjust to your deployment)
table <inside4> persist { 10.0.0.0/8, 192.168.0.0/16 }
table <inside6> persist { 2001:db8:10::/48 }
# -------------------------
# Normalization
# -------------------------
match in all scrub (no-df)
# -------------------------
# Base policy
# -------------------------
block all
# Permit IPv6 control traffic before quick source checks (including DAD from ::).
pass quick inet6 proto icmp6
# Anti-spoof on all relevant interfaces
antispoof quick for $wan
antispoof quick for $inside
# Reverse-path filtering: drop traffic whose source would not route back on the rx interface
block in quick on $wan from urpf-failed
block in quick on $inside from urpf-failed
# Bogon filtering on WAN
block in quick on $wan inet from <bogons4> to any
block in quick on $wan inet6 from <bogons6> to any
# Permit inside to anywhere (refine per policy; examples below)
pass in on $inside inet from <inside4> to any keep state
pass in on $inside inet6 from <inside6> to any keep state
# Outbound from the firewall itself
pass out on egress proto { tcp udp icmp } keep state
# Example: published HTTPS service; apply synproxy and limits
web_svc = "10.10.10.50"
pass in on $wan proto tcp to ($wan) port 443 \
flags S/SA synproxy state (max-src-conn 50, max-src-conn-rate 30/5) \
rdr-to $web_svc
The IPv6 ICMP exception is needed for link-local Neighbor Discovery. Strict uRPF assumes symmetric routing. Adapt the WAN tables for private upstreams and permit required DHCP client traffic before quick bogon blocks.
Keep the bogon tables current and document any exceptions.
2) Failsafe anchor and safe PF rollouts #
Load a small management anchor before restrictive rules. Limit it to the required administration service and source address.
## /etc/pf.failsafe.conf — allow SSH from a management /32 only
mgmt = "198.51.100.10"
pass in quick on egress proto tcp from $mgmt to (egress) port 22 keep state
Reference it at the top of your main policy:
## Excerpt — top of /etc/pf.conf
anchor "failsafe"
load anchor "failsafe" from "/etc/pf.failsafe.conf"
Staged reload with validation and roll-back timer (local shell on the router):
doas cp -p /etc/pf.conf /etc/pf.conf.rollback
# Save the working policy BEFORE editing /etc/pf.conf
doas pfctl -nf /etc/pf.conf
# Stop here on any error; do not load an invalid policy
doas at now + 5 minutes <<< 'pfctl -f /etc/pf.conf.rollback'
# Roll-back PF to the previous ruleset in 5 minutes (requires the at job service provided by cron(8))
doas pfctl -f /etc/pf.conf
# Load the new rules atomically
doas atq
# Confirm the rollback job is queued
doas atrm <jobid>
# Cancel rollback after you confirm access and function
If at(1) is unavailable, keep a second session open (serial/IPMI/ILO) while loading the new policy and verify end-to-end before closing the out-of-band path.
3) Management-plane access: SSH and doas #
Restrict SSH exposure and disable weak options. Keep privileged escalation explicit and minimal.
sshd(8) hardening in sshd_config(5) :
## /etc/ssh/sshd_config — minimal hardened example
ListenAddress 10.10.10.1
AddressFamily inet
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
KbdInteractiveAuthentication no
ClientAliveInterval 30
ClientAliveCountMax 2
AllowUsers netops
# Optional: restrict source subnets with Match
# Match Address 198.51.100.0/24
# AllowUsers netops
doas sshd -t && doas rcctl reload sshd
# Apply changes without disconnecting existing sessions
doas(1) policy in doas.conf(5) :
## /etc/doas.conf — selected administrative commands
permit persist :wheel as root cmd pfctl
permit persist :wheel as root cmd rcctl
permit persist :wheel as root cmd tail args -f /var/log/messages
# Deny by default; enumerate additional commands as required
Unrestricted rcctl and pfctl access is powerful administrative access. Restrict arguments for narrowly delegated roles; this example is for trusted administrators.
4) Secret file hygiene and daemon posture #
Ensure private keys and PSKs are root-readable only. Many base daemons already chroot and drop privileges; keep defaults intact.
doas chmod 600 /etc/iked.conf
# PSKs and identities for iked(8)
doas sh -c 'chmod 600 /etc/ssl/private/*.key'
# TLS private keys used by httpd(8) and other daemons
doas chown -R root:wheel /etc/ssl/private
doas rcctl check iked
# Confirm daemon status in the current chroot/priv-sep model
5) Patches and Change Tracking #
Apply errata with syspatch(8) . Verify set signatures with signify(1) during upgrades. Plan maintenance windows and reboot when required by the patch notes.
doas syspatch
# Apply available binary patches; review /var/syspatch logs
signify -C -p /etc/signify/openbsd-80-base.pub -x SHA256.sig
# Verify set signatures during upgrades (adjust key/filenames per release notes)
Use version control for
/etc(for example,rc.conf.local,pf.conf,vm.conf,rad.conf) and include a human-readable change rationale with each commit.
Verification #
- PF exposure and counters:
doas pfctl -vvsr | egrep 'failsafe|urpf|antispoof|bogons'
# Confirm presence and order of hardening rules
doas pfctl -s state | wc -l
# State count; watch during change windows
doas pfctl -s Interfaces
# Interface list and status as seen by PF
- Tables and lookups:
doas pfctl -t bogons4 -T show | head
# Verify bogon table content
doas pfctl -t inside4 -T test 192.168.1.10
# Expect "in table" for an inside address
- Management-plane checks:
ssh -o PreferredAuthentications=publickey netops@10.10.10.1 true
# Non-interactive key auth succeeds
ssh -o PreferredAuthentications=password netops@10.10.10.1 true
# Should fail if PasswordAuthentication no
- Patch state:
syspatch -l
# List installed patches
Troubleshooting #
- Locked out after PF reload. Use your out-of-band console. Confirm that the failsafe anchor loads before restrictive rules and that the management source matches the
AllowUsersand PF criteria. If you must recover rapidly, you can disable filtering withpfctl -dfrom the console, fix the policy, and re-enable withpfctl -e. - Legitimate traffic drops on WAN. Inspect
urpf-failedhits withtcpdump -ni $wanwhile correlating PF counters. Return-path failure often indicates asymmetric routing; adjust policy for those flows or remove the strict uRPF rule from the affected interface. - Bogon table blocks intended partners. Documentation/demo prefixes (for example,
192.0.2.0/24) occasionally appear in lab peering. Remove those entries temporarily or maintain a separate<bogons4_prod>table for production. - SSH disconnects during long operations. Use
ClientAliveIntervaland keep-alives; consider running disruptive changes inside atmuxorscreensession over the out-of-band console. - Daemon refuses to read keys. Confirm file mode
600and correct path. Many daemons require keys under/etc/ssl/privatewith root-only permissions. Review daemon logs in/var/log/daemonand service-specific files. - Patch requires reboot. Some
syspatch(8)updates patch the kernel or core libraries. Schedule a short reboot window and verify services come back underrcctl.