Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

terraform backend s3 not working with mfa profile and assume role #17530

Open
RaviKumar1209 opened this issue Mar 8, 2018 · 8 comments
Open

Comments

@RaviKumar1209
Copy link

Hi,
We are trying to use backend s3 to store terraform state file. We have 3 different accounts(each used as prod, dev and staging) .
We have enabled MFA for IAM users. IAM user(s) can login using MFA in one of these accounts and then from their they use switch role to access resources in any of these accounts.
For eg:, we have a s3 bucket (for eg: s3-tfstate-bucket) in dev account, we are trying to store the tf state into the same bucket under different key(path) for each account. we can only use access key and secret key for dev as we do not have IAM users for other 2 accounts, we use switch role to access resources for other accounts.
We use assume role features for the resources that we would like to launch in these accounts. Also for s3, we thought to use assume role but it is not working, hence we thought to use access key and secret access key. We get below error when trying to initialize the backend. Could you please advise how should we fix this?

Initializing the backend...

Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.
Error loading state: AccessDenied: Access Denied
status code: 403, request id: 327D810FBEFCE503

Here is the terraform code that we are using:

terraform {
backend "s3" {
bucket = "s3-tfstate-bucket"
key = "dev/bastion/terraform.tfstate"
dynamodb_table = "dynamodbtable-east-lock"
region = "us-east-1"
encrypt = "true"
access_key = "xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
secret_key = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
}

provider "aws" {
region = "us-east-1"
shared_credentials_file = "~/.aws/credentials"
profile = "dev-mfa"
assume_role {
role_arn = "arn:aws:iam::xxxxxxxxxxxxxxxxx:role/abcd"
}
}

module "bastion" {
source = "../../.../../../modules/core/services/bastion"
vpc_id = "${var.vpc_id}"
asg_subnets = ["${var.asg_subnets}"]
}

Regards,
Ravi

Terraform Version

...

Terraform Configuration Files

...

Debug Output

Crash Output

Expected Behavior

Actual Behavior

Steps to Reproduce

Additional Context

References

@GeoffMillerAZ
Copy link

GeoffMillerAZ commented Nov 30, 2018

@RaviKumar1209 here is my solution:
mfa_script.sh --realm=test --code=123456

# I obviously left some boilerplate stuff out of this script post...
if [[ $code = "" ]]; then
    echo "code or c is required"
    exit_do_to_missing_required_vars=1
fi
if [[ $realm = "" ]]; then
    echo "realm or r is required"
    exit_do_to_missing_required_vars=1
fi

mfa_arn=$(aws configure get mfa_serial --profile $realm)

if [[ $mfa_arn = "" ]]; then
    echo "mfa_arn is required to be set your realm's .aws/credentials profile"
    exit_do_to_missing_required_vars=1
fi
if [[ $exit_do_to_missing_required_vars -eq 1 ]]; then
    usage
    exit
fi

return_body=$(aws --profile=$realm sts get-session-token --serial-number $mfa_arn --token-code $code)
aws_session_token=$(echo $return_body | jq -r '.Credentials | .SessionToken')
secret_access_key=$(echo $return_body | jq -r '.Credentials | .SecretAccessKey')
access_key_id=$(echo $return_body | jq -r '.Credentials | .AccessKeyId')

aws configure set profile.${realm}_mfa.source_profile $realm
aws configure set profile.${realm}_mfa.aws_access_key_id $access_key_id
aws configure set profile.${realm}_mfa.aws_secret_access_key $secret_access_key
aws configure set profile.${realm}_mfa.aws_session_token $aws_session_token

~/.aws/credentials:

[test]
aws_access_key_id=xxx
aws_secret_access_key=xxx
mfa_serial=xxxmyarnxxx

[test_mfa]
aws_access_key_id=xxxReturnedFromMFAScriptxxx
aws_secret_access_key=xxxReturnedFromMFAScriptxxx
aws_session_token=xxxReturnedFromMFAScriptxxx

[test_identity_tf]
source_profile = test_mfa
role_arn = arn:aws:iam::123456789:role/terraform

[test_shared_services_tf]
source_profile = test_mfa
role_arn = arn:aws:iam::987654321:role/terraform

terraform:

terraform {
  required_version = ">= 0.11.10"

  backend "s3" {
    bucket         = "xxxxxxxxxxxxxxxxxxx"
    key            = "terraform.tfstate"
    region         = "us-west-2"
    dynamodb_table = "xxxxxxxxxxxxxxxxxx"
    profile        = "test_identity_tf"
  }
}

This works. I run the mfa_script passing in the name of the "realm". Realm is what I call the group of accounts that work together to form related environments. So log into the identity account in the realm but then you assume roles to get to the other accounts.

Once the mfa script updates the credentials file with the credentials provided by the mfa operation, these can be used in a way that satisfies mfa and terraform doesn't need to know that they are even the result of mfa. They expire by default in 12 hours. So just run this script once every 12 hours or whatever.

I backed up my state file with a terraform state pull and then I did a terraform init --reconfigure and it worked fine. I did a terraform state list to confirm the new backend had everything and deleted the local backup.

putting the mfa_serial in the credentials file helps a lot if you have multiple environments with similar setups. I'm actually going to refactor this and just store the username and account number as separate vars in the credentials file and then put them together in a string in the script in case I have other automation needs that also need username and account number. Might as well, right?

Hope this helps!

@joariasl
Copy link

joariasl commented Jun 9, 2020

This is my custom script to issue a STS token using an AWS profile credential that set another AWS profile credential with the result.
https://gist.github.com/joariasl/d2a4a05ec05b68218ea3ed9d9eeb27bb

Exampe of usage
$ aws-sts.sh --profile-mfa example.mfa --profile-set example --duration-seconds 129600 --serial-number arn:aws:iam::000000000000:mfa/jorge.arias --token-code 000000

#!/bin/bash
script_name=`basename "$0"`

text_bold=$(tput bold)
text_normal=$(tput sgr0)

showHelp() {
  echo -e "${script_name}

${text_bold}DESCRIPTION${text_normal}
        The aws configure set command can be used to set a single configuration
        Script to issue a STS token using an AWS profile  credential  that  set
        another AWS profile credential with  the  result  configuration  values
        from the config file.

        See '${script_name} help' for descriptions of global parameters.

${text_bold}SYNOPSIS${text_normal}
        ${script_name}
        [--profile-mfa <value>]
        [--profile-set <value>]
        [--duration-seconds <value>]
        [--serial-number <value>]
        [--token-code <mfa-code>]

${text_bold}EXAMPLES${text_normal}
        Issue a STS token using example.mfa profile to set the example profile
            $ ${script_name} --profile-mfa example.mfa --profile-set example --duration-seconds 129600 --serial-number arn:aws:iam::000000000000:mfa/iam_user
            $ ${script_name} --profile-mfa example.mfa --profile-set example --duration-seconds 129600 --serial-number arn:aws:iam::000000000000:mfa/iam_user --token-code 000000

                                                        ${script_name}" | less
}

if (( ${#@} == 0 )); then
	showHelp
	exit 1
fi

while [ "$1" != "" ]; do
  case $1 in
    --profile-mfa )
                            shift
                            profile_mfa=$1
                            ;;
    --profile-set )
                            shift
                            profile_set=$1
                            ;;
    --duration-seconds )
                            shift
                            duration_seconds=$1
                            ;;

    --serial-number )
                            shift
                            serial_number=$1
                            ;;

    --token-code )
                            shift
                            token_code=$1
                            ;;
    help | --help | -h )
                            showHelp
                            exit 0
                            ;;
    * )
                            showHelp
                            exit 1
                            ;;
  esac
  shift
done

if [ -z "${profile_set}" ]; then
    profile_set="default"
fi

if [ -z "${token_code}" ]; then
    echo -n "Enter token code: "
    read -r token_code
    if [ -z "${token_code}" ]; then
        echo "--token-code is required"
        exit 1
    fi
fi

command="aws sts get-session-token --output text --query '*.[AccessKeyId,SecretAccessKey,SessionToken]'"
if [ "${profile_mfa}" ]; then
    command="${command} --profile ${profile_mfa}"
fi
if [ "${duration_seconds}" ]; then
    command="${command} --duration-seconds ${duration_seconds}"
fi
if [ "${serial_number}" ]; then
    command="${command} --serial-number ${serial_number}"
fi
if [ "${token_code}" ]; then
    command="${command} --token-code ${token_code}"
fi

result=$(eval ${command}) || exit 1;
access_key_id=$(printf '%s' "${result}" | awk '{print $1;}')
secret_access_key=$(printf '%s' "${result}" | awk '{print $2;}')
session_token=$(printf '%s' "${result}" | sed 's/[[:blank:]]$//g' | awk '{print $3;}')

aws configure set profile.${profile_set}.aws_access_key_id $access_key_id
aws configure set profile.${profile_set}.aws_secret_access_key $secret_access_key
aws configure set profile.${profile_set}.aws_session_token $session_token

exit 0

@andyfeller
Copy link

I don't know if what I'm about to add is exactly the issue you're seeing here, @RaviKumar1209, but I think there is correlation. If this is the case, then hopefully this is enough to truly fix the underlying issue rather than requiring working around terraform.
Just to note, all of the following was gathered by setting TF_LOG=trace TF_LOG_PATH=... as suggested in debugging terraform documentation.

At my place of business, we use an external MFA / SSO service to acquire a AWS STS token ultimately to assume an AWS role, which is ultimately given to terraform for its use. If the control machine has never been initialized, everything works fine. Otherwise, we get the same 403 errors you mention above. By capturing low-level logging debugging, it seems that terraform init fails to reuse merged backend configuration from multiple sources consistently and falls back to tfstate files that contain expired credentials.

MFA provided values:

  • access_key=AS...PS...(from MFA)
  • token=FwoGZXIvYXdzEPf...(from MFA)
  • secret_key=ex...Im...(from MFA)

Versus the ones from the tfstate file

  • access_key=AS...DS...(from tfstate file)
  • token=FwoGZXIvYXdzEOT...(from tfstate file)
  • secret_key=lh...Au...(from tfstate file)

Redacted log from terraform 0.12.28 showing aws / s3 backend failing to merged backend config:

2020/07/14 09:05:09 [INFO] Terraform version: 0.12.28  
2020/07/14 09:05:09 [INFO] Go runtime version: go1.12.13
...
2020/07/14 09:05:19 [INFO] CLI command args: []string{"init", "-input=false", "-backend-config", "access_key=AS...PS...(from MFA)", "-backend-config", "token=FwoGZXIvYXdzEPf...(from MFA)", "-backend-config", "secret_key=ex...Im...(from MFA)"}
2020/07/14 09:05:19 [TRACE] Meta.Backend: merging -backend-config=... CLI overrides into backend configuration
2020/07/14 09:05:19 [TRACE] Meta.Backend: built configuration for "s3" backend with hash value 704415183
2020/07/14 09:05:19 [TRACE] Preserving existing state lineage "b8a2dd6a-19d2-50b8-a684-4f4a1a87079a"
2020/07/14 09:05:19 [TRACE] Preserving existing state lineage "b8a2dd6a-19d2-50b8-a684-4f4a1a87079a"
2020/07/14 09:05:19 [TRACE] Meta.Backend: working directory was previously initialized for "s3" backend
2020/07/14 09:05:19 [TRACE] backendConfigNeedsMigration: configuration values have changed, so migration is required
2020/07/14 09:05:19 [TRACE] Meta.Backend: backend configuration has changed (from type "s3" to type "s3")
2020/07/14 09:05:19 [WARN] backend config has changed since last init
2020/07/14 09:05:19 [INFO] Setting AWS metadata API timeout to 100ms
2020/07/14 09:05:20 [INFO] Ignoring AWS metadata API endpoint at default location as it doesn't return any instance-id
2020/07/14 09:05:20 [INFO] AWS Auth provider used: "StaticProvider"
2020/07/14 09:05:20 [DEBUG] Trying to get account information via sts:GetCallerIdentity
2020/07/14 09:05:20 [DEBUG] [aws-sdk-go] DEBUG: Request sts/GetCallerIdentity Details:
---[ REQUEST POST-SIGN ]-----------------------------
POST / HTTP/1.1
Host: sts.amazonaws.com
User-Agent: aws-sdk-go/1.25.3 (go1.12.13; darwin; amd64) APN/1.0 HashiCorp/1.0 Terraform/0.12.17
Content-Length: 43
Authorization: AWS4-HMAC-SHA256 Credential=AS...PS...(from MFA)/20200714/us-east-1/sts/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-date;x-amz-security-token, Signature=5800ce51476dba8da135b25569d9b9b819a0e5573cbc381b88e50c6ce314c75e
Content-Type: application/x-www-form-urlencoded; charset=utf-8
X-Amz-Date: 20200714T130520Z
X-Amz-Security-Token: FwoGZXIvYXdzEPf...(from MFA)
Accept-Encoding: gzip

Action=GetCallerIdentity&Version=2011-06-15
-----------------------------------------------------
2020/07/14 09:05:20 [DEBUG] [aws-sdk-go] DEBUG: Response sts/GetCallerIdentity Details:
---[ RESPONSE ]--------------------------------------
HTTP/1.1 200 OK
Connection: close
Content-Length: 456
Content-Type: text/xml
Date: Tue, 14 Jul 2020 13:05:20 GMT
X-Amzn-Requestid: ...-...-...-...-...


-----------------------------------------------------
2020/07/14 09:05:20 [DEBUG] [aws-sdk-go] <GetCallerIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
  <GetCallerIdentityResult>
    <Arn>arn:aws:sts::...:assumed-role/.../...</Arn>
    <UserId>...:...</UserId>
    <Account>...</Account>
  </GetCallerIdentityResult>
  <ResponseMetadata>
    <RequestId>...-...-...-...-...</RequestId>
  </ResponseMetadata>
</GetCallerIdentityResponse>

2020/07/14 09:05:20 [INFO] Setting AWS metadata API timeout to 100ms
2020/07/14 09:05:21 [INFO] Ignoring AWS metadata API endpoint at default location as it doesn't return any instance-id
2020/07/14 09:05:21 [INFO] AWS Auth provider used: "StaticProvider"
2020/07/14 09:05:21 [DEBUG] Trying to get account information via sts:GetCallerIdentity
2020/07/14 09:05:21 [DEBUG] [aws-sdk-go] DEBUG: Request sts/GetCallerIdentity Details:
---[ REQUEST POST-SIGN ]-----------------------------
POST / HTTP/1.1
Host: sts.amazonaws.com
User-Agent: aws-sdk-go/1.25.3 (go1.12.13; darwin; amd64) APN/1.0 HashiCorp/1.0 Terraform/0.12.17
Content-Length: 43
Authorization: AWS4-HMAC-SHA256 Credential=AS...DS...(from tfstate file)/20200714/us-east-1/sts/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-date;x-amz-security-token, Signature=721d88c60276f604c5ed2b7226c1527eb0d159cd5fa3e73dba888cf4d17dadad
Content-Type: application/x-www-form-urlencoded; charset=utf-8
X-Amz-Date: 20200714T130521Z
X-Amz-Security-Token: FwoGZXIvYXdzEOT...(from tfstate file)
Accept-Encoding: gzip

Action=GetCallerIdentity&Version=2011-06-15
-----------------------------------------------------
2020/07/14 09:05:21 [DEBUG] [aws-sdk-go] DEBUG: Response sts/GetCallerIdentity Details:
---[ RESPONSE ]--------------------------------------
HTTP/1.1 403 Forbidden
Connection: close
Content-Length: 297
Content-Type: text/xml
Date: Tue, 14 Jul 2020 13:05:21 GMT
X-Amzn-Requestid: ...-...-...-...-...


-----------------------------------------------------
2020/07/14 09:05:21 [DEBUG] [aws-sdk-go] <ErrorResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
  <Error>
    <Type>Sender</Type>
    <Code>ExpiredToken</Code>
    <Message>The security token included in the request is expired</Message>
  </Error>
  <RequestId>...-...-...-....-...</RequestId>
</ErrorResponse>
2020/07/14 09:05:21 [DEBUG] [aws-sdk-go] DEBUG: Validate Response sts/GetCallerIdentity failed, attempt 0/5, error ExpiredToken: The security token included in the request is expired
	status code: 403, request id: ...-...-...-...-...
2020/07/14 09:05:21 [DEBUG] [aws-sdk-go] DEBUG: Retrying Request sts/GetCallerIdentity, attempt 1
...

Whenever I modified the execution to include -reconfigure, then terraform worked as expected. Still, there is a bug in the underlying s3 logic where it doesn't consistently resolve backend config correctly.

@mildwonkey
Copy link
Contributor

I am going to close this issue due to inactivity; there have been many changes to the s3 backend since this was originally opened.

If you are still experiencing a problem in v0.13 and there isn't already an issue open that describes it, please open a new issue and fill out the issue template in full.
Thanks!

@andyfeller
Copy link

@mildwonkey : sorry, are you saying this isn’t an issue in 0.13 or are we just closing this because because Hashicorp is ignoring this?

@mildwonkey
Copy link
Contributor

Hi @andyfeller ! We're definitely not ignoring it and I will happily re-open this (I went on a bit of an old/duplicate issue closing spree yesterday and figured I'd make some mistakes).

I closed this particular issue because:

But again, I can reopen this. My intent was not to ignore this and I am sorry that's what came across.

@mildwonkey mildwonkey reopened this Aug 28, 2020
@andyfeller
Copy link

Thanks @mildwonkey for the explanation!

@sathish86
Copy link

sathish86 commented Sep 11, 2022

Hi,
I had same issue and below is my usecase.
AWS account 1: Management account (IAM user created here and this user will assume role into Dev and Prod account)
AWS account 2: Dev environment account (Role is created here for the trusted account in this case Management account user)
AWS account 3: Prod environment account (Role is created here for the trusted account in this case Management account user)

So I created a dev-backend.conf and prod-backend.conf file with the below content. The main point that fixed this issue is passing the "role_arn" value in S3 backend configuration

Defining below content in dev-backend.conf and prod-backend.conf files

bucket = "<your bucket name>" key = "< your key path>" region = "<region>" dynamodb_table = "<db name>" encrypt = true profile = "< your profile>" # this profile has access key and secret key of the IAM user created in Management account role_arn = "arn:aws:iam::<dev/prod account id>:role/<dev/prod role name >"

Terraform initialise with dev s3 bucket config from local state to s3 state
$ terraform init -reconfigure -backend-config="dev-backend.conf"

Terraform apply using dev environment variables file
$ terraform apply --var-file="dev-app.tfvars"

Terraform initialise with prod s3 bucket config from local state to s3 state
$ terraform init -reconfigure -backend-config="prod-backend.conf"

Terraform apply using prod environment variables file
$ terraform apply --var-file="prod-app.tfvars"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants