$_ BSDHowTo.ch
How To... Why not..? Scripts Patches RSS logo

How to quick and easy block SSH bruteforcers

Last update: 2020-08-25

Introduction

Are you annoyed of the bruteforce attacks against your SSH daemon? Are you looking for an easy way to block offending IPs on OpenBSD without installing an intrusion prevention system? In this post I present you a small shell script which uses tools from OpenBSDs base to achieve IP blocking. It has proven effective against slow bruteforce attacks.

Configuration of pf

The OpenBSD firewall pf(4) comes with the handy tool pfctl(8). We will use this to dynamically add offending IP addresses to pf. In pf.conf(5) you should have lines like these:

table <bruteforce> persist

block drop in quick on egress from <bruteforce> to any

Any packets from IP addresses in the table bruteforce get silently dropped on arrival by pf.

Description of the script

The script scans the file /var/log/authlog for specific entries from sshd(8). It uses sed(1) to extract the IP address and store it to a file. As a safety mechanism you can define addresses that should be excluded from blocking. The remaining addresses are added to the pf table bruteforce and all existing connections from these IPs are dropped.

The script itself

The script starts with the shebang and the optional header for rcs(1):

#!/bin/ksh
#
# $Header$

Next comes the array of IPs which should be excluded from blocking. This comes handy if you try to log in from a trusted system and fail, e. g. because you try to log in as root:

exclude[0]=192.0.2.19
exclude[1]=$(dig +short host.example.org)
dump=/tmp/ipdump.txt
list=/tmp/iplist.txt

The sed(1) script is embedded into the script. Each block scans for a specific line in the log file /var/log/authlog. sshd(8) frames the IP address with the words from / with and port. For each line found by sed(1) all text before and after the IP address is removed. The address itself is then written to the file /tmp/ipdump.txt:

sed -nE '/sshd.*Invalid user/{
s/.* from //
s/ port .*//w /tmp/ipdump.txt
}
/sshd.*Failed password for user root/{
s/.* from //
s/ port .*//w /tmp/ipdump.txt
}
/sshd.*Unable to negotiate with/{
s/.* with //
s/ port .*//w /tmp/ipdump.txt
}
/sshd.*Bad protocol version identification/{
s/.* from //
s/ port .*//w /tmp/ipdump.txt
}
/sshd.*Disconnected from authenticating user root/{
s/.* root //
s/ port .*//w /tmp/ipdump.txt
}' /var/log/authlog

The list in /tmp/ipdump.txt is not ordered, contains duplicates and maybe IPs from the exclusion array. The next step is to clean up the list and store it to a new file:

cat $dump | sort -u > $list
rm -rf $dump

for ip in ${exclude[@]} ; do sed -Ei "/$ip/d" $list ; done

Now the IP address list is in a form which makes it suitable as input for pfctl(8). First all addresses in the list are added to the table bruteforce. Second any existing states for IPs in the list get removed from pf(4):

pfctl -q -f $list -t bruteforce -T add
for ip in $(cat $list) ; do pfctl -q -k $ip ; done

Finally, it is time for cleaning up:

rm -rf $list
exit 0

You can download the complete script file addbrute.sh.

How to use it

Of course, you can run the script manually whenever you want to. But it gets really useful if you let cron(8) do the job for you. The script requires root in order to run pfctl(8) commands. So you can either use the global crontab(5) file /etc/crontab or the personal crontab(5) of root. I prefer the second method:

$ doas crontab -e

My entry in the crontab(5) of root runs the script every 15 minutes:

*/15    *   *   *   *   /root/bin/addbrute.sh

Housekeeping

The Internet is a dynamic place. And so your bruteforce table should be dynamic too. The IP address you block today might be assigned to you tomorrow. Although it is not required for the script to work you may want to clean old entries from bruteforce regularly:

pfctl -t bruteforce -T expire 86400

With this command all IP addresses get removed from bruteforce which have not sent any packets to pf(4) for at least one day. Where you place this command is a matter of taste. You can add it to the script if you like. I prefer to run this command as part of the daily maintenance of the OpenBSD system. So I add the above line to /etc/daily.local (see daily(8)).

The contents of any pf(4) tables are lost if you reboot the system. My post How to save and restore tables for pf(4) describes an easy solution for this.