Skip to content
AWS aws storage 5 min read

S3 Bucket Policies & Access Control

Amazon S3 (Simple Storage Service, AWS’s object storage) gives you several overlapping ways to say “who is allowed to do what” with your data. Getting access control right is the single most important thing you can do with S3, because a misconfigured bucket is the classic cause of public data leaks. This page explains the three layers — IAM identity policies, resource-based bucket policies, and legacy ACLs (Access Control Lists) — how they combine into one yes-or-no decision, and the two gotchas that trip up almost everyone.

The three layers of S3 access control

S3 access can be granted in three different places. They all matter at the same time, and S3 evaluates all of them together.

LayerAttached toBest for
IAM identity policyA user, group, or roleControlling what your own AWS principals can do across many buckets
Bucket policy (resource-based)The bucket itselfGranting access to other accounts, or setting broad rules for one bucket
ACL (legacy)Bucket or individual objectAlmost never — AWS now recommends keeping ACLs disabled

An IAM (Identity and Access Management) policy lives on the identity — a user or role. It says “this user may call s3:GetObject on these buckets.” Use it when you manage the principal and want one place to control their access to many resources.

A bucket policy lives on the resource — the bucket. It says “anyone matching this rule may do these actions on this bucket.” Use it when you need to grant access to a different AWS account, or apply a rule that should hold no matter which user shows up (for example, “deny any request that is not encrypted in transit”).

An ACL is the original 2006-era mechanism. It is coarse-grained and hard to audit. AWS now ships new buckets with Object Ownership set to “Bucket owner enforced,” which disables ACLs entirely. Leave it that way unless a legacy system truly requires ACLs.

When NOT to use ACLs: for essentially all new work. Modern designs use bucket policies plus IAM policies. ACLs exist mainly for backward compatibility.

How the layers combine

S3 merges every applicable policy into one decision using two simple rules:

  1. An explicit Deny always wins. If any policy says Deny, the request is denied — full stop.
  2. Otherwise, you need at least one Allow. With no matching Allow anywhere, the default is deny.

So a request succeeds only when something allows it and nothing denies it. For access within your own account, an Allow in either the IAM policy or the bucket policy is enough.

The bucket ARN vs object ARN gotcha

This is the mistake everyone makes. In a policy you reference S3 resources by their ARN (Amazon Resource Name, a unique identifier for an AWS resource), and there are two different ARNs for one bucket:

ARNWhat it meansUse for actions like
arn:aws:s3:::my-bucketThe bucket itselfs3:ListBucket, s3:GetBucketLocation
arn:aws:s3:::my-bucket/*Every object inside the buckets3:GetObject, s3:PutObject, s3:DeleteObject

Bucket-level actions (like listing contents) need the bucket ARN with no /*. Object-level actions (reading and writing files) need the /* ARN. If you grant s3:GetObject on arn:aws:s3:::my-bucket (no slash-star), downloads will fail with Access Denied even though the policy “looks” correct. Many real policies list both ARNs in one statement to cover both kinds of action.

Writing a bucket policy

Here is a policy that lets a specific IAM role read every object in devcraftly-assets and list the bucket, while denying any request that does not use HTTPS.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowRoleReadAccess",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::111122223333:role/AppReadRole"
      },
      "Action": ["s3:GetObject", "s3:ListBucket"],
      "Resource": [
        "arn:aws:s3:::devcraftly-assets",
        "arn:aws:s3:::devcraftly-assets/*"
      ]
    },
    {
      "Sid": "DenyInsecureTransport",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::devcraftly-assets",
        "arn:aws:s3:::devcraftly-assets/*"
      ],
      "Condition": { "Bool": { "aws:SecureTransport": "false" } }
    }
  ]
}

Note how both ARNs appear, and how the Deny statement protects every request regardless of who makes it.

Apply it via the console

  1. Open the S3 console and click your bucket name.
  2. Go to the Permissions tab.
  3. Scroll to Bucket policy and choose Edit.
  4. Paste the JSON, then choose Save changes.

Apply it via the AWS CLI

aws s3api put-bucket-policy \
  --bucket devcraftly-assets \
  --policy file://bucket-policy.json

This command prints nothing on success. To confirm it was stored:

aws s3api get-bucket-policy --bucket devcraftly-assets --output text

Output:

{"Version":"2012-10-17","Statement":[{"Sid":"AllowRoleReadAccess","Effect":"Allow","Principal":{"AWS":"arn:aws:iam::111122223333:role/AppReadRole"},"Action":["s3:GetObject","s3:ListBucket"],"Resource":["arn:aws:s3:::devcraftly-assets","arn:aws:s3:::devcraftly-assets/*"]}, ... ]}

Block Public Access overrides everything

S3 has an account-wide and per-bucket safety switch called Block Public Access (BPA). When BPA is on (the default for all new buckets since 2023), it overrides any bucket policy or ACL that would make data public. This is by design and it is your friend.

So if you write a bucket policy with "Principal": "*" and "Effect": "Allow" to make objects publicly readable, but BPA is enabled, S3 will still block the public access and the console will warn you. The policy is not “wrong” — BPA simply wins. To intentionally host public content (for example a static website), you must first turn off the relevant BPA settings on that specific bucket. Never disable BPA account-wide just to fix one bucket.

Gotcha: A bucket policy that grants public access does nothing while Block Public Access is enabled. If your “public” objects return Access Denied, check BPA before debugging the policy itself.

To check BPA from the CLI:

aws s3api get-public-access-block --bucket devcraftly-assets

Output:

{
    "PublicAccessBlockConfiguration": {
        "BlockPublicAcls": true,
        "IgnorePublicAcls": true,
        "BlockPublicPolicy": true,
        "RestrictPublicBuckets": true
    }
}

Best Practices

  • Keep Block Public Access enabled everywhere except buckets that genuinely need to be public, and disable it only at the bucket level.
  • Disable ACLs by keeping Object Ownership = Bucket owner enforced; manage access with policies instead.
  • Always include both the bucket ARN and the /* object ARN when a statement covers listing and object actions.
  • Add a deny-on-insecure-transport statement to every bucket to force HTTPS.
  • Grant the least privilege — name specific actions and principals rather than s3:* and "*".
  • Prefer IAM roles over long-lived access keys, and use bucket policies mainly for cross-account or bucket-wide guardrails.
  • Use the S3 console Access Analyzer findings to catch any bucket that has become unintentionally public.
Last updated June 15, 2026
Was this helpful?