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:
Updated June 28th 2019
This guide also works on Ubuntu 16.04 and 18.04 without additional configuration or steps.
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
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
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 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