Skip to content

S3 buckets should prohibit public write access

A publicly writable S3 bucket lets anyone on the internet upload arbitrary content, overwrite existing objects, or use your bucket as free storage for malware distribution. Attackers routinely scan for open buckets and have used them to host phishing pages, inject malicious code into software supply chains, and run up massive data transfer bills. AWS charges the bucket owner for all requests and storage, regardless of who wrote the data.

Blocking public write at the bucket level is a non-negotiable baseline. Even if your application never serves public content, a single misconfigured bucket policy or legacy ACL can expose the entire bucket. S3 Block Public Access gives you an account-wide and bucket-level guardrail that overrides permissive ACLs and policies.

Retrofit consideration

Buckets intentionally using public-read-write ACLs for legacy upload workflows will break. Audit existing bucket policies for "Principal": "*" grants with write actions before enabling Block Public Access.

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  = "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  = "awscontroltower.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  = "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  = "ffiec.compliance.tf/terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"
}

module "s3_bucket" {
  source  = "cccsmedium.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  = "rbicybersecurity.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  = "cisv130.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  = "nistcsfv11.compliance.tf/terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"
}

module "s3_bucket" {
  source  = "nist80053rev4.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

Each aws_s3_bucket must have an associated aws_s3_bucket_public_access_block resource with block_public_acls and block_public_policy set to true. These two settings prevent any ACL or bucket policy from granting public access. ignore_public_acls: true neutralizes existing public ACLs; restrict_public_buckets: true limits access granted by public policies to AWS service principals and authorized users only. A bucket fails if any of the four settings are false or if no aws_s3_bucket_public_access_block resource exists for it. Any aws_s3_bucket_acl resource using public-read-write as the acl argument also fails. Any aws_s3_bucket_policy granting s3:PutObject, s3:DeleteObject, or s3:* to "Principal": "*" or "Principal": {"AWS": "*"} without a restrictive Condition block fails as well. The safest configuration sets all four flags to true at both the bucket level and the account level via aws_s3_account_public_access_block.

Common pitfalls

  • Account-level Block Public Access does not replace bucket-level

    Some compliance tools evaluate each bucket independently of the account default. Even with aws_s3_account_public_access_block active, a bucket without its own aws_s3_bucket_public_access_block resource may report as non-compliant. Always attach a per-bucket resource.

  • Deprecated inline ACL argument on aws_s3_bucket

    Older Terraform configurations set acl = "public-read-write" directly on the aws_s3_bucket resource. That inline argument is deprecated in AWS provider v4+. During migration, the ACL can persist in AWS even after you remove it from Terraform if you don't explicitly replace it with a private aws_s3_bucket_acl resource or enable Block Public Access. Removing the argument from HCL is not the same as revoking the ACL.

  • Bucket policies with overly broad wildcard actions

    A bucket policy granting s3:* to "Principal": "*" with a Condition restricting to a specific VPC endpoint passes some automated checks but still allows public write from within that VPC. Check aws:sourceVpce conditions carefully to confirm they actually restrict access to the intended networks, not just any VPC endpoint of that service.

  • Object ACLs set at upload time

    block_public_acls: true is required even on otherwise private buckets. Without it, anyone with PutObject permissions can upload objects with --acl public-read-write via aws s3api put-object, and the bucket-level ACL won't stop them. The bucket-level ACL controls the bucket itself, not per-object grants applied at upload time.

Audit evidence

Auditors expect Config rule evaluation results showing s3-bucket-public-write-prohibited as compliant for all in-scope buckets. Console screenshots of Block Public Access settings at both the account and individual bucket level provide direct visual confirmation. The aws s3api get-public-access-block --bucket <name> output for each bucket should show all four settings as true.

CloudTrail logs for PutBucketPublicAccessBlock and PutBucketPolicy events show when and by whom these configurations were applied. Access Analyzer for S3 findings showing no public or cross-account write access add corroborating evidence. Security Hub's S3.3 control status provides continuous compliance history for auditors who want automated, ongoing verification.

Framework-specific interpretation

SOC 2: CC6.1 calls for logical access controls that prevent unauthorized modification of data in scope. Blocking public write is a direct implementation of that criterion at the S3 layer, covering both the Security and Integrity trust service criteria.

PCI DSS v4.0: Any bucket that could store, transmit, or touch cardholder data falls under Requirement 7's need-to-know rules. Publicly writable buckets fail that test outright because the access path exists regardless of whether cardholder data ever lands there.

HIPAA Omnibus Rule 2013: 45 CFR 164.312(a)(1) requires covered entities to implement access controls on systems that store, process, or transmit ePHI. A bucket with public write has no meaningful access control for write operations, which makes this a baseline requirement for any bucket in a HIPAA-covered environment.

NIST SP 800-53 Rev 5: AC-3 requires the system to enforce authorization decisions; AC-6 says no principal gets more access than required. Anonymous write permission fails both controls, since no valid authorization decision produces an anonymous grant.

NIST Cybersecurity Framework v2.0: PR.AA expects only authenticated and authorized entities to modify organizational assets. Blocking public write to S3 applies that principle at the storage layer, where anonymous write access is the most direct violation of it.

FedRAMP Moderate Baseline Rev 4: At the Moderate baseline, AC-3 and AC-6 are required controls. Federal data in S3 can't have anonymous write paths. S3 Block Public Access closes those paths at the infrastructure level, which is what FedRAMP assessors expect to see documented.

Tool mappings

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

  • Compliance.tf Control: s3_bucket_restrict_public_write_access

  • AWS Config Managed Rule: S3_BUCKET_PUBLIC_WRITE_PROHIBITED

  • Checkov Check: CKV_AWS_57

  • Powerpipe Control: aws_compliance.control.s3_bucket_restrict_public_write_access

  • Prowler Checks: s3_bucket_policy_public_write_access, s3_bucket_public_access, s3_bucket_public_write_acl

  • AWS Security Hub Control: S3.3

  • KICS Queries: 38c5ee0d-7f22-4260-ab72-5073048df100, 64a222aa-7793-4e40-915f-4b302c76e4d4, a4966c4f-9141-48b8-a564-ffe9959945bc, d0cc8694-fcad-43ff-ac86-32331d7e867f, d24c0755-c028-44b1-b503-8e719c898832

Last reviewed: 2026-03-09