Dynamic DNS Updating with Route 53 & Linux

Table of Contents

Why?

I recently decided to consolidate a physical box running Ubuntu 16.04 and Nextcloud to a VM. In the process I decided to do a fresh install of Ubuntu 18.04 so I am on the latest LTS release. Since I was doing that I also did a fresh install of Nextcloud and migrated data. For install I followed this guide by riegercloud and after discovered he has a GitHub script to automate the process here.

Since this server is running out of my home which is on a residential ISP account I cannot get a Static IP address and have to use a dynamic update service. I had been using this Route 53 Dynamic DNS update script from WillWarren.com and after upgrading I discovered that the script was no longer working. The fix was actually very simple. Apparently the dig command in 18.04 requires the use of the -4 option otherwise it will return an IPv6 address if you have an IPv6 address.

Updated: Now using Amazon AWS to get IP

Another issue I discovered is that this script also relies on an IPFILE to tell if your DNS needs to be updated. The issue with this would be if your IP was somehow changed on Route53 this script would never know and never update the IP. I initially corrected this by adding another dig command. Because I use mobile devices to sync to my Nextcloud I have a static entry in my pfSense firewall’s DNS resolver which captures all DNS traffic using pfBlocker for filtering and pointing my hostname to the local IP address while on my local network. This caused my dig command to return the local address for my DNS hostname rather than the public IP address on Route53 servers. Why not use the same aws cli to get the IP from Route53? BINGO and While the fix was simple I wasn’t a fan of how it did it’s logging. My modified version cleans it up and lets the crontab log to a specified file there.

Requirements:

Domain & Route53 DNS

First you need a domain name from your choice of provider and DNS through Amazon’s AWS Route53 service. If you do not have a domain this is fairly inexpensive at ~$12/year for Domain Registration from Amazon, $0.50/mo for Route53 DNS, and possibly a few extra pennies depending on the amount of traffic. To get started with Amazon Route53 you can visit here: https://aws.amazon.com/route53/getting-started/

If your domain and DNS are configured elsewhere you can create a subdomain and point it to Route53 and create a Hosted Zone for that subdomain.

Creating the record set now is not necessary as the script will create it the first time it runs.

Linux Server

This script is tested and working on Ubuntu 18.04. Other flavors of linux will require different commands for installing dependencies however the script should still work.

Configure AWS IAM User

First we need to create an IAM user, give permissions to update Route53 and get the security credentials for the AWSCLI.

  1. Use this Add IAM User Quicklink (or from your IAM Users Page select ‘Users’ in the left sidebar and then ‘Add User’ on the following page.)
  2. Enter a ‘Username’, select ‘Programmatic Access’, and ‘Next: Permissions’
  3. Choose ‘Create Group’ and enter a ‘Group Name’. For simplicity you can choose Filter Policies: Route53 and select ‘AmazonRoute53FullAccess’ (You cannot limit users to specific record sets however you can create Hosted Zones for SubDomains)
  4. After creating your group select Next: Tags > Next: Review > Create User
  5. Copy your Access Key ID and Secret Access Key to a secure location. We will need those for configuring the AWSCLI.

Install & Configure AWSCLI

The AWS CLI can be installed through repositories however the recommend method for getting the latest version is to use Python.

Install Python3

sudo apt-get install python3

Get PIP

curl -O https://bootstrap.pypa.io/get-pip.py
python get-pip.py

Confirm PIP Install

pip --version

Install AWSCLI with PIP

pip install awscli

To update in the future use the command

pip install awscli --upgrade

Configure AWSCLI with IAM Credentials

aws configure

You will now be asked for the ‘AWS Access Key ID’, ‘AWS Secret Access Key’, ‘Default Region Name’, and ‘Default Output Format’.  Enter the Access Key and Secret Access Key obtained in IAM User creation.  Route53 does not require a region but your default region can be set now if desired.  ‘Default Output Format’ can also be left blank.

AWSCLI keeps it’s configuration in ~/.aws/config and ~/.aws/credentials files.

Make The Script

Make directory where scripts will run and save log files. Here I am creating the directory aws in my users home directory. We also make the script executable and open it for adding our script.

mkdir ~/aws
touch ~/aws/awsupdate.sh ~/aws/awsupdate.log
sudo chmod u+x ~/aws/awsupdate.sh
nano ~/aws/awsupdate.sh

Copy and paste this script

<span class="token shebang important">#!/bin/bash</span>

<span class="token comment"># (optional) You might need to set your PATH variable at the top here</span>
<span class="token comment"># depending on how you run this script</span>
PATH<span class="token operator">=</span>/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

<span class="token comment"># Hosted Zone ID e.g. BJBK35SKMM9OE</span>
ZONEID<span class="token operator">=</span><span class="token string">"YOURHOSTEDZONEID"</span>

<span class="token comment"># The CNAME you want to update e.g. hello.example.com</span>
RECORDSET<span class="token operator">=</span><span class="token string">"YOURDOMAINTOUPDATE"</span>

<span class="token comment"># The Time-To-Live of this recordset</span>
TTL<span class="token operator">=</span>300

<span class="token comment"># Change this if you want</span>
COMMENT<span class="token operator">=</span><span class="token string">"<span class="token variable"><span class="token variable">`</span><span class="token function">date</span><span class="token variable">`</span></span> Updated AWS CLI"</span>

<span class="token comment"># Change to AAAA if using an IPv6 address.  You must also update dig command in IP variable removing the -4 option.</span>
TYPE<span class="token operator">=</span><span class="token string">"A"</span>

<span class="token comment"># Get Record set IP from Route 53</span>
DNSIP<span class="token operator">=</span><span class="token string">"$(
   aws route53 list-resource-record-sets \
      --hosted-zone-id "</span><span class="token variable">$ZONEID</span><span class="token string">" --start-record-name "</span><span class="token variable">$RECORDSET</span><span class="token string">" \
      --start-record-type "</span><span class="token variable">$TYPE</span><span class="token string">" --max-items 1 \
      --output json | jq -r \ '.ResourceRecordSets[].ResourceRecords[].Value'
)"</span>

<span class="token comment"># Get the external IP address from OpenDNS. Remove -4 for IPv6</span>
<span class="token comment"># I've decided to follow Mike P's suggestion of using AWS and curl becuase dig is not shipped with as many distros as curl.</span>
IP<span class="token operator">=</span><span class="token variable"><span class="token variable">`</span>curl -s http://checkip.amazonaws.com/<span class="token variable">`
</span></span>
<span class="token comment"># Check that IP is valid</span>
<span class="token keyword">function</span> valid_ip<span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    local  ip<span class="token operator">=</span><span class="token variable">$1</span>
    local  stat<span class="token operator">=</span>1

    <span class="token keyword">if</span> <span class="token punctuation">[</span><span class="token punctuation">[</span> <span class="token variable">$ip</span> <span class="token operator">=</span>~ ^<span class="token punctuation">[</span>0-9<span class="token punctuation">]</span><span class="token punctuation">{</span>1,3<span class="token punctuation">}</span>\.<span class="token punctuation">[</span>0-9<span class="token punctuation">]</span><span class="token punctuation">{</span>1,3<span class="token punctuation">}</span>\.<span class="token punctuation">[</span>0-9<span class="token punctuation">]</span><span class="token punctuation">{</span>1,3<span class="token punctuation">}</span>\.<span class="token punctuation">[</span>0-9<span class="token punctuation">]</span><span class="token punctuation">{</span>1,3<span class="token punctuation">}</span>$ <span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
        OIFS<span class="token operator">=</span><span class="token variable">$IFS</span>
        IFS<span class="token operator">=</span><span class="token string">'.'</span>
        ip<span class="token operator">=</span><span class="token punctuation">(</span><span class="token variable">$ip</span><span class="token punctuation">)</span>
        IFS<span class="token operator">=</span><span class="token variable">$OIFS</span>
        <span class="token punctuation">[</span><span class="token punctuation">[</span> <span class="token variable">${ip[0]}</span> -le 255 <span class="token operator">&&</span> <span class="token variable">${ip[1]}</span> -le 255 \
            <span class="token operator">&&</span> <span class="token variable">${ip[2]}</span> -le 255 <span class="token operator">&&</span> <span class="token variable">${ip[3]}</span> -le 255 <span class="token punctuation">]</span><span class="token punctuation">]</span>
        stat<span class="token operator">=</span><span class="token variable">$?</span>
    <span class="token keyword">fi</span>
    <span class="token keyword">return</span> <span class="token variable">$stat</span>
<span class="token punctuation">}</span>

<span class="token keyword">if</span> <span class="token operator">!</span> valid_ip <span class="token variable">$IP</span> <span class="token punctuation">;</span> <span class="token keyword">then</span>
    <span class="token keyword">echo</span> <span class="token variable"><span class="token variable">`</span><span class="token function">date</span><span class="token variable">`</span></span><span class="token string">" Invalid IP address <span class="token variable">$IP</span> Check dig command."</span>
    <span class="token keyword">exit</span> 1
<span class="token keyword">fi</span>

<span class="token comment"># Check if the IP has changed and if so create JSON string for updating.</span>
<span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token string">"<span class="token variable">$IP</span>"</span> <span class="token operator">==</span> <span class="token string">"<span class="token variable">$DNSIP</span>"</span> <span class="token punctuation">]</span> <span class="token punctuation">;</span> <span class="token keyword">then</span>
    <span class="token keyword">echo</span> <span class="token variable"><span class="token variable">`</span><span class="token function">date</span><span class="token variable">`</span></span><span class="token string">" IP is still <span class="token variable">$IP</span>. Exiting"</span>
    <span class="token keyword">exit</span> 0
<span class="token keyword">else</span>
    <span class="token keyword">echo</span> <span class="token variable"><span class="token variable">`</span><span class="token function">date</span><span class="token variable">`</span></span><span class="token string">" IP has changed from <span class="token variable">$DNSIP</span> to <span class="token variable">$IP</span>"</span>
    TMPFILE<span class="token operator">=</span><span class="token variable"><span class="token variable">$(</span>mktemp /tmp/temporary-file.XXXXXXXX<span class="token variable">)</span></span>
    <span class="token function">cat</span> <span class="token operator">></span> <span class="token variable">${TMPFILE}</span> <span class="token operator"><<</span> <span class="token string">EOF
    {
      "Comment":"<span class="token variable">$COMMENT</span>",
      "Changes":[
        {
          "Action":"UPSERT",
          "ResourceRecordSet":{
            "ResourceRecords":[
              {
                "Value":"<span class="token variable">$IP</span>"
              }
            ],
            "Name":"<span class="token variable">$RECORDSET</span>",
            "Type":"<span class="token variable">$TYPE</span>",
            "TTL":<span class="token variable">$TTL</span>
          }
        }
      ]
    }
EOF</span>

    <span class="token comment"># Update the Hosted Zone record</span>
    aws route53 change-resource-record-sets \
        --hosted-zone-id <span class="token variable">$ZONEID</span> \
        --change-batch file://<span class="token string">"<span class="token variable">$TMPFILE</span>"</span> \
		--query <span class="token string">'[ChangeInfo.Comment, ChangeInfo.Id, ChangeInfo.Status, ChangeInfo.SubmittedAt]'</span> \
		--output text

    <span class="token comment"># Clean up</span>
    <span class="token function">rm</span> <span class="token variable">$TMPFILE</span>
<span class="token keyword">fi</span>

You can test the script by running

~/aws/awsupdate.sh

Setup Cron Job

This script will run at 20 minute intervals with the */20 * * * * and at startup with the @reboot sleep 60 &&. The sleep 60 allows time for networking services to be restarted which are of course necessary for this script to run successfully. I had initially tried to run the script as root however there was an issue with the AWS credentials. They can be manually specified if you with to place this in the root crontab. All output is logged to the specified location that we created at the beginning. For initial testing of the cron job you may with to specify an interval of every minute (* * * * *) and monitor the log file with the command:tail -n10 ~/aws/awsupdate.log

crontab -e
*/20 * * * * ~/aws/awsupdate.sh >> ~/aws/awsupdate.log 2>&1
@reboot sleep 60 && ~/aws/awsupdate.sh >> ~/aws/awsupdate.log 2>&1

I hope to make a demo of a similar script for the Windows AWSCLI client in the near future!

If you have any suggestions, questions, or comments please drop a line below!

Share This Post

More Posts

5 Responses

  1. Sweet script! Thank you very much!

    One minor correction: there’s a typo above (‘asw’ switched for ‘aws’ in the filename)…

    the line:
    touch ~/aws/aswupdate.sh ~/aws/awsupdate.log

    should be:
    touch ~/aws/awsupdate.sh ~/aws/awsupdate.log

    I used in on Ubuntu 19.04, I needed to install jq and comment out the path line as I had aws-cli installed in snap/bin/aws and had added it to my .bashrc entry.

  2. Thanks for the script! It does exactly what I was looking for.

    I added a pushbullet notification that gets pushed when the IP address changes that contains the old IP and the new IP address.

    Before the last fi after “Clean up” section I added:

    # Send pushbullet
    curl -s –header ‘Access-Token: your_pushbullet_API_key_here’ \
    –header ‘Content-Type: application/json’ \
    –data-binary ‘{“body”:”‘”Old: $DNSIP\nNew: $IP”‘”,”title”:”IP Address changed”,”type”:”note”}’ \
    –request POST \
    https://api.pushbullet.com/v2/pushes > /dev/null

  3. There are a couple of alternatives to dig which may give better results. Among other things, dig isn’t always installed by default, which may limit the portability of this.

    ipify.org has an API that returns your IP:
    “`IP=$(curl -s https://api.ipify.org/)“`

    AWS also has an API that is not rate-limited and is free:
    “`IP=$(curl -s http://checkip.amazonaws.com/)“`

    (granted curl may not be installed either, but I’ve run into way more systems without dig than without curl)

    I then made two more modifications:

    1) because I use OpenDNS on my local network, and have customized settings, I need to update OpenDNS so my settings follow me. While they have an updater that runs on Windows, Mac, and Android, they do not have a linux version. And I don’t really recommend setting the updater on your Mobile device, unless you are on a paid plan, as the free plan only allows a single network, which means that your home would lose any protection you might be gaining by using OpenDNS in the first place. Enter this script, which helpfully knows my IP has changed. Enter OpenDNS’s “DNSoMatic” service, which can simultaniously update a wide variety of Dynamic DNS services. They have a simple API:
    “`curl “https://updates.dnsomatic.com/nic/update?hostname=all.dnsomatic.com&myip=$IP&wildcard=NOCHG&mx=NOCHG&backmx=NOCHG” –user :“`

    2) taking inspiration from J Suisman, you can send a request to IFTTT in addition to pushbullet, so I can send notifications multiple places via email or SMS

Leave a Reply to Clint Johnson Cancel reply

Your email address will not be published. Required fields are marked *

Related Content

Microsoft Office 2021

Updating MS Office with Microsoft Endpoint Manager

My apologies that this isn’t really a technical write-up.  I just got really frustrated trying to find answers as to why I was stuck at “Install Pending” and feel like this part of the process is skipped in any existing Microsoft Office Volume documentation.  Of course always setup a deployment test group and verify it is successful there first!

Read More »