Allow SSH access based on GeoIP country

Written by - 2 comments

Published on - last updated on November 13th 2019 - Listed in Linux Security


For a certain online infrastructure there's a SSH jump host in place so that admins, developers and also some external users involved in projects are allowed to connect to the servers.

Of course only key authentication works and we also limited the source ip addresses to the ranges of certain internet providers. But this turned out to be a constant bugger. Either suddenly some new ranges appeared or sometimes a developer has changed his internet provider and the firewall rule needed to be changed again.

Because I know that people involved in this environment only work from a few countries, why not use an access filter based on GeoIP?

Whenever a connection comes in to the SSH daemon, the IP address' origin should be determined by a GeoIP lookup. Depending on which countries are defined as "allowed", they should be allowed to connect.
And this is how it's done (source: https://www.axllent.org/docs/view/ssh-geoip/) on an Ubuntu server (in my case Ubuntu 14.04).

First install the necessary geoip packages:

# apt-get install geoip-database geoip-bin

Then create the GeoIP lookup script, which will be launched whenever a new SSH connection is opened. I created it as /usr/local/bin/ipfilter.sh:

#!/bin/bash
# License: WTFPL

# UPPERCASE space-separated country codes to ACCEPT
ALLOW_COUNTRIES="CH"
LOGDENY_FACILITY="authpriv.notice"

if [ $# -ne 1 ]; then
  echo "Usage:  `basename $0` " 1>&2
  exit 0 # return true in case of config issue
fi

if [[ "`echo $1 | grep ':'`" != "" ]] ; then
  COUNTRY=`/usr/bin/geoiplookup6 "$1" | awk -F ": " '{ print $2 }' | awk -F "," '{ print $1 }' | head -n 1`
else
  COUNTRY=`/usr/bin/geoiplookup "$1" | awk -F ": " '{ print $2 }' | awk -F "," '{ print $1 }' | head -n 1`
fi
[[ $COUNTRY = "IP Address not found" || $ALLOW_COUNTRIES =~ $COUNTRY ]] && RESPONSE="ALLOW" || RESPONSE="DENY"

if [[ "$RESPONSE" == "ALLOW" ]] ; then
  logger -p $LOGDENY_FACILITY "$RESPONSE sshd connection from $1 ($COUNTRY)"
  exit 0
else
  logger -p $LOGDENY_FACILITY "$RESPONSE sshd connection from $1 ($COUNTRY)"
  exit 1
fi

Make the script executable:

# chmod 755 /usr/local/bin/ipfilter.sh

In /etc/hosts.deny set all connections to sshd to DENY:

# cat /etc/hosts.deny | grep sshd
sshd: ALL

In /etc/hosts.allow define the GeoIP lookup script created before for all connections to sshd:

# cat /etc/hosts.allow | grep sshd
sshd: ALL: aclexec /usr/local/bin/ipfilter.sh %a

You probably already guessed it correctly. The variable %a represents the incoming IP address, which is then forwarded to the lookup script. The lookup script then makes the GeoIP lookup with the given IP address. If the country code is in the list of allowed countries (ALLOW_COUNTRIES) or is an unknown source the script will exit 0 (OK) and therefore the connection will be allowed. If the country isn't in the allowed list, the script will exit 1 (NOT OK) and that's the signal to the system to drop the connection.

Let's try to make an ssh connection from a host in Germany:

$ ssh claudio@jumphost.example
ssh_exchange_identification: Connection closed by remote host

On the jump host itself the following log entries appear:

Nov  3 12:17:14 jumphost logger: DENY sshd connection from 144.76.85.24 (DE)
Nov  3 12:17:14 jumphost sshd[11345]: aclexec returned 1
Nov  3 12:17:14 jumphost sshd[11345]: refused connect from 144.76.85.24 (144.76.85.24)

Note: If you're using a RHEL based distribution (for example CentOS), the command "aclexec" doesn't seem to work (see http://tecadmin.net/allow-server-access-based-on-country).
In this case use "spawn" instead of "aclexec" in hosts.allow:

root@rhel # cat /etc/hosts.allow | grep sshd
sshd: ALL: spawn /usr/local/bin/ipfilter.sh %a

On the other hand spawn didn't work on my Ubuntu jump host - here only aclexec really blocked the access.

Of course this is not to be considered as a high security setting. The source IP address can be spoofed to whatever you like. So make sure you have your ssh daemon up to date and harden your settings, for example the typical ones:

  • PasswordAuthentication no
  • PermitRootLogin no
  • AllowUsers (list of allowed users)

Works on Ubuntu 16.04 and 18.04, too

Updated June 28th 2019

This guide also works on Ubuntu 16.04 and 18.04 without additional configuration or steps.

What about fixed IP address whitelisting?

Updated November 13th 2019

While this method is very handy for GeoIP based access, sometimes you may want to allow a single IP address rather than allow a whole country. This can be done by adding a list of fixed IPv4 addresses ALLOW_IPS at the begin and define a block for fixed IP addresses before the GeoIP lookups. Resulting in the following script:

#!/bin/bash
# License: WTFPL

# UPPERCASE space-separated country codes to ACCEPT
ALLOW_COUNTRIES="CH"
LOGDENY_FACILITY="authpriv.notice"
ALLOW_IPS="46.101.15.103"

if [ $# -ne 1 ]; then
  echo "Usage:  `basename $0` " 1>&2
  exit 0 # return true in case of config issue
fi

# Fixed static IP
if [[ $ALLOW_IPS =~ $1 ]]; then
  RESPONSE="ALLOW"
  logger -p $LOGDENY_FACILITY "$RESPONSE sshd connection from $1 (Manual whitelist)"
  exit 0
fi

# Geoiplookup
if [[ "`echo $1 | grep ':'`" != "" ]] ; then
  COUNTRY=`/usr/bin/geoiplookup6 "$1" | awk -F ": " '{ print $2 }' | awk -F "," '{ print $1 }' | head -n 1`
else
  COUNTRY=`/usr/bin/geoiplookup "$1" | awk -F ": " '{ print $2 }' | awk -F "," '{ print $1 }' | head -n 1`
fi
[[ $COUNTRY = "IP Address not found" || $ALLOW_COUNTRIES =~ $COUNTRY ]] && RESPONSE="ALLOW" || RESPONSE="DENY"

if [[ "$RESPONSE" == "ALLOW" ]] ; then
  logger -p $LOGDENY_FACILITY "$RESPONSE sshd connection from $1 ($COUNTRY)"
  exit 0
else
  logger -p $LOGDENY_FACILITY "$RESPONSE sshd connection from $1 ($COUNTRY)"
  exit 1
fi



Add a comment

Show form to leave a comment

Comments (newest first)

pac0san from wrote on Nov 29th, 2019:



Use CDIRs whitelist (needed 'grepcidr' http://www.pc-tools.net/unix/grepcidr/)
Use 'mmdblookup' instead of 'geoiplookup'.
Default: allow 'not found IPs' (ZZ)
Log messages to both 'LOG_FACILITY' and 'stderr'


#!/bin/bash
# License: WTFPL
# Date: 20191129
# Original Source:

# UPPERCASE space-separated country codes to ACCEPT
ALLOW_COUNTRIES="CH ZZ"

# Space-separated IPs to ACCEPT
ALLOW_IPS="192.168.1.0/24"

LOG_FACILITY="authpriv.notice"

if [ $# -ne 1 ]; then
echo "Usage: `basename $0` " 1>&2
exit 0 # return true in case of config issue
fi

echo $1 | /usr/bin/grepcidr "$ALLOW_IPS" &> /dev/null

# Fixed static IP
if [ ${PIPESTATUS[1]} -eq 0 ]; then
RESPONSE="ALLOW"
logger -s -p $LOG_FACILITY "$RESPONSE sshd connection from $1 (Whitelisted)"
exit 0
fi

# Geoip-Lookup
COUNTRY="$(/usr/bin/mmdblookup -f /usr/share/GeoIP/GeoLite2-Country.mmdb -i "$1" country iso_code 2>&1 | awk '/".*"/{gsub(/"/,""); print $1 }')"

[[ $COUNTRY == "" ]] && COUNTRY="ZZ" || True

[[ $ALLOW_COUNTRIES =~ $COUNTRY ]] && RESPONSE="ALLOW" || RESPONSE="DENY"

logger -s -p $LOG_FACILITY "$RESPONSE sshd connection from $1 ($COUNTRY)"

[[ "$RESPONSE" == "ALLOW" ]] && exit 0 || exit 1


Someone from wrote on Nov 13th, 2019:

Even that above works great. Thanks for it, it inspired me to improve it a little.

you can replace :
# use mmdblook also works for ipv6
COUNTRY="$(/usr/bin/mmdblookup -f /var/lib/GeoIP/GeoLite2-Country.mmdb -i "$1" country iso_code 2>&1| awk -F '"' '{ print $2 }'|head -n 2|tail -n 1)"
COUNTRY=${COUNTRY:=IP Address not found}

with
COUNTRY="$(/usr/bin/mmdblookup -f /var/lib/GeoIP/GeoLite2-Country.mmdb -i "$1" country iso_code 2>&1| awk -F '"' '{ print $2 }'|head -n 2|tail -n 1)"

after you installed : mmdb-bin


RSS feed

Blog Tags:

  AWS   Android   Ansible   Apache   Apple   Atlassian   BSD   Backup   Bash   Bluecoat   CMS   Chef   Cloud   Coding   Consul   Containers   CouchDB   DB   DNS   Database   Databases   Docker   ELK   Elasticsearch   Filebeat   FreeBSD   Galera   Git   GlusterFS   Grafana   Graphics   HAProxy   HTML   Hacks   Hardware   Icinga   Influx   Internet   Java   KVM   Kibana   Kodi   Kubernetes   LVM   LXC   Linux   Logstash   Mac   Macintosh   Mail   MariaDB   Minio   MongoDB   Monitoring   Multimedia   MySQL   NFS   Nagios   Network   Nginx   OSSEC   OTRS   Office   OpenSearch   PGSQL   PHP   Perl   Personal   PostgreSQL   Postgres   PowerDNS   Proxmox   Proxy   Python   Rancher   Rant   Redis   Roundcube   SSL   Samba   Seafile   Security   Shell   SmartOS   Solaris   Surveillance   Systemd   TLS   Tomcat   Ubuntu   Unix   VMWare   VMware   Varnish   Virtualization   Windows   Wireless   Wordpress   Wyse   ZFS   Zoneminder