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.