FreeBSD pf Configuration for an Internet-Facing Host
Whenever running an internet-facing host, it is a good idea to lock down network traffic incoming to the host, as a security precaution. My main reason for running pf is to counter ssh bruteforce attempts. The following steps outline a simple configuration of the pf firewall.
Note: This has been tested on FreeBSD 14.1.
pf Configuration Preparation
For preparation, check IPv4 and IPv6 public internet connectivity, with ping
. This is to ensure that networking works. If then the network stack is down while pf is running, then pf is the cause of this.
ping -4 -c4 www.freebsd.org
ping -6 -c4 www.freebsd.org
Write pf Configuration File.
The following pf configuration file sets up pf to perform these tasks:
- Allow SSH ingoing traffic (to allow remote logins)
- Allow outgoing traffic for SSH, DNS, NTP, HTTP and HTTPS (this allows for time sync and things like FreeBSD updates)
- Basic fragmented packet assembly and antispoofing
- Blocking of non-routable IP addresses
- Maintain a blacklist of IP addresses which attempt bruteforce ssh logins
doas vim /etc/pf.conf
public_if = "vtnet0"
table <ssh_bruteforce> persist
# not routable address ranges, according to RFC6890
table <not_routable> { 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.0.0/24 192.0.0.0/29 \
192.0.2.0/24 192.88.99.0/24 \
192.168.0.0/16 198.18.0.0/15 \
198.51.100.0/24 203.0.113.0/24 \
240.0.0.0/4 255.255.255.255/32 }
# allow all traffic on loopback interface
set skip on lo0
# assemble fragmented packets
scrub in all fragment reassemble
# Block address spoofing
antispoof quick for $public_if
# Not routable address ranges, should never pass the public interface
block in quick on $public_if from <not_routable>
block return out quick on egress to <not_routable>
# default to block all traffic
block all
# allow SSH in, with brute force scanner protection
pass in on $public_if proto tcp to port 22 \
keep state (max-src-conn 5, max-src-conn-rate 1/5, \
overload <ssh_bruteforce> flush global)
# allow SSH, DNS, HTTP, NTP, HTTPS out
pass out proto { tcp udp } to port { 22 53 80 123 443 }
# allow ICMP in and out
pass inet proto icmp
pass inet6 proto icmp6
Enable pf
The following commands:
- Enables pf and ensure it is started on FreeBSD boot.
- Check the pf configuration file for errors.
- Start the pf firewall.
- Check IPv4 and IPv6 public internet connectivity again.
doas sysrc pf_enable="YES"
doas pfctl -nf /etc/pf.conf
doas service pf start
ping -4 -c4 www.freebsd.org
ping -6 -c4 www.freebsd.org
This should result in a bootable FreeBSD host, with public internet connectivity, protected by the pf firewall.
Done.
pf Maintenance
pf maintenance commands:
- Statistics (show info):
doas pfctl -si
- Check configuration syntax:
doas pfctl -nf /etc/pf.conf
- Load configuration:
doas pfctl -f /etc/pf.conf
- Show SSH bruteforce table
doas pfctl -t ssh_bruteforce -T show
- Expire SSH bruteforce table:
doas pfctl -t ssh_bruteforce -T expire <seconds>
Add a crontab entry to periodically clean the SSH bruteforce table: doas crontab -e
, add @daily /sbin/pfctl -t ssh_bruteforce -T expire 172800
. 172.800 seconds = 60 seconds * 60 minutes * 48 hours, is the duration to keep IP addresses in the table.