26

I need to execute a Terraform template to provision infrastructure for an AWS account which I can access by assuming a role.

The problem I have now is I do not have an IAM user in that AWS account so I do not have an aws_access_key_id or an aws_secret_access_key to set up another named profile in my ~/.aws/credentials. When I run command terraform apply, the template creates the infrastructure for my account, not the other account.

How to run Terraform template using your account which has a role to access services of another AWS account?

Here's my Terraform file:

# Input variables
variable "aws_region" {
    type = "string"
    default = "us-east-1"
}

variable "pipeline_name" {
    type = "string"
    default = "static-website-terraform"
}

variable "github_username" {
    type = "string"
    default = "COMPANY"
}

variable "github_token" {
    type = "string"
}

variable "github_repo" {
    type = "string"
}

provider "aws" {
    region = "${var.aws_region}"
    assume_role {
        role_arn = "arn:aws:iam::<AWS-ACCOUNT-ID>:role/admin"
        profile = "default"
    }
}

# CodePipeline resources
resource "aws_s3_bucket" "build_artifact_bucket" {
    bucket = "${var.pipeline_name}-artifact-bucket"
    acl = "private"
}

data "aws_iam_policy_document" "codepipeline_assume_policy" {
    statement {
        effect = "Allow"
        actions = ["sts:AssumeRole"]

        principals {
            type = "Service"
            identifiers = ["codepipeline.amazonaws.com"]
        }
    }
}

resource "aws_iam_role" "codepipeline_role" {
    name = "${var.pipeline_name}-codepipeline-role"
    assume_role_policy = "${data.aws_iam_policy_document.codepipeline_assume_policy.json}"
}

# CodePipeline policy needed to use CodeCommit and CodeBuild
resource "aws_iam_role_policy" "attach_codepipeline_policy" {
    name = "${var.pipeline_name}-codepipeline-policy"
    role = "${aws_iam_role.codepipeline_role.id}"

    policy = <<EOF
{
    "Statement": [
        {
            "Action": [
                "s3:GetObject",
                "s3:GetObjectVersion",
                "s3:GetBucketVersioning",
                "s3:PutObject"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "cloudwatch:*",
                "sns:*",
                "sqs:*",
                "iam:PassRole"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "codebuild:BatchGetBuilds",
                "codebuild:StartBuild"
            ],
            "Resource": "*",
            "Effect": "Allow"
        }
    ],
    "Version": "2012-10-17"
}
EOF
}

# CodeBuild IAM Permissions
resource "aws_iam_role" "codebuild_assume_role" {
    name = "${var.pipeline_name}-codebuild-role"

    assume_role_policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "codebuild.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
EOF
}

resource "aws_iam_role_policy" "codebuild_policy" {
    name = "${var.pipeline_name}-codebuild-policy"
    role = "${aws_iam_role.codebuild_assume_role.id}"

    policy = <<POLICY
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:GetObjectVersion",
                "s3:GetBucketVersioning"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Effect": "Allow",
            "Resource": [
                "${aws_codebuild_project.build_project.id}"
            ],
            "Action": [
                "codebuild:*"
            ]
        },
        {
            "Effect": "Allow",
            "Resource": [
                "*"
            ],
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ]
        }
    ]
}
POLICY
}

# CodeBuild Section for the Package stage
resource "aws_codebuild_project" "build_project" {
    name = "${var.pipeline_name}-build"
    description = "The CodeBuild project for ${var.pipeline_name}"
    service_role = "${aws_iam_role.codebuild_assume_role.arn}"
    build_timeout = "60"

    artifacts {
        type = "CODEPIPELINE"
    }

    environment {
        compute_type = "BUILD_GENERAL1_SMALL"
        image = "aws/codebuild/nodejs:6.3.1"
        type = "LINUX_CONTAINER"
    }

    source {
        type = "CODEPIPELINE"
        buildspec = "buildspec.yml"
    }
}

# Full CodePipeline
resource "aws_codepipeline" "codepipeline" {
    name = "${var.pipeline_name}-codepipeline"
    role_arn = "${aws_iam_role.codepipeline_role.arn}"

    artifact_store = {
        location = "${aws_s3_bucket.build_artifact_bucket.bucket}"
        type     = "S3"
    }

    stage {
        name = "Source"

        action {
            name = "Source"
            category = "Source"
            owner = "ThirdParty"
            provider = "GitHub"
            version = "1"
            output_artifacts = ["SourceArtifact"]

            configuration {
                Owner = "${var.github_username}"
                OAuthToken = "${var.github_token}"
                Repo = "${var.github_repo}"
                Branch = "master"
                PollForSourceChanges = "true"
            }
        }
    }

    stage {
        name = "Deploy"

        action {
            name = "DeployToS3"
            category = "Test"
            owner = "AWS"
            provider = "CodeBuild"
            input_artifacts = ["SourceArtifact"]
            output_artifacts = ["OutputArtifact"]
            version = "1"

            configuration {
                ProjectName = "${aws_codebuild_project.build_project.name}"
            }
        }
    }
}

Update:

Following Darren's answer (it makes a lot of sense) below, I added:

provider "aws" {
  region                  = "us-east-1"
  shared_credentials_file = "${pathexpand("~/.aws/credentials")}"
  profile                 = "default"

  assume_role {
    role_arn = "arn:aws:iam::<OTHER-ACCOUNT>:role/<ROLE-NAME>"
  }
}

However, I ran into this error:

  • provider.aws: The role "arn:aws:iam:::role/" cannot be assumed.

    There are a number of possible causes of this - the most common are:

    • The credentials used in order to assume the role are invalid
    • The credentials do not have appropriate permission to assume the role
    • The role ARN is not valid

I've checked the role in the other account and I can switch to that role using the AWS Console from my account. I've also checked AWS guide here

So: that role ARN is valid, I do have credentials to assume the role and all the permissions I need to run the stack.

Update

I've also tried with a new role that has all access to services. However, I ran into this error:

Error: Error refreshing state: 2 error(s) occurred:

    * aws_codebuild_project.build_project: 1 error(s) occurred:

    * aws_codebuild_project.build_project: aws_codebuild_project.build_project: Error retreiving Projects:

"InvalidInputException: Invalid project ARN: account ID does not match caller's account\n\tstatus code: 400, request id: ..." * aws_s3_bucket.build_artifact_bucket: 1 error(s) occurred:

    * aws_s3_bucket.build_artifact_bucket: aws_s3_bucket.build_artifact_bucket: error getting S3 Bucket CORS

configuration: AccessDenied: Access Denied status code: 403, request id: ..., host id: ...

=====

UPDATE 29 Apr 2019:

Following @Rolando's suggestion, I've added this policy to the user of the MAIN ACCOUNT that I'm trying to use to assume the role of the OTHER ACCOUNT where I'm planning to execute terraform apply.

{
    "Version": "2012-10-17",
    "Statement": {
        "Effect": "Allow",
        "Action": "sts:AssumeRole",
        "Resource": "arn:aws:iam::<OTHER-ACCOUNT-ID>:role/admin"
    }
}

This is the Trust Relationship of the role admin belongs to the OTHER ACCOUNT:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::<MAIN_ACCOUNT_ID>:root"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "Bool": {
          "aws:MultiFactorAuthPresent": "true"
        }
      }
    }
  ]
}

However, when I ran this command:

aws sts assume-role --role-arn arn:aws:iam::<OTHER-ACCOUNT-ID>:role/admin --role-session-name "RoleSession1" --profile default > assume-role-output.txt

I have this error:

An error occurred (AccessDenied) when calling the AssumeRole operation: Access denied
Viet
  • 6,513
  • 12
  • 42
  • 74

4 Answers4

29

I have a bulletproof solution anytime you want to run commands as a specific role (including other accounts). I assume you have the AWS CLI tools installed. You will also have to install jq (easy tool to parse and extract data from json), although you can parse the data any way you wish.

aws_credentials=$(aws sts assume-role --role-arn arn:aws:iam::1234567890:role/nameOfMyrole --role-session-name "RoleSession1" --output json)

export AWS_ACCESS_KEY_ID=$(echo $aws_credentials|jq '.Credentials.AccessKeyId'|tr -d '"')
export AWS_SECRET_ACCESS_KEY=$(echo $aws_credentials|jq '.Credentials.SecretAccessKey'|tr -d '"')
export AWS_SESSION_TOKEN=$(echo $aws_credentials|jq '.Credentials.SessionToken'|tr -d '"')

First line assigns the response from the aws sts command and puts it in a variable. Last 3 lines will select the values from the first command and assigned them to variables that the aws cli uses.

Considerations:

If you create a bash script, add your terraform commands there as well. You can also just create a bash with the lines above, and run it with a '.' in front (ie: . ./get-creds.sh). This will create the variables on your current bash shell.

Role expires, keep in mind that roles have expiration of usually an hour.

Your shell will now have the three variables AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN. This means that it will override your ~/.aws/credentials. Easiest thing to do to clear this is to just start a new bash session.

I used this article as my source to figure this out: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html

Michael
  • 146
  • 1
  • 9
Rolando Cintron
  • 553
  • 5
  • 8
  • I followed the link you posted and granted access to the role (on the other account) following: https://docs.aws.amazon.com/IAM/latest/UserGuide/tutorial_cross-account-with-roles.html. However, when I ran the command: `aws sts assume-role --role-arn arn:aws:iam::123456789012:role/role-name --role-session-name "RoleSession1" --profile IAM-user-name > assume-role-output.txt` I had this error: `An error occurred (AccessDenied) when calling the AssumeRole operation: Access denied` – Viet Apr 29 '19 at 17:19
  • 1
    This means the user assuming the role doesn't have access to assume the role. Check the Trust Relationship of the role the user is attempting to assume, should look something like this: `{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::1234567890:root" }, "Action": "sts:AssumeRole" } ] }` Where 1234567890 is the account of where the user belongs to... – Rolando Cintron Apr 29 '19 at 18:06
  • Also check that the user can assume roles. If you are administrator, you should be able to, but if not, then you would have to add a policy that allows you to assume roles. – Rolando Cintron Apr 29 '19 at 18:10
  • I have. I've just updated the question above. I've added policy so my user in the MAIN ACCOUNT can assume the role of the OTHER ACCOUNT. Also, I confirm that the OTHER ACCOUNT has Trusted entities of the MAIN ACCOUNT. – Viet Apr 29 '19 at 18:13
  • 2
    FYI, you can use `jq -r` to eliminate the pipe to `tr`, e.g. `export AWS_ACCESS_KEY_ID=$(echo $aws_credentials | jq -r '.Credentials.AccessKeyId')` – mm689 May 19 '21 at 12:41
6

You should be able to do it like this: In Terraform configure the aws provider to use your local shared_credentials_file

provider "aws" {
  region                  = "us-east-1"
  shared_credentials_file = "${pathexpand("~/.aws/credentials")}"
  profile                 = "default"

  assume_role {
    role_arn = "arn:aws:iam::1234567890:role/OrganizationAccountAccessRole"
  }
}

"profile" is a named profile in ~/.aws/credentials that has AWS Access keys. E.g.

[default]
region = us-east-1
aws_access_key_id = AKIAJXXXXXXXXXXXX
aws_secret_access_key = Aadxxxxxxxxxxxxxxxxxxxxxxxxxxxx    

This is not an IAM user in the account you want to access. It's in the "source" account (you need keys at some point to access the AWS cli).

"assume_role.role_arn" is the role in the account you want to assume. The IAM user in "profile" needs to be allowed to assume that role.

  • Thanks, Darren. I'll try it and let you know. – Viet Mar 19 '19 at 01:08
  • I've run into another error. I've updated my question above. – Viet Mar 19 '19 at 14:19
  • Hmm ok. Are you sue that the profile "default" in your ~/.aws/credentials has the AWS keys of the IAM user that can assume the role in ? Also, I'm not sure if "region" should matter at the assume-role step since IAM is a global service but no harm in making sure the correct one is set anyway. – Darren O'Brien Mar 20 '19 at 16:54
  • I'm sure the "default" profile has the keys to assume role in the other account as I can use my console to switch to that role. Both accounts' services are in the same region. I've also checked that. – Viet Mar 20 '19 at 18:00
  • I've also had a new role created and tried it. I ran into another error. I'll update my question. – Viet Mar 20 '19 at 18:01
  • The assume_role statement of the aws provider is recommended from the Terraform documentation. – Brandon Bradley Mar 18 '21 at 06:45
2

Looking at your policy of trust relationship in the other account, there's a condition applied multi factor authentication below highlighted. So User should 2 factor authenticated before assuming the role. Remove this condition and try to run code.

   "Condition": {
        "Bool": {
          "aws:MultiFactorAuthPresent": "true"
        }
      }
Naveen
  • 99
  • 5
  • Thank you. You're right and I've upvoted your answer. Now I'm looking back at my question, actually the answer by Rolando was correct as he mentioned "bullet proof". Your answer won't work if I need to enforce MFA. – Viet Aug 23 '19 at 22:58
0

Generally speaking you'll need to bootstrap the target account. Minimally this means creating a role that is assumable from the pipeline role, but could include some other resources.

Aaron
  • 1,575
  • 10
  • 18
  • Thanks, Aaron. Do you know if there's any documentation/answer/guide/example somewhere that goes into details for a case like this? For example: what the role policy should look like. – Viet Mar 13 '19 at 14:49
  • Thank you, @Aaron. – Viet Mar 14 '19 at 13:35
  • 1
    I'm not aware of an example. The role would need to reflect everything you want the pipeline to be able to create/update/delete, so this probably varies quite a bit from customer to customer - especially if you try to apply the principal of least privilege. – Aaron Mar 15 '19 at 16:10