AWS and other interesting stuff

Securing Access To AWS With Short-Lived Credentials And MFA

· by Steve Hogg · Read in about 9 min · (1837 Words)


Using IAM Roles is one of the most important security measures you can take when running systems on AWS. They let you use short-lived credentials to access AWS resources rather than long-lived credentials. In this blog I’ll explain why short-lived credentials are important from a security perspective, and I’ll investigate the use of roles in combination with Multi-Factor Authentication (MFA) when using the command line (CLI). Are roles and MFA worth the hassle? How much extra effort is there on the users’ part? How does this setup affect user activity logging?

Long-lived versus Short-lived credentials

When you’re managing AWS resources, creating, reading and writing things like S3 buckets, DynamoDB tables etc, you need to provide credentials to prove that you’re allowed to perform those actions. In this context, long-lived credentials are access keys that you generate for a user. They’re long-lived in that they’ll typically be set once and then not changed. You can setup your software to use these long-lived credentials so they can perform these actions, but it’s a really bad idea. Attackers love finding credentials like this hard-coded into version control or in environment variables as they can use them to access your account to steal data or cause you other headaches.

Short-lived credentials also provide access to perform actions on resources, but the credentials are only temporary; they will expire soon and once they expire access is denied. To continue to use the role, a new set of credentials must be requested. Expiry times when assuming an AWS role can be configured to last anywhere between 15 minutes and 1 hour1. The idea here is that the less time credentials are valid for, the less time would-be-attackers have to use them; while an attacker could still use these credentials before they’ve expired, it minimises your exposure. When combined with Multi-Factor Authentication, roles provide a nice way to protect staff access.

Multi-Factor Authentication (MFA) means users have to provide more than one piece of evidence that they are who they say they are. In this example, they provide their assigned access credentials and also a code that shows they have a specific device in their possession. This device can be within an app on their phone (a virtual device), or a physical device. The device continually generates new codes that the user can present to prove that they have that device at hand.

Example MFA Devices
Google Authenticator running on my phone SafeNet IDProve, a physical MFA card

When staff use MFA to prove their identity and request short-lived credentials it improves security. You can be confident users are who they say they are and that their access will automatically expire after a short time, thereby minimising the window of opportunity for attackers. This makes things like one staff member impersonating another, BYOD device theft and office network compromise less-likely to be issues.

In the sections below I’ll investigate the use of roles+MFA with the AWS CLI: what it’s like from a user’s perspective and how easy it is to audit user activity.

Test Setup

To test MFA CLI access I setup the following resources:

  • 6 Users: barbara, mary, matthew, stewart, sam and stan
  • 3 Groups: BossGroup, MarketingGroup, SalesGroup
  • 3 Roles: BossRole, MarketingRole, SalesRole
  • 2 Managed Policies: ManageOwnCredentialsPolicy, RequireMFAPolicy
  • 1 S3 Bucket: “h4-company-bucket”

The 3 CloudFormation templates linked below setup the resources listed above:

The users belong to these groups:

User BossGroup MarketingGroup SalesGroup
barbara y
mary y
matthew y
stewart y
sam y
stan y y y

Each group has a policy attached that allows it to assume the associated role e.g. anyone in the SalesGroup can assume the SalesRole.

Each role has polices that restricts access to the h4-company-bucket in the following way:

Folder BossRole MarketingRole SalesRole
/boss read/write
/marketing read read/write
/sales read read read/write

Each group has the ManageOwnCredentialsPolicy attached so that users can change their own passwords, access keys and set MFA devices. The RequireMFAPolicy policy is applied to each group to make MFA a requirement for all user requests. 2

MFA Setup

Here are the steps I went through to setup an MFA device for the user stan. The interesting point here is that the ManageOwnCredentialsPolicy allows users to do this themselves.

aws --profile h4-stan iam create-virtual-mfa-device --virtual-mfa-device-name stan --outfile stan-mfa-qrcode.png --bootstrap-method QRCodePNG
    "VirtualMFADevice": {
        "SerialNumber": "arn:aws:iam::12345678910:mfa/stan"

Open the png to scan the QR code

open stan-mfa-qrcode.png

Enable the MFA device

aws --profile h4-stan iam enable-mfa-device --user-name stan --serial-number "arn:aws:iam::12345678910:mfa/stan" --authentication-code-1 "111111" --authentication-code-2 "222222"

Get the MFA device details

aws --profile h4-stan iam list-virtual-mfa-devices | jq '.VirtualMFADevices[] | select(.SerialNumber | endswith("mfa/stan"))'
  "SerialNumber": "arn:aws:iam::12345678910:mfa/stan",
  "EnableDate": "2016-11-27T00:58:36Z",
  "User": {
    "UserName": "stan",
    "Path": "/",
    "CreateDate": "2016-11-24T07:02:17Z",
    "UserId": "<REDACTED>",
    "Arn": "arn:aws:iam::12345678910:user/stan"
aws --profile h4-stan iam enable-mfa-device --user-name stan --serial-number


Without MFA Settings - (It Should Fail)

Using the credentials without an MFA device configured …


aws_access_key_id = AAAAAAAAAAAAAAAAAAAA

role_arn = arn:aws:iam::12345678910:role/SalesRole
source_profile = h4-stan

… results in the following error:

$ aws --profile h4-sales s3 ls

An error occurred (AccessDenied) when calling the AssumeRole operation: User: arn:aws:iam::12345678910:user/stan is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::12345678910:role/SalesRole with an explicit deny

With MFA Settings - (It Should Work)

The mfa_serial setting needs to be set for the role rather than the user, otherwise you get the same “not authorized to perform: sts:AssumeRole with an explicit deny” as above.

Now, with the mfa_serial set:

aws_access_key_id = AAAAAAAAAAAAAAAAAAAA

role_arn = arn:aws:iam::12345678910:role/SalesRole
source_profile = h4-stan
mfa_serial = arn:aws:iam::12345678910:mfa/stan

Stan assumes the sales role and uploads a file. An MFA code is required.

$ aws --profile h4-sales s3 cp readme.txt s3://h4-company-bucket/sales/
Enter MFA code:
upload: ./readme.txt to s3://h4-company-bucket/sales/readme.txt

Subsequent calls do not need another MFA code until the 1 hour session times out.

$ aws --profile h4-sales s3 ls s3://h4-company-bucket/sales/readme.txt
2016-11-25 10:51:23        138 readme.txt

The role Stan is using does not have access to other folders

$ aws --profile h4-sales s3 ls s3://h4-company-bucket/marketing

An error occurred (AccessDenied) when calling the ListObjects operation: Access Denied

In this example, Stan also belongs to the marketing group, so he can assume the marketing role too by adding the configuration to the file …

role_arn = arn:aws:iam::12345678910:role/MarketingRole
source_profile = h4-stan
mfa_serial = arn:aws:iam::12345678910:mfa/stan

… and running the same command above, but this time with the marketing role (this is a newly assumed role so a new MFA code needs to be entered)

$ aws --profile h4-marketing s3 cp s3://h4-company-bucket/marketing/
Enter MFA code:
upload: ./ to s3://h4-company-bucket/marketing/
$ aws --profile h4-marketing s3 ls s3://h4-company-bucket/marketing/
2016-11-25 11:27:01          6

The marketing role has read-only access to the sales folder, so a copy fails …

$ aws --profile h4-marketing s3 cp s3://h4-company-bucket/sales/

upload failed: ./ to s3://h4-company-bucket/sales/ An error occurred (AccessDenied) when calling the PutObject operation: Access Denied

… but a read succeeds as expected (marketing has read access to the sales folder)

$ aws --profile h4-marketing s3 ls s3://h4-company-bucket/sales/
2016-11-24 23:54:22          0
2016-11-25 10:51:23        138 readme.txt

If Stan really wants to upload that file, he’ll need to use the sales role.

$ aws --profile h4-sales s3 cp s3://h4-company-bucket/sales/
upload: ./ to s3://h4-company-bucket/sales/

Note, MFA was not required above as the role token is still cached in stan’s ~/.aws/cli/cache folder.


Role based user access with MFA works well. It doesn’t seem to be too cumbersome from the users’ point-of-view (depending on how many groups they belong to), and the additional security benefits are worth the investment in time setting it up.

As far as I’m aware, there is no way to get a list of active role sessions from the AWS API. To track user activity you would need to enable CloudTrail so that all requests are logged to an S3 bucket. You would then be able to trace user activity by looking up the roleSessionName they’re assigned when they assume a role e.g.

cat 12345678910_CloudTrail_us-east-1_20161125T0155Z_VBRMTQ4yysJY6Zrn.json.gz | gzip -d | jq '.Records[] | select(.eventName=="AssumeRole") | [ {"userName": .userIdentity.userName, "roleArn": .requestParameters.roleArn, "roleSessionName": .requestParameters.roleSessionName, "sourceIPAddress": .sourceIPAddress, "eventTime": .eventTime, "expiration": .responseElements.credentials.expiration } ]'
    "userName": "stan",
    "roleArn": "arn:aws:iam::12345678910:role/SalesRole",
    "roleSessionName": "AWS-CLI-session-1480038822",
    "sourceIPAddress": "",
    "eventTime": "2016-11-25T01:53:43Z",
    "expiration": "Nov 25, 2016 2:53:43 AM"

An Idea For Automatically Distributing Credentials

Here is a way of extending this example to distribute credentials automatically and securely using a CloudFormation Custom Resource.

Quirks With A Role-based Setup

Why No Bucket Polices?

As roles are being used, we need to know the RoleId for each role to setup a working bucket policy3. This is because roles can not be used as policy principals. CloudFormation’s AWS::IAM::Role does not allow you to get a RoleId value. One way to do this could be to setup a custom resource that triggers a Lambda function to lookup the RoleId for a given role e.g. the SDK equivalent of this:

$ aws iam get-role --role-name BossRole

It would be worth doing this if cross-account access was required. From the reference link (3) above:

“When accessing a bucket from within the same account, it is not necessary to use a bucket policy in most cases. This is because a bucket policy defines access that is already granted by the user’s direct IAM policy. S3 bucket policies are usually used for cross-account access…”

FYI: if you try and use a role as the principal in a bucket policy it takes CloudFormation about 10 minutes to come back with an error4

Removing A User From A Group Doesn’t Revoke Access Immediately

When you remove a user from a group, the user can still use any role they still have an active session for. i.e. you remove their access keys or delete their user completely and they’ll still be able to use the roles they have tokens for. Tokens timeout after 1 hour and they’re cached in ~/.aws/cli/cache 5

For security, you should use the Revoke Session option on the role the old group used. That’ll make all users using that role have to re-authenticate with an MFA code. In a production environment a manual security step like this wouldn’t be ideal.

A brute-force blanket security measure may be to revoke all sessions for all user roles whenever a user configuration change is made. That’d mean the whole company having to re-authenticate again, but if user changes aren’t frequent then it’d be a quick and easy way get rid of the 1 hour window of potential abuse.

Alternatively, a custom resource that tracks user group state and revokes sessions in a finer-grained way would be an option.


Updating a CloudFormation stack from CLI

Validate the template and see which capabilities are required:

$ aws cloudformation validate-template --template-body file://mfa-cli-groups.json

Update the stack:

$ aws cloudformation update-stack --stack-name MFAGroupsStack --template-body file://mfa-cli-groups.json --capabilities CAPABILITY_NAMED_IAM
$ aws cloudformation update-stack --stack-name MFAUsers --template-body file://mfa-cli-users.json --capabilities CAPABILITY_NAMED_IAM

The update is executed asynchronously, so you can watch its status using:

$ aws cloudformation describe-stacks --stack-name MFAGroupsStack