Skip to content

S3 buckets should not use ACLs for user access control

S3 ACLs predate IAM and bucket policies. They grant coarse permissions (READ, WRITE, FULL_CONTROL) that cannot express conditions, require MFA, or restrict by source IP. When ACLs coexist with bucket policies, the effective permission set becomes harder to reason about, raising the chance of accidental public exposure or over-privileged access.

AWS's recommended path is disabling ACLs entirely with the BucketOwnerEnforced object ownership setting. That collapses permission audits to a single plane: IAM and resource policies.

Retrofit consideration

Existing buckets that rely on ACL grants for cross-account or application access will break when you switch to BucketOwnerEnforced. Audit every bucket's current ACL grants with aws s3api get-bucket-acl before migrating.

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  = "nistcsf.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  = "acscism2023.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  = "nistcsfv11.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"

  object_ownership = "BucketOwnerEnforced"
}

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

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

resource "aws_s3_bucket_ownership_controls" "this" {
  bucket = "example-bucket-abc123"
  rule {
    object_ownership = "BucketOwnerEnforced"
  }
}

What this control checks

To pass, each S3 bucket needs an aws_s3_bucket_ownership_controls resource with object_ownership set to "BucketOwnerEnforced". This setting rejects any request that includes an ACL and removes all existing grants except those held by the bucket owner.

In Terraform, declare aws_s3_bucket_ownership_controls referencing the bucket's ID and set object_ownership to "BucketOwnerEnforced" in the rule block. Omit the aws_s3_bucket_acl resource entirely. If one exists with any acl value other than "private", or with explicit access_control_policy grants to non-owner accounts or groups, the control fails.

"BucketOwnerPreferred" and "ObjectWriter" both still allow ACLs and will fail this control.

Common pitfalls

  • BucketOwnerPreferred still allows ACLs

    Teams migrating to fix cross-account object ownership sometimes stop at "BucketOwnerPreferred" and consider the job done. It's not. BucketOwnerPreferred changes ownership semantics but leaves ACLs enabled. Only "BucketOwnerEnforced" disables them.

  • Legacy inline acl argument on aws_s3_bucket

    Older Terraform configs use the deprecated acl argument directly on aws_s3_bucket. In provider v4+, S3 bucket settings split into dedicated resources, including aws_s3_bucket_acl. Removing the inline acl argument without adding aws_s3_bucket_ownership_controls leaves ACLs enabled by default, so ACL-based access remains possible.

  • CloudFront OAI requires ACL grants

    Before switching to BucketOwnerEnforced, check whether any CloudFront distributions use Origin Access Identity (OAI). If they do, migrate to Origin Access Control (OAC) first. OAC authenticates with SigV4 and a bucket policy, so it works cleanly with the new ownership setting.

  • Cross-account log delivery via ACL

    S3 server access logging and some AWS services (ELB access logs in certain regions, for example) historically required the log-delivery-write ACL grant. With BucketOwnerEnforced, those grants stop working. Replace them with a bucket policy granting s3:PutObject to the logging.s3.amazonaws.com service principal instead.

Audit evidence

Config rule s3-bucket-acl-prohibited evaluation results showing all in-scope buckets as COMPLIANT are the primary evidence artifact. Supplement with console screenshots of each bucket's Permissions tab showing "ACLs disabled" under Object Ownership, and programmatic output from aws s3api get-bucket-ownership-controls confirming BucketOwnerEnforced on every bucket in scope.

CloudTrail logs for PutBucketOwnershipControls and PutBucketAcl calls establish when the migration occurred and confirm no ACL modifications have been made since.

Framework-specific interpretation

PCI DSS v4.0: Requirement 7 says access to cardholder data must follow business-need-to-know principles. ACLs can't enforce conditions like source IP, MFA, or time windows, so they can't carry the full weight of a least-privilege model. Bucket policies can, and replacing ACLs with them means access to S3 buckets storing cardholder data is governed by auditable, condition-aware rules that align with what Requirement 7 expects.

NIST Cybersecurity Framework v2.0: PR.AA under NIST CSF v2 calls for managing access permissions and authorizations through defined policies. ACL-based permissions can't express conditions or integrate with centralized IAM auditing. Consolidating S3 access control into IAM and bucket policies is how you meet that requirement.

Tool mappings

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

  • Compliance.tf Control: s3_bucket_acls_should_prohibit_user_access

  • AWS Config Managed Rule: S3_BUCKET_ACL_PROHIBITED

  • Checkov Check: CKV2_AWS_65

  • Powerpipe Control: aws_compliance.control.s3_bucket_acls_should_prohibit_user_access

  • Prowler Check: s3_bucket_acl_prohibited

  • AWS Security Hub Control: S3.12

Last reviewed: 2026-03-09