UFW: Allow traffic only from a domain with dynamic IP address

82,492

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)

Share:
82,492

Related videos on Youtube

Carles Sala
Author by

Carles Sala

Updated on September 18, 2022

Comments

  • Carles Sala
    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
    Carles Sala over 10 years
    How 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
    Krystian over 8 years
    note: on Debian 7 I had to change line Current_IP=$(host $HOSTNAME | cut -f4 -d' ') to Current_IP=$(host $HOSTNAME | head -n1 | cut -f4 -d ' ')
  • Tim Kennedy
    Tim Kennedy about 7 years
    That could even be added to cron to have it run periodically on it's own.
  • Mattias Pettersson
    Mattias Pettersson about 7 years
    That's what I did ;)
  • Freedo
    Freedo about 7 years
    Will I be able to see this when using ufw status verbose ? I mean, the rules?
  • Freedo
    Freedo about 7 years
    sorry I'm a little newbie. Where do I need to store this script and where I change things to reflect my specific case ?
  • slm
    slm about 7 years
    @Freedo not sure, try it and see what happens.
  • Guerlando OCs
    Guerlando OCs about 6 years
    This 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 to LOGFILE=$HOME/ufw.$HOSTNAME.log to permit more than one script run at the same time
  • Matthew
    Matthew over 5 years
    @GuerlandoOCs how do you reset if you run into this issue?
  • chris6953
    chris6953 about 4 years
    Possible 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 of host's output. Solution: Replace head -n1 with tail -n1.
  • Scott - Слава Україні
    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 - Слава Україні
    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
    Evaristo almost 4 years
    No, my answer is based on the last comment, which is from "Sebastian".
  • Evaristo
    Evaristo almost 4 years
    Sorry. 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
    mr_squall almost 4 years
    You need to install apt-get install dnsutils
  • DiagramChasingBlues
    DiagramChasingBlues over 3 years
    @Freedo I googled "where to store bash scripts." For all users its: /usr/local/bin. So do a allow_host_with_dynamic_ip.sh. That creates a file. Then do ls /usr/local/bin to see the file listed. Now do vim /usr/local/bin/allow_host_with_dynamic_ip.sh. Edit it and make sure replace / insert mode is set by hitting insert 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
    DiagramChasingBlues over 3 years
    Genius 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
    DiagramChasingBlues over 3 years
    Ahah! You cd /usr/local/bin and rm myfile.sh.
  • assayag.org
    assayag.org over 2 years
    2021: :~$ /etc/init.d/iptables save -bash: /etc/init.d/iptables: No such file or directory
  • David
    David over 2 years
    Thanks @AdminBee
  • mckenzm
    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
    mckenzm over 2 years
    Of course you can still use legacy /usr/sbin/iptables-restore every 5 minutes - but use actual domain names.