AWS S3 Bucket Public Exposure: How It Happens and How to Fix It
S3 misconfiguration is one of the top causes of cloud data breaches. Learn how buckets become public, how to audit your entire AWS account, and how to lock them down permanently.
S3 misconfiguration has caused some of the largest data breaches in recent years: Capital One (100M records), Twitch source code, Facebook user data, and thousands of smaller incidents. In almost every case, the root cause was an S3 bucket made public unintentionally — either through a bucket policy, an ACL setting, or by disabling the Block Public Access setting.
This guide explains exactly how S3 buckets become public, how to audit your account, and how to prevent it from happening.
How S3 Buckets Become Public
There are three independent mechanisms that can make an S3 bucket or its objects public:
1. Block Public Access (account/bucket level)
AWS introduced Block Public Access (BPA) in 2018 as a safety net. It has four settings:
| Setting | What It Blocks |
|---|---|
BlockPublicAcls | Prevents new public ACLs from being set |
IgnorePublicAcls | Ignores existing public ACLs (overrides them) |
BlockPublicPolicy | Prevents bucket policies that grant public access |
RestrictPublicBuckets | Restricts access to the bucket if any public policy exists |
These can be set at the account level (applying to all buckets) or per-bucket. If any of these four are off, the corresponding public access mechanism is active.
Fix: Enable all four settings at the account level unless you have a specific bucket that needs public access:
aws s3control put-public-access-block \
--account-id YOUR_ACCOUNT_ID \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
2. Bucket ACLs (Access Control Lists)
ACLs are the legacy permission mechanism for S3. A bucket or object ACL with AllUsers or AuthenticatedUsers as the grantee exposes data publicly or to any authenticated AWS user.
Common culprit: old aws s3 sync --acl public-read commands that were copied from documentation without understanding the consequences.
Check for public ACLs:
# List all buckets
aws s3api list-buckets --query 'Buckets[].Name' --output text | tr '\t' '\n' | while read bucket; do
acl=$(aws s3api get-bucket-acl --bucket "$bucket" --query 'Grants[?Grantee.URI!=`null`]' --output text 2>/dev/null)
if [ -n "$acl" ]; then
echo "POSSIBLE PUBLIC ACL: $bucket"
echo "$acl"
fi
done
Fix: AWS recommends disabling ACLs entirely. With Block Public Access enabled and IgnorePublicAcls=true, existing ACLs are ignored even if you don't remove them. To fully disable ACLs on a bucket:
aws s3api put-bucket-ownership-controls \
--bucket my-bucket \
--ownership-controls Rules=[{ObjectOwnership=BucketOwnerEnforced}]
BucketOwnerEnforced disables ACLs for both the bucket and all objects.
3. Bucket Policies
A bucket policy with "Principal": "*" grants access to anyone on the internet.
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket/*"
}
This makes every object in the bucket publicly readable. This is the correct configuration for static website hosting buckets — and completely wrong for anything containing user data.
Check for public policies:
aws s3api get-bucket-policy --bucket my-bucket | python3 -m json.tool
Look for "Principal": "*" with Effect: Allow.
Auditing Your Entire AWS Account
Using AWS Config
AWS Config has built-in managed rules for S3:
s3-bucket-public-read-prohibited— Detects buckets allowing public reads3-bucket-public-write-prohibited— Detects buckets allowing public writes3-account-level-public-access-blocks-periodic— Checks account-level BPA settings
Using AWS Security Hub
Security Hub's AWS Foundational Security Best Practices standard includes S3 checks (S3.1 through S3.13). Enable it in the Security Hub console.
Manual Audit Script
#!/bin/bash
# Audit all S3 buckets for public access
for bucket in $(aws s3api list-buckets --query 'Buckets[].Name' --output text); do
echo "=== $bucket ==="
# Check Block Public Access
bpa=$(aws s3api get-public-access-block --bucket "$bucket" 2>/dev/null)
echo "Block Public Access: $bpa"
# Check bucket policy public-ness
public=$(aws s3api get-bucket-policy-status --bucket "$bucket" \
--query 'PolicyStatus.IsPublic' --output text 2>/dev/null)
echo "Policy IsPublic: $public"
# Check encryption
enc=$(aws s3api get-bucket-encryption --bucket "$bucket" \
--query 'ServerSideEncryptionConfiguration.Rules[0].ApplyServerSideEncryptionByDefault.SSEAlgorithm' \
--output text 2>/dev/null)
echo "Encryption: $enc"
echo ""
done
Additional S3 Security Controls
Encryption at Rest
All new S3 buckets have server-side encryption enabled by default (SSE-S3) as of January 2023. For sensitive data, use SSE-KMS with a customer-managed key:
aws s3api put-bucket-encryption --bucket my-bucket \
--server-side-encryption-configuration '{
"Rules": [{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": "arn:aws:kms:us-east-1:123456789:key/abc-123"
},
"BucketKeyEnabled": true
}]
}'
Versioning
Enable versioning to protect against accidental deletion and ransomware:
aws s3api put-bucket-versioning --bucket my-bucket \
--versioning-configuration Status=Enabled
Access Logging
Log all access to S3 buckets containing sensitive data:
aws s3api put-bucket-logging --bucket my-bucket \
--bucket-logging-status '{
"LoggingEnabled": {
"TargetBucket": "my-access-logs-bucket",
"TargetPrefix": "my-bucket/"
}
}'
Lifecycle Policies
Delete old versions and incomplete multipart uploads automatically:
aws s3api put-bucket-lifecycle-configuration --bucket my-bucket \
--lifecycle-configuration '{
"Rules": [{
"ID": "cleanup",
"Status": "Enabled",
"AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 7 },
"NoncurrentVersionExpiration": { "NoncurrentDays": 90 }
}]
}'
Incident Response: You Have a Public Bucket
If you discover a public bucket with sensitive data:
- Enable BPA immediately — This cuts off new access
- Review CloudTrail — Check
s3.amazonaws.comevents for downloads (GetObject) in the relevant time window - Check for data exfiltration — Look for high GetObject request counts from unknown IPs
- Notify as required — Depending on the data type and jurisdiction, breach notification may be required (GDPR: 72 hours; HIPAA: 60 days; various US state laws)
- Document the timeline — When was the bucket created? When was it made public? When was it discovered?
The most important step is acting quickly. CloudTrail logs by default retain 90 days of events, so you have a limited window to investigate.