Cloudflare
CF provides an excellent DDNS service and all you need is curl. Pretty simple, right?
ZONE_ID=
RECORD_ID=
TOKEN=
IP=$(curl -4 --silent --show-error --fail --max-time 10 https://api.ipify.org)
curl --silent --show-error --fail --max-time 10 \
-X PATCH \
"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$RECORD_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data "{\"content\":\"$IP\"}")
Who needs ddclient!
Noice! Done!
OK - well, you need to know your IDs, and you have to set up a token that only has DNS rights to the specific zone in question. And you need to handle errors, so I guess you need something more like…
vi ~/ddns-update
#!/bin/bash
set -euo pipefail
# Configuration
ZONE="your.org"
RECORD="server.your.org"
TOKEN="XXXXXXX"
# State dirs (systemd uses /var/lib/SERVICENAME)
STATE_DIR="/var/lib/cloudflare-ddns"
ZONE_ID_FILE="$STATE_DIR/zone_id"
RECORD_ID_FILE="$STATE_DIR/record_id"
LAST_IP_FILE="$STATE_DIR/last_ip"
# Load or fetch zone and record IDs needed for CF's API
if [ -f "$ZONE_ID_FILE" ]; then
ZONE_ID=$(<"$ZONE_ID_FILE")
else
ZONE_ID=$(curl --silent --show-error --fail --max-time 10 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"https://api.cloudflare.com/client/v4/zones?name=$ZONE" \
| jq -r '.result[0].id')
echo "$ZONE_ID" > "$ZONE_ID_FILE"
fi
[ -n "$ZONE_ID" ] && [ "$ZONE_ID" != "null" ] || {
echo "Failed to discover Cloudflare Zone ID"
exit 1
}
if [ -f "$RECORD_ID_FILE" ]; then
RECORD_ID=$(<"$RECORD_ID_FILE")
else
RECORD_ID=$(curl --silent --show-error --fail --max-time 10 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?type=A&name=$RECORD" \
| jq -r '.result[0].id')
echo "$RECORD_ID" > "$RECORD_ID_FILE"
fi
[ -n "$RECORD_ID" ] && [ "$RECORD_ID" != "null" ] || {
echo "Failed to discover Cloudflare Record ID"
exit 1
}
# Load the last IP.
LAST_IP=""
if [ -f "$LAST_IP_FILE" ]; then
LAST_IP=$(<"$LAST_IP_FILE")
fi
# Get the current IP
IP=$(curl -4 --silent --show-error --fail --max-time 10 https://api.ipify.org)
[[ $IP =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || {
echo "Failed validation check for data: $IP"
exit 1
}
# If they aren't the same
if [ "$IP" != "$LAST_IP" ]; then
echo "Updating IP to $IP"
RESPONSE=$(curl --silent --show-error --fail --max-time 10 \
-X PATCH \
"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$RECORD_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data "{\"content\":\"$IP\"}")
RESULT=$(echo "$RESPONSE" | jq -r '.success')
if [ "$RESULT" = "true" ]; then
echo "$IP" > "$LAST_IP_FILE"
echo "Update successful"
else
echo "Cloudflare update failed"
echo "$RESPONSE" | jq -r '.errors[]?.message'
exit 1
fi
fi
Whew, OK. Now you need to get this running somehow. Cron? Oh, right. Not in Debian 13 by default. Systemd timer it is, then.
sudo install -m 700 ddns-update /usr/local/sbin/ddns-update
sudo vi /etc/systemd/system/cloudflare-ddns.service
[Unit]
Description=Cloudflare DDNS Update
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
StateDirectory=cloudflare-ddns
StateDirectoryMode=0700
ExecStart=/usr/local/sbin/ddns-update
sudo vi /etc/systemd/system/cloudflare-ddns.timer
[Unit]
Description=Run Cloudflare DDNS updater every 15 minutes
LogLevelMax=warning
[Timer]
OnBootSec=1min
OnUnitActiveSec=15min
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now cloudflare-ddns.timer
Queue Spongebob: Two hours later…and we’ve just made a (worse) copy of ddclient that does (a lot) less.
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.