Writing a Good pf.conf
pf.conf is one of those config files that's easy to write badly and surprisingly satisfying to write well. The syntax is clean enough that a good pf.conf is almost self-documenting — you can hand it to someone who's never used pf and they can generally figure out what it's doing. Here's how to structure it properly and avoid the common mistakes.
The Golden Rule: Test Before You Load
Always, always run this before reloading pf on a remote machine:
pfctl -nf /etc/pf.conf
The -n flag parses and validates the config without loading it. A syntax error in a freshly loaded pf.conf that blocks all traffic, on a remote box, is a very bad time. If you don't have out-of-band access (console, IPMI, VNC), a bad pf.conf load can mean a trip to the datacenter.
Structure and Order
pf.conf must follow this order — sections that appear out of order will cause errors:
- Macros and variables
- Tables
- Options (
setdirectives) - Normalization (
scrub) - Queueing (if using traffic shaping)
- NAT and redirection (
nat,rdr) - Filtering rules (
pass,block)
Macros: Write Them First, Thank Yourself Later
# Interfaces
ext_if = "em0"
int_if = "em1"
# Networks
lan = "192.168.1.0/24"
dmz = "10.0.0.0/24"
# Services
admin_hosts = "{ 192.168.1.5, 192.168.1.10 }"
web_ports = "{ 80, 443 }"
mail_ports = "{ 25, 587, 993 }"
When your external interface changes from em0 to vtnet0 (it will, eventually), you change one line. When you add a new admin host, you add one IP to one place. Macros pay for themselves.
Tables for Dynamic Lists
# Persistent tables survive pf reloads
table <bruteforce> persist
table <whitelist> persist file "/etc/pf.whitelist"
# Constant tables can't be changed at runtime
table <martians> const { \
0.0.0.0/8, 10.0.0.0/8, 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, 224.0.0.0/3 }
Tables loaded from files (file "/etc/pf.whitelist") are useful for maintaining lists outside pf.conf — you can update the file and reload just the table without touching the ruleset:
pfctl -t whitelist -T replace -f /etc/pf.whitelist
Sensible Defaults
# Don't filter loopback — filtering lo0 traffic causes mysterious failures set skip on lo # Drop blocked packets silently (don't send RST or ICMP unreachable) # Reduces information given to scanners set block-policy drop # Log stats on the external interface set loginterface $ext_if # Normalize incoming traffic scrub in all fragment reassemble
The Ruleset Pattern
pf rules are evaluated in order, and the last matching rule wins (unlike iptables, where the first match wins). quick breaks this — a rule with quick is evaluated immediately when matched, skipping the rest. Use quick for definitive blocks and allows.
# 1. Block everything as the baseline
block all
# 2. Quick blocks — these exit immediately when matched
block in quick from <martians>
block in quick from <bruteforce>
# 3. Allow established states — keeps return traffic flowing
pass in quick on $ext_if proto tcp from any keep state \
(max-src-conn-rate 15/5, overload <bruteforce> flush global)
# 4. Specific service rules
pass in on $ext_if proto tcp to port 22 keep state
pass in on $ext_if proto tcp to port $web_ports keep state
pass in on $ext_if proto icmp all keep state
# 5. Allow everything outbound
pass out on $ext_if all keep state
# 6. Internal network
pass on $int_if all keep state
Logging Rules Selectively
Don't log everything — you'll generate noise that buries real events. Log what's interesting:
# Log SSH attempts for monitoring pass in log on $ext_if proto tcp to port 22 keep state # Log blocked traffic on the external interface block in log on $ext_if all # Log brute-force table additions block in log quick from <bruteforce>
Watch the log in real time:
tcpdump -n -e -ttt -i pflog0
Reloading Without Dropping Connections
pfctl -nf /etc/pf.conf # validate first pfctl -f /etc/pf.conf # reload — existing states are preserved
Reloading pf.conf doesn't drop established connections. Existing state table entries stay alive until they expire naturally or you flush them explicitly with pfctl -F states.
Useful pfctl Commands
pfctl -sr # show current ruleset pfctl -ss # show state table pfctl -ss | wc -l # count active states pfctl -si # statistics (packets, bytes, states) pfctl -t bruteforce -T show # list entries in a table pfctl -t bruteforce -T flush # clear a table pfctl -F states # flush all states (drops connections!)
dispelled