AWS Security Blog

How Do I Protect Cross-Account Access Using MFA?

Today AWS announced support for adding multi-factor authentication (MFA) for cross-account access. In this blog post, I will walk you through a common use case, including a code sample, which demonstrates how to create policies that enforce MFA when IAM users from one AWS account make programmatic requests for resources in a different account.

Many of you maintain multiple AWS accounts, so I am frequently asked how to simplify access management across those accounts. IAM roles provide a secure and controllable mechanism to enable cross-account access. Roles allow you to accomplish cross-account access without any credential sharing and without the need to create duplicate IAM users. With today’s announcement, you can add another layer of protection for cross-account access by requiring the users to authenticate using an MFA device before assuming a role.

Imagine your company maintains multiple AWS accounts: Development, Staging, and Production. Let’s assume you want to centralize the access management of all these “child” accounts using a single “parent” account that contains your IAM users.

Let’s take a look at how you would configure this parent-child relationship from the perspective of access control. The goal is to allow IAM users in the parent account to perform privileged actions like Amazon EC2 TerminateInstances and Amazon DynamoDB DeleteTable in the child accounts, but only if the they are authenticated using MFA.

There are three steps involved – establishing trust between accounts, configuring MFA devices for users, and performing privileged actions. Let’s dive in to the details of each:

Diagram illustrating a trusted account and trusting accounts

1. Establishing trust between accounts

In this step the child accounts become the “trusting” accounts and the parent account becomes the “trusted” account.

Let’s begin with the Development account. You set it up to trust the parent account by creating a role, say PrivilegedActionsRole, and specifying the parent account as the trusted principal. You then configure the role to require MFA. You can complete these steps using the AWS Management Console. Switch to the IAM console and use the Create Role wizard for Role for Cross-Account Access and then select the Require MFA check box, as shown in the picture below.

Screenshot showing the Require MFA check box

Behind the scenes, the role gets configured with a role trust policy which effectively says “I trust the IAM users from the parent account to assume this role as long as the user is authenticated using MFA.”  The role trust policy created by the wizard is shown below.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {"AWS": "Parent-Account-ID"},
      "Action": "sts:AssumeRole",
      "Condition": {"Bool": {"aws:MultiFactorAuthPresent": true}}
    }
  ]
}

Notice that the principal is set as the parent account ID and the condition specifies the aws:MultiFactorAuthPresent key. The condition checks if aws:MultiFactorAuthPresent is true, to verify that the user either authenticated with MFA when they first signed in to the AWS Management Console, or provided MFA authentication information when they made the AssumeRole API call either from the AWS CLI or using one of the AWS SDKs. If either of these two scenarios does not apply, the user will not be able to assume the role.

It is important to note that the MFA condition can be specified only in the role trust policy (and not in the role access policy discussed below). It controls whether MFA is required or not to assume the role.

The next step of the wizard assigns an access policy to the role which determines the permissions granted to the user that successfully assumes the role. Continuing with our example, the following policy grants permission to call Amazon EC2 TerminateInstances and Amazon DynamoDB DeleteTable.

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

Repeat these steps for the Staging and Production accounts.

Now let’s move to the parent account setup. You need to grant your users permission to assume the roles created in the respective child accounts. You accomplish this by attaching a user or group policy that grants the AssumeRole action and sets the resource to the ARN (Amazon Resource Name) of the roles you created in your child accounts.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "sts:AssumeRole",
      "Resource": [
       "arn:aws:iam::Development-Account-ID:role/PrivilegedActionsRole",
       "arn:aws:iam::Staging-Account-ID:role/PrivilegedActionsRole",
       "arn:aws:iam::Production-Account-ID:role/PrivilegedActionsRole",
     ]
    }
  ]
}

2. Configuring MFA devices for users

In this step, you configure MFA devices for IAM users in the parent account so that the users can assume the roles you created in the child accounts. You can setup a hardware or virtual MFA device using the AWS Management Console, by following the steps described in the IAM documentation.

3. Performing privileged actions

With the setup described above in place, users from the parent account can now perform actions on the resources in the child accounts. The following picture shows the three steps involved when users from the parent account want to assume the MFA-protected role to perform actions on the resources in the child accounts. These steps can be performed either using AWS SDKs or using AWS CLI.

Diagram illustrating the three steps involved when users from the parent account want to assume the MFA-protected role

Step 1:  Authenticate using access key ID and secret access key.

Step 2:  Get a code (Time-Based One-Time Password or TOTP) from the MFA device. Pass the ID of the MFA device and the code to the AssumeRole request. These are the new SerialNumber and TokenCode parameters. These parameters are in addition to the required parameters for the ARN of the role to assume and a session name. On success, get back temporary security credentials.

Step 3:  Use the temporary security credentials to perform privileged actions in the child account.

Now that we have looked at the entire setup, let’s take a look at a code sample. The following example program, written using the AWS SDK for Python (Boto), shows how you can assume an MFA-protected role by passing MFA parameters to AssumeRole. The sample code then uses the temporary security credentials from the AssumeRole response to terminate an instance in the trusting account.

import boto
from boto.sts import STSConnection
from boto.ec2 import EC2Connection

# Role from the trusting account to assume
role_arn = "arn:aws:iam::ACCOUNT-NUMBER-WITHOUT-HYPHENS:role/ROLE-TO-ASSUME"
# String to identify the role session
role_session_name = "AssumeRoleSessionWithMFA"

# MFA device ID (serial number for hardware device or ARN for virtual device)
mfa_serial_number = "arn:aws:iam::ACCOUNT-NUMBER-WITHOUT-HYPHENS:mfa/MFA-DEVICE-ID"

# The calls to AssumeRole must be signed using the access key ID and
# secret access key of an IAM user. The IAM user credentials can be 
# in environment variables or in a configuration file and will be 
# discovered automatically by the STSConnection() function. For more 
# information, see the Python SDK documentation:
# http://boto.readthedocs.org/en/latest/boto_config_tut.html
print "nConnecting to Security Token Service..."
sts_connection = STSConnection()
print "Connection successful."

# Assume the role
print "nAssuming role", role_arn, "using MFA device", mfa_serial_number, "..."
# Prompt for MFA one-time-password
mfa_token = raw_input("Enter the MFA code: ")
role_session = sts_connection.assume_role(
    role_arn=role_arn,
    role_session_name=role_session_name,
    mfa_serial_number=mfa_serial_number,
    mfa_token=mfa_token
)
print "Assumed the role successfully."

# Use the role-provided temporary security credentials to connect to EC2
print "nConnecting to Elastic Compute Cloud service..."
ec2_connection = EC2Connection(
    aws_access_key_id=role_session.credentials.access_key,
    aws_secret_access_key=role_session.credentials.secret_key,
    security_token=role_session.credentials.session_token
)
print "Connection successful."

# Terminate instance
print "nTerminating EC2 instance..."
instance_id = raw_input("Enter id of the instance to teminate: ")
response = ec2_connection.terminate_instances(instance_id)
print response
print "Done."

Here is the output from a sample run:

Screenshot showing the output from a sample run

To summarize, IAM roles provide a secure and controllable mechanism to simplify access management across accounts. With today’s announcement, you can add another layer of protection by enforcing MFA for such cross-account access. For additional information about this new feature, visit the Configuring MFA-Protected API Access section in the Using IAM guide.

Please let us know what you think. You can post comments below or start a thread in the IAM forum.

– Shon