Skip to content

S3 buckets should have policies that prohibit public access

An S3 bucket policy with "Principal": "*" and no limiting Condition block exposes objects to the entire internet. This is the root cause of most high-profile S3 data breaches, where sensitive records, backups, or logs become downloadable by anyone with the bucket name. Unlike S3 Block Public Access (which acts as a guardrail), the bucket policy defines the actual permissions, so checking its content is a direct test for exposure.

Public bucket policies also create indirect risk: even if current objects are non-sensitive, future writes inherit the same open access. Catching overly permissive policies at deploy time prevents silent exposure drift.

Retrofit consideration

Existing bucket policies with broad Principal grants may be serving legitimate use cases, static website hosting and cross-account replication are common ones. Tightening them requires auditing every consumer of the bucket before touching the policy, or you risk breaking dependent services.

Implementation

Choose the approach that matches how you manage Terraform.

Use the compliance.tf module to enforce this control by default. See get started with compliance.tf.

module "s3_bucket" {
  source  = "soc2.compliance.tf/terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"
}

module "s3_bucket" {
  source  = "pcidss.compliance.tf/terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"
}

module "s3_bucket" {
  source  = "hipaa.compliance.tf/terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"
}

module "s3_bucket" {
  source  = "nist80053.compliance.tf/terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"
}

module "s3_bucket" {
  source  = "nistcsf.compliance.tf/terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"
}

module "s3_bucket" {
  source  = "cisv80ig1.compliance.tf/terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"
}

module "s3_bucket" {
  source  = "nist800171.compliance.tf/terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"
}

module "s3_bucket" {
  source  = "nydfs23.compliance.tf/terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"
}

module "s3_bucket" {
  source  = "cisv140.compliance.tf/terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"
}

module "s3_bucket" {
  source  = "acscessentialeight.compliance.tf/terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"
}

module "s3_bucket" {
  source  = "cfrpart11.compliance.tf/terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"
}

module "s3_bucket" {
  source  = "rbiitfnbfc.compliance.tf/terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"
}

module "s3_bucket" {
  source  = "hipaasecurity2003.compliance.tf/terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"
}

module "s3_bucket" {
  source  = "iso270012013.compliance.tf/terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"
}

module "s3_bucket" {
  source  = "nistcsfv11.compliance.tf/terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"
}

module "s3_bucket" {
  source  = "pcidssv321.compliance.tf/terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"
}

If you use terraform-aws-modules/s3-bucket/aws, set the right module inputs for this control. You can later migrate to the compliance.tf module with minimal changes because it is compatible by design.

module "s3_bucket" {
  source  = "terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"

  block_public_policy     = true
  restrict_public_buckets = true
}

Use AWS provider resources directly. See docs for the resources involved: aws_s3_bucket_public_access_block.

resource "aws_s3_bucket" "this" {
  bucket        = "pofix-abc123"
  force_destroy = true
}

resource "aws_s3_bucket_public_access_block" "this" {
  bucket                  = "example-bucket-abc123"
  block_public_policy     = true
  restrict_public_buckets = true
}

What this control checks

This control evaluates the aws_s3_bucket_policy resource and inspects the JSON policy document in the policy argument. Every Statement must restrict access through at least one of: a scoped Principal (specific AWS account, IAM role, or service principal rather than "*"), or a Condition block limiting access by aws:SourceIp, aws:SourceVpc, aws:SourceVpce, aws:PrincipalOrgID, or similar keys. A statement with "Principal": "*" or "Principal": {"AWS": "*"} and no restricting Condition fails. Buckets with no aws_s3_bucket_policy resource pass; no policy means no public grant exists. When using aws_iam_policy_document data sources, ensure each statement block sets principals to specific identities or includes a condition block with network or organization constraints.

Common pitfalls

  • Wildcard principal hidden in Allow with Deny override

    Some teams use a broad Allow with "Principal": "*" paired with an explicit Deny to restrict access. The control flags the Allow statement regardless, because it checks statement-level grants, not net-effective permissions. If your architecture relies on this pattern, refactor to scope the Principal on the Allow statement directly rather than relying on the Deny to neutralize it.

  • Policy generated outside Terraform

    Raw JSON strings passed to aws_s3_bucket_policy from external files or variables won't be parseable at plan time when they contain values interpolated at apply time. Use aws_iam_policy_document instead so the policy structure is visible during terraform plan and the control can evaluate it before anything is deployed.

  • CloudFront OAI/OAC policies look public

    CloudFront Origin Access Control policies should grant access to the cloudfront.amazonaws.com service principal with a Condition on AWS:SourceArn scoped to a specific distribution. Origin Access Identity policies use a canonical user principal. Get either the principal or the condition wrong, and the bucket falls back to public access with no obvious error on apply.

  • Static website hosting exemption assumptions

    Buckets serving static websites via aws_s3_bucket_website_configuration need public read access by design, and this control flags those policies with no automatic exemption. The cleaner path is CloudFront with OAC in front of a private bucket rather than direct public bucket access, which avoids the exemption problem entirely.

Audit evidence

Auditors expect Config rule evaluation results for each S3 bucket, typically via the managed rule s3-bucket-policy-grantee-check, showing compliant or non-compliant status per bucket. Supplementary evidence is a point-in-time export of all bucket policies from aws s3api get-bucket-policy across every account, confirming no policy contains an unscoped "Principal": "*" without a limiting condition. Screenshots from the S3 console policy editor for previously flagged buckets corroborate remediation.

For continuous assurance, Security Hub findings tied to this control, or CSPM scan results from Prowler or Steampipe showing a passing state across all regions and accounts, are what recurring audits ask to see.

Framework-specific interpretation

SOC 2: CC6.1 requires restricting access to information assets to authorized users. A public bucket policy is a direct failure of that criterion, and this check catches it.

PCI DSS v4.0: For systems that store cardholder data in S3, Requirement 7's business-need-to-know rules out any policy that grants unrestricted access. This control verifies no policy statement opens that door.

HIPAA Omnibus Rule 2013: 45 CFR 164.312(a)(1) requires covered entities to implement technical controls that limit access to ePHI to authorized users. A bucket policy with an unscoped "Principal": "*" puts any ePHI stored in that bucket on the open internet. This control checks that bucket-level permissions are explicitly bounded to authorized entities.

NIST SP 800-53 Rev 5: AC-3 (Access Enforcement) and AC-6 (Least Privilege) both apply here. S3 bucket policies are the primary access enforcement mechanism for object storage, and "Principal": "*" without conditions violates both by definition.

NIST Cybersecurity Framework v2.0: S3 bucket policies are the data-layer enforcement point for PR.AA, which covers identity management and access control under the Protect function. Restricting them to named principals or network boundaries is how least privilege gets applied at the storage layer, preventing uncontrolled data disclosure.

Tool mappings

Use these identifiers to cross-reference this control across tools, reports, and evidence.

  • Compliance.tf Control: s3_bucket_policy_restrict_public_access

  • AWS Config Managed Rule: S3_BUCKET_LEVEL_PUBLIC_ACCESS_PROHIBITED

  • Checkov Check: CKV_AWS_70

  • Powerpipe Control: aws_compliance.control.s3_bucket_policy_restrict_public_access

  • Prowler Checks: s3_bucket_policy_public_write_access, s3_bucket_public_access

  • AWS Security Hub Controls: S3.2, S3.3

  • KICS Query: 7af43613-6bb9-4a0e-8c4d-1314b799425e

Last reviewed: 2026-03-09