Integrating PF with Fail2ban 0.9

2011-03-20 20:27:04 by chort

Many security practitioners are familiar with Fail2ban, an application that scans log files for various types of suspicious failures and bans the source IP after too many attempts. Most users implement it to protect their Linux systems (via Netfilter/iptables and TCP wrappers), but it also includes methods for Sendmail and IPFW (FreeBSD and OSX).

What is notably missing from the above list is the wildly popular PF (Packet Filter). It was originally designed by Daniel Hartmeier to replace IPF in OpenBSD, but has since been adopted by FreeBSD, NetBSD, and DragonflyBSD. PF is widely embraced due to the simplicity and clarity of the syntax, and the comprehensive array of professional-grade features available.

Ironically, PF is probably better known now due to FreeBSD than the originating project, OpenBSD. It's somewhat startling that no one has yet included PF support in Fail2ban. It's also disappointing that Apple hasn't switch from IPFW to PF as their packet filtering firewall (hint hint).

In the spirit of the Open Source "submit a patch or GTFO" mentality, here's how you can use Fail2ban to insert rules into your PF firewall.

This example is using OpenBSD, but it's a very similar process for any of the other *BSDs. The only likely difference is the package manager. Before I could install Fail2ban, I needed Python and optionally Gamin. I installed them thusly (this assumes you have PKG_PATH set):

$ sudo pkg_add -iv python-2.6
$ sudo pkg_add -iv gamin

I grabbed the lastest snapshot of Fail2ban from here. Then, followed these steps to uncompress it and install the scripts:

$ ftp ./
$ tar xjf fail2ban-trunk.tar.bz2
$ cd fail2ban-0.9-SVN
$ sudo python install

Now I had a working Fail2ban installation. All I had to do was teach it how to use PF. One of the brilliant features of PF is the ability to use anchors. These are call-outs in your firewall ruleset that allow dynamic loading of rules at that point. Incidentally this is how Authpf is implemented to allow creation of firewall rules based on successful ssh authentication.

There are several ways to create a rule anchor for PF. The method I choose was to insert the anchor statement in my main firewall ruleset, but have the anchor rules defined in a separate file in the fail2ban directory. Here's a simplified version of my main (/etc/pf.conf) ruleset (this is on my webserver, not my gateway):

# /etc/pf.conf

# don't filter packets on loopback interface
set skip on lo

# statistics counter will be tied to this interface
set loginterface gem0

# filter rules and anchor for ftp-proxy(8)
#anchor "ftp-proxy/*"
#pass in quick proto tcp to port ftp rdr-to port 8021

# anchor for relayd(8)
#anchor "relayd/*"

# anchor for fail2ban pwnage
anchor fail2ban

pass            # to establish keep-state

# By default, do not permit remote connections to X11
block in on ! lo0 proto tcp to port 6000:6010

So I have an anchor called "fail2ban", now I need to define what rules get loaded there. I don't want to have to add separate rules for each address that is blocked, because managing the order of those rules is messy. Fortunately PF has a concept called tables, which is an object that stores multiple addresses or CIDR blocks. You can use a table as a source or destination in a rule, which means that you only have to write a single rule to block banned hosts. The banning and unbanning of hosts is done by adding addresses to, and deleting them from, the table. Here's what my anchor file (/etc/fail2ban/pf-anchor.conf) looks like:

# /etc/fail2ban/pf-anchor.conf

table <fail2ban> counters
block drop log quick from <fail2ban> to any

Table names are always surrounded by angle brackets, to differentiate them from other objects. The counters keyword tells PF to keep individual traffic counters for each address in the table, rather than just an aggregate number for all the addresses in the table. The next line is fairly self-explanatory (the beauty of PF syntax). It's going to drop any traffic going through PF from a banned address to any address.

The quick keyword tells PF to immediately block the packet, short-cutting the normal behavior which is to traverse the ruleset completely and take the action of the last matching rule (i.e. whatever rule's conditions most recently matched the packet at the end of the ruleset wins). The log keyword tells PF to record it's action in /var/log/pflog, which is a PCAP file that can be read via tcpdump -netttr /var/log/pflog. I encourage increasing the pflogd snaplength to 256 bytes by adding 'pflogd_flags="-s 256"' to /etc/rc.conf.local to facilitate easier debugging.

Note that the table name does not need to match the anchor name. Also note that tables declared inside an anchor are "private" tables, only visible to rules in that anchor. This can be a bit confusing if you're used to using pfctl to view tables. Just remember that you need to use '-a fail2ban' to see any rules and tables specific to the fail2ban anchor when using pfctl.

Now I need to tell Fail2ban how to interact with PF. This is done by creating a template in the fail2ban/action.d directory. I decided to copy the ipfw action template as a basis for customization.

$ cd /etc/fail2ban/action.d
$ sudo cp ipfw.conf pf-drop-all.conf
$ sudo vi pf-drop-all.conf

Here are the essential sections of the file after modification:

# /etc/fail2ban/action.d/pf-drop-all.conf


# Option:  actionstart
# Notes.:  command executed once at the start of Fail2Ban.
# Values:  CMD
# -a fail2ban -t fail2ban -Ts || = check if it's already loaded
# -a fail2ban = operate on this anchor
# -f /etc/fail2ban/pf-anchor.conf = load configuration from this file
## (only to the specified anchor)
actionstart = /sbin/pfctl -a fail2ban -t fail2ban -Ts || /sbin/pfctl -a fail2ban -f /etc/fail2ban/pf-anchor.conf

# Option:  actionstop
# Notes.:  command executed once at the end of Fail2Ban
# Values:  CMD
# -a fail2ban = operate on this anchor
# -F rules = flush the rules (only for the specified anchor)
actionstop = /sbin/pfctl -a fail2ban -F rules

# Option:  actioncheck
# Notes.:  command executed once before each actionban command
# Values:  CMD
# -s info = show pf statistics and information
# | grep Enabled = return an error unless pf is enabled
actioncheck = /sbin/pfctl -s info | grep Enabled

# Option:  actionban
# Notes.:  command executed when banning an IP. Take care that the
#          command is executed with Fail2Ban user rights.
# Tags:    <ip>  IP address
#          <failures>  number of failures
#          <time>  unix timestamp of the ban time
# Values:  CMD
# -a fail2ban = operate on this anchor
# -t fail2ban = operate on this table
# -T add <ip> = add address(es) to this table
# && = also perform this if the previous operation succeeds
# -k <ip> = remove state table entries for this address,
## i.e. kill existing connections
actionban = /sbin/pfctl -a fail2ban -t fail2ban -T add <ip> && /sbin/pfctl -k <ip>

# Option:  actionunban
# Notes.:  command executed when unbanning an IP. Take care that the
#          command is executed with Fail2Ban user rights.
# Tags:    <ip>  IP address
#          <failures>  number of failures
#          <time>  unix timestamp of the ban time
# Values:  CMD
# -a fail2ban = operate on this anchor
# -t fail2ban = operate on this table
# -T delete <ip> = remove this address from this table
actionunban = /sbin/pfctl -a fail2ban -t fail2ban -T delete <ip>


The above action allows me to ban IPs entirely from connecting to this machine. It needs to be used carefully (i.e. only for TCP violations) to avoid an attacker being able to DoS friendly IPs by spoofing malicious packets from them. Speaking of violations, now I need to define what bad actions warrant a ban.

The conditions to look for are called "filters" and they exist in the fail2ban/filter.d directory. I was motivated to install Fail2ban due to probes against my web server, so I was interested in the Apache filters. The default httpd in OpenBSD is based on the Apache 1.3 branch. Perhaps this is why the default regex that Fail2ban tries to match doesn't work under OpenBSD. This is what the default regexes look like:

failregex = [[]client <HOST>[]] user .* authentication failure

But I found I had to prefix it so that it looks like this:

failregex = [[]<TIME>[]] [[]error[]] [[]client <HOST>[]] user .* authentication failure

Do that step for any of the fail2ban/filter.d/apache-*.conf files you intend to use.

The last step is configuring fail2ban/jail.conf file to specify what behavior we want to look for to trigger bans. Here are the relevant sections that I added to my jail.conf file (there are global options that you probably want to configure, those are up to you):

# /etc/fail2ban/jail.conf


# chort's custom rules


enabled = true
filter = apache-auth
action = pf-drop-all
logpath = /var/www/logs/error_log


enabled = true
filter = apache-noscript
action = pf-drop-all
logpath = /var/www/logs/error_log


enabled = true
filter = apache-overflows
action = pf-drop-all
logpath = /var/www/logs/error_log


The very straight-forward syntax means anything matching fail2ban/filter.d/*.conf can be used as a filter = parameter, and anything matching fail2ban/action.d/*.conf can be used as an action. Kudos to the Fail2ban team for making it so easy to plug in actions and filters.

If you want to test your filters, you can use the fail2ban-regex command to specify a log file or log line and a filter.d file or regex to match. It will tell you wether the given filter will match the given input. You should familiarize yourself with the command as it is very useful.

Now for the moment of truth! Time to fire off fail2ban-client start to get things rolling! I haven't written an init script yet, but that should be pretty simple. If you want to check the status of various aspects of Fail2ban's running state, check out the arguments to fail2ban-client. In general, any fail2ban- command when supplied without arguments will give you a usage message.

Last, if you want to check the status of PF to see what IPs are blocked, or whether the rules are loaded, use the commands below.

# remotely trigger the apache-noscript filter (seq is Linux, jot is similar on BSD)
$ for i in `seq 1 6` ; do echo "GET /foo${i}.php HTTP/1.0" | nc yoursite.tld 80 ; done

# show the main ruleset and any anchors (and their rules) inline
$ sudo pfctl -a '*' -sr

# show the configured anchors
$ sudo pfctl -sA

# show only the rules for a particular anchor
$ sudo pfctl -a anchorname -sr

# show only the tables for a particular anchor
$ sudo pfctl -a anchorname -sT

# show the contents of a particular table for a particular anchor
$ sudo pfctl -a anchorname -t tablename -Ts

# show the statistics of a particular table for a particular anchor
$ sudo pfctl -a anchorname -t tablename -vTs

pfctl man page
pf.conf man page
PF Config at
Fail2ban website


at 2011-08-09 12:27:03, MattW wrote in to say...

Great writeup - would be awesome to have this in the OpenBSD package tree...

at 2011-08-15 03:13:38, Mihail wrote in to say...

Really good article, however I couldn't get PF table rule "counter" to work on my FreeBSD since we have older version of PF in our ports. Do you know any work around for this?

at 2011-08-15 08:39:23, chort wrote in to say...

Hello Mihail, which version of FreeBSD? I was under the impression that the latest version is sync'd to the OpenBSD version periodically. I've tweaked my config a bit since this article, so it's due for an update.

at 2011-08-15 23:32:38, Mihail wrote in to say...

FreeBSD 8.2-RELEASE. I believe this version of FreeBSD is using old 4.6 version of PF so there is no counter option available :(.

at 2013-01-05 15:05:36, jbow wrote in to say...

tnx for post it. it didn't solve my problem but it's nice point to undestand better pf and fail 2 ban. code section "white" on "light grey" is a bit unreadable but firebug solve it.

Add a comment:




max length 1000 chars