AWS S3 Misconfigurations: The Complete Prevention Guide

S3 buckets go public through policies, ACLs, and disabled Block Public Access. Learn the layered defense model, hardening checklist, and CLI remediation.

Marcus Bell· Cloud Security Engineer· Updated July 20, 202616 min read
Guides & TutorialsRedStrike

TL;DR

Amazon S3 buckets leak data through four channels: over-permissive bucket policies, legacy ACL grants, disabled Block Public Access, and unencrypted or unmonitored storage. Since 2023, S3 ships secure by default — Block Public Access is on and ACLs are disabled for every new bucket — but real environments drift as engineers add exceptions. Enable account-level Block Public Access, keep ACLs disabled with Bucket owner enforced, require TLS and encryption in the bucket policy, and continuously scan every account and Region for drift. Every one of these controls is verifiable from the S3 API in seconds.

Why do S3 buckets keep leaking data?

S3 buckets keep leaking because access is governed by several independent mechanisms — bucket policies, IAM policies, ACLs, and Block Public Access — and a single mistake in any one of them can expose objects publicly. Amazon S3 is secure by default today, but production environments accumulate exceptions faster than teams remove them. A bucket opened for a one-off static site, an ACL granted to AuthenticatedUsers (which means any AWS account on Earth, not yours), or a policy with "Principal": "*" quietly turns private storage into a public website.

The blast radius is what makes S3 different from a leaky database. A bucket can hold billions of objects, so one wrong setting at the bucket or account level can expose millions of files at once. Attackers do not need to guess object keys — they enumerate predictable bucket names, scrape public bucket indexes, and use open-source tooling that crawls S3 continuously. Historically, some of the largest cloud data exposures traced back to a single public bucket, not a sophisticated exploit chain.

The good news is that S3's access model, while layered, is fully introspectable. Every control that determines whether an object is reachable — the account-level public access block, the bucket's public access block, its policy, its ownership setting, and its encryption configuration — can be read back through the API. That makes S3 misconfiguration a detectable and preventable problem rather than an unavoidable one.

What changed in S3's default security posture?

As of April 27, 2023, Amazon S3 automatically enables Block Public Access and disables ACLs for every new bucket in all AWS Regions, and since January 5, 2023, it encrypts all new object uploads with SSE-S3 (AES-256) by default. These two changes moved S3 from "secure if you configure it" to "secure unless you actively weaken it."

Three defaults now apply to newly created buckets:

  • Block Public Access is on. All four settings are enabled, so public policies and public ACLs are rejected. This applies regardless of how the bucket is created — console, CLI, SDK, or CloudFormation.
  • ACLs are disabled. Object Ownership defaults to Bucket owner enforced, so ACLs no longer affect access at all. The bucket owner owns every object, and access is decided purely by policy.
  • Default encryption is applied. Every new object is encrypted at rest with SSE-S3 (AES-256) at no extra cost.

These defaults apply to new buckets and new objects only. Existing buckets keep whatever settings they had, and objects already sitting in an unencrypted bucket are not retroactively encrypted. If your account predates 2023, you almost certainly have legacy buckets that never received these protections. Audit them explicitly — do not assume the new defaults saved you.

How does an S3 bucket actually become public?

A bucket becomes public through exactly one of three mechanisms: a bucket policy that grants access to an anonymous or wildcard principal, an ACL that grants access to a public group, or a Block Public Access configuration that is disabled and therefore stops overriding the first two. Understanding which mechanism is in play tells you exactly how to fix it.

1. The bucket policy. A resource-based policy with "Principal": "*" and no restricting Condition grants anonymous access to whoever it names — often s3:GetObject on arn:aws:s3:::bucket/*. This is the most common path to a public bucket because it is the one engineers deliberately reach for when they want "just make it work."

2. Access control lists (ACLs). ACLs are a legacy mechanism. Two grants make a bucket effectively public: AllUsers (truly anonymous) and AuthenticatedUsers (any AWS principal in any account). Because ACLs are now disabled by default, this path mostly affects older buckets — but those are exactly the ones that get forgotten.

3. Block Public Access being off. Block Public Access (BPA) does not grant access; it overrides policies and ACLs that would grant public access. When BPA is enabled, a public policy is simply ignored. When someone disables BPA to "test something," any latent public policy or ACL immediately takes effect. This is why BPA is the single highest-leverage control — it neutralizes the other two paths.

Block Public Access has four independent toggles: BlockPublicAcls and IgnorePublicAcls govern ACLs, while BlockPublicPolicy and RestrictPublicBuckets govern policies. RestrictPublicBuckets is the strongest: even if a public policy exists, only AWS service principals and authorized users in the account can use it. Set all four to true unless you have a documented, audited reason not to.

What is the layered defense model for S3?

S3 access is evaluated across four independent layers — account-level Block Public Access, bucket-level Block Public Access, resource and identity policies, and Object Ownership/ACLs — and an object is only reachable if every applicable layer allows it. Defense in depth means configuring each layer so that no single mistake is sufficient to expose data.

ControlScopeWhat it decidesBest practice
Account-level BPAEntire accountOverrides all buckets; blocks public policies/ACLs everywhereEnable all four settings
Bucket-level BPAOne bucketOverrides that bucket's policy/ACLEnable all four settings
Bucket policyOne bucketResource-based allow/deny for principalsScope principals; add deny guardrails
IAM policyPrincipals in accountIdentity-based allow/denyGrant least privilege
Object Ownership / ACLsOne bucketWhether ACLs apply at allBucket owner enforced (ACLs off)

The relationship between these matters. An explicit Deny always wins over any Allow. Block Public Access sits above policy evaluation entirely — it can veto a public policy that would otherwise succeed. And with Bucket owner enforced, the ACL layer is removed from evaluation completely, shrinking the attack surface to policies you can reason about centrally.

How do bucket policies, IAM policies, and ACLs differ?

MechanismTypeApplies toRecommended use
Bucket policyResource-basedThe bucket and its objectsCross-account access, TLS/encryption guardrails, public CDN origin rules
IAM policyIdentity-basedUsers, groups, rolesDay-to-day least-privilege access for your own principals
ACLResource-based (legacy)Bucket or individual objectAvoid; keep disabled unless a specific per-object case requires it

For modern workloads, the guidance from AWS is unambiguous: manage access with IAM policies and bucket policies, and keep ACLs disabled. ACLs cannot express conditions, cannot be audited as easily, and were the root cause of a large share of historical S3 exposures.

How do you harden an S3 bucket, step by step?

Hardening S3 means enabling Block Public Access at the account level, disabling ACLs, enforcing encryption and TLS through the bucket policy, and turning on logging — then verifying each setting through the API. Work top-down so account-level controls protect you before you touch individual buckets.

#ControlSettingVerify with
1Account BPAAll four = trueget-public-access-block (s3control)
2Bucket BPAAll four = trueget-public-access-block (s3api)
3Object OwnershipBucketOwnerEnforcedget-bucket-ownership-controls
4Default encryptionSSE-S3 or SSE-KMSget-bucket-encryption
5TLS enforcementDeny aws:SecureTransport=falseget-bucket-policy
6VersioningEnabledget-bucket-versioning
7Access loggingServer access log + CloudTrail data eventsget-bucket-logging
8Public statusIsPublic=falseget-bucket-policy-status

Start with account-level Block Public Access. This one command overrides every bucket in the account and is the most defensible baseline you can set:

aws s3control put-public-access-block \
  --account-id 123456789012 \
  --public-access-block-configuration \
    BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

Then set the same block on individual buckets for defense in depth:

aws s3api put-public-access-block \
  --bucket amzn-s3-demo-bucket \
  --public-access-block-configuration \
    BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

Disable ACLs by enforcing bucket-owner ownership, which removes the ACL layer from access evaluation entirely:

aws s3api put-bucket-ownership-controls \
  --bucket amzn-s3-demo-bucket \
  --ownership-controls '{"Rules":[{"ObjectOwnership":"BucketOwnerEnforced"}]}'

Set default encryption. SSE-S3 is applied automatically for new objects, but for sensitive data prefer a customer-managed KMS key so you get key-level access control and CloudTrail auditing of key usage:

aws s3api put-bucket-encryption \
  --bucket amzn-s3-demo-bucket \
  --server-side-encryption-configuration '{
    "Rules": [{
      "ApplyServerSideEncryptionByDefault": {
        "SSEAlgorithm": "aws:kms",
        "KMSMasterKeyID": "arn:aws:kms:us-east-1:123456789012:key/abcd-1234"
      },
      "BucketKeyEnabled": true
    }]
  }'

Finally, enforce TLS in transit and reject unencrypted uploads with a bucket policy. The aws:SecureTransport condition denies any request that is not over HTTPS:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyInsecureTransport",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::amzn-s3-demo-bucket",
        "arn:aws:s3:::amzn-s3-demo-bucket/*"
      ],
      "Condition": { "Bool": { "aws:SecureTransport": "false" } }
    }
  ]
}

For applications that need to share a single object temporarily, do not open the bucket — issue a presigned URL instead. A presigned URL grants time-limited access to one object using the credentials of the principal that signed it, so the bucket stays private. For traffic that should never leave AWS, add a VPC gateway endpoint for S3 and restrict the bucket policy to that endpoint with the aws:SourceVpce condition.

How do you detect S3 misconfigurations before attackers do?

Detection combines AWS-native tooling — IAM Access Analyzer, AWS Config, and CloudTrail — with continuous CSPM scanning that queries the S3 API across every account and Region on a schedule. Manual review does not scale past a handful of buckets, and it never catches drift that happens at 2 a.m.

IAM Access Analyzer continuously evaluates resource-based policies and reports when a bucket is shared with an external entity — public or cross-account. It is Regional, so enable it in every Region where you have buckets. After a policy change it can take up to 30 minutes to surface a new finding, and it re-scans all policies every 24 hours. Treat every external-access finding on a bucket that is not deliberately public as an incident.

AWS Config records the configuration of each bucket over time and can run managed rules such as s3-bucket-public-read-prohibited, s3-bucket-public-write-prohibited, s3-bucket-server-side-encryption-enabled, and s3-bucket-ssl-requests-only. Config gives you the historical timeline — when a bucket became public and what changed.

Open-source scanners such as Prowler and ScoutSuite codify the same checks as CIS controls. A Prowler-style S3 check verifies account and bucket BPA, encryption, TLS enforcement, versioning, and logging in one run, mapping each result to a CIS or AWS Foundational Security Best Practices control ID.

Detection latency is the enemy. IAM Access Analyzer's up-to-30-minute window and Config's rule-evaluation cadence both mean a bucket can be public for a meaningful stretch before anything alerts. Pair detection with a preventive guardrail: a service control policy (SCP) that denies s3:PutBucketPublicAccessBlock changes that would disable BPA org-wide stops the drift from happening at all.

How do you test and verify a bucket is not public?

Verify a bucket's exposure by reading back its public-access status, public access block, ownership, and encryption directly from the S3 API, then confirming anonymous access is refused. Trust the API, not the console's green checkmarks.

Ask S3 whether it considers the bucket public. IsPublic: false is what you want:

aws s3api get-bucket-policy-status --bucket amzn-s3-demo-bucket

Confirm all four Block Public Access settings are true:

aws s3api get-public-access-block --bucket amzn-s3-demo-bucket

Check ownership (expect BucketOwnerEnforced) and encryption:

aws s3api get-bucket-ownership-controls --bucket amzn-s3-demo-bucket
aws s3api get-bucket-encryption --bucket amzn-s3-demo-bucket

To sweep an entire account, list every bucket and check each one's public-access block in a loop:

for b in $(aws s3api list-buckets --query 'Buckets[].Name' --output text); do
  echo "== $b =="
  aws s3api get-public-access-block --bucket "$b" \
    --query 'PublicAccessBlockConfiguration' --output json 2>/dev/null \
    || echo "NO PUBLIC ACCESS BLOCK SET"
done

Finally, prove the negative from an unauthenticated context. Fetching an object with credentials disabled should return 403 Forbidden or AccessDenied:

aws s3api get-object --bucket amzn-s3-demo-bucket --key test.txt out.txt \
  --no-sign-request

If that command succeeds, the bucket is serving anonymous reads — stop and remediate before doing anything else.

How do you remediate a bucket that is already public?

Remediate in order: re-enable Block Public Access, remove or scope the offending policy statement or ACL grant, confirm the public status flips to false, then turn on logging so you have a forensic trail. Re-enabling BPA first is the fastest way to cut off access while you investigate the root cause.

# 1. Cut off public access immediately
aws s3api put-public-access-block \
  --bucket amzn-s3-demo-bucket \
  --public-access-block-configuration \
    BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

# 2. Inspect the policy that granted access
aws s3api get-bucket-policy --bucket amzn-s3-demo-bucket

# 3. Replace it with a scoped policy, or delete it entirely if unused
aws s3api delete-bucket-policy --bucket amzn-s3-demo-bucket

# 4. Confirm the bucket is no longer public
aws s3api get-bucket-policy-status --bucket amzn-s3-demo-bucket

Then enable CloudTrail data events so future object-level reads and writes are recorded. Trails do not log S3 data events by default; you opt in with advanced event selectors that filter on fields like eventName, readOnly, and resources.ARN to control cost while still capturing what matters.

Key takeaways

  • Block Public Access is the single highest-leverage S3 control — enable all four settings at the account level and it overrides every risky per-bucket policy or ACL underneath it.
  • S3 has been secure by default since 2023, but only for new buckets and new objects — legacy buckets and pre-existing unencrypted objects are never retroactively protected.
  • A bucket becomes public through exactly three mechanisms: a wildcard bucket policy, a public ACL grant, or disabled Block Public Access.
  • AuthenticatedUsers does not mean your users — it means any authenticated AWS principal anywhere, so treat that ACL grant as effectively public.
  • Keep ACLs disabled with Bucket owner enforced and manage access exclusively through IAM and bucket policies, which support conditions and central auditing.
  • Every S3 security setting is verifiable from the API in seconds — public status, public access block, ownership, and encryption can all be read back and automated.
  • Detection alone leaves an exposure window — pair IAM Access Analyzer and CSPM scanning with a preventive SCP guardrail that blocks disabling BPA.

Frequently asked questions

Does enabling Block Public Access break static website hosting?

It can, if you were serving the site directly from a public bucket. The recommended pattern is to keep the bucket private and serve it through Amazon CloudFront using an Origin Access Control (OAC), which lets only the CloudFront distribution read the objects. The bucket's Block Public Access stays fully enabled and the public never touches S3 directly.

Is S3's default encryption enough for compliance?

Default SSE-S3 (AES-256) satisfies the baseline "encryption at rest" requirement in most frameworks, and it is applied automatically to new objects. For regulated workloads, prefer SSE-KMS with a customer-managed key so you get key-level access policies, automatic key rotation, and CloudTrail logging of every key use. DSSE-KMS adds a second independent layer of KMS encryption for the strictest requirements. Pair any of these with access logging for full SOC 2 or PCI DSS coverage.

What is the difference between account-level and bucket-level Block Public Access?

Account-level BPA is set once via the s3control API and applies to every bucket in the account, including buckets created later. Bucket-level BPA applies to a single bucket. Setting both gives you defense in depth: even if someone weakens a bucket's own setting, the account-level block still overrides it. The account-level control requires the s3:PutAccountPublicAccessBlock permission, which should be tightly restricted.

How do I audit hundreds of existing buckets quickly?

Script a loop over list-buckets that calls get-public-access-block, get-bucket-policy-status, get-bucket-encryption, and get-bucket-ownership-controls for each bucket, or let IAM Access Analyzer and a CSPM tool do it continuously. Access Analyzer will flag any bucket shared externally, and CSPM will alert on drift the moment a setting changes rather than only when you next run a script.

Do presigned URLs create a public exposure risk?

Presigned URLs are time-limited and carry the permissions of the principal that signed them, so they are far safer than opening a bucket. The risk is operational: a URL with a long expiry that leaks (in a log, a referrer header, or a shared link) grants access until it expires. Keep expiry times short, generate them server-side, and never embed long-lived presigned URLs in client code.

Why did my bucket policy get rejected when I tried to make it public?

Because Block Public Access is enabled — specifically BlockPublicPolicy, which rejects any PutBucketPolicy call that would grant public access. This is working as intended. If you genuinely need public access (rare), you must first disable that setting, which should itself require review and be caught by your guardrails.

How does CloudTrail help after an S3 incident?

CloudTrail management events record configuration changes like PutBucketPolicy and PutBucketPublicAccessBlock, so you can see who made a bucket public and when. CloudTrail data events — which are off by default and enabled via advanced event selectors — record object-level GetObject and PutObject calls, letting you determine exactly which objects an attacker read during the exposure window.


S3 misconfiguration is not a one-time fix; it is a posture you maintain. Buckets, policies, and ownership settings drift every time an engineer ships a feature, and the gap between "configured correctly" and "still configured correctly" is where breaches live. Continuous cloud security posture management (CSPM) closes that gap by querying the S3 and IAM APIs on a schedule, flagging a newly public bucket in minutes rather than months, and mapping each finding back to CIS and AWS best- practice controls.

Sources