DDNS Updater
Overview
DDNS Updater is an open-source dynamic DNS client. On a schedule, it resolves the machine's current public IPv4 and IPv6 address. Whenever that address changes, it calls your DNS provider's API to rewrite the A and AAAA records you asked it to maintain, so a name you own always points at the machine you are running.
It supports more than fifty providers, including Cloudflare, DuckDNS, Namecheap, GoDaddy, Route 53, deSEC, Hetzner, Porkbun, DynV6 and No-IP. A generic custom HTTP provider covers anything not on the list, and it can manage several domains and subdomains at once. A built-in web UI lists every record with its provider, IP version, last update result, current IP and previous-IP history, and it can force an immediate refresh.
DDNS Updater has no user accounts and no authentication of its own, and its records page reveals your domains and your public IP address. The SecureAnyCloud image therefore never exposes it directly. The pinned DDNS Updater 2.10.0 Go binary runs under systemd as an unprivileged service bound to 127.0.0.1:8000, with its health server on 127.0.0.1:9999, and an nginx reverse proxy on port 80 is the only network-reachable surface. nginx puts an HTTP Basic Auth gate in front of the application. The gate password is generated uniquely on the first boot of each VM, so no credential is baked into the image.
What you get:
- DDNS Updater 2.10.0 installed as a single static Go binary and running as the
ddns-updatersystemd service - The DDNS Updater web UI on
:80fronted by nginx, with the application bound to loopback only - Per-VM HTTP Basic Auth (user
admin) protecting the web UI, with a unique password generated on first boot - The application running as the dedicated unprivileged
ddnsupdatersystem user with systemd hardening applied - A valid empty provider configuration at
/var/lib/ddns-updater/config.json, readable only by root and the service, ready for your own provider tokens - An unauthenticated
/healthzendpoint for Azure Load Balancer health probes ddns-updater.serviceandnginx.serviceas systemd units, enabled and active- Unattended security updates left enabled
- 24/7 SecureAnyCloud support
Prerequisites
You need an active Azure subscription, an SSH key pair, and a VNet and subnet in the target region.
Standard_B2s (2 vCPU / 4 GiB RAM) is ample, because DDNS Updater is a small Go process that spends nearly all its time idle.
Configure the NSG to allow inbound 22/tcp from your management network and 80/tcp for the web UI. You also need an account with a supported DNS provider and an API token or key for the zone you want to keep updated.
Step 1 - Deploy from the Azure Marketplace
Sign in to the Azure Portal, choose Create a resource, search the Marketplace for DDNS Updater by SecureAnyCloud, and select Create.
On the Basics tab, pick your subscription, resource group, region and size. Under Administrator account, choose SSH public key and paste your key. Under Inbound port rules, allow SSH (22) and HTTP (80). Then select Review + create -> Create.
Step 2 - Deploy from the Azure CLI
# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:ddns-updater-ubuntu-24-04:default:latest
az vm create \
--resource-group <your-rg> \
--name ddns-updater \
--image sac1647283583153:ddns-updater-ubuntu-24-04:default:latest \
--size Standard_B2s \
--admin-username azureuser \
--ssh-key-values ~/.ssh/id_ed25519.pub \
--vnet-name <your-vnet> --subnet <your-subnet> \
--public-ip-sku Standard
az vm open-port --resource-group <your-rg> --name ddns-updater --port 80 --priority 1010Step 3 - Connect to your VM
ssh azureuser@<vm-public-ip>Step 4 - Confirm the services are running
systemctl is-active ddns-updater.service nginx.serviceBoth services report active. DDNS Updater serves its web UI on the loopback address 127.0.0.1:8000 and its health server on 127.0.0.1:9999. nginx fronts the UI on port 80 and adds the per-VM HTTP Basic Auth gate. Nothing but nginx is reachable from the network:
/usr/local/bin/ddns-updater --version ps -o user= -C ddns-updater | head -1 ss -tlnH | awk '{print $1, $4}' | grep -E ':8000$|:9999$|:80$' | sort -uThe version is 2.10.0, the process runs as the unprivileged ddnsupdater user, and only port 80 is bound on a public address.
Step 5 - Retrieve your web UI password
nginx protects the DDNS Updater web UI with HTTP Basic Auth. The username is admin and a unique password is generated on the first boot of your VM and written to a root-only file:
sudo cat /root/ddns-updater-credentials.txtThis file contains DDNS_UPDATER_USERNAME, DDNS_UPDATER_PASSWORD and the DDNS_UPDATER_URL to open in a browser. On disk, the password exists only as a bcrypt hash in /etc/nginx/.ddns-updater.htpasswd, so no plaintext password ships in the image. Store the password somewhere safe.
Step 6 - Confirm the health endpoint
nginx serves an unauthenticated health endpoint for load balancers and probes:
curl -s http://localhost/healthzIt returns ok. This endpoint never requires authentication, so you can use it as an Azure Load Balancer health probe without giving the probe a password.
Step 7 - Confirm authentication
Because a password is set on first boot, an unauthenticated request to the web UI returns HTTP 401, so nobody reaches DDNS Updater without it. The following commands read the per-VM password from the credentials file and prove the round-trip. An unauthenticated request is rejected, a wrong password is rejected, and only the correct password reaches the UI:
PW=$(sudo grep '^DDNS_UPDATER_PASSWORD=' /root/ddns-updater-credentials.txt | cut -d= -f2-)
echo "healthz : $(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1/healthz)"
echo "unauth : $(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1/)"
echo "wrongpw : $(curl -s -o /dev/null -w '%{http_code}' -u admin:wrong-password http://127.0.0.1/)"
echo "authed : $(curl -s -o /dev/null -w '%{http_code}' -u admin:$PW http://127.0.0.1/)" curl -s -u admin:$PW http://127.0.0.1/ | grep -E '<title>|<th>' | sed 's/^[[:space:]]*//' | head -8It prints healthz : 200, unauth : 401, wrongpw : 401, authed : 200, then the page title and the records table headers, which proves that the authenticated response is the DDNS Updater web UI.
A browser meets the same gate. Opening http://<vm-public-ip>/ without credentials returns nginx's 401 challenge, and the browser prompts for the username and password before anything is shown:
Step 8 - Sign in to the web UI
Browse to http://<vm-public-ip>/. When your browser prompts for a username and password, enter admin and the password from Step 5.
DDNS Updater opens on its records page, a single table that lists every record it manages. On a freshly deployed VM the table is empty, because the image ships with no provider configuration and your DNS credentials are yours to add. Once records are configured, the table shows the following for each one: the domain and owner, the provider it belongs to, whether it tracks IPv4 or IPv6, the result of the last update attempt, the address currently published, and any previous addresses.
Step 9 - Add your first DNS record
DDNS Updater reads its records from /var/lib/ddns-updater/config.json. The image ships a valid but empty configuration, which you can confirm:
sudo jq . /var/lib/ddns-updater/config.json
sudo stat -c '%a %U:%G %n' /var/lib/ddns-updater/config.jsonIt prints an empty settings array and mode 640 root:ddnsupdater. The file is readable only by root and the service account, because your provider API tokens will live there.
To add a record, edit the file and put one object in settings per DNS record. Each provider takes different fields, and the provider documentation lists them all. A Cloudflare record looks like this:
sudo nano /var/lib/ddns-updater/config.json{ "settings": [ { "provider": "cloudflare", "zone_identifier": "<your-zone-id>", "domain": "home.example.com", "ttl": 600, "token": "<your-api-token>", "ip_version": "ipv4" } ] }Restart the service so that it picks up the new configuration:
sudo systemctl restart ddns-updater systemctl is-active ddns-updaterCheck the service after every configuration change. DDNS Updater validates provider settings at start-up. If any record is malformed, it logs the reason and exits rather than starting with a broken record. If systemctl is-active reports anything other than active, read the reason with journalctl -u ddns-updater -n 20 (Step 11), fix the entry and restart again.
Step 10 - Force an update and watch it work
DDNS Updater checks your public IP every five minutes by default. You do not have to wait, because the web UI's /update endpoint forces an immediate refresh of every record.
PW=$(sudo grep '^DDNS_UPDATER_PASSWORD=' /root/ddns-updater-credentials.txt | cut -d= -f2-)
curl -s -w '\nHTTP %{http_code}\n' -u admin:$PW http://127.0.0.1/updateIt returns HTTP 202 and reports how long the refresh took. Visiting http://<vm-public-ip>/update in the browser does the same thing.
Return to the records page, where each record shows its refreshed status together with the public IP address now on record. The status is Success where the published address was changed and Up to date where it already matched.
Step 11 - Read the records table and the log
Everything the web UI shows is also readable from the shell. This is useful for monitoring and for diagnosing a record that will not update:
PW=$(sudo grep '^DDNS_UPDATER_PASSWORD=' /root/ddns-updater-credentials.txt | cut -d= -f2-)
curl -s -u admin:$PW http://127.0.0.1/ | sed -n '/<tbody>/,/<\/tbody>/p' | sed -E 's/<[^>]*>//g' | grep -v '^[[:space:]]*$'
sudo journalctl -u ddns-updater -n 15 --no-pagerThe log records the public IP address DDNS Updater resolved, the address each domain currently resolves to, and every update it sent to a provider. A rejected API token or a rate-limited provider is therefore always visible here.
Maintenance
- Password: The web UI password is set on first boot and stored as a bcrypt entry in
/etc/nginx/.ddns-updater.htpasswd. To change it, runsudo htpasswd -B /etc/nginx/.ddns-updater.htpasswd adminand thensudo systemctl reload nginx. - Configuration: Records live in
/var/lib/ddns-updater/config.json. Validate your JSON withjq . /var/lib/ddns-updater/config.jsonbefore restarting, and always confirm that the service came back up afterwards. An invalid provider block makes DDNS Updater exit at start-up rather than run with a broken record. - Update interval: The check period, request timeouts and other runtime settings live in
/etc/ddns-updater/ddns-updater.env. For example, setPERIOD=1mfor a faster check, and then runsudo systemctl restart ddns-updater. The full variable reference is in the upstream documentation. - Loopback binding: DDNS Updater is bound to
127.0.0.1:8000and its health server to127.0.0.1:9999throughLISTENING_ADDRESSandHEALTH_SERVER_ADDRESSin that same env file, so nginx is the only path in. Keep it that way. The application has no authentication of its own, so pointing it at a public interface would publish your domains and IP history to anyone who asks. - Restrict access: The web UI is served over plain HTTP on port 80. For production, restrict port 80 to trusted IP ranges in your Network Security Group and front it with TLS on
:443using your own domain and certificate. - Storage: The record configuration and the update history (
updates.json) live under/var/lib/ddns-updater. Back up that directory to keep your provider settings and IP history. - Security patches: unattended-upgrades remains enabled, so the OS continues to receive security updates automatically.
Support
SecureAnyCloud provides 24/7 expert support for this image. Contact sales@secureanycloud.com.
Deploy on Azure
Launch DDNS Updater on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.
View this image
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com