Skip to content
AWS aws iam 5 min read

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:

ValueEnvironment variableWhat it is
Access key IDAWS_ACCESS_KEY_IDPublic identifier for the temporary credentials. Starts with ASIA (long-lived user keys start with AKIA).
Secret access keyAWS_SECRET_ACCESS_KEYThe secret half, used to sign requests.
Session tokenAWS_SESSION_TOKENAn 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_ID and AWS_SECRET_ACCESS_KEY but forgetting AWS_SESSION_TOKEN. You then get a confusing InvalidClientTokenId or The security token included in the request is invalid error, 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/config with role_arn and source_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:

  1. Sign in to the AWS Management Console as your normal user.
  2. Click your account name (top-right) and choose Switch role.
  3. Enter the target Account ID (for example 111122223333) and the Role name (for example DeploymentRole).
  4. Optionally set a display name and color, then click Switch Role.
  5. 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, or GetSessionToken calls. 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 DurationSeconds as 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-role calls 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 Condition on aws:MultiFactorAuthPresent. See policy conditions.
  • Never paste AWS_SESSION_TOKEN into shared scripts or logs; treat the whole bundle as secret and short-lived.
Last updated June 15, 2026
Was this helpful?