
I Made an S3 Bucket Public on Purpose, Then Watched AWS Try to Stop Me (3 Locks, 1 Lab)
A hands-on S3 security lab: leak a bucket on purpose, see which of the 3 access layers blocks you, find the one setting that lets it through, then lock it down and detect it like a defender. Includes attacker recon, an org-wide prevention plan, and a checklist.
TL;DR: S3 buckets don't "accidentally become public" by magic. It takes a policy, an ACL, or an IAM mistake, and someone turning off the safety net. In this article you'll try to leak a bucket on purpose, watch AWS stop you, see exactly which switch lets it through, then lock it back down and detect it.
Start here: which bucket is exposed?
Four buckets. Which ones can an anonymous stranger on the internet read?
A) A bucket with a policy allowing
Principal: "*" for s3:GetObject, and Block Public Access ON B) The same policy, with Block Public Access OFF C) A bucket where only your IAM role has access D) A bucket with a policy allowing Principal: {"AWS": "*"} plus a Condition limited to your VPC endpointAnswer: only B is truly public. A is blocked by the safety net, C is private, and D is restricted by its condition (but go test it, because conditions are where people slip).
If that surprised you, good. This article is a lab to make it stick.
What you'll get
- β The 3 layers of S3 access control, in plain English
- β A 15-minute lab: make a bucket public on purpose, then fix it
- β How attackers actually find exposed buckets
- β How defenders detect and prevent it (account-wide)
- β A checklist and a cheat sheet
π The mental model: three locks on one door
Think of an S3 bucket as a storeroom with three locks. Every lock must allow access, and the safety net can override the rest.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Anonymous request: GET /secret.txt
|
v
+---------------------------------------------+
| LOCK 1: Block Public Access (account level) | <- the master override
+---------------------------------------------+
|
v
+---------------------------------------------+
| LOCK 2: Block Public Access (bucket level) | <- the same, per bucket
+---------------------------------------------+
|
v
+---------------------------------------------+
| LOCK 3: Bucket policy / ACL / IAM policy | <- who is allowed to do what
+---------------------------------------------+
|
v
Object returned (or 403)
The key idea: Block Public Access (BPA) beats everything below it. A bucket policy that says "allow everyone" does nothing while BPA is on.
BPA is really four settings. Know all four:
| Setting | What it does |
|---|---|
| BlockPublicAcls | Rejects requests that try to set a public ACL |
| IgnorePublicAcls | Ignores any public ACL already on the bucket or objects |
| BlockPublicPolicy | Rejects bucket policies that would make the bucket public |
| RestrictPublicBuckets | Restricts access to a bucket with a public policy to AWS service principals and authorized users in the account |
New buckets get BPA turned on and ACLs disabled by default these days. The risk lives in older buckets, buckets someone deliberately opened, and accounts where BPA was switched off "to make a demo work."
π§ͺ Lab: leak a bucket on purpose (and catch yourself)
Use a test account and a fake secret. Never upload anything real.
Step 1: Create a private bucket
1
2
3
4
5
6
7
8
export AWS_DEFAULT_REGION=ap-south-1
BUCKET=lab-s3-leak-$RANDOM$RANDOM
aws s3api create-bucket --bucket $BUCKET \
--create-bucket-configuration LocationConstraint=$AWS_DEFAULT_REGION
echo "fake-secret: this-is-not-a-real-key" > secret.txt
aws s3 cp secret.txt s3://$BUCKET/secret.txt
Step 2: Be the anonymous stranger
1
2
URL=https://$BUCKET.s3.$AWS_DEFAULT_REGION.amazonaws.com/secret.txt
curl -s -o /dev/null -w "%{http_code}\n" $URL
Expect 403. You're not logged in, so you're a stranger, and the door is shut.
Step 3: Try to make it public (BPA is on)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
cat > policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "PublicRead",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::$BUCKET/*"
}]
}
EOF
aws s3api put-bucket-policy --bucket $BUCKET --policy file://policy.json
AWS refuses with an AccessDenied error that mentions public policies being blocked. That's
BlockPublicPolicy doing its job. Screenshot this, it's your "the safety net works" moment.Step 4: Remove the safety net (test bucket only!)
1
2
3
4
5
aws s3api delete-public-access-block --bucket $BUCKET
aws s3api put-bucket-policy --bucket $BUCKET --policy file://policy.json
curl -s -o /dev/null -w "%{http_code}\n" $URL
curl -s $URL
Now you get 200 and the fake secret prints in your terminal. Anyone with that URL has it, no credentials needed.
Stuck at Step 4? If your account has account-level BPA enabled, deleting the bucket-level setting won't be enough. That's the master lock doing its job. Check with:aws s3control get-public-access-block --account-id $(aws sts get-caller-identity --query Account --output text)For the lab, either use a separate sandbox account, or temporarily relax it there. Don't touch a shared or production account.
Step 5: Detect it like a defender
1
aws s3api get-bucket-policy-status --bucket $BUCKET
Look for
"IsPublic": true. Then check the bucket's BPA state:1
aws s3api get-public-access-block --bucket $BUCKET
You'll get an error saying no configuration exists. That error is the finding: the bucket has no safety net.
Step 6: Fix it
1
2
3
4
5
aws s3api put-public-access-block --bucket $BUCKET \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
curl -s -o /dev/null -w "%{http_code}\n" $URL
Back to 403, even though the public policy is still attached.
RestrictPublicBuckets makes AWS ignore the open policy. The policy is still a bad idea, but the net caught it.π§Ή Cleanup
1
2
3
aws s3 rm s3://$BUCKET/secret.txt
aws s3api delete-bucket --bucket $BUCKET
rm -f secret.txt policy.json
π΄ How attackers find exposed buckets
| Technique | What it looks like |
|---|---|
| Name guessing | Try company-backup, company-prod, company-logs, company-dev and see which return 403 (exists) vs 404 (doesn't) |
| Source recon | Grep JavaScript, mobile apps, GitHub, and error pages for *.s3.amazonaws.com URLs |
| Anonymous listing | aws s3 ls s3://name --no-sign-request to check whether s3:ListBucket is open |
| "Any authenticated user" tricks | Old ACLs and policies that trust any AWS account, not just yours |
| Credential leaks | Keys found in a repo, then used with the real API |
A 403 doesn't mean "safe." It means "this bucket exists." That's already useful to an attacker.
π΅ How defenders prevent and detect it
Prevent (set once, protect everything):
1
2
3
4
5
# Account-wide master lock
aws s3control put-public-access-block \
--account-id $(aws sts get-caller-identity --query Account --output text) \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
- At the organization level, use an SCP to deny
s3:PutBucketPublicAccessBlockands3:PutAccountPublicAccessBlockfor everyone except a break-glass role, so nobody can turn the lock off - Keep ACLs disabled (Bucket owner enforced)
- Need to serve public content? Put CloudFront in front of a private bucket instead of opening the bucket
- Use policy conditions like
aws:SourceVpce,aws:PrincipalOrgID, andaws:SecureTransport - Encrypt with SSE-KMS where the data warrants it, and turn on versioning
Detect:
- IAM Access Analyzer flags buckets shared outside your account or organization
- AWS Config and Security Hub have managed checks for public buckets
- CloudTrail records bucket-policy and BPA changes (management events). Alert on
PutBucketPolicy,DeletePublicAccessBlock, andPutBucketAcl, the same way you alerted onCreateAccessKey - Object-level reads (
GetObject) need CloudTrail data events or S3 server access logs. Both are optional and cost money, so enable them where the data is sensitive
β Mistakes that actually cause breaches
- Turning off BPA "just to get the demo working" and never turning it back on
Principal: "*"in a policy with noCondition- Trusting "any AWS account" or "any authenticated user" instead of your own org ID
- Putting secrets,
.envfiles, or database dumps in a bucket "temporarily" - Long-lived presigned URLs shared in tickets or chat
- Static website hosting enabled on a bucket that holds more than the website
- No alerting on policy changes, so nobody knows it happened until a researcher emails you
β Your S3 security checklist
- [ ] Account-level Block Public Access: all four settings ON
- [ ] SCP preventing anyone from turning BPA off
- [ ] ACLs disabled on all buckets
- [ ] Access Analyzer enabled in every region you use
- [ ] Alerts on
PutBucketPolicy,PutBucketAcl,DeletePublicAccessBlock - [ ] Sensitive buckets: encryption, versioning, and data-event logging
- [ ] Public content served through CloudFront, not an open bucket
- [ ] Presigned URLs short-lived, and never pasted into public places
π Cheat sheet
| Situation | Do this |
|---|---|
| "Is this bucket public?" | aws s3api get-bucket-policy-status --bucket NAME |
| "Does it have a safety net?" | aws s3api get-public-access-block --bucket NAME |
| "Lock everything, account-wide" | aws s3control put-public-access-block ... |
| "Share one file temporarily" | Presigned URL with a short expiry |
| "Serve a public website" | CloudFront + private bucket |
| "Who changed this policy?" | CloudTrail: PutBucketPolicy events |
| "Who shares with outside accounts?" | IAM Access Analyzer findings |
β οΈ Where this falls short
- BPA stops public exposure. It does not stop an over-permissioned IAM role or a leaked access key from reading your data. Public access and IAM access are different problems
- Access Analyzer tells you what is shared, not whether the data is sensitive. Pair it with data classification
- This lab uses one account. In a real organization, you'd enforce all of this centrally (Organizations, SCPs, Config conformance packs)
π¬ Your turn
- What did Step 3 return for you? Paste the error message (blur your account ID)
- Which layer surprised you most: account-level BPA, bucket-level BPA, or the policy itself?
- Vote for the next post: 1) IAM role chaining and
AssumeRoleattacks, 2) CloudTrail log investigation, 3) securing EC2 metadata (IMDSv2)
Found this useful? Save it for your next AWS interview. π
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article