Skip to content
C Codeloom
DevOps

AWS S3 Bucket Policies Explained

How S3 bucket policies, IAM policies, and ACLs interact, how to write least-privilege bucket policies, and patterns for cross-account access without footguns.

·5 min read · By Codeloom
Intermediate 10 min read

What you'll learn

  • How S3 evaluates requests across IAM, bucket, and SCP layers
  • Writing least-privilege bucket policies with conditions
  • Block Public Access and why it overrides almost everything
  • Cross-account access via assume-role and bucket policies
  • Common bucket policy pitfalls and audit tactics

Prerequisites

  • Basic AWS familiarity

What and why

An S3 bucket policy is a JSON document attached to a bucket that grants or denies access to that bucket and its objects. It is one of three places where S3 permissions live: IAM policies on principals, bucket policies on resources, and the legacy ACL system.

You need bucket policies when access decisions are about the bucket more than the principal: cross-account reads, public website hosting, replicating from another account, or enforcing TLS on every request. They are also the right tool for “deny” rules that override any allow.

Mental model

A request to S3 succeeds only if at least one policy allows it and no policy denies it. The evaluation considers SCPs (organization-wide), identity policies (on the caller’s IAM role), resource policies (the bucket policy), and Block Public Access (a global override).

PutObject request from role X to bucket B
      |
      v
+----------------------+
| Block Public Access  |  -- if it matches a public grant, DENY
+----------+-----------+
         |
         v
+----------------------+
| Explicit Deny check  |  -- any Deny in any policy wins
+----------+-----------+
         |
         v
+----------------------+
| SCP allow            |  -- org guardrails must allow
+----------+-----------+
         |
         v
+----------------------+
| IAM identity allow   |  -- role's policies must allow
+----------+-----------+
         |
         v
+----------------------+
| Bucket policy allow  |  -- bucket must allow (same-account: either side)
+----------+-----------+
         |
         v
        ALLOW
S3 request evaluation order

Hands-on example

A bucket policy that forces TLS, denies unencrypted uploads, and allows a specific role to read and write:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyInsecureTransport",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::acme-uploads",
        "arn:aws:s3:::acme-uploads/*"
      ],
      "Condition": {
        "Bool": { "aws:SecureTransport": "false" }
      }
    },
    {
      "Sid": "DenyUnencryptedUploads",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::acme-uploads/*",
      "Condition": {
        "StringNotEquals": {
          "s3:x-amz-server-side-encryption": "aws:kms"
        }
      }
    },
    {
      "Sid": "AppRoleReadWrite",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::111122223333:role/uploader"
      },
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:::acme-uploads/*"
    }
  ]
}

The two Deny statements apply to every principal and protect the bucket against misconfigured clients. The Allow is narrow: one role, three actions, on object keys only (no bucket-level actions like ListBucket unless you add them explicitly).

For cross-account access, the bucket policy in account A trusts the role from account B, and account B’s IAM policy on that role lets it call S3.

{
  "Sid": "CrossAccountRead",
  "Effect": "Allow",
  "Principal": { "AWS": "arn:aws:iam::222233334444:role/data-importer" },
  "Action": ["s3:GetObject", "s3:ListBucket"],
  "Resource": [
    "arn:aws:s3:::acme-shared-exports",
    "arn:aws:s3:::acme-shared-exports/*"
  ]
}

Account B’s role needs its own IAM policy granting the same actions; both sides must allow.

For a static website behind CloudFront with Origin Access Control, only CloudFront should reach S3:

{
  "Sid": "AllowCloudFrontOnly",
  "Effect": "Allow",
  "Principal": { "Service": "cloudfront.amazonaws.com" },
  "Action": "s3:GetObject",
  "Resource": "arn:aws:s3:::acme-site/*",
  "Condition": {
    "StringEquals": {
      "AWS:SourceArn": "arn:aws:cloudfront::111122223333:distribution/EXAMPLE"
    }
  }
}

Then keep Block Public Access fully on.

Common pitfalls

Treating the bucket policy as the only line of defense. Without Block Public Access on, a single "Principal": "*" typo opens the bucket to the world. Always leave BPA enabled.

Mixing object-level and bucket-level resource ARNs. s3:ListBucket needs the bucket ARN (arn:aws:s3:::name), not the object ARN (arn:aws:s3:::name/*). The opposite is true for s3:GetObject.

Using "Principal": "*" with a condition you think narrows it enough. Conditions can be circumvented if mis-scoped; explicit principals are safer.

Forgetting that ACLs still exist and can grant access independently. The “Object Ownership” setting (BucketOwnerEnforced) disables ACLs entirely. Turn it on for any bucket you own end-to-end.

Confusing aws:SourceIp with aws:VpcSourceIp. From inside a VPC endpoint, aws:SourceIp shows the private address. Use aws:SourceVpce or aws:SourceVpc for VPC restrictions.

Production tips

Use IAM Access Analyzer to scan bucket policies before applying them. It flags public, cross-account, and overly broad grants.

Tag buckets by data sensitivity and write SCPs that deny s3:PutBucketPolicy with public principals on tagged buckets. This stops anyone, including admins, from accidentally regressing.

Prefer KMS keys with explicit grants over default SSE-S3 for sensitive data. The KMS key policy adds another layer of “who can decrypt these objects.”

Log every request. Enable S3 server access logging or CloudTrail data events for the bucket. When something leaks, you will want the audit trail.

Rotate any role with s3:* privileges into narrower scopes. The most common over-privilege is s3:* on a single bucket; nearly every job needs only a subset.

Test policies with the IAM policy simulator before applying. Catching a deny in the simulator is cheaper than catching it in a 3 AM page.

Wrap-up

S3 bucket policies are resource-side permissions evaluated together with IAM, SCPs, BPA, and ACLs. Use bucket policies for cross-account access, blanket denies, and TLS/encryption enforcement. Keep Block Public Access on, enable BucketOwnerEnforced, log everything, scan with Access Analyzer, and prefer explicit principals over wildcards. Done that way, S3 stays a vault instead of an open share.