I'm having a terrible time getting Terraform to assume an IAM role with another account with MFA required. Here's my setup
AWS Config
[default]
region = us-west-2
output = json
[profile GEHC-000]
region = us-west-2
output = json
....
[profile GEHC-056]
source_profile = GEHC-000
role_arn = arn:aws:iam::~069:role/hc/hc-master
mfa_serial = arn:aws:iam::~183:mfa/username
external_id = ~069
AWS Credentials
[default]
aws_access_key_id = xxx
aws_secret_access_key = xxx
[GEHC-000]
aws_access_key_id = same as above
aws_secret_access_key = same as above
Policies assigned to IAM user
STS Policy
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AssumeRole",
"Effect": "Allow",
"Action": [
"sts:AssumeRole"
],
"Resource": [
"arn:aws:iam::*:role/hc/hc-master"
]
}
]
}
User Policy
{
"Statement": [
{
"Action": [
"iam:*AccessKey*",
"iam:*MFA*",
"iam:*SigningCertificate*",
"iam:UpdateLoginProfile*",
"iam:RemoveUserFromGroup*"
],
"Effect": "Allow",
"Resource": [
"arn:aws:iam::~183:mfa/${aws:username}",
"arn:aws:iam::~183:mfa/*/${aws:username}",
"arn:aws:iam::~183:mfa/*/*/${aws:username}",
"arn:aws:iam::~183:mfa/*/*/*${aws:username}",
"arn:aws:iam::~183:user/${aws:username}",
"arn:aws:iam::~183:user/*/${aws:username}",
"arn:aws:iam::~183:user/*/*/${aws:username}",
"arn:aws:iam::~183:user/*/*/*${aws:username}"
],
"Sid": "Write"
},
{
"Action": [
"iam:*Get*",
"iam:*List*"
],
"Effect": "Allow",
"Resource": [
"*"
],
"Sid": "Read"
},
{
"Action": [
"iam:CreateUser*",
"iam:UpdateUser*",
"iam:AddUserToGroup"
],
"Effect": "Allow",
"Resource": [
"*"
],
"Sid": "CreateUser"
}
],
"Version": "2012-10-17"
}
Force MFA Policy
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "BlockAnyAccessOtherThanAboveUnlessSignedInWithMFA",
"Effect": "Deny",
"NotAction": "iam:*",
"Resource": "*",
"Condition": {
"BoolIfExists": {
"aws:MultiFactorAuthPresent": "false"
}
}
}
]
}
main.tf
provider "aws" {
profile = "GEHC-056"
shared_credentials_file = "${pathexpand("~/.aws/config")}"
region = "${var.region}"
}
data "aws_iam_policy_document" "test" {
statement {
sid = "TestAssumeRole"
effect = "Allow"
actions = [
"sts:AssumeRole",
]
principals = {
type = "AWS"
identifiers = [
"arn:aws:iam::~183:role/hc-devops",
]
}
sid = "BuUserTrustDocument"
effect = "Allow"
principals = {
type = "Federated"
identifiers = [
"arn:aws:iam::~875:saml-provider/ge-saml-for-aws",
]
}
condition {
test = "StringEquals"
variable = "SAML:aud"
values = ["https://signin.aws.amazon.com/saml"]
}
}
}
resource "aws_iam_role" "test_role" {
name = "test_role"
path = "/"
assume_role_policy = "${data.aws_iam_policy_document.test.json}"
}
Get Caller Identity
bash-4.4$ aws --profile GEHC-056 sts get-caller-identity
Enter MFA code for arn:aws:iam::772660252183:mfa/503072343:
{
"UserId": "AROAIWCCLC2BGRPQMJC7U:botocore-session-1537474244",
"Account": "730993910069",
"Arn": "arn:aws:sts::730993910069:assumed-role/hc-master/botocore-session-1537474244"
}
And the error:
bash-4.4$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
Error: Error refreshing state: 1 error(s) occurred:
* provider.aws: Error creating AWS session: AssumeRoleTokenProviderNotSetError: assume role with MFA enabled, but AssumeRoleTokenProvider session option not set.
role_arn. Specifies the ARN of the role to assume. web_identity_token_file. Specifies the path to a file which contains an OAuth 2.0 access token or OpenID Connect ID token that is provided by the identity provider.
Assuming a role involves using a set of temporary security credentials that you can use to access AWS resources that you might not normally have access to. These temporary credentials consist of an access key ID, a secret access key, and a security token.
Use an AWS credential profile The easiest way to do this is to hardcode the access key and secret key in plain text inside of the configuration file, but for security reasons, it's best to separate configuration information from the execution code. Use the AWS credentials file to handle credentials.
One other way is to use credential_process
in order to generate the credentials with a local script and cache the tokens in a new profile (let's call it tf_temp
)
This script would :
check if the token is still valid for the profile tf_temp
if token is valid, extract the token from existing config using aws configure get xxx --profile tf_temp
if token is not valid, prompt use to enter mfa token
generate the session token with aws assume-role --token-code xxxx ... --profile your_profile
set the temporary profile token tf_temp
using aws configure set xxx --profile tf_temp
You would have:
[prod]
aws_secret_access_key = redacted
aws_access_key_id = redacted
[tf_temp]
[tf]
credential_process = sh -c 'mfa.sh arn:aws:iam::{account_id}:role/{role} arn:aws:iam::{account_id}:mfa/{mfa_entry} prod 2> $(tty)'
gist
move this script in /bin/mfa.sh
or /usr/local/bin/mfa.sh
:
#!/bin/sh
set -e
role=$1
mfa_arn=$2
profile=$3
temp_profile=tf_temp
if [ -z $role ]; then echo "no role specified"; exit 1; fi
if [ -z $mfa_arn ]; then echo "no mfa arn specified"; exit 1; fi
if [ -z $profile ]; then echo "no profile specified"; exit 1; fi
resp=$(aws sts get-caller-identity --profile $temp_profile | jq '.UserId')
if [ ! -z $resp ]; then
echo '{
"Version": 1,
"AccessKeyId": "'"$(aws configure get aws_access_key_id --profile $temp_profile)"'",
"SecretAccessKey": "'"$(aws configure get aws_secret_access_key --profile $temp_profile)"'",
"SessionToken": "'"$(aws configure get aws_session_token --profile $temp_profile)"'",
"Expiration": "'"$(aws configure get expiration --profile $temp_profile)"'"
}'
exit 0
fi
read -p "Enter MFA token: " mfa_token
if [ -z $mfa_token ]; then echo "MFA token can't be empty"; exit 1; fi
data=$(aws sts assume-role --role-arn $role \
--profile $profile \
--role-session-name "$(tr -dc A-Za-z0-9 </dev/urandom | head -c 20)" \
--serial-number $mfa_arn \
--token-code $mfa_token | jq '.Credentials')
aws_access_key_id=$(echo $data | jq -r '.AccessKeyId')
aws_secret_access_key=$(echo $data | jq -r '.SecretAccessKey')
aws_session_token=$(echo $data | jq -r '.SessionToken')
expiration=$(echo $data | jq -r '.Expiration')
aws configure set aws_access_key_id $aws_access_key_id --profile $temp_profile
aws configure set aws_secret_access_key $aws_secret_access_key --profile $temp_profile
aws configure set aws_session_token $aws_session_token --profile $temp_profile
aws configure set expiration $expiration --profile $temp_profile
echo '{
"Version": 1,
"AccessKeyId": "'"$aws_access_key_id"'",
"SecretAccessKey": "'"$aws_secret_access_key"'",
"SessionToken": "'"$aws_session_token"'",
"Expiration": "'"$expiration"'"
}'
Use the tf
profile in provider settings. The first time, you will be prompted mfa token :
# terraform apply
Enter MFA token: 428313
This solution works fine with terraform and/or terragrunt
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With