pf firewall basics on OpenBSD
pf, OpenBSD's native packet filter (later ported to FreeBSD and macOS), is configured in /etc/pf.conf with a compact, readable rule syntax - here's a working default-deny example.
31 January 2025 4 min read
pf – the Packet Filter – is OpenBSD’s own firewall, originally written for OpenBSD and later ported to FreeBSD, macOS, and other systems. If you’re coming from Linux, forget iptables or nftables syntax entirely; pf’s rule language is its own thing, and it’s considerably more readable once you know the shape of it.
pf is already running
Since OpenBSD 4.6, pf has been enabled by default in the standard install – it’s just running with a very permissive default ruleset. Your job isn’t to turn the firewall “on” so much as to replace its rules with something that actually restricts traffic. You can check whether it’s currently enabled with:
$ doas pfctl -s info
The rule file: /etc/pf.conf
Rules live in /etc/pf.conf. The basic building blocks of a rule are:
- block / pass – deny or allow the matching traffic.
- in / out – direction of the traffic relative to the interface.
- on interface – which network interface the rule applies to.
- proto – protocol, e.g.
tcp,udp,icmp. - port – used with
from/toto match a source or destination port.
An important quirk: unlike many firewall syntaxes, pf evaluates rules top to bottom and the last matching rule wins – not the first. If you want a rule to take effect immediately and stop further evaluation, add the quick keyword to it.
A working example: default-deny with SSH, HTTP and HTTPS
Here’s a straightforward ruleset for a VPS acting as a web server that you also administer over SSH. Adjust egress or the interface name to match your setup – on most VPS images you can find your interface name with ifconfig.
# macros
ext_if = "vio0"
# skip filtering on loopback
set skip on lo
# normalize and scrub incoming packets
match in all scrub (no-df)
# default deny
block in log all
block out all
# allow all outbound traffic initiated from this host
pass out quick on $ext_if all keep state
# allow ICMP (ping) in, useful for diagnostics
pass in quick on $ext_if inet proto icmp all
# allow SSH, HTTP, HTTPS in
pass in quick on $ext_if proto tcp to port { 22, 80, 443 } keep state
This is a reasonable starting point: everything inbound is blocked except ICMP and the three ports you actually need, everything outbound initiated by the box itself is allowed, and blocked inbound packets are logged so you can review what’s being turned away. keep state lets pf track the connection so return traffic is permitted automatically – you don’t need a separate rule for the response packets.
If you’re managing SSH on a non-standard port, or want to restrict SSH to specific source addresses, adjust the SSH line accordingly, for example:
pass in quick on $ext_if proto tcp from 203.0.113.10 to port 22 keep state
Loading and checking rules
Before loading a new ruleset, check it parses cleanly without actually applying it:
# doas pfctl -nf /etc/pf.conf
If that comes back clean, load it for real:
# doas pfctl -f /etc/pf.conf
To see the rules currently active in the kernel (as opposed to what’s written in the file – useful for confirming your load actually took effect):
$ doas pfctl -s rules
To watch blocked traffic as it’s logged in real time, pf writes to the pflog0 interface, readable with tcpdump:
$ doas tcpdump -n -e -i pflog0
Making sure it survives a reboot
As noted above, pf is enabled by default on a standard OpenBSD install, so in most cases you don’t need to do anything extra – your custom /etc/pf.conf will be loaded automatically at boot via rc. If pf has ever been disabled on your system, re-enable it with:
# rcctl enable pf
On older OpenBSD systems you may instead see pf=YES set in /etc/rc.conf.local – that’s the historical mechanism and still works, but modern installs manage this through rcctl instead. Either way, confirm it’s enabled with:
$ rcctl get pf status
One last habit worth building: whenever you edit /etc/pf.conf, always run pfctl -nf first. A single mistyped rule on a remote VPS with no other access path can lock you out of your own server – the parse-check step catches errors before they take effect.