Automatically update a UFW rule so that only the current public IP behind your DDNS hostname can reach a specific port (TCP/UDP). Ideal for exposing a service (e.g., Home Assistant on a non-standard port) to the internet while restricting access to your own dynamic IP.
Works with any DDNS provider whose hostname resolves via DNS (example here uses DuckDNS).
- Resolves
DDNS_HOSTto get the current public IP (dig +short). - Compares it with the last IP recorded in
/var/run/ufw-ddns-<hostname>. - If the IP changed, it:
- Removes the old allow rule (or any lingering rules for that
PORT/PROTO). - Adds a new UFW rule:
allow from <current_ip> to any port <PORT> proto <PROTO>.
- Removes the old allow rule (or any lingering rules for that
- Saves the new IP for next run.
All changes are idempotent and logged to stdout.
- Support multiple DDNS hostnames
- Add option to
denyorlimitrules (not onlyallow) - Config file support instead of editing the script directly
- Improve IPv6 handling
- Add logging to syslog instead of only stdout
- Ubuntu/Debian (or any distro using UFW)
ufwinstalled and enableddig(fromdnsutilspackage on Debian/Ubuntu)- Permissions to run
ufw(typically viasudo) - Bash
-
Install dependencies
sudo apt-get update sudo apt-get install ufw dnsutils -y sudo ufw enable -
Place the script
sudo install -m 0755 ufw-ddns-updater.sh /usr/local/bin/ufw-ddns-updater.sh
-
Configure variables (inside the script):
DDNS_HOST="mihome.duckdns.org" # your DDNS hostname PORT=81 # the port you want to protect PROTO="tcp" # "tcp" or "udp"
-
Run once to verify
sudo /usr/local/bin/ufw-ddns-updater.sh sudo ufw status numbered
You should see an entry allowing traffic from your current IP to PORT/PROTO.
Create a oneshot service:
# /etc/systemd/system/ufw-ddns-updater.service
[Unit]
Description=Update UFW rule to allow current DDNS IP
[Service]
Type=oneshot
ExecStart=/usr/local/bin/ufw-ddns-updater.shCreate a timer (every 5 minutes, adjust as desired):
# /etc/systemd/system/ufw-ddns-updater.timer
[Unit]
Description=Run UFW DDNS updater periodically
[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
Unit=ufw-ddns-updater.service
[Install]
WantedBy=timers.targetEnable + start:
sudo systemctl daemon-reload
sudo systemctl enable --now ufw-ddns-updater.timer
sudo systemctl status ufw-ddns-updater.timersudo crontab -e
# Update every 5 minutes
*/5 * * * * /usr/local/bin/ufw-ddns-updater.sh >> /var/log/ufw-ddns-updater.log 2>&1- Script (recommended):
/usr/local/bin/ufw-ddns-updater.sh - State file (auto-managed):
/var/run/ufw-ddns-<DDNS_HOST>
/var/run(or/run) is volatile; this is intentional, as rules will be re-applied by the scheduler on reboot.
- Lockdown risk: This script deletes all UFW rules for the specified
PORT/PROTObefore adding the new allow rule if it detects mismatches. Use a dedicated port for this pattern. - IPv6: The script filters out
(v6)entries when cleaning. It manages IPv4 rules only. - Least privilege: Consider granting passwordless sudo only for the exact
ufwcommands used (via/etc/sudoers.d/...) rather than global passwordless sudo. - Firewall defaults: Make sure your default UFW policy aligns with your intent:
sudo ufw default deny incoming sudo ufw default allow outgoing
No se pudo resolver <host>/ can't resolve hostname- Check
dig <host>manually. Ensure DNS works and your DDNS provider updated.
- Check
- Rule not changing
- Confirm the timer or cron is running.
- Check
/var/run/ufw-ddns-<host>content vsdig +short <host>.
- Multiple rules hanging around
- The script runs a cleanup for
PORT/PROTO, but if you manually edited rules, run:sudo ufw status numbered # then delete by number as needed
- The script runs a cleanup for
Protect TCP port 81 for mihome.duckdns.org:
DDNS_HOST="mihome.duckdns.org"
PORT=81
PROTO="tcp"
sudo /usr/local/bin/ufw-ddns-updater.shIssues and PRs welcome! Please read CONTRIBUTING.md.
This project is licensed under the MIT License — see LICENSE.