Audit and Close Accidental Public Storage Buckets
Inventory storage buckets, test anonymous access, separate intentional public assets from accidental exposure, and close the gap with Block Public Access.
Objective
Find every bucket or container that allows anonymous reads or writes, confirm which exposure is intentional, and close accidental access without breaking public assets.
Environment
- Object storage inventory covering every account, including buckets created by automation
- Permission to read bucket policies, ACLs and public access block settings
- A CDN or private distribution path for assets that must remain publicly reachable
- An incident response process in case anonymous write access is found
- Approval from the asset owner before changing a bucket used by a public website
Steps
1. Build the bucket inventory
List every bucket across all accounts, including the account id, region, creation date, website configuration and public access block settings.
Expected state: Every bucket has an owner and a recorded public access configuration.
2. Evaluate anonymous access
Read each bucket policy and ACL for wildcard principals, then send an unauthenticated GET and PUT to a known object and a nonexistent key.
Expected state: Each bucket is classified as private, intentionally public read, or exposed beyond intent.
3. Confirm intent with the owner
Ask the owning team whether anonymous access is required, which paths are public, and whether a CDN with signed URLs or origin access control is already in place.
Expected state: Intentional public buckets have a named owner and a documented delivery path.
4. Back up configuration and close exposure
Export the bucket policy and ACL, then enable all four Block Public Access settings or apply the provider equivalent for buckets that are not intentionally public.
Expected state: Anonymous GET and PUT return 403 on every non-public bucket.
5. Move intentional assets to a controlled path
Serve public assets through a CDN with origin access control, keep the bucket private, and update application references to the CDN hostname.
Expected state: Public URLs still resolve through the CDN while direct bucket access is denied.
6. Verify and monitor
Re-run the anonymous checks from outside the network, enable bucket-level access logging and alert on any policy change that allows a wildcard principal.
Expected state: A repeat scan finds no accidental public bucket and a policy change raises an alert.
Decision points
What should happen when anonymous write access is found?
- Close access and continue the audit
- Close access and open a security incident immediately
Treat anonymous write as a security incident; preserve logs, because closing the bucket destroys the evidence of what was written.
How should intentionally public assets be served?
- Keep the bucket public and rely on monitoring
- Make the bucket private and serve through a CDN
Make the bucket private and serve through a CDN with origin access control; it preserves public URLs without leaving the bucket open.
Should Block Public Access be enabled at the account level?
- Per bucket only
- At the account level with documented exceptions
Enable at the account level and carve out only the buckets that genuinely need public access; account-level protection catches automation that forgets the setting.
Expected outcome
Only buckets with documented intent remain publicly readable, anonymous writes are impossible, and future public exposure raises an alert.
Verification
Run unauthenticated GET and PUT requests against each bucket from a machine outside the cloud network and confirm 403 responses, then check the public access block settings and access logs.
Cleanup
Store the configuration exports with the audit evidence, remove temporary test objects, and schedule the anonymous access scan monthly.
Exposure is a configuration, not a feature
A public bucket is usually created by a copy-pasted policy, an old website workflow or an automation role that still has s3:PutBucketPolicy. The audit must cover every account, not just the production account, and it must test anonymous access from outside the network, because a bucket policy can still allow Principal: "*" while an endpoint policy hides it from inside. Classify every bucket as private, intentionally public read, or exposed beyond intent before changing anything.
Safety checks
Back up the policy and ACL before changing either, and record the current public access block settings so a rollback is exact. Check whether a bucket is an origin for a CDN or a static website before closing public access; changing it can take a live site down. Do not delete objects while investigating exposure, and do not test writes against a bucket containing real data unless the write is restricted to a throwaway key.
Inspect, back up, then close
Save the policy and ACL before remediation so the change is reversible. The commands below show the configuration, archive the policy and enable all four public access blocks on a bucket that should be private.
aws s3api get-public-access-block --bucket acme-marketing-assets
aws s3api get-bucket-policy --bucket acme-marketing-assets > policy-backup.json
aws s3api put-public-access-block --bucket acme-marketing-assets \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
curl -s -o /dev/null -w "%{http_code}\n" \
https://acme-marketing-assets.s3.amazonaws.com/index.html
The final request should return 403. For a bucket that must serve public files, keep it private and expose it through a CDN using origin access control, then update the application to use the CDN hostname.
Check for writes, not just reads
Anonymous read exposure leaks data; anonymous write exposure corrupts it. If a PUT succeeds, stop remediation and start incident response: enable access logging if it is not already on, snapshot the bucket inventory, and review object versions and creation times before closing access. Closing the bucket first is tempting but destroys the evidence you need to scope the incident.
Rollback
Restoring the exported policy and disabling the public access block returns the bucket to its previous state within seconds. The irreversible step is deleting objects or versions during cleanup; never combine a bucket cleanup with an exposure fix. Re-enable public access only for a bucket with a named owner and a documented reason, and re-run the anonymous test afterward.