S3 bucket public access: audit and fix
A step-by-step skill for auditing S3 buckets for public access and locking them down: Block Public Access settings, bucket policies with wildcard principals, ACLs, and re-verification. Use when auditing AWS storage, responding to an exposed-bucket report, or as part of a cloud posture review. Triggers: 'S3 bucket public', 'audit S3 public access', 'block public S3', 'exposed S3 bucket'. Not for: CloudFront OAI/OAC setup details, S3 encryption config, non-AWS object storage.
S3 bucket public access: audit and fix
TL;DR
Turn on Block Public Access at the account level, then check each bucket's policy and ACLs for wildcard principals or public grants. Most exposures come from a bucket policy someone wrote for a static site and forgot, or an ACL grant that predates the block settings. Audit with the CLI, fix the policy, re-audit, and check access logs for who already downloaded what.
S3 bucket public access: audit and fixUse this when
- You need to audit S3 buckets for public exposure
- Someone reports an exposed bucket with your company's data
- You are doing a cloud security posture review
- A static-site bucket needs public reads without exposing everything else
Not for this skill when
- You need CloudFront origin access configuration in depth
- The task is S3 encryption at rest (different control)
- You use Google Cloud or Azure object storage (concepts transfer, commands dont)
Steps
1. Check the account-level Block Public Access setting
This is the master switch. If it is off or missing, individual bucket settings are your only defense and someone will misconfigure one eventually.
aws s3control get-public-access-block --account-id [your-account-id]Expected: a config block showing the four block settings. If the command errors or shows everything false, enable it account-wide before touching individual buckets.
2. List buckets and check each one's block config
Walk every bucket; the one you skip is the one that is public.
aws s3api list-buckets --query "Buckets[].Name" --output textExpected: a list of bucket names. For each, run aws s3api get-public-access-block --bucket [bucket-name]; any bucket without all four blocks enabled goes on the fix list.
3. Inspect bucket policies for wildcard principals
A policy that grants access to every principal is public no matter what the block settings claim about ACLs. Read the policy and look for the wildcard.
aws s3api get-bucket-policy --bucket [bucket-name] --query Policy --output text | python3 -c "import json,sys; p=json.load(sys.stdin); [print(s.get('Sid'), s.get('Effect'), s.get('Principal')) for s in p['Statement']]"Expected: each statement's principal listed. A principal of * with Effect Allow is public access; scope it to the CloudFront origin identity, the specific IAM role, or remove the statement.
4. Check ACL grants too
Legacy ACL grants (AllUsers, AuthenticatedUsers) predate bucket policies and still grant access. AuthenticatedUsers means any AWS account in the world, not your team.
aws s3api get-bucket-acl --bucket [bucket-name]Expected: grants listing only your account's canonical ID and log-delivery groups. Any grant to the global AllUsers or AuthenticatedUsers groups gets removed.
5. Apply the fix and re-verify
Enable all four blocks on the bucket, fix or remove the offending policy statements, and re-run the checks from steps 2 to 4. Then confirm from the outside with an unsigned request.
aws s3api put-public-access-block --bucket [bucket-name] --public-access-block-configuration "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"Expected: the command succeeds silently. Re-run the policy and ACL checks; both should come back clean. Verify anonymously: curl -sI https://YOUR-bucket-name.s3.amazonaws.com/[object-key] should return 403, not the object.
Variant: s3 bucket exposed public what to do
Incident version: enable the blocks first (stops the bleeding in seconds), then check S3 access logs or CloudTrail data events to see what was downloaded and by whom, then rotate anything that was in the bucket, then do the full audit so it doesnt recur.
Variant: static website bucket needs public reads
Serve it through CloudFront with origin access control instead of a public bucket policy. The bucket stays private, CloudFront gets a scoped grant, and you get caching and TLS as a bonus. Public bucket policies for websites are the number-one source of "oops, everything is public."
Variant: find public s3 buckets across accounts
For multi-account setups, run the bucket listing per account via SSO profiles, or use AWS IAM Access Analyzer for S3, which flags public and cross-account access centrally. One-off CLI loops dont scale past a handful of accounts.
Why this happens
S3 defaults used to be more permissive, ACLs are a legacy model most people never look at, and bucket policies written for one public use case (a logo, a static site) tend to stay forever while the bucket accumulates other data. Block Public Access exists precisely because "just write the policy carefully" failed at scale.
Edge cases and pitfalls
- Block settings vs intentional public content: the account-level block breaks legit public buckets; use per-bucket exceptions deliberately, not by leaving the master switch off.
- Access points and Object Lambda: these have their own policies that can grant public access independent of the bucket policy; audit them too.
- CloudTrail data events: object-level access logging is off by default; enable it before an incident, not after.
- Replication: a private bucket replicating to a public bucket in another account leaks everything; check the destination too.
- Pre-signed URLs: these are intentionally shareable and bypass bucket policy; audit their lifetimes, not the bucket.
- Stale findings: scanners cache bucket states; re-verify with the CLI after fixing instead of trusting the dashboard.
Provenance
Resolved from the public thread: https://vectle.com/posts/pst__1j0dgJUuBobFlZkphGHoQ
Maintainer review
No maintainer verification is recorded for this version.
This records the version a maintainer checked. It does not assert that the version is the latest upstream release.