DevTools Hub

Search tools

Search for a developer tool

How to Write an S3 Bucket Policy

Part of the AWS Toolkit
Pattern
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":"*","Action":"s3:GetObject","Resource":"arn:aws:s3:::my-bucket/*"}]}

Principal is required — the field that makes this a resource-based policy, not an identity-based one

Explanation

An S3 bucket policy is a resource-based policy — attached directly to the bucket, not to a user, group, or role. That distinction is what makes Principal required: an identity-based IAM policy is already scoped to whoever it's attached to, so it never needs to say who it applies to. A bucket policy has no identity to inherit that scope from, so every statement has to name one explicitly — "*" for everyone, { "Service": "..." } for an AWS service like CloudFront, or { "AWS": "arn:...:root" } for another account.

Three patterns cover most real bucket policies. Public read grants s3:GetObject to Principal: "*" — the static-website and public-asset case. CloudFront Origin Access Control grants the cloudfront.amazonaws.com service principal read access, restricted by a Condition matching your specific distribution's ARN, so no other distribution — yours or anyone else's — can use the grant. And cross-account access grants another account's root principal a set of actions, which that account can then delegate internally to its own users and roles through its own IAM policies — the bucket policy alone isn't enough on the other side.

Two things trip people up after the JSON itself is correct. First, S3's Block Public Access settings sit in front of the bucket policy and can override it entirely — a syntactically perfect public-read policy does nothing while Block Public Access is still on for the relevant settings at the bucket or account level. Second, when a statement grants an object-level action like GetObject, the Resource has to be the object ARN pattern (bucket/*), not the bucket ARN alone — the bucket ARN has no trailing /* and matches zero actual objects, so the grant silently does nothing rather than erroring.

A policy can hold more than one statement, and AWS evaluates all of them together: an explicit Deny anywhere always wins over an Allow, regardless of which statement comes first. That's how a baseline requirement like blocking plain HTTP gets layered on top of any of the recipes above — one Allow statement for the actual access pattern, plus a separate Deny statement checking aws:SecureTransport.

For the identity-based side of access control — policies attached to users, groups, and roles instead of resources — see IAM Policy Basics, and build one with IAM Policy Generator.

Valid examples

  • "Principal": { "Service": "cloudfront.amazonaws.com" }, "Condition": { "StringEquals": { "AWS:SourceArn": "arn:aws:cloudfront::111122223333:distribution/EDFDVBD6EXAMPLE" } }

    CloudFront Origin Access Control — restricted to one specific distribution via the Condition block.

  • "Principal": { "AWS": "arn:aws:iam::123456789012:root" }

    Cross-account access — the other account can then delegate this internally through its own IAM policies.

  • "Effect": "Deny", "Principal": "*", "Action": "s3:*", "Condition": { "Bool": { "aws:SecureTransport": "false" } }

    A second statement denying any request over plain HTTP — layered on top of any Allow statement, not a replacement for one.

Invalid examples

  • { "Effect": "Allow", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::my-bucket/*" }

    No Principal — valid in an IAM identity policy, rejected as malformed in a bucket policy, which always requires one.

  • "Action": "s3:GetObject", "Resource": "arn:aws:s3:::my-bucket"

    GetObject is object-level; the bucket ARN with no trailing /* matches zero objects, so the grant silently does nothing.

  • Bucket policy: Principal "*" — Block Public Access: still ON

    Block Public Access sits in front of the bucket policy and overrides it — nothing becomes public until it's off too.

Try it now