Automated vs. Manual Penetration Testing: When to Use Each
Automated penetration testing gives speed and continuous coverage; manual testing gives depth and creativity. Compare the two, see where each wins, and how to combine them.
S3 buckets go public through policies, ACLs, and disabled Block Public Access. Learn the layered defense model, hardening checklist, and CLI remediation.
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.
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.
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:
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.
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.
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.
| Control | Scope | What it decides | Best practice |
|---|---|---|---|
| Account-level BPA | Entire account | Overrides all buckets; blocks public policies/ACLs everywhere | Enable all four settings |
| Bucket-level BPA | One bucket | Overrides that bucket's policy/ACL | Enable all four settings |
| Bucket policy | One bucket | Resource-based allow/deny for principals | Scope principals; add deny guardrails |
| IAM policy | Principals in account | Identity-based allow/deny | Grant least privilege |
| Object Ownership / ACLs | One bucket | Whether ACLs apply at all | Bucket 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.
| Mechanism | Type | Applies to | Recommended use |
|---|---|---|---|
| Bucket policy | Resource-based | The bucket and its objects | Cross-account access, TLS/encryption guardrails, public CDN origin rules |
| IAM policy | Identity-based | Users, groups, roles | Day-to-day least-privilege access for your own principals |
| ACL | Resource-based (legacy) | Bucket or individual object | Avoid; 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.
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.
| # | Control | Setting | Verify with |
|---|---|---|---|
| 1 | Account BPA | All four = true | get-public-access-block (s3control) |
| 2 | Bucket BPA | All four = true | get-public-access-block (s3api) |
| 3 | Object Ownership | BucketOwnerEnforced | get-bucket-ownership-controls |
| 4 | Default encryption | SSE-S3 or SSE-KMS | get-bucket-encryption |
| 5 | TLS enforcement | Deny aws:SecureTransport=false | get-bucket-policy |
| 6 | Versioning | Enabled | get-bucket-versioning |
| 7 | Access logging | Server access log + CloudTrail data events | get-bucket-logging |
| 8 | Public status | IsPublic=false | get-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.
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.
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.
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.
AuthenticatedUsers does not mean your users — it means any authenticated
AWS principal anywhere, so treat that ACL grant as effectively public.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.
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.
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.
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.
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.
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.
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.
Automated penetration testing gives speed and continuous coverage; manual testing gives depth and creativity. Compare the two, see where each wins, and how to combine them.
Penetration Testing as a Service (PTaaS) delivers pentesting through a continuous platform instead of a once-a-year PDF. Compare cost, cadence, coverage, and when each model wins.
The OWASP Top 10:2025 ranking explained — what moved, the two new categories, and how to continuously test each risk in modern apps and APIs.