An S3 bucket policy is a JSON document you attach directly to a bucket to control who can do what with it. It looks like an IAM policy but with one key addition: a Principal.
The shape
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadPublicAssets",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-public-assets/*"
}
]
}
Field by field
- Version — always
"2012-10-17". Not a date you edit. - Effect —
"Allow"or"Deny". A Deny always wins over any Allow. - Principal — this is the S3-specific part. Who the statement applies to.
"*"= anyone on the internet.{ "AWS": "arn:aws:iam::111122223333:role/app" }scopes it to one identity. - Action — the S3 API operations, e.g.
s3:GetObject,s3:PutObject,s3:ListBucket. Wildcards likes3:*are allowed but rarely a good idea. - Resource — the ARNs the actions apply to.
- Condition (optional) — tests that must be true, e.g. require HTTPS or a specific source.
The two-ARN rule
The single most common bucket-policy bug: using one ARN for actions that need two.
- ❌
ListBucketonarn:aws:s3:::my-bucket/*— fails, list acts on the bucket, not objects. - ✅
ListBucketonarn:aws:s3:::my-bucketandGetObjectonarn:aws:s3:::my-bucket/*.
Bucket-level actions use the bare bucket ARN; object-level actions use /*.
Always deny insecure transport
Any public or shared bucket should pair its Allow with a deny for plain HTTP:
{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-public-assets",
"arn:aws:s3:::my-public-assets/*"
],
"Condition": { "Bool": { "aws:SecureTransport": "false" } }
}
Verify before you ship
Public read means everyone can fetch every object — never store secrets in such a bucket, and review with S3 Access Analyzer first.
Open the S3 bucket policy template in the workspace and ask "explain each statement" — the AI narrates every field against the values actually present, so you can see exactly what your policy grants.