Mastering pf (Packet Filter), OpenBSD's built-in firewall: reading and arranging /etc/pf.conf, understanding block and pass rulesets, writing service rules for SSH, HTTP, and HTTPS, enabling logging via pflog, and validating the configuration safely with pfctl.

In episode 11 you secured data with backups. Now we've reached the main reason many people use OpenBSD: pf, or Packet Filter. pf isn't just a firewall — it's the heart of OpenBSD networking, the same tool that handles filtering, NAT, load balancing, and logging.
Linux systems have iptables/nftables, which often feels messy. pf feels different: its syntax is concise, reads like a language, and its default is deny. The first rule to internalize: whatever isn't passed is blocked. Security starts from deny, not from allow.
pf is a regular service managed with rcctl:
rcctl enable pf
rcctl start pf
rcctl status pfAt boot, pf reads /etc/pf.conf. If the file doesn't exist, pf stays inactive — so the first step is to make sure the service is active and the ruleset is loaded.
/etc/pf.conf contains several important sections. Here's a minimal, safe example:
table <bruteforce> persist
set skip on lo
block return in all
block out all
pass in on egress proto tcp to port 22
pass in on egress proto tcp to port 80
pass in on egress proto tcp to port 443
pass out inet proto tcp to port 53
pass out inet proto udp to port 53set skip on lo — loopback traffic is not filtered.block return in all and block out all — deny everything by default, return sends RST/ICMP.pass rules open gaps explicitly.Rules are evaluated top to bottom, and the first matching rule wins. That's why block must come before more specific pass rules.
pf.conf is structured into several sections:
if="em0" and web_port="{80 443}" to keep the ruleset readable.set block-policy and set skip.block/pass rules.Example with macros:
int_if = "em0"
pass in on $int_if proto tcp from $int_if:network to any port ssh
pass in on $int_if proto tcp to any port httpMacros keep your ruleset easy to maintain and read — essential once the ruleset grows to hundreds of lines.
Every service you want to open needs a clear pass rule. The basic pattern:
pass in on egress proto tcp to port { ssh http https }
pass in on egress proto tcp to port smtp
pass in on egress proto { tcp udp } to port domainssh, http, https are service names pf recognizes from /etc/services.Don't open ports you aren't running. Every open port is an attack surface — the secure by default principle has applied from episode 1 to here.
The scrub section cleans packets of fragmentation and anomalies — a defense against various evasion attacks:
match in all scrub (no-df max-mss 1440)In modern pf versions, match replaces the old scrub. Normalization is applied to all incoming packets. This is a cheap defense layer with a big impact.
To log blocked packets, add the log keyword:
block return log in all
pass in log on egress proto tcp to port 22pf logs are captured by the pflog0 device and read with tcpdump:
tcpdump -n -e -ttt -i pflog0
tail -f /var/log/pflog/var/log/pflog is the binary pflog file written by pflogd. Analyzing this log is the main way to know what attacks are being attempted against you.
Warning
Never reload a ruleset that hasn't been validated. Always run pfctl -nf /etc/pf.conf before rcctl reload pf — a syntax error in pf.conf will stop pf from loading the ruleset and traffic could come to a complete halt.
pfctl is the main tool for loading, validating, and inspecting rulesets:
pfctl -nf /etc/pf.conf
pfctl -f /etc/pf.conf
pfctl -s rules
pfctl -s states
pfctl -s info-nf validates without loading (a safe draft).-f loads the ruleset.-s rules shows the active ruleset.-s states shows the tracked connection states.-s info shows pf statistics.The -nf then -f then -s rules combination is the daily work cycle of a pf administrator.
When changing a ruleset remotely, there's a classic danger: write a wrong rule, and your SSH connection is cut forever. The safe practice is to test first with a ruleset that keeps SSH open, then load carefully:
pfctl -nf /etc/pf.conf && pfctl -f /etc/pf.confThe && operator ensures the ruleset is only loaded if valid. For big changes, consider adding pass in on egress proto tcp to port 22 at the very top of your ruleset — your SSH connection will never be cut.
In episode 12 you mastered the pf foundation: enabling it with rcctl, reading the structure of /etc/pf.conf, understanding block/pass rulesets evaluated top to bottom, writing service rules for SSH/HTTP/HTTPS, normalizing with match ... scrub, logging via pflog, and validating with pfctl.
Key takeaways:
block before pass.pfctl -nf before loading, and make sure SSH is always open.In the next episode, episode 13, we'll level up: advanced pf — outbound NAT and redirects (rdr), tables with persist, anchors for modularity, and dynamic rules with state tracking.