Skip to main content
AWS·8 min read

AWS IAM Role Assume — Cross-Account Access Without Sharing Credentials

Master AWS IAM role assumption for cross-account access, CI/CD pipelines with OIDC, and service-to-service authentication. Covers trust policies, session policies, and troubleshooting.

DT

DevOps Engineer & Technical Writer

The Problem

Your organization has multiple AWS accounts — production, staging, shared services, data lake. Engineers need access to resources across accounts without sharing long-lived credentials. Your CI/CD pipeline needs to deploy to AWS without storing access keys. IAM role assumption solves all of these with temporary, auditable credentials.

Account A User / Service AssumeRole API Call AWS STS Security Token Service Account B Access Resources IAM Role Assumption Flow Temp Credentials Trust Policy Required Request Flow Credentials Returned Token Service

How Role Assumption Works

When you assume a role, AWS STS returns temporary credentials that expire after a configurable duration. The role needs:

  • Trust policy — Who can assume this role
  • Permission policy — What the role can do once assumed
  • Cross-Account Role Assumption

    Create the role in the target account (111111111111)

    {
    

    "Version": "2012-10-17",

    "Statement": [

    {

    "Effect": "Allow",

    "Principal": {

    "AWS": "arn:aws:iam::222222222222:root"

    },

    "Action": "sts:AssumeRole",

    "Condition": {

    "StringEquals": {

    "sts:ExternalId": "UniqueExternalId123"

    }

    }

    }

    ]

    }

    Grant permission in the source account (222222222222)

    {
    

    "Version": "2012-10-17",

    "Statement": [

    {

    "Effect": "Allow",

    "Action": "sts:AssumeRole",

    "Resource": "arn:aws:iam::111111111111:role/CrossAccountDeployRole"

    }

    ]

    }

    Assume the role

    aws sts assume-role \
    

    --role-arn arn:aws:iam::111111111111:role/CrossAccountDeployRole \

    --role-session-name deploy-session \

    --external-id UniqueExternalId123 \

    --duration-seconds 3600

    # Export credentials

    eval $(aws sts assume-role \

    --role-arn arn:aws:iam::111111111111:role/CrossAccountDeployRole \

    --role-session-name my-session \

    --external-id UniqueExternalId123 \

    --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' \

    --output text | \

    awk '{print "export AWS_ACCESS_KEY_ID="$1"\nexport AWS_SECRET_ACCESS_KEY="$2"\nexport AWS_SESSION_TOKEN="$3}')

    aws sts get-caller-identity

    # ~/.aws/config
    

    [profile production]

    role_arn = arn:aws:iam::111111111111:role/CrossAccountDeployRole

    source_profile = default

    external_id = UniqueExternalId123

    region = us-east-1

    [profile staging]

    role_arn = arn:aws:iam::333333333333:role/CrossAccountDeployRole

    source_profile = default

    region = us-east-1

    aws s3 ls --profile production
    

    aws ecs list-services --profile staging

    OIDC Federation for CI/CD

    GitHub Actions OIDC

    Create OIDC provider in AWS

    aws iam create-open-id-connect-provider \
    

    --url https://token.actions.githubusercontent.com \

    --client-id-list sts.amazonaws.com \

    --thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1

    Create role with OIDC trust policy

    {
    

    "Version": "2012-10-17",

    "Statement": [

    {

    "Effect": "Allow",

    "Principal": {

    "Federated": "arn:aws:iam::111111111111:oidc-provider/token.actions.githubusercontent.com"

    },

    "Action": "sts:AssumeRoleWithWebIdentity",

    "Condition": {

    "StringEquals": {

    "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"

    },

    "StringLike": {

    "token.actions.githubusercontent.com:sub": "repo:myorg/myrepo:ref:refs/heads/main"

    }

    }

    }

    ]

    }

    Use in GitHub Actions

    name: Deploy
    

    on:

    push:

    branches: [main]

    permissions:

    id-token: write

    contents: read

    jobs:

    deploy:

    runs-on: ubuntu-latest

    steps:

    - uses: actions/checkout@v4

    - uses: aws-actions/configure-aws-credentials@v4

    with:

    role-to-assume: arn:aws:iam::111111111111:role/GitHubActionsDeployRole

    aws-region: us-east-1

    - run: aws sts get-caller-identity

    - run: aws s3 sync ./dist s3://my-bucket/

    GitLab CI OIDC trust policy

    {
    

    "Version": "2012-10-17",

    "Statement": [

    {

    "Effect": "Allow",

    "Principal": {

    "Federated": "arn:aws:iam::111111111111:oidc-provider/gitlab.com"

    },

    "Action": "sts:AssumeRoleWithWebIdentity",

    "Condition": {

    "StringEquals": {

    "gitlab.com:aud": "https://gitlab.com"

    },

    "StringLike": {

    "gitlab.com:sub": "project_path:mygroup/myproject:ref_type:branch:ref:main"

    }

    }

    }

    ]

    }

    Service-to-Service Role Assumption

    EC2 instance profiles

    {
    

    "Version": "2012-10-17",

    "Statement": [

    {

    "Effect": "Allow",

    "Principal": {

    "Service": "ec2.amazonaws.com"

    },

    "Action": "sts:AssumeRole"

    }

    ]

    }

    aws ec2 associate-iam-instance-profile \
    

    --instance-id i-0abc123 \

    --iam-instance-profile Name=my-app-profile

    EKS IRSA (IAM Roles for Service Accounts)

    apiVersion: v1
    

    kind: ServiceAccount

    metadata:

    name: my-app

    namespace: production

    annotations:

    eks.amazonaws.com/role-arn: arn:aws:iam::111111111111:role/MyAppPodRole

    Trust policy:

    {
    

    "Version": "2012-10-17",

    "Statement": [

    {

    "Effect": "Allow",

    "Principal": {

    "Federated": "arn:aws:iam::111111111111:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE"

    },

    "Action": "sts:AssumeRoleWithWebIdentity",

    "Condition": {

    "StringEquals": {

    "oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE:sub": "system:serviceaccount:production:my-app",

    "oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE:aud": "sts.amazonaws.com"

    }

    }

    }

    ]

    }

    Lambda execution roles

    {
    

    "Version": "2012-10-17",

    "Statement": [

    {

    "Effect": "Allow",

    "Principal": {

    "Service": "lambda.amazonaws.com"

    },

    "Action": "sts:AssumeRole"

    }

    ]

    }

    Role Chaining

    # Assume role in shared account
    

    aws sts assume-role --role-arn arn:aws:iam::222222222222:role/SharedRole \

    --role-session-name chain-step-1

    # From shared role, assume role in production

    aws sts assume-role --role-arn arn:aws:iam::111111111111:role/ProdRole \

    --role-session-name chain-step-2

    Limitation: Role chaining has a maximum session duration of 1 hour regardless of role configuration.

    Troubleshooting

    aws sts get-caller-identity
    

    aws sts decode-authorization-message --encoded-message <message>

    Common errors

    • "not authorized to perform sts:AssumeRole" — Check both the trust policy on the target AND the IAM policy on the source.
    • "Invalid identity token" — Verify OIDC provider thumbprint and audience match.
    • "Access Denied" after assuming — The role's permission policy does not allow the action you are attempting.

    Common Mistakes

  • Missing trust policy on target role — Both the source IAM policy AND target trust policy must be configured.
  • Forgetting ExternalId for third-party access — Without ExternalId, any entity knowing your role ARN can attempt assumption.
  • Overly broad OIDC conditions — Using StringLike: * in subject allows any repository to assume your role.
  • Not handling session expiration — Temporary credentials expire. Use SDK credential provider chains that auto-refresh.
  • Storing assumed credentials in environment variables — SDK providers handle refresh automatically. Manual export breaks on expiry.
  • Role chaining without understanding the 1-hour limit — Chained sessions cannot exceed 1 hour.
  • Quick Reference

    ScenarioTrust Principal
    Cross-account user<code class="inline-code">&quot;AWS&quot;: &quot;arn:aws:iam::ACCOUNT:root&quot;</code>
    Specific role<code class="inline-code">&quot;AWS&quot;: &quot;arn:aws:iam::ACCOUNT:role/Name&quot;</code>
    EC2 instance<code class="inline-code">&quot;Service&quot;: &quot;ec2.amazonaws.com&quot;</code>
    Lambda<code class="inline-code">&quot;Service&quot;: &quot;lambda.amazonaws.com&quot;</code>
    ECS task<code class="inline-code">&quot;Service&quot;: &quot;ecs-tasks.amazonaws.com&quot;</code>
    GitHub Actions<code class="inline-code">&quot;Federated&quot;: &quot;arn:...oidc.../token.actions.githubusercontent.com&quot;</code>
    GitLab CI<code class="inline-code">&quot;Federated&quot;: &quot;arn:...oidc.../gitlab.com&quot;</code>
    EKS IRSA<code class="inline-code">&quot;Federated&quot;: &quot;arn:...oidc.../oidc.eks.REGION...&quot;</code>

    Summary

    Role assumption is the foundation of AWS security. Use cross-account roles instead of sharing credentials. Use OIDC federation for CI/CD instead of storing access keys. Use instance profiles and IRSA for service authentication. Every assumed session is time-limited and auditable in CloudTrail.

    ---

    Frequently Asked Questions

    What does it mean to assume an IAM role in AWS?

    Assuming a role means temporarily obtaining a set of security credentials (access key, secret key, session token) that grant the permissions defined in that role's policies. The original identity's permissions are replaced by the role's permissions for the session duration. This is used for cross-account access, service-to-service authentication, and temporary privilege escalation.

    How do I assume a role from the AWS CLI?

    Use aws sts assume-role --role-arn arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME --role-session-name my-session to get temporary credentials. Then export the returned AccessKeyId, SecretAccessKey, and SessionToken as environment variables. Alternatively, configure a profile in ~/.aws/config with role_arn and source_profile for automatic role assumption.

    What is a trust policy and how does it work?

    A trust policy is a JSON document attached to an IAM role that specifies which principals (AWS accounts, services, or users) are allowed to assume that role. Without a matching trust policy, even users with sts:AssumeRole permission cannot assume the role. Both the trust policy on the role and the IAM policy on the caller must allow the action.

    Why is my cross-account role assumption failing?

    Verify three things: the trust policy on the target role lists the source account or principal, the calling identity has sts:AssumeRole permission for the target role ARN, and there are no SCPs blocking the action. Also check for ExternalId conditions in the trust policy — if present, you must pass the correct external ID in the assume-role call.

    How long do assumed role credentials last?

    Default session duration is 1 hour, configurable up to 12 hours via --duration-seconds on the CLI or the role's MaxSessionDuration setting. For chained role assumptions (role assuming another role), the maximum is 1 hour regardless of settings. Design your automation to handle credential refresh before expiration.

    ---