S3 Bucket Policy vs IAM Policy
Understand where each policy lives and how AWS evaluates them both at once.

There are exactly two places you can attach an S3 access control policy: to the identity making the request, or to the bucket receiving it. That's it. Think of it like a two-sided lock — the key (IAM) lives with the person, and the keyhole (bucket policy) lives on the door. Everything confusing about S3 permissions, and there is a lot that confuses people, traces back to not having a clear picture of that structural difference. Once you do have that picture, the right tool for any given situation becomes pretty obvious.
A quick vocabulary note before diving in. An IAM policy attaches to a user, group, or role. It answers the question: what is this identity allowed to do? A bucket policy attaches to the bucket itself. It answers the question: who is allowed to touch this bucket? Both are written in the same JSON policy language. Both get evaluated on every request. The key structural tell is the Principal element. IAM policies lack one because the identity is implicit. Bucket policies require one because you have to name who you're talking to. That difference isn't cosmetic. It determines what each policy type can reach, and who can be named in it.
There are also two other layers worth flagging early: ACLs, which are legacy and essentially deprecated, and Resource Control Policies, which are newer and operate at the organizational level. More on both below.
AWS Always Reads Both Policies at the Same Time
AWS does not read your bucket policy first and then check your IAM policy. It reads all applicable policies simultaneously, on every single API call. The governing rule is simple:
- An explicit Deny anywhere wins. Full stop.
- Otherwise, at least one policy must explicitly Allow the action.
This means an Allow in your bucket policy cannot save a caller who has an explicit Deny in their IAM policy. And an Allow in an IAM policy cannot override a Deny in a bucket policy. Denies are absolute.
For callers inside your own AWS account, Allows are additive. An Allow in IAM plus an Allow in the bucket policy just stack. Either one is enough to get access through, as long as there's no Deny lurking somewhere.
For cross-account callers, it's stricter. Both sides must independently Allow the action. The bucket owner's side needs a bucket policy naming the external principal. The caller's side needs an IAM policy allowing access to that bucket. One Allow without the other is a silent failure, not an error. The request just gets denied, with no helpful explanation about which side is missing.
This is where teams get burned. Someone adds a permissive bucket policy and assumes IAM is "handling the rest," or vice versa. The gap (or the unexpected block) sits there quietly until something breaks in production.
One practical footnote: bucket policies allow up to 20 KB; IAM policies cap at 5 KB. Rarely a deciding factor, but worth knowing when your policy statements start growing large.
IAM Is the Right Tool When All Your Principals Live in Your Account
If every identity that needs access to a bucket is an IAM user, group, or role inside your own AWS account, IAM is the cleaner solution. Here's why it wins in that scenario:
- Central management. Update a role's policy once, and every resource that role touches reflects the change. You don't have to touch individual bucket policies scattered across dozens of buckets.
- Multi-resource reuse. A single IAM policy can reference multiple S3 buckets, keeping permissions consolidated for a whole service or team.
- Prefix-level precision. IAM policies support resource ARNs like
arn:aws:s3:::my-bucket/uploads/*, which lets you scope access to a specific folder without exposing the whole bucket.
A classic fit for IAM: a data pipeline role that reads from one prefix and writes to another, with no external callers involved. Attach the policy to the role, define the exact prefixes, done.
What IAM cannot do is grant access to a principal outside your account. A policy attached to your identities cannot reach across to a foreign caller. That's just not what IAM is built for.
Bucket Policies Are the Right Tool When the Caller Is External
The moment someone outside your account needs to touch your bucket, a bucket policy stops being optional. It becomes the only mechanism available on your side of the transaction.
Specific situations where bucket policies are required:
- Cross-account access. You can name a foreign account ID or role ARN as the
Principal. IAM cannot do this from the owner's side. - Public access. A static website serving unauthenticated users needs a bucket policy allowing anonymous
s3:GetObject. IAM has no concept of anonymous principals. - AWS service principals. CloudFront, Lambda functions in another account, or other AWS services all need to be named as principals. Bucket policies handle this; IAM doesn't.
- Bucket-wide enforcement. A Deny on non-HTTPS requests in a bucket policy applies to every caller, regardless of their IAM policies. This is the standard pattern for enforcing encryption in transit across all principals at once.
The cross-account reminder is worth repeating: even with a bucket policy granting access, the external caller still needs an IAM policy on their side. The bucket policy is necessary but not sufficient.
Practitioner rule of thumb, stated plainly: use IAM for your own users and roles, use bucket policies for external access or bucket-wide rules.
Using Both Together Is the Normal Production Setup
For same-account access, you can technically get by with one or the other. IAM alone works. A bucket policy alone works. Allows are additive, so either path gets you there.
For cross-account access, both are mandatory. There's no getting around it.
Here's a concrete pattern. A data science team in Account B needs read access to a training-data bucket in Account A.
- Account A sets a bucket policy with the data science role ARN from Account B as the
Principal, and allowss3:GetObject. - Account B attaches an IAM policy to that data science role, allowing
s3:GetObjecton the Account A bucket ARN.
Remove either policy and access is denied. Both must exist.
Beyond cross-account scenarios, using both policies together also creates a clean division of labor. The bucket policy handles the bucket-wide rules (HTTPS-only enforcement, for example). The IAM policy handles least-privilege scoping per role. That's not redundancy. It's each tool doing the job it's actually built for.
One audit point that catches people off guard: when both policies exist, you have to read them together to understand effective permissions. Reading only one gives you an incomplete picture. This sounds obvious, but it's easy to forget when you're three accounts deep and debugging an access issue at 11pm.
ACLs Used to Matter. They Don't Anymore.
ACLs predate IAM and bucket policies. They were originally the only way to control object-level access in S3, letting you grant another AWS account read access to a specific object rather than an entire bucket. At the time, it was the best available tool.
AWS's current position is unambiguous: set S3 Object Ownership to "Bucket owner enforced," which disables ACLs entirely. All new S3 buckets created since early 2023 ship with ACLs disabled, encryption enabled, and public access blocked by default.
Every use case ACLs ever covered is now handled by bucket policies and IAM policies, with better auditability and less confusion.
You'll still encounter ACLs on legacy buckets that predate those defaults, or on buckets that third-party tools historically wrote ACLs to. When you do, the right move is to migrate off them, not to extend them. There's no need to learn ACL syntax for new work. Knowing they exist is enough to recognize them when you're reviewing an older configuration.
Resource Control Policies Are the New Guardrail at the Org Level
AWS announced Resource Control Policies (RCPs) in AWS Organizations in late 2024. At launch, they applied to S3, STS, KMS, SQS, and Secrets Manager.
RCPs sit above both bucket policies and IAM policies in the evaluation stack. They set the maximum permissions any resource in the organization can grant, regardless of what individual bucket policies say. Think of them as an organizational ceiling — no matter how high your bucket policy tries to reach, the RCP defines the highest rung on the ladder.
Concrete example: if a bucket policy grants s3:GetObject to an external account, but an RCP says "no principal outside this organization may access S3 buckets," the RCP's Deny wins. The bucket policy cannot override it.
Important clarification: RCPs do not grant permissions. They only restrict. You still need IAM policies and bucket policies to actually allow access. RCPs just define the outer boundary of what's possible.
The practical use case is managing a data perimeter across hundreds of accounts without auditing every individual bucket policy by hand. For large enterprises or regulated environments in healthcare, finance, or government, running many accounts under a single AWS Organization, this is a meaningful operational upgrade.
In August 2025, AWS Security Blog updated data perimeter guidance to include the new aws:VpceAccount condition key. This replaces the need to enumerate individual VPC and VPC endpoint IDs in bucket policies, which reduces policy size and simplifies auditing as infrastructure scales across regions and accounts.
Enforcing VPC Endpoint Access Through Bucket Policies
VPC Gateway Endpoints for S3 let traffic move between a VPC and S3 without touching the public internet. No NAT gateway, no VPN, no egress over public infrastructure.
Bucket policies are how you enforce that this private path is the only path. Using aws:SourceVpc or aws:SourceVpce condition keys, you can deny any request that doesn't arrive through a specified VPC or endpoint. For compute-heavy workloads, this matters on two levels: you get the performance and cost benefits of private network paths, and you get a policy-level guarantee that no other path can reach the data.
The friction at scale is real though. aws:SourceVpc and aws:SourceVpce require you to enumerate every VPC and endpoint ID. Every time infrastructure changes, your bucket policies need updating. As data processing scales across regions and accounts, this becomes a genuine maintenance burden.
The RCP-based alternative using aws:VpceOrgID (available since August 2025) addresses this directly. Instead of listing individual IDs, you restrict to any endpoint owned by accounts in your organization. The list maintains itself.
This is a natural example of bucket policies and RCPs working together. The bucket policy handles per-bucket logic. The RCP handles the org-wide floor. Neither is doing the other's job.
Keeping Permissions Auditable as Things Get Complicated
Here's the compounding problem. As teams add roles, cross-account relationships, VPC conditions, and org-level RCPs, effective permissions become the product of multiple interacting policies that no single document captures. You can't just open one JSON file and understand what's actually allowed.
A few disciplines that hold up well as configurations grow:
- Use IAM Access Analyzer. It identifies S3 buckets accessible from outside your account or organization. As of recent updates, it also surfaces unused access findings, flagging roles and policies with permissions that haven't been exercised. External access detection and least-privilege auditing in one tool.
- Always read the bucket policy and the calling identity's IAM policy together. For org-level environments, also check applicable SCPs and RCPs. Effective permissions live at the intersection of all of these.
- Verify both sides of every cross-account grant. Confirm the bucket policy exists on the owner's side and the IAM policy exists on the caller's side. A one-sided grant is a silent failure. No error message, just denied.
- Audit ACLs on existing buckets. If ACLs are still active, migrate to bucket policy and IAM before they create confusion in future access reviews.
- Watch your IAM policy size. If an IAM policy is approaching the 5 KB limit because of complex S3 rules, that's often a signal. Some of that logic probably belongs in a bucket policy or an RCP instead.
The permissions model for S3 is genuinely not that complicated when you hold onto the core idea: two attachment points, same evaluation engine, explicit Deny always wins. Everything else is just knowing which tool reaches which part of the problem.


