FreeBSD SSH port security 2

From JonDonym Wiki
Jump to: navigation, search

JonDos GmbH thanks for the right to publish the following article. We made some changes in it but the original text was provided by

navigation: Main Page | FreeBSD and Jails

Setting up the PF packet filter

PF is „on board“ with each FreeBSD system and already installed as a standard feature when you set up a host. But it needs to get activated and configured before you can use it.

Make sure you're on your host system, please. All PF stuff will only get installed on the host system. No work needs to be done within any of the jails. Become root by


First thing to do is to load the PF kernel modules. To load those modules while the host system is already up just enter:

kldload pf
kldload pflog

Then check whether the modules are loaded indeed by entering:


Both modules should get listed (among others).

By the way, you do not need the pflog module in any case as it's needed only for logging of PF activities. But loading the „pf“ module is a must have for this setup.

As you almost certainly want the modules get automatically loaded at boot time you also should open the kernel modules file and add entries for both modules.

vi /boot/loader.conf

Then add two lines there:


Loading the kernel modules (no matter whether manually or during bootup) does not mean that the PF packet filter is already active. There's also a daemon needed to get it started.

You can make PF starting automatically at boot time by means of additional entries in the /etc/rc.conf file.

vi /etc/rc.conf

Add two lines there (no matter where, just two new lines):


Next, you need two new files in the /etc directory. pf.conf is the file which will contain the firewall ruleset. pf.table.fail2ban is the file collecting the banned IP addresses from PF and fail2ban.

touch /etc/pf.conf
touch /etc/pf.table.fail2ban

Ensure, both files will be writeable only by root.

chmod 600 /etc/pf.conf 
chmod 600 /etc/pf.table.fail2ban

There, by the way, is another pf file located in /etc – the pf.os file. You do not need to do anything there. pf.os is for SYN signatures (we will not work with these). Just leave it as it is.

Setting up a firewall on a remote host is always a bit risky as you could block yourself by accident. So, better to prevent those sort of accidents by doing a little trick. Add a line to your /etc/crontab file which will switch off the PF firewall every 5 minutes.

vi /etc/crontab

Add a line containing the following entry:

0/5	*	*	*	*	root	/sbin/pfctl -d 2>/dev/null

So, in case you blocked yourself just look at your watch to see when you will be able to SSH again into your host system (assuming your watch and the hosts system clock are synchronized).

Short introduction on how to control PF

The control program for the packet filter is /sbin/pfctl (PF control).

You can activate (enable) PF by

pfctl -e

or alternatively by

/etc/rc.d/pf start

You can de-activate (disable) PF by

pfctl -d

or alternatively by

/etc/rc.d/pf stop

Leaving path/filename away when using pfctl means, the default path/filename (/etc/pf.conf) will be used. Or in other words – giving a path/filename with the -f option allows you to chose a different file than /etc/pf.conf. Of course, it otherwise doesn't hurt to use the -f option with the default path/filename.

To test the ruleset in pf.conf for syntax errors without actually loading the ruleset:

pfctl -nf /etc/pf.conf

Same as before but with verbose output:

pfctl -nvf /etc/pf.conf

To actually load a ruleset (not yet with enabling PF):

pfctl -f /etc/pf.conf

Using PF packet filter

We now can enable PF. Of course, it will not show any effect as the PF ruleset isn't present this time (the /etc/pf.conf file is still empty) but we nonetheless can test things as done so far.


pfctl -e

The output will be:

No ALTQ support in kernel
ALTQ related functions disabled
pf enabled

Don't worry about the two error message lines on ALTQ. This isn't something we need for our purposes.

Then switch it off again by entering:

pfctl -d
No ALTQ support in kernel
ALTQ related functions disabled
pf disabled

Everything seems okay so far.

We now need to insert the ruleset to the /etc/pf.conf file. You may checkout a standard ruleset file for PF from the JonDos svn repository. This file is a lean but fully functional skeleton. You only need to adapt it to your host's individual environment.

Download the file pf.conf from the repository, save it in the /etc/pf.conf (overwriting the old and empty pf.conf there). Then open it with your editor as you will need to adapt some of the lines.

Adaptions in the MACROS section of the file:

Find the line


Replace the string „em0“ by the device name of the ethernet adapter on your host. If the device name is „myk0“ then change the line to


If you are not sure about the device name use „ifconfig“ to check the device name. If there's more than one adapter, use the one (and the IP address) assigned to the SSH server daemon.

Now, find the line


Replace the example IP by the generic IP of your host (the one, SSHD will listen to).

In case your hosts SSHD will listen on port 22, nothing needs to be changed in the line

bfports=“{ 22 }“

Otherwise replace „22“ by the port number your SSHD listens to.

Adaptions in the TABLES section:

Find the line

table <myhosts> …

Replace the IP addresses between the brackets by those IPs from which you want to access your host without the PF ruleset being applied.

Done. No further adaptions are needed. Doublecheck your changes, then save the file and exit.

Now we'll test the firewall again, but this time with the real ruleset in effect. As you will almost probably by time adapt the ruleset, here's a method of careful testing (avoiding full risk of blocking yourself by accident):

First, let PF check the ruleset without actually loading the ruleset:

pfctl -nvf /etc/pf.conf

If your pf.conf is free of syntax errors, pfctl will output the ruleset, resolving the macros. If there are syntax errors, it otherwise will complain about them and you first have to fix them.

Assuming there are no errors, we'll go ahead – this time loading the ruleset:

pfctl -vf /etc/pf.conf

The output will now be the same as before but the two lines with error messages about ALTQ will be displayed first. But despite of the loaded ruleset, PF still does not work.

To start PF, using the already loaded ruleset, enter:

pfctl -e

PF is active now. You might experience, that your SSH connection gets stuck for a short moment (through enabling PF) but usually it will recover at once and you can go ahead with your SSH session. In worst case, you get thrown out and need to re-ssh onto your host (which will be possible without further problems as long as the ruleset was adapted properly).

In case you cannot log in via SSH you almost probably blocked yourself by accident (through some sort of misconfiguration). You then have to look at your watch and wait until the cronjob will switch off the firewall and let you log in again. You then have to find and correct the failure, enable PF again and see what happens then.

In case your original SSH session was not interrupted, nonetheless try to log in via a second SSH session to see whether it is still possible. Best is to try that two times – one from an IP included in your exception list and another one not included.

Back in your (first) SSH session, you now may do some further investigations and testing on PF.

The next command will inform you on whether or not PF is running (enabled).

pfctl -s info

Something like „Status: Enabled for 0 days 00:01:08“ will tell you that PF is working since one minute and 8 seconds.

To see the currently active ruleset:

pfctl -sr

To list the current content of the fail2ban table in memory:

pfctl -t fail2ban -T show

To write out the table in memory to a disk file:

pfctl -t fail2ban -T show > /etc/pf.table.fail2ban

Same, with eliminating error messages:

pfctl -t fail2ban -T show > /etc/pf.table.fail2ban  2>/dev/null

To manually add a certain IP address to the table:

pfctl -t fail2ban -T add

You might use the latter to test whether you get blocked when trying to log in from a certain machine.

To manually add a complete subnet (written in CIDR) to the table:

pfctl -t fail2ban -T add

To delete a certain IP address from the table:

pfctl -t fail2ban -T delete

To load the IP list from the disk file into the table in memory:

pfctl -t fail2ban -T replace -f /etc/pf.table.fail2ban

As soon as you are sure your ruleset works fine and you feel comfortable with PF you can do the last steps.

We will now ensure that all caught IP addresses will not only be persistent (in memory) while the host system runs. We also want PF to remember them after rebooting. To ensure the latter, we will write the table containing all the banned IPs to the /etc/pf.table.fail2ban file. CRON can do that for us every minute so almost probably none of the banned IPs will get lost.

Open again /etc/crontab

vi /etc/crontab

and add another line there: 0/1 * * * * root pfctl -t fail2ban -T show >/etc/pf.table.fail2ban 2>/dev/null

This will overwrite the then existent version of /etc/pf.table.fail2ban each minute by the then content of PFs table of banned IPs in memory (which we named „fail2ban“ as it will be served by the fail2ban program which we are discussing later on). Additionally, banned IPs can only get lost if an additional IP gets caught with your host crashing before the table was written to disk by CRON once more. But if this IP is a stubborn attacker, he/she will try again after the host recovered and then gets caught once more very quickly.

Finally, after you're done playing around, ensure PF goes on working with the right ruleset:

Stop PF by

pfctl -d

Edit the cronfile:

vi /etc/crontab

and comment or change the line we inserted earlier to switch off PF every 5 minutes.

Load the „production version“ of the ruleset again from /etc/pf.conf:

pfctl -vf /etc/pf.conf

Doublecheck the loaded ruleset:

pfctl -sr

Enable PF again:

pfctl -e

Check whether there are any IPs in the fail2ban table in memory:

pfctl -t fail2ban -T show

In case there are any IPs in the table left, you might have added earlier for testing, delete them by:

pfctl -t fail2ban -T delete

Check also the file version of the table by:

cat /etc/pf.table.fail2ban

Done. PF is working now with the appropriate ruleset. The table in memory is empty as well as the file version /etc/pf.table.fail2ban. It's now awaiting the first IP to block because of an brute force trial against your SSH server.

We next will add the fail2ban python script, collaborating with PF.

Personal tools