journal of recreational computer science


AWS, MFA, assuming roles with Terraform

14 May 2020

Reading AWS’ security good practices, you’ll come across a pattern where your organisation’s AWS assets would be partitioned across a number of subaccounts - for example - one subaccount for every environment such as development, testing, production, etc. This is a good idea as is keeps different resources as separate as they can get (logically) - you simply get another account, and then only the billing function is kept shared as you’d want to deal with one invoice rather than 10.

Roles and role-based access control

When I first started out with AWS, I’d create a new user every time I wanted to protect access to a different subset of my resources. Very quickly, I’d be drowning in access keys, and trying to remember which accounts are still used, and what they are used for.

How would we untangle that mess? We ought to first identify what needs to be accessed, to do what, and then by whom. For example, if we’re dealing with someone who manages our web presence, we might only give them a minimal set of permissions to access a particular S3 bucket, and nothing else. Similarly, if we have another function which requires access to logs, we can create a separate role for that and assign relevant policies. This fits in nicely with the principle of least privilege and separation of responsibilities.

The correct way of modelling this is to assign one AWS user account per person. A person can then use supplied credentials to issue calls through APIs – this is how we get from a person to a user. This, coupled with what is described below allows us to reduce credential sharing. With the added power of MFA, we are reducing the potential of unauthorised access - a real, breathing person must reach for their OTP token, or other additional authentication factor to execute an operation.

A role’s trust relationship defines who can assume the role. So this is where you’d define a set of users who can assume a particular role. This can refer to both user accounts within our own AWS account, a sub-organisation, or another organisation for example one which provides a service to our organisation and operates on resources in our AWS account.


Assuming roles - putting on one of many hats, for a few hours

Once trust relationships are in place, and we’ve defined who can take one a role, there is a mechanism which allows a particular user context to temporarily assume a role.

This is accomplished by the sts:AssumeRole API call which will generate a set of temporary security credentials (id/secret) and a security token valid for a short time (by default 1 hour).

One can assume roles quite easily by using the profiles capability of the AWS SDK, and some additional tools like aws-vault.


Within your .aws/config file, you can define profiles, which are pieces of configuration to be used together. For example

[profile jan-work]
region = eu-west-1

When using the profile, either using --profile command line options, or by setting the AWS_PROFILE environment variable, the context in which operations will be executed, will be that profile, so the SDK will set the region to Ireland.

Assuming roles automatically

Let use define an additional profile.

[profile security-auditor]
source_profile = jan-work
role_arn = arn:aws:iam::1234567890:role/security-auditor

Provided that there’s a trust relationship established between jan-work and security-auditor, we can assume that role and get read only access to all AWS resources, for auditing purposes. As long as the trust relationship exists, the role can even live in a different account. This allows us to have many sub-organisations and to assume roles within those organisations.

I’ve used aws-vault to automate this process. The first step is to import the your credentials for the account into aws-vault - this boils down to executing the following and entering your AWS credentials.

$ aws-vault add jan-work
Enter Secret Access Key: 6b1479c92c82ff1047b9995194255942c0e84c4585e35708e67e9aaf
Added credentials to profile "jan-work" in vault

The credentials are now securely stored within your operating system’s secure credential storage mechanism.

You can then use aws-vault in conjunction with tools which use the AWS SDK, and pass temporary credentials to them using environment variables. For example:

$ aws-vault exec security-auditor -- aws s3 ls
2020-02-19 18:19:25 work-terraform-state
2020-02-20 16:48:15 work-logs
2019-01-11 10:53:37 work-archive

Slapping MFA on top

Suppose that you’d want some sort of assurance, that the person assuming this role is actually the person who is the user. You can utilise multi factor authentication to accomplish that. In short - every time the role is to be assumed, the user will be prompted to enter a single use token, which has a lifetime of 1 minute. This video will walk you through how to set up a virtual token provider in the form of a mobile application such as Authy or Google Authenticator.

Adding MFA to an existing role is a two-step process. First, add the virtual MFA device generated above to the profile which contains the role to be assumed. Edit the profile by adding a mfa_serial configuration directive to the config.

[profile security-auditor]
source_profile = jan-work
role_arn = arn:aws:iam::1234567890:role/security-auditor
mfa_serial = arn:aws:iam::1234567890:mfa/jan-work

The next step involves modifying the trust relationship on the role, to only permit users which have successfully authenticated themselves using both AWS credentials and an MFA token. Go to the role you wish to assume in your AWS console, click on Trust relationships | Edit trust relationship, and modify it so that it has an additional condition - aws:MultiFactorAuthPresent. See below for an example:

  "Version": "2012-10-17",
  "Statement": [
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::1234567890:user/jan-work"
      "Action": "sts:AssumeRole",
      "Condition": {
        "Bool": {
          "aws:MultiFactorAuthPresent": "true"

Next time you try to run aws-vault with that profile you should get the following:

$ aws-vault exec security-auditor -- aws s3 ls
Enter token for arn:aws:iam::1234567890:mfa/jan-work: 162287
2020-02-19 18:19:25 work-terraform-state
2020-02-20 16:48:15 work-logs
2019-01-11 10:53:37 work-archive

This works with everything!

… well, mostly. It will work with any executable which uses AWS environment variables for their configuration, so any tool based on the official AWS SDK, of which there are many.

Terraform is one such tool and it boils down to prepending a call to aws-vault like this:

$ aws-vault exec dev-infra-admin -- terraform apply

This will prompt you for an MFA token, if you’ve configured that and will run terraform plan just as you did before.

comments powered by Disqus