PacketFilter (pf) is a well known firewall application originally developed by the OpenBSD project. Though the implementation details between the FreeBSD and OpenBSD versions differ each are similar in syntax and common rulesets may be ported between distributions. This tutorial covers buidling a simple ruleset from scratch.
Default deny and default permit are the two approaches to building a firewall. The default deny approach blocks all traffic and permits only traffic specified by a rule. The default permit does the opposite. It allows all traffic and blocks only traffic specified by a rule. This tutorial uses the default deny approach.
Pf rulesets are stored in a configuration file at /etc/pf.conf
. It's okay to store this file elsewhere as long as the location is specified in /etc/rc.conf
.
This tutorial adds rules to control SSH traffic. Before starting ensure that you can access your SDF VPS console. It is recommended to connect to your VPS through the console for this tutorial to avoid inadvertently locking yourself out via SSH.
Begin by taking note of the external interface name of your VPS. This can be found using ifconfig -a
and looking for the interface that contains your EXTERNAL_IP. In the example output below the interface is xn0
.
xn0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500 options=3<RXCSUM,TXCSUM> ether aa:00:00:d1:09:33 inet EXTERNAL_IP netmask 0xffffff00 broadcast 205.166.94.255 media: Ethernet manual status: active nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL>
Edit /etc/pf.conf
and begin by adding a few macros.
ext_if="xn0" # Define the interface name ssh_in="{ 22 }" # Define the inbound service ports svc_out="{ 22 53 80 123 443 }" # Define the outbound service ports icmp_types="{ echoreq unreach }" # Define allowed ICMP types
These macros will be expanded where referenced in the rules. If you have set any services to run on non-standard ports make sure that the port numbers are in the appropriate macro. The services defined in svc_out
are SSH, DNS, HTTP, NTP, and HTTPS respectively. Pf supports use of service names such as SSH, DNS, etc. which map to the service's default port. This tutorial sticks to port numbers for consistency.
Next, add some tables to the config. Tables are similar to macros but are designed to hold groups of IP addresses. The first table is <rfc6809>
which will hold a list of reserved IP addresses. The second is <excess>
which is defined as empty and will be used to hold rate-limited IP addresses.
table <rfc6809> { 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 \ 240.0.0.0/4 255.255.255.255/32 } table <excess>
Set the default return policy and logging.
set block-policy return # Set the default return policy on blocks. This will send a RST message to the connecting host set loginterface $ext_if # Configure logging for the external interface set skip on lo0 # Don't apply any filtering on the loopback interface scrub in all fragment reassemble max-mss 1440 # Reassemble all fragmented packets before processing with a max packet size of 1440
Add the rules as the last part of the config.
# Apply the antispoof directive on $ext_if. The automatic # antispoofing blocks all traffic from the network of that # interface unless it originates from that same interface. antispoof quick for $ext_if # The quick keyword executes a rule immediately without # considering the rest of the ruleset. The egress keyword # automatically finds the default route(s) on a given # interface. block in quick on egress from <rfc6809> block return out quick on egress to <rfc6809> # The default deny policy block all # The inbound SSH rule. This rule allows traffic on # $ext_if to the $ssh_in port, limiting connections # to 15 per-host at a rate of 3 connections per-second # while adding hosts breaking those rules to the # <excess> table. pass in on $ext_if proto tcp to port $ssh_in \ keep state (max-src-conn 15, max-src-conn-rate 3/1, \ overload <excess> flush global) # Allow all TCP and UDP traffic on the $svc_out ports. # This permits communication to the defined services. pass out proto { tcp udp } to port $svc_out # Allow the defined ICMP types pass out inet proto icmp icmp-type $icmp_types
Save the ruleset at /etc/pf.conf
, enable the services, and start the services.
# Enable the services via rc.conf sysrc pf_enable="YES" sysrc pflog_enable="YES" # Start the services service pf start service pflog start # Load the rules pfctl -f /etc/pf.conf
Pf is now started and the ruleset is enabled. For good measure reboot the system as well.
After rebooting, test the firewall by attempting to ping or connect to outside hosts and connect to your VPS via SSH. To see pf stats, run pfctl -si
Over time the <excess>
will fill with IP addresses. It can be inspected by running pfctl -t excess -T show
. The table can be cleared by running pfctl -t excess -T expire 172800
. That will clear all entries that have aged more than 48 hours in the table. If you notice IP addresses appearing in the table frequently, they can be added as quick blocks in the ruleset config. Adding a daily cronjob to clear the script is a good idea. The script can be added to root's crontab or you can put the script in /usr/local/etc/periodic/daily
.
$Id: VPS_FreeBSD_Setup_PF.html,v 1.2 2023/09/11 00:09:18 dnielsen Exp $ Setting up PacketFilter (pf) on FreeBSD - traditional link (using RCS)