DEV Community

Yash
Yash

Posted on

IAM role assumption across AWS accounts: the right way (with working Terraform)

IAM role assumption across AWS accounts: the right way

Most teams still store long-lived AWS access keys in CI/CD secrets. Here's the right pattern.

Why role assumption beats stored credentials

Approach Risk Rotation Auditability
Access key in CI secret High (never expires) Manual Poor
OIDC + role assumption Low (per-job token) Automatic Full CloudTrail

Architecture

GitHub Actions → OIDC JWT → IAM (TOOLING account)
                            → sts:AssumeRole → ci-deploy-role (PROD account)
                                               → Deploy
Enter fullscreen mode Exit fullscreen mode

OIDC provider + trust policy

resource "aws_iam_openid_connect_provider" "github" {
  url             = "https://token.actions.githubusercontent.com"
  client_id_list  = ["sts.amazonaws.com"]
  thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}

resource "aws_iam_role" "github_actions" {
  name = "github-actions-oidc"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Federated = aws_iam_openid_connect_provider.github.arn }
      Action    = "sts:AssumeRoleWithWebIdentity"
      Condition = {
        StringLike   = { "token.actions.githubusercontent.com:sub" = "repo:myorg/*:ref:refs/heads/main" }
        StringEquals = { "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com" }
      }
    }]
  })
}
Enter fullscreen mode Exit fullscreen mode

Cross-account role (target account)

resource "aws_iam_role" "ci_deploy" {
  name = "ci-deploy-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { AWS = "arn:aws:iam::111111111111:role/github-actions-oidc" }
      Action    = "sts:AssumeRole"
      Condition = { StringEquals = { "sts:ExternalId" = "myorg-prod-deploy" } }
    }]
  })
}
Enter fullscreen mode Exit fullscreen mode

GitHub Actions workflow

jobs:
  deploy:
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::111111111111:role/github-actions-oidc
          aws-region: us-east-1
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::222222222222:role/ci-deploy-role
          role-chaining: true
          aws-region: us-east-1
Enter fullscreen mode Exit fullscreen mode

Common mistakes

  1. Forgetting role-chaining: true when chaining two AssumeRole calls
  2. Wildcard sub condition — scope per repo for prod
  3. No ExternalId — prevents confused deputy attacks

Step2Dev automates this pattern for every project.

👉 step2dev.com

Top comments (0)