journal of recreational computer science


Terraform, MFA and assuming roles

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 = functional security contexts

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, you'll be drowning in access keys, and trying to remember which accounts are still used, what they are used for, and finally who created them and for what quickly gets out of control.

The right way of modelling this is to assign one 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 physical means to execute an operation.

The next step is to figure out how we want to compartmentalise access to our resources. 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 priviledge and separation of responsiblities.

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, one, which for example - provides a service to us which operates on resources in our AWS account.


A good analogy can be made with how senior officials in government operate - a user corresponds to an individual, a politician, while a role will correspond to their post. In some cases, two posts can be filled with the same person (for example they can be the acting prime minister, as well as the minister for finance, until a suitable replacement is found). Now, each post (role) comes with predefined authority - which is what a person may do when in a given role. This is where the direct analogy ends. In real life, a person may abuse their power beyond the authority of their role, and suffer consequences - or not.

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

There exists a mechanism permitting a user, to assume a particular role to perform a function. Your user may have no permissions other than to assume a pre-defined set of roles, and only being in that role (wearing a hat) lets them access these resources.

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]
aws_access_key_id = AKIA1E3GDYEOGFJPIT7
aws_secret_access_key = 6b1479c92c82ff1047b9995194255942c0e84c4585e35708e67e9aaf
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 use that particular set of credentials and the region will be set 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 profile 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

Once this is done, you can remove the personal profile from your .aws/config file. 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 other 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
comments powered by Disqus