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.
How Role Assumption Works
When you assume a role, AWS STS returns temporary credentials that expire after a configurable duration. The role needs:
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
Using AWS CLI profiles (recommended)
# ~/.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
StringLike: * in subject allows any repository to assume your role.Quick Reference
| Scenario | Trust Principal |
|---|---|
| Cross-account user | <code class="inline-code">"AWS": "arn:aws:iam::ACCOUNT:root"</code> |
| Specific role | <code class="inline-code">"AWS": "arn:aws:iam::ACCOUNT:role/Name"</code> |
| EC2 instance | <code class="inline-code">"Service": "ec2.amazonaws.com"</code> |
| Lambda | <code class="inline-code">"Service": "lambda.amazonaws.com"</code> |
| ECS task | <code class="inline-code">"Service": "ecs-tasks.amazonaws.com"</code> |
| GitHub Actions | <code class="inline-code">"Federated": "arn:...oidc.../token.actions.githubusercontent.com"</code> |
| GitLab CI | <code class="inline-code">"Federated": "arn:...oidc.../gitlab.com"</code> |
| EKS IRSA | <code class="inline-code">"Federated": "arn:...oidc.../oidc.eks.REGION..."</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.
---
Related Resources
- Production Reference Architectures — Multi-region and serverless AWS architectures
- Certification Exam Prep — AWS certification exam prep guides
- AWS IAM Least Privilege Guide — Principle of least privilege for IAM policies
- AWS S3 Bucket Policy Examples — Cross-account S3 access with assumed roles
- Terraform Import Existing Resources — Managing IAM roles with Terraform