Restricting access to ssh using fail2ban and geoip

Recently I wanted to add some additional security layers to a server that I administer. To a seasoned admin, this will be old news, but I’m a hobbyist and just learned about this. Maybe some of you find it useful as well.

If you have a server that is publically accessible, when you look at /var/log/auth.log log file you will find a lot of unsuccessful authentication attempts from bots from around the world. If you require public keys to authenticate, this doesn’t have to be a problem. If you don’t, you should disable password based authentication and switch to using ssh keys. You can do this in /etc/ssh/sshd_config by setting this option:

PasswordAuthentication no

Afterwards, reload the sshd configuration or restart the ssh daemon.

sudo systemctl reload ssh

Also, you can get prevent a lot of unwanted login attempts by using a non standard tcp port for your ssh server. Again, you can do this in you sshd_config:

Port 12345

However, it can still be a good idea to further restrict who can connect to the server in the first place.

Just a little note upfront: my server runs on Ubuntu, for other distributions you will need to adapt the instructions below.


One tool that you can use to increase security is fail2ban: it’s an intrusion prevention system that, once configured, will scan the log files of different services for suspicious log entries and ban IPs from connecting to your server. For every service that fail2ban monitors it creates a so called jail. The default configuration works with ssh and already does a good job filtering out much of the noise at firewall level. Installation is easy:

sudo apt update
sudo apt install fail2ban

Afterwards you need to create a local copy of the default configuration file, so that your changes don’t conflict with possible updates at a later point.

cd /etc/fail2ban
sudo cp jail.conf jail.local

Have a look inside jail.local, you can adjust different options here (ban time, number of failed authentication attemps, etc.). The default values make sense, be sure to think about your changes so that you don’t create unexpected side effects. For example: if you ban ip’s for a very long time, you might put the ban on some other user after the ip address was reassigned.

If this sounds interesting, check this tutorial for more detailed instructions on how to install Fail2Ban.

Restricting access by geo location

Most of the unsuccessful connection attempts in /var/log/auth.log come from countries that I never use for logins to my server. So it is obvious that we should only allow connection attempts from certain countries.

To achieve this, you can install the GeoIP database:

sudo apt install geoip-bin

The package installs two new commands that enable you to lookup geo information for ipv4 and ipv6 addresses (geolookup and geolookup6). In order to check an ip address, we need a filter script.

# License: WTFPL
# UPPERCASE space-separated country codes to ACCEPT
if [ $# -ne 1 ]; then
echo "Usage: `basename $0` " 1>&2
exit 0 # return true in case of config issue
if [[ "`echo $1 | grep ':'`" != "" ]] ; then
COUNTRY=`/usr/bin/geoiplookup6 "$1" | awk -F ": " '{ print $2 }' | awk -F "," '{ print $1 }' | head -n 1`
COUNTRY=`/usr/bin/geoiplookup "$1" | awk -F ": " '{ print $2 }' | awk -F "," '{ print $1 }' | head -n 1`
if [[ "$RESPONSE" == "ALLOW" ]] ; then
logger -p $LOGDENY_FACILITY "$RESPONSE sshd connection from $1 ($COUNTRY)"
exit 0
logger -p $LOGDENY_FACILITY "$RESPONSE sshd connection from $1 ($COUNTRY)"
exit 1
view raw hosted with ❤ by GitHub
You can check the script by calling it with a ip address as an argument.

Next, we need to integrate the script into the ssh authentication process. We can do this by facilitating TCP wrappers, a networking ACL system, used to filter network access. To use them, we need to configure the two hosts access files /etc/hosts.deny and /etc/hosts.allow.

Allow rules take precedence over deny rules and the first matching rule terminates validation. Since we want to have an allowlist, we first deny all requests to sshd in /etc/hosts.deny:

sshd: ALL

Then, in /etc/hosts.allow, we call into our filter script:

sshd: ALL: aclexec /usr/local/bin/ %a

You don’t need to restart anything, these rules take immediate effect.

You can find this solution on the web already, but I noticed it didn’t work for me. The filter script itself logged the expected outcome and returned the correct result (exit code 1 for blocked countries, exit code 0 for allowed countries), the tcp wrapper also called the script and log entries were created upon ssh connection. But when I tested it, blocked ip’s were still allowed to connect. After some research I found the problem: The solution that I tried to apply didn’t use aclexec to call the filter script but instead used spawn. The latter doesn’t actually use the exit code to do anything. It just spawns a child process that executes the given command. aclexec will take into account the exit code and abort connection attempts.

There are other methods to restrict access of unwanted ips to ssh (i’m thinking of rate limiting, for example), but this is good enough for me, right now.


2 thoughts on “Restricting access to ssh using fail2ban and geoip

  1. I run the script and nothing happens no output at all. If I run the script without an IP it tells me : Usage:

    Not sure if anyone else has this issue. I would really love to geoblock but it seems every different way I have tried I have not been able to.

    I wodner if anyone else hqs tried this way.

    • Well, the script doesn’t do readable output. it has two possible exit codes, upon which ssh authentication will then react. if you want to check if the correct result is produced, you could run the script with an ip address and then use the following command to print the last exit code: “echo $?”. 0 is used for ALLOW, 1 is used for DENY.

Leave a Reply

Your email address will not be published. Required fields are marked *