The brutal thing about an open S3 bucket is the asymmetry: one unauthenticated HTTP request can list every key, and a second can pull the file behind it. No account, no credentials, no rate limit worth mentioning. And the buckets that end up exposed are almost never the ones anyone is watching. They're the forgotten ones, spun up for a one-off data transfer or a quick static site and left wide open long after the person who made them moved on.

How S3 buckets end up public

S3 access is controlled by two overlapping mechanisms: ACLs (Access Control Lists) and bucket policies. ACLs are attached to individual buckets and objects; bucket policies are JSON documents that express rules across an entire bucket. Either one can grant public access independently of the other.

ACLs and IAM-style policies use different vocabularies, and this trips people up. An ACL grant doesn't speak in s3:ListBucket or s3:GetObject; those are policy/IAM actions, a separate mechanism entirely. ACLs use a short set of permissions: READ, WRITE, READ_ACP, WRITE_ACP, FULL_CONTROL. The classic mistake is granting the predefined AllUsers group READ. On a bucket, READ means list the contents; on an object, it means download it. AllUsers maps to any unauthenticated caller, no AWS account required. The policy-based equivalent is a bucket policy with "Principal": "*" and no Condition clause. Different syntax, same outcome.

Block Public Access (BPA) is the override that sits above both. It can be set at the account level or per bucket, and when enabled it ignores whatever ACLs and policies say. In April 2023 AWS changed the defaults at the bucket level: every newly created bucket now ships with Block Public Access turned on and ACLs disabled (Object Ownership set to "Bucket owner enforced"). That default applies to new buckets going forward. It did not reach back and flip account-level BPA on legacy accounts, and it did nothing for buckets that already existed. Plenty of older buckets still carry permissive ACLs or wide-open policies set during early development that nobody ever tightened.

The result is a long tail of S3 buckets (including those inside large enterprises) that remain publicly listable or downloadable years after they were first provisioned.

How to check if a bucket is public

Unauthenticated listing. The S3 ListObjectsV2 API endpoint is reachable over HTTPS without credentials. If a bucket named example-bucket is listable, the following request returns an XML manifest of its contents:

GET https://example-bucket.s3.amazonaws.com/?list-type=2

A 200 response with <ListBucketResult> XML confirms public listing is enabled. A 403 AccessDenied means listing is restricted, but don't read it as the bucket being locked down and done. A 404 NoSuchBucket usually means no bucket exists at that name, though it isn't a guarantee: S3 returns 403 instead of 404 in some cross-account and enumeration scenarios, so a 403 doesn't even confirm the name is taken. Treat the codes as signals, not verdicts. You can also target a specific region endpoint:

GET https://example-bucket.s3.us-east-1.amazonaws.com/?list-type=2

AWS CLI policy status check. If you own the bucket, the canonical way to verify is:

aws s3api get-bucket-policy-status --bucket example-bucket

This returns {"PolicyStatus": {"IsPublic": true}} when the bucket is public. Treat it as AWS's own public-status indicator, but don't stop there. Check the ACL explicitly too, since a bucket can be exposed by an ACL grant alone:

aws s3api get-bucket-acl --bucket example-bucket

Look for a Grantee with URI set to http://acs.amazonaws.com/groups/global/AllUsers or http://acs.amazonaws.com/groups/global/AuthenticatedUsers.

Check your own bucket here. Paste the bucket URL or hostname into the open-bucket viewer and the tool will run both the listing check and a policy-status lookup, then report what an anonymous caller can access.

What's at risk

The worst case isn't exotic, and it's the one to plan around: a bucket where listing and download are both open. An attacker lists the whole keyspace, reads it like a directory tree, picks the objects that matter (db-backup-prod.sql.gz, terraform.tfstate, .env.production) and pulls exactly those. No guessing, no brute-forcing object names: just enumerate and grab. With ordinary tooling that's a few minutes of work. What comes out is usually database backups full of PII and credentials, config files with hardcoded keys and connection strings, source archives, or logs carrying tokens and internal IPs.

Listing alone, even with downloads locked, is not harmless. A directory manifest hands over every key, size, and timestamp. That's enough to leak naming conventions, internal path structure, and the occasional secret baked straight into a filename. It also tells an attacker precisely what to come back for.

The pattern behind nearly every exposed bucket is the same: created for something legitimate, opened up "temporarily" for convenience, then forgotten. How they get found is more mundane than people assume. Certificate transparency logs won't reveal bucket names, because S3 serves traffic under a shared wildcard certificate, and passive DNS rarely helps either. The real discovery vectors are brute-forcing common naming patterns (acme-backups, acme-prod-assets), public indexes like GrayhatWarfare that already catalog open buckets, and leaked references buried in client-side JavaScript, source repos, or archived pages on the Wayback Machine.

How to lock it down

Enable account-level Block Public Access. Start here; nothing else you do moves the needle as far. Navigate to the S3 console, select "Block Public Access settings for this account", and enable all four toggles:

  • Block public access granted through new ACLs
  • Block public access granted through any ACLs
  • Block public access granted through new bucket policies
  • Block public access granted through any bucket policies

These account-level settings override bucket-level ACLs and policies, so they protect every bucket in the account (including those created by third-party tools or CloudFormation stacks) without requiring per-bucket changes.

Enabling "Block public access granted through any ACLs" will break existing static-website hosting that relies on AllUsers grants, so test in a non-production account first if you use S3 static hosting.

Audit bucket policies for wildcard principals. A bucket policy with "Principal": "*" or "Principal": {"AWS": "*"} is public by definition unless a Condition block restricts it to a specific VPC endpoint or IP range. Review all bucket policies:

aws s3api list-buckets --query 'Buckets[*].Name' --output text | \
  tr '\t' '\n' | \
  xargs -I{} aws s3api get-bucket-policy --bucket {} 2>/dev/null

Any policy containing "Principal": "*" without a Condition clause should be reviewed and tightened.

Deploy the AWS Config rule s3-bucket-public-read-prohibited. AWS Config offers a managed rule that continuously evaluates every bucket in the account and flags any that allow public read access. A companion rule, s3-bucket-public-read-write-prohibited, covers write access. Both can alert your security team on a violation, or be wired to AWS Config auto-remediation (via an SSM Automation runbook) so an offending bucket is corrected without manual intervention.

Rotate any credentials found in previously-public buckets. If a bucket was public for any period of time, treat every secret it contained as compromised. Rotate API keys, database passwords, and any other credentials that appear in objects or object names, even if access logs show nothing suspicious.


For a full index of publicly exposed S3 buckets indexed by this service, see the AWS S3 exposure index. To check a specific bucket URL right now, use the open-bucket viewer.