Posts Tagged xml
[UPDATE – March 2014]
This blog post was about discovering the low level details about AWS API signature. Additional signature methods are available today.
Nevertheless, enjoy the reading !
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
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')
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)
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.
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.