Assuming roles with STS

boto3 · Advanced · 12 min

Cross-account access, least privilege and CI deployments all rely on assuming IAM roles instead of sharing keys.

An IAM role is an identity without long-term keys. Principals allowed by the role's trust policy can call STS assume_role and receive temporary credentials (access key, secret key, session token) valid for 15 minutes to the role's maximum session duration (1 hour by default, up to 12).

In code:

  • sts.assume_role(RoleArn=..., RoleSessionName="report-job", DurationSeconds=3600) returns a Credentials dict with AccessKeyId, SecretAccessKey, SessionToken and Expiration.
  • Build a boto3.Session(aws_access_key_id=..., aws_secret_access_key=..., aws_session_token=...) from them and create clients.
  • Optional: ExternalId (required by many third-party trust policies) and an inline session Policy to restrict permissions further.

Credentials made this way don't refresh: after expiry, calls fail with ExpiredToken. For long-running processes, prefer letting botocore manage it: a profile in ~/.aws/config with role_arn and source_profile (or credential_source = Ec2InstanceMetadata / EcsContainer) makes boto3 assume the role and refresh credentials automatically before they expire.

Use a meaningful RoleSessionName (job name, user): it appears in CloudTrail, making audits possible. Verify with get_caller_identity(), whose ARN changes to assumed-role/<RoleName>/<session name>.

CI systems use the related assume_role_with_web_identity (OIDC tokens from GitHub Actions or GitLab) so no AWS keys are stored at all.

Example

import boto3

def session_for_role(role_arn, name, duration=3600):
    creds = boto3.client("sts").assume_role(
        RoleArn=role_arn, RoleSessionName=name, DurationSeconds=duration)["Credentials"]
    return boto3.Session(
        aws_access_key_id=creds["AccessKeyId"],
        aws_secret_access_key=creds["SecretAccessKey"],
        aws_session_token=creds["SessionToken"],
        region_name="us-east-1"), creds["Expiration"]

audit, expires = session_for_role("arn:aws:iam::123456789012:role/AuditReadOnly", "weekly-audit")
print(audit.client("sts").get_caller_identity()["Arn"])
# arn:aws:sts::123456789012:assumed-role/AuditReadOnly/weekly-audit
print(expires.tzinfo is not None)   # True: an aware datetime; no auto-refresh!

# Auto-refreshing alternative in ~/.aws/config:
# [profile audit]
# role_arn = arn:aws:iam::123456789012:role/AuditReadOnly
# source_profile = default
# -> boto3.Session(profile_name="audit") refreshes credentials automatically

Remember

  • assume_role returns temporary credentials for a role you're trusted to use
  • Sessions built from raw assume_role credentials don't refresh
  • Profiles with role_arn + source_profile refresh automatically
  • Use descriptive RoleSessionNames for CloudTrail audits

Watch out for

  • Caching assumed-role credentials in a long-running worker and failing with ExpiredToken after an hour.

Check yourself

  1. A daemon builds a Session from assume_role credentials at startup. What happens after the duration expires?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html#assume-role-provider