Skip to main content
This script is useful for both VPS and Bare Metal customers who want to restrict SSH access to their own IP without manually updating firewall rules every time their ISP assigns a new address.

Prerequisites

  • A Dynamic DNS hostname (e.g., from DuckDNS, No-IP, Dynu, or your router’s built-in DDNS)
  • dig installed (dnsutils on Debian/Ubuntu, bind-utils on RHEL)
apt install dnsutils -y

UFW Script

For servers using UFW (Debian / Ubuntu):
#!/bin/bash
set -euo pipefail

# --- Configuration ---
DDNS_HOSTNAME="myhome.duckdns.org"
SSH_PORT="22"
STATE_FILE="/var/tmp/ddns-firewall-last-ip"
# ---------------------

# Resolve the current IP
CURRENT_IP=$(dig +short "$DDNS_HOSTNAME" A | head -1)

if [ -z "$CURRENT_IP" ]; then
    echo "$(date): Failed to resolve $DDNS_HOSTNAME" >> /var/log/ddns-firewall.log
    exit 1
fi

# Read the last known IP
LAST_IP=""
if [ -f "$STATE_FILE" ]; then
    LAST_IP=$(cat "$STATE_FILE")
fi

# If the IP hasn't changed, do nothing
if [ "$CURRENT_IP" = "$LAST_IP" ]; then
    exit 0
fi

echo "$(date): IP changed from ${LAST_IP:-none} to $CURRENT_IP" >> /var/log/ddns-firewall.log

# Remove the old rule (if it exists)
if [ -n "$LAST_IP" ]; then
    ufw delete allow from "$LAST_IP" to any port "$SSH_PORT" 2>/dev/null || true
fi

# Add the new rule
ufw allow from "$CURRENT_IP" to any port "$SSH_PORT" comment "DDNS: $DDNS_HOSTNAME"

# Save the current IP
echo "$CURRENT_IP" > "$STATE_FILE"

firewalld Script

For servers using firewalld (RHEL / AlmaLinux / Rocky):
#!/bin/bash
set -euo pipefail

# --- Configuration ---
DDNS_HOSTNAME="myhome.duckdns.org"
SSH_PORT="22"
STATE_FILE="/var/tmp/ddns-firewall-last-ip"
# ---------------------

# Resolve the current IP
CURRENT_IP=$(dig +short "$DDNS_HOSTNAME" A | head -1)

if [ -z "$CURRENT_IP" ]; then
    echo "$(date): Failed to resolve $DDNS_HOSTNAME" >> /var/log/ddns-firewall.log
    exit 1
fi

# Read the last known IP
LAST_IP=""
if [ -f "$STATE_FILE" ]; then
    LAST_IP=$(cat "$STATE_FILE")
fi

# If the IP hasn't changed, do nothing
if [ "$CURRENT_IP" = "$LAST_IP" ]; then
    exit 0
fi

echo "$(date): IP changed from ${LAST_IP:-none} to $CURRENT_IP" >> /var/log/ddns-firewall.log

# Remove the old rule (if it exists)
if [ -n "$LAST_IP" ]; then
    firewall-cmd --permanent --remove-rich-rule="rule family=ipv4 source address=$LAST_IP port port=$SSH_PORT protocol=tcp accept" 2>/dev/null || true
fi

# Add the new rule
firewall-cmd --permanent --add-rich-rule="rule family=ipv4 source address=$CURRENT_IP port port=$SSH_PORT protocol=tcp accept"
firewall-cmd --reload

# Save the current IP
echo "$CURRENT_IP" > "$STATE_FILE"

Installation

Save the appropriate script for your firewall:
# For UFW
curl -o /usr/local/bin/ddns-firewall.sh https://raw.githubusercontent.com/your-repo/ddns-firewall-ufw.sh

# For firewalld
curl -o /usr/local/bin/ddns-firewall.sh https://raw.githubusercontent.com/your-repo/ddns-firewall-firewalld.sh

chmod +x /usr/local/bin/ddns-firewall.sh
Edit the configuration variables at the top:
DDNS_HOSTNAME="myhome.duckdns.org"   # Your DDNS hostname
SSH_PORT="22"                          # Your SSH port

Setting Up the Cron Job

Run the script every 5 minutes to keep the firewall in sync:
crontab -e
*/5 * * * * /usr/local/bin/ddns-firewall.sh

How It Works

  1. The script resolves your DDNS hostname to its current IP address
  2. It compares the result against the last known IP (stored in /var/tmp/ddns-firewall-last-ip)
  3. If the IP has changed, it removes the old firewall rule and adds a new one for the current IP
  4. If DNS resolution fails, the script exits without modifying any rules (your existing access is preserved)
  5. All changes are logged to /var/log/ddns-firewall.log

Verifying

After the cron runs, confirm the rule is in place:
ufw status | grep DDNS
Check the log for recent activity:
tail -f /var/log/ddns-firewall.log
This script manages a single rule for one DDNS hostname. If you have multiple dynamic IPs (home, office, etc.), create separate copies of the script with different DDNS_HOSTNAME and STATE_FILE values.