Skip to content

S3 public access should be blocked at bucket level

A single misconfigured bucket ACL or bucket policy can expose sensitive data to the internet. Account-level public access blocks provide a safety net, but bucket-level blocks give you explicit, per-bucket enforcement that survives account-level setting changes and makes intent visible in code review.

Public S3 bucket exposures remain one of the most common causes of large-scale data breaches. Enforcing all four flags at the bucket level closes off entire classes of accidental exposure, including cases where a developer attaches a wildcard principal policy or sets a canned ACL like public-read.

Retrofit consideration

Existing buckets intentionally serving public content (static websites, public datasets) will fail. Identify them and either exclude them from the policy or migrate to CloudFront with an origin access control pointing at a private bucket.

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  = "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  = "fedrampmoderate.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  = "cisacyberessentials.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  = "fedramplow.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_acls       = true
  block_public_policy     = true
  ignore_public_acls      = 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_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

What this control checks

The policy checks that every S3 bucket has an aws_s3_bucket_public_access_block resource with all four arguments set to true: block_public_acls, ignore_public_acls, block_public_policy, and restrict_public_buckets. It fails if any argument is false, omitted (each defaults to false), or if no block resource exists for the bucket. The bucket argument must reference the target aws_s3_bucket resource. This is separate from aws_s3_account_public_access_block, which applies globally; both should be configured, but this policy only checks bucket-level settings.

Common pitfalls

  • Omitted arguments default to false

    Each of the four arguments in aws_s3_bucket_public_access_block defaults to false if not set explicitly. Creating the resource with only block_public_acls = true leaves the other three off and the control fails. Set all four to true explicitly every time.

  • Account-level block does not satisfy bucket-level check

    aws_s3_account_public_access_block with all four flags set to true protects the account globally, but this control checks the bucket-level resource specifically. Account-level and bucket-level blocks are evaluated independently. You need both.

  • Static website hosting conflict

    Enabling all four block settings on a bucket configured for static hosting via aws_s3_bucket_website_configuration will break public access to the site. Move off direct S3 hosting and serve through CloudFront with aws_cloudfront_origin_access_control pointing at a private bucket.

  • Terraform import may miss the resource

    aws_s3_bucket_public_access_block is a separate resource from aws_s3_bucket. When you import an existing bucket with terraform import aws_s3_bucket.example my-bucket, Terraform has no visibility into the public access block unless you also import or declare aws_s3_bucket_public_access_block. Get this wrong and the control treats the bucket as unprotected even if the block is enabled in AWS.

Audit evidence

The primary evidence is the Config rule s3-bucket-level-public-access-prohibited evaluation showing all buckets as COMPLIANT. Direct API confirmation via aws s3api get-public-access-block --bucket <name> showing all four fields as true works as point-in-time evidence for individual buckets. For continuous posture, a Security Hub or CSPM dashboard showing zero public S3 buckets is standard.

If buckets are intentionally excluded for public static content, auditors will want a documented exception with business justification, risk acceptance sign-off, and evidence that those buckets hold no sensitive data.

Framework-specific interpretation

PCI DSS v4.0: Requirements 7.1 and 7.2 restrict access to system components by business need and least privilege. For S3 buckets holding cardholder data, public accessibility is never a valid business need, and bucket-level blocks enforce that even when someone later introduces a permissive bucket policy or canned ACL.

HIPAA Omnibus Rule 2013: 45 CFR 164.312(a)(1) requires access controls and 164.312(e)(1) addresses transmission security. PHI in S3 must not be reachable by the public under either provision. Bucket-level blocks prevent accidental exposure through misconfigured ACLs or wildcard policies, which is the failure mode these provisions are designed to catch.

NIST SP 800-53 Rev 5: AC-3, AC-6, and SC-7 together require access enforcement, least privilege, and boundary protection. Bucket-level blocks address all three: they enforce the access boundary, prevent privilege escalation via public ACL or policy grant, and apply per-resource rather than globally, which is the scope AC-6 expects.

NIST Cybersecurity Framework v2.0: PR.DS and PR.AA, both in the Protect function, cover data security and identity management respectively. Blocking public access at the bucket level covers the storage side of both subcategories.

FedRAMP Moderate Baseline Rev 4: AC-3 calls for access enforcement that prevents unauthorized disclosure of federal data. At the Moderate baseline, any S3 bucket holding government data needs explicit access boundaries set at the resource level, not just the account, so that a downstream account-level change can't silently widen exposure.

Tool mappings

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

  • Compliance.tf Control: s3_public_access_block_bucket

  • AWS Config Managed Rule: S3_BUCKET_LEVEL_PUBLIC_ACCESS_PROHIBITED

  • Checkov Check: CKV2_AWS_6

  • Powerpipe Control: aws_compliance.control.s3_public_access_block_bucket

  • Prowler Check: s3_bucket_level_public_access_block

  • AWS Security Hub Controls: S3.1, S3.8

  • KICS Queries: 1ec253ab-c220-4d63-b2de-5b40e0af9293, 4fa66806-0dd9-4f8d-9480-3174d39c7c91

  • Trivy Checks: AWS-0086, AWS-0087, AWS-0091, AWS-0093, AWS-0094

Last reviewed: 2026-03-09