AWS S3 bucket exposed: what to do when it's accidentally public
You just realized your S3 bucket is public and anyone can read your files. Here's the exact fix to lock it down and audit what leaked.
When this happens
You're reviewing your AWS billing and notice a spike in S3 GET requests. Or worse, someone sends you a link to a file in your bucket that they found on Shodan. You check the bucket permissions and see that the bucket policy has "Effect": "Allow", "Principal": "*" or the ACL has AllUsers read access. This usually happens when a developer copied a bucket policy from a tutorial and forgot to restrict it, or when someone toggled "Make public" in the console thinking it only applies to one object — it doesn't.
What's actually happening here
AWS S3 gives you two independent ways to make a bucket public: bucket policies and ACLs. If either one grants public access, the bucket is public. The console's "Public access" badge is a summary of both. The root cause is almost always a bucket policy with "Principal": "*" or an ACL entry like READ for http://acs.amazonaws.com/groups/global/AllUsers. AWS's default is private, but one wrong click or a copy-pasted policy overrides that.
The fix: lock it down and audit the damage
The order matters here. If you change the policy first but leave a public ACL, the bucket stays public. You have to check both.
Step 1: Block all public access via S3 Block Public Access
This is the nuclear option, but it's the fastest way to stop the bleeding. Go to the bucket's Permissions tab → Block public access (bucket settings) → click Edit. Check all four boxes:
Block public access to buckets and objects granted through new access control lists (ACLs)
Block public access to buckets and objects granted through any access control lists (ACLs)
Block public access to buckets and objects granted through new public bucket or access point policies
Block public access to buckets and objects granted through any public bucket or access point policies
Click Save. AWS will warn you this overrides individual permissions. That's what you want.
Step 2: Remove the bucket policy that made it public
Still in the Permissions tab, find Bucket Policy. If you see a policy with "Principal": "*" and "Effect": "Allow", it's the culprit. Delete the entire policy. Don't try to edit it — start fresh. A common mistake is leaving a Deny statement that doesn't actually block anonymous access. Start with an empty policy.
Step 3: Audit the ACLs
Scroll down to Access control list (ACL). Click Edit. Look for Everyone (public access) with any checkmark (Read, Write, Read ACP, Write ACP). Uncheck all of them. If you see an entry for Authenticated Users group (any AWS account), that's not public per se, but it's still broad — decide if you need it. For most use cases, remove it too.
Step 4: Verify it's really private
Go to the bucket's Permissions tab. Look for the Public access badge. It should say Bucket and objects not public. If it still says Public, check for bucket policies or ACLs that were missed — sometimes a bucket policy that grants access only to specific IAM roles but uses "Principal": "*" with a condition that fails still gets flagged as public by AWS's own heuristic. Also check if you have a CloudFront origin access identity (OAI) or Origin Access Control (OAC) — those are safe but can confuse the badge.
Step 5: Audit what was exposed and rotate credentials
You need to know what leaked. Use S3 Inventory or Athena to list objects that were accessed by anonymous principals. A faster way: enable S3 server access logs for the bucket (if you didn't have them on, you're late but enable them now for forward-looking). Then run a query:
SELECT key, size, last_event_time
FROM s3_access_logs
WHERE bucket = 'your-bucket-name'
AND user_identity = 'Anonymous'
ORDER BY last_event_time DESC;
If you find files with secrets (API keys, database passwords, PII), invalidate those secrets immediately. Rotate any AWS access keys that were in those files. Notify your security team or compliance officer.
What to check if it still fails
Three things trip people up:
- Object-level ACLs: Even if the bucket is private, individual objects can have public ACLs. After step 1, this won't matter because Block Public Access overrides it. But if you didn't enable Block Public Access, objects set to public are still readable. Use the AWS CLI to batch-set ACLs to private:
aws s3api put-object-acl --bucket your-bucket --key your-key --acl private. Or use S3 Batch Operations to do it for all objects. - Cross-account access: Another account may have a bucket policy that grants access to a specific IAM role in your account. That's not public but can look like a leak in audits. Check for cross-account policies under Bucket Policy where
Principalis an AWS account ID, not a wildcard. - S3 Block Public Access at the account level: If you want to prevent this from ever happening again, enable S3 Block Public Access at the AWS account level (not just the bucket). It's under S3 → Block Public Access → Account settings. This will prevent any bucket in the account from ever being made public, even by accident. It's a one-time toggle and you can't override it per bucket. Good for production accounts, maybe too strict for dev.
The most common "still fails" scenario: you removed the bucket policy and ACLs, but the bucket still shows as public because S3 Object Ownership is set to Bucket owner preferred and some objects were uploaded by another AWS account with public ACLs. The fix: change Object Ownership to Bucket owner enforced — that disables ACLs entirely and makes all objects owned by the bucket owner. This is actually the AWS-recommended setting as of 2024.
One real case: A startup had a bucket with a policy that allowed public
s3:GetObjectands3:ListBucket. They thought the"Condition": { "IpAddress": { "aws:SourceIp": "192.0.2.0/24" } }would lock it to their office IP. But the IP condition only worked forGetObject, notListBucket. So anyone could list the bucket contents from anywhere. The fix was to use a bucket policy with explicit denies for public access, or better, use presigned URLs for their app.
Was this solution helpful?