Python Script for AWS’ Route 53 API Authentication


[UPDATE – March 2014]

This blog post was about discovering the low level details about AWS API signature.  Additional signature methods are available today.

I would not recommend anyone to actually use the type of scripts explained below or to manually compute signatures.  You can rely on AWS SDK s to do that automatically for you (see an example here).

Nevertheless, enjoy the reading :-) !

[/UPDATE]

In my last post entry – Setting Up a Private VPN Server on Amazon EC2 – I end up by providing tips to rely on a fixed DNS name every time you start your server.

For the impatient : the full script is available on GitHub.  For all the others, let’s understand the problem and the proposed solution.

Why automating DNS configuration ?

This is a common problem with public cloud machines : at every start, the machine receives a different public IP address and public DNS name.  There are two methods to keep a consistent name to access your machine :

  • bundle a Dynamic DNS client (such as inadyn) and access your machine through its DynDNS domain name.
  • create a DNS A (address) or CNAME record (an alias) to point to the public IP address of your machine

The latter solution is only valid if you have your own domain name.  It offers the maximum flexibility as you can configure the DNS as you need.

To automatize the task, your DNS provider must provide you with a programmatic way to change its configuration : an API. This is exactly what Amazon’s Route 53 DNS Service offers you.

To complete my previous article, I choose to add at the end of my script a command to dynamically associate the instance public IP name to my own domain name, such as myservice.aws.stormacq.com.  I choose to define an alias to the public DNS name setup by AWS, using a CNAME record.

How to programmatically configure your Route 53 DNS ?

AWS’ Route 53 API is RESTful, making it easy to manipulate it with command line, using “curl” command for example.  curl can be used to issue GET requests to read configuration data and POST requests to change DNS configuration.

Requests payload is defined in XML.  An example GET query would be

<?xml version="1.0"?>
<ListHostedZonesResponse xmlns="https://route53.amazonaws.com/doc/2012-12-12/">
  <HostedZones>
    <HostedZone>
      <Id>/hostedzone/MY_ZONE_ID</Id>
      <Name>aws.mydomain.com.</Name>
      <CallerReference>22F684C6-3886-3FFF-8437-E22C5DCB56E7</CallerReference>
      <Config>
        <Comment>AWS Route53 Hosted subdomain</Comment>
      </Config>
      <ResourceRecordSetCount>4</ResourceRecordSetCount>
    </HostedZone>
  </HostedZones>
  <IsTruncated>false</IsTruncated>
  <MaxItems>100</MaxItems>
</ListHostedZonesResponse>

To restrict access to  your DNS configuration, the API requires authentication.  Route 53 mandate the use of a proprietary HTTP header to authenticate requests.

Full details about Route 53 API is available on Amazon’s documentation.

The problem when using authentication and curl

AWS’ Route 53 authentication is described with great details and examples in the official documentation. Basically, it is based on a HMAC signature computed from the current date/time and your AWS Secret Key.

The HTTP header to be added to the request is as following

AWS3-HTTPS AWSAccessKeyId=MyAccessKey,Algorithm=ALGORITHM,Signature=Base64( Algorithm((ValueOfDateHeader), SigningKey) )

The Algorithm can be HMacSHA1 or HMacSHA256. The date is the current system time.  You can use your system time or you can ask AWS what is their system time.  The latter needs an additional HTTP call but this method will avoid time synchronisation issues between your machine and AWS. While curl is very versatile and can accommodate to many different situations, it can not compute an HMac signature to send as authentication header, a short python script is my solution.

The Python Solution

I choose to wrap the curl call into Python, let Python compute the signature, generate the appropriate HTTP header and then call curl, passing all remaining command line arguments to curl itself. The general idea is as following :

  • collect AWS_ACCESS_KEY and AWS_SECRET_KEY
  • Compute the Signature
  • Call curl with correct parameters to inject the authentication HTTP header and all command line parameters we have received

Signature

The signature is generated with this code.  It receives two String as input (the text to sign and the key). I hard-coded the algorithm. The function returns the base64 encoded signature.

def getSignatureAsBase64(text, key):
    import hmac, hashlib, base64
    hm  = hmac.new(bytes(key, "ascii"), bytes(text, "utf-8"), hashlib.sha256)
    return base64.b64encode(hm.digest()).decode('utf-8')

AWS’s date

Retrieving AWS’s date is similarly easy

def getAmazonDateTime():
    import urllib.request
    httpResponse=urllib.request.urlopen("https://route53.amazonaws.com/date")
    httpHeaders=httpResponse.info()
    return httpHeaders['Date']

Formatting the header

And the header is formatted with

def getAmazonV3AuthHeader(accessKey, signature):
    # AWS3-HTTPS AWSAccessKeyId=MyAccessKey,Algorithm=ALGORITHM,Signature=Base64( Algorithm((ValueOfDateHeader), SigningKey) )
    return "AWS3-HTTPS AWSAccessKeyId=%s,Algorithm=HmacSHA256,Signature=%s" % (accessKey,signature)

Calling curl

Finally, we just have to call the curl command :

        import subprocess
        curlCmd = ["/usr/bin/curl",
                        "-v" if DEBUG else "",
                        "-s", "-S",
                        "--header",
                        "X-Amzn-Authorization: %s" % AWS_AUTH,
                        "--header",
                        "x-amz-date: %s" % AWS_DATE]
        curlCmd += args.curl_parameters
        curlCmd += [args.curl_url]
        logging.debug(" ".join(curlCmd))                
        return subprocess.call(curlCmd)

The full script is available under a BSD license on GitHub.  There is some additional plumbery to handle command line arguments, to load the AWS credentials etc … which is out of the scope of this article.

Conclusion

Using this script, you can easy use curl command to GET or POST REST requests to Route 53’s API.

I am using this script to create custom CNAME records whenever an EC2 instance is started, allowing me to reuse a well known, stable DNS public name to access my instance. A sample XML to define a CNAME is posted on GitHub together with the source code.

Enjoy !

, , , , , ,

  1. #1 by andrew on 12/03/2014 - 07:47

    Hi,

    Im stuck at how to write the key + secret key to the file.. i get the following error

    File “./AWSDNSAuth.py”, line 160, in
    sys.exit(main())
    File “./AWSDNSAuth.py”, line 148, in main
    raise(e)
    File “./AWSDNSAuth.py”, line 104, in main
    config.read(args.credentials)
    File “/usr/lib/python3.3/configparser.py”, line 689, in read
    self._read(fp, filename)
    File “/usr/lib/python3.3/configparser.py”, line 1075, in _read
    raise MissingSectionHeaderError(fpname, lineno, line)
    configparser.MissingSectionHeaderError: File contains no section headers.
    file: /home/ubuntu/aws/aws.key, line: 1
    ‘xxxxxxxxxxxxxxxxx’
    -:1: parser error : Document is empty

    ^
    -:1: parser error : Start tag expected, ‘<' not found

    ^

  2. #2 by andrew on 12/03/2014 - 08:36

    ok ignore that, I found the instructions at the bottom of the read me file… However,

    Now I get

    Unable to determine service/operation name to be authorized

    not really sure how to configure the url… part

  3. #3 by andrew on 12/03/2014 - 09:06

    Thee posts all by me!

    I got it working. I can view the records, and using the Create xml create a new record set.. but this is where my knowledge stops…

    Two things, I can’t seem to modify a record I get this:

    SenderInvalidChangeBatchTried to create resource record set [name=’vpn.amelvin.co.uk.’, type=’CNAME’] but it already exists662bd0af-a9bc-11e3-ad76-25e436c9fcb4

    I can create a new record but not modify one. so this is a start..

    The second thing is that you xml script has the amazon.public.dns just written in there.. How do you automate it so that the xml file picks up the current address of the amazon instance?

    If I can get that working then it would be great!

    Then I just need the VPN to work in ubuntu 13 and I’m set!

  4. #4 by Sébastien Stormacq on 12/03/2014 - 13:27

    Actually, if you’re serious about this I would encourage you to explore the higher level Python SDK (http://boto.readthedocs.org/en/latest/ref/route53.html) – It contains methods and objects to manage your Route 53 configuration without bothering about the low level HTTP communication. This blog post is over a year old and AWS did add many new features in Boto since then :-)

(will not be published)