Dynamic DNS on Route 53

At the start of the new year, I moved my domains to Amazon’s Route53. This was remarkably painless, but by itself Route53 doesn’t support any dynamic DNS updates. Most dyndns systems either use TSIG, or some kind of proprietary RESTful service; there’s a number of home routers that support both approaches (at least for popular dyndns providers). When I was running my own nameserver, I kept a couple TSIG keys to do updates for a record pointing to my home. Unfortunately, that’s now completely broken - my domain no longer has a record pointing to the DHCP address I get from my ISP. Time to fix that.

As a colleague of mine once said, “Overkill is a subset of kill.” With that in mind, I figured this would be a great excuse to also make something more practical in AWS Lambda, and configure up an API Gateway to boot. Arguably overkill, but a fun learning exercise.

General Design

I built a small Python program to run on a server at home to request the update (called ddupdate). ddupdate takes a fully-qualified domain name (aka FQDN), the Amazon ZoneID for the relevant domain, and a security token, then passes all these along as query string parameters to a given URL. The URL is provided by Amazon’s API Gateway, and is a simple wrapper around an AWS Lambda function (also written in Python) that does the heavy lifting. TTL, IPv4, and IPv6 addresses are optional parameters passed in the query string. If no IPv4 address is provided, the Lambda picks apart the calling data structure provided by the API Gateway and uses the client’s percieved IPv4 address (which is usually the right thing to use, especially for my home servers).

The security token is a SHA-256 hash of the FQDN, ZoneID, and a secret string known only to the Lambda function. The Lambda can easily verify whatever token is provided by ddupdate, but this does mean the token needs to be shared with the client beforehand. I’ve also whipped up a short ddmktoken script that generates appropriate tokens. As-is, this would be vulnerable to replay attacks and server spoofing, so it’s very important to use an HTTPS URL (like Amazon’s API Gateway uses by default) for contacting the server.

All relevant code is up in my dyndns Github repositorty. Full disclosure: if you pick through the commit history you’ll see I originally implemented the authentication token bits with SHA-1. Naturally, two days after I finished up I saw Google’s announcement about SHA-1’s first collision attack. Yeah, yeah, I know - I shouldn’t have been using SHA-1 in 2017 in the first place. Point taken, mea culpa.

Lambda Function & Roles

The source for my Lambda function can be found in my Github repo; it’s pretty self-explanatory. The function does need a role capable of modifying the zone in Route53, though. Simple enough; just go into AWS’ IAM web UI and create a new role. It needs two blocks of policies: one for modifying Route53 entries, and one for executing AWS Lambda functions. Rather than combining these, I just made two separate policies and added both to the new DynamicDNSmodifications role.

For DNS updates, I used:

1
2
3
4
5
6
7
8
9
10
11
12
13
    "Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"route53:*"
],
"Resource": [
"*"
]
}
]
}

Obviously, that can be locked down to a single ZoneID resource if you have multiple zones (and don’t want dynamic DNS functionality for all of them). For Lambda execution, I used:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    "Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "logs:CreateLogGroup",
"Resource": "arn:aws:logs:us-east-1:849183371819:*"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": [
"arn:aws:logs:us-east-1:849183371819:log-group:/aws/lambda/publish:*"
]
}
]
}

Again, you probably want to lock this down (in particular, to your account’s preferred AWS log group).

Once both policies are attached to a new role, the role should appear in the Existing role drop-down list on your Lambda function’s Configuration tab. Note that the drop-down will only show roles with the Lambda execution/logging policy attached; if your role lacks this, it may not even be considered a valid role for a Lambda function.

API Gateway

Once the Lambda is set up, and given an appropriate set of policies/roles, creating the gateway is trivial. Just need to make a stage for production use, and copy the “Invoke URL” at the top of the production stage’s editor page. That’s what the client tool will need when it goes to update DNS.

Client Tool

Once configured, ddupdate should be able to change records in Route53 whenever it’s run - I’m currently doing every 15 minutes from cron on a home server. Once the command runs, it may take up to 2-3 minutes for Amazon to actually process the request (one of the consequences of deploying DNS changes to multiple time zones, most likely).

It may not be a good idea to leave everything as a command-line option, so ddupdate supports an INI-style config file in either /etc/dyndns.conf or /usr/local/etc/dyndns.conf. It’s possible to list multiple hostnames in the same file, and provide multiple FQDNs on the command line to assign multiple domain names at once. Syntax should look like:

1
2
3
4
5
6
7
8
[host1.example.org]
url=https://asdfli8asd1.execute-api.us-east-1.amazonaws.com/Production
token=12819401012847129401128
zoneid=UDN7DNA2AFIC001
[host1.example.org]
url=https://asdfli8asd1.execute-api.us-east-1.amazonaws.com/Production
token=18219848272727192477179
zoneid=UDN7DNA2AFIC001

Obviously, replace the values with real data for your site.

If this is of use, feel free to borrow it heavily!