Fix AWS S3 bucket misconfiguration exposing your data

Server & Cloud Intermediate 👁 1 views 📅 May 29, 2026

Your S3 bucket is publicly readable. Change the bucket policy or ACL to block anonymous access. Here's how to lock it down fast.

Quick answer: Stop the leak now

Edit your S3 bucket's permissions block, remove any Principal: "*" statements from the bucket policy, and switch ACLs to private. Then verify with the AWS CLI or a browser test.

I've seen this on dozens of production buckets. You upload a CSV, a user profile image, or a server log, and someone finds it because the bucket's set to "public" under permissions. The AWS Console makes it easy to accidentally grant public read access, especially when you're rushing a deployment. The real culprit is either a bucket policy with "Principal": "*" or an ACL that allows public reads. Both turn your bucket into a free-for-all download server.

Why this happens

Three common triggers:

  • You or a teammate clicked "Grant public read access to this bucket" during setup, thinking it was needed for a website.
  • An IAM policy or S3 bucket policy was copied from a tutorial that used "Principal": "*" and "Effect": "Allow" for a static site, but you never restricted it to only the CloudFront origin.
  • An ACL was set to public-read on individual objects when you uploaded them via the CLI with --acl public-read.

Step-by-step fix

Step 1: Block all public access at the bucket level

  1. Open the S3 Console and select the affected bucket.
  2. Go to the Permissions tab.
  3. Under Block public access (bucket settings), click Edit.
  4. Check all four boxes. Yes, all of them. AWS recommends this for any bucket not explicitly serving public content.
  5. Click Save changes. You'll be prompted to confirm—type "confirm" and proceed.

Step 2: Remove the bucket policy granting public access

  1. In the same Permissions tab, scroll to Bucket policy and click Edit.
  2. Look for a statement like this:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::your-bucket-name/*"
    }
  ]
}

If you see "Principal": "*", delete the entire statement. Don't just comment it out—remove it. Also check if there's a separate condition allowing public access; sometimes the condition is missing entirely.

  • Click Save.
  • Step 3: Fix object ACLs

    1. In the bucket, click on Objects.
    2. Select all objects, then click Actions > Make private.
    3. For buckets with thousands of objects, use the AWS CLI:
    aws s3 rm s3://your-bucket-name --recursive --exclude "*" --include "*" --acl private

    Wait, that's wrong. Use this instead:

    aws s3 cp s3://your-bucket-name/ s3://your-bucket-name/ --recursive --acl private

    That copies all objects in place with new ACLs. Yes, it's slow if you have millions of objects—run it in the background.

    Step 4: Test the fix

    1. In a private browser window, try to open a known file URL like https://your-bucket-name.s3.amazonaws.com/your-file.csv.
    2. You should see an AccessDenied XML error, not the file.

    Also test with the AWS CLI:

    aws s3api get-object --bucket your-bucket-name --key your-file.csv /dev/null --no-sign-request

    If it fails with AccessDenied, you're good. If it returns the file, you missed something—go back to Step 1 and double-check.

    Alternative fixes if the main steps don't work

    Scenario: You need the bucket to be public for a static website, but only from CloudFront

    Don't set the bucket policy to Principal: "*"—that exposes it directly. Instead, use an Origin Access Identity (OAI) in CloudFront. Update the bucket policy to allow only the OAI:

    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Principal": {
            "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity EABCDEFGHIJKLM"
          },
          "Action": "s3:GetObject",
          "Resource": "arn:aws:s3:::your-bucket-name/*"
        }
      ]
    }

    Replace EABCDEFGHIJKLM with your CloudFront OAI ID. This limits access to only CloudFront—direct S3 URLs will then fail.

    Scenario: ACLs keep reverting to public-read

    This happens if you have an S3 event notification (like Lambda) that writes objects with --acl public-read. Check your Lambda code or any upload scripts. The fix is to change the script to omit the ACL or set --acl private.

    How to prevent this from happening again

    • Enable S3 Block Public Access at the account level. Go to AWS Organizations or the S3 Console settings and turn on all four blocks for the whole account. This overrides bucket-level settings.
    • Use an IAM policy that explicitly denies any action that sets public ACLs or modifies bucket policies without a specific approval.
    • Automate compliance checks: Set up AWS Config with the rule s3-bucket-public-read-prohibited or s3-bucket-public-write-prohibited. If a bucket goes public, you'll get an alert within minutes.
    • Never use --acl public-read in your CLI commands. If you need a file publicly accessible, put it behind CloudFront and use signed URLs.
    • Audit your existing buckets: Run aws s3api list-buckets and check each one. I've found buckets three years old that were accidentally public. Don't assume old ones are safe.

    One more thing: If you've already had a breach, check CloudTrail logs for GetObject calls from unknown IPs. You might need to notify affected users depending on the data type (GDPR, HIPAA, PCI). But that's a separate article.

    That's the fix. You won't trip on this again.

    Was this solution helpful?