> ## Documentation Index
> Fetch the complete documentation index at: https://docs.digitalfyre.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Dynamic DNS Firewall

<Info>
  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.
</Info>

## Prerequisites

* A Dynamic DNS hostname (e.g., from [DuckDNS](https://www.duckdns.org/), [No-IP](https://www.noip.com/), [Dynu](https://www.dynu.com/), or your router's built-in DDNS)
* `dig` installed (`dnsutils` on Debian/Ubuntu, `bind-utils` on RHEL)

<Tabs>
  <Tab title="Debian / Ubuntu">
    ```bash lines theme={"theme":{"light":"light-plus","dark":"ayu-dark"}}
    apt install dnsutils -y
    ```
  </Tab>

  <Tab title="RHEL / AlmaLinux / Rocky">
    ```bash lines theme={"theme":{"light":"light-plus","dark":"ayu-dark"}}
    dnf install bind-utils -y
    ```
  </Tab>
</Tabs>

## UFW Script

For servers using UFW (Debian / Ubuntu):

```bash expandable lines theme={"theme":{"light":"light-plus","dark":"ayu-dark"}}
#!/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):

```bash expandable lines theme={"theme":{"light":"light-plus","dark":"ayu-dark"}}
#!/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:

```bash lines theme={"theme":{"light":"light-plus","dark":"ayu-dark"}}
# 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:

```bash lines theme={"theme":{"light":"light-plus","dark":"ayu-dark"}}
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:

```bash lines theme={"theme":{"light":"light-plus","dark":"ayu-dark"}}
crontab -e
```

```cron lines theme={"theme":{"light":"light-plus","dark":"ayu-dark"}}
*/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:

<Tabs>
  <Tab title="UFW">
    ```bash lines theme={"theme":{"light":"light-plus","dark":"ayu-dark"}}
    ufw status | grep DDNS
    ```
  </Tab>

  <Tab title="firewalld">
    ```bash lines theme={"theme":{"light":"light-plus","dark":"ayu-dark"}}
    firewall-cmd --list-rich-rules
    ```
  </Tab>
</Tabs>

Check the log for recent activity:

```bash lines theme={"theme":{"light":"light-plus","dark":"ayu-dark"}}
tail -f /var/log/ddns-firewall.log
```

<Warning>
  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.
</Warning>
