38

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

56

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.

| improve this answer | |
  • 3
    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! – Carles Sala Sep 21 '13 at 14:42
  • @CarlesSala - glad it solved your issue. In addition to accepting you can also upvote 8-). – slm Sep 21 '13 at 14:48
  • 1
    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 ' ') – Krystian Jan 6 '16 at 13:47
  • Will I be able to see this when using ufw status verbose ? I mean, the rules? – Freedo Apr 7 '17 at 9:02
  • @Freedo not sure, try it and see what happens. – slm Apr 7 '17 at 11:16
8

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
| improve this answer | |
  • sorry I'm a little newbie. Where do I need to store this script and where I change things to reflect my specific case ? – Freedo Apr 7 '17 at 9:52
7

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
| improve this answer | |
  • That could even be added to cron to have it run periodically on it's own. – Tim Kennedy Mar 21 '17 at 15:18
  • That's what I did ;) – Mattias Pettersson Mar 22 '17 at 14:39
  • 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 – Guerlando OCs Apr 25 '18 at 2:38
  • @GuerlandoOCs how do you reset if you run into this issue? – Matthew Dec 28 '18 at 15:14
  • 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. – chris6953 Apr 13 at 6:36
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
| improve this answer | |
  • You need to install apt-get install dnsutils – mr_squall Jun 23 at 10:25
0

I based on the last comment of Sebastian, but for multiple ports, and added an IP check that I found on the web. Since duckdns sometimes crashes and servers don't respond with the IP. host == "Found"

This works for me.

#!/bin/bash
function valid_ip()
{
    local  ip=$1
    local  stat=1

    if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
        OIFS=$IFS
        IFS='.'
        ip=($ip)
        IFS=$OIFS
        [[ ${ip[0]} -le 255 && ${ip[1]} -le 255 \
            && ${ip[2]} -le 255 && ${ip[3]} -le 255 ]]
        stat=$?
    fi
    return $stat
}

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 ! valid_ip $new_ip;
then
   exit 1
fi


if [ "$new_ip" = "$old_ip" ] ; then
    exit 0
else
    if [ -n "$old_ip" ] ; then
        /usr/sbin/ufw delete allow from $old_ip to any port 22,6556 proto tcp
    fi
    /usr/sbin/ufw allow from $new_ip to any port 22,6556 proto tcp comment $HOSTNAME
    echo "iptables have been modified by change of ip $new_ip"
    exit 0
fi
| improve this answer | |
  • (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 May 21 at 3:12
  • (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. – Scott May 21 at 3:12
  • No, my answer is based on the last comment, which is from "Sebastian". – Evaristo May 21 at 23:33
  • 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. – Evaristo May 21 at 23:35
0

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)

| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.