Learning Objectives
- Configure an IAM Role trust policy in a target account specifying an external trusted identity.
- Utilize AWS Security Token Service (STS) AssumeRole API to acquire temporary security credentials across AWS accounts.
- Validate cross-account identity switching and permissions using the AWS CLI.
- Diagnose and resolve common cross-account access permission errors and policy misconfigurations.
Checking Your Multi-Account Access Setup
Striking the right balance means using SCPs for non-negotiable security guardrails while keeping local IAM policies flexible enough for engineering teams to build efficiently. However, before you can pass credentials across organizational boundaries, you must verify your multi-account baseline and understand how AWS identifies principals and resources across separate environments.
To configure cross-account workflows, your architecture requires at least two distinct AWS environments:
- Source Account (Identity Account): The AWS account where your primary human users, IAM users, or central services reside (for example, Account ID
111111111111). - Target Account (Workload Account): The destination AWS account containing the resources, applications, or infrastructure that the source identity needs to manage (for example, Account ID
222222222222).
text +---------------------------------+ +---------------------------------+ | SOURCE ACCOUNT | | TARGET ACCOUNT | | (111111111111) | | (222222222222) | | | | | | [ IAM User: ops-engineer ] | -----> | [ IAM Role: WorkloadAdmin ] | | - Needs permission to call | | - Requires trust relationship | | sts:AssumeRole | | to allow Source Account | +---------------------------------+ +---------------------------------+
Deconstructing AWS Account IDs and ARN Syntax
AWS relies on strict formatting to reference resources uniquely across global infrastructure. Every AWS account is assigned a immutable 12-digit Account ID, which acts as the core namespace for all IAM identities and service resources created within that boundary.
To address an identity or resource across account boundaries, AWS uses an Amazon Resource Name (ARN). An ARN standardizes the complete path to any object in AWS.
The structure of an ARN follows a precise syntax depending on the resource type:
| ARN Component | Description | Example |
|---|---|---|
| Scheme | Always begins with the arn literal prefix |
arn |
| Partition | The group of AWS regions (typically aws, aws-cn, or aws-us-gov) |
aws |
| Service | The AWS service namespace | iam |
| Region | The AWS region (IAM is global, so this field is left blank) | empty |
| Account ID | The 12-digit target or source AWS account number | 222222222222 |
| Resource | The path and name of the specific resource | role/WorkloadAdminRole |
When constructed fully, an IAM user in your source account and an IAM role in your target account look like this:
- Source IAM User ARN:
arn:aws:iam::111111111111:user/ops-engineer - Target IAM Role ARN:
arn:aws:iam::222222222222:role/WorkloadAdminRole
Source Identity Permissions: Calling AssumeRole
Before a user in Account 111111111111 can request access to Account 222222222222, the calling identity must be explicitly granted permission within its own source IAM policy to invoke the sts:AssumeRole API operation.
Without an explicit Allow statement in the identity's permission policy, AWS implicit deny evaluation drops the request immediately long before the call ever reaches the target account boundary.
To prepare the source identity for cross-account operations, attach an IAM identity policy that specifies sts:AssumeRole as the allowed action, pointing directly to the target role's exact ARN as the destination resource:
json { "Version": "2012-10-17", "Statement": [ { "Sid": "AllowSourceIdentityToAssumeTargetRole", "Effect": "Allow", "Action": "sts:AssumeRole", "Resource": "arn:aws:iam::222222222222:role/WorkloadAdminRole" } ] }
With this identity policy active in Account 111111111111, your source identity possesses the local outbound authority required to initiate cross-account requests toward the target workload environment.
Target End-State: Secure Cross-Account Access
With this identity policy active in Account 111111111111, your source identity possesses the local outbound authority required to initiate cross-account requests toward the target workload environment. But what does the completed end-state actually look like when your request lands in the target environment?
To establish secure cross-account access, we avoid sharing long-term access keys or creating duplicate IAM users across accounts. Instead, the ultimate goal is to enable an identity in Account 111111111111 to temporarily step into a dedicated IAM role residing inside Account 222222222222.
The Cross-Account Role Assumption Flow
The end-state architecture relies on a coordinated request-and-response flow managed by AWS. When properly configured, cross-account access follows a four-step lifecycle:
- The Request: An identity (user or application) in Account
111111111111makes a request to access Account222222222222. - The Verification: AWS evaluates both sides of the request checking if the source identity is allowed to leave Account
111111111111and if the target role in Account222222222222trusts Account111111111111to enter. - Credential Issuance: If both sides agree, AWS generates a set of short-lived, temporary credentials specifically scoped to the target role.
- Target Access: The source identity switches context, using those temporary credentials to perform authorized operations directly inside Account
222222222222.
Temporary Security Credentials Overview
Unlike long-term IAM user access keys, temporary security credentials are non-static and expire automatically. When an identity assumes a role, AWS issues temporary credentials that grant access only for a specified duration.
These credentials consist of three distinct components:
AccessKeyId: A unique key identifier starting with the prefixASIA(which distinguishes temporary access keys from staticAKIAlong-term keys).SecretAccessKey: A secret key used to sign programmatic requests sent to AWS service endpoints.SessionToken: An encrypted token required by AWS service endpoints to validate the active session and verify its exact authorization context.
In addition to these keys, every temporary credential set includes an Expiration timestamp (typically ranging from 15 minutes to 12 hours). Once this expiration window closes, AWS immediately rejects any further API requests signed with those credentials.
| Credential Feature | Static IAM User Keys | Temporary Session Credentials |
|---|---|---|
| Key Prefix | AKIA... |
ASIA... |
| Lifespan | Indefinite (until manually revoked) | 15 minutes up to 12 hours |
| Components | Access Key + Secret Key | Access Key + Secret Key + Session Token |
| Security Profile | High risk if leaked | Low risk due to automatic expiration |
Understanding this temporary credential payload helps clarify the overall target state. Now that you understand the target end-state, we need to inspect the precise gatekeeper mechanism in Account 222222222222 that determines whether Account 111111111111 is trusted.
Mapping the Cross-Account Trust Topology
Now that you understand the target end-state, we need to inspect the precise gatekeeper mechanism in Account 222222222222 that determines whether Account 111111111111 is trusted.
To bridge two distinct AWS accounts safely, AWS does not allow direct long-term user creation across boundaries. Instead, it relies on a trusted broker to issue short-lived identity passes. That trusted broker is AWS Security Token Service (STS), and the gatekeeper rules are written directly into an IAM Role's Trust Policy.
The Identity Broker: AWS Security Token Service (STS)
AWS Security Token Service (STS) is the global web service that authenticates requestors and mints temporary, time-bound security credentials.
When an identity in Account 111111111111 wants to perform actions inside Account 222222222222, it never talks directly to target resources like Amazon S3 or Amazon DynamoDB first. Instead, the identity calls AWS STS.
AWS STS intercept the request and performs a critical handoff evaluation:
1. It looks at the target IAM Role requested in Account 222222222222.
2. It evaluates the JSON document attached to that target role known as the Trust Policy.
3. If the Trust Policy explicitly permits the incoming identity, AWS STS returns temporary access keys and a session token.
Without AWS STS acting as this central validation engine, cross-account access would require duplicating long-term IAM users across accounts creating a massive security anti-pattern.
Trust Relationships in JSON
An IAM Role in the target account (222222222222) has two distinct policy types attached to it:
* Permissions Policy (Identity/Resource Policy): Defines what actions the role can perform (e.g., s3:GetObject).
* Trust Policy (Resource-Based Policy): Defines who is allowed to assume the role.
A Trust Policy is a mandatory JSON document that grants an external principal permission to call the sts:AssumeRole API operation.
Below is the foundational JSON structure of a target trust policy configured inside Account 222222222222:
json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::111111111111:root" }, "Action": "sts:AssumeRole" } ] }
Notice the key components working together:
* Effect: Must be explicitly set to Allow.
* Action: Specifies sts:AssumeRole, the exact API action required to exchange credentials.
* Principal: Specifies the external identity being granted permission.
Anatomy of the Principal Element
The Principal element is the single most critical element in a trust policy because it defines the boundary of who Account 222222222222 trusts.
Depending on your security architecture, you can scope the Principal element broadly to an entire external AWS account, or narrowly to specific IAM entities within that account.
| Principal Scope | Syntax Example | Security Authorization Behavior |
|---|---|---|
| Entire Account | "AWS": "arn:aws:iam::111111111111:root" |
Delegates control. Trusts Account 111111111111. Administrators in Account 111111111111 decide which of their own users/roles can assume the target role. |
| Specific IAM User | "AWS": "arn:aws:iam::111111111111:user/Alice" |
Strict narrowing. Only the specific IAM user Alice in Account 111111111111 can attempt role assumption. |
| Specific IAM Role | "AWS": "arn:aws:iam::111111111111:role/SecOpsRole" |
Workload-to-Workload. Only sessions already running under SecOpsRole in Account 111111111111 can assume this target role. |
Architect's Warning: Specifying arn:aws:iam::111111111111:root does not give the root account user absolute access by default. Using :root in a trust policy simply designates Account 111111111111 as an administrative authority, delegating the responsibility to Account 111111111111 to grant its identities outbound permissions.
By combining AWS STS as the credential broker with a strictly defined Principal in your JSON trust policy, you establish a deterministic authorization pathway between disconnected AWS accounts.
Deploying the Trust Policy and STS Role Switch
With the trust topology fully mapped, we can now construct the trust policy document, deploy the target IAM role, and execute the role switch using the AWS CLI. Moving from architecture to execution requires precise JSON policy syntax and a clear understanding of how the AWS Security Token Service (STS) handles API requests under the hood.
Step 1: Constructing the JSON Trust Policy Document
Before creating an IAM role in the target account, you must define the trust relationship in a JSON document. The trust policy acts as the gatekeeper attached to the target IAM role, explicitly delegating permission to an external principal to call sts:AssumeRole.
Create a local file named trust-policy.json containing the policy definition:
json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::111122223333:root" }, "Action": "sts:AssumeRole" } ] }
In this policy definition:
* Effect: Set to Allow to grant permission.
* Principal: Specifies the trusted source account ID (111122223333). Using the :root identifier delegates authority to the source account's IAM system to decide which specific users or roles can assume this role.
* Action: Specifies sts:AssumeRole as the single API action permitted by this trust policy.
With the policy file saved, deploy the role in the target account (444455556666) using the AWS CLI:
bash aws iam create-role \ --role-name TargetCrossAccountRole \ --assume-role-policy-document file://trust-policy.json
Step 2: Mechanics of the sts:AssumeRole Execution
Once the IAM role exists in the target account with its trust policy attached, you can invoke the STS service from the source account. When an identity calls sts:AssumeRole, AWS STS evaluates the target policy, validates the caller's permissions, and generates dynamic credential sets.
To manually trigger this process, execute the assume-role CLI command from the source environment:
bash aws sts assume-role \ --role-arn arn:aws:iam::444455556666:role/TargetCrossAccountRole \ --role-session-name DeveloperSession
When this command runs, the following low-level execution mechanics take place:
- API Request Signing: The AWS CLI signs the request using the active credentials in the source account (
111122223333). - Endpoint Routing: The request hits the regional or global AWS STS endpoint (
sts.amazonaws.com). - Trust Policy Evaluation: STS inspects the target role
arn:aws:iam::444455556666:role/TargetCrossAccountRoleand verifies that account111122223333is listed as a trustedPrincipal. - Session Initialization: STS logs the session under the custom identifier provided in
--role-session-name(DeveloperSession) for CloudTrail audit tracking.
Step 3: Automating Role Assumption with AWS CLI Profiles
Executing aws sts assume-role manually returns dynamic credentials that you would otherwise have to export into your shell environment manually. By linking role_arn to a source_profile in your local AWS config file, the AWS CLI handles role assumption automatically for every command.
Open your local configuration file (~/.aws/config on Linux/macOS or %USERPROFILE%\.aws\config on Windows) and add the target role configuration:
ini [profile source-account] region = us-east-1 output = json
[profile target-account] region = us-east-1 role_arn = arn:aws:iam::444455556666:role/TargetCrossAccountRole source_profile = source-account role_session_name = AutomatedCLISession
Here is how the CLI configuration parameters function together:
* profile target-account: Defines the profile name you will pass to CLI commands.
* role_arn: Specifies the exact Amazon Resource Name of the target IAM role to assume.
* source_profile: Directs the CLI to use the credentials stored in source-account to sign the initial call to STS.
* role_session_name: Sets a static or dynamic session identifier for logging purposes.
When you issue any command using --profile target-account, the AWS CLI seamlessly intercepts the invocation, calls sts:AssumeRole behind the scenes using the source_profile, and uses the returned credentials to execute your request against the target account.
With the configuration files deployed and the role switch executed, our next step is to inspect the output and verify our newly acquired identity.
Verifying Identity and Temporary Credentials
With the configuration files deployed and the role switch executed, our next step is to inspect the output and verify our newly acquired identity. Assuming a role isn't just a conceptual state shift it gives your CLI or application a concrete set of short-lived security tokens. To confirm that your cross-account requests are originating from the target account rather than your original identity, you need to validate your current caller context.
Validating Context with aws sts get-caller-identity
The quickest way to answer "Who am I right now?" in AWS is the aws sts get-caller-identity command. This call acts as the definitive whoami tool for AWS, querying STS to return the exact identity context attached to your active request.
When using AWS CLI profiles configured for automated cross-account switching, pass the target profile name via the --profile flag:
bash aws sts get-caller-identity --profile cross-account-role
If your trust policy and CLI profile are properly configured, AWS STS returns a JSON payload confirming your elevated session:
json { "UserId": "AROA123456789EXAMPLE:my-session", "Account": "444455556666", "Arn": "arn:aws:sts::444455556666:assumed-role/CrossAccountAdminRole/my-session" }
Notice the crucial changes in this output:
* Account: Displays the target account ID (444455556666), proving your command executed inside the target account boundary rather than your home account.
* Arn: Notice the prefix changes from arn:aws:iam:: to arn:aws:sts::. It specifies assumed-role, followed by the target role name (CrossAccountAdminRole) and the session identifier (my-session).
* UserId: Contains a unique identifier generated by AWS (prefixed with AROA for role sessions), appended with your session name.
Anatomy of Temporary Credentials
When you invoke aws sts assume-role directly, AWS STS returns a dynamic credential bundle. Unlike permanent IAM user credentials which rely on a static key pair, temporary access requires a three-part credential set to authenticate API requests.
json { "Credentials": { "AccessKeyId": "ASIAIOSFODNN7EXAMPLE", "SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", "SessionToken": "IQoJb3JpZ2luX2VjE...", "Expiration": "2026-03-30T15:00:00Z" } }
Understanding these three core parameters is essential when passing temporary credentials to third-party scripts, SDKs, or environment variables:
AccessKeyId: The unique identifier for the temporary session. Temporary access keys generated by STS always begin with the string prefixASIA, whereas permanent IAM user keys begin withAKIA.SecretAccessKey: The secret key used to cryptographically sign API requests made during the session.SessionToken: An encrypted token containing session metadata, including permissions limits and expiration boundaries. AWS signature authentication will fail if you pass anASIAkey without its correspondingSessionToken.Expiration: The exact UTC timestamp when STS invalidates these credentials. Once reached, any API calls using this token set are immediately rejected.
| Key Attribute | Long-Term Credentials (IAM User) | Short-Term Credentials (STS AssumeRole) |
|---|---|---|
| Key Prefix | AKIA... |
ASIA... |
| Required Components | AccessKeyId, SecretAccessKey |
AccessKeyId, SecretAccessKey, SessionToken |
| Lifecycle | Permanent until manually rotated | Expire automatically (1 to 12 hours) |
| Storage Best Practice | Avoid using; store securely if required | Ephemeral; held in memory or session variables |
When using AWS CLI profile switching, the AWS CLI automatically captures these three credentials, refreshes them when necessary, and injects them into your command signatures behind the scenes.
Fixing Common Cross-Account Access Blockers
Now that we know how to verify a successful identity switch and inspect temporary credentials, we can tackle what happens when something breaks.
In AWS cross-account operations, an AccessDenied error is the cloud equivalent of a brick wall. Because the AWS Security Token Service (STS) returns generic error messages by default to prevent information disclosure, diagnosing why your aws sts assume-role call failed requires systematic troubleshooting.
The Two-Way Handshake Rule
Cross-account access always requires an explicit two-way handshake. Permission must be granted on both sides of the account boundary for a role assumption to succeed.
text +---------------------------------+ +---------------------------------+ | Source Account (111111111111) | | Target Account (999999999999) | | | | | | [ Calling Identity ] | | [ Target IAM Role ] | | | | | | | | Identity Policy: | | Trust Policy: | | Allows sts:AssumeRole =======> AssumeRole =====> Allows Principal | | on Target Role ARN | Request | arn:aws:iam::111111111111:...| +---------------------------------+ +---------------------------------+
If either side of this handshake fails, AWS blocks the request. The evaluation logic works as follows:
- Source Evaluation: Does the calling identity in Account
111111111111have an identity policy grantingsts:AssumeRoleon the target role ARN? - Target Evaluation: Does the target role's trust policy in Account
999999999999list the calling identity (or its account) as a trustedPrincipalforsts:AssumeRole?
Both sides must evaluate to an ALLOW, and neither side can contain an explicit Deny statement.
Evaluating Failure Modes
To fix an access failure quickly, you need to map the error symptom to the specific side of the handshake that caused it.
| Failure Location | Symptom | Root Cause | Remediation |
|---|---|---|---|
| Source Identity Policy | An error occurred (AccessDenied) |
The source user or role lacks an attached policy granting sts:AssumeRole. |
Attach an IAM policy to the source identity allowing sts:AssumeRole targeting the destination role ARN. |
| Target Trust Policy | An error occurred (AccessDenied) |
The target role trust policy lists an incorrect Principal ARN or account ID. |
Update the target role's trust policy to include the exact ARN of the source identity or parent account. |
| Condition Mismatch | An error occurred (AccessDenied) |
The request missing required condition flags (e.g., sts:ExternalId). |
Pass the required flag in the CLI call or update policy conditions. |
| Resource Policy (Post-Switch) | AccessDenied when running target commands |
Target role permissions lack rights to the requested target service (e.g., S3). | Attach resource permissions (like s3:ListBucket) to the target IAM role itself. |
Debugging Scenario 1: Source Identity Policy Denials
When you execute an aws sts assume-role request from your terminal:
bash aws sts assume-role \ --role-arn arn:aws:iam::999999999999:role/CrossAccountTargetRole \ --role-session-name DebuggingSession
If your local user lacks permission to initiate the assume role call, STS immediately returns an AccessDenied error.
To fix this, inspect the IAM user or role permissions in your source account (111111111111). Ensure an identity-based policy contains an explicit sts:AssumeRole block matching the exact ARN of the target role.
json { "Version": "2012-10-17", "Statement": [ { "Sid": "AllowAssumeTargetRole", "Effect": "Allow", "Action": "sts:AssumeRole", "Resource": "arn:aws:iam::999999999999:role/CrossAccountTargetRole" } ] }
Debugging Scenario 2: Target Trust Policy Denials
Even if your source policy is correctly configured, if the target role's trust policy does not explicitly permit your principal, the assume request will fail.
Common target trust policy mistakes include:
- ARN Typos: Specifying
arn:aws:iam::111111111111:user/devuserwhen the real IAM user name isDevUser(IAM ARNs are case-sensitive). - Identity vs. Account Trust Mismatch: Trusting
arn:aws:iam::111111111111:rootdelegates access to the source account's administrators, but individual non-admin users in111111111111still require explicit source identity permissions. - Deleted and Recreated Entities: If an IAM user or role in the source account is deleted and recreated, its underlying unique ID (e.g.,
AIDA...) changes, invalidating previous trust configurations until the trust policy is saved again.
Here is a corrected trust policy snippet in the target account (999999999999):
json { "Version": "2012-10-17", "Statement": [ { "Sid": "TrustSourceAccountUser", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::111111111111:user/DevUser" }, "Action": "sts:AssumeRole" } ] }
Systematic Troubleshooting Checklist
When facing persistent AccessDenied errors during cross-account role assumption, work through this step-by-step checklist:
- Confirm Current Identity: Run
aws sts get-caller-identityto verify the exact IAM principal making the API request. - Verify Exact ARNs: Compare the target role ARN in your CLI command character-for-character against the target role's configuration in the AWS Management Console.
- Check Service Control Policies (SCPs): If using AWS Organizations, ensure an upper-level SCP is not blocking
sts:AssumeRolecalls across account boundaries. - Validate Required Conditions: If the target role's trust policy requires an
ExternalIdcondition, ensure you include--external-idin youraws sts assume-roleinvocation.