Modernizing S3 Access Control: Migrating from Terraform's `aws_s3_bucket_acl` to Object Ownership and Public Access Blocks

The landscape of AWS Simple Storage Service (S3) access control has undergone a significant architectural shift. Historically, the acl attribute within the aws_s3_bucket resource was the primary mechanism for defining permissions, utilizing "canned ACLs" such as private, public-read, or log-delivery-write. However, AWS has deprecated these ACL-based configurations in favor of more granular, policy-driven, and ownership-centric security models. For infrastructure engineers using Terraform, this transition presents a critical challenge: how to migrate existing stateful resources away from the deprecated acl parameter without causing unintended security downgrades or state drift. This article provides a comprehensive technical analysis of the terraform aws_s3_bucket_acl migration, detailing the underlying mechanisms of the new S3 Bucket Ownership Controls, the specific pitfalls encountered when removing the acl argument, and the robust patterns available within the community-maintained terraform-aws-modules/s3-bucket module to handle both legacy log delivery and modern security postures.

The Deprecation of Canned ACLs and the Security Implication

The primary driver for this migration is the AWS recommendation to disable ACLs entirely where possible and rely on IAM policies and Bucket Policies. When using Terraform, the aws_s3_bucket resource includes an acl argument. In older versions of the AWS provider, and in many existing codebases, this argument was used to set the bucket's default permission set. For example, a bucket configured with acl = "public-read" allowed public internet access to the objects within the bucket.

The critical issue arises when engineers attempt to modernize their configurations by removing the acl argument. Terraform's state management relies on a diff between the current configuration and the remote AWS state. If an acl was previously applied to a bucket and is subsequently removed from the Terraform configuration, Terraform interprets this as a desire to revert the bucket to its default security posture. In AWS, the default S3 bucket ACL is private. Consequently, a plan that removes acl = "public-read" will execute an in-place update that changes the effective ACL to private, potentially breaking applications that rely on public access. This behavior was highlighted in community discussions where users found that simply commenting out the acl line in their HCL code resulted in a Terraform plan showing a change from "public-read" to "private". This "silent downgrade" is a significant operational risk, as it can lead to immediate service outages if the new access control mechanisms (such as a Bucket Policy) have not been fully implemented or if the IAM policies have not been updated to grant the necessary permissions via other means.

To resolve this, AWS introduced the aws_s3_bucket_acl resource as a standalone resource. This resource allows the ACL to be managed separately from the bucket creation. By using this dedicated resource, engineers can import the existing ACL state and manage it independently, ensuring that the aws_s3_bucket resource no longer manages the ACL attribute. However, this transition is not merely a refactor; it requires careful handling of the expected_bucket_owner argument and an understanding of how Terraform manages the separation of concerns between bucket creation and permission assignment.

Implementing S3 Bucket Ownership Controls

The modern replacement for the expected_bucket_owner argument and the nuances of ACL ownership is the S3 Bucket Ownership Control feature. This feature allows you to change how ownership of objects in your bucket is assigned. By default, when a user uploads an object to an S3 bucket, the object owner is the uploading user. However, with Bucket Ownership Control, you can assign ownership to the bucket owner (the AWS account that owns the bucket) instead of the uploader. This is a crucial security improvement because it simplifies access management and prevents "orphaned" objects where the original uploader's account permissions become the de facto security boundary.

The terraform-aws-modules/s3-bucket module provides native support for this feature through two specific arguments: control_object_ownership and object_ownership.

  • control_object_ownership: A boolean that determines whether the module should manage the S3 Bucket Ownership Controls resource. When set to true, the module will create or update the aws_s3_bucket_ownership_controls resource.
  • object_ownership: A string that specifies the type of ownership. The valid values are typically BucketOwnerPreferred, ObjectWriter, and BucketOwnerEnforced.

When control_object_ownership is set to true, the module ensures that the ownership controls are applied to the bucket. This is particularly important for buckets that receive objects from other AWS accounts or services, such as load balancer logs or WAF logs. In these scenarios, the uploading entity may not have the same account ID as the bucket owner. By enforcing ObjectWriter or BucketOwnerEnforced, you ensure that the bucket owner retains full control over the objects, allowing for consistent application of IAM policies and Bucket Policies.

For example, when configuring a bucket for load balancer logs, the standard modern configuration looks like this:

```hcl
module "s3bucketfor_logs" {
source = "terraform-aws-modules/s3-bucket/aws"

bucket = "my-s3-bucket-for-logs"

# Allow deletion of non-empty bucket
force_destroy = true

controlobjectownership = true
object_ownership = "ObjectWriter"

attachelblogdeliverypolicy = true
attachlblogdeliverypolicy = true
}
```

In this configuration, the acl argument is entirely absent. The force_destroy argument is used to allow the bucket to be deleted even if it contains objects, which is a common requirement for log buckets that accumulate data. The attach_elb_log_delivery_policy and attach_lb_log_delivery_policy arguments generate the necessary bucket policies to allow the AWS services to write logs to the bucket. These policies are applied independently of the ACL, ensuring that the log delivery mechanism works regardless of the bucket's ownership settings.

Comparison of Legacy and Modern Configuration Patterns

The transition from legacy ACL-based configurations to modern ownership-control-based configurations can be illustrated by comparing the two approaches. The following table highlights the key differences in arguments, security implications, and provider version requirements.

Feature Legacy ACL Approach Modern Ownership Control Approach
Primary Argument acl = "private", acl = "public-read" control_object_ownership, object_ownership
Policy Management Managed via acl and grant blocks Managed via Bucket Policies and IAM Policies
Ownership Uploaders own objects by default Bucket owner or writer can own objects
Log Delivery Requires acl = "log-delivery-write" Requires specific Bucket Policies (attach_*_log_delivery_policy)
Security Posture Coarse-grained, deprecated Fine-grained, recommended by AWS
Provider Version Any version AWS Provider >= 6.42
Terraform Version Any version Terraform >= 1.5.7

The modern approach eliminates the need for grant blocks and acl arguments. Instead, it relies on explicit bucket policies. For instance, the terraform-aws-modules/s3-bucket module supports attach_elb_log_delivery_policy, attach_lb_log_delivery_policy, and attach_waf_log_delivery_policy. These arguments generate the appropriate JSON policies to grant write permissions to the respective AWS services. This is superior to using ACLs because it is more explicit and easier to audit.

Advanced Configuration Options and Conditional Creation

The terraform-aws-modules/s3-bucket module offers a wide range of advanced features that go beyond simple ACL management. These features are essential for building robust, scalable, and secure S3 infrastructure. The module supports static website hosting, access logging, versioning, CORS, lifecycle rules, server-side encryption, object locking, Cross-Region Replication (CRR), and various log delivery policies.

One of the most powerful features is the ability to conditionally create the bucket itself. Terraform does not allow the use of count inside a module block, which complicates conditional resource creation. To address this, the module provides a create_bucket argument.

  • create_bucket: A boolean that controls whether the S3 bucket should be created. When set to false, the module will not create the bucket resource, but other resources managed by the module (such as policies or configurations) may still be created if applicable. This is useful in scenarios where you want to apply configurations to an existing bucket without Terraform managing its creation.

```hcl

This S3 bucket will not be created

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

create_bucket = false
# ... other configuration
}
```

Additionally, the module supports placeholders in bucket policies. This is particularly useful when using bucket prefixes or when the bucket name is not known at configuration time. The placeholders _S3_BUCKET_ID_, _S3_BUCKET_ARN_, and _AWS_ACCOUNT_ID_ are replaced with the actual values during the policy attachment. This allows for the creation of highly reusable and dynamic policy documents.

The module also supports the creation of S3 Directory Buckets, S3 Table Buckets, and S3 Vectors, representing the latest iterations of S3 storage types. These advanced storage types have specific configuration requirements that are handled by the module, ensuring compatibility and best practices are followed.

IAM Permissions for Terraform S3 Backend

When using S3 as the backend for Terraform state storage, specific IAM permissions are required. The terraform backend s3 configuration requires a set of permissions to manage the state file and lock file. These permissions are distinct from the permissions required for general S3 object management.

When not using workspaces, or when only using the default workspace, the following IAM permissions are required on the target backend bucket:

  • s3:ListBucket on arn:aws:s3:::mybucket
  • s3:GetObject on arn:aws:s3:::mybucket/path/to/my/key
  • s3:PutObject on arn:aws:s3:::mybucket/path/to/my/key

If use_lockfile is set, additional permissions are required for the lock file:

  • s3:GetObject on arn:aws:s3:::mybucket/path/to/my/key.tflock
  • s3:PutObject on arn:aws:s3:::mybucket/path/to/my/key.tflock
  • s3:DeleteObject on arn:aws:s3:::mybucket/path/to/my/key.tflock

Note that s3:DeleteObject is not required on the state file itself, as Terraform does not delete it. The following IAM statement illustrates the minimal required permissions:

json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "s3:ListBucket", "Resource": "arn:aws:s3:::mybucket", "Condition": { "StringEquals": { "s3:prefix": "mybucket/path/to/my/key" } } }, { "Effect": "Allow", "Action": ["s3:GetObject", "s3:PutObject"], "Resource": [ "arn:aws:s3:::mybucket/path/to/my/key" ] }, { "Effect": "Allow", "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"], "Resource": [ "arn:aws:s3:::mybucket/path/to/my/key.tflock" ] } ] }

When using workspaces, additional permissions are required to create, list, read, update, and delete the workspace state files. This includes the ability to list the bucket to discover workspace keys.

Backend Configuration and Authentication

The S3 backend configuration in Terraform includes several parameters for authentication and encryption. The assume_role_with_web_identity block allows the use of OIDC or OAuth tokens for authentication, which is a common pattern in CI/CD pipelines. The web_identity_token or web_identity_token_file arguments are used to provide the token. One of these arguments is required when using this block.

The configuration also supports server-side encryption of the state and lock files. The encrypt argument enables this encryption, and the kms_key_id argument specifies the ARN of the KMS key to use. This is a critical security feature for protecting sensitive state data.

Other optional parameters include:

  • acl: A canned ACL to be applied to the state and lock files. Note that this is distinct from the bucket ACL discussed earlier; this applies to the specific objects used for state.
  • endpoint: A custom endpoint URL for the AWS S3 API (deprecated in favor of endpoints.s3).
  • force_path_style: Enables path-style S3 URLs (deprecated).

The following configuration example demonstrates the use of web identity authentication:

```hcl
terraform {
backend "s3" {
bucket = "example-bucket"
key = "path/to/state"
region = "us-east-1"

assume_role_with_web_identity = {
  role_arn          = "arn:aws:iam::PRODUCTION-ACCOUNT-ID:role/Terraform"
  web_identity_token = "<token value>"
}

}
}
```

Module Input Variables Reference

The terraform-aws-modules/s3-bucket module exposes a comprehensive set of input variables. Understanding these variables is essential for effective usage. The following table lists key variables relevant to access control and bucket management.

Variable Description Type Default Required
acceleration_status Sets the accelerate configuration of an existing bucket string null no
bucket The name of the bucket. If omitted, Terraform will assign a random, unique name. string null no
bucket_namespace Namespace for the bucket. Valid values: account-regional, global. Defaults to global. string null no
bucket_prefix Creates a unique bucket name beginning with the specified prefix. Conflicts with bucket. string null no
control_object_ownership Whether to manage S3 Bucket Ownership Controls on this bucket. bool false no
cors_rule List of maps containing rules for Cross-Origin Resource Sharing. any [] no
create_bucket Controls if S3 bucket should be created. bool true no
create_metadata_configuration Whether to create metadata configuration resource. bool false no
data_redundancy Data redundancy. Valid values: SingleAvailabilityZone. string null no
expected_bucket_owner The account ID of the expected bucket owner. string null no
force_destroy Indicates all objects should be deleted from the bucket so that the bucket can be destroyed without error. bool false no
grant An ACL policy grant. Conflicts with acl. any [] no
ignore_public_acls Whether Amazon S3 should ignore public ACLs for this bucket. bool null no

The ignore_public_acls variable is part of the Account-level Public Access Block configuration. This feature allows you to enforce a strict security posture by ignoring public ACLs, ensuring that only IAM policies and Bucket Policies can grant access. This is a recommended setting for most production environments.

Conclusion

The migration from aws_s3_bucket_acl to modern S3 access control mechanisms is a necessary step in aligning with AWS security best practices. The deprecation of canned ACLs and the introduction of Bucket Ownership Controls, along with the granular control provided by Bucket Policies and IAM Roles, offer a more secure and flexible model. The terraform-aws-modules/s3-bucket module facilitates this transition by providing abstracted, high-level arguments for managing ownership controls, log delivery policies, and public access blocks.

Engineers must be aware of the state management implications of removing the acl argument, as Terraform will attempt to revert the bucket to a private ACL if the new mechanisms are not correctly implemented. By using the control_object_ownership and object_ownership arguments, along with the appropriate log delivery policies, teams can eliminate the need for ACLs entirely while maintaining the functionality required for log ingestion and other cross-account operations. Furthermore, the module's support for conditional creation, placeholder policies, and advanced storage types like S3 Vectors and Table Buckets ensures that it remains a viable and robust solution for S3 infrastructure as code across a wide range of use cases. Adhering to the recommended provider and Terraform version requirements, and implementing the necessary IAM permissions for backend state management, are critical for a successful and secure deployment.

Sources

  1. terraform-aws-modules/s3-bucket
  2. Handling AWS S3 bucket ACL deprecation - HashiCorp Discuss
  3. S3 Backend - Terraform Language

Related Posts