⚙️ Automated Cloudflare DNS Updates

Cloudflare acts as the public ingress layer for the homelab, so the WAN IP must remain accurate in DNS. Because residential ISPs periodically rotate IP addresses, a lightweight DDNS script runs on the homelab to automatically update the Cloudflare A record whenever the WAN IP changes. The script uses Cloudflare’s API.

This implementation keeps all secrets in a separate file, ensuring credentials never appear in scripts, logs, or version control.

📁 Secrets File

Create a secure file containing your Cloudflare credentials:

/etc/cloudflare/secrets.conf

Example:

CF_API_TOKEN="your_api_token"
ZONE_ID="your_zone_id"
RECORD_ID="your_record_id"
RECORD_NAME="refol.us"

Lock down permissions:

chmod 600 /etc/cloudflare/secrets.conf

🧩 DDNS Update Script

This script loads the secrets file, checks the current public IP, compares it to the value stored in Cloudflare, and updates the DNS record only when necessary.

#!/usr/bin/env bash

SECRET_FILE="/etc/cloudflare/secrets.conf"

if [ ! -f "$SECRET_FILE" ]; then
    echo "Missing secrets file: $SECRET_FILE"
    exit 1
fi

# shellcheck source=/etc/cloudflare/secrets.conf
source "$SECRET_FILE"

REQUIRED_VARS=(CF_API_TOKEN ZONE_ID RECORD_ID RECORD_NAME)
for var in "${REQUIRED_VARS[@]}"; do
    if [ -z "${!var}" ]; then
        echo "Missing required variable: $var"
        exit 1
    fi
done

CURRENT_IP=$(curl -s https://api.ipify.org)

CF_DATA=$(curl -s -X GET \
  "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$RECORD_ID" \
  -H "Authorization: Bearer $CF_API_TOKEN" \
  -H "Content-Type: application/json")

CF_IP=$(echo "$CF_DATA" | jq -r '.result.content')
CF_PROXIED=$(echo "$CF_DATA" | jq -r '.result.proxied')

if [ "$CURRENT_IP" = "$CF_IP" ]; then
    echo "IP unchanged ($CURRENT_IP). No update needed."
    exit 0
fi

echo "Updating Cloudflare DNS: $CF_IP$CURRENT_IP"

UPDATE=$(curl -s -X PUT \
  "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$RECORD_ID" \
  -H "Authorization: Bearer $CF_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data "{\"type\":\"A\",\"name\":\"$RECORD_NAME\",\"content\":\"$CURRENT_IP\",\"ttl\":120,\"proxied\":$CF_PROXIED}")

echo "$UPDATE"

⏱️ Scheduling

Add a cron entry to run the script periodically:

*/5 * * * * /usr/local/bin/cloudflare-ddns.sh >/dev/null 2>&1

This ensures Cloudflare always reflects the current WAN IP without exposing the homelab directly.