UFW: Allow traffic only from a domain with dynamic IP address
Solution 1
I don't believe this is possible with ufw
. ufw
is just a frontend to iptables
which also lacks this feature, so one approach would be to create a crontab entry which would periodically run and check if the IP address has changed. If it has then it will update it.
You might be tempted to do this:
$ iptables -A INPUT -p tcp --src mydomain.dyndns.org --dport 22 -j ACCEPT
But this will resolve the hostname to an IP and use that for the rule, so if the IP later changes this rule will become invalid.
Alternative idea
You could create a script like so, called, iptables_update.bash
.
#!/bin/bash
#allow a dyndns name
HOSTNAME=HOST_NAME_HERE
LOGFILE=LOGFILE_NAME_HERE
Current_IP=$(host $HOSTNAME | cut -f4 -d' ')
if [ $LOGFILE = "" ] ; then
iptables -I INPUT -i eth1 -s $Current_IP -j ACCEPT
echo $Current_IP > $LOGFILE
else
Old_IP=$(cat $LOGFILE)
if [ "$Current_IP" = "$Old_IP" ] ; then
echo IP address has not changed
else
iptables -D INPUT -i eth1 -s $Old_IP -j ACCEPT
iptables -I INPUT -i eth1 -s $Current_IP -j ACCEPT
/etc/init.d/iptables save
echo $Current_IP > $LOGFILE
echo iptables have been updated
fi
fi
source: Using IPTables with Dynamic IP hostnames like dyndns.org
With this script saved you could create a crontab entry like so in the file /etc/crontab
:
*/5 * * * * root /etc/iptables_update.bash > /dev/null 2>&1
This entry would then run the script every 5 minutes, checking to see if the IP address assigned to the hostname has changed. If so then it will create a new rule allowing it, while deleting the old rule for the old IP address.
Solution 2
I know this is old but I ran across it and ended up with this solution in the end which seems even better because no log file is needed and it very easy to add additional hosts as needed. Works like a charm!
Source: http://rdstash.blogspot.ch/2013/09/allow-host-with-dynamic-ip-through.html
#!/bin/bash
DYNHOST=$1
DYNHOST=${DYNHOST:0:28}
DYNIP=$(host $DYNHOST | grep -iE "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" |cut -f4 -d' '|head -n 1)
# Exit if invalid IP address is returned
case $DYNIP in
0.0.0.0 )
exit 1 ;;
255.255.255.255 )
exit 1 ;;
esac
# Exit if IP address not in proper format
if ! [[ $DYNIP =~ (([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]) ]]; then
exit 1
fi
# If chain for remote doesn't exist, create it
if ! /sbin/iptables -L $DYNHOST -n >/dev/null 2>&1 ; then
/sbin/iptables -N $DYNHOST >/dev/null 2>&1
fi
# Check IP address to see if the chain matches first; skip rest of script if update is not needed
if ! /sbin/iptables -n -L $DYNHOST | grep -iE " $DYNIP " >/dev/null 2>&1 ; then
# Flush old rules, and add new
/sbin/iptables -F $DYNHOST >/dev/null 2>&1
/sbin/iptables -I $DYNHOST -s $DYNIP -j ACCEPT
# Add chain to INPUT filter if it doesn't exist
if ! /sbin/iptables -C INPUT -t filter -j $DYNHOST >/dev/null 2>&1 ; then
/sbin/iptables -t filter -I INPUT -j $DYNHOST
fi
fi
Solution 3
Based on previous answers I updated the following as bash script that works on Debian Jessie
#!/bin/bash
HOSTNAME=dynamichost.domain.com
LOGFILE=$HOME/ufw.log
Current_IP=$(host $HOSTNAME | head -n1 | cut -f4 -d ' ')
if [ ! -f $LOGFILE ]; then
/usr/sbin/ufw allow from $Current_IP to any port 22 proto tcp
echo $Current_IP > $LOGFILE
else
Old_IP=$(cat $LOGFILE)
if [ "$Current_IP" = "$Old_IP" ] ; then
echo IP address has not changed
else
/usr/sbin/ufw delete allow from $Old_IP to any port 22 proto tcp
/usr/sbin/ufw allow from $Current_IP to any port 22 proto tcp
echo $Current_IP > $LOGFILE
echo iptables have been updated
fi
fi
Solution 4
Based on all answers before I combined them. No logfile needed. Tested on Ubuntu 18.04
#!/bin/bash
HOSTNAME=YOUR.DNS.NAME.HERE
if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root"
exit 1
fi
new_ip=$(host $HOSTNAME | head -n1 | cut -f4 -d ' ')
old_ip=$(/usr/sbin/ufw status | grep $HOSTNAME | head -n1 | tr -s ' ' | cut -f3 -d ' ')
if [ "$new_ip" = "$old_ip" ] ; then
echo IP address has not changed
else
if [ -n "$old_ip" ] ; then
/usr/sbin/ufw delete allow from $old_ip to any
fi
/usr/sbin/ufw allow from $new_ip to any comment $HOSTNAME
echo iptables have been updated
fi
You can add a port to the rules with "port" parameter. e.G.:
if [ -n "$old_ip" ] ; then
/usr/sbin/ufw delete allow from $old_ip to any port 22
fi
/usr/sbin/ufw allow from $new_ip to any port 22 comment $HOSTNAME
Solution 5
Here is a version in python which can add or remove ipv4 and ipv6 rules if the hostname resolves to multiple endpoints (ufw). Note that my scenario was slightly different as I started with an "Allow everything" profile.
Based on the version from Tim Kennedy and Mattias Pettersson
#!/usr/bin/env python
# Only allow a particular HOSTNAME to access the given port...
# from https://unix.stackexchange.com/a/534117/66983
# and https://unix.stackexchange.com/a/91711/66983
# If the ufw table is empty you might need to execute the script twice (as inserting on top will not work properly)
# crontab -e and add '*/5 * * * * root /path/to/update_ufw.py > /dev/null 2>&1'
HOSTNAME="<hostname>"
PORT=<port>
import os
import subprocess
if os.geteuid() != 0:
print("This script must be run as root")
exit(1)
def run(cmd):
process = subprocess.Popen(['bash', '-c', cmd],
stdout=subprocess.PIPE)
stdout, stderr = process.communicate()
return stdout.decode('utf-8')
new_ip_output = run("getent ahosts \"{}\" | awk '{{ print $1 }}'".format(HOSTNAME))
new_ips=set(new_ip_output.split())
old_ip_output = run("/usr/sbin/ufw status | grep {} | head -n1 | tr -s ' ' | cut -f3 -d ' '".format(HOSTNAME))
old_ips=set(old_ip_output.split())
if old_ips == new_ips:
print ("All IPs still OK.")
else:
# add new IPs
for new_ip in new_ips:
if new_ip not in old_ips:
out = run("/usr/sbin/ufw insert 1 allow from {} to any port {} comment {}".format(new_ip, PORT, HOSTNAME))
print(out)
# remove old IPs
for old_ip in old_ips:
if old_ip not in new_ips:
out = run("/usr/sbin/ufw delete allow from {} to any port {}".format(old_ip, PORT))
print(out)
# add deny rule
out = run("/usr/sbin/ufw deny {}".format(PORT))
print(out)
Related videos on Youtube
Carles Sala
Updated on September 18, 2022Comments
-
Carles Sala over 1 year
I run a VPS which I would like to secure using UFW, allowing connections only to port 80. However, in order to be able to administer it remotely, I need to keep port 22 open and make it reachable from home.
I know that UFW can be configured to allow connections to a port only from specific IP address:
ufw allow proto tcp from 123.123.123.123 to any port 22
But my IP address is dynamic, so this is not yet the solution.
The question is: I have dynamic DNS resolution with DynDNS, so is it possible to create a Rule using the domain instead of the IP?
I already tried this:
ufw allow proto tcp from mydomain.dyndns.org to any port 22
but I got
ERROR: Bad source address
-
Carles Sala over 10 yearsHow silly that I didn't think about resolving the hostname periodically. I modified your script (added logging, etc.) and it works like a charm. Thank you!
-
Krystian over 8 yearsnote: on Debian 7 I had to change line
Current_IP=$(host $HOSTNAME | cut -f4 -d' ')
toCurrent_IP=$(host $HOSTNAME | head -n1 | cut -f4 -d ' ')
-
Tim Kennedy about 7 yearsThat could even be added to
cron
to have it run periodically on it's own. -
Mattias Pettersson about 7 yearsThat's what I did ;)
-
Freedo about 7 yearsWill I be able to see this when using ufw status verbose ? I mean, the rules?
-
Freedo about 7 yearssorry I'm a little newbie. Where do I need to store this script and where I change things to reflect my specific case ?
-
slm about 7 years@Freedo not sure, try it and see what happens.
-
Guerlando OCs about 6 yearsThis script has a small problem: on first usage if you forgot to run as root, it'll create the log file but not add the rules. Then if you run again as root it'll just say 'ip address didn't change'. It has to be run as root the first time! Also, would be good to change
LOGFILE=$HOME/ufw.log
toLOGFILE=$HOME/ufw.$HOSTNAME.log
to permit more than one script run at the same time -
Matthew over 5 years@GuerlandoOCs how do you reset if you run into this issue?
-
chris6953 about 4 yearsPossible issue when setting
Current_IP
: If the hostname is given by a DNS CNAME record (basically, an alias for another DNS name), then the IP address is not in the first line ofhost
's output. Solution: Replacehead -n1
withtail -n1
. -
Scott - Слава Україні almost 4 years(1) Please don’t post partial answers. Each answer should be free-standing, self-contained and self-sufficient. This doesn’t make much sense unless we look at it in conjunction with slm’s answer. (2) As I said, your answer obviously builds on slm’s answer, and you say that you found part of your answer on the web. You should identify any posts that you build on or copy from, linking to them and identifying their authors. (3) You modified the answer to allow multiple ports. Why? … (Cont’d)
-
Scott - Слава Україні almost 4 years(Cont’d) … The question explicitly calls for port 22; so, in that regard, you aren’t answering the question. (4) Most of Stack Exchange works in English. Please don’t post text in other languages.
-
Evaristo almost 4 yearsNo, my answer is based on the last comment, which is from "Sebastian".
-
Evaristo almost 4 yearsSorry. I added the references you were saying, and corrected the little phrase in Spanish. As for adding more ports, it just solved a need. I thought it provided extra value. Sorry for the inconvenience.
-
mr_squall almost 4 yearsYou need to install apt-get install dnsutils
-
DiagramChasingBlues over 3 years@Freedo I googled "where to store bash scripts." For all users its:
/usr/local/bin
. So do aallow_host_with_dynamic_ip.sh
. That creates a file. Then dols /usr/local/bin
to see the file listed. Now dovim /usr/local/bin/allow_host_with_dynamic_ip.sh
. Edit it and make sure replace / insert mode is set by hittinginsert
key. Write the file by first escaping (esc
key) out of edit mode and then typing:w
. Quit with, you guessed it:q
. That will bring you back to your command session. -
DiagramChasingBlues over 3 yearsGenius move by switching from bash to Python. I was fine typing out a shell script, but now I'll just copy / paste your python and go through it line by line. You have very clean coding skills. I hope your rep goes much higher, so I've upvoted. Question is, how do I now delete the .sh script...
-
DiagramChasingBlues over 3 yearsAhah! You
cd /usr/local/bin
andrm myfile.sh
. -
assayag.org over 2 years2021: :~$ /etc/init.d/iptables save -bash: /etc/init.d/iptables: No such file or directory
-
David over 2 yearsThanks @AdminBee
-
mckenzm over 2 years@daniel assayag things have moved and changed. Most notably iptables is now considered to be "opt in". A lot has been moved from /sbin to /usr/sbin. See also iptables "legacy" as a wrapper around the now adopted NFTables.
-
mckenzm over 2 yearsOf course you can still use legacy /usr/sbin/iptables-restore every 5 minutes - but use actual domain names.