Getting Started with the pf Firewall

Updated on November 21, 2023
Getting Started with the pf Firewall header image

Introduction

pf, short for packet filter, is a commonly used firewall on BSD systems.

The firewall controls the network traffic based on the rules in the configuration file. For each incoming and outgoing packet, the firewall evaluates the filtering rules sequentially (from top to bottom). By default, each packet passes through unless a specific rule in the firewall blocks it. Packets part of an existing connection pass through without being evaluated against the rules. pf is a stateful firewall.

A rule can apply (match) to none, some, or all of the packets. The last matching rule decides whether to allow or block the packet.

Compatibility

This guide has been tested on FreeBSD 13.0-RELEASE. It should be compatible with all recent FreeBSD and OpenBSD versions.

Start and Stop

FreeBSD and OpenBSD both come with pf pre-installed. On OpenBSD, pf is enabled by default. On FreeBSD, you have to enable it before using it.

Enable and Start

Create the Configuration File

By default, pf loads the configuration in the file /etc/pf.conf. Before starting pf, create this file and add to it a single rule:

pass log all

Save the file.

Enable pf

To enable pf, add an entry in the /etc/rc.conf file using the sysrc command:

# sysrc pf_enable="YES"

Check if the pf_enable entry has been added:

# sysrc pf_enable

Start the pf Service (First Time)

After enabling pf in rc.conf, reboot the system, or start the pf service manually:

# service pf start

This will start pf and load the default configuration file /etc/pf.conf.

Check if the pf service is enabled:

# service pf status

Start pf

pfctl is the tool used for managing pf.

Check if pf is running:

# pfctl -s Running

Start pf and load the default configuration file /etc/pf.conf:

# pfctl -e

Start pf and load a specific configuration file:

# pfctl -e -f /path/to/config_file

Stop and Disable

Stop pf

Stop the firewall:

# pfctl -d

You can also stop the pf service :

# service pf stop

Disable pf

To disable pf, update the pf_enable entry in the rc.conf file using the sysrc command:

# sysrc pf_enable="NO"

View Current Ruleset

Use pfctl with the -s' option and the modifier rules` to view the current filter rules:

# pfctl -s rules

To view the current NAT redirection rules, use the modifier nat:

# pfctl -s nat

See a complete overview (excluding the list of tables) of the current setup:

pfctl -s all

View summary information about the overall activity of the firewall:

# pfctl -s info

Loading Rulesets

Load New Ruleset

Load a new configuration when pf is already running:

# pfctl -f /path/to/config_file

Note: Do this every time you change the default configuration file /etc/pf.conf.

Dry-runs

Do a dry-run to check if the configuration is valid:

# pfctl -vnf /path/to_config_file

Errors are shown at the prompt. If the dry run was successful, it shows the complete configuration.

Temporarily Loading Rulesets

Timer Script

Use a shell script with a timer to temporarily load a new configuration file. When the timer expires, the script rolls back the new configuration and reloads the old configuration.

The examples in this section assume that pf is already running with the default configuration file /etc/pf.conf.

Create a new file titled pftimer.sh in your home directory /home/your_user_name. Add to it the lines below:

#!/bin/sh

# load the new configuration
pfctl -f /etc/pf.conf.NEW

# set a timer (in seconds) - as much as you need
sleep 30

# bring back the old working configuration
pfctl -f /etc/pf.conf

Save the file. Change the ownership and permissions of this script such that your user account can write to it, but only root can execute it.

# chown root /home/your_user_name/pftimer.sh

# chmod 766 /home/your_user_name/pftimer.sh

Temporarily Block All Traffic

Create a new configuration file /etc/pf.conf.NEW. Add to it a single line:

block all

Save the file. The above rule blocks all (incoming and outgoing) traffic.

Temporarily enable the new configuration using the timer script:

# /home/your_user_name/pftimer.sh

Note: If you lose access to your own server because of a misconfiguration, use the Vultr Web Console to log back in and fix the settings.

Temporarily Allow All Traffic

Create a new configuration file /etc/pf.conf.NEW with a single rule:

pass all

Save the file and enable it with the timer script to allow all (incoming and outgoing) traffic to pass through temporarily.

Allow Selected Traffic

Allow Only Outgoing Traffic

The simplest configuration is to block all incoming traffic and allow all outgoing traffic:

block in all
pass out all

With this as a starting point, add rules for the incoming traffic you want to allow.

Allow SSH Traffic

To allow SSH connections, allow incoming TCP and UDP traffic over the ssh port:

pass in proto { tcp udp } to port ssh

Allow HTTP(S) Traffic

HTTP and HTTPS traffic go over the TCP protocol. To allow incoming HTTP and HTTPS traffic, add these lines in the configuration file:

pass in proto tcp to port http
pass in proto tcp to port https

Or, write them together:

pass in proto tcp to port { http https }

Allow Specific IPs

To allow a specific IP address (for example, 203.0.113.1) to access the server, add this line to the configuration file:

pass quick from 203.0.113.1

Note: Write rules containing the quick keyword at the beginning of the filtering rules.

Allow Certbot

Certbot needs to communicate with the Let's Encrypt servers via HTTP(S) traffic over the standard ports, so allow outgoing TCP and UDP traffic on ports 53, 80, and 443.

pass out proto { tcp udp } to port { 53 80 443 }

The above rule allows outgoing TCP and UDP traffic on the HTTP and HTTPS ports and on port 53. You may take a more casual approach and allow all outgoing TCP and UDP traffic to pass through:

pass out proto { tcp udp }

Multiple Interfaces

When a system has multiple interfaces, each interface needs separate filtering rules. It is common to filter packets coming in on the external interface but have free movement on all internal interfaces. You might want to redirect certain types of packets to certain interfaces.

The examples in this section are based on the following setup consisting of a Virtual Machine (VM) on a Virtual Private Cloud (VPC):

  • The external (public facing) interface, vtnet0, receives Internet traffic.

  • lo0 is the localhost interface.

  • lo1 is the (virtual internal) interface of the VM.

  • A web-server listening on lo1 handles HTTP(S) traffic.

  • There is free movement on internal interfaces while the public interface is protected.

Adapt the rules and interface names in the examples below based on your own configuration.

Check the list of interfaces available for pf to control:

# pfctl -s Interfaces

Allow All Traffic on Internal Interfaces

pass quick on { lo0 lo1 }

This rule allows traffic on the internal interfaces to pass freely.

Port Forwarding (Redirection)

Forwarding rules redirect requests to different ports and interfaces.

rdr on vtnet0 proto tcp to port http -> lo1 port http
rdr on vtnet0 proto tcp to port https -> lo1 port https

The above rules redirect incoming TCP requests on the HTTP and HTTPS ports on the external interface to the corresponding ports on the internal interface lo1.

Note: In the configuration file, redirection rules and NAT rules must be written before filtering rules (pass and block).

Network Address Translation (NAT)

In the outgoing response from lo1, the source address is the internal network address. But in the response received by the client, the source address needs to be the server's public IP address. NAT rules handle this.

nat on vtnet0 from lo1 to any -> vtnet0

This rule acts on the vtnet0 interface on packets coming from lo1 and going out to any destination. It translates the packet's source address from the (internal) address of lo1 to the (external) address of vtnet0.

Allow Incoming TCP Traffic

Allow incoming TCP traffic to pass through on the external interface.

pass in on vtnet0 proto tcp to lo1 port { http https }

This rule allows incoming TCP packets to pass through to the HTTP(S) port. The earlier rdr rules have already redirected the packets to the lo1 interface.

Logging

Enable logging on pf:

# sysrc pflog_enable="YES"

Reboot the system, or start the logging service manually:

# service pflog start

The logging service uses the pflog0 interface. The default log file is /var/log/pflog. Logs are written in binary format.

Note: By default, no logs are produced. Specify the keyword log in a rule to log traffic matching that rule.

View the logs in real-time using tcpdump with the `-i' option:

# tcpdump -ni pflog0

Use the -r option to view log files:

# tcpdump -nr /var/log/pflog

The -n option in the above commands is to show raw IP addresses.

References

The standard working reference is the FreeBSD manual page. The FreeBSD Handbook has a chapter on Firewalls with explanations and examples.