Cross-Account Access & STS
Cross-Account Access & STS
At big-tech scale, a single AWS account is an anti-pattern. Production, staging, security, logging, and shared services live in separate accounts inside an AWS Organization. Engineers, CI/CD pipelines, and services must cross those account boundaries constantly — and the mechanism that makes all of it possible without sharing long-lived credentials is AWS Security Token Service (STS) combined with IAM AssumeRole.
Why Separate Accounts?
Account-level isolation is the strongest blast-radius control AWS offers. A compromised credential in staging cannot touch production data. Service Control Policies (SCPs) at the Organization level enforce guardrails that not even the account root can override. Cost allocation, CloudTrail, and AWS Config run per-account, giving clean audit trails. The standard pattern is: one management account for Organizations only, one security/audit account, one shared-services account (DNS, ECR, CI/CD tooling), and separate workload accounts per team or environment.
The AssumeRole Flow
The core mechanic is a four-step chain: a principal (IAM user, role, or AWS service) in Account A calls sts:AssumeRole targeting a role ARN in Account B. STS validates that the trust policy on the target role permits the caller, issues short-lived credentials (access key ID, secret access key, session token) valid for 15 minutes to 12 hours, and returns them. The caller embeds those credentials in subsequent API calls — AWS evaluates them against the target role's permission policies in Account B.
Trust Policies — The Door
A trust policy is the resource-based policy attached to the role itself that answers: "who is allowed to assume this role?" It uses the sts:AssumeRole action with a Principal element naming the trusted entity — an account ID, a specific role ARN, or an AWS service.
External IDs — The Confused Deputy Defense
Imagine you build a SaaS tool that assumes a role in your customer's AWS account to read CloudWatch metrics. You configure the trust policy to trust your SaaS's AWS account. Now consider: a different customer could trick your SaaS into using its own legitimate credentials to assume your first customer's role by supplying that role ARN. This is the confused deputy attack.
The fix is an External ID: a shared secret value that both you (the SaaS) and the customer (who writes the trust policy) agree on, unique per customer. When your service calls AssumeRole it must supply the correct ExternalId for that customer, and the trust policy enforces it with a StringEquals condition. A malicious caller who does not know the External ID cannot trigger a successful assumption.
Assuming a Role from the CLI
Use aws sts assume-role to get temporary credentials, then export them as environment variables for subsequent CLI calls.
In CI/CD systems (GitHub Actions, Jenkins, GitLab CI) you never do this manually. Instead you configure the job runner to use aws-actions/configure-aws-credentials (GitHub Actions) or an EC2 instance role, and the SDK handles token refresh automatically.
Session Policies — Scoping Down at Assume Time
When calling AssumeRole you may pass an optional session policy: an inline JSON policy that further restricts the effective permissions for this session. It cannot grant permissions beyond what the role itself allows — the effective permission set is always the intersection of the role's policies and the session policy. This is powerful for least-privilege: a general-purpose role with broad read permissions can be scoped to a single S3 bucket for a specific automated task.
Cross-Account Role Patterns in Production
The hub-and-spoke pattern is standard: a central CI/CD role in the shared-services account has trust from each workload account's DeployRole. The pipeline assumes the target account's role directly — no chaining. For read-only audit access, a dedicated security account role is trusted by all workload accounts, giving the security team a single place to initiate cross-account reads without any standing access.
For AWS Organizations, SCPs add a third layer: even if a trust policy and permission policy both allow an action, an SCP denying it at the OU level will block it. Always validate cross-account access with aws iam simulate-principal-policy after SCP changes.