Using DigitalOcean as Dynamic DNS
Recently, I’ve migrated my personal infrastructure to DigitalOcean. Since DigitalOcean offers DNS service at no added cost, I’ve moved my DNS from Cloudflare over to them as well. I simply trust a company I’m paying more than a company that I’m not. Previously, I had been using this excellent Cloudflare dynamic DNS script, and so used it as a basis to write my own for DigitalOcean.
Prerequisites
-
curl
The shell script requires
curl
installed. Luckily, most Linux distributions havecurl
packaged officially. -
Domain name
DigitalOcean’s DNS is a fully-fledged DNS service but they’re not a registrar. So, you’ll have to bring your own domain.
-
A record
Once you’ve added your domain to DigitalOcean, add an A record for the subdomain you want to point to your dynamic IP. You’ll want a reasonably low TTL - I use 300 seconds (5 minutes).
-
DigitalOcean personal access token
In order to change the A record programmatically, you’ll need access to DigitalOcean’s API. Go to DigitalOcean’s API page and click “Generate New Token”. The token will need read and write permissions in order to change the IP that the A record points to.
-
Record ID
This one’s a bit tricky. The API to change DNS records requires that you identify which record you want to change via a Record ID. However, DigitalOcean’s online management site does not show this ID anywhere. You’ll have to query the API directly using
curl
:curl -X GET \ -H "Content-Type: application/json" \ -H "Authorization: Bearer {DigitalOcean personal access token}" \ "https://api.digitalocean.com/v2/domains/{Domain name: mysite.com}/records" \ | python3 -m json.tool
Remember to fill out the token and domain parts. The pipe into Python is to get pretty-printing of the JSON and not strictly necessary. The response will list all your existing domain records, so you’ll have to look for the
id
of the one corresponding to the subdomain you want to point to your dynamic IP.
The Shell Script
Next, install the shell script on a computer behind the dynamic IP. I’ve put mine at /usr/local/bin/digitalocean-ddns
. Again, don’t forget to fill out the placeholders on the first few lines.
#/usr/bin/env sh
set -e
TOKEN={DigitalOcean personal access token}
RECORD_ID={DNS Record ID: 12345678}
DOMAIN_NAME="{Domain name: mysite.com}"
TMP_DIR="/tmp/digitalocean-ddns"
PREV_IP_FILE="$TMP_DIR/public-ip.txt"
API_RESP_FILE="$TMP_DIR/response.json"
# Ensure TMP_DIR exists
mkdir -p "$TMP_DIR"
# Get previous IP address
_PREV_IP=$(cat "$PREV_IP_FILE" &> /dev/null)
# Lookup current public IP
_IP=$(curl --silent https://api.ipify.org)
# If new/previous IPs match, no need for an update.
if [ "$_IP" = "$_PREV_IP" ]; then
exit 0
fi
# Construct JSON payload for request
_UPDATE=$(cat <<EOF
{ "data": "$_IP" }
EOF
)
# Fire API request
curl "https://api.digitalocean.com/v2/domains/$DOMAIN_NAME/records/$RECORD_ID" \
--silent \
-X PUT \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d "$_UPDATE" > "$API_RESP_FILE" && \
echo $_IP > "$PREV_IP_FILE"
Since it contains a sensitive API token, it’s best practice to restrict reading/writing/executing this script to root (700 permissions).
Systemd Files
Many Linux distributions these days come with systemd
, and systemd
timers have many advantages over good ol’ cron
. You’ll need two files: a service
file and a timer
file. The service file runs the shell script. In /etc/systemd/system/digitalocean-ddns.service
:
[Unit]
Description=Update DNS entry for this host to current IP
[Service]
Type=oneshot
ExecStart=/bin/sh /usr/local/bin/digitalocean-ddns
The timer starts the service at regular intervals. In /etc/systemd/system/digitalocean-ddns.timer
[Unit]
Description=Update DNS entry in digitalocean every 5 minutes
[Timer]
OnBootSec=1min
OnCalendar=*:0/5
Unit=digitalocean-ddns.service
[Install]
WantedBy=basic.target
Now, refresh systemd’s configuration, then enable and start the timer. Enabling the timer makes it active after a fresh boot, and starting the timer make it active now.
$ sudo systemctl daemon-reload
$ sudo systemctl enable digitalocean-ddns.timer
$ sudo systemctl start digitalocean-ddns.timer
The last thing to do is to do a sanity check - you can use DigitalOcean’s online management site to set your subdomain to point to a known IP, like 1.1.1.1
for Cloudflare. Then, directly run the shell script with sudo
. On refresh, the management site should show your subdomain pointing to your dynamic IP. A second sanity check would be to let the timer run the script. Make sure you delete /tmp/digitalocean-ddns/public-ip.txt
first, or else the API request won’t be made!
Debugging
Since this is a pretty simple shell script, it should be pretty easy to debug. Some pointers:
-
Check the
API_RESP_FILE
file (by default/tmp/digitalocean-ddns/response.json
) to see what the JSON payload of the response is. -
Change the
set -e
line at the top toset -ex
. It’ll then print each line before it executes.
Notes on the Script
-
Why
curl ipify.org
as opposed todig myip.opendns.com
?dig myip.opendns.com
is indeed faster (as noted by the original Cloudflare ddns script I linked in the intro). However, without DNSSEC or any form of DNS authentication, it’s possible for a man-in-the-middle attacker to change what your A record points to simply by giving you a faked answer fromopendns.com
. Withipify.org
’s TLS-protected API, it’s harder. I didn’t think the slight speed and lower data usage warranted the downgrade in security. -
How fast will this update the DNS record when my IP changes?
If the record’s TTL is 300 seconds and the timer is run every 5 minutes, the maximum possible lag should be 10 minutes.