Assuming Roles with STS
AWS STS (Security Token Service) is the part of AWS that hands out short-lived, temporary credentials. Instead of carrying around a permanent access key that never expires, you call STS, it checks that you are allowed in, and it gives you a set of keys that automatically stop working after a while. This is the engine behind almost every “elevated access” or “cross-account access” pattern in AWS. If you understand sts:AssumeRole, you understand how most secure access in AWS actually works.
What STS gives you
When you successfully assume an IAM role (Identity and Access Management role), STS returns a bundle of three values, not one:
| Value | Environment variable | What it is |
|---|---|---|
| Access key ID | AWS_ACCESS_KEY_ID | Public identifier for the temporary credentials. Starts with ASIA (long-lived user keys start with AKIA). |
| Secret access key | AWS_SECRET_ACCESS_KEY | The secret half, used to sign requests. |
| Session token | AWS_SESSION_TOKEN | An extra blob that proves these are temporary credentials. Required for every request. |
The big difference from a normal IAM user access key is the session token and an expiration time. The credentials are valid only for a limited window (1 hour by default), then they stop working and you must assume the role again.
Gotcha: The single most common STS mistake is setting
AWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEYbut forgettingAWS_SESSION_TOKEN. You then get a confusingInvalidClientTokenIdorThe security token included in the request is invaliderror, even though the keys are correct. Temporary credentials are useless without the session token.
When to use this (and when not to)
Use sts:AssumeRole when:
- You need cross-account access — a user or service in account A needs to act in account B.
- You want elevated permissions on demand (for example, a developer who normally has read-only access assumes an “admin” role only when needed).
- Your application runs somewhere that should not store permanent keys, and you want automatically rotating, expiring credentials.
Do not use raw assume-role calls when:
- The workload runs on AWS compute (EC2, Lambda, ECS). Use an instance profile or task/execution role instead — AWS assumes the role for you and rotates credentials automatically. See EC2 instance profiles.
- Humans are signing in interactively. Use IAM Identity Center, which calls STS for you behind the scenes.
The trust relationship
A role you want to assume must explicitly allow the caller in its trust policy (the AssumeRolePolicyDocument). This says who is allowed to assume the role:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::111122223333:root" },
"Action": "sts:AssumeRole"
}
]
}
This trust policy lets any principal in account 111122223333 (that also has matching IAM permissions) assume the role. Without a trust policy that names the caller, AssumeRole fails with AccessDenied no matter what permissions the caller has.
Assuming a role with the AWS CLI
The command is aws sts assume-role. You must pass the role ARN and a role session name (a free-form label that shows up in CloudTrail logs so you can tell who did what).
aws sts assume-role \
--role-arn arn:aws:iam::111122223333:role/DeploymentRole \
--role-session-name jaswinder-deploy
Output:
{
"Credentials": {
"AccessKeyId": "ASIAEXAMPLE0A1B2C3D4E",
"SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"SessionToken": "IQoJb3JpZ2luX2VjEJr...truncated...VeryLongToken==",
"Expiration": "2026-06-15T14:30:00+00:00"
},
"AssumedRoleUser": {
"AssumedRoleId": "AROAEXAMPLE:jaswinder-deploy",
"Arn": "arn:aws:sts::111122223333:assumed-role/DeploymentRole/jaswinder-deploy"
}
}
To actually use these credentials, export all three values:
export AWS_ACCESS_KEY_ID="ASIAEXAMPLE0A1B2C3D4E"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
export AWS_SESSION_TOKEN="IQoJb3JpZ2luX2VjEJr...truncated...VeryLongToken=="
aws sts get-caller-identity
Output:
{
"UserId": "AROAEXAMPLE:jaswinder-deploy",
"Account": "111122223333",
"Arn": "arn:aws:sts::111122223333:assumed-role/DeploymentRole/jaswinder-deploy"
}
Tip: You rarely need to copy keys by hand. Add the role to a named profile in
~/.aws/configwithrole_arnandsource_profile, then run any command with--profile deploy. The CLI calls STS, caches the credentials, and refreshes them for you.
The easier way: a named profile
# ~/.aws/config
[profile deploy]
role_arn = arn:aws:iam::111122223333:role/DeploymentRole
source_profile = default
region = us-east-1
Then simply: aws s3 ls --profile deploy.
Assuming a role from the Management Console
The console version is Switch Role, which assumes a role for you in the browser:
- Sign in to the AWS Management Console as your normal user.
- Click your account name (top-right) and choose Switch role.
- Enter the target Account ID (for example
111122223333) and the Role name (for exampleDeploymentRole). - Optionally set a display name and color, then click Switch Role.
- You are now operating as the role; switch back any time from the same menu.
Assuming a role from the SDK
The SDK never makes you handle the session token manually — you pass the returned credentials into a new client.
JavaScript (AWS SDK v3):
import { STSClient, AssumeRoleCommand } from "@aws-sdk/client-sts";
import { S3Client, ListBucketsCommand } from "@aws-sdk/client-s3";
const sts = new STSClient({ region: "us-east-1" });
const { Credentials } = await sts.send(
new AssumeRoleCommand({
RoleArn: "arn:aws:iam::111122223333:role/DeploymentRole",
RoleSessionName: "node-app-session",
DurationSeconds: 3600,
})
);
const s3 = new S3Client({
region: "us-east-1",
credentials: {
accessKeyId: Credentials.AccessKeyId,
secretAccessKey: Credentials.SecretAccessKey,
sessionToken: Credentials.SessionToken,
},
});
const result = await s3.send(new ListBucketsCommand({}));
console.log(result.Buckets.map((b) => b.Name));
Python (boto3):
import boto3
sts = boto3.client("sts")
resp = sts.assume_role(
RoleArn="arn:aws:iam::111122223333:role/DeploymentRole",
RoleSessionName="python-app-session",
DurationSeconds=3600,
)
creds = resp["Credentials"]
s3 = boto3.client(
"s3",
aws_access_key_id=creds["AccessKeyId"],
aws_secret_access_key=creds["SecretAccessKey"],
aws_session_token=creds["SessionToken"],
)
print([b["Name"] for b in s3.list_buckets()["Buckets"]])
Expiration and session duration
Temporary credentials live for DurationSeconds: minimum 900 seconds (15 minutes), default 3600 seconds (1 hour). The maximum depends on the role’s maximum session duration setting (up to 12 hours), which you set on the role itself. There is a catch: if you assume a role from another assumed role (role chaining), the duration is capped at 1 hour regardless of the role setting.
When credentials expire, calls fail with ExpiredToken. The fix is to assume the role again. SDKs and named profiles handle this refresh automatically; only manual export-style setups need to re-run assume-role.
Cost note: STS itself is free — there is no charge for
AssumeRole,GetCallerIdentity, orGetSessionTokencalls. You only pay for whatever AWS resources the resulting credentials touch.
Best Practices
- Always set a meaningful
RoleSessionName— it is your audit trail in CloudTrail for “who assumed this role and when”. - Keep
DurationSecondsas short as practical; do not raise the role’s max session duration just for convenience. - Prefer instance profiles, task roles, or IAM Identity Center over hand-managed
assume-rolecalls so you never store or print credentials. - Scope the trust policy tightly — name a specific role or external ID instead of an entire account where possible.
- Require MFA in the trust policy for sensitive roles using a
Conditiononaws:MultiFactorAuthPresent. See policy conditions. - Never paste
AWS_SESSION_TOKENinto shared scripts or logs; treat the whole bundle as secret and short-lived.