DEV Community

Rev for AWS Community Builders

Posted on

Running an AWS Lambda + Route 53 DDNS Client on EdgeRouter X

Introduction

My home network is assigned a dynamic global IP address by the ISP, so DDNS is essential for inbound connections. I built a system that periodically updates DNS records from an EdgeRouter X using the serverless DDNS solution (Lambda + Route 53 + DynamoDB) published by AWS.

This article covers the steps from setting up the AWS environment to configuring the client on the EdgeRouter X and verifying operation.

The ultimate goal is to connect to the EdgeRouter X via remote VPN using DDNS, but first we need to build the DDNS infrastructure.

For details on how the solution works, see Building a Serverless Dynamic DNS System with AWS.

References

Test Environment

Tested on EdgeRouter X (ER-X) firmware 3.0.1.

About the Forked Repository

I forked the official AWS repository awslabs/route53-dynamic-dns-with-lambda and created revsystem/route53-dynamic-dns-with-lambda.

The fork includes the following changes, organized by branch:

  • fix/lambda-security: Fixed security vulnerabilities in the Lambda function (timing attack, input validation, exception handling, information leakage). Improved code quality (resolved function name conflicts, unified return types, moved boto3 to module level, removed Python 2 compatibility code).
  • fix/cdk-improvements: Tightened IAM policy resource restrictions (Route 53, CloudWatch Logs). Changed DynamoDB RemovalPolicy to RETAIN and billing mode to PAY_PER_REQUEST. Set Lambda log retention period and reserved concurrency. Removed unused imports and improved CDK-Nag suppressions.
  • fix/client-scripts: Fixed variable quoting, operator errors, deprecated command substitutions, and JSON construction safety in dyndns.sh. Added JSON injection prevention, improved exception handling, and secret masking in newrecord.py.
  • test/add-tests: Completely rewrote the unimplemented tests (all assertions were commented out). Added 7 CDK stack tests and 15 Lambda unit tests (22 total). Covers GET/SET normal cases, validation, authentication, and error handling.
  • docs/fix-typos-and-docs: Fixed typos in README.md and invocation.md. Updated response examples in invocation.md to match the Lambda implementation. Added version range specification to requirements.txt. Updated cdk.json feature flags.
  • fix/cdk-nag-log-retention: Added CDK-Nag suppressions for the custom resource Lambda auto-generated by the log_retention setting. Since the CDK internal resource cannot be modified, the suppression includes an applies_to clause with documented rationale.
  • feat/manage-record-script: Created managerecord.py to allow viewing configuration, immediately changing TTL, and deleting records via subcommands.

CDK-Nag is a tool that automatically checks CDK applications against security and compliance best practices. Error-level violations cause cdk synth to fail, blocking deployment. Warning-level violations do not block deployment. To intentionally exempt a rule, you set a suppression with a documented reason.

Setting Up the AWS Environment

Prerequisites

  • A domain hosted in Route 53
  • Permissions to operate Lambda, DynamoDB, and Route 53
  • An environment with the AWS CLI available

Architecture Overview

[Operator]
  |
  |-- newrecord.py (create/update) --> DynamoDB (hostname -> JSON)
  |
[EdgeRouter X]
  |-- dyndns.sh --> Lambda URL --> Lambda --> DynamoDB + Route 53
Enter fullscreen mode Exit fullscreen mode

Deploying the CDK Stack

This solution is deployed using AWS CDK. The Lambda function, DynamoDB table, and IAM roles are created at once.

Clone the repository:

git clone https://github.com/revsystem/route53-dynamic-dns-with-lambda.git

cd route53-dynamic-dns-with-lambda
Enter fullscreen mode Exit fullscreen mode

Install the Python dependencies:

pip install -r requirements.txt
Enter fullscreen mode Exit fullscreen mode

Bootstrap is required the first time you deploy to a given account and region combination (environment). If the account is the same but the region differs, run it separately for each region.

cdk bootstrap
Enter fullscreen mode Exit fullscreen mode

If you are told the CDK CLI version is too old, update it:

sudo npm install -g aws-cdk
Enter fullscreen mode Exit fullscreen mode

Deploy the stack:

cdk deploy
Enter fullscreen mode Exit fullscreen mode

When the deployment completes, the Lambda function URL is output. You will use this URL in the client configuration later.

Configuring the DNS Record

Run newrecord.py to interactively configure the Route 53 hosted zone and record set. The configuration is saved to the DynamoDB table.

python3 newrecord.py
Enter fullscreen mode Exit fullscreen mode

If your AWS profile or region differs from the defaults, specify the environment variables:

AWS_PROFILE=production AWS_DEFAULT_REGION=us-east-1 python3 newrecord.py
Enter fullscreen mode Exit fullscreen mode

Enter the hosted zone name (e.g., example.jp), hostname (e.g., router.example.jp), TTL (default 60), and shared secret in order. If the hosted zone does not exist, you will be asked whether to create it.

The shared secret, together with the Lambda function URL, is the key to authentication. Set it to a sufficiently complex string. Using a random string generated by openssl rand -hex 32 is recommended.

Once the input is complete, a confirmation is displayed:

##############################################
#                                            #
# The following configuration will be saved: #
#                                            #
  Host name:  router.example.jp
  Hosted zone id: ZXXXXXXXXXXXXXXXXXXXXXXX
  Record set TTL: 60
  Secret: ********
#                                            #
#      do you want to continue? (y/n)        #
#                                            #
##############################################
Enter fullscreen mode Exit fullscreen mode

When the setup is complete, the parameters needed to run dyndns.sh are displayed:

./dyndns.sh -m set -u https://XXXXXXXXXX.lambda-url.REGION.on.aws/ -h router.example.jp -s <YOUR_SECRET>
Enter fullscreen mode Exit fullscreen mode

Take note of the Lambda function URL and shared secret. They will be used in the EdgeRouter X configuration.

Verification

Test-run dyndns.sh to verify the integration between the Lambda function and Route 53. On environments that use shasum, you may need to install perl-Digest-SHA.

sudo yum install perl-Digest-SHA
Enter fullscreen mode Exit fullscreen mode

Test the IP address retrieval:

./dyndns.sh -m get -u "https://XXXXXXXXXX.lambda-url.REGION.on.aws/"
Enter fullscreen mode Exit fullscreen mode

If a public IP address is returned, the Lambda function is working correctly.

DDNS Client Implementation Approach

EdgeRouter X has a built-in DDNS feature that can be configured with set service dns dynamic. Internally it runs ddclient and supports standard protocols such as dyndns2 and cloudflare.

However, the AWS DDNS solution uses a protocol that sends an HTTP POST with JSON to a Lambda function URL and performs application-level authentication using a SHA-256 hash. The Lambda function URL itself is published with authentication type NONE (public access), and authentication is achieved by verifying the hash value included in the request within the Lambda function. Since ddclient does not have a definition for this protocol, the built-in DDNS feature cannot be used.

Instead, we periodically execute the shell script dyndns.sh provided in the repository using the EdgeRouter X task scheduler.

Note: The following steps are executed on the EdgeRouter X CLI or via SSH.

Deploying dyndns.sh

SSH into the EdgeRouter X and download the script. Files placed under /config/scripts/ are preserved after firmware upgrades.

curl -o /config/scripts/dyndns.sh https://raw.githubusercontent.com/revsystem/route53-dynamic-dns-with-lambda/master/dyndns.sh

chmod +x /config/scripts/dyndns.sh
Enter fullscreen mode Exit fullscreen mode

dyndns.sh internally uses shasum -a 256 to generate the authentication hash. Firmware 3.0.1 includes the shasum command, so no script modification was needed. If shasum is not found on older firmware, you can replace the relevant line with openssl dgst -sha256.

Verifying dyndns.sh

IP Address Retrieval Check

First, use the -m get mode to verify that the connection to the Lambda function URL and IP address retrieval succeed.

/config/scripts/dyndns.sh -m get -u "https://XXXXXXXXXX.lambda-url.REGION.on.aws/"
Enter fullscreen mode Exit fullscreen mode

If a global IP address is returned, the network connection and Lambda function are working correctly. If nothing is returned, check the Lambda function URL, DNS resolution, or firewall rules.

DNS Record Update Check

Next, use the -m set mode to actually update the Route 53 record.

/config/scripts/dyndns.sh -m set \
  -u "https://XXXXXXXXXX.lambda-url.REGION.on.aws/" \
  -h "router.example.jp" \
  -s "<YOUR_SECRET>"
Enter fullscreen mode Exit fullscreen mode

On success, a response like the following is returned:

{
    "return_status": "success",
    "return_message": "router.example.jp has been updated to XXX.XXX.XXX.XXX",
    "status_code": "201"
}
Enter fullscreen mode Exit fullscreen mode

If the IP address has not changed, the following response is returned. This is also normal behavior.

{
    "return_status": "success",
    "return_message": "Your IP address matches the current Route53 DNS record.",
    "status_code": "200"
}
Enter fullscreen mode Exit fullscreen mode

Creating a Wrapper Script

Since dyndns.sh operates via command-line arguments, prepare a wrapper script for the task scheduler to call.

cat << 'EOF' > /config/scripts/run-ddns.sh
#!/bin/bash
/config/scripts/dyndns.sh \
  -m set \
  -u "https://XXXXXXXXXX.lambda-url.REGION.on.aws/" \
  -h "router.example.jp" \
  -s "<YOUR_SECRET>" \
  >> /var/log/aws-ddns.log 2>&1
EOF

chmod +x /config/scripts/run-ddns.sh
Enter fullscreen mode Exit fullscreen mode

Registering with the Task Scheduler

Configure the task scheduler in EdgeOS CLI configuration mode:

configure
set system task-scheduler task aws-ddns interval 300m
set system task-scheduler task aws-ddns executable path /config/scripts/run-ddns.sh
commit
save
exit
Enter fullscreen mode Exit fullscreen mode

This runs run-ddns.sh every 300 minutes (5 hours). Adjust the interval value based on how frequently your ISP changes your IP address.

Verifying the Configuration

Check the task scheduler registration:

configure
show system task-scheduler
exit
Enter fullscreen mode Exit fullscreen mode

Verifying the DNS Response

Verify that the record has been correctly reflected in Route 53. There is one caveat here: the EdgeRouter X CLI is Vyatta-based and interprets ? as a help invocation key. This behavior persists even in operational mode (the $ prompt). Therefore, you cannot paste a URL containing ? directly into the CLI.

To work around this, create and execute a script file with vi. Using echo does not help because the CLI still interprets the ?.

vi /tmp/dnscheck.sh
Enter fullscreen mode Exit fullscreen mode

Write the following content and save:

curl -s "https://dns.google/resolve?name=router.example.jp&type=A"
Enter fullscreen mode Exit fullscreen mode

Execute the script:

sh /tmp/dnscheck.sh
Enter fullscreen mode Exit fullscreen mode

If everything is working, JSON like the following is returned:

{
  "Status": 0,
  "TC": false,
  "RD": true,
  "RA": true,
  "AD": false,
  "CD": false,
  "Question": [{"name": "router.example.jp.", "type": 1}],
  "Answer": [{"name": "router.example.jp.", "type": 1, "TTL": 60, "data": "XXX.XXX.XXX.XXX"}],
  "Comment": "Response from 205.251.196.248."
}
Enter fullscreen mode Exit fullscreen mode

If the IP address in the data field under Answer matches the WAN interface IP address, the DDNS setup is working correctly. You can check the WAN IP address with show interfaces. The 205.251.196.248 in the Comment is a Route 53 nameserver IP address, indicating that the record is being served from the AWS side.

If you want to use dig or nslookup, you can install the dnsutils package, but the EdgeRouter X has limited storage (256MB NAND). For simple DNS verification, the method above is sufficient, and it is best to avoid installing unnecessary packages.

ping can also be used as a verification method that does not involve ?:

ping router.example.jp
Enter fullscreen mode Exit fullscreen mode

The first line shows PING router.example.jp (XXX.XXX.XXX.XXX), revealing the resolved IP address. When run from the LAN behind the router, the ping targets the router's own WAN IP address, so no reply is returned. Stop with Ctrl+C. Use this only to verify whether name resolution succeeds.

Checking the Logs

The execution results of run-ddns.sh are appended to /var/log/aws-ddns.log.

cat /var/log/aws-ddns.log
Enter fullscreen mode Exit fullscreen mode

Task scheduler execution may not be recorded in the system log. Verify scheduler operation by checking the log output in /var/log/aws-ddns.log.

Managing DNS Records

Use managerecord.py to view, modify, or delete configurations set by newrecord.py. The DynamoDB table name is automatically resolved from CloudFormation, so you do not need to look it up manually.

Reconfiguring / Changing the Shared Secret

Re-running newrecord.py overwrites the configuration for the same hostname. Use this method to change the TTL or shared secret.

Viewing the Current Configuration

python3 managerecord.py show router.example.jp
Enter fullscreen mode Exit fullscreen mode

Displays the configuration stored in DynamoDB along with the current IP and TTL from Route 53.

Hostname        : router.example.jp
Hosted zone ID  : ZXXXXXXXXXXXXXXXXXXXXXXX
TTL             : 60
Secret          : ********
Current IP      : XXX.XXX.XXX.XXX
Route 53 TTL    : 60
Enter fullscreen mode Exit fullscreen mode

Immediately Applying a TTL Change

The Lambda function only checks for IP address changes and does not compare TTL values. Therefore, the Route 53 TTL is not updated unless the IP address changes. To apply a TTL change immediately, use the update-ttl subcommand.

python3 managerecord.py update-ttl router.example.jp 300
Enter fullscreen mode Exit fullscreen mode

This updates both the DynamoDB TTL and the Route 53 record simultaneously. From the next IP address change onward, the updated TTL is automatically applied.

Deleting a Record

To delete only the DynamoDB record:

python3 managerecord.py delete router.example.jp
Enter fullscreen mode Exit fullscreen mode

To also delete the Route 53 DNS record, add --also-route53:

python3 managerecord.py delete --also-route53 router.example.jp
Enter fullscreen mode Exit fullscreen mode

In both cases, a confirmation prompt is displayed before execution.

Conclusion

I built a DDNS client that updates Route 53 DNS records from an EdgeRouter X using AWS Lambda + Route 53 + DynamoDB. This allows DDNS operation entirely within the AWS ecosystem, without depending on external DDNS services.

Since this solution retrieves the IP address and calls the Lambda function URL using curl in a shell script, it can be adapted to other network devices or servers that support curl and a scheduler such as cron.

The AWS repository does not provide a way to modify or delete configurations, so I created managerecord.py to fill that gap. It supports viewing configurations, immediately changing TTL, and deleting records via subcommands.

Top comments (0)