View on GitHub

aws-assumerole-in-terraform

Solution for assumeRole with delegated MFA in terraform

Motivation

awscli stores its credentials by default in plaintext in ~/.aws/credentials. By using aws-vault we can store them encrypted in a file or in a keyring. The invocation now looks like this:

$ aws-vault --prompt=terminal exec calvin -- env
Enter passphrase to unlock /home/yoda/.awsvault/keys/: ______
..snipped..

Following best practices, we add an iam-policy to enforce MFA (Virtual MFA) for all IAM users. This forces aws-vault to prompt for the token.

$ aws-vault --prompt=terminal exec calvin -- env
Enter passphrase to unlock /home/yoda/.awsvault/keys/: ______
Enter token for arn:aws:iam::111111111111:mfa/calvin: 890980
..snipped..

All looks good. Now we want to run terraform via aws-vault. The aws provider has an assume_role section that looks promising.

provider "aws" {
  assume_role {
    role_arn     = "arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME"
  }
}

Unfortunately this does not work. And a few tickets have been open for this problem.

Scenario

To discuss my solution, I will use this scenario for illustration.

scenario

For the IAM role productionEC2Full, its trust policy - which demands the presence of MFA - looks like this. I will limit this discussion to Virtual MFA only.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": [
          "arn:aws:iam::111111111111:user/calvin"
        ]
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "Bool": {
          "aws:MultiFactorAuthPresent": "true"
        }
      }
    }
  ]
}

The IAM user in the master account calvin is assigned this policy to assume the target role.

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

Solution

It is assumed that a calvin profile has been created via aws-vault:

[profile calvin]
output = json
region = us-east-2
role_arn = arn:aws:iam::222222222222:role/productionEC2Full
role_session_name = calvin
mfa_serial = arn:aws:iam::111111111111:mfa/calvin

We will not use terraform’s assume_role section to indicate what role to assume. That job is left instead to aws-vault and it will pass the following environment variables to terraform

AWS_VAULT=calvin
AWS_DEFAULT_REGION=us-east-2
AWS_REGION=us-east-2
AWS_ACCESS_KEY_ID=ASIA************
AWS_SECRET_ACCESS_KEY=Vcga********
AWS_SESSION_TOKEN=FQo*************
AWS_SECURITY_TOKEN=FQo************

The AWS provider is then simplified to:

provider "aws" {
  region   = "${var.aws_region}"
}

Test that the assumeRole works with the aws CLI

$ aws-vault --prompt=terminal exec calvin -- aws sts get-caller-identity

The Cloudtrail event generated:

{
  "eventVersion": "1.05",
  "userIdentity": {
    "type": "AssumedRole",
    "principalId": "AROA************:calvin",
    "arn": "arn:aws:sts::222222222222:assumed-role/productionEC2Full/calvin",
    "accountId": "222222222222",
    "accessKeyId": "ASIA***********",
    "sessionContext": {
      "attributes": {
        "mfaAuthenticated": "true",
        "creationDate": "2018-11-15T00:45:40Z"
      },
      "sessionIssuer": {
        "type": "Role",
        "principalId": "AROA************",
        "arn": "arn:aws:iam::222222222222:role/productionEC2Full",
        "accountId": "222222222222",
        "userName": "productionEC2Full"
      }
    }
  },
  "eventTime": "2018-11-15T00:45:40Z",
  "eventSource": "sts.amazonaws.com",
  "eventName": "GetCallerIdentity",
  "awsRegion": "us-east-1",
  "sourceIPAddress": "1.1.1.1",
  "userAgent": "aws-cli/1.16.55 Python/2.7.13 Linux/4.14.33+ botocore/1.12.45",
  "requestParameters": null,
  "responseElements": {
    "userId": "AROA*************:calvin",
    "account": "222222222222",
    "arn": "arn:aws:sts::222222222222:assumed-role/productionEC2Full/calvin"
  },
  "requestID": "c6c1bf48-****-****-****-**********",
  "eventID": "f2e8baad-****-****-****-*********",
  "eventType": "AwsApiCall",
  "recipientAccountId": "222222222222"
}

Productivity tip

To ensure that you are indeed executing under a specific profile, create a Makefile to simplify your life.

VAULT := /home/yoda/gopath/bin
TERRAFORM := /home/yoda/bin
PROFILE ?= calvin
TF := TF_LOG=DEBUG $(VAULT)/aws-vault --prompt=terminal exec $(PROFILE) -- $(TERRAFORM)/terraform

.PHONY: init
init:
        $(TF) init 

.PHONY: plan
plan:
        $(TF) plan 

.PHONY: apply
apply:
        $(TF) apply

.PHONY: destroy
destroy:
        $(TF) destroy

Et voilĂ !

Reference