Sunday 5 October 2014

AWS Tip of the day: Signing requests using IAM instance roles

If you are averse to having the AWS SDKs conveniently manage IAM instance role permissions and service requests (including Signature V4) then knowing that you need to include the IAM role token in the request header is quite important. This post describes the steps needed to sign an API request using an instance IAM role. It is assumed that you already have an instance launched with a role that has permission to perform the requested API action. For this example the role is going to be named ec2-ro and as the name implies it has read-only permissions on EC2 APIs.

As a good starting point we will use the first GET example on this page as a working base and modify it to retrieve the instance role credentials and add the session token to the request as follows.

Step 1: Retrieve the instance role credentials

As per the AWS documentation, instance credentials can be retrieved from the instance metadata by performing a GET against the following URL, where <role-name> is the name of the instance role containing the relevant permissions:

http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name>

In our example with the role named ec2-ro the result of the request will be similar to the result below where the body of the token has been replaced with [...] for brevity:

$ curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ec2-ro
{
  "Code" : "Success",
  "LastUpdated" : "2014-10-05T07:25:22Z",
  "Type" : "AWS-HMAC",
  "AccessKeyId" : "ASIXXXXXXXXXXXX",
  "SecretAccessKey" : "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234",
  "Token" : "AXXXXXX//////////[...]==",
  "Expiration" : "2014-10-05T13:30:35Z"
}

Achieving the same in Python is pretty simple:

creds = requests.get('http://169.254.169.254/latest/meta-data/iam/security-credentials/ec2-ro')
access_key = creds.json()['AccessKeyId']
secret_key = creds.json()['SecretAccessKey']
token = creds.json()['Token']

Which we can then use to replace access key code in the example:

access_key = os.environ.get('AWS_ACCESS_KEY_ID')
secret_key = os.environ.get('AWS_SECRET_ACCESS_KEY')

Unfortunately we are not finished yet as running the new code now results in a 401 response with the following message:

"AWS was not able to validate the provided access credentials"

The reason for the error above is that the instance credentials require a session token to be considered valid, moving on to the next step.

Step 2: Add the session token to the request headers

Fixing the invalid credential error above is relatively trivial and simply involves adding the session token to the request headers with a header name of x-amz-security-token. So replacing this line:

headers = {'x-amz-date':amzdate, 'Authorization':authorization_header}

With the following line:

headers = {'x-amz-date':amzdate, 'Authorization':authorization_header, 'x-amz-security-token':token}

Allows the code to execute successfully. One thing to be aware of is that the instance credentials are rotated fairly frequently and the credentials will need to be refreshed after the date and time in the instance metadata Expiration field.