OpenBSD’s PF firewall is brilliant. Not only is it easy to configure with a straightforward syntax, but it’s easy to control on-the-fly.

Supposing we had a script that scanned through log files and picked up the IP address of someone trying random passwords to log in. It’s easy enough to write one. Or we noticed someone trying it while logged in. How can we block them quickly and easily without changing /etc/pf.conf? The answer is a pf table.
You will need to edit pf.conf to declare the table, thus:
# Table to hold abusive IPs
table <abuse> persist
“abuse” is the name of the table, and the <> are important! persist tells pf you want to keep the table even if it’s empty. It DOES NOT persist the table through reboots, or even restarts of the pf service. You can dump and reload the table if you want to, but you probably don’t in this use case.
Next you need to add a line to pf.conf to blacklist anything in this table:
# Block traffic from any IP in the abuse table
block in quick from <abuse> to any
Make sure you add this in the appropriate place in the file (near or at the end).
And that’s it.
To add an IP address (example 1.2.3.4) to the abuse table you need the following:
pfctl -t abuse -T add 1.2.3.4
To list the table use:
pfctl -t abuse -T show
To delete entries or the whole table use one of the following (flush deletes all):
pfctl -t abuse -T delete 1.2.3.4
pfctl -t abuse -T flush
Now I prefer to use a clean interface, and on all systems I implement a “blackhole” command, that takes any number of miscreant IP addresses and blocks them using whatever firewall is available. It’s designed to be used by other scripts as well as on the command line, and allows for a whitelist so you don’t accidentally block yourself! It also logs additions.
#!/bin/sh
/sbin/pfctl -sTables | /usr/bin/grep '^abuse$' >/dev/null || { echo "pf.conf must define an abuse table" >&2 ; exit 1 ; }
whitelistip="44.0 88.12 66.6" # Class B networks that shouldn't be blacklisted
for nasty in "$@"
do
echo "$nasty" | /usr/bin/grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' >/dev/null || { echo "$nasty is not valid IPv4 address" >&2 ; continue ; }
classb=$(echo "$nasty" | cut -d . -f 1-2)
case " $whitelistip " in
*" $classb "*)
echo "Whitelisted Class B $nasty"
continue
;;
esac
if /sbin/pfctl -t abuse -T add "$nasty"
then
echo Added new entry $nasty
echo "$(date "+%b %e %H:%M:%S") Added $nasty" >>/var/log/blackhole
fi
done
That’s all there is two it. Obviously my made-up whitelist should be set to something relevant to you.
So how do you feed this blackhole script automatically? It’s up to you, but here are a few examples:
/usr/bin/grep "checkpass failed" /var/log/maillog | /usr/bin/cut -d [ -f3 | /usr/bin/cut -f1 -d ] | /usr/bin/sort -u
This goes through mail log and produces a list of IP addresses where people have used the wrong password to sendmail
/usr/bin/grep "auth failed" /var/log/maillog | /usr/bin/cut -d , -f 4 | /usr/bin/cut -c 6- | /usr/bin/sort -u
The above does the same for dovecot. Beware, these are brutal! In reality I have an additional grep in the chain that detects invalid usernames, as most of the script kiddies are guessing at these and are sure to hit on an invalid one quickly.
Both of these examples produce a list of IP addresses, one per line. You can pipe this output using xargs like this.
findbadlogins | xargs -r blackhole
The -r simply deals with the case where there’s no output, and will therefore not run blackhole – a slight efficiency saving.
If you don’t have pf, the following also works (replace the /sbin/pfctl in the script with it):
/sbin/route -q add $nasty 127.0.0.1 -blackhole 2>/dev/null
This adds the nasty IP address to the routing table and directs packets from it to somewhere the sun don’t shine. pf is probably more efficient that the routing table, but only if you’re using it. This is a quick and dirty way of blocking a single address out-of-the-box.

