Set Up the pf Firewall on FreeBSD 15.1 Without Locking Yourself Out

Updated on Nov 5, 2026
Mila H
8 MINS READ
Table of Contents
Set Up the pf Firewall on FreeBSD

A public server gets scanned within minutes of going online. This guide shows a FreeBSD pf firewall setup for a VPS that you can copy, test, and reload safely over SSH. It uses FreeBSD 15.1 and a small pf.conf that blocks all unwanted inbound traffic and opens only SSH, HTTP, and HTTPS.

This is a rule set for an internet-facing host. It is not a home-router NAT guide, so there is no NAT and no gateway_enable.

Why FreeBSD pf Works Well on a VPS

pf is the packet filter that ships with FreeBSD. It loads as a kernel module when you enable it in rc.conf. FreeBSD does not ship a default ruleset, so pf does nothing until you create /etc/pf.conf. That is why a FreeBSD pf firewall setup for a VPS starts with writing your own file.

pf is stateful. When your server opens a connection, pf remembers it and lets the replies in. Everything else coming in is dropped.

What You Need

For this guide, you need a FreeBSD 15.1 VPS with root access. If you do not have one yet, you can follow our FreeBSD VPS setup guide.

Make sure you have console or VNC access from your provider panel. This is your way back in if you lock yourself out. Also, you need a second SSH session open.

Then, find your network interface name and your SSH port:

Bash
freebsd-versionifconfig -lsockstat -4 -6 -l | grep sshd

On most KVM VPS hosts, the interface is vtnet0. Other hosts may use em0 or igb0. Use your real name in the rules below.

Write a Minimal pf.conf File

The file below is short and easy to read. It blocks all new inbound connections and opens only SSH, HTTP, and HTTPS. Your server can still go out to the internet as normal. Back up any old file, then create the new one:

Bash
[ -f /etc/pf.conf ] && cp -p /etc/pf.conf /etc/pf.conf.baknano /etc/pf.conf

Add the following configuration to the file:

# /etc/pf.conf - public FreeBSD 15.1 hostext_if = "vtnet0" tcp_services  = "{ 22, 80, 443 }"icmp_types    = "{ echoreq, unreach }"icmp6_types   = "{ echoreq, unreach, toobig, timex, paramprob, \                   neighbrsol, neighbradv, routersol, routeradv }" set skip on lo0set block-policy dropset loginterface $ext_if # Clean up incoming packetsmatch in all scrub (no-df) # Default deny for inbound traffic, and log what is blockedblock in log all # Allow all outbound traffic from this serverpass out quick keep state # Allow ping and needed ICMPpass in quick on $ext_if inet proto icmp icmp-type $icmp_typespass in quick on $ext_if inet6 proto icmp6 icmp6-type $icmp6_types # Allow SSH, HTTP and HTTPSpass in on $ext_if proto tcp to port $tcp_services

If you changed your SSH port, replace 22 with the new port. Once you are done, save and close the file. Here is how the rules work:

  • set skip on lo0 skips pf on loopback, so local services keep working.
  • set block-policy drop drops blocked packets silently. Scanners get no reply.
  • match in all scrub (no-df) cleans up incoming packets. It is the current syntax, so use it instead of the old scrub in all on FreeBSD 15.1.
  • block in log all is the default deny. It logs blocked packets, and the pass rules below it open only what you list.
  • pass out quick keep state lets your server reach the internet, for example for pkg and DNS.
  • pf adds flags S/SA and keep state to TCP pass rules by default, so you don't need to type them.
  • Keep the ICMPv6 line. IPv6 needs neighbor discovery to work.

Some VPS hosts give you an IPv4 address through DHCP. If yours does, add this line before the last pass in rule:

pass in quick on $ext_if proto udp from any port 67 to any port 68

Test the Rules Before You Apply Them

Never load a file you have not checked. This command reads the file and prints the final rules, but does not apply them:

Bash
pfctl -vnf /etc/pf.conf

If it prints your expanded rules and no errors, the syntax is fine. Any error shows the line number. Before you proceed, fix them. Checking a file this way is the most important step in any FreeBSD pf firewall setup for a VPS.

Enable pf and Logging at Boot

At this point, you must set pf to start when the server boots. Also, you must turn on logging, so pf saves blocked packets to a file. Use sysrc to edit /etc/rc.conf safely:

Bash
sysrc pf_enable="YES"sysrc pf_rules="/etc/pf.conf"sysrc pflog_enable="YES"sysrc pflog_logfile="/var/log/pflog"

pflog_enable starts pflogd, which saves logged packets to /var/log/pflog. Don't start pf yet. The first start is the riskiest step, so read the next section first.

Reload Rules Safely From a Remote Shell

A bad rule can cut your SSH session. This method gives you a timed rollback. If you lose access, the old rules return on their own. First, copy your rules to a safe file:

Bash
cp -p /etc/pf.conf /etc/pf.conf.good

Before the first start, set a timer. It turns pf off after 120 seconds unless you cancel it:

Bash
nohup sh -c 'sleep 120; pfctl -d' >/dev/null 2>&1 &echo $! > /tmp/pf-rollback.pid

Then, you can start pf from your session:

Bash
service pf startservice pflog start

Open a new SSH window and log in. If it works, cancel the timer in your first session:

Bash
kill $(cat /tmp/pf-rollback.pid)

For later edits, use the same timer. This time it restores your copied file instead of turning pf off:

Bash
pfctl -vnf /etc/pf.confnohup sh -c 'sleep 120; pfctl -f /etc/pf.conf.good' >/dev/null 2>&1 &echo $! > /tmp/pf-rollback.pidpfctl -f /etc/pf.conf

Now open a new SSH login to test. If it works, cancel the timer and save the new file as your copy:

Bash
kill $(cat /tmp/pf-rollback.pid)cp -p /etc/pf.conf /etc/pf.conf.good

Do not use pfctl -F all -f for a normal reload. Flushing the state table can drop your live SSH session. A plain pfctl -f /etc/pf.conf reloads the rules and keeps existing states. Safe reloads are what make a FreeBSD pf firewall setup for a VPS practical on a remote server.

Check That the Rules Work

Now check that pf is running and your rules are loaded. These three commands show the status, the rules, and the active connections.

Bash
pfctl -sipfctl -srpfctl -ss

From another machine, test the open and closed ports with the command below:

Bash
nmap -Pn -p 22,80,443,3306 YOUR_SERVER_IP

Ports 22, 80, and 443 should show as open only if a service listens on them. Port 3306 should show as filtered.

Read Logs and Capture Packets

Logs and packet captures show you what pf blocks and what reaches your server. Use them to find out why a connection fails. The log keyword in your rules sends blocked packets to the pflog0 interface, which you can watch live.

Bash
tcpdump -n -e -ttt -i pflog0

Read the saved log file with the command below:

Bash
tcpdump -n -e -ttt -r /var/log/pflog

To cut the noise, you can filter the output by a port or an IP address:

Bash
tcpdump -n -e -ttt -r /var/log/pflog port 22tcpdump -n -e -ttt -i pflog0 host 203.0.113.10

To see real traffic on the network card, you can capture on the main interface instead:

Bash
tcpdump -ni vtnet0 'tcp port 443'tcpdump -ni vtnet0 'tcp port 22 and host 203.0.113.10'

Capture on pflog0 to see what pf blocked. Capture on vtnet0 to see what arrives from the network. If a packet shows on vtnet0 but not in the log, a rule passed it, or it was not logged.

How to Recover From a pf Lockout

A bad rule can block your own SSH login. You can use your provider's console or VNC, which does not need SSH, to turn pf off and fix the file. Log in as root and run:

Bash
pfctl -d

This turns pf off immediately, so you can fix the file. Then, check and reload:

Bash
cp -p /etc/pf.conf.good /etc/pf.confpfctl -vnf /etc/pf.confpfctl -epfctl -f /etc/pf.conf

If pf blocks you at every boot, boot into single-user mode and mount the disk read-write:

Bash
mount -u /mount -a -t ufssysrc pf_enable="NO"reboot

If your VPS uses ZFS, run zfs mount -a instead of the two mount commands. Fix the rules after you regain access, then set pf_enable="YES" again.

Rate-Limit SSH in Your pf Firewall

Bots try to guess SSH passwords all day. You can slow them down with a rate limit. If an IP opens too many connections too fast, pf adds it to a block list. Add the table and rules below before your pass in line:

table <bruteforce> persistblock quick from <bruteforce>pass in on $ext_if proto tcp to port 22 \    keep state (max-src-conn 10, max-src-conn-rate 5/30, \    overload <bruteforce> flush global)

Remove 22 from tcp_services so SSH uses this stricter rule. Check the file with pfctl -vnf before loading it. Be careful with this rule if many users share one office IP.

If you need more power or full control, a reliable dedicated server gives you the whole machine. No one else shares it, and your pf rules are the only firewall on it.

Conclusion

At this point, you have a working FreeBSD pf firewall setup for a VPS. It denies unwanted inbound traffic, opens SSH, HTTP, and HTTPS, logs blocked packets, and reloads with a safe rollback. Always run pfctl -vnf, keep a copy file, and keep a console login ready.

If you are running both FreeBSD and Linux, you can review the NVMe Linux VPS hosting options for your Linux workloads.

We hope you enjoy this guide. For NAT, queues, and other advanced rules, you can check the FreeBSD pf documentation.

Yes. pf is part of the base system. You only need to enable it in rc.conf and write /etc/pf.conf.

You can easily run pfctl -vnf /etc/pf.conf.

No. It keeps existing states. Flushing with pfctl -F all can drop it.

FreeBSD does not ship one. Example files are in /usr/share/examples/pf/.