S3 Transfer Acceleration Must Not Be Unexpectedly Enabled
Severity: low
Type: unsafe_state
Domain: exposure
Compliance: nist_800_53_r5: CM-7;
S3 Transfer Acceleration creates an additional public endpoint (s3-accelerate.amazonaws.com) that bypasses VPC endpoint restrictions. It must not be enabled unless explicitly required and documented.
Remediation: Suspend Transfer Acceleration under bucket Properties unless explicitly required. If required, document the business justification and ensure VPC endpoint policies account for the acceleration endpoint.
S3 bucket policies must not grant access to external AWS accounts. allowed_accounts contains trusted external AWS account IDs (12-digit). Access from accounts outside this allowlist is unsafe.
Remediation: Review bucket policy Principal elements for external account IDs. Remove statements granting access to accounts not in your organization. Use aws:PrincipalOrgID condition to restrict access to your AWS Organization.
S3 bucket policies must not grant access to a wildcard principal (Principal "" or AWS: ""). A wildcard principal makes every statement in the policy effectively anonymous or cross-account unless a restricting Condition (aws:PrincipalOrgID, aws:SourceVpc, aws:SourceIp with a fixed CIDR, aws:SourceArn, etc.) narrows it. Without such a Condition, the policy is a public-access grant regardless of which specific S3 actions are allowed.
Remediation: 1. Narrow the Allow. Replace the wildcard principal with the
specific AWS account IDs or role ARNs that actually need
access. If a wildcard must remain (cross-org distribution,
service-bound caller), add a restricting Condition that fixes
the caller set: aws:PrincipalOrgID, aws:SourceVpc /
aws:SourceVpce, aws:SourceIp with a fixed CIDR, or
aws:SourceArn.
If the bucket genuinely needs to be reachable from the
public internet (CDN origin, static asset distribution),
prefer scoped mechanisms over a wildcard bucket policy:
CloudFront Origin Access Control, S3 Access Points with
network-bound policies, or signed/presigned URLs. These
give you a narrow Allow on a defined caller surface
instead of a "*" Allow with hopeful Conditions.
Keep S3 Block Public Access fully enforced as an interim
guard while the policy change is being rolled out — PAB
does not fix a broad Allow, it only suppresses its effect
at the network boundary, so treat it as a containment
layer rather than the remediation.
S3 buckets must not grant write or delete permissions to external AWS accounts. Cross-account read access may be acceptable for analytics or auditing, but write access from external accounts creates data integrity and supply chain risks.
Remediation: Remove bucket policy statements granting s3:PutObject, s3:DeleteObject, or s3:PutBucketPolicy to external accounts. If cross-account write is required, restrict to specific account IDs with condition keys.
Bucket policy evaluates as effectively public under AWS PolicyStatus.IsPublic semantics — a wildcard principal (Principal: "*" or Principal: {"AWS": "*"}) without a scoping Condition on aws:SourceVpc, aws:SourceVpce, aws:PrincipalOrgID, aws:PrincipalArn, or a narrow aws:SourceIp. The control reads the policy in isolation: Public Access Block state does not affect whether it fires — PAB only affects whether the exposure is active right now or latent (one account-level or bucket-level PAB toggle away from active). Paired with the PAB controls, this is the posture signal that says "if every PAB layer were removed tomorrow, this bucket would be public". Distinct from CTL.S3.ACCESS.002, which detects the raw presence of a wildcard principal without reasoning about scoping Conditions: ACCESS.002 answers "is there a wildcard whose Conditions we still need to verify?", ACCESS.004 answers "has the verification already concluded that the policy is public?".
Remediation: 1. Narrow the Allow. Identify the Allow statement with the
wildcard principal. If it is not intentional, remove it. If
it is intentional (CDN origin, cross-organization data
distribution), add a Condition that fixes the caller set —
aws:PrincipalOrgID for same-org access, aws:SourceVpc or
aws:SourceVpce for VPC-bound access, aws:SourceIp with a
fixed CIDR for known network ranges, or aws:SourceArn for a
specific invoking service or distribution.
If the bucket genuinely needs internet reachability, prefer
scoped mechanisms over a wildcard bucket policy: CloudFront
Origin Access Control, S3 Access Points, or signed /
presigned URLs. These collapse the reachable principal set
to a defined surface instead of "*" with restricting
Conditions stacked on top.
Keep S3 Block Public Access fully enforced at the account
and bucket level as an interim containment guard while the
policy change is in flight. PAB does not fix the underlying
broad Allow — it only suppresses its effect at the network
boundary, leaving the exposure latent rather than active.
S3 buckets tagged data-classification=phi must not have policy statements granting access to principals outside the AWS Organization. The control reads the engine-derived external_account_ids list (set when bucket policy Principals or Conditions admit any non-org account); a non-empty list paired with a phi data-classification tag is an HIPAA-reportable breach exposure regardless of which specific S3 actions are allowed.
Remediation: 1) Review the bucket policy and remove any statement whose Principal lists an external AWS account ID. 2) If cross-org access is intentional (e.g., a sanctioned BAA partner), scope the Allow with aws:PrincipalOrgID matching the partner's organization ID and aws:PrincipalArn restricting to the specific partner role. 3) Tag the bucket with phi_external_intended: <baa-id> to suppress this control only after the legal/compliance review is on file.
S3 Access Grants provide temporary credentials scoped to a bucket or prefix. An Access Grant with READWRITE permission on a broad scope (entire bucket or wildcard prefix) bypasses bucket policy restrictions.
Remediation: Restrict grant scope to specific prefixes. Use READ not READWRITE.
When S3 Access Grants are enabled, IAM Identity Center should be attached to the Access Grants instance. Without Identity Center, grants can only target IAM principals — losing the benefit of centralized identity governance and SSO-based access control.
Remediation: Associate IAM Identity Center with the Access Grants instance using aws s3control associate-access-grants-identity-center. This enables directory-based grantee resolution.
S3 buckets tagged with data-classification=phi must have access restricted to explicitly named principals and prefixes. Broad bucket-level access (wildcard principals, unrestricted actions) on PHI data violates the HIPAA minimum necessary standard (§164.502(b)). Access must be narrowed to the exact IAM roles, account IDs, and object prefixes required for each authorized workflow.
Remediation: Restrict bucket policy to named IAM role ARNs and specific object prefixes. Remove wildcard principals and broad s3:* actions. Use IAM Access Analyzer to identify unused permissions and generate least-privilege policies from CloudTrail activity.
S3 Access Log Destination Must Be in Same Account or Org
Severity: high
Type: unsafe_state
Domain: exposure
Compliance: nist_800_53_r5: AU-9; soc2: CC6.1;
When S3 server access logging is enabled, the log destination bucket must be in the same account or organization. An attacker can redirect access logs to their own bucket, capturing every S3 request path — which may contain sensitive data in query strings and object keys. Technique: hackingthe.cloud S3 server access logs exfiltration.
Remediation: Change the access log destination to a bucket in the same account or organization. Investigate whether the external destination was intentionally configured.
S3 access point has a policy that grants broader access than the bucket policy. Access points are intended to scope access for specific use cases — a broad access point policy defeats this purpose and may grant access that the bucket policy restricts. Access points with Internet network origin and broad policies are effectively public endpoints.
Remediation: Restrict the access point policy to the specific principals and operations required for its use case. Access points should scope access narrower than the bucket policy, not broader. Set network origin to VPC if internet access is not required.
S3 bucket is owned by an AWS account that is not a member of the organization. The bucket is outside organizational governance — no SCPs apply, no centralized logging, no CloudTrail. The bucket may belong to a former employee's personal account, a retired project account, or an account moved to a different organization.
Remediation: Move the bucket to an account within the organization, or migrate the data and decommission the external bucket. Ensure all data-bearing buckets are in accounts governed by organizational SCPs and centralized logging.
The AWS account must have S3 Block Public Access enabled at the account level. Account-level PAB overrides all bucket and object settings, providing a hard ceiling that prevents any S3 resource in the account from being made public regardless of bucket policies, ACLs, or access point policies. Without account-level PAB, each bucket's public access depends on its own settings, and a single misconfigured bucket or object ACL can expose data. Account-level PAB is the strongest single defense against accidental public exposure.
Remediation: Enable all four S3 Block Public Access settings at the account level using aws s3control put-public-access-block with the --account-id parameter. This blocks public access for all current and future buckets in the account. If specific buckets require public access, use CloudFront with Origin Access Control instead of making buckets directly public.
S3 Bucket Account SCP Does Not Restrict S3 Public Access
Severity: medium
Type: unsafe_state
Domain: governance
Compliance: nist_800_53_r5: AC-6; soc2: CC6.1;
The account's Service Control Policy does not include restrictions on S3 public access configuration changes. Without an S3-restrictive SCP, account-level Public Access Block settings can be changed by any account administrator, undermining organization-level security guardrails.
Remediation: Add an SCP to the organization that denies s3:PutBucketPublicAccessBlock and s3:PutBucketPolicy actions that would weaken public access restrictions. This prevents account administrators from overriding account-level PAB settings.
S3 bucket ACLs must not be writable by AllUsers or AuthenticatedUsers. WRITE_ACP permission enables attackers to modify the ACL itself, granting themselves FULL_CONTROL and escalating to read, write, and delete all objects.
Remediation: Remove WRITE_ACP grants from the bucket ACL and remove policy statements granting s3:PutBucketAcl or s3:PutObjectAcl to public principals. Enable S3 Public Access Block with BlockPublicAcls set to true.
S3 bucket ACL grants permissions to a canonical ID that does not belong to the owning account. S3 ACLs are a legacy access mechanism that predates bucket policies. Unlike bucket policies — which are visible, auditable, and centrally manageable — ACL grants are opaque: a canonical ID is a 64-character hex string with no human-readable account mapping. Cross-account ACL grants bypass bucket policy restrictions, Public Access Block settings, and Access Point controls. They create an invisible trust relationship that standard access reviews miss.
Remediation: Replace the ACL grant with an equivalent bucket policy statement that grants the external account access via its account ID. Bucket policies are auditable, revocable, and visible to IAM Access Analyzer. After migrating the grant, set the bucket ACL to BucketOwnerFullControl and enable BlockPublicAcls to prevent future ACL-based grants.
S3 bucket ACLs must not grant FULL_CONTROL to AllUsers or AuthenticatedUsers. FULL_CONTROL is the worst-case ACL misconfiguration — the grantee can read, write, and delete objects and modify the ACL itself.
Remediation: Replace the bucket ACL with "BucketOwnerFullControl" or remove the FULL_CONTROL grant to public groups. Enable S3 Public Access Block with BlockPublicAcls and IgnorePublicAcls set to true.
S3 buckets must not contain objects that are individually made public through object-level ACL grants. When a bucket itself is not public, individual objects can still be accessible from the internet if their ACL grants read access to AllUsers or AuthenticatedUsers. This is the "Objects can be public" status in AWS — the bucket is private but objects inside it are exposed. This is a primary vector for data leakage through misplaced sensitive files, where a single object with a public ACL in an otherwise private bucket exposes data that was never intended to be public.
Remediation: Set Object Ownership to BucketOwnerEnforced to disable all ACLs. If that is not immediately possible, enable S3 Block Public Access with IgnorePublicAcls set to true, then audit object ACLs using S3 Inventory with the optional ACL fields. Remove public grants from individual objects using aws s3api put-object-acl.
S3 bucket ACLs should not be readable by unauthenticated users. READ_ACP permission enables attackers to enumerate ACL grants, discover which principals have access, and find escalation paths.
Remediation: Remove READ_ACP grants from the bucket ACL and remove policy statements granting s3:GetBucketAcl or s3:GetObjectAcl to public principals. Enable S3 Public Access Block with BlockPublicAcls set to true.
S3 bucket ACL has been set to public-read more than once within 7 days. ACL modification is a deliberate action — not accidental IaC drift. A single recurrence within a week is a strong signal of intentional repeated action requiring investigation.
Remediation: Investigate the root cause of the repeated oscillation. Determine whether the pattern indicates a broken process, operational workaround, or active compromise. Review CloudTrail for the API calls that triggered each transition.
Bucket Must Not Be Publicly Accessible Via An Access Point While Its Own Controls Pass
Severity: critical
Type: unsafe_state
Domain: exposure
Compliance: hipaa: 164.312(a)(1); soc2: CC6.1;
An S3 bucket whose own controls all evaluate clean — Public Access Block fully enforcing, bucket policy not effectively public — can still be publicly reachable through a single-region S3 Access Point that names it as the delegate bucket. Access Points carry their own PAB and their own resource policy, evaluated independently of the parent bucket; a public Access Point is a parallel reach path. This control fires on the bucket, not the AP, because the finding is "this bucket is exposed via an AP it may not know about" — the bucket is the asset at risk. Mirrors the shape of CTL.S3.CDN.BYPASS.001 (the CloudFront-fronted-bucket bypass). Firing requires the bucket-side controls to be clean: if the bucket is already publicly accessible on its own, the bucket-level controls already caught it and this finding would be noise. The derived field storage.exposure.has_public_access_point comes from the EnrichBucketAPExposure derivation — a post-collection join of aws_s3_access_point assets onto aws_s3_bucket assets within the same snapshot; the trace decomposes back to the raw AP and bucket observations that produced it.
Remediation: 1. Identify the offending Access Point from the
storage.exposure.public_access_point_names field on this
finding, or from the co-occurring AP-side findings.
2. On the Access Point, enable Block Public Access fully
(put-access-point-public-access-block with all four flags
true) and remove any wildcard-principal Allow from its
resource policy — or add a restricting Condition on
aws:SourceVpc, aws:SourceVpce, aws:PrincipalOrgID,
aws:PrincipalArn, or a fixed aws:SourceIp.
3. Consider whether the Access Point is still needed. Access
Points left over from decommissioned services are a common
source of this pattern.
S3 Access Point Must Have Block Public Access Enabled
Severity: critical
Type: unsafe_state
Domain: exposure
Compliance: hipaa: 164.312(a)(1); soc2: CC6.1;
Single-region S3 Access Points have their own Public Access Block settings, independent of the parent bucket's PAB. A bucket can be hardened with PAB fully enforcing while one of its Access Points has PAB disabled — the Access Point endpoint remains a public path to the same underlying data. Prowler and ScoutSuite treat Access Points as a distinct resource with their own exposure surface precisely because of this overlay semantics.
Remediation: Enable all four PAB flags on the Access Point via aws s3control put-access-point-public-access-block with BlockPublicAcls=true, IgnorePublicAcls=true, BlockPublicPolicy=true, RestrictPublicBuckets=true. If the Access Point is intentionally public (rare), document the exposure and add a Stave exemption.
S3 Access Point Block Public ACLs Flag Must Be Enabled
Severity: high
Type: unsafe_state
Domain: exposure
Compliance: hipaa: 164.312(a)(1); soc2: CC6.1;
The BlockPublicAcls flag of a single-region S3 Access Point's Public Access Block configuration rejects any new ACL grant applied through the Access Point endpoint that would make the data publicly accessible. When this specific flag is false, new PUT-ACL calls routed via the Access Point that grant READ, WRITE, READ_ACP, or WRITE_ACP to http://acs.amazonaws.com/groups/global/AllUsers or .../AuthenticatedUsers succeed rather than being rejected at the Access Point boundary. The umbrella CTL.S3.AP.PAB.001 fires when any of the four AP PAB flags is off; this control narrows the finding to the specific flag so remediation is a one-command fix rather than requiring the operator to enumerate which of the four is missing. Prowler and ScoutSuite both report the four flags independently.
Remediation: Enable the BlockPublicAcls flag on the Access Point's Public Access Block configuration. From the CLI:
S3 Access Point Block Public Policy Flag Must Be Enabled
Severity: high
Type: unsafe_state
Domain: exposure
Compliance: hipaa: 164.312(a)(1); soc2: CC6.1;
The BlockPublicPolicy flag of a single-region S3 Access Point's Public Access Block configuration rejects any new Access Point policy that grants access to a public principal (Principal: "*", Principal: {"AWS": "*"}, or an Allow block with no Principal) without a narrowing Condition. When this specific flag is false, new policies with wildcard principals and no scoping Condition succeed at PUT time rather than being rejected at the Access Point boundary. The umbrella CTL.S3.AP.PAB.001 fires when any of the four AP PAB flags is off; this control narrows the finding to the specific flag so remediation is a one-command fix rather than requiring the operator to enumerate which of the four is missing. Prowler and ScoutSuite both report the four flags independently.
Remediation: Enable the BlockPublicPolicy flag on the Access Point's Public Access Block configuration. From the CLI:
S3 Access Point Ignore Public ACLs Flag Must Be Enabled
Severity: high
Type: unsafe_state
Domain: exposure
Compliance: hipaa: 164.312(a)(1); soc2: CC6.1;
The IgnorePublicAcls flag of a single-region S3 Access Point's Public Access Block configuration causes any existing public ACL grants on objects exposed through the Access Point to be ignored at evaluation time. When this specific flag is false, existing ACLs that grant READ, WRITE, READ_ACP, or WRITE_ACP to AllUsers or AuthenticatedUsers remain effective when requests arrive through the Access Point endpoint, preserving public reachability regardless of bucket-level hardening. The umbrella CTL.S3.AP.PAB.001 fires when any of the four AP PAB flags is off; this control narrows the finding to the specific flag so remediation is a one-command fix rather than requiring the operator to enumerate which of the four is missing. Prowler and ScoutSuite both report the four flags independently.
Remediation: Enable the IgnorePublicAcls flag on the Access Point's Public Access Block configuration. From the CLI:
S3 Access Point Restrict Public Buckets Flag Must Be Enabled
Severity: high
Type: unsafe_state
Domain: exposure
Compliance: hipaa: 164.312(a)(1); soc2: CC6.1;
The RestrictPublicBuckets flag of a single-region S3 Access Point's Public Access Block configuration suppresses any existing Access Point policy statement that grants public access; at evaluation time only AWS-service principals (via aws:PrincipalService) and principals in the same AWS account remain able to reach the Access Point. When this specific flag is false, previously-authored public Access Point policies remain effective and the endpoint stays publicly reachable regardless of bucket-level hardening. The umbrella CTL.S3.AP.PAB.001 fires when any of the four AP PAB flags is off; this control narrows the finding to the specific flag so remediation is a one-command fix rather than requiring the operator to enumerate which of the four is missing. Prowler and ScoutSuite both report the four flags independently.
Remediation: Enable the RestrictPublicBuckets flag on the Access Point's Public Access Block configuration. From the CLI:
Single-region S3 Access Points carry their own resource policy that is evaluated independently of the parent bucket policy. A public Access Point policy creates a public access path to the bucket even when the bucket's own policy is scoped correctly. This mirrors the MRAP case (CTL.S3.MRAP.POLICY.001) and is the single-region analogue.
Remediation: Remove the public grant from the Access Point policy, or add a scoping Condition that binds the caller to a fixed VPC, IP range, organization, or ARN. If public reach is intentional, enforce it through a narrower mechanism such as CloudFront with Origin Access Control and keep the Access Point policy private.
CloudTrail S3 object-level data event logging must be enabled for PHI buckets. Server access logging captures bucket-level operations but not individual object access patterns. CloudTrail data events record GetObject, PutObject, and DeleteObject calls required for HIPAA audit controls.
Remediation: Configure a CloudTrail trail with a data event selector for AWS::S3::Object covering this bucket. Use aws cloudtrail put-event-selectors to add the selector.
S3 buckets must not grant read access to all authenticated AWS users. AuthenticatedUsers scope means any AWS account can read objects, which is nearly as dangerous as fully public access.
Remediation: Remove the ACL grant to AuthenticatedUsers. Replace with specific IAM principals or use bucket policy with explicit account IDs. Enable S3 Public Access Block with IgnorePublicAcls set to true.
S3 buckets must not grant write or delete access to all authenticated AWS users. AuthenticatedUsers scope means any AWS account holder worldwide can upload, overwrite, or delete objects — enabling data injection, ransomware, and supply chain poisoning.
Remediation: Remove the ACL grant or policy statement granting write access to AuthenticatedUsers. Replace with specific IAM principals or use bucket policy with explicit account IDs. Enable S3 Public Access Block with BlockPublicAcls and IgnorePublicAcls set to true.
S3 buckets tagged with data-classification=phi must have all four detection components active: server access logging, CloudTrail object-level logging, GuardDuty S3 protection, and AWS Config recording. Missing any one component creates a gap in breach detection and incident investigation capability. HIPAA §§164.400-414 requires the ability to detect and investigate unauthorized access to PHI.
Remediation: Ensure all four components are active for this bucket: 1. Server access logging (aws s3api put-bucket-logging) 2. CloudTrail object-level data events (aws cloudtrail put-event-selectors) 3. GuardDuty S3 protection (aws guardduty update-detector) 4. AWS Config recording (aws configservice put-configuration-recorder)
S3 bucket name matches a known predictable pattern used by AWS services that auto-create storage (Glue, SageMaker, CDK, Athena, Beanstalk, EMR Studio, CodeStar, CloudFormation). Buckets in the global namespace with predictable names are vulnerable to the Bucket Monopoly attack: an attacker who knows the account ID can pre-create the bucket name in a region the account has not used yet, hijacking the service's auto-created storage. The structural fix is enforcing the S3 account-regional namespace via SCP (CTL.ORG.SCP.S3NAMESPACE.001); this control identifies existing buckets that remain in the global namespace.
Remediation: Migrate to the S3 account-regional namespace (s3:x-amz-bucket-namespace) or verify bucket ownership in all active regions. Apply CTL.ORG.SCP.S3NAMESPACE.001 to prevent future predictable-name buckets in the global namespace.
Any externally referenced S3 bucket must exist and be owned. Dangling references (missing or unowned buckets) enable bucket takeover and attacker-controlled content delivery.
Remediation: Create the S3 bucket in your AWS account, or remove the DNS record, CDN origin, or application reference pointing to the unclaimed bucket.
CloudFront-Fronted Bucket Must Not Allow Direct Public Access
Severity: high
Type: unsafe_state
Domain: exposure
Compliance: hipaa: 164.312(a)(1); soc2: CC6.1;
A bucket that is both fronted by a CloudFront distribution and publicly readable on its direct S3 endpoint gives an attacker a full bypass of the CloudFront layer. CloudFront typically carries the defensive controls — WAF rules, geographic restrictions, request logging, signed URLs, TLS policy — while the raw S3 URL (bucket.s3.<region>.amazonaws.com) carries none of them. Anyone who learns the bucket name can fetch objects around every CloudFront-layer control.
Remediation: 1. Enable Block Public Access on the bucket and remove any
Principal "*" grants from the bucket policy so the direct
S3 endpoint is no longer publicly reachable.
2. Restrict the bucket policy to the specific CloudFront
distribution via an Origin Access Control and a condition on
aws:SourceArn = arn:aws:cloudfront::<account>:distribution/<id>.
3. Verify the CloudFront distribution still serves objects
after the lockdown (OAC-signed requests survive; direct
anonymous requests fail).
Private Bucket Must Not Be Publicly Exposed Via CloudFront
Severity: high
Type: unsafe_state
Domain: exposure
Compliance: hipaa: 164.312(a)(1); soc2: CC6.1;
A bucket with Block Public Access enabled can still serve objects publicly through CloudFront if the bucket policy grants access to the cloudfront.amazonaws.com service principal. This creates a false sense of security — the bucket appears private but objects are accessible via the CloudFront distribution URL.
Remediation: 1. Review whether public CDN access is intentional for this bucket. 2. If not intentional, remove the CloudFront distribution or restrict
it with signed URLs/cookies.
3. If intentional, document this as an acknowledged exposure path
and add a Stave exemption for this bucket.
When S3 objects are served via CloudFront, Origin Access Control (OAC) should be used instead of the legacy Origin Access Identity (OAI). OAC supports SSE-KMS, SigV4, and all S3 features. OAI is a legacy mechanism that does not support KMS encryption and is being deprecated.
Remediation: 1. Create an Origin Access Control for the distribution. 2. Update the distribution origin to use OAC instead of OAI. 3. Update the bucket policy to grant cloudfront.amazonaws.com
with a Condition restricting to the distribution ARN.
4. Remove the legacy OAI.
CloudFront distribution fetches objects from S3 via HTTP, not HTTPS. Traffic between CloudFront and S3 is unencrypted. The user sees HTTPS in the browser but the backend fetch is plaintext — the same false-HTTPS pattern as Cloudflare Flexible SSL mode.
Remediation: Configure the CloudFront distribution origin to use HTTPS-only or match-viewer with HTTPS. Set the S3 origin protocol policy to https-only in the CloudFront distribution configuration.
Every S3 bucket must have a data-classification tag with a value from the recognized taxonomy (phi, pii, confidential, internal, public, non-sensitive). The data-classification tag is the gating condition for the majority of Stave's sensitive data controls — PHI encryption, Object Lock, Macie scanning, lifecycle retention, and access scoping are all conditional on this tag. A bucket without the tag silently passes all tag-conditional controls regardless of its actual contents. CIS 2.1.3 requires that all S3 data is discovered, classified, and secured. This control establishes the classification baseline — it does not verify what classification was applied, only that every bucket has been explicitly classified so downstream controls can evaluate it.
Remediation: Apply a data-classification tag to the bucket with a value from the recognized taxonomy: phi, pii, confidential, internal, public, or non-sensitive. Use AWS Tag Editor or the S3 PutBucketTagging API to apply tags. Establish a tagging policy requiring classification at bucket creation time. Use AWS Config rules or SCPs to enforce mandatory tagging.
The S3 bucket storing CloudTrail logs must not be publicly accessible. A public CloudTrail bucket exposes the complete API activity log and allows log tampering.
S3 buckets must have the public access block fully enabled. When disabled, the bucket has no safety net against accidental public exposure from policy or ACL changes. This detects the enabling condition for public access, not the exposure itself.
Remediation: Enable all four Public Access Block settings on the bucket: BlockPublicAcls, IgnorePublicAcls, BlockPublicPolicy, RestrictPublicBuckets.
S3 CORS Wildcard Origin Must Be Explicitly Intended
Severity: medium
Type: unsafe_state
Domain: exposure
Compliance: soc2: CC6.6;
S3 bucket CORS configurations that set AllowedOrigins to "*" expose the bucket's CORS-permitted methods to every web origin. For buckets serving genuinely public static assets this may be intentional; for buckets holding tenant data, authenticated user content, or signed URLs it widens the attack surface for cross-origin abuse. S3 does not expose an AllowCredentials field on CORS rules — browsers refuse the wildcard+credentials combination — so the unsafe state for S3 is wildcard origin on a bucket that is not tagged as intentionally public. The observation shape mirrors the raw "aws s3api get-bucket-cors" response.
Remediation: Replace "*" in AllowedOrigins with the specific origins that need cross-origin access. If the bucket is a public CDN or static asset origin where wildcard CORS is intentional, add the tag cors_wildcard_intended=true to declare intent. To remove CORS entirely, run "aws s3api delete-bucket-cors --bucket ".
CloudFront distributions must not reference S3 origins that do not exist. A missing/unclaimed origin bucket enables takeover and CDN content poisoning.
Remediation: Create the S3 bucket in your AWS account to claim the name, or remove the dangling origin from the CloudFront distribution. Update the distribution to use an Origin Access Control (OAC).
A delegated vendor principal has permissions that could make this bucket publicly accessible — s3:PutBucketPolicy with the ability to add a public Principal, or s3:PutBucketAcl with the ability to grant public-read. The vendor is the weakest link in the access chain: if the vendor is compromised, the attacker can expose the bucket to the internet through the vendor's delegated permissions without ever touching the customer account. This is the supply-chain version of the public-bucket finding — the bucket is not public today, but one compromised vendor credential away from being public. This is a distinct check from S3 Block Public Access (the backstop): ESCALATION.001 asks whether the vendor CAN escalate; the BPA control asks whether the backstop is in place. Both are needed.
Remediation: Remove the vendor's ability to modify bucket accessibility: remove s3:PutBucketPolicy and s3:PutBucketAcl from the vendor's allowed actions. As defense-in-depth, enable S3 Block Public Access at the account level (separate backstop check) and add an SCP denying s3:PutBucketPolicy and s3:PutBucketAcl for the vendor's account. Re-run the collector to refresh delegation.vendor_can_make_public.
This S3 bucket's policy grants control-level actions (PutObject, DeleteObject, PutBucketPolicy, PutBucketAcl, or equivalent) to an external AWS principal that is not registered in the vendor registry (controldata/taxonomy/vendor_registry.yaml). The access may be legitimate but undeclared — or it may be residual from a decommissioned integration. Unknown external principals with control rights are the supply-chain entry point: if the external account is compromised, the attacker inherits whatever permissions the bucket policy grants, and the security team does not know the principal exists.
Remediation: Identify the unknown principal by reading the bucket policy. Determine whether the access is still needed. If needed, add the vendor to controldata/taxonomy/vendor_registry.yaml with a declared purpose, scope, and review date. If not needed, remove the principal from the bucket policy. Re-run the collector to refresh delegation.has_unknown_external_principal.
This bucket has delegated access to a vendor whose review_date in the vendor registry (controldata/taxonomy/vendor_registry.yaml) has passed. The access has not been formally reviewed since the scheduled date. Vendor access without periodic review becomes abandoned access: the vendor's needs may have changed, their security posture may have degraded, the team that managed the integration may have left, or the integration may no longer be needed. Time degrades assurance, and a passed review date means the operator's re-verification commitment was not kept.
Remediation: Conduct the overdue vendor access review. Confirm the vendor relationship is still active, verify the vendor's current security posture, confirm the declared scope still matches operational need, and update review_date in controldata/taxonomy/vendor_registry.yaml. If the relationship has ended, remove the vendor entry from the registry and remove the corresponding principal from the bucket policy.
The customer account does not retain the ability to revoke this vendor's access to the bucket. This can occur when the vendor owns the bucket in the customer's namespace (cross-account bucket ownership), when the vendor's IAM role has s3:PutBucketPolicy on this bucket (the vendor can restore their own access after revocation), or when the bucket policy uses a condition the customer cannot modify. Irrevocable vendor access means the customer cannot contain a vendor-side breach: if the vendor is compromised, the customer must wait for the vendor to remediate, or delete the bucket entirely.
Remediation: Verify bucket ownership is with the customer account (not the vendor's account). Remove s3:PutBucketPolicy and s3:PutBucketAcl from the vendor's allowed actions. Ensure the customer's IAM retains s3:PutBucketPolicy on this bucket so customer-side changes can override vendor changes. Re-run the collector to refresh delegation.customer_can_revoke.
A registered vendor's actual permissions on this bucket exceed the scope declared for them in the vendor registry. The vendor was approved for specific actions (e.g., PutObject to raw-data/*) but the bucket policy grants additional actions (e.g., DeleteObject, PutBucketPolicy) beyond the declared purpose. Scope creep in vendor permissions is the supply-chain equivalent of IAM privilege creep: the vendor's declared purpose says "ingest data"; the actual permissions say "ingest data, delete data, and rewrite the bucket policy." The gap is the attack surface.
Remediation: Compare the vendor's actual_actions against allowed_actions in vendor_registry.yaml. For each exceeded action: if it is still needed, update the vendor registry to reflect the new scope and obtain approval; if not, remove the action from the bucket policy. The exceeded actions are listed in the finding evidence under delegation.external_principals[].exceeded_actions.
S3 buckets tagged with a non-public data classification (phi, pii, confidential, internal) must be monitored by Amazon Macie. Macie uses machine learning and pattern matching to discover and classify sensitive data, detecting PII, PHI, and credentials that may have been stored without proper controls. Without Macie, sensitive data can accumulate undetected in buckets that were not originally intended for it.
Remediation: Enable Amazon Macie in the account and region, then add this bucket to a Macie classification job. Use aws macie2 create-classification-job to configure automated scanning. For organization-wide coverage, enable Macie via AWS Organizations delegated administrator.
Buckets monitored by Macie must have automated sensitive data discovery actively running, not just enabled. A Macie classification job can exist but be paused, cancelled, or have never completed a scan. Without active discovery, new sensitive data uploaded after the last scan goes undetected. Automated discovery continuously samples bucket contents to find sensitive data as it arrives.
Remediation: Verify the Macie classification job for this bucket is in RUNNING status. If paused, resume it with aws macie2 update-classification-job. Enable automated sensitive data discovery at the account level with aws macie2 update-automated-discovery-configuration to ensure continuous sampling of all monitored buckets.
S3 buckets must have server-side encryption enabled. Unencrypted storage is the top audit finding in regulated industries.
Remediation: Enable default bucket encryption using SSE-S3 (AES256) or SSE-KMS. Use aws s3api put-bucket-encryption to set the default encryption configuration. For sensitive data, use SSE-KMS with a customer-managed key.
S3 buckets tagged with data-classification=phi must use SSE-KMS encryption with a customer-managed key (CMK), not the default AWS-managed key or SSE-S3. This ensures the organization controls key rotation, access policies, and audit logging for PHI data at rest.
Remediation: Change the bucket default encryption to SSE-KMS and specify a customer-managed KMS key ARN. Ensure the KMS key policy grants access only to authorized principals. Enable KMS key rotation.
S3 buckets with any non-public data classification must use SSE-KMS encryption with a customer-managed key, not SSE-S3 (AES256). AES256 uses AWS-managed keys with no customer control over key rotation, access policies, or audit trails. This fires on all classified data except explicitly public or non-sensitive buckets.
Remediation: Change the bucket default encryption to SSE-KMS with a customer-managed key. Re-encrypt existing objects by copying them in place with the new encryption settings.
S3 bucket uses a KMS key owned by a different AWS account. The external account controls the key — they can revoke access, disable the key, or modify the key policy at any time. Data encrypted with an externally-owned key is accessible only as long as the external account permits. This is a supply chain risk for encryption.
Remediation: Use a KMS key owned by the same account as the bucket. If cross-account key usage is required, ensure the key policy grants only the minimum necessary permissions and the key owner is within the organization.
The KMS key used for S3 encryption has an overly broad key policy — kms:* to root, or kms:Decrypt to principals beyond the intended S3 service. An overly broad KMS key policy undermines encryption. If the key policy grants kms:Decrypt to many principals, the encryption is nominal — anyone with decrypt access can read the data regardless of S3 bucket policies.
Remediation: Scope the KMS key policy to the specific principals and services that need access. Use kms:ViaService condition to restrict usage to s3.amazonaws.com. Remove kms:* grants and limit kms:Decrypt to authorized principals.
An S3 bucket has server-side encryption enabled but also allows public read access. Encryption protects data at rest on disk, but any principal who can read the object via GetObject receives the plaintext — S3 decrypts transparently on read. When the bucket is publicly readable, encryption provides zero confidentiality benefit: the data is served unencrypted over HTTPS to anyone who requests it. The encryption creates a false sense of data protection in audit output. Individually, the encryption control passes and the public-access control fails — the compound finding is that the passing encryption control is meaningless because the data is publicly readable.
Remediation: Enable Block Public Access on the bucket (all 4 sub-flags) to make the encryption meaningful. If public access is intentional, remove encryption from the bucket's security posture description — it is not providing confidentiality.
S3 bucket policies should deny requests using TLS versions below 1.2 via the s3:TlsVersion condition key. The aws:SecureTransport condition only distinguishes HTTP from HTTPS but does not enforce a minimum TLS version — a client using TLS 1.0 passes the SecureTransport check. The s3:TlsVersion condition key (available since 2020) provides granular version enforcement.
Remediation: Add a bucket policy statement that denies all actions when s3:TlsVersion is less than 1.2. This supplements the aws:SecureTransport condition by enforcing a version floor.
Glacier-archived objects that are restored inherit the bucket's access policy without additional restrictions. If the bucket policy allows broad access, the restored copy is accessible to anyone the bucket policy permits during the restore window. Glacier archival is often treated as a security boundary but restored objects become standard S3 objects for the duration of the restore window.
Remediation: Add a bucket policy condition restricting access to restored objects. Use s3:ExistingObjectTag or scoped IAM policies to limit who can read restored Glacier objects during the restore window.
Glacier restore window exceeds 7 days. Extended restore windows leave formerly-archived data in a standard-accessible state longer than necessary. The restore window should be the minimum duration required for the restore purpose.
Remediation: Reduce the Glacier restore window to the minimum duration required. Use 1-3 day restore windows for operational restores. Extended restore windows leave archived data in a standard-accessible state unnecessarily.
S3 buckets must have a data-classification tag. Without this tag, tag-conditional controls for PHI, PII, confidential data, backup integrity, and compliance retention cannot evaluate — the bucket silently passes all sensitivity-gated checks regardless of actual content.
Remediation: Add a data-classification tag to the bucket with an appropriate value (e.g., phi, pii, confidential, internal, public, non-sensitive). Update your tagging policy to require this tag on all S3 buckets.
S3 bucket safety cannot be proven when policy or ACL data is missing from the snapshot.
Remediation: Re-run the observation collector with full permissions to read bucket policies and ACLs. Ensure the collector IAM role has s3:GetBucketPolicy, s3:GetBucketAcl, and s3:GetBucketPolicyStatus permissions.
S3 Batch Operations can copy millions of objects across buckets in a single job. An attacker with s3:CreateJob permission can initiate a batch copy of all objects in a bucket to an external account — staging data for exfiltration without triggering per-object API calls. Batch operations generate a single CloudTrail event rather than millions of GetObject events — making large-scale data collection harder to detect via API call volume monitoring.
Remediation: Restrict s3:CreateJob to approved IAM roles. Add bucket policy conditions preventing cross-account batch operations. Deny s3:CreateJob when the destination account does not match the source account.
S3 buckets must have S3 Inventory configured to provide a complete manifest of all objects, their storage classes, encryption status, and optionally their ACL grants. Without Inventory, organizations have no baseline visibility into what data exists in a bucket, making it impossible to detect misplaced sensitive files, verify encryption coverage, or audit object-level access. S3 Inventory is essential when Amazon Macie is not deployed, as it provides the only mechanism for systematic bucket content auditing at scale.
Remediation: Configure S3 Inventory on the bucket using aws s3api put-bucket-inventory-configuration. Include optional fields for encryption status and ACL grants. Set the inventory to report daily or weekly to a secured destination bucket. Use the inventory reports to audit for misplaced sensitive data, unencrypted objects, and objects with public ACL grants.
Retention-Tagged Buckets Must Have Lifecycle Rules
Severity: medium
Type: unsafe_state
Domain: exposure
Compliance: nist_800_53_r5: SI-12; soc2: C1.2;
S3 buckets tagged with data-retention must have at least one enabled lifecycle rule configured. HIPAA requires defined data retention policies for protected health information (PHI), audit logs, and billing records. Without lifecycle rules, data persists indefinitely, increasing exposure surface and violating retention policy requirements.
Remediation: Add S3 lifecycle rules to manage object expiration and transitions. Configure rules matching the retention period specified in the data-retention tag. Use lifecycle transitions to move data to cheaper storage classes before expiration.
PHI Buckets Must Not Expire Data Before Minimum Retention
Severity: medium
Type: unsafe_state
Domain: exposure
Compliance: nist_800_53_r5: SI-12;
S3 buckets tagged with data-classification=phi must not have lifecycle expiration rules that delete data before the minimum HIPAA retention period. HIPAA requires medical records to be retained for a minimum of 6 years (2190 days). This control detects PHI buckets with expiration rules set below this threshold, which could result in premature deletion of protected health information.
Remediation: Increase the lifecycle expiration period to at least the configured min_retention_days value. If the current rule is for storage class transition, ensure the expiration rule is separate and meets the minimum retention period.
S3 bucket has no lifecycle rule that aborts incomplete multipart uploads. An attacker who can initiate uploads (s3:PutObject) can start millions of multipart uploads and never complete them. The incomplete parts accumulate storage costs indefinitely — there is no automatic cleanup. This is a denial-of-wallet vector: the attacker pays nothing (no completed objects), the bucket owner pays for every stored part. A lifecycle rule with AbortIncompleteMultipartUpload and a short DaysAfterInitiation threshold (e.g. 7 days) limits exposure.
Remediation: Add a lifecycle rule with AbortIncompleteMultipartUpload set to a short DaysAfterInitiation threshold (7 days is typical). Apply to the entire bucket (no prefix filter) to ensure all incomplete uploads are cleaned up.
S3 buckets must not allow anonymous s3:ListBucket access. Anonymous bucket listing exposes all object keys to unauthenticated users, enabling reconnaissance of stored data. Attackers use object key enumeration to identify sensitive files, backup archives, and configuration artifacts before attempting data exfiltration.
Remediation: Enable S3 Public Access Block with BlockPublicPolicy and RestrictPublicBuckets set to true. Remove any bucket policy statements granting s3:ListBucket to Principal "*". Remove any ACL grants to the AllUsers group.
S3 buckets tagged with any compliance framework (soc2, gdpr, hipaa, pci-dss, etc.) must have S3 Object Lock enabled. Object Lock provides WORM (Write Once Read Many) protection, preventing objects from being deleted or overwritten for a specified retention period. Regulatory frameworks require immutable storage for audit logs, compliance records, and protected data.
Remediation: Enable S3 Object Lock on the bucket. Note: Object Lock can only be enabled at bucket creation. If the bucket already exists, create a new bucket with Object Lock enabled and migrate objects. Set a default retention period appropriate for your compliance framework.
S3 buckets tagged with data-classification=phi that have Object Lock enabled must use COMPLIANCE mode, not GOVERNANCE mode. COMPLIANCE mode prevents ANY user, including the root account, from deleting or overwriting protected objects during the retention period. GOVERNANCE mode allows users with special permissions to override retention, which is insufficient for HIPAA-regulated PHI data where tamper-proof storage is required.
Remediation: Change the Object Lock default retention mode from GOVERNANCE to COMPLIANCE. In COMPLIANCE mode, no user (including root) can delete or modify protected objects during the retention period.
PHI Object Lock Retention Must Meet Minimum Period
Severity: medium
Type: unsafe_state
Domain: exposure
S3 buckets tagged with data-classification=phi that have Object Lock enabled must have a default retention period of at least 2190 days (6 years) to meet HIPAA minimum retention requirements. Shorter retention periods risk premature expiration of WORM protection, allowing deletion or modification of PHI data before the regulatory retention period has elapsed.
Remediation: Increase the Object Lock default retention period to at least 2190 days. Use aws s3api put-object-lock-configuration to update the default retention settings.
S3 Object Lock is enabled but no objects have legal hold applied. For organizations subject to litigation hold requirements, legal hold prevents object deletion or modification regardless of retention period. Without legal hold, objects can be deleted after the retention period expires — even if a litigation hold requires indefinite preservation.
Remediation: Apply legal hold to objects that must be preserved for litigation, regulatory investigation, or eDiscovery obligations. Use s3:PutObjectLegalHold to apply legal hold to specific objects or object prefixes.
S3 buckets must have server access logging enabled for audit trail and visibility into data access patterns.
Remediation: Enable S3 server access logging and specify a target bucket for log delivery. Ensure the target bucket has appropriate access controls and is in the same region.
When server access logging is enabled, the log destination bucket must have a lifecycle policy configured. Without lifecycle management, log storage grows unbounded, increasing costs and making audit analysis impractical.
Remediation: Configure a lifecycle policy on the log destination bucket to manage log retention. Transition older logs to cheaper storage classes and expire logs after the required retention period.
When server access logging is enabled, the log destination bucket must have Object Lock enabled. Without Object Lock, log files can be deleted by any principal with s3:DeleteObject permission, even with versioning enabled.
Remediation: Enable Object Lock on the log destination bucket with a retention policy. Object Lock prevents log file deletion for the retention period, ensuring audit trail immutability. Note that Object Lock must be enabled at bucket creation time.
When server access logging is enabled, the log destination bucket must not be publicly accessible. Public log buckets expose audit trail contents to external actors, enabling reconnaissance and detection evasion.
Remediation: Block all public access on the log destination bucket using the S3 Block Public Access settings. Enable all four block public access options to prevent any public access configuration.
When server access logging is enabled, the log destination bucket must have versioning enabled. Without versioning, deleted or overwritten log files cannot be recovered, allowing attackers to destroy audit evidence.
Remediation: Enable versioning on the log destination bucket to preserve all versions of log objects. This ensures that even if log files are deleted or overwritten, previous versions remain recoverable.
When server access logging is enabled, a target prefix must be configured to organize log files and enable efficient search and analysis of audit records.
Remediation: Set a target prefix for S3 server access logging to organize log files by source bucket. Use a prefix that identifies the source bucket, such as the bucket name followed by a slash.
When server access logging is enabled, the log destination bucket must retain logs for at least 90 days. Short retention periods allow attackers to wait out the retention window, after which evidence of unauthorized access is automatically destroyed.
Remediation: Set the lifecycle expiration on the log destination bucket to at least 90 days. Many compliance frameworks require longer retention; consult your compliance team for the appropriate retention period.
S3 buckets tagged with data-classification=phi must have malware scanning enabled via GuardDuty S3 Malware Protection or an equivalent scanning pipeline. Without scanning, uploaded files containing malware can persist in PHI storage indefinitely, creating both a security risk (malware distribution) and a compliance violation (HIPAA §164.308(a)(5)(ii)(B) requires protection against malicious software).
Remediation: Enable GuardDuty S3 Malware Protection for the bucket. Navigate to GuardDuty > S3 Protection > Enable. Alternatively, deploy a Lambda-based AV scanning pipeline triggered by S3 PutObject events.
Fact-recording marker for S3 buckets tagged data-classification=phi. Emits an informational finding (NOT a violation) on every PHI-tagged bucket so cross-resource chains can compose it with violation findings on related identities or resources. Examples: a Cognito unauth role with s3:GetObject targeting a PHI bucket; an IAM role outside the covered entity that can AssumeRole to a principal that reaches a PHI bucket. PHI tagging on its own is the desired state — never a finding to triage. The marker exists so the chain engine can join "violation on the access side" with "data classification on the storage side" without forcing the collector to denormalise every join shape into a per-asset boolean.
Remediation: None. This marker exists as a chain-detection ingredient. To suppress noise on dashboards, filter findings by control_id when rendering Findings — markers should appear in a separate "facts" panel rather than the violation list.
S3 buckets should have MFA Delete enabled on versioned buckets. MFA Delete requires a second factor to permanently delete object versions, preventing unauthorized or accidental data destruction.
Public-Principal Policies Must Have Network Conditions
Severity: high
Type: unsafe_state
Domain: exposure
Compliance: nist_800_53_r5: AC-3;
S3 bucket policies that grant access to Principal * (any AWS principal) must include network-scoping conditions such as aws:SourceIp, aws:sourceVpce, aws:SourceVpc, or aws:PrincipalOrgID. Without these conditions, the bucket is accessible to anyone on the internet. This control detects policies where wildcard principals are used without network restrictions.
Remediation: Add network-scoping conditions to the bucket policy: aws:SourceIp for IP range restrictions, aws:SourceVpce for VPC endpoint restrictions, aws:SourceVpc for VPC restrictions, or aws:PrincipalOrgID for organization-only access.
VPC endpoint policy must be attached and must not be the default full-access policy (Allow * on *). The default policy allows any principal on the VPC to reach any S3 bucket in any account via the endpoint, bypassing firewall controls. A restrictive endpoint policy limits which bucket ARNs and actions are reachable.
Remediation: Replace the default endpoint policy with one that restricts Resource to specific bucket ARNs and Action to required S3 operations only.
S3 bucket access must be restricted by a VPC endpoint condition (aws:SourceVpce) or an IP address condition (aws:SourceIp) in the bucket policy. Without network-level restrictions, the bucket is reachable from any network path. This control enforces transmission security for PHI workloads.
Remediation: Add a VPC gateway endpoint for S3 and route bucket traffic through it, or add an IP condition (aws:SourceIp) to the bucket policy to restrict access to known CIDR ranges.
S3 Event Notifications Must Not Target Deleted Resources
Severity: high
Type: unsafe_state
Domain: detection
Compliance: nist_800_53_r5: SI-4; soc2: CC7.1;
S3 bucket event notification configurations must not target deleted SNS topics, SQS queues, or Lambda functions. Object events (PutObject, DeleteObject) go undelivered when the target is absent. If notifications feed security monitoring, the monitoring stops.
Remediation: Update the notification configuration to reference existing resources.
S3 Object Lambda Access Point References Missing Lambda
Severity: high
Type: unsafe_state
Domain: governance
Compliance: nist_800_53_r5: CM-8; soc2: CC6.1;
S3 Object Lambda Access Point references a Lambda function that no longer exists. GetObject calls against the access point fail silently or fall back to insecure paths depending on the caller's retry logic. The access point exists in the inventory, is reachable, and produces non-deterministic behavior on access.
Remediation: Either delete the orphaned access point or repoint it at an existing Lambda function. Validate the function ARN against the live IAM inventory before re-attaching.
S3 Object Lambda Access Point grants external invocation while the underlying bucket's policy explicitly does not permit external access. The access point is a privilege-escalation bypass of the bucket policy: external callers who would be denied at the bucket layer reach the bucket's data indirectly by invoking the transformation function whose Lambda role does have bucket access.
Remediation: Either tighten the access point policy to match the bucket policy (deny the same external accounts), or relax the bucket policy to match the access point's intentional exposure (rarely the right answer for sensitive data). The two policies should never disagree in the direction of "access point looser than bucket." If the access point is intentionally cross- account, document the bucket-policy carve-out explicitly so auditors don't read the bucket policy as the source of truth.
S3 Object Lambda Access Point's resource policy grants invocation to principals outside the access point's own AWS account. External callers reach the transformation function — which in turn reads the underlying bucket data on their behalf — bypassing any account-boundary controls the bucket policy itself enforces.
Remediation: Restrict the access point resource policy's Principal to accounts inside the organization. If cross-account access is intentional, pair it with explicit Condition keys naming the expected aws:SourceAccount and aws:SourceArn values and verify the underlying bucket policy also permits the same accounts directly — never let the access point be the looser layer.
The Lambda function backing an S3 Object Lambda Access Point has an execution role with S3 read permission on more buckets than the access point's underlying bucket. An attacker who can invoke the access point gets the Lambda's full reach, not the access point's narrower promise — the transformation function reads buckets the access-point policy never exposes.
Remediation: Scope the Lambda's execution role to the single underlying bucket the access point exposes. Use a resource-bounded s3:GetObject policy with the access point's underlying bucket ARN as the only resource. Audit the role's other bucket references and remove any that aren't required for the transformation logic.
S3 buckets must have Object Ownership set to BucketOwnerEnforced, which disables ACLs entirely. When ACLs are disabled, the bucket owner automatically owns every object regardless of who uploaded it, and access is controlled exclusively through IAM and bucket policies. This eliminates the entire class of ACL-based exposure: public grants, privilege escalation via WRITE_ACP, and object-level ACL overrides. Since April 2023 new buckets default to BucketOwnerEnforced, but buckets created before this date may still have ACLs enabled.
Remediation: Set Object Ownership to BucketOwnerEnforced using aws s3api put-bucket-ownership-controls. This disables all ACLs on the bucket. Before enabling, audit existing ACL grants and migrate any legitimate access to bucket policies or IAM policies. All existing ACL-based access will stop working once BucketOwnerEnforced is set.
The BlockPublicAcls flag of the bucket's Public Access Block configuration rejects any new ACL grant that would make the bucket or its objects publicly accessible. When this specific flag is false, new PUT-ACL calls that grant READ, WRITE, READ_ACP, or WRITE_ACP to http://acs.amazonaws.com/groups/global/AllUsers or .../AuthenticatedUsers succeed rather than being rejected at the API boundary. The umbrella CTL.S3.CONTROLS.001 fires when any of the four PAB flags is off; this control narrows the finding to the specific flag so remediation is a one-command fix rather than requiring the operator to enumerate which of the four is missing. Prowler and ScoutSuite both report the four flags independently.
Remediation: Enable the BlockPublicAcls flag on the bucket's Public Access Block configuration. From the CLI:
The BlockPublicPolicy flag of the bucket's Public Access Block configuration rejects any new bucket-policy update that would evaluate as public under AWS PolicyStatus.IsPublic. When this specific flag is false, a PutBucketPolicy call with Principal: "*" and no restricting Condition succeeds rather than being rejected at the API boundary. The umbrella CTL.S3.CONTROLS.001 fires when any of the four PAB flags is off; this control narrows the finding to the specific flag so remediation targets the policy-write path rather than the full PAB tuple. Prowler and ScoutSuite both report the four flags independently.
Remediation: Enable the BlockPublicPolicy flag on the bucket's Public Access Block configuration. Preserve the other three flag values so enabling this one doesn't silently disable the others.
The IgnorePublicAcls flag of the bucket's Public Access Block configuration causes S3 to disregard any existing ACL grant that would make the bucket or its objects publicly accessible, even if the grant is already in place. When this specific flag is false, historical ACL grants to AllUsers or AuthenticatedUsers stay effective — enabling BlockPublicAcls alone does not neutralize grants that were made before the flag was enabled. The umbrella CTL.S3.CONTROLS.001 fires when any of the four PAB flags is off; this control narrows the finding to the specific flag so the operator knows that the fix is not "block new ACLs" but "ignore the ones already there." Prowler and ScoutSuite both report the four flags independently.
Remediation: Enable the IgnorePublicAcls flag on the bucket's Public Access Block configuration. Pair it with BlockPublicAcls for full ACL-path coverage — the pair prevents new public ACL grants and neutralizes existing ones.
The RestrictPublicBuckets flag of the bucket's Public Access Block configuration restricts evaluation of any existing public bucket policy: when the flag is on, public grants in the bucket policy are limited to AWS service principals and authorized AWS services (e.g., CloudFront OAC). When this specific flag is false, historical Principal: "*" grants stay fully effective — enabling BlockPublicPolicy alone does not neutralize policies that were in place before the flag was turned on. The umbrella CTL.S3.CONTROLS.001 fires when any of the four PAB flags is off; this control narrows the finding to the specific flag so the operator knows the fix is not "block new public policies" but "restrict the ones already there." Prowler and ScoutSuite both report the four flags independently.
Remediation: Enable the RestrictPublicBuckets flag on the bucket's Public Access Block configuration. Pair it with BlockPublicPolicy for full policy-path coverage — the pair prevents new public policy writes and limits the effect of any historical public grants to service principals.
S3 bucket's data perimeter is missing one or more of the three required dimensions — identity, network, or confused deputy. A complete data perimeter ensures: (1) identity — only principals from the organization can access the bucket (aws:PrincipalOrgID); (2) network — access is restricted to known network paths with coverage for both internet (aws:SourceIp) and VPC endpoint (aws:SourceVpce) traffic; (3) confused deputy — AWS services acting on behalf of other accounts are restricted to the organization (aws:SourceOrgID). Any missing dimension creates a bypass path that the other dimensions cannot compensate for. Identity without network allows exfiltration from compromised VPC workloads. Network without identity allows any account's credentials to reach the bucket from allowed networks. Both without confused deputy allow service-to-service cross-organization access. This control is the meta-check — it fires when any dimension is absent, complementing the per-dimension controls that identify which specific dimension is missing.
Remediation: Implement all three data perimeter dimensions. Identity: add aws:PrincipalOrgID to all cross-account grants. Network: add both aws:SourceIp (for internet access) and aws:SourceVpce or aws:SourceVpc (for VPC endpoint access) — SourceIp alone is bypassed by VPC endpoint traffic. Confused deputy: add aws:SourceOrgID to all service-principal grants. Enforce at the SCP/RCP level to cover all resource policies in the account. Reference: AWS data perimeter guidance at docs.aws.amazon.com/whitepapers/latest/building-a-data-perimeter-on-aws.
S3 Bucket Service Principal Grant Without SourceOrgID
Severity: medium
Type: unsafe_state
Domain: identity
Compliance: nist_800_53_r5: AC-3; soc2: CC6.1;
S3 bucket policy grants access to an AWS service principal (e.g., cloudtrail.amazonaws.com, logging.s3.amazonaws.com) without an aws:SourceOrgID condition. Without the confused deputy dimension of the data perimeter, the service principal can write to or read from the bucket on behalf of any AWS account — not just accounts in the organization. An attacker who controls a different AWS account can configure the same service to deliver data to the victim's bucket (write path) or request data through the service on the victim's behalf (read path). aws:SourceOrgID restricts service-principal grants to requests originating from accounts within the specified organization. This is distinct from aws:SourceAccount (single account) and sts:ExternalId (third-party roles) — SourceOrgID scales across the entire organization without per-account updates.
Remediation: Add an aws:SourceOrgID condition to all service-principal Allow statements: {"StringEquals": {"aws:SourceOrgID": "o-exampleorgid"}}. For services that also support aws:SourceAccount (CloudTrail, Config, ELB access logs), SourceOrgID is preferred because it covers all accounts in the org without enumeration. For services that do not support SourceOrgID, fall back to aws:SourceAccount with explicit account IDs.
S3 bucket policy grants cross-account access without an aws:PrincipalOrgID or aws:PrincipalOrgPaths condition. Without the identity dimension of the data perimeter, any AWS account — inside or outside the organization — that obtains valid credentials can access the bucket. PrincipalOrgID restricts access to principals whose accounts are members of the specified AWS Organization, collapsing the reachable principal set from every AWS account on Earth to only accounts in the org. This condition is automatically satisfied by all principals in the org without per-account enumeration, making it the scalable identity boundary for resource policies. Without it, adding a new account to the org requires updating every resource policy that should grant access — a gap that grows with org size.
Remediation: Add an aws:PrincipalOrgID condition to all cross-account Allow statements: {"StringEquals": {"aws:PrincipalOrgID": "o-exampleorgid"}}. For SCPs, enforce at the org level so all resource policies inherit the boundary. For buckets that must grant access outside the org (partner integrations), use aws:PrincipalAccount to enumerate specific accounts instead of relying on open cross-account grants.
S3 bucket policy uses aws:SourceIp for network restriction but lacks a VPC endpoint condition (aws:SourceVpce or aws:SourceVpc). When requests route through a VPC gateway endpoint for S3, the aws:SourceIp key is absent from the request context — the IP condition silently evaluates to no match, and a Deny-with-SourceIp fails to deny. An attacker who compromises a workload inside a VPC with an S3 gateway endpoint can exfiltrate data from the bucket while the SourceIp restriction provides no protection. The fix is not to remove SourceIp (it still protects against direct internet access) but to add aws:SourceVpce or aws:SourceVpc alongside it so the policy covers both network paths. This is the most common data perimeter gap in AWS environments and the canonical example in AWS's data perimeter guidance.
Remediation: Add a VPC endpoint condition alongside the existing SourceIp condition. Use aws:SourceVpce to restrict to specific VPC endpoints or aws:SourceVpc to restrict to specific VPCs. The policy should deny access when the request comes from neither an allowed IP range nor an allowed VPC endpoint. Example Deny statement condition: {"StringNotEquals": {"aws:SourceVpce": "vpce-1a2b3c4d"}, "NotIpAddress": {"aws:SourceIp": "203.0.113.0/24"}}.
S3 bucket policy exempts all AWS service principals from data perimeter conditions using a broad aws:PrincipalIsAWSService condition without restricting which services are exempted. A condition like {"Bool": {"aws:PrincipalIsAWSService": "true"}} applied to an Allow statement with no further scoping grants every AWS service principal access to the bucket, bypassing the identity and network dimensions of the data perimeter. Only services that actually need access (e.g., CloudTrail for log delivery, Config for snapshots) should be exempted, and each exemption should be scoped with aws:SourceOrgID to maintain the confused deputy boundary. Broad service exemptions are the most common cause of data perimeter completeness failures because they appear to preserve the perimeter while creating a bypass path through any AWS service.
Remediation: Replace the broad PrincipalIsAWSService exemption with specific service principal grants scoped by aws:SourceOrgID. For each service that needs access, add a separate statement: {"Effect": "Allow", "Principal": {"Service": "cloudtrail.amazonaws.com"}, "Action": "s3:PutObject", "Resource": "arn:aws:s3:::bucket/*", "Condition": {"StringEquals": {"aws:SourceOrgID": "o-exampleorgid"}}}. If the PrincipalIsAWSService exemption is required for compatibility, at minimum add aws:SourceOrgID to the same statement to maintain the confused deputy boundary.
Bucket Policy Must Not Allow Access Despite IAM Deny
Severity: medium
Type: unsafe_state
Domain: exposure
Compliance: nist_800_53_r5: AC-3; soc2: CC6.1;
An IAM policy explicitly denies S3 actions for a principal, but the bucket policy allows the same actions for the same principal. In AWS policy evaluation for same-account access, an explicit Deny in any policy always wins — so this is NOT an active bypass. However, the configuration creates a maintenance hazard: if the IAM Deny is later removed (policy update, detach, or role change), the bucket policy Allow silently activates. The latent grant is invisible to IAM auditing because the Deny masks it. This control flags the conflicting policy pair so operators can remove the bucket policy Allow rather than relying on a Deny that may be transient.
Remediation: Remove the Allow statement from the bucket policy. Do not rely on the IAM Deny as the sole enforcement mechanism — Deny statements are often transient (attached to temporary policies, permission boundaries, or SCPs that change during reorganization).
S3 bucket policies must not grant s3:GetBucketPolicy to an anonymous or wildcard principal. A publicly readable bucket policy is a reconnaissance primitive: anyone can retrieve the policy, enumerate which specific object ARNs are granted public read, and use that map to target the exposed content. Reju Kole's disclosure (January 2026) exploited exactly this: the target bucket granted s3:GetBucketPolicy to Principal "*", the researcher read the policy to discover that backup.xlsx was publicly granted, downloaded the file, cracked its Office password offline, and used the recovered credentials for further compromise. This control is distinct from controls that fire on the exposed objects themselves (CTL.S3.PUBLIC.001, .004) — the policy-disclosure primitive is what makes object-scoped public grants discoverable, and closing it removes the reconnaissance step even when other public grants remain.
Remediation: Remove any policy statement granting s3:GetBucketPolicy to Principal "" or to AWS: "". If the policy must remain readable for tooling, restrict the grant with a Condition on aws:PrincipalOrgID, aws:SourceVpc, or a fixed aws:SourceIp CIDR. Enable S3 Public Access Block with BlockPublicPolicy set to true to reject future policies that grant public access to bucket metadata.
S3 buckets must have an explicit resource-based bucket policy. Without a policy, access controls rely entirely on IAM and ACLs — there is no resource-level enforcement of encryption requirements, VPC restrictions, or transport security.
Remediation: Attach a bucket policy that restricts access to known principals, VPC endpoints, or organization IDs (aws:PrincipalArn, aws:PrincipalOrgID, aws:SourceVpc, aws:SourceVpce). The scoped Allow is the primary control: it defines who can reach the bucket at all and is the line of defence that would still hold if every other guard were removed.
Layered Deny statements (deny aws:SecureTransport=false to enforce TLS, deny PutObject without server-side encryption headers) are valuable secondary guards but do not replace a scoped Allow — they harden a bucket whose Allow is already narrow, they do not rescue one whose Allow is broad. Apply them after the Allow has been scoped, not instead of scoping it.
Warning: do not add Deny statements before scoping the Allow. A Deny that references your own principals (e.g. denying access unless aws:PrincipalOrgID matches and SourceIP is on a missing list) can lock the account out of its own bucket if the Allow has not been verified to admit the operator first. Order matters: scope the Allow, verify access still works, then add Deny guards.
S3 bucket policies must not grant access to principal ARNs that don't exist in the IAM inventory. Unlike IAM trust policies (which replace deleted ARNs with unique IDs), resource-based policies evaluate ARN strings directly. A new entity created with the same name as a deleted principal inherits every permission the bucket policy grants. An attacker with iam:CreateRole can claim the deleted principal's name and gain bucket access.
Remediation: Remove the ghost principal ARN from the bucket policy or recreate the intended principal.
Bucket Policy Must Not Self-Lock Out Administration
Severity: high
Type: unsafe_state
Domain: governance
Compliance: nist_800_53_r5: AC-6;
An S3 bucket policy must not contain a Deny statement that blocks the bucket owner's own policy-management actions (s3:PutBucketPolicy, s3:DeleteBucketPolicy) without a guaranteed administrative carve-out. The classic self-lockout pattern is a broad "Deny s3:* unless aws:SourceIp / aws:SourceVpce matches" that also covers policy management: if the operator loses that network path — an office IP change, a deleted VPC endpoint, a rotated CIDR — then no principal can edit the policy to recover. The bucket, often an audit-log or compliance bucket, becomes permanently unmanageable without AWS root or Support intervention. This is a resilience failure, not an exposure: the data stays private, but administration is irrecoverable.
Remediation: Carve out break-glass administration from the Deny. Either (a) add an explicit Allow for s3:PutBucketPolicy / s3:DeleteBucketPolicy to a dedicated administrative role that is assumable WITHOUT the network condition, or (b) scope the Deny to data-plane actions only (s3:GetObject, s3:PutObject, ...) and keep policy-management actions out of the Deny entirely. Never gate policy-management behind a single IP/VPC condition.
Bucket policy contains at least one Allow statement with a non-narrow principal whose Resource field names one or more specific object keys (e.g. arn:aws:s3:::bucket/backup.xlsx) rather than the whole bucket (arn:aws:s3:::bucket/*) or a logical prefix (bucket/assets/*). Object-scoped public grants are a distinct attack surface from bucket-wide public grants: the specific object ARN in the policy readback (exposed by iam:GetBucketPolicy if that permission is also public — see CTL.S3.POLICY.DISCLOSURE.001) hands an attacker a targeting list. The January 2026 Reju Kole disclosure chained exactly this pattern: a public GetBucketPolicy grant revealed that backup.xlsx was individually granted to Principal: "*", the researcher fetched it, cracked its Office password offline, and reused the recovered credentials for further compromise. Prowler and Pacu treat object-scoped grants as a distinct finding class for the same reason.
Severity is medium, not high, because object-scoped public grants are a legitimate pattern for individually-published documents (PDFs, binaries, static assets pinned to specific keys). Operators triage based on the specific object keys listed in the finding's diagnostic context. When the target bucket also carries storage.tags.data-classification in [phi, pii, confidential], CTL.S3.PUBLIC.002 already fires at high severity on the composite signal — this control does not duplicate that coverage; it catches the untagged case where the object's sensitivity is not expressed in the contract.
Remediation: Review each object listed as a public target. For intentionally published documents, move to a published-assets prefix pattern (bucket/public/*) so the policy no longer enumerates individual keys. For anything not intentionally public, remove the grant. Consider moving published documents to a dedicated bucket so the host bucket never has object-scoped public grants at all. If the bucket also has s3:GetBucketPolicy open to the public (see CTL.S3.POLICY.DISCLOSURE.001), close that first — it is the discovery primitive that makes object-scoped grants exploitable without prior knowledge of key names.
Bucket Policy Grants s3: via PrincipalOrgID Condition*
Severity: high
Type: unsafe_state
Domain: exposure
Compliance: nist_800_53_r5: AC-6; soc2: CC6.1;
S3 bucket policy grants s3:* (or an equivalently broad action set) to Principal:* conditioned only on aws:PrincipalOrgID. This grants full S3 access to every identity in the AWS Organization — including vendor audit roles, CI/CD service accounts, and any compromised credential. PrincipalOrgID is a useful scoping condition for read-only access but it is not a substitute for least-privilege action grants. The collector pre-computes policy_orgid_broad_action when the bucket policy has a statement with Principal:* plus PrincipalOrgID whose effective action set includes s3:* or covers both read and write families.
Remediation: Replace s3:* with the minimum action set required. For shared data buckets, grant s3:GetObject and s3:ListBucket. For backup destinations, grant only the replication actions (s3:ReplicateObject, s3:ReplicateDelete). If write access is required, scope it to specific principals via aws:PrincipalArn rather than PrincipalOrgID.
S3 bucket policies with Allow statements whose Principal is non-narrow (Principal: "*", Principal: {"AWS": "*"}, or an Allow block with no Principal) should constrain every such statement with at least one scoping Condition: aws:PrincipalOrgID, aws:SourceVpc, aws:SourceIp with a fixed CIDR, or aws:SourceArn. Without a scoping Condition, the effective principal set is the full internet (anonymous) or every AWS account on Earth (AWS: "*"). Scoping Conditions do not fix the name of the principal but they collapse the reachable principal set to callers routed through a known org, VPC, IP range, or service — a posture hardening step that prevents a future policy edit from silently expanding exposure. This control does not fire on buckets with no policy, nor on buckets whose Allow statements all name specific accounts or role ARNs; both states are captured by policy_has_scoping_condition being absent or null.
Remediation: For every Allow statement with Principal: "*", Principal: {"AWS": "*"}, or no Principal block, add a Condition that binds the request to a fixed value: aws:PrincipalOrgID to limit to the organization, aws:SourceVpc to limit to a VPC endpoint, aws:SourceIp with a CIDR to limit to a known range, or aws:SourceArn to limit to a specific caller. If the statement is supposed to be globally reachable (CDN origin, public data distribution), replace the bucket policy grant with a narrower mechanism — Origin Access Control, Access Points, or signed URLs.
The S3 bucket policy grants access to a principal whose IAM policy does not include the corresponding S3 actions. In AWS policy evaluation, a resource policy Allow with a specific principal ARN grants access even when the principal's IAM policy has no S3 permissions — the resource policy acts as a standalone grant for same-account principals. This creates a "shadow allow" invisible to IAM-centric auditing: scanning the principal's attached/inline policies shows no S3 access, but the bucket policy silently grants it. For cross-account principals, both IAM and resource policy must allow the action; for same-account principals, either policy alone is sufficient. This control flags same-account shadow allows where the bucket policy is the sole grant source.
Remediation: Review the bucket policy statements that name specific principal ARNs. For each, verify the principal's IAM policies also grant the same S3 actions. If the bucket policy is the sole source of access, either add the permission to the principal's IAM policy (making it visible) or remove the bucket policy statement.
S3 bucket policies must not grant write access to an anonymous or wildcard principal. Policy-based write access — a Statement with Effect Allow, Principal "" or AWS: "", and an Action such as s3:PutObject or s3:DeleteObject without a restricting Condition — enables attackers to upload malicious objects, overwrite existing content, or hold data ransom. This is distinct from CTL.S3.PUBLIC.003, which fires on the composite public_write signal regardless of mechanism; this control narrows to the policy path so the finding points the operator at the bucket policy specifically. For ACL-based public write, see CTL.S3.ACL.FULLCONTROL.001 (FULL_CONTROL grants) and CTL.S3.ACL.ESCALATION.001 (WRITE_ACP grants).
Remediation: Remove or constrain the policy statement granting write actions to Principal "" or AWS: "". If broad write access is genuinely required, add a restricting Condition on aws:PrincipalOrgID, aws:SourceVpc, or a fixed aws:SourceIp CIDR. Enable S3 Public Access Block with BlockPublicPolicy set to true to reject future policies that grant public write.
Publicly Accessible S3 Bucket Has No Lifecycle Policy
Severity: high
Type: unsafe_state
Domain: exposure
Compliance: nist_800_53_r5: AC-3; soc2: CC6.6;
S3 bucket is publicly accessible AND has no lifecycle policy configured. A public bucket that is actively maintained has an owner who can justify the exposure and manages the data lifecycle. A public bucket with no lifecycle policy has no evidence of active management — it is likely abandoned, with data exposed to the internet and no one responsible for its contents. This is the pollution compound of CTL.S3.NETWORK.001 (public access) and CTL.S3.LIFECYCLE.001 (missing lifecycle) — together they identify the Farris "public buckets that no one knows how they're referenced" pattern.
Remediation: Determine whether the bucket is still needed. If not, delete the bucket and its contents. If the public exposure is intentional, add a lifecycle policy and tag the bucket with an owner and data classification. If the exposure is unintentional, block public access immediately.
PHI bucket policy must restrict presigned URL access using s3:signatureAge (maximum age in milliseconds) or s3:authType (require REST-HEADER to block presigned URLs). Without these guardrails, presigned URLs can provide long-lived unauthenticated access to PHI data.
Remediation: Add a Deny statement with Condition NumericGreaterThan s3:signatureAge (e.g., 600000 for 10 minutes) or StringNotEquals s3:authType REST-HEADER to block presigned URL access.
S3 buckets must not allow public read access. Detects buckets with anonymous read exposure via policy or ACL.
Remediation: Enable S3 Public Access Block (all four settings). Remove any bucket policy statements granting access to Principal "*". Remove any ACL grants to AllUsers or AuthenticatedUsers.
S3 buckets tagged with sensitive data classifications (PHI, PII, confidential) must not allow any public access.
Remediation: Immediately enable S3 Public Access Block (all four settings). Remove bucket policy statements granting access to Principal "*". Remove ACL grants to AllUsers or AuthenticatedUsers. Audit CloudTrail logs for unauthorized access during the exposure window.
S3 buckets must not allow public write or delete access. Public write enables data injection, ransomware, and policy takeover.
Remediation: Remove bucket policy statements that grant s3:PutObject or s3:DeleteObject to Principal "*". Remove ACL grants that allow WRITE or FULL_CONTROL to AllUsers or AuthenticatedUsers. Enable S3 Public Access Block.
S3 bucket policies must not grant read access to an anonymous or wildcard principal. A Statement with Effect Allow, Principal "" or AWS: "", and an Action such as s3:GetObject without a restricting Condition enables any unauthenticated caller to read objects. This is distinct from CTL.S3.PUBLIC.001, which fires on the composite public_read signal regardless of mechanism; this control narrows to the bucket-policy path so the finding points the operator at the policy specifically. For ACL-based public read, see CTL.S3.ACL.FULLCONTROL.001 (FULL_CONTROL grants) and CTL.S3.ACL.ESCALATION.001 (WRITE_ACP grants).
Remediation: Remove or constrain the policy statement granting read actions to Principal "" or AWS: "". If broad read access is genuinely required, add a restricting Condition on aws:PrincipalOrgID, aws:SourceVpc, or a fixed aws:SourceIp CIDR. Enable S3 Public Access Block with BlockPublicPolicy set to true to reject future policies that grant public read.
S3 buckets must not have latent public read exposure where a public mechanism (policy or ACL) is masked only by Public Access Block. Removing PAB would immediately expose the bucket.
Remediation: Remove the underlying public-granting policy statement or ACL entry so the bucket does not depend solely on PAB for protection. Then verify PAB remains enabled as defense-in-depth.
S3 bucket has a policy or ACL that would allow public listing if the public access block were removed. The public access block is currently the only control preventing directory enumeration. This is a latent vulnerability — one configuration change away from exposing all object keys.
Remediation: Remove the underlying policy statement or ACL entry that grants s3:ListBucket to Principal "*" or AllUsers. Do not rely solely on PAB to prevent directory enumeration.
IAM identity-based policies attached to users or roles must not grant broad read access to S3 buckets that is effectively public — for example, a policy attached to a widely-assumed role or to a role trusted by a public federated identity provider. This is distinct from CTL.S3.PUBLIC.004 (resource-based bucket policy granting public read); the identity-policy path is a separate AWS evaluation branch and deserves its own finding so the operator fixes the right artifact.
Remediation: Identify the identity-based policy statement granting read access and either remove it, scope it to specific bucket/object ARNs, or add conditions restricting the requesting principal. If a widely-trusted role is the root cause, tighten that role's trust policy first.
IAM identity-based policies attached to users or roles must not grant broad list access (s3:ListBucket) that is effectively public. Anonymous-or-near-anonymous listing enables enumeration of the bucket contents, which typically precedes targeted object exfiltration. This is distinct from the ACL-based list exposure covered by CTL.S3.PUBLIC.LIST.001 / .002 and from resource-based list grants; the identity-policy path is its own finding.
Remediation: Identify the identity-based policy statement granting s3:ListBucket and remove it, scope it to specific bucket ARNs, or add conditions restricting the requesting principal. If a widely-trusted role is the root cause, tighten that role's trust policy first.
S3 buckets must not allow anonymous listing of objects. Public listing exposes object keys, enabling targeted data exfiltration.
Remediation: Remove bucket policy statements that grant s3:ListBucket to Principal "*". Remove ACL grants that allow READ to AllUsers. Enable S3 Public Access Block.
Anonymous bucket listing increases exposure surface even when objects are public by design. Listing must be explicitly intended via tag.
Remediation: If listing is intentional, add the tag public_list_intended=true to the bucket. Otherwise, remove the policy or ACL granting s3:ListBucket to Principal "*" or AllUsers.
S3 bucket prefixes marked as protected must not be publicly readable. Evaluates bucket policies, ACL grants, and public access block settings to determine effective public read access for each protected prefix. Customize the prefix lists below to match your bucket layout.
Remediation: 1. Review the protected_prefixes and allowed_public_prefixes lists
in this control and adjust them to match your bucket layout.
2. Enable S3 Public Access Block to restrict policy and ACL exposure. 3. Remove bucket policy statements granting s3:GetObject to Principal "*"
for protected prefixes.
4. Remove ACL grants to AllUsers or AuthenticatedUsers.
S3 bucket has oscillated between private and publicly accessible more than twice within 30 days. Repeated public exposure indicates a broken deployment process, operational workaround, or an attacker re-enabling public access. The response is investigation, not remediation — determine who is making the bucket public and how they still have access.
Remediation: Investigate the root cause of the repeated oscillation. Determine whether the pattern indicates a broken process, operational workaround, or active compromise. Review CloudTrail for the API calls that triggered each transition.
S3 buckets containing personal data must be located in approved regions as determined by data residency requirements (e.g., EU/EEA regions for GDPR). Storing data outside approved regions may violate data transfer restrictions.
Remediation: Create a new bucket in an approved region and migrate data. Use S3 replication to move data, then delete the original bucket.
S3 buckets tagged with a compliance framework (soc2, gdpr, hipaa, pci-dss, etc.) must have replication configured. Without replication, a regional outage or accidental bucket deletion can cause permanent data loss for regulated data. Replication provides an independent copy that survives single-region failures and supports disaster recovery objectives.
Remediation: Configure S3 replication on the bucket using aws s3api put-bucket-replication. Use cross-region replication (CRR) for disaster recovery or same-region replication (SRR) for compliance copies. Ensure versioning is enabled on both source and destination buckets.
S3 buckets tagged with data-classification=phi that have replication enabled must replicate to a different AWS region. Same-region replication (SRR) does not protect against regional outages, AZ-wide failures, or region-scoped service disruptions. HIPAA contingency planning requires data to survive regional disasters.
Remediation: Update the replication configuration to use a destination bucket in a different AWS region. Ensure the destination bucket has versioning enabled, appropriate encryption, and a bucket policy that permits the replication role.
When S3 replication is enabled, the destination bucket must have server-side encryption configured. Replicating data to an unencrypted destination creates a shadow copy that bypasses the source bucket's encryption controls. This is especially dangerous for sensitive data where the source meets encryption requirements but the replica does not.
Remediation: Configure default encryption on the destination bucket using SSE-S3 or SSE-KMS. For replication of encrypted objects, add a ReplicaKmsKeyID to the replication rule so objects are re-encrypted with a key in the destination region.
Delete marker replication is enabled. When objects are deleted in the source, the deletion replicates to the destination. An attacker who gains delete access to the source can destroy both copies simultaneously — the destination is not an independent backup, it is a synchronized mirror that replicates deletions.
Remediation: Disable DeleteMarkerReplication in the replication rule unless both copies are intentionally synchronized. For disaster recovery, the destination should retain objects independently of source deletions.
S3 replication destination bucket is encrypted with a KMS key that does not restrict kms:ScheduleKeyDeletion. If an attacker schedules the key for deletion (minimum 7-day waiting period), all encrypted backup objects become permanently undecryptable after the waiting period expires. The key should be in a separate backup account with key policy restricting ScheduleKeyDeletion to a break-glass role.
Remediation: Update the KMS key policy to deny kms:ScheduleKeyDeletion except for a dedicated break-glass role. Ideally, the backup encryption key should reside in a separate backup account that the source account cannot administer.
S3 replication destination bucket policy grants the source account permissions beyond what replication requires. The minimum set is s3:ReplicateDelete, s3:ReplicateObject, s3:List*, and s3:GetBucketVersioning. If the destination policy allows s3:DeleteObjectVersion, s3:PutBucketPolicy, s3:PutLifecycleConfiguration, or s3:* from the source account, a compromised source account can destroy the backup. The collector pre-computes replication.destination_overprivileged by comparing the destination bucket policy grants against the replication minimum-privilege set.
Remediation: Restrict the destination bucket policy to the replication minimum privilege set: s3:ReplicateObject, s3:ReplicateDelete, s3:List*, s3:GetBucketVersioning. Remove s3:DeleteObjectVersion, s3:PutBucketPolicy, s3:PutLifecycleConfiguration, and any wildcard grants from the source account's allowed actions.
S3 replication is configured to a destination bucket that has public access enabled or weaker Public Access Block settings than the source. Data replicated from a private, locked-down source bucket arrives at a publicly accessible destination. Replication copies objects — it does not copy the source bucket's access controls, Public Access Block settings, or encryption configuration.
Remediation: Verify the destination bucket has Public Access Block fully enabled and no public bucket policy. Apply the same PAB settings as the source bucket to the destination.
No S3 bucket should have replication configured to a destination outside the organization. An attacker with s3:PutReplicationConfiguration creates a replication rule to an external bucket, and all new objects are automatically copied to the attacker's bucket. This is a data exfiltration technique that operates continuously and silently after initial setup. Technique: hackingthe.cloud S3 bucket replication exfiltration.
Remediation: Verify the replication destination account is legitimate. If not, delete the replication configuration immediately. Add an SCP denying s3:PutReplicationConfiguration to external destinations using aws:PrincipalOrgID conditions.
S3 bucket has a replication rule configured but the destination bucket has been deleted. Replication silently fails — objects written to the source bucket are not replicated. If the destination bucket name is re-registered under a different account, replication may resume to attacker-controlled storage. This is a direct bucket hijacking vector: the source bucket's replication configuration still contains the destination bucket name, and S3 resolves names globally.
Remediation: Either (a) recreate the destination bucket with the original name and restore the replication IAM role policy, or (b) update the replication rule to point at an existing bucket: aws s3api put-bucket-replication --bucket
--replication-configuration file://repl.json. If replication is no longer needed, delete the rule entirely rather than leaving a dangling reference. After repointing, verify by uploading a test object to the source and confirming it appears in the destination.
S3 replication rule does not have Replication Time Control (RTC) enabled. Without RTC, AWS provides no SLA on replication latency — objects may take hours to replicate during high load, creating an unbounded RPO gap. RTC guarantees 99.99% of objects replicate within 15 minutes and provides CloudWatch metrics to monitor replication lag.
Remediation: Enable Replication Time Control on the replication rule. RTC adds cost but provides a 15-minute RPO SLA and CloudWatch metrics for replication lag monitoring.
Buckets that serve public content must not expose version control artifacts such as .git/ or .svn/. Presence of these paths enables repo reconstruction and can leak secrets.
Remediation: Remove .git/, .svn/, and other VCS directories from the bucket. Add a lifecycle rule or deployment script that excludes VCS artifacts from uploads. If the bucket is a static website, configure your build pipeline to strip VCS files before deployment.
S3 buckets that are intentionally publicly accessible should have Requester Pays enabled to prevent denial-of-wallet attacks. An attacker makes millions of GET/LIST requests to a public bucket, amplifying the owner's bill. Requester Pays shifts the cost to the requester. Technique: Wiz "Storage Denial of Wallet amplification attack".
Remediation: Enable Requester Pays on the bucket. Note this changes the API behavior for legitimate callers — they must include x-amz-request-payer header.
S3 buckets must have SSE-C (server-side encryption with customer-provided keys) disabled. SSE-C allows any principal with s3:PutObject to encrypt objects with an externally-held key that AWS never stores. Ransomware syndicates use this to encrypt data without needing KMS access — the data is irrecoverable without the attacker's key. Unlike BYOKM ransomware (CTL.KMS.ENTROPY.EXTERNAL.ORIGIN.001), SSE-C requires no KMS permissions and bypasses all SCP controls on kms:ImportKeyMaterial. AWS began disabling SSE-C by default for new buckets in April 2026. Technique documented by Farris, extended from the BYOKM ransomware kill chain.
Remediation: Disable SSE-C on the bucket. As of April 2026, AWS disables SSE-C by default for new buckets and existing buckets in accounts without SSE-C-encrypted data. For buckets that still allow SSE-C, disable it via the S3 console or API. Verify existing objects are not SSE-C-encrypted before disabling.
S3 buckets must have at least one telemetry source enabled — either server access logging or CloudTrail data events. A bucket with neither is a breach-reconstruction blind spot: no evidence of who accessed what data or when. Individual logging controls check each source independently; this control catches the compound failure where both are absent simultaneously.
Remediation: Enable at least one: (1) S3 server access logging to a dedicated log bucket, or (2) CloudTrail data event logging for S3 objects. Both is preferred for defense in depth.
Shared-Bucket Tenant Isolation Must Enforce Prefix
Severity: high
Type: unsafe_state
Domain: exposure
When a shared S3 bucket uses prefix-based tenant isolation, every app-signer identity that produces presigned URLs must enforce the tenant prefix. An identity that allows path traversal (../) or disables prefix enforcement lets one tenant read or overwrite another tenant's objects.
Remediation: Update the app-signer configuration to enforce tenant prefix restrictions (enforce_prefix=true) and block path traversal (allow_traversal=false) on all presigned URL signers.
S3 buckets must have versioning enabled to protect against accidental deletion and enable recovery from negligent operations.
Remediation: Enable versioning on the bucket using aws s3api put-bucket-versioning. Once enabled, configure lifecycle rules to manage noncurrent versions and control storage costs.
S3 buckets tagged with backup=true must have MFA delete enabled. MFA delete requires multi-factor authentication to permanently delete object versions, protecting against ransomware attacks and accidental mass deletion of backup data. Without MFA delete, any principal with s3:DeleteObject permission can permanently destroy backup versions.
Remediation: Enable MFA delete on the bucket using aws s3api put-bucket-versioning with the MFA flag. This requires the root account credentials and an MFA device. Only the root account can enable or disable MFA delete.
S3 Versioning Undermined by Aggressive Lifecycle Expiration
Severity: medium
Type: unsafe_state
Domain: governance
Compliance: nist_800_53_r5: SI-12; soc2: C1.2;
S3 bucket has versioning enabled but a lifecycle rule expires noncurrent versions in less than 1 day. Versioning provides protection against accidental deletion and ransomware by preserving prior object versions — but if lifecycle immediately deletes noncurrent versions, the protection window collapses to near-zero. The bucket appears protected (versioning enabled, passes CTL.S3.VERSION.001) but the lifecycle configuration silently defeats the purpose. The collector pre-computes lifecycle_defeats_versioning when versioning is enabled and the shortest NoncurrentVersionExpiration across all lifecycle rules is less than 1 day.
Remediation: Increase NoncurrentVersionExpiration NoncurrentDays to at least 30 days (recommended) or the retention period required by your data classification. For compliance-scoped buckets, align with the mandatory retention window.
Versioned Bucket Without Noncurrent Version Expiration
Severity: medium
Type: unsafe_state
Domain: governance
Compliance: nist_800_53_r5: SI-12; soc2: C1.2;
S3 bucket has versioning enabled but no NoncurrentVersionExpiration lifecycle rule. Every overwrite and delete creates a noncurrent version that persists indefinitely — storage costs grow without bound and incident recovery must sift through an unbounded version history. The companion control CTL.S3.VERSIONING.LIFECYCLE.CONFLICT.001 catches the inverse (lifecycle too aggressive); this control catches the absence of any lifecycle management for noncurrent versions.
Remediation: Add a lifecycle rule with NoncurrentVersionExpiration set to a retention period appropriate for your data classification — 30 days for non-sensitive data, longer for compliance-scoped buckets. This bounds storage costs while preserving a recovery window.
S3 buckets with static website hosting enabled must not also have public read access. Website hosting combined with public read serves content directly to the internet.
Remediation: If public hosting is not intended, disable static website hosting and remove public read access. If hosting is intended, move content behind CloudFront with an Origin Access Control (OAC) and remove direct public access from the bucket.
Signed upload policies must restrict allowed content types. Unrestricted content types enable attackers to upload SVGs with embedded JavaScript or HTML files, causing stored XSS when served from the bucket's domain.
Remediation: Add an exact content-type condition to the signed upload policy (e.g., eq $Content-Type image/jpeg). Avoid starts-with with empty prefix, which allows any content type.
Signed upload policies must restrict write permission to a single exact object key. Prefix-wide permissions (e.g., starts-with $key files/) enable arbitrary overwrite and cross-tenant tampering.
Remediation: Change the signed upload policy to use an exact key condition (eq instead of starts-with) that binds each upload to a specific object path. Generate unique object keys server-side.