aoitcloud

Guides and How Tos

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/to to 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.

Get in touch

Drop our team a message today