The Problem
S3 bucket policies are JSON documents that control who can access your buckets and objects. The syntax is confusing, the interaction with IAM policies creates unexpected behavior, and a misconfigured policy can either lock you out of your own bucket or expose sensitive data to the internet.
This guide provides copy-paste-ready bucket policies for the scenarios you will encounter in production.
How Bucket Policies Work
A bucket policy evaluates alongside IAM policies. The rules:
- An explicit Deny always wins, regardless of any Allow
- If there is no explicit Deny, an explicit Allow grants access
- If there is neither, access is implicitly denied (default)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DescriptiveName",
"Effect": "Allow or Deny",
"Principal": "Who",
"Action": "What operations",
"Resource": "Which objects",
"Condition": {}
}
]
}
Public Read Access (Static Website Hosting)
Allow anyone to read objects — use for static websites and CDN origins:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-website-bucket/*"
}
]
}
You must also disable S3 Block Public Access settings:
aws s3api put-public-access-block \
--bucket my-website-bucket \
--public-access-block-configuration \
BlockPublicAcls=false,IgnorePublicAcls=false,BlockPublicPolicy=false,RestrictPublicBuckets=false
Public read for specific prefix only
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadForAssets",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket/public/*"
}
]
}
Cross-Account Access
Allow another AWS account to read objects
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CrossAccountRead",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111222333444:root"
},
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-shared-bucket",
"arn:aws:s3:::my-shared-bucket/*"
]
}
]
}
Allow a specific role in another account
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CrossAccountRoleAccess",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111222333444:role/DataPipelineRole"
},
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::shared-data-bucket",
"arn:aws:s3:::shared-data-bucket/partner-uploads/*"
]
}
]
}
Cross-account with bucket owner enforced
When another account uploads objects, they own those objects by default. Force bucket owner ownership:
aws s3api put-bucket-ownership-controls \
--bucket shared-data-bucket \
--ownership-controls Rules=[{ObjectOwnership=BucketOwnerEnforced}]
IP-Based Restrictions
Allow access only from your office/VPN IPs
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowFromOfficeOnly",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::confidential-bucket",
"arn:aws:s3:::confidential-bucket/*"
],
"Condition": {
"NotIpAddress": {
"aws:SourceIp": [
"203.0.113.0/24",
"198.51.100.0/24"
]
},
"StringNotEquals": {
"aws:PrincipalServiceName": "cloudtrail.amazonaws.com"
}
}
}
]
}
Note the Deny with NotIpAddress pattern — this denies all requests NOT from the specified IPs.
Allow from specific VPC endpoint only
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyNonVPCAccess",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::internal-bucket",
"arn:aws:s3:::internal-bucket/*"
],
"Condition": {
"StringNotEquals": {
"aws:sourceVpce": "vpce-0abc123def456"
}
}
}
]
}
Enforce Encryption
Deny unencrypted uploads (require SSE-S3)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyUnencryptedUploads",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::secure-bucket/*",
"Condition": {
"StringNotEquals": {
"s3:x-amz-server-side-encryption": "AES256"
}
}
},
{
"Sid": "DenyNoEncryptionHeader",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::secure-bucket/*",
"Condition": {
"Null": {
"s3:x-amz-server-side-encryption": "true"
}
}
}
]
}
Require KMS encryption
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "RequireKMSEncryption",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::sensitive-bucket/*",
"Condition": {
"StringNotEquals": {
"s3:x-amz-server-side-encryption": "aws:kms"
}
}
}
]
}
Enforce HTTPS Only
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
}
}
]
}
CloudFront Origin Access
Allow only CloudFront to read from your S3 bucket:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-website-bucket/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/EDFDVBD6EXAMPLE"
}
}
}
]
}
Combined Production Policy
A production bucket typically combines several restrictions:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "EnforceHTTPS",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::prod-data-bucket",
"arn:aws:s3:::prod-data-bucket/*"
],
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
}
},
{
"Sid": "EnforceEncryption",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::prod-data-bucket/*",
"Condition": {
"Null": {
"s3:x-amz-server-side-encryption": "true"
}
}
},
{
"Sid": "AllowDataTeam",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/DataEngineerRole"
},
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::prod-data-bucket",
"arn:aws:s3:::prod-data-bucket/*"
]
}
]
}
Applying Bucket Policies
# Apply a policy from a file
aws s3api put-bucket-policy \
--bucket my-bucket \
--policy file://bucket-policy.json
# View current policy
aws s3api get-bucket-policy --bucket my-bucket | jq '.Policy | fromjson'
# Delete bucket policy
aws s3api delete-bucket-policy --bucket my-bucket
# Validate policy syntax
aws accessanalyzer validate-policy \
--policy-document file://bucket-policy.json \
--policy-type RESOURCE_POLICY
Common Mistakes
s3:ListBucket needs the bucket ARN (arn:aws:s3:::bucket), while s3:GetObject needs the object ARN (arn:aws:s3:::bucket/*). Missing either causes Access Denied.Principal: "*" with Allow — This makes the bucket publicly accessible. Always pair with a Condition unless you genuinely want public access.s3:ListBucket separately — Listing contents requires s3:ListBucket on the bucket resource. Object operations are on the object resource with /*.Quick Reference
| Scenario | Key Elements |
|---|---|
| Public read | <code class="inline-code">Principal: "*"</code>, <code class="inline-code">Action: "s3:GetObject"</code> |
| Cross-account | <code class="inline-code">Principal: { AWS: "arn:aws:iam::ACCOUNT:root" }</code> |
| IP restriction | <code class="inline-code">Condition: { NotIpAddress: { aws:SourceIp: [...] } }</code> |
| VPC endpoint only | <code class="inline-code">Condition: { StringNotEquals: { aws:sourceVpce: "vpce-..." } }</code> |
| Enforce encryption | Deny PutObject without <code class="inline-code">s3:x-amz-server-side-encryption</code> |
| HTTPS only | Deny when <code class="inline-code">aws:SecureTransport: "false"</code> |
| CloudFront only | <code class="inline-code">Principal: { Service: "cloudfront.amazonaws.com" }</code> |
| Prevent delete | Deny <code class="inline-code">s3:DeleteObject</code> for all except admin role |
Summary
S3 bucket policies enforce access control at the resource level. Start with HTTPS enforcement and encryption requirements on every bucket. Add cross-account or public access only where needed, with the most restrictive conditions possible. Always test policies by assuming the target role and verifying access before relying on them in production.
---
Frequently Asked Questions
What is the difference between S3 bucket policies and IAM policies?
S3 bucket policies are resource-based policies attached directly to buckets that control access for any principal, including cross-account access. IAM policies are attached to users or roles within a single account. Use bucket policies for public access rules, cross-account sharing, and VPC endpoint restrictions. Use IAM policies for controlling what your own users and services can access.
How do I make an S3 bucket completely private?
Enable S3 Block Public Access at both the account and bucket level, remove any bucket policy statements with "Principal": "*", and ensure no ACLs grant public access. Use aws s3api get-bucket-policy-status to verify the bucket is not public. For extra protection, add a bucket policy that explicitly denies all access except from specific IAM roles or VPC endpoints.
Why is my S3 bucket policy giving "Access Denied"?
Check that S3 Block Public Access settings are not overriding your policy, verify the Principal ARN is exactly correct, and look for explicit Deny statements that override Allows. Condition keys like aws:SourceVpc, aws:SourceIp, or s3:x-amz-server-side-encryption can silently block requests that don't match. Use CloudTrail to see the exact policy evaluation result.
How do I restrict S3 access to a specific VPC?
Add a bucket policy condition using aws:SourceVpce for VPC endpoint access or aws:SourceVpc for VPC-level restriction. Create a VPC Gateway Endpoint for S3 in your VPC, then add a Deny statement in the bucket policy for all requests not originating from your VPC endpoint. This ensures data never traverses the public internet.
Can I use S3 bucket policies for cross-account access?
Yes, add a bucket policy statement with the cross-account principal ARN (role or account) and the desired actions. The target account's role must also have IAM permissions to access S3. For cross-account access, both the bucket policy (resource-based) and the IAM policy on the caller must allow the action.
---
Related Resources
- Production Reference Architectures — Multi-region and serverless AWS architectures
- Certification Exam Prep — AWS certification exam prep guides
- AWS IAM Least Privilege Guide — Applying least privilege to all AWS resources
- AWS IAM Role Assume Guide — Cross-account role patterns for S3 access
- Secrets Management in DevOps — Protecting sensitive data stored in S3