Skip to content

Instantly share code, notes, and snippets.

@benkulbertis
Last active September 30, 2024 05:52
Show Gist options
  • Save benkulbertis/fff10759c2391b6618dd to your computer and use it in GitHub Desktop.
Save benkulbertis/fff10759c2391b6618dd to your computer and use it in GitHub Desktop.
Cloudflare API v4 Dynamic DNS Update in Bash
#!/bin/bash
# CHANGE THESE
auth_email="user@example.com"
auth_key="c2547eb745079dac9320b638f5e225cf483cc5cfdda41" # found in cloudflare account settings
zone_name="example.com"
record_name="www.example.com"
# MAYBE CHANGE THESE
ip=$(curl -s http://ipv4.icanhazip.com)
ip_file="ip.txt"
id_file="cloudflare.ids"
log_file="cloudflare.log"
# LOGGER
log() {
if [ "$1" ]; then
echo -e "[$(date)] - $1" >> $log_file
fi
}
# SCRIPT START
log "Check Initiated"
if [ -f $ip_file ]; then
old_ip=$(cat $ip_file)
if [ $ip == $old_ip ]; then
echo "IP has not changed."
exit 0
fi
fi
if [ -f $id_file ] && [ $(wc -l $id_file | cut -d " " -f 1) == 2 ]; then
zone_identifier=$(head -1 $id_file)
record_identifier=$(tail -1 $id_file)
else
zone_identifier=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=$zone_name" -H "X-Auth-Email: $auth_email" -H "X-Auth-Key: $auth_key" -H "Content-Type: application/json" | grep -Po '(?<="id":")[^"]*' | head -1 )
record_identifier=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$zone_identifier/dns_records?name=$record_name" -H "X-Auth-Email: $auth_email" -H "X-Auth-Key: $auth_key" -H "Content-Type: application/json" | grep -Po '(?<="id":")[^"]*')
echo "$zone_identifier" > $id_file
echo "$record_identifier" >> $id_file
fi
update=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$zone_identifier/dns_records/$record_identifier" -H "X-Auth-Email: $auth_email" -H "X-Auth-Key: $auth_key" -H "Content-Type: application/json" --data "{\"id\":\"$zone_identifier\",\"type\":\"A\",\"name\":\"$record_name\",\"content\":\"$ip\"}")
if [[ $update == *"\"success\":false"* ]]; then
message="API UPDATE FAILED. DUMPING RESULTS:\n$update"
log "$message"
echo -e "$message"
exit 1
else
message="IP changed to: $ip"
echo "$ip" > $ip_file
log "$message"
echo "$message"
fi
@channprj
Copy link

Good Idea! 👍

@4ft35t
Copy link

4ft35t commented Sep 20, 2018

mod for openwrt, grep of busybox doesn't support -P option
https://gist.github.com/4ft35t/510897486bc6986d19cac45b3b9ca1d0

@boypt
Copy link

boypt commented Dec 5, 2018

I have my version of updating IPv6 address for AAAA record:
https://gist.github.com/pentie/4827058990343db9a5eede346d76ba0f

@vircloud
Copy link

Line 27, will report an error on some platforms,like

[: too many arguments

change to:

if [ "$ip" == "$old_ip" ]; then

fix that.

@minhazulOO7
Copy link

THANKS MAN! Was looking for this! 😃

@fire1ce
Copy link

fire1ce commented Jan 15, 2019

If you want to use the script from crontab without problems add this:
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
cd "$(dirname "$0")"

the PATH with use the system bash's environment variables
the CD commands with change the directory to the script's. so all the commands will be executed with that folder.
This will solve the problem when executing the script with crontab so it will be able to write and read the files like:
"ip.txt"
"cloudflare.ids"
"cloudflare.log"

@fire1ce
Copy link

fire1ce commented Jan 15, 2019

if you want to use the script to update local ip, change ip variable to:
ip=$(ifconfig | grep -Eo 'inet (addr:)?([0-9].){3}[0-9]' | grep -Eo '([0-9].){3}[0-9]' | grep -v '127.0.0.1')

@minhazulOO7
Copy link

minhazulOO7 commented May 6, 2019

Thanks @benkulbertis!

I edited and made this gist.

Hope it helps somebody.

@nunesgh
Copy link

nunesgh commented May 31, 2019

For those having the same issue as @ptts, @Kiendeleo, and @danieluramg, consider checking if the auth_key parameter you have set is indeed your Cloudflare API Key.

You can find your Cloudflare API Key going to My Profile, which is located under the menu at the top right of the Cloudflare Dashboard. Once there, scroll down to API Keys and locate Global API Key. Click View, enter your password, and you will have access to your API Key.

@HillLiu
Copy link

HillLiu commented Jun 19, 2019

another clue was make sure the record_name already exits in your zone, when this script purpose was update.

For those having the same issue as @ptts, @Kiendeleo, and @danieluramg, consider checking if the auth_key parameter you have set is indeed your Cloudflare API Key.

You can find your Cloudflare API Key going to My Profile, which is located under the menu at the top right of the Cloudflare Dashboard. Once there, scroll down to API Keys and locate Global API Key. Click View, enter your password, and you will have access to your API Key.

@HillLiu
Copy link

HillLiu commented Jun 19, 2019

Thanks this gist.

I also add more feature such as

  1. env file
  2. debug mode.
  3. replace grep to sed that it also work on MacOS.

if someone interested, check out my version
https://github.com/HillLiu/cloudflare-bash-util

@danfraser007
Copy link

danfraser007 commented Jun 29, 2019

I'm also getting the same error as @Kiendeleo and @danieluramg namely:

{
   "success":false,
   "errors":[
      {
         "code":7003,
         "message":"Could not route to \/zones\/dns_records, perhaps your object identifier is invalid?"
      },
      {
         "code":7000,
         "message":"No route for that URI"
      }
   ],
   "messages":[

   ],
   "result":null
}

These are the files and permissions:

drwxr-xr-x 2 ptts ptts 4096 Dec  2 16:17 .
drwxr-xr-x 5 ptts ptts 4096 Dec  2 16:11 ..
-rw-r--r-- 1 ptts ptts    2 Dec  2 16:13 cloudflare.ids
-rw-r--r-- 1 ptts ptts  666 Dec  2 16:15 cloudflare.log
-rw-rw-rw- 1 ptts ptts 1928 Dec  2 16:15 cloudflare-update-record.sh

I ran the script with bash cloudflare-update-record.sh and sudo bash cloudflare-update-record.sh, both yield the same result.
Any suggestions? Thanks!

@ptts, @Kiendeleo, and @danieluramg

If you are getting the error "Could not route to /zones/dns_records, perhaps your object identifier is invalid?" .....try deleting the log file filecloudflare.ids .

For some reason when I first ran the script it didnt populate the file. This may of been because I had an invalid API key. However it still created the file, but it was just empty. So the script would not run when I re-ran it. Anyway, I deleted the empty file. Re-ran the script and now it works.

To the dev, maybe its worth deleting the file once it has been used? or checking to see if the file is empty?

@DTM450
Copy link

DTM450 commented Jul 24, 2019

If you are using this on a Raspberry Pi make sure to use bash not sh and to remove the spacing at line 36 before the "fi"

@samywee
Copy link

samywee commented Sep 2, 2019

Thank you worked! perfectly. Make sure run under bash.

@issess
Copy link

issess commented Oct 7, 2019

I updated API_TOKEN version like this :

#!/bin/bash

# Created by benkulbertis/cloudflare-update-record.sh
# CHANGE THESE
api_token="c2547eb745079dac9320b638f5e225cf483cc5cfdda41" # found in cloudflare account my profile - API Tokens - (Permission Zone.Zone, Zone.DNS)
zone_name="example.com"
record_name="www.example.com"
proxied="true"

# MAYBE CHANGE THESE
ip=$(curl -s http://ipv4.icanhazip.com)
ip_file="ip.txt"
id_file="cloudflare.ids"
log_file="cloudflare.log"

# LOGGER
log() {
    if [ "$1" ]; then
        echo -e "[$(date)] - $1" >> $log_file
    fi
}

# SCRIPT START
log "Check Initiated"

if ! [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
    message="Fetched IP does not look valid! Quitting"
    log "$message"
    echo -e "$message"
    exit 1 
fi

if [ -f $ip_file ]; then
    old_ip=$(cat $ip_file)
    if [ $ip == $old_ip ]; then
        echo "IP has not changed."
        exit 0
    fi
fi

if [ -f $id_file ] && [ $(wc -l $id_file | cut -d " " -f 1) == 2 ]; then
    zone_identifier=$(head -1 $id_file)
    record_identifier=$(tail -1 $id_file)
else
    zone_identifier=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=$zone_name" -H "Authorization: Bearer $api_token" -H "Content-Type: application/json" | grep -Po '(?<="id":")[^"]*' | head -1 )
    record_identifier=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$zone_identifier/dns_records?name=$record_name" -H "Authorization: Bearer $api_token" -H "Content-Type: application/json"  | grep -Po '(?<="id":")[^"]*')
    echo "$zone_identifier" > $id_file
    echo "$record_identifier" >> $id_file
fi

echo "zone : $zone_identifier"
echo "record : $record_identifier"

update=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$zone_identifier/dns_records/$record_identifier" -H "Authorization: Bearer $api_token" -H "Content-Type: application/json" --data "{\"id\":\"$zone_identifier\",\"type\":\"A\",\"proxied\":\"$proxied\",\"name\":\"$record_name\",\"content\":\"$ip\"}")

case "$update" in
  *"\"success\":false"*)
    message="API UPDATE FAILED. DUMPING RESULTS:\n$update"
    log "$message"
    echo -e "$message"
    exit 1;;
  *)
      message="IP changed to: $ip"
    echo "$ip" > $ip_file
    log "$message"
    echo "$message";;
esac

@DaveSanders
Copy link

I had to change the "update" line to remove the quotes around "$proxied". Cloudflare expects that json value to be a boolean:

update=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$zone_identifier/dns_records/$record_identifier" -H "Authorization: Bearer $api_token" -H "Content-Type: application/json" --data "{\"id\":\"$zone_identifier\",\"type\":\"A\",\"proxied\":$proxied,\"name\":\"$record_name\",\"content\":\"$ip\"}")

@pedrof1gueiredo
Copy link

I am getting the following result when I run the script:
{"success":false,"errors":[{"code":7003,"message":"Could not route to \/zones\/dns_records, perhaps your object identifier is invalid?"},{"code":7000,"message":"No route for that URI"}],"messages":[],"result":null}

I am attempting to update and A record of a subdomain on an account with multiple domains. Is anyone else getting this error?

I have the same issue, also in an account with multiple domains. No solution on this thread worked so far.

@ray-moncada
Copy link

Thank you for the script saved me lots of time. Tweaked to add DNS Record instead of updating. I did not add a check to see if the record already exist.

...

if [ -f $id_file ] &amp;&amp; [ $(wc -l $id_file | cut -d " " -f 1) == 2 ]; then
zone_identifier=$(head -1 $id_file)
else
zone_identifier=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=${zone_name}" -H "X-Auth-Email: $auth_email" -H "X-Auth-Key: $auth_key" -H "Content-Type: application/json" | grep -Po '(?<="id":")[^"]*' | head -1 )
echo "$zone_identifier" > $id_file
fi

addDns=$(curl -X POST "https://api.cloudflare.com/client/v4/zones/${zone_identifier}/dns_records" -H "X-Auth-Email: ${auth_email}" -H "X-Auth-Key: ${auth_key}" -H "Content-Type: application/json" --data '{"type":"A","name":"'${record_name}'","content":"'${ip}'","ttl":120,"priority":10,"proxied":true}')

if [[ $addDns == ""success":false" ]]; then
message="API UPDATE FAILED. DUMPING RESULTS:\n$addDns"
log "$message"
echo -e "$message"
exit 1
else
message="IP changed to: $ip"
echo "$ip" > $ip_file
log "$message"
echo "$message"
fi

@mrwhizzy
Copy link

just a heads up everyone, apparently Cloudflare made a few changes to their API and I found out the hards way today (bit of a downtime). All you need to do is to add a space character ( ) in every regex when parsing a field.

specifically:
if [[ $record == *"\"count\":0"* ]]; then
needs to become
if [[ $record == *"\"count\": 0"* ]]; then

old_ip=$(echo "$record" | grep -Po '(?<="content":")[^"]*' | head -1)
to
old_ip=$(echo "$record" | grep -Po '(?<="content": ")[^"]*' | head -1)

*"\"success\":false"*)
has to become
*"\"success\": false"*)

@HillLiu
Copy link

HillLiu commented Apr 30, 2020

Thanks @mrwhizzy

I also update my sed sample here.
HillLiu/cloudflare-bash-util@304d58e

just a heads up everyone, apparently Cloudflare made a few changes to their API and I found out the hards way today (bit of a downtime). All you need to do is to add a space character ( ) in every regex when parsing a field.

specifically:
if [[ $record == *"\"count\":0"* ]]; then
needs to become
if [[ $record == *"\"count\": 0"* ]]; then

old_ip=$(echo "$record" | grep -Po '(?<="content":")[^"]*' | head -1)
to
old_ip=$(echo "$record" | grep -Po '(?<="content": ")[^"]*' | head -1)

*"\"success\":false"*)
has to become
*"\"success\": false"*)

@niklasmato
Copy link

just a heads up everyone, apparently Cloudflare made a few changes to their API and I found out the hards way today (bit of a downtime). All you need to do is to add a space character ( ) in every regex when parsing a field.

specifically:
if [[ $record == *"\"count\":0"* ]]; then
needs to become
if [[ $record == *"\"count\": 0"* ]]; then

old_ip=$(echo "$record" | grep -Po '(?<="content":")[^"]*' | head -1)
to
old_ip=$(echo "$record" | grep -Po '(?<="content": ")[^"]*' | head -1)

*"\"success\":false"*)
has to become
*"\"success\": false"*)

Updated my scripts but still doesn't seem to work.
Anyone got it working again?

@flashtel
Copy link

grep -Po '(?<="id":")[^"]*')

change to

grep -Po '(?<="id": ")[^"]*')

@flashtel
Copy link

Why Cloudflare why do you do these things.

Thanks for the update

@AkimoA
Copy link

AkimoA commented May 2, 2020

tx guys for the update, was wondering what was going on !

@niklasmato
Copy link

Did they change something again? Scripts no longer seem to work..

@mrwhizzy
Copy link

@niklasmato, yes they did, I've uploaded an up-to-date version of the script on GitHub which will work a bit more reliably no matter if they have spaces after semicolons or not, plus it will also update multiple records.

@niklasmato
Copy link

@niklasmato, yes they did, I've uploaded an up-to-date version of the script on GitHub which will work a bit more reliably no matter if they have spaces after semicolons or not, plus it will also update multiple records.

Thank you for this!

@by-JohnChen
Copy link

by-JohnChen commented Dec 4, 2020

#!/bin/sh
#modify by: John Chen
# CHANGE THESE
zone_identifier="your_zone_id"
record_identifier="Your_record_iid"
auth_key="Your_auth_keys" # found in cloudflare account settings
zone_name="example.com"
record_name="www.example.com"

# MAYBE CHANGE THESE
ip=$(curl -s http://ipv4.icanhazip.com)
ip_file="ip.txt"
id_file="cloudflare.ids"
log_file="cloudflare.log"

# LOGGER
log() {
if [ "$1" ]; then
echo -e "[$(date)] - $1" >> $log_file
fi
}

# SCRIPT START
log "Check Initiated"

if [ -f $ip_file ]; then
old_ip=$(cat $ip_file)
if [ $ip == $old_ip ]; then
echo "IP has not changed."
exit 0
else if [$ip == ""]; then
echo "Timeout! "
fi
fi

update=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$zone_identifier/dns_records/$record_identifier" -H "Authorization: Bearer $auth_key" -H "Content-Type: application/json" --data "{"id":"$zone_identifier","type":"A","name":"$record_name","content":"$ip"}")

if [[ $update == ""success":false" ]]; then
message="API UPDATE FAILED. DUMPING RESULTS:\n$update"
log "$message"
echo -e "$message"
exit 1
else
message="IP changed to: $ip"
echo "$ip" > $ip_file
log "$message"
echo "$message"
fi

@0neday
Copy link

0neday commented Aug 31, 2021

change -H "X-Auth-Email: $auth_email" -H "X-Auth-Key: $auth_key" to -H "Authorization: Bearer $auth_key"
and
change use sed to instead of grep to parse json data
like
sed -E "s/.+\{\"id\":\"([a-f0-9]+)\".+\"type\":\"A\".+/\1/g"
and
if [ -f $id_file ] && [ -n $(sed -n '1p' $id_file) ] && [ -n $(sed -n '2p' $id_file) ]; then

and

success=$( echo $update | sed -E "s/.+\"success\":[ ]*([a-z]+).+/\1/g")


if [[ $success == "false" ]]; then
    message="API UPDATE FAILED. DUMPING RESULTS:\n$update"
    log "$message"
    echo -e "$message"
    exit 1 
else

check here

@lifehome
Copy link

lifehome commented Sep 3, 2021

Hi all,

Previously I have posted a bash/systemd version fork of this script in another gist, I have moved and updated the script to an actual repository, please kindly visit it instead at: https://github.com/lifehome/systemd-cfddns

Regards,
Ivan

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment