A storage bucket left readable by anyone on the internet — no login, no token, just a URL away from your private data.
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.
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.
| Detection signal | What 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 |
public-read ACL. Check both; a clean policy alone is not “closed.”keys.env. Scope by what was reachable and for how long, not by the count of files.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.
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?