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 totrue, the module will create or update theaws_s3_bucket_ownership_controlsresource.object_ownership: A string that specifies the type of ownership. The valid values are typicallyBucketOwnerPreferred,ObjectWriter, andBucketOwnerEnforced.
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 tofalse, 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:ListBucketonarn:aws:s3:::mybuckets3:GetObjectonarn:aws:s3:::mybucket/path/to/my/keys3:PutObjectonarn:aws:s3:::mybucket/path/to/my/key
If use_lockfile is set, additional permissions are required for the lock file:
s3:GetObjectonarn:aws:s3:::mybucket/path/to/my/key.tflocks3:PutObjectonarn:aws:s3:::mybucket/path/to/my/key.tflocks3:DeleteObjectonarn: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 ofendpoints.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.