Cloud Security

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.

January 6, 20265 min readShipSafer Team

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:

SettingWhat It Blocks
BlockPublicAclsPrevents new public ACLs from being set
IgnorePublicAclsIgnores existing public ACLs (overrides them)
BlockPublicPolicyPrevents bucket policies that grant public access
RestrictPublicBucketsRestricts 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 read
  • s3-bucket-public-write-prohibited — Detects buckets allowing public write
  • s3-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:

  1. Enable BPA immediately — This cuts off new access
  2. Review CloudTrail — Check s3.amazonaws.com events for downloads (GetObject) in the relevant time window
  3. Check for data exfiltration — Look for high GetObject request counts from unknown IPs
  4. 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)
  5. 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.

aws
s3
cloud-security
misconfiguration
data-exposure

Check Your Security Score — Free

See exactly how your domain scores on DMARC, TLS, HTTP headers, and 25+ other automated security checks in under 60 seconds.