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:
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:
[ -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:
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:
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:
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:
nohup sh -c 'sleep 120; pfctl -d' >/dev/null 2>&1 &echo $! > /tmp/pf-rollback.pid
Then, you can start pf from your session:
service pf startservice pflog start
Open a new SSH window and log in. If it works, cancel the timer in your first session:
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:
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:
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.
pfctl -sipfctl -srpfctl -ss
From another machine, test the open and closed ports with the command below:
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.
tcpdump -n -e -ttt -i pflog0
Read the saved log file with the command below:
tcpdump -n -e -ttt -r /var/log/pflog
To cut the noise, you can filter the output by a port or an IP address:
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:
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:
This turns pf off immediately, so you can fix the file. Then, check and reload:
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:
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.