Skip to main content

Don't touch me there: An aggressive pf(4) filtering setup

If some unknown person came up to your house and started trying to open each window, jiggling each door handle, and punching in random codes on your garage-door opener, you could safely assume that they wanted trouble and that you should stop them. Similarly, if some unknown computer on the internet started probing at ports on your server where you offer no services, it should trip alarm-bells and you should cut off this miscreant from interacting with your server. Especially SSH access. You know what services your machine provides. If anybody attempts to connect to some other service on your machine, in all likelihood they don't have your best interests in mind.

In this post, we configure pf on OpenBSD to catch inappropriate connection-attempts and block the offender.


This post assumes a few things about you:

OS installed that can run pf
While I wrote this article using pf on OpenBSD, many of the aspects should translate to FreeBSD even though FreeBSD's version of pf represents an older snapshot from when their codebases diverged. Particularly, I found that FreeBSD's pf lacks the match keyword. Substituting pass for match might make those work, but I haven't tested that.
Administrative rights
This should go without saying, but if you don't administer the machine, you can't modify the firewall configuration.
Basic CLI
In this case, we have minimal work to do at the command-line but it helps to know your way around. OpenBSD has a template /etc/examples/pf.conf you might want to copy as a starting-point; you might find it easier to cd into /etc directory; or you might use tmux and ssh to access the server. Note that editing pf.conf can leave your server inaccessible (like cutting off the tree-branch while sitting on it), so you'll want to make sure that you have console access — locally, via a serial connection/KVM, or VNC connection.
Basic networking knowledge
The basic networking ideas of UDP, TCP, ports, services, etc. If you need to get up to speed here you might want a resource like Michael W. Lucas's Networking for System Administrators, O'Reilly's TCP/IP Network Administration, or some other such book.
Edit text-files
This focuses primarily on modifying your pf.conf which you can edit them with your favorite $EDITOR, whether nano, emacs/mg, vi/vim/neovim, ed, cat, a magnetized needle and a steady hand, or butterflies. But this guide assumes that you can edit text files and save them.


For purposes here, I'll use OpenBSD's doas command to elevate privileges but you can use sudo if you have it installed on FreeBSD or execute these commands under a su - to root if you like to live dangerously. This assumes that doas (or sudo) has a configuration that lets you run the commands in question.

This also assumes starting with an empty (or mostly-empty if you did doas cp /etc/examples/pf.conf /etc/ ) pf.conf file. If you already have an existing configuration, you'll need to work out how these changes integrate with your existing setup.


This post covers several tricks to protect the actual SSH port.

  1. change port on which SSH listens
  2. flag any IP addresses that touch our server's minefield and prevent them from accessing SSH
  3. if anybody actually finds the SSH port and hammers on it, ban them too
  4. limit the country/countries from which we can SSH

Change the sshd port number

For purposes of this post, I configure sshd to listen on port 2345 but you should choose a different one that suits you. This doesn't give much in the way of security, but it reduces some of the noise in your log-files and makes the minefield work by making it unknown and surrounded by trip-ports.

Change the listening port

Edit your /etc/ssh/sshd_config file and specify the Port by adding/editing the line to read

Port 2345
Save the file and restart sshd
doas rcctl restart sshd
Note: If you make this configuration change while SSHed into the server, you might cut off your access. As mentioned, make sure that you have console, serial, or VNC access in case you need to recover from making an error here.

Change the default connectin port

On your local machine, I find it painful to remember how to specify the port to various ssh commands. Some take -p $PORTNUM, some take -P $PORTNUM, and yet others take -o Port=$PORTNUM. To simplify this, edit (or create) your ~/.ssh/config to specify the port-number there

host example
    Port 2345
Now ssh, scp, sftp, rsync, etc, should all default to the right port-number without you needing to specify it explicitly.

Creating a minefield

Rate-limiting connections

Limiting by country

To begin with, I know that I will almost certainly never connect to SSH from outside the United States. provides regularly-updated lists of IP-ranges for each country that I can use as a rough determination of country-of-origin. Yes, this can get thrown off by VPNs but it cuts off a surprising number of attackers.

Create a _geoip user

To limit attack surface, I create a _geoip user to perform the downloading cron task nightly. I put this user in the "daemon" login-class, specify an empty password, and then disable password logins

doas adduser _geoip
Use option ``-silent'' if you don't want to see all warnings and questions.

Reading /etc/shells
Check /etc/master.passwd
Check /etc/group

Ok, let's go.
Don't worry about mistakes. There will be a chance later to correct any input.
Enter username []: _geoip
Enter full name []: GeoIP downloader
Enter shell csh git-shell ksh nologin sh [ksh]:        
Uid [1002]: 
Login group _geoip [_geoip]: 
Login group is ``_geoip''. Invite _geoip into other groups: guest no 
Login class authpf bgpd daemon default pbuild staff unbound 
[default]: daemon
Enter password []: 
Disable password logins for the user? (y/n) [n]: y

Name:        _geoip
Password:    ****
Fullname:    GeoIP downloader
Uid:         1002
Gid:         1002 (_geoip)
Groups:      _geoip 
Login Class: daemon
HOME:        /home/_geoip
Shell:       /bin/ksh

Create a script to download & install GeoIP info

First, create the folders that pf will look in to find the GeoIP tables. Create a tmp/ directory within so that we can download there, compare the results with what we already have, and move the new data atop the old atomically:

doas mkdir -p "$DEST/tmp"
doas chown _geoip:_geoip "$DEST{,/tmp}"
doas chmod 755 "$DEST"
doas chmod 750 "$DEST/tmp"
I switch to the _geoip user and create the script to use
doas su - _geoip
mkdir -p ~/bin
chmod +x
I then used my $EDITOR to populate that file with the following script:

For FreeBSD,
you would use
instead of
and adjust the parameters accordingly.

#TODO cron job

#TODO configure doas/sudo to let _geoip restart pf

For the first

NOTES: - use `overload` to put offenders in a table - need to know valid ports - using geo-blocking - while can use for UDP, can spoof the sender, so best to stick to TCP - has country information OpenBSD port range: sysctl net.inet.ip.port{,hi}{first,last} FreeBSD port range: sysctl net.inet.ip | grep -e first -e last