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.
#!/bin/bashset -euo pipefail# --- Configuration ---DDNS_HOSTNAME="myhome.duckdns.org"SSH_PORT="22"STATE_FILE="/var/tmp/ddns-firewall-last-ip"# ---------------------# Resolve the current IPCURRENT_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 1fi# Read the last known IPLAST_IP=""if [ -f "$STATE_FILE" ]; then LAST_IP=$(cat "$STATE_FILE")fi# If the IP hasn't changed, do nothingif [ "$CURRENT_IP" = "$LAST_IP" ]; then exit 0fiecho "$(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 || truefi# Add the new ruleufw allow from "$CURRENT_IP" to any port "$SSH_PORT" comment "DDNS: $DDNS_HOSTNAME"# Save the current IPecho "$CURRENT_IP" > "$STATE_FILE"
For servers using firewalld (RHEL / AlmaLinux / Rocky):
#!/bin/bashset -euo pipefail# --- Configuration ---DDNS_HOSTNAME="myhome.duckdns.org"SSH_PORT="22"STATE_FILE="/var/tmp/ddns-firewall-last-ip"# ---------------------# Resolve the current IPCURRENT_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 1fi# Read the last known IPLAST_IP=""if [ -f "$STATE_FILE" ]; then LAST_IP=$(cat "$STATE_FILE")fi# If the IP hasn't changed, do nothingif [ "$CURRENT_IP" = "$LAST_IP" ]; then exit 0fiecho "$(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 || truefi# Add the new rulefirewall-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 IPecho "$CURRENT_IP" > "$STATE_FILE"
After the cron runs, confirm the rule is in place:
UFW
firewalld
ufw status | grep DDNS
firewall-cmd --list-rich-rules
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.