Role-playing with AWS

Last year the code hosting service Code Spaces was forced to close its doors after a hacker compromised their AWS account and deleted everything. We never learned the full story of how the account was compromised, but there’s little question that their fate would have been different if they had been using multifactor authentication (MFA). As we’ve moved much of our infrastructure to Amazon Web Services over the past year, we've tried to learn from their mistakes by requiring MFA everywhere.

AWS makes it easy to add MFA as an extra layer of protection on your account, but this only applies to the username and password you use to log into the AWS Console. The API keys associated with your account — which have the same level of access — take a little more effort to protect.

A critical piece of implementing MFA-protected API access is a feature of the Identity and Access Management (IAM) system called roles. A role is a temporary set of permissions that can be acquired — or assumed in AWS parlance — by IAM users and other AWS resources. A role is defined by a set of permissions and certain criteria, like a recent MFA authentication, that must be cleared before permitting access. Once a role is assumed, the acquirer gets a new set of temporary API keys that can be used to act with the role’s permissions.

Design

In reality we can’t actually MFA protect an IAM user’s API keys directly, but we can design our permissions model such that access to all interesting resources and actions are hidden behind roles. Users needing access to these actions and resources must assume these roles, and the role can enforce that an MFA authentication has happened, even for API-based requests (more on that later). As an example, our permissions model at Stitch looks like this:

Groups

  • Human — Basic permission like self-management of passwords, and nothing else

  • Engineering — Permission to assume the SuperEng role

  • Operations — Permission to assume the SuperOps role

  • SuperMan— Permission to assume the SuperMan role

Roles

  • SuperEng — access to resources and actions needed by software engineers, like EC2, RDS, EMR, and SQS. Requires MFA.

  • SuperOps — access to resources and actions used by our ops team, like Route53, VPC, and Security Groups. Requires MFA.

  • SuperMan — full access, including the ability to alter IAM resources like users, groups, and roles. Requires MFA.

All of our users belong to the Human group, and anyone requiring real access belongs to at least one of the other groups as well. Following the principle of least privilege, we keep all of the groups as small as possible, and, although we won’t describe them here, we actually have several additional groups and roles for more granular tiers of access.

Users working under this model can’t have permission to manage their own MFA devices directly, since that would enable an attacker to disable the device via the API and then break into the rest of the system. Another crucial element of this design is that users have no direct access to anything potentially damaging, since direct API access would not be MFA-protected. So, when implementing a design like this, be sure to neuter your user accounts.

Setting it up

Let’s start by setting up our first role. We’re going to follow Amazon’s documentation for this, but here’s a little extra guidance:

  • Head over to the Roles section of the IAM service and click Create New Role.

  • In Step 2, select Provide access between AWS accounts you own. This sounds like you need multiple accounts, but you don’t.

  • In Step 3, input the Account ID of the account that you’re logged into (find it here) and select Require MFA.

  • You don’t need to select any policies in Step 4 if you aren’t ready yet.

You now have a role that requires MFA, so next we’ll create a group of users with access to it. We’re again going to follow Amazon’s documentation for creating a basic group — except don’t add a policy to your group. Instead, after you’ve created the group, attach the following policy to it, swapping in your account ID and the name of the role you just created:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "sts:AssumeRole"
            ],
            "Resource": [
                "arn:aws:iam::000123456789:role/YourRoleName"
            ]
        }
    ]
}

That’s it – you’ve got the groundwork laid. Now, add a user to the group you just created, and add some more permissions to the role — but not the group!

Using roles

To do anything useful in our AWS account now, we’re going to have to assume a role, since we stripped all of the permissions out of our user account. Luckily, Amazon makes it easy to switch roles in the console, and their documentation for it is spot-on.

For API-based access, we have to send our user’s API keys, an MFA token, and the role name to the AssumeRole action, which will return a set of temporary API keys with the role’s permissions. Doing this every time we need to make an API call would be tedious, so I cooked up a couple of scripts to retrieve and install the temporary keys into a shell environment:

  • get-role-token is a Python script that uses the AWS CLI to generate a set of role session tokens based on the role’s name, an MFA code, and the existing API keys installed in your environment.

  • aws-assume-role provides bash functions for installing the a role’s temporary API keys. It depends on finding get-role-token on the PATH and sourcing the user’s API keys from /etc/profile.d/aws-env.sh.

Installing these scripts is easy: Make sure get-role-token is executable and available on the PATH, and then add aws-assume-role to your /etc/profile.d directory or source it from your user profile.

Using the scripts is even easier. Open up a new session in your terminal, call assume-role, supply the needed inputs, and you’re good to go. This approach will work with all up-to-date AWS SDKs, the fantastic boto library, and any other tools that follow Amazon’s suggested practices. If you start getting permission errors later, it means the temporary tokens have expired, so call assume-role again to get a new set.

A word of caution: these scripts are intended to be used in a development environment, not in production. In their current form, they won’t even work particularly well in a multitenant development environment because they require a single user’s credentials to be globally installed. If you’re tempted to use these in production, you should probably use service roles instead. As with any code you download from the internet, be sure you understand what it does before using it.

There you have it — a generic framework for implementing MFA-protected API access for AWS. Using the scripts above, we were able to migrate onto this framework in a small number of days without disrupting our team’s development flow. Give them a try, and let us know how they work for you.

Image credit: Carol Rosegg