Public bucket exposure

A storage bucket left readable by anyone on the internet — no login, no token, just a URL away from your private data.

The idea

Object storage like S3 keeps files (“objects”) in named buckets. By default a bucket is private — only your account can read it. But a single permissive setting can flip that: a bucket policy that allows "Principal": "*", an object ACL of public-read, or a disabled public-access-block.

When that happens, anyone who knows or guesses the URL can GET the object anonymously — no credentials at all. Incident response is a tight loop: discover the exposure, contain it fast, then trace the root cause so it can't recur.

internet anonymous bucket: prod-user-exports
Press play, or step through the response one move at a time.

How it works

Containment first: turn on block all public access at the bucket (and ideally the account) so no policy or ACL can grant anonymous access, regardless of what's already set. Then fix the cause — strip the wildcard "Principal": "*" grant from the bucket policy, reset any public-read object ACLs, and re-scope the policy to the exact principals that genuinely need it.

Contain with one CLI call:

aws s3api put-public-access-block \
  --bucket prod-user-exports \
  --public-access-block-configuration \
    BlockPublicAcls=true,IgnorePublicAcls=true,\
BlockPublicPolicy=true,RestrictPublicBuckets=true

Then replace the over-broad bucket policy with a least-privilege one. The before allowed the world; the after grants read only to a named role and only under TLS:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowExportReaderRoleOnly",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::111122223333:role/export-reader"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::prod-user-exports/*",
      "Condition": {
        "Bool": { "aws:SecureTransport": "true" }
      }
    }
  ]
}

The offending policy looked almost identical but with "Principal": "*" and no condition — which is exactly what made every object world-readable.

Signals

Detection signalWhat it implies
IAM Access Analyzer flags the bucket as publicly accessible A policy or ACL grants access outside your account — investigate the exact grant it cites
Anonymous GET requests in S3 server access logs / CloudTrail data events The data was actually reached without credentials — treat as a real exposure, not a near-miss
Public-access-block reads as disabled on the bucket There is no backstop — a permissive policy or ACL can take effect immediately
Object ACL shows public-read (grant to AllUsers) Specific objects are public even if the bucket policy is clean — fix per object
Bucket policy contains "Principal": "*" with s3:GetObject The whole bucket is intentionally or accidentally world-readable — the primary root cause

Watch out for

Worked example

An on-call alert fires: Access Analyzer reports prod-user-exports as publicly accessible. You pull S3 server access logs and find anonymous GETs on users.csv from unknown IPs over the last six hours — a confirmed exposure, not a false positive.

Contain. You run put-public-access-block with all four flags true. The bucket lock closes; a fresh anonymous curl to the object now returns 403 AccessDenied. Exposure stopped.

Root cause. Reading the bucket policy, you find a statement with "Principal": "*" and s3:GetObject on arn:aws:s3:::prod-user-exports/* — added weeks ago to “quickly share a report.” You replace it with a policy that grants read only to the export-reader role under TLS, reset the lingering public-read object ACLs, and turn on block-public-access at the account level so the next mistake can't reach the internet. Finally you note the rotation needed for anything sensitive that was reachable.

Check yourself

Your bucket policy has no wildcard principal, yet Access Analyzer still flags the bucket as public. What's the most likely cause?

You're paged for a confirmed public exposure. What's the right first move?