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.
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Ă !