Account A owns an S3 bucket and a role in account B must read objects under one prefix. What has to be configured in each account, and why does an Allow in only one of them fail?
answer
- consent from both accounts
- union inside, intersection across
- two ARN shapes again
- confine listing with a prefix condition
- encrypted objects need the key too
basics
~20 sCross-account S3 access needs two allows: the bucket policy in account A must permit account B's role, and an identity policy in account B must permit the same actions on that bucket. Either side alone is an implicit deny, so the request fails.
solid answer
~40 sBoth sides must say yes. In account A, the bucket policy names the role's ARN as `Principal` and allows `s3:GetObject` on `arn:aws:s3:::bucket/prefix/*`, plus `s3:ListBucket` on `arn:aws:s3:::bucket` — usually with a `s3:prefix` condition so listing is confined too. In account B, the role's identity policy allows the same actions on the same ARNs. Cross-account access is an intersection, not a union: account A cannot force permissions onto B's principals, and B cannot grant itself access to someone else's data, so a single-sided allow leaves an implicit deny standing. Two extra gotchas: if the objects are encrypted with a customer managed KMS key, that key's policy must also allow the role; and Block Public Access does not affect this, because a named cross-account principal is not public.
code
json · 16 lines{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::shared-exports/partner-b/*"
},
{
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::shared-exports",
"Condition": {"StringLike": {"s3:prefix": "partner-b/*"}}
}
]
}go deeper
Know that cross-account access needs a grant on both sides — a bucket policy naming the other account's role, and an IAM policy on that role. Be able to point at which document lives where.
Explain why the rule is an intersection rather than a union, write both documents with the correct bucket and object ARNs, and add the s3:prefix condition that keeps listing scoped.
Debug it under pressure: read the denial in the caller's CloudTrail, rule out SCPs and boundaries, and recognise the KMS variant where only encrypted objects fail. Know when to grant the account rather than the role.
Decide the sharing model before the bucket policy grows into a partner directory — account-level delegation, access points per consumer, or a dedicated distribution bucket — and set the standard for how external shares are reviewed and revoked.
## The intersection rule Inside one account, an `Allow` in the identity policy *or* the bucket policy is sufficient. Across accounts that changes: the request must be allowed by a policy in the caller's account **and** by the resource policy in the owner's account. The reason is sovereignty. If a bucket policy alone were enough, any account could hand permissions to your roles without you knowing; if an identity policy alone were enough, you could grant yourself access to anyone's bucket. Requiring both means neither account can move without the other's consent. ## Side A: the bucket policy ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": {"AWS": "arn:aws:iam::222233334444:role/PartnerReader"}, "Action": "s3:GetObject", "Resource": "arn:aws:s3:::shared-exports/partner-b/*" }, { "Effect": "Allow", "Principal": {"AWS": "arn:aws:iam::222233334444:role/PartnerReader"}, "Action": "s3:ListBucket", "Resource": "arn:aws:s3:::shared-exports", "Condition": {"StringLike": {"s3:prefix": "partner-b/*"}} } ] } ``` Two details matter. First, the two ARN shapes: object actions take `bucket/prefix/*`, bucket actions take the bare bucket ARN. Second, the `s3:prefix` condition — without it, a partner allowed to list the bucket can enumerate every other tenant's key names even if they cannot read the objects. You can name the principal at three granularities: the account root ARN `arn:aws:iam::222233334444:root`, which delegates the decision entirely to account B's own IAM; a specific role ARN, which is tighter; or a condition on `aws:PrincipalOrgID` when the audience is "any principal in my organisation". Naming the account root is not naming the root *user* — it is shorthand for "this account may grant this". ## Side B: the identity policy Account B attaches a matching allow to the role. It must list the same actions and the same resource ARNs; being broader than A's policy is harmless, being narrower is what silently blocks the call. There is no `Principal` element here — the role is the principal. ## Why single-sided fails, and how it looks Missing side produces a plain `403 AccessDenied` with no hint about which policy was silent, because AWS deliberately does not leak the other account's configuration. The diagnostic move is to check both documents rather than guessing, and to use the IAM policy simulator or CloudTrail in the *caller's* account, where the denied event is recorded. A related confusion: without `s3:ListBucket`, a GET for a key that does not exist returns `403 AccessDenied` instead of `404 NoSuchKey`. S3 hides existence from callers that cannot list, so "403 on a key I am sure exists" may actually be a typo in the key name. ## The KMS trap If the objects use SSE-KMS with a customer managed key, reading them requires `kms:Decrypt` on that key, and cross-account KMS follows the same two-sided rule — the key policy in account A must allow the role, and account B's identity policy must allow the KMS action. A cross-account share that works for unencrypted objects and fails only for encrypted ones is nearly always this. ## Ways to avoid hand-rolling the pair For one consumer, the two policies are fine. As the consumer list grows, the bucket policy becomes a directory of every partner and heads toward its 20 KB limit. The scaling answers are to grant the *account* and let it delegate internally, or to move the per-consumer surface into S3 Access Points so each consumer gets its own endpoint and its own policy while the bucket policy stays one delegation statement. ## Checklist for a working share - Bucket policy allows the role for object actions on `bucket/prefix/*` and list on `bucket`, with an `s3:prefix` condition. - Role's identity policy allows the same actions on the same ARNs. - KMS key policy allows the role if the objects are SSE-KMS with a customer managed key. - Nothing in either account explicitly denies it — check SCPs and permissions boundaries, which are invisible from the other side. - The caller addresses the bucket in its own Region and account context; using the right endpoint avoids a redirect that some SDK configurations surface as an unrelated error.
- The role can GetObject fine, but `aws s3 ls s3://shared-exports/partner-b/` returns AccessDenied. What is missing?`s3:ListBucket` on the bucket ARN, `arn:aws:s3:::shared-exports` — listing is a bucket-level action, so a policy that only covers `bucket/*` grants reads but no enumeration. Add a second statement for the bucket ARN, and constrain it with an `s3:prefix` condition so the partner sees only their own key names.
- Why does naming arn:aws:iam::222233334444:root in a bucket policy not mean the root user?In a resource policy, the account root ARN is shorthand for "any principal in that account that its own IAM also allows". It delegates the decision to account B's administrators rather than granting the root user specifically. It is broader than naming one role, so use it when you trust the other account to manage the grant internally.
- Cross-account reads work for older objects and fail for newly written ones. Where do you look?Encryption. If the newer objects are encrypted with a customer managed KMS key, the reader also needs `kms:Decrypt`, granted on both sides — the key policy in the owning account and the role's identity policy. A bucket-level change such as switching the default encryption key produces exactly this split between old and new objects.
saying these in an interview costs you the question
- Thinks the bucket policy alone can grant a foreign role access
- Assumes an identity policy can reach any bucket it names
- Grants ListBucket on the object ARN pattern
- Forgets the KMS key policy for encrypted objects
- Reads a 403 on a missing key as proof of a policy problem