Geolocation checks for SSH
Even though there is no such thing as perfect security, it certainly
doesn't hurt to tighten some screws every now and then. So in addition
to my already decently hardened sshd configuration, I've
decided to block ssh connections from outside of Austria.
My implementation is based on the approach taken in the blog article "Restricting
access to ssh using fail2ban and geoip" by
reinhard.codes.
Access control files
Access control for incoming requests can be implemented via
tcpd, which uses the files
/etc/hosts.{allow,deny}. By default, we deny access to
ssh for everybody, in/etc/hosts.deny:
sshd: ALL
In the file /etc/hosts.allow, we specify a script that
takes the client's IP address (%a) and determines whether
or not to allow the client's connection via its exit code:
sshd: ALL: aclexec /usr/local/bin/ssh-ip-check.sh %a
Client IP address checking
The script /usr/local/bin/ssh-ip-check.sh uses
geoiplookup and python to check if the client
is connecting from an allowed IP address range or country:
#!/bin/bash
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: 2026 Max Moser
allowed_countries=("AT")
allowed_ips=("192.168.1.0/24" "127.0.0.0/8")
if "$#" -ne 1 ; then
echo "usage: $0 <ip>"
exit 2
fi
ip="$1"
# use python for subnet checking
function check_ip() {
python - "$1" "$2" << EOF
import sys
import ipaddress
ip = ipaddress.ip_address(sys.argv[1])
net = ipaddress.ip_network(sys.argv[2], strict=False)
sys.exit(0 if ip in net else 1)
EOF
return "$?"
}
# first, check the subnet of the IP address
for aip in "${allowed_ips[@]}"; do
if check_ip "$ip" "$aip"; then
logger "allowed IP address: $ip"
exit 0
fi
done
# afterwards, check the country of the IP address
for ac in "${allowed_countries[@]}"; do
# the command's output typically looks like:
# GeoIP Country Edition: AT, Austria
c="$(geoiplookup "$ip" | sed -E 's/^.*\s([A-Z]+),.*$/\1/')"
if "$c" = "$ac" ; then
logger "allowed country: $c"
exit 0
fi
done
# if both checks failed, deny access
logger "denied IP address: $ip"
exit 1The logger command writes directly to the logging
facilities instead of std{out,err}, which may be a bit
unexpected when testing the script (e.g.
$ ssh-ip-check.sh 192.168.1.166). The logs can be viewed
e.g. via journalctl, and will look like:
Apr 26 17:41:49 max-moser.dev root[97845]: allowed IP address: 192.168.1.166