Cross-account access
Role assumption vs resource-based policies, how AWS evaluates a cross-account request, external IDs, and the S3 and KMS patterns the exam loves.
Exam tasks: 1.2 (cross-account access, IAM roles and policies, resource-based policies, least privilege across accounts)
The decision: should the caller switch into a role in the other account, or should the resource in the other account trust the caller directly? And which policies in both accounts have to say yes?
Choosing a pattern
Roles vs resource-based policies
| Assume a role (STS) | Resource-based policy | |
|---|---|---|
| How it works | Caller calls sts:AssumeRole and gets temporary credentials in account B | Resource in account B lists account A (or a principal in it) as allowed |
| Caller's own permissions | Given up for the session. It only has the role's permissions | Kept. It acts as itself in account A |
| Who must allow it | Role trust policy (B) + sts:AssumeRole in the caller's identity policy (A) | Resource policy (B) + identity policy (A) |
| Services | Any service | Only services that support resource policies (S3, SQS, SNS, KMS, Lambda, Secrets Manager, ECR, EventBridge and others) |
| Resource ownership | Objects written are owned by account B | S3 writes are owned by the bucket owner when Object Ownership is enforced |
| Audit trail | CloudTrail shows the role session in B, with the source identity if set | CloudTrail shows the principal from A |
| Good for | Operators, pipelines, broad admin access | One bucket, queue or key shared with a few accounts |
The copy-between-accounts signal
A Lambda function in account A must read from its own bucket and write to a bucket in account B in one flow. If it
assumes a role in B, it loses access to its own bucket for that session. A bucket policy in B that grants the
function's role s3:PutObject lets it keep both.
Roles in practice
- Trust policy says who can assume the role. Permissions policy says what the session can do.
- Session duration is 1 hour by default and can be set up to 12 hours on the role. Role chaining (a role session assuming another role) is capped at 1 hour.
- Pass a session policy on
AssumeRoleto narrow a session further. It can only remove permissions. - Set
sts:SourceIdentityso CloudTrail keeps the original human or workload name across chained sessions.
How a cross-account request is evaluated
For a request from account A to a resource in account B, every layer must allow it and an explicit deny anywhere wins.
| Layer | Lives in | Limits | Grants anything? |
|---|---|---|---|
| SCP | Organization, on the caller's account | What principals in that account can do | No, it only sets the ceiling |
| RCP | Organization, on the resource's account | What anyone (including outsiders) can do to resources in that account | No, ceiling only |
| Permissions boundary | The caller's IAM user or role | That one principal's maximum | No |
| Session policy | Passed at AssumeRole or federation | That one session | No |
| Identity policy | The caller's user, group or role | The caller | Yes |
| Resource policy | The bucket, key, queue and so on | Who can use that resource | Yes |
- Same account: either the identity policy or the resource policy can grant access on its own. KMS key policies and role trust policies are the exceptions: they must allow it explicitly.
- Cross-account: you need both the identity policy in A and the resource policy in B.
- SCPs and RCPs don't restrict the management account. SCPs also don't restrict service-linked roles.
- RCPs cover a set of services that includes S3, STS, KMS, SQS and Secrets Manager. Use them to put a data perimeter around resources, for example "nothing outside my organization can read any bucket".
An SCP that 'grants' access
An SCP with Allow never gives anyone a permission. If a question says a user can't reach a resource and the fix
offered is "add an Allow to the SCP", it's wrong unless the SCP was the thing denying it. The grant must come from
an identity or resource policy.
External ID and the confused deputy
A SaaS vendor's account assumes roles in hundreds of customer accounts. If customer X can tell the vendor "my role
ARN is arn:aws:iam::222233334444:role/Vendor" (which is really customer Y's role), the vendor becomes a confused
deputy acting on Y's data.
- The vendor generates a unique external ID per customer and stores it with the customer record.
- Each customer's trust policy requires
sts:ExternalIdto match. The vendor always sends the ID it holds for the customer who made the request, so it can't be tricked into using another customer's role. - The external ID is not a secret password. It's an anti-mix-up token that the vendor controls.
- For AWS services acting on your behalf (for example SNS writing to your SQS queue), the equivalent is
aws:SourceArnandaws:SourceAccountin the resource policy.
Exam signal
"Third party", "vendor", "monitoring provider" or "several customers" with cross-account roles means the answer
includes an external ID. For your own accounts, use aws:PrincipalOrgID or the account ID instead.
Organization-wide conditions
| Condition key | Use it to |
|---|---|
aws:PrincipalOrgID | Allow every principal in your organization in a resource policy, without listing 300 account IDs |
aws:PrincipalOrgPaths | Allow only principals under a given OU path |
aws:ResourceOrgID | In an identity policy or SCP, stop principals from writing to resources outside the organization |
aws:SourceOrgID | Limit an AWS service acting for you to your organization's resources |
New accounts that join the organization get access automatically, and accounts that leave lose it.
S3 cross-account patterns
Object Ownership
| Setting | ACLs | Who owns objects uploaded by another account |
|---|---|---|
| Bucket owner enforced (default for new buckets) | Disabled | The bucket owner, always |
| Bucket owner preferred | Enabled | The bucket owner, if the uploader sends bucket-owner-full-control |
| Object writer | Enabled | The uploading account |
With Object writer, the bucket owner can be unable to read objects that sit in its own bucket. Bucket owner enforced removes that problem and lets bucket policies alone control access.
When objects are encrypted with KMS
For SSE-KMS objects, S3 permissions aren't enough. The reader in account A also needs kms:Decrypt (and writers
need kms:GenerateDataKey):
- The key policy in the key's account must allow account A (or its role).
- The identity policy in account A must allow the KMS actions on that key ARN.
- It must be a customer managed key. The AWS managed
aws/s3key's policy can't be edited, so it can't be shared across accounts.
See encryption for the key policy details.
Bucket policy fixed, still AccessDenied
If a cross-account reader gets AccessDenied after the bucket policy is correct, check the KMS key policy. It's
the most common missing piece in exam scenarios that mention SSE-KMS.
Requester Pays
- The requester pays for requests and data transfer out. The bucket owner still pays for storage.
- Requests must include
x-amz-request-payer: requester(or the--request-payerCLI flag), which acknowledges the charge. Without it, S3 returns 403. - Anonymous requests are rejected. Every requester must be an authenticated AWS principal.
- If the requester assumes a role in the bucket owner's account, the bucket owner pays. For the requester to be billed, it must call with credentials from its own account, and the bucket policy grants that account access.
Finding what's shared
IAM Access Analyzer with the organization as the zone of trust flags every bucket, key, role, queue and other supported resource that grants access to principals outside the organization. Use it to prove least privilege after you've set up cross-account access.
Scenarios
This is the confused deputy problem, and the external ID solves it: the vendor sends the ID it holds for the requesting customer, so a role belonging to someone else won't accept it. PrincipalOrgID would let any vendor principal assume every customer role, which is exactly the mix-up to prevent. Source IP limits where calls come from, not which customer they're for, and an automated vendor service can't satisfy MFA.
Cross-account writes need an allow on both sides: the bucket policy in the destination account and the KMS key policy for the encryption key. The aws/s3 key can't be shared across accounts. Assuming a role in the destination account would make the job lose access to its source bucket. An SCP can't grant permissions.
When the requester assumes a role in the bucket owner's account, the request comes from the owner's account and the owner pays. Partners must use credentials from their own accounts, with a bucket policy granting those accounts, and send the request-payer header. The other options describe features that don't exist.