
Bucket policy vs IAM policy is a distinction that trips people up because both are written in the same JSON policy language and both can grant or deny the exact same S3 permissions. The real difference isn't what they can express, it's what they're attached to. AWS's own documentation draws the line precisely: one is "a resource-based policy that is attached to an AWS resource, such as a bucket policy for a bucket," the other is "an identity-based policy that is attached to an AWS Identity and Access Management (IAM) identity, such as an IAM user, group, or role." That single difference in attachment point is what determines which one can actually do a specific job the other structurally cannot.
TL;DR:
Both policy types use the exact same JSON structure, the same actions, the same conditions. What differs is where the policy lives. An IAM policy lives on the identity making the request, it travels with that user, group, or role wherever it goes across AWS services. A bucket policy lives on the S3 bucket itself, it applies to that bucket regardless of which identity, inside or outside your account, is making the request.
AWS describes an identity-based policy as one you attach "to IAM users, groups, or roles in your account," used to "grant an IAM identity access to your buckets or objects." The practical implication is centralization: manage a user's entire AWS footprint, S3 included, from one place. AWS's own guidance names a specific scenario where this wins clearly: "if you have a very large number of buckets with different access permissions for each bucket, and only a few user roles to manage, you can use an IAM policy for users, groups, or roles," rather than maintaining a separate bucket policy on every single bucket.
A bucket policy is, in AWS's own words, "a JSON-formatted IAM resource-based policy that is attached to a particular bucket," used to "grant other AWS accounts or IAM identities permissions for the bucket and the objects in it." Because it lives on the resource rather than an identity in your account, it can name principals your IAM system has no relationship with at all, another AWS account entirely, a specific AWS service, or, if written that way, the public.
This is the clearest, least ambiguous dividing line between the two, and AWS states it as a hard rule rather than a soft recommendation: "to grant permission to another AWS account, you must use a bucket policy or one of the following recommended access management tools. You cannot use an identity-based access policy for this use case." An IAM policy, no matter how it's written, only governs identities inside your own account. If the goal is letting a completely separate AWS account read from or write to your bucket, a bucket policy isn't the better option, it's structurally the only option among these two.
A common misconception treats these as alternatives you pick between. AWS's documentation describes the actual evaluation model directly: "when Amazon S3 receives a request, it must evaluate all of the access policies to determine whether to authorize or deny the request." Every applicable policy, IAM policies attached to the requesting identity and any bucket policy on the target bucket, gets evaluated together on every single request. That means the two aren't competing tools, they're layers that combine, which is also exactly why a permission that looks correct in one policy can still fail if a conflicting deny exists in the other.
For anything with genuinely complex permission requirements, this is a real practical ceiling, not a theoretical one. AWS states plainly that "both bucket policies and identity-based policies have a 20 KB size limit." A sufficiently complex set of conditions, principals, and resource ARNs can actually hit that ceiling, at which point AWS's own guidance is to look at other access management tools, like IAM groups or S3 Access Points, rather than trying to cram everything into a single oversized policy document.
The "evaluate all policies together" rule has a precise shape, and it differs depending on whose account the requester lives in.
Same account: the union, minus any explicit deny. When an IAM user or role requests a resource in its own account, AWS's IAM documentation says the resulting permissions are "the union of the permissions of the two types. If an action is allowed by an identity-based policy, a resource-based policy, or both, then AWS allows the action. An explicit deny in either of these policies overrides the allow." So inside one account, an IAM policy that allows s3:GetObject is enough on its own, and so is a bucket policy that names that user. Either grants access, and a Deny in either one blocks it.
Cross-account: both sides have to agree. S3's authorization documentation spells this out: "To perform a specific operation on a resource, an IAM principal needs permission from both the parent AWS account to which it belongs and the AWS account that owns the resource." In practice, the bucket owner's bucket policy has to grant the other account access, and an administrator in that other account still has to allow the specific user or role through an IAM policy. Granting in only one place gives you 403 Access Denied, which is the most common reason a freshly written cross-account bucket policy "doesn't work."
S3 checks this in three contexts, in order: the user context (the parent account's policies on the user), the bucket context (the bucket owner's policies, including any explicit deny), and the object context (the object owner's policies). For an operation on an object the bucket owner doesn't own, S3 still checks the bucket policy for an explicit deny, because AWS's documentation says the bucket owner can explicitly deny access to objects in the bucket regardless of who owns them.
Other policy layers only ever narrow this. A permissions boundary on the identity gives "the intersection" with its identity-based policies, and when an account belongs to an AWS Organization, service control policies and resource control policies can limit the result further. The rule that governs all of it is the same: an explicit deny anywhere wins.
Say account 111122223333 owns a bucket named shared-reports, and a role in account 444455556666 needs to read from it. The bucket owner attaches a resource-based policy to the bucket:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowPartnerAccountRead",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::444455556666:root" },
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::shared-reports",
"arn:aws:s3:::shared-reports/*"
]
}
]
}That covers the bucket side. The other account's administrator then attaches an identity-based policy to the role, which has no Principal element because the identity it's attached to is the principal:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::shared-reports",
"arn:aws:s3:::shared-reports/*"
]
}
]
}Two details in these documents cause a lot of denied requests. First, s3:ListBucket acts on the bucket itself, so it needs the bucket ARN (arn:aws:s3:::shared-reports), while s3:GetObject acts on objects and needs the /* form. Listing both ARNs, as above, is the simple way to avoid missing one. Second, the resource-based policy needs a Principal naming who's being granted access, and the identity-based policy doesn't, which is a fast way to tell which type of policy you're looking at.
Because an explicit deny always wins, a Deny statement is the right tool for rules that must hold no matter what other policies say. AWS's own example bucket policies include one that rejects unencrypted connections using the aws:SecureTransport condition key, which is false when a request was sent over HTTP:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "RestrictToTLSRequestsOnly",
"Action": "s3:*",
"Effect": "Deny",
"Resource": [
"arn:aws:s3:::shared-reports",
"arn:aws:s3:::shared-reports/*"
],
"Condition": {
"Bool": { "aws:SecureTransport": "false" }
},
"Principal": "*"
}
]
}A statement like this belongs on the bucket rather than in each user's IAM policy, because it applies to every requester, including ones you haven't thought of yet. That's the case where a resource-based policy is genuinely the better place for a rule.
One condition key to be careful with: AWS warns that aws:Referer shouldn't be used to keep unauthorized parties out, since "unauthorized parties can use modified or custom browsers to provide any aws:Referer value that they choose." It's meant to stop other sites from hotlinking your content, not to act as access control.
If a request fails even though a policy appears to allow it, work through the layers in order:
Deny somewhere? Check the bucket policy, the identity's policies, any permissions boundary, and any organization-level SCP. One deny beats every allow.s3:ListBucket need the bucket ARN, and object-level actions like s3:GetObject need the /* ARN.s3:ListAllMyBuckets, s3:GetBucketLocation and s3:ListBucket.AIOZ Storage doesn't implement either of these mechanisms. Its access-control model is built on macaroons, bearer tokens carrying their own cryptographic proof of authorization, scoped to exactly four actions per bucket, rather than a JSON policy document attached to either an identity or a resource. That's a genuinely different architecture, closer to capability-based access control than to either the identity-based or resource-based model this article covers, worth knowing plainly if you're coming to AIOZ Storage expecting to write an IAM-style or bucket-policy-style JSON document, since that's not the shape its permission system takes.
What's the difference between a bucket policy and an IAM policy?
Attachment point. An IAM policy attaches to a user, group, or role and travels with that identity across AWS services. A bucket policy attaches directly to a bucket and applies to any request against it, regardless of which identity is making the request.
Can an IAM policy grant access to another AWS account?
No. AWS states directly that identity-based policies cannot be used for cross-account access, only a bucket policy (or a similar resource-based tool) can grant permissions to a completely separate AWS account.
Does S3 use the bucket policy or the IAM policy, not both?
Both, together. S3 evaluates every applicable policy on each request, IAM policies attached to the requester and any bucket policy on the target bucket, rather than picking one and ignoring the other.
What's the size limit on these policies?
20 KB for both bucket policies and identity-based (IAM) policies, per AWS's own documentation. Genuinely complex permission requirements can actually hit that ceiling.
When should you use an IAM policy instead of a bucket policy?
When you're managing a large number of buckets with a small number of user roles, centralizing permissions in IAM is simpler than maintaining a separate bucket policy per bucket, and when you also need to manage that identity's access to non-S3 AWS services from one place.
When should you use a bucket policy instead of an IAM policy?
When you need to grant access to a different AWS account, a specific AWS service principal, or you want to control access based on something outside AWS IAM identity entirely, like a requester's IP address.
If the bucket policy allows access but my IAM policy doesn't, is the request allowed?
Within the same account, yes. AWS evaluates the union of identity-based and resource-based policies, so an allow in either one is enough unless there's an explicit deny. For a cross-account request the answer is no, because the requester needs permission from both its own account and the account that owns the bucket.
Does an explicit deny override an allow in S3?
Yes. An explicit Deny in any applicable policy overrides every allow, whether it sits in a bucket policy, an IAM policy, a permissions boundary or an organization SCP.
How do I force HTTPS-only access to an S3 bucket?
Add a bucket policy statement with Effect: Deny, Principal: "" and a Bool condition where aws:SecureTransport is "false", applied to both the bucket ARN and the / object ARN. That denies any request sent over plain HTTP.
Why do I get Access Denied when my IAM policy allows s3:GetObject?
Check for an explicit deny elsewhere, confirm the bucket policy also grants access if the request is cross-account, and make sure the policy's Resource uses the bucket/* ARN for object actions. A permissions boundary or SCP can also be silently narrowing what the IAM policy grants.
Do bucket policies need a Principal element?
Yes, a resource-based policy has to say who it applies to. An identity-based IAM policy doesn't include a Principal because the identity it's attached to is the principal.
Does AIOZ Storage use bucket policies or IAM policies?
Neither. AIOZ Storage's access-control model is built on macaroons, bearer tokens with their own embedded authorization, a capability-based design rather than either the identity-based or resource-based JSON policy model this article covers.

AIOZ Storage Super Intelligence Memory gives AI agents memory: Record a statement, Recall it by meaning, with no embeddings or vector index to manage yourself.

Static website hosting on AIOZ Storage turns any bucket into a live site with a custom domain. Here is the full setup, from bucket to DNS verification.

An embedding model turns text, images, or audio into vectors so similar meanings land close together in space. Here is how embedding models actually work.

Retrieval-augmented generation (RAG) grounds an LLM answer in your own data, not its training set. Here is how the retrieval and generation steps work.

AIOZ Storage has no official MCP server yet. Here is a working MCP server for AIOZ Storage, built with the official Python SDK and boto3, tools included.

What is an MCP server: a standard letting AI agents like Claude use tools and data through one protocol. How MinIO and Azure apply it to object storage.