30 June, 2020

Static IPs using a Raspberry Pi and AWS

In need of a static IP? Maybe you want to run a VPN at home, maybe you want to be able to access your NAS when you're away or maybe it's one of those things that you don't really need but kinda want for reasons.

If you can't afford an enterprise ISP at home (who typically provide a static IP as part of the deal) the most popular option are subscription static IP services. You pay a monthly fee and run software either on a home server, PC or phone that pings back to the service provider so they can update an A record with your public IP. These services can get quite expensive (as well as being bad news for the subscription-phobic) and as the old saying goes, "why let someone else do better and with more uptime what you can do with a Raspberry Pi in your cupboard?".

We can use the AWS CLI to allow the Pi to update A-records for DNS records in route53. The Pi can figure out what its public IP is so all we need to do is periodically check the public IP and update route53 as we go.

Prerequisites

  • A (working) Raspberry Pi
  • An AWS account
  • A domain with DNS managed by route53

AWS Setup

The first thing we need to do is create an IAM role that locks the Pi's CLI access down to just the resources it needs. This might seem like more hassle than it's worth but if your Pi does get compromised, using root AWS credentials will give attackers the ability to do anything with your AWS account including running up large bills and accessing any unsecured data.

The first thing to do is to create a policy that'll allow the IAM user to update the record sets for your domain. Navigate to the policy page, create a policy and under the JSON tab enter:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "route53:GetHostedZone",
                "route53:ChangeResourceRecordSets"
            ],
            "Resource": "arn:aws:route53:::hostedzone/<YOUR_HOSTED_ZONE_ID>"
        }
    ]
}

If you're not sure what your hosted zone id is, skip ahead to the script section for the relevant AWS CLI commands. Save the policy with a meaningful name.

We're now ready to attach this policy to an IAM user that your Pi can use. Create a user, select the policy you created in the previous step to attach it to the user, enter a meaningful name e.g. pi-home and check the box for programmatic access. Save the id and secret somewhere secure for now.

Pi Setup

The following is assuming you're using Raspberry Pi OS. Install the AWS CLI and jq by running:

sudo apt install -y python3-pip libffi-dev jq
sudo pip3 install awscli

and configure the cli by running:

aws configure

Enter the key and secret that you generated for your user and your Pi is now ready to modify the DNS settings for your domain.

The script

We'll use a simple bash script running on a cron schedule to keep the A-record for the domain up-to-date. Our script will take a hosted zone id and a subdomain as arguments and when executed will upsert an A-record for the subdomain to point to the public IP address of the caller.

#!/bin/bash
ip=$(dig TXT +short o-o.myaddr.l.google.com @ns1.google.com | awk -F'"' '{ print $2}')


# Check to see if we managed to extract an IP
if [ -z "$ip" ]
then
	echo "No ip returned from DNS probe"
	exit 1
else
	echo "DNS probe returned: $ip"
fi

# Get the domain associated with the hosted zone id
domain=$(aws route53 get-hosted-zone --id $1 | jq -r ".HostedZone | .Name")

if [ -z "$domain" ]
then
	echo "Unable to fetch domain from route53"
	exit 1
else
	echo "Using domain: $domain"
fi

echo "Creating A-record: $ip -> $2.$domain"

# Update the recordset with the A-record for our IP address
response=$(aws route53 change-resource-record-sets --hosted-zone-id $1 --change-batch "{\\"Comment\\":\\"\\",\\"Changes\\":\[{\\"Action\\":\\"UPSERT\\",\\"ResourceRecordSet\\":{\\"Name\\":\\"$2.$domain\\",\\"Type\\":\\"A\\",\\"TTL\\":300,\\"ResourceRecords\\":\[{\\"Value\\":\\"$ip\\"}\]}}\]}")

echo "Response from route53: $response"
exit 0

And that's it. Make it executable and we can call it as follows and it'll create an A-record that points to your Pi's IP.

./dns.sh <hosted_zone_id> <subdomain>

The last thing to do is to add it to a crontab to run periodically to ensure your A-records stay up-to-date if your ISP issues you a new IP address.

© 2023 Henry Course