Skip to content

Instantly share code, notes, and snippets.

@federicociro
Last active November 30, 2025 22:26
Show Gist options
  • Select an option

  • Save federicociro/f5058f12cafa5757306f81549e5f584c to your computer and use it in GitHub Desktop.

Select an option

Save federicociro/f5058f12cafa5757306f81549e5f584c to your computer and use it in GitHub Desktop.
Bash script to update DNS records in Cloudflare if local IP changed

Cloudflare Multi-Domain DDNS Updater

A bash script to automatically update multiple Cloudflare DNS A records when your public IP address changes. Perfect for home servers and dynamic IP addresses. And for when your pfSense/OPNsense is broken / sold.

Features

  • Support for unlimited domains
  • Works with subdomains (sub.domain.com, www.domain.com, etc.)
  • Easy configuration using simple arrays
  • API Token support (more secure than Global API Key)
  • Checks if IP actually changed before updating
  • Success/failure reporting for each domain
  • Systemd integration for automatic updates
  • Detailed logging

Subdomain Support

The script works perfectly with:

  • Root domains: domain.com
  • Subdomains: sub.domain.com, api.domain.com, etc.
  • www subdomain: www.domain.com
  • Multiple subdomains per domain: All use the same Zone ID, different Record IDs

Example configuration:

DOMAINS=(
    "example.com:abc123:record1"           # root domain
    "www.example.com:abc123:record2"       # www subdomain (same zone ID)
    "api.example.com:abc123:record3"       # api subdomain (same zone ID)
    "otherdomain.com:xyz789:record4"       # different domain (different zone ID)
)

## Files Included

### Using Global API Key (legacy)
1. **cloudflare-ddns-multi.sh** - Main DDNS update script
2. **cloudflare-get-record-ids.sh** - Helper to fetch DNS record IDs

### Using API Token (RECOMMENDED - more secure)
1. **cloudflare-ddns-token.sh** - Main DDNS update script with API token
2. **cloudflare-get-record-ids-token.sh** - Helper to fetch DNS record IDs with API token
3. **API-TOKEN-GUIDE.md** - Complete guide on creating and using API tokens

### Systemd files (work with either version)
1. **cloudflare-ddns.service** - Systemd service file
2. **cloudflare-ddns.timer** - Systemd timer file (runs every 5 minutes)

## Installation

### Choose Your Authentication Method

**Option A: API Token (RECOMMENDED)** - More secure, limited permissions
- See **[API-TOKEN-GUIDE.md](API-TOKEN-GUIDE.md)** for detailed instructions
- Use `cloudflare-ddns-token.sh` and `cloudflare-get-record-ids-token.sh`

**Option B: Global API Key (Legacy)** - Full account access
- Use `cloudflare-ddns-multi.sh` and `cloudflare-get-record-ids.sh`

The instructions below work for both methods. Just use the appropriate script names.

---

### Step 1: Get your Cloudflare credentials

**For API Token (Option A):**
1. Follow the complete guide in **[API-TOKEN-GUIDE.md](API-TOKEN-GUIDE.md)**
2. Create a token with **Zone.DNS - Edit** permission for **All zones**
3. Copy and save your token securely

**For Global API Key (Option B):**
1. Log in to [Cloudflare](https://dash.cloudflare.com/)
2. Get your **Global API Key**: Profile → API Tokens → Global API Key → View
3. Note your **email address** associated with your Cloudflare account

### Step 2: Get Zone IDs

For each domain:
1. Go to your domain's overview page in Cloudflare
2. Scroll down to find the **Zone ID** on the right sidebar
3. Copy and save it

### Step 3: Configure the helper script

**For API Token:**
Edit `cloudflare-get-record-ids-token.sh`:

```bash
# Set your API token
AUTH_TOKEN="your_api_token_here"

# Add your domains with their zone IDs (works with subdomains!)
DOMAINS=(
    "example.com:zone_id_for_example"
    "www.example.com:zone_id_for_example"      # same zone as example.com
    "api.otherdomain.com:zone_id_for_other"    # subdomain of different domain
)

For Global API Key: Edit cloudflare-get-record-ids.sh:

# Set your credentials
AUTH_EMAIL="your-email@domain.com"
AUTH_KEY="your-global-api-key"

# Add your domains with their zone IDs (works with subdomains!)
DOMAINS=(
    "example.com:zone_id_for_example"
    "www.example.com:zone_id_for_example"
    "api.example2.com:zone_id_for_example2"
)

Step 4: Get Record IDs

Make the helper script executable and run it:

For API Token:

chmod +x cloudflare-get-record-ids-token.sh
./cloudflare-get-record-ids-token.sh

For Global API Key:

chmod +x cloudflare-get-record-ids.sh
./cloudflare-get-record-ids.sh

The script will output the record IDs in the correct format. Copy these lines.

Step 5: Configure the main script

For API Token: Edit cloudflare-ddns-token.sh:

# Set your API token
AUTH_TOKEN="your_api_token_here"

# Paste the domain configurations from step 4
# This works with root domains AND subdomains!
DOMAINS=(
    "example.com:zone_id:record_id"
    "www.example.com:zone_id2:record_id2"
    "api.example.com:zone_id3:record_id3"
    # Add as many as you need
)

# Optional: Adjust these settings
TTL=180              # DNS TTL in seconds
PROXIED=true         # Set to false to disable Cloudflare proxy

For Global API Key: Edit cloudflare-ddns-multi.sh:

# Set your credentials
AUTH_EMAIL="your-email@domain.com"
AUTH_KEY="your-global-api-key"

# Paste the domain configurations from step 4
DOMAINS=(
    "example.com:zone_id:record_id"
    "example2.com:zone_id2:record_id2"
    # Add as many as you need
)

# Optional: Adjust these settings
TTL=180              # DNS TTL in seconds
PROXIED=true         # Set to false to disable Cloudflare proxy

Step 6: Install the script

For API Token:

# Make script executable
chmod +x cloudflare-ddns-token.sh

# Copy to system location
sudo cp cloudflare-ddns-token.sh /usr/local/bin/cloudflare-ddns-multi.sh

# Test the script
sudo /usr/local/bin/cloudflare-ddns-multi.sh

For Global API Key:

# Make script executable
chmod +x cloudflare-ddns-multi.sh

# Copy to system location
sudo cp cloudflare-ddns-multi.sh /usr/local/bin/

# Test the script
sudo /usr/local/bin/cloudflare-ddns-multi.sh

Note: Both versions are copied to the same final location (/usr/local/bin/cloudflare-ddns-multi.sh) so the systemd service works with either.

Step 7: Set up systemd (optional but recommended)

# Copy systemd files
sudo cp cloudflare-ddns.service /etc/systemd/system/
sudo cp cloudflare-ddns.timer /etc/systemd/system/

# Reload systemd
sudo systemctl daemon-reload

# Enable and start the timer
sudo systemctl enable cloudflare-ddns.timer
sudo systemctl start cloudflare-ddns.timer

# Check timer status
sudo systemctl status cloudflare-ddns.timer

Usage

Manual execution

sudo /usr/local/bin/cloudflare-ddns-multi.sh

View logs

# If using systemd
sudo journalctl -u cloudflare-ddns.service -f

# Or check the log file
sudo tail -f /var/log/cloudflare-ddns.log

Check timer status

# See when the timer last ran and when it will run next
sudo systemctl list-timers cloudflare-ddns.timer

# View timer logs
sudo journalctl -u cloudflare-ddns.timer -f

Add more domains

Simply edit /usr/local/bin/cloudflare-ddns-multi.sh and add more entries to the DOMAINS array:

DOMAINS=(
    "domain1.com:zoneid1:recordid1"
    "domain2.com:zoneid2:recordid2"
    "newdomain.com:newzoneid:newrecordid"  # Just add here
)

Then restart the timer:

sudo systemctl restart cloudflare-ddns.timer

Troubleshooting

Script fails to update DNS

  1. Verify your credentials are correct
  2. Check that your API key has the necessary permissions
  3. Ensure the zone IDs and record IDs are correct
  4. Check the logs for error messages

IP not detected

Try the alternative IP detection method in the script:

# Comment out the default method
# PUBLIC_IP=$(dig +short myip.opendns.com @resolver1.opendns.com) || exit 1

# Uncomment the alternative
PUBLIC_IP=$(curl --silent https://api.ipify.org) || exit 1

Test individual domain update

You can test updating a specific domain using curl:

curl -X PUT \
    "https://api.cloudflare.com/client/v4/zones/ZONE_ID/dns_records/RECORD_ID" \
    -H "Content-Type: application/json" \
    -H "X-Auth-Email: YOUR_EMAIL" \
    -H "X-Auth-Key: YOUR_API_KEY" \
    -d '{
        "type": "A",
        "name": "domain.com",
        "content": "YOUR_IP",
        "ttl": 180,
        "proxied": true
    }'

Configuration Options

Change update frequency

Edit cloudflare-ddns.timer:

# Update every 1 minute
OnUnitActiveSec=60

# Update every 10 minutes
OnUnitActiveSec=600

# Update every hour
OnUnitActiveSec=3600

Then reload:

sudo systemctl daemon-reload
sudo systemctl restart cloudflare-ddns.timer

Disable Cloudflare proxy

In the main script, set:

PROXIED=false

Change TTL

In the main script, adjust:

TTL=300  # 5 minutes
TTL=600  # 10 minutes
TTL=3600 # 1 hour

Security Notes

  • Keep your API key secure and don't commit it to version control
  • Consider using API tokens instead of the Global API Key for better security
  • The script stores the current IP in /tmp/cloudflare-ddns-ip-record
  • Logs may contain IP addresses

License

This script is provided as-is for personal and commercial use.

[Unit]
Description=Cloudflare Multi-Domain DDNS Updater
Documentation=https://api.cloudflare.com/#dns-records-for-a-zone-update-dns-record
[Service]
Type=oneshot
ExecStart=/usr/local/bin/cloudflare-ddns-multi.sh
StandardOutput=append:/var/log/cloudflare-ddns.log
StandardError=append:/var/log/cloudflare-ddns.log
[Install]
WantedBy=default.target
[Unit]
Description=Check Cloudflare DNS records every 5 minutes
RefuseManualStart=no
RefuseManualStop=no
[Timer]
# Execute job if it missed a run due to machine being off
Persistent=true
# Run 120 seconds after boot for the first time
OnBootSec=120
# Run every 5 minutes thereafter
OnUnitActiveSec=300
# File describing job to execute
Unit=cloudflare-ddns.service
[Install]
WantedBy=timers.target
#!/bin/bash
# Helper script to fetch DNS record IDs for multiple domains using API Token
set -eu
# Cloudflare Authentication
# Option 1: Use API Token (RECOMMENDED - more secure)
AUTH_TOKEN="your_api_token_here"
# Option 2: Use Global API Key (legacy method)
# AUTH_EMAIL="example@domain.tld"
# AUTH_KEY="cloudflare_auth_key"
# Define your domains and zone IDs
# Format: "domain_name:zone_id"
# Works with root domains (domain.com) and subdomains (sub.domain.com)
DOMAINS=(
"domain1.com:zoneid1"
"sub.domain2.com:zoneid2"
"www.domain3.com:zoneid3"
# Add more domains/subdomains here
)
echo "=========================================="
echo "Cloudflare DNS Record ID Fetcher"
echo "=========================================="
echo ""
for domain_config in "${DOMAINS[@]}"; do
# Parse domain configuration
IFS=':' read -r DOMAIN ZONE_ID <<< "$domain_config"
echo "----------------------------------------"
echo "Domain: $DOMAIN"
echo "Zone ID: $ZONE_ID"
echo ""
# Fetch DNS records using API Token
if [ -n "${AUTH_TOKEN:-}" ]; then
# Using API Token (recommended)
RESPONSE=$(curl -s -X GET \
"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?name=$DOMAIN&type=A" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $AUTH_TOKEN")
else
# Using Global API Key (legacy)
RESPONSE=$(curl -s -X GET \
"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?name=$DOMAIN&type=A" \
-H "Content-Type: application/json" \
-H "X-Auth-Email: $AUTH_EMAIL" \
-H "X-Auth-Key: $AUTH_KEY")
fi
# Check if request was successful
SUCCESS=$(echo "$RESPONSE" | grep -o '"success":[^,]*' | grep -o '[^:]*$')
if [ "$SUCCESS" = "true" ]; then
# Extract record ID using grep and sed
RECORD_ID=$(echo "$RESPONSE" | grep -o '"id":"[^"]*"' | head -1 | sed 's/"id":"//;s/"//')
if [ -n "$RECORD_ID" ]; then
echo "✓ Record ID: $RECORD_ID"
echo ""
echo "Add this to your DOMAINS array:"
echo " \"$DOMAIN:$ZONE_ID:$RECORD_ID\""
else
echo "✗ No A record found for $DOMAIN"
echo " You may need to create the A record first in Cloudflare"
fi
else
echo "✗ Failed to fetch records"
echo "Response: $RESPONSE"
fi
echo ""
done
echo "=========================================="
echo "Done!"
echo "=========================================="
#!/bin/bash
# Cloudflare DDNS Multi-domain con control de proxy por registro
# Uso: Añade tus dominios en DOMAINS con el formato: "nombre,zone_id,record_id,proxy"
# donde proxy puede ser "true" o "false"
set -eu
# Configuración de autenticación con API Token
AUTH_TOKEN="your_cloudflare_api_token"
# Lista de dominios en formato: "nombre,zone_id,record_id,proxy"
DOMAINS=(
"domain1.com,zoneid1,recordid1,true"
"sub.domain2.com,zoneid2,recordid2,false"
"www.domain3.com,zoneid3,recordid3,true"
# Add more domains/subdomains here following the same pattern
)
# Archivo para almacenar la última IP conocida
IP_RECORD="/tmp/ip-record"
touch "$IP_RECORD"
RECORDED_IP=$(cat "$IP_RECORD" 2>/dev/null || echo "")
# Obtener fecha actual
NOW=$(date -u +"%Y-%m-%d %H:%M:%S %Z")
# Obtener la IP pública actual
PUBLIC_IP=$(dig +short myip.opendns.com @resolver1.opendns.com) || exit 1
# Alternativa: PUBLIC_IP=$(curl --silent https://api.ipify.org) || exit 1
# Si la IP no ha cambiado, salir
if [ "$PUBLIC_IP" = "$RECORDED_IP" ]; then
echo "$NOW | Actual Public IP is: $PUBLIC_IP | Public IP is still OK."
exit 0
fi
# La IP ha cambiado, actualizar registros
echo "$NOW | Public IP changed from $RECORDED_IP to $PUBLIC_IP"
echo "$PUBLIC_IP" > "$IP_RECORD"
# Procesar cada dominio
for domain_entry in "${DOMAINS[@]}"; do
# Separar los valores usando IFS (corregido para evitar problemas)
A_RECORD_NAME=$(echo "$domain_entry" | cut -d',' -f1)
ZONE_ID=$(echo "$domain_entry" | cut -d',' -f2)
A_RECORD_ID=$(echo "$domain_entry" | cut -d',' -f3)
PROXIED=$(echo "$domain_entry" | cut -d',' -f4)
echo ""
echo "Updating DNS for: $A_RECORD_NAME (proxy: $PROXIED)"
# Crear el registro JSON
RECORD=$(cat <<EOF
{
"type": "A",
"name": "$A_RECORD_NAME",
"content": "$PUBLIC_IP",
"ttl": 180,
"proxied": $PROXIED
}
EOF
)
# Actualizar el registro en Cloudflare usando API Token (URL CORREGIDA)
RESPONSE=$(curl -s "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$A_RECORD_ID" \
-X PUT \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $AUTH_TOKEN" \
-d "$RECORD")
# Verificar si la actualización fue exitosa
SUCCESS=$(echo "$RESPONSE" | grep -o '"success":[^,]*' | grep -o '[^:]*$')
if [ "$SUCCESS" = "true" ]; then
echo "✓ Successfully updated $A_RECORD_NAME"
else
echo "✗ Failed to update $A_RECORD_NAME"
echo "Response: $RESPONSE"
fi
done
echo ""
echo "Update process completed."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment