Using MFA With the AWS API

Mon, Jul 3 2017 - 6 minute read

The awesome and informative Last week in AWS newsletter by Corey Quinn has been around for a few weeks now, with curated AWS announcements, tips, tools and blog posts. If you are responsible for making services run using AWS, you should definitely subscribe. At the bottom of the very first issue there is a quick tip about requiring MFA for API usage, which solves a long standing issue we have had with AWS, namely that while you could require an MFA code to log into the web console, there didn’t seem to be a similar limitation for API access, meaning that having API keys on your laptop was a potential security risk. The tip links to the AWS docs on MFA, with instructions on preventing access to the API unless you entered an MFA code, but there was little on that page about how to actually go about entering said code when using the AWS command line tools (or other tools that use the API, such as terraform). This blog post explains what I discovered and set up in order to make using MFA with the AWS command line pretty delightful.

Restricting API access

First, I’ll go through the setting for restricting access to the API without having first typed an MFA code.

I’m assuming here that you have an IAM user, and that you have already set up an MFA token (you will be prompted for your MFA code on login to the web console if this is the case). I’m also assuming that you have set up access keys and can access the API via the AWS CLI tools (in other words, you can run something like aws s3 ls on the command line and it will list your S3 buckets).

When setting up your IAM user, you will have assigned a policy to it granting it permissions. The policy will look something like this (the policy here is the AdministratorAccess policy, which grants access to everything):

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "*",
            "Resource": "*"
        }
    ]
}

in order to require MFA, you just add a condition to this policy (if you’re using a predefined IAM policy such as AdministratorAccess then you’ll have to make a copy of it in order to change it) requiring that aws:MultiFactorAuthPresent be true. The AdministratorAccess policy shown above would be changed to look like:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "*",
            "Resource": "*",
            "Condition": {
                "Bool": {
                    "aws:MultiFactorAuthPresent": "true"
                }
            }
        }
    ]
}

Now, if you apply that policy to a user, and they have no other policies applied to them giving them access, then when they try to access the API they will get an AccessDenied error, unless they type in the MFA code first.

If you wish, you can allow access to certain APIs without MFA by adding additional policies to the user or group, such as ReadOnlyAccess. In my experience however, while this may seem like a good idea on the surface, it’s actually more convenient to just enter your MFA key at the start of the day and use it than it is to have to think about whether your actions are read-only or not and choose the appropriate long or short-term AWS keys to use.

Using MFA via the AWS API with GetSessionToken

Now we have limited access to the API without MFA, how do we actually type the code when using the API?

There are a few different solutions to this, but if you are only protecting access to a single AWS account, then the easiest is to use the STS GetSessionToken api call to get a set of temporary credentials and use them.

If you did this manually, you would run something like the following:

$ aws --profile myprofile sts get-session-token \
  --serial-number arn:aws:iam::1234567890:mfa/yourname \
  --token-code 123456
{
    "Credentials": {
        "SecretAccessKey": "...",
        "SessionToken": "...",
        "Expiry": "2017-07-03T00:00:00Z",
        "AccessKeyId": "..."
    }
}

Then you would copy/paste the keys into AWS_ACCESS_KEY_ID and similar environment variables, and you could use the AWS command line as you normally would until the temporary keys expire.

Manually copying/pasting keys is a pain, and while you could write a simple shell wrapper to extract the keys and set the relevant environment variables, this is also less than ideal if you deal with multiple AWS accounts. What would be better is if the temporary credentials could be stored as a profile in ~/.aws/credentials and you could just pass --profile myprofile to the aws command line.

There is a tool at https://github.com/broamski/aws-mfa that does exactly this. The way it works is that you put your AWS credentials into a ‘long-term’ profile, and when you run the tool it will prompt you for your MFA key, generating temporary credentials under your normal profile name, which will be valid for up to 12 hours. During this time, you use the aws api as normal, specifying your profile, and once it runs out, you get a TokenExpired error.

$ cat ~/.aws/credentials
[myprofile-long-term]
aws_access_key_id = AKIAHUNTER2HUNTER2HU
aws_secret_access_key = V293IC0geW91IGFjdHVhbGx5IGRlY29kZWQgdGhpcy4K
aws_mfa_device = arn:aws:iam::1234567890:mfa/yourname

$ aws-mfa --profile myprofile
INFO - Validating credentials for profile: myprofile
INFO - Your credentials have expired, renewing.
Enter AWS MFA code for device [arn:aws:iam::1234567890:mfa/yourname] (renewing
for 43200 seconds):123456
INFO - Success! Your credentials will expire in 43200 seconds at: 2017-07-03
16:00:00+00:00

$ aws --profile myprofile s3 ls
2016-07-03 00:00:00 mybucket

It’s a python app, so you install it using pip:

pip install aws-mfa

If you’re on a mac however, there is a homebrew formula I created to make installation a little easier:

brew tap chef/chefops-tools
brew install aws-mfa

To set up the aws-mfa tool, first rename your existing profile in ~/.aws/credentials and add -long-term to the end. Next, add an aws_mfa_device line to the end. You can look this up in the IAM console if you wish, but it’s always of the format arn:aws:iam::YOURAWSACCOUNTID:mfa/YOURUSERNAME, so it should be fairly easy to construct. Once done, your ~/.aws/credentials file should look something like this:

[myprofile-long-term]
aws_access_key_id = AKIAHUNTER2HUNTER2HU
aws_secret_access_key = V293IC0geW91IGFjdHVhbGx5IGRlY29kZWQgdGhpcy4K
aws_mfa_device = arn:aws:iam::1234567890:mfa/yourname

Once you have completed set up, run aws-mfa --profile myprofile (without -long-term at the end), and it will prompt you to enter your MFA code. Enter the code, and press enter. Then, aws-mfa will store the temporary credentials in the ‘myprofile’ profile, which you would then use just like you did before mfa was set up. Do this once at the start of the day (or when you first need it), and you’re good for the rest of the day.

There are some caveats. Most of these however are related to cross account access or single sign on. With cross-account access, you can still use and require MFA, but the method of entering the key differs, and you enter it as part of your AssumeRole api call. The other caveat is if you use SAML. Because you don’t have a single IAM user when using SAML, you can’t register an MFA token, and you’re at the mercy of whatever MFA facilities your SAML provider provides. Sadly, I haven’t yet worked out a good way to use MFA with the AWS API if you use something like Okta for sign in. Maybe that’s a topic for a future blog post.