Limiting SSH access for dynamic IP source

Summary

This post is about a small script that can update the /etc/hosts.allow with single IPv4 for use with Dynamic DNS.

Please check the ‘ Warning ‘ section before using the script.


If you’ve Server or VPS with public IP and want to limit SSH access (allow or deny) to specific IP or IP range to make it more secure, one of the ways is to simply possible by modifying the /etc/hosts.allow and /etc/hosts.deny.
This is normal while the source IP(s) of connection is known but what if you want to connect from home or place whose IP could change, somewhere like home which most likely has dynamic IP (on Cable, DSL, etc.).

There’s a chance your modem support one or more Dynamic DNS service provider such as DynDNS or No-IP which then, in this case, you can create an account and configure modem/device to update the created record based on schedule.

Sample for configuring modem with Dynamic DNS Services

This is still dynamic IP but since it’s updating a record on DNS, now it’s possible to use reverse DNS lookup to grab this IP and place it in the hosts.allow configuration file.

Below is a small script I made which can be configured in cron with target DNS record (the hostname/record you’ve made on any dynamic DNS service provider) as a parameter and then it’ll update hosts.allow to only permit that specific IP.

Please check the ‘ Warning ‘ section before using the script.

Script

#######################################################
### Safe Source - ver 0.2 - by Sohrab Kasraeianfard ###

#!/bin/bash

Allow_File="/etc/hosts.allow"
Deny_File="/etc/hosts.deny"

if [ ! -z  `which dig` ]; then
	Safe_Source=`dig +short $1`
	[ -z $Safe_Source ] || printf "dig found '$Safe_Source' for '$1'\n"
elif [ ! -z `which getent` ]; then
	Safe_Source=`getent hosts $1 | awk '{print $1}'`
	[ -z $Safe_Source ] || printf "getent found '$Safe_Source' for '$1'\n"
elif [ ! -z `which nslookup` ]; then
        Safe_Source=`nslookup $1 | grep Address | grep -v "#53\|::" | awk '{print $2}'`
        [ -z $Safe_Source ] || printf "nslookup found '$Safe_Source' for '$1'\n"
else
	printf "Can't find dig, nslookup or getent application which script needs\nConsider installing one of these tools.\n"
fi

Current=`cat $Allow_File | awk '{print $3}'`

if [ -z `dig +short $1` ]; then
	printf "Hostname can't be resolved.\n"
	if [ "$2" == "not-secure" ]; then
		printf "Removed SSH access limitation from both below files (not recommended)\n$Allow_File\n$Deny_File\n"
		printf "ALL : ALL\n" > $Allow_File
		printf "\n" > $Deny_File
	else
		printf "Default is secure.\nSSH access blocked, script will keep looking for $1\n"
		printf "\n" > $Allow_File
		printf "ALL : ALL\n" > $Deny_File
	fi
else
	if [ "$Safe_Source" != "$Current" ]; then
	        printf "New IP exist, updating record ...\n"
	        printf "sshd : $Safe_Source\n" > $Allow_File
		printf "ALL : ALL\n" > $Deny_File
		systemctl restart sshd
	else
		printf "Nothing to change.\n"
	fi
fi

In the above script first parameter that the script will see would be the hostname and the second parameter will be the security option.

In case of expiration of host-name or when the script can’t resolve the name, if the second parameter has been set to not-secure, it’ll unblock access so the connection from all IP would be possible which is not recommended.
Otherwise, by setting nothing or anything else, the script will clear /etc/hosts.allow so previous IP can’t connect and it’ll keep looking for new IP (resolving the hostname).

This file can be placed anywhere and cron can run it as an hourly, daily, or custom timing task but don’t forget to modify the file permission and make it executable.

Cron configuration

There are a lot of resources online on how to configure the cron for scheduling tasks which I’ve listed a few below.

Below is an example of what can be used to check each 1 hour or 10 minutes and if needed update the hosts.allow file; assuming the script placed in a folder called /script/.

# For running every one hour
0 1 * * * /script/safe_source dns-record.dynamic-dns-server.domain

# For running every 10 minutes
*/10 * * * * /script/safe_source dns-record.dynamic-dns-server.domain

# For running every 10 minutes with non-secure option
*/10 * * * * /script/safe_source dns-record.dynamic-dns-server.domain not-secure

Warning

  • Please read the below notes before using the script.
  • Create backup from your current /etc/hosts.allow and /etc/hosts.deny before using the script.
    You can simply copy both files to your home or create a copy in the same location with a new name or run the below commands.
# Create copy of file in same location with new name.
cp -a /etc/hosts.allow{,.org}
cp -a /etc/hosts.deny{,.org}
  • Make sure you can access the server from KVM, hosting portal, or another way in case the script didn’t work as expected.
  • Keep one SSH session connected and test from another terminal in parallel so if there’s an issue you can rollback.
  • Take a note of your public IP address and compare it with IP added to /etc/hosts.allow to be sure your Dynamic DNS configuration works correctly.
    You can find your public IP by searching for ‘my IP address’ in DuckDuckGo (top of the page, below the search bar) or Google or visiting sites such as whatismyipaddress or monip.
  • For rollback, simply edit both /etc/hosts.allow and /etc/hosts.deny and add # before added lines or copy the backup file over current files and restart the sshd services or run below commands.
# Roll back by copying original files over modified version
/bin/cp /etc/hosts.allow.org /etc/hosts.allow
/bin/cp /etc/hosts.deny.org /etc/hosts.deny
systemctl restart sshd.service
  • If you place the script in cron and if you are receiving emails from your Dynamic DNS provider to confirm your host-name periodically, in case of ignoring those requests, it might cause inaccessibility.
    Since cron will keep running the script and as a result script keep checking that host-name (DNS record) to keep /etc/hosts.allow file up to date based lookup result, if you don’t confirm your host-name and your record expires then the script will not be able to resolve successfully and it’s possible that it clear the ‘hosts.allow’ file (in case of not using ‘not-secure‘ option) which means no SSH connection will be accepted until DNS record be resolvable again.
  • This script needs one of the below tools to be able to resolve the names, having any of these should be ok.
  • There’s a script that can automate host-name confirmation for no-ip but I didn’t test or use it.
  • In case of troubleshooting ‘/var/log/cron’ might be useful; depends on system logging configuration.
root@test-node: ~$  tail -f /var/log/cron

Dec  2 23:18:01 test-node CROND[7162]: (root) CMD (/root/bin/safe_source my-host.example-dns-provider.domain)
Dec  2 23:18:01 test-node CROND[7161]: (root) CMDOUT (dig found 'AAA.BBB.CCC.DDD' for 'my-host.example-dns-provider.domain')
Dec  2 23:18:01 test-node CROND[7161]: (root) CMDOUT (Nothing to change.)

Dec  2 23:20:01 test-node CROND[7291]: (root) CMD (/root/bin/safe_source my-expired-host.example-dns-provider.domain not-secure)
Dec  2 23:20:01 test-node CROND[7290]: (root) CMDOUT (Hostname can't be resolved.)
Dec  2 23:20:01 test-node CROND[7290]: (root) CMDOUT (Removed SSH access limitation from both below files (not recommended))
Dec  2 23:20:01 test-node CROND[7290]: (root) CMDOUT (/etc/hosts.allow)
Dec  2 23:20:01 test-node CROND[7290]: (root) CMDOUT (/etc/hosts.deny)

  • It’s also possible to install client application for some Dynamic DNS providers such as noip.org on the Linux system.
  • By doing so, it can be on a laptop or tablet and whenever you take the device, that node should be able to connect.
  • In this case, be careful about public networks as the public IP of the network will be whitelisted until next cron run.

It's your kindness to leave a reply/feedback