"S3 bucket policy for static website" and "for CloudFront" are two different jobs. Here are both, and why the second is usually the right one.
Option A — direct public read (static website endpoint)
The bucket serves objects to anyone. Simple, but every object is world-readable.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-site/*"
},
{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": ["arn:aws:s3:::my-site", "arn:aws:s3:::my-site/*"],
"Condition": { "Bool": { "aws:SecureTransport": "false" } }
}
]
}
You must also turn off the relevant Block Public Access settings — and accept that anyone can enumerate and fetch objects directly.
Option B — CloudFront + OAC (keep the bucket private)
The modern pattern: the bucket is private, and only your CloudFront distribution can read it via Origin Access Control (OAC). Users hit CloudFront, never S3 directly.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": { "Service": "cloudfront.amazonaws.com" },
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-site/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::111122223333:distribution/E1XXXXXXXXXXXX"
}
}
}
]
}
The Principal is the CloudFront service, and the SourceArn condition locks it to one distribution — so even other CloudFront distributions can't read the bucket. Block Public Access stays fully on.
Which to pick
- Prototype / throwaway → Option A is fine.
- Anything real → Option B. You get TLS, caching, WAF, and no directly-reachable objects. Use OAC, not the older Origin Access Identity (OAI).
Tweak it safely
Open the S3 bucket policy template and ask "convert this to a CloudFront OAC origin for distribution E123…" — the workspace rewrites the Principal and condition and shows you the diff before you apply it.