Both an S3 bucket policy and an IAM policy can grant s3:GetObject. They are not interchangeable — they attach to different things and evaluate together.
The one-line difference
- IAM policy — identity-based. Attached to a user, group or role. Has no Principal (the identity it attaches to is the principal). Answers: "what is this identity allowed to do?"
- Bucket policy — resource-based. Attached to the bucket. Always has a Principal. Answers: "who is allowed to touch this bucket?"
Side by side
| IAM policy | Bucket policy | |
|---|---|---|
| Attaches to | User / group / role | The bucket |
Has Principal? | No | Yes (required) |
| Good for | Your own principals | Cross-account, anonymous/public |
| Lives in | IAM | S3 |
How they combine
For a request to succeed, AWS evaluates both:
- Collect every applicable statement from IAM policies and the bucket policy.
- If any statement has an explicit
Deny→ denied. Deny always wins. - Otherwise, if any statement has an
Allow→ allowed. - Otherwise → denied by default.
So for same-account access you only need an Allow in one of them. For cross-account access you generally need an Allow on both sides — the bucket policy trusts the other account, and that account's IAM lets its principal use S3.
Which do I reach for?
- Granting your own role read access → IAM policy.
- A different account needs access → bucket policy (name their account/role as Principal) + their IAM.
- Anonymous public read → bucket policy with
Principal: "*". - Blanket guardrail (deny non-HTTPS, deny outside a VPC) → bucket policy
Deny— it covers every principal at once.
Verify the combination, not just one file
Because a Deny in either policy wins, reading one file in isolation lies to you. Paste both into the workspace and ask "does this identity get GetObject on this bucket, and why?" — the AI walks the allow/deny evaluation across both documents.