Terraform S3 Bucket Policy Attachment Patterns and State Protection

Amazon S3 or Simple Storage Service is a widely used object storage service. When data is stored in S3, by default it is private for security purposes. S3 bucket policy is one of the frequently used ways to provide access to a bucket and the objects in it. The reference material establishes that creating an S3 bucket policy using Terraform is a common infrastructure as code task and that the policy can be created and attached to the bucket through declarative configuration. The material also positions the exercise within a prior learning path about creating an S3 bucket using Terraform and then extending that work with policy attachment.

The meaning of resource-based policy is that instead of applying the policy to a principal like user, group, or role, you apply the policy to the S3 bucket itself on which access is required. This distinction changes how access decisions are evaluated in AWS. An S3 bucket policy is a resource-based IAM policy that you can use to provide access to your S3 bucket and the objects in it. S3 bucket policies are JSON documents that define who can do what with your bucket and its objects. They complement IAM policies by providing resource-based access control, meaning you can grant access to principals from other AWS accounts, enforce encryption requirements, restrict access to specific VPCs, and more.

When working with Terraform, the state file is one of the most sensitive assets in your infrastructure stack. It does not just track resources, it often contains confidential details such as resource IDs, networking configurations, and even secrets or credentials that may have been provisioned. When using an S3 backend, an S3 bucket policy can add additional protection. While IAM user and role policies already provide access control to the bucket, it is not uncommon to find organisations granting full S3 access to anyone who needs it. Ideally this should not happen, but it unfortunately does.

What S3 Bucket Policy Means in AWS

An S3 bucket policy is a resource-based IAM policy that you can use to provide access to your S3 bucket and the objects in it. The resource-based nature means the policy is attached to the bucket resource itself rather than to an identity. This allows cross-account access grants and centralized control at the storage resource.

The impact of this design is that a single policy document can govern all access attempts to a bucket, regardless of which IAM principal initiates the request. The policy can override permissive IAM policies because resource-based denies take precedence. The context with Terraform is that the policy document is generated or authored as code and applied via the aws_s3_bucket_policy resource, ensuring that the bucket's access posture is versioned and reproducible.

Default Private Posture of S3 and Why Policy Matters

Amazon S3 or Simple Storage Service is a widely used object storage service. When you are storing your data in S3, by default they are private for security purposes. S3 bucket policy is one of the frequently used ways to provide access to your bucket and objects.

The default private posture means new buckets are not publicly reachable unless explicitly configured. The real-world consequence is that developers must intentionally open access, which reduces accidental exposure. The policy becomes the mechanism to move from private to controlled access. The context is that Terraform codifies this transition, making the decision to allow public read or role-specific read explicit and reviewable.

Prerequisite Setup Before Terraforming a Policy

Before creating an S3 bucket policy using Terraform, the reference material lists prerequisites.

  • An AWS Account: Setup Free Tier Account on AWS In Right Way
  • Required Permission to Create S3 Bucket and Policy
  • AWS CLI
  • An Editor Like Notepad or VS Code

The presence of an AWS account with appropriate permissions ensures the Terraform provider can authenticate and assume the necessary actions. The AWS CLI is typically used for authentication configuration and profile management. An editor enables authoring of Terraform configuration files. The impact is that missing any prerequisite blocks execution and causes provider authentication failures or policy attachment errors. The context links these prerequisites to the provider configuration shown later, which uses a profile and region.

Two Terraform Ways to Attach a Policy and the Deprecation Notice

When it comes to creating an S3 bucket policy using terraform, there are two ways in which you can do it.

  • Using policy parameter in the resource aws_s3_bucket
  • Creating a aws_s3_bucket_policy resource (recommended)

As of now, specifying policy in the aws_s3_bucket resource is the old way of doing it and is already deprecated.

The recommended pattern separates bucket creation from policy attachment. The impact of using the deprecated parameter is reduced future compatibility and potential drift between bucket and policy lifecycle. The context with the reference material is that the article explicitly advises using the standalone resource aws_s3_bucket_policy to create a policy or use policy parameter in resource aws_s3_bucket. However, using policy parameter on bucket resource is deprecated and it is recommended to use aws_s3_bucket_policy resource to attach an S3 bucket policy to your bucket.

Provider and Terraform Configuration Block

The final configuration file example begins with provider configuration.

hcl terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 3.27" } } } provider "aws" { profile = "default" region = "ap-south-1" }

The required_providers block pins the AWS provider to a version range. The provider block sets profile and region. The impact is deterministic provider selection and consistent region targeting. The context is that all subsequent resources inherit this provider configuration, meaning the bucket ck-demo-bucket-04th will be created in ap-south-1 using the default profile credentials.

Bucket Creation Resource

hcl resource "aws_s3_bucket" "demo-bucket"{ bucket = "ck-demo-bucket-04th" }

The resource creates the S3 bucket that will later receive a policy. The bucket name is static in the example. The impact is that the bucket must be globally unique. The context is that the bucket ID is later referenced by the policy resource via aws_s3_bucket.demo-bucket.id and ARN via aws_s3_bucket.demo-bucket.arn.

Attaching Policy with awss3bucket_policy

hcl resource "aws_s3_bucket_policy" "public_read_access" { bucket = aws_s3_bucket.demo-bucket.id policy = data.aws_iam_policy_document.public_read_access.json }

The resource attaches a policy to the bucket. The bucket argument references the bucket ID. The policy argument consumes a JSON document generated by a data source. The impact is that the policy becomes an independent Terraform resource with its own lifecycle, allowing updates without recreating the bucket. The context is that this is the recommended pattern over embedding policy in the bucket resource.

Generating Policy Document with awsiampolicy_document

hcl data "aws_iam_policy_document" "public_read_access" { statement { principals { type = "*" identifiers = ["*"] } actions = [ "s3:GetObject", "s3:ListBucket", ] resources = [ aws_s3_bucket.demo-bucket.arn, "${aws_s3_bucket.demo-bucket.arn}/*", ] } }

The data source builds a policy document. The principals are wildcard *. The actions are s3:GetObject and s3:ListBucket. The resources include the bucket ARN and the bucket ARN with /* suffix for objects.

The impact is that the policy grants public read-only access to the bucket and its objects. The context is that the reference material shows the user navigating to the Permissions tab in the console and seeing the public read-only policy attached to the bucket. The separation of resources into bucket ARN and object ARN reflects the AWS requirement that ListBucket applies to the bucket ARN without /* while GetObject applies to objects with /*.

Public Read Access Policy Breakdown

A bucket policy consists of one or more statements, each with an Effect (Allow/Deny), Principal (who), Action (what), and Resource (which objects).

The example uses Effect Allow, Principal *, Action s3:GetObject and s3:ListBucket, and Resource bucket ARN and object ARN.

The impact for users is public readability of objects. The context is that the policy is created via Terraform and attached to the bucket, demonstrating the end-to-end flow from code to console visibility.

Basic Bucket Policy Pattern with IAM Role

hcl resource "aws_s3_bucket" "data" { bucket_prefix = "app-data-" } resource "aws_s3_bucket_policy" "data" { bucket = aws_s3_bucket.data.id policy = jsonencode({ Version = "2012-10-17" Statement = [ { Sid = "AllowAppRoleRead" Effect = "Allow" Principal = { AWS = aws_iam_role.app.arn } Action = [ "s3:GetObject", "s3:ListBucket" ] Resource = [ aws_s3_bucket.data.arn, # For ListBucket "${aws_s3_bucket.data.arn}/*" # For GetObject ] } ] }) }

The pattern shows a specific IAM role granted read access. The Sid identifies the statement. The Principal is the role ARN. The Actions are s3:GetObject and s3:ListBucket.

Notice that ListBucket applies to the bucket ARN (without /) while GetObject applies to the objects (with /).

The impact is least-privilege access for an application role. The context is that resource-based access control complements IAM policies by allowing cross-account principals and resource-specific conditions.

Terraform AWS S3 Bucket Module Features

Terraform module which creates S3 bucket on AWS with all (or almost all) features provided by Terraform AWS provider.

These features of S3 bucket configurations are supported:

  • static web-site hosting
  • access logging
  • versioning
  • CORS
  • lifecycle rules
  • server-side encryption
  • object locking
  • Cross-Region Replication (CRR)
  • ELB log delivery bucket policy
  • ALB/NLB log delivery bucket policy
  • WAF log delivery bucket policy
  • Account-level Public Access Block
  • S3 Directory Bucket
  • S3 Table Bucket
  • S3 Vectors

The module centralizes complex bucket configuration. The impact is reduced boilerplate and standardized settings for security features like encryption and public access block. The context is that the module can also attach log delivery policies automatically.

Module Example for Standard Bucket

hcl module "s3_bucket" { source = "terraform-aws-modules/s3-bucket/aws" bucket = "my-s3-bucket" acl = "private" control_object_ownership = true object_ownership = "ObjectWriter" versioning = { enabled = true } }

The module sets a private ACL, controls object ownership, and enables versioning. The impact is immutable version history and controlled ownership model. The context is that this module usage is distinct from manual aws_s3_bucket_policy resources but can be combined with them.

Module Example for Log Delivery Bucket with ELB Policy

hcl module "s3_bucket_for_logs" { source = "terraform-aws-modules/s3-bucket/aws" bucket = "my-s3-bucket-for-logs" acl = "log-delivery-write" force_destroy = true control_object_ownership = true object_ownership = "ObjectWriter" attach_elb_log_delivery_policy = true }

The module configures a bucket for logs with log-delivery-write ACL and attaches ELB log delivery policy. The impact is that ELB can write logs without additional manual policy. The context is that log delivery buckets require specific ACL and policy settings.

Module Example for ALB/NLB Log Delivery

hcl module "s3_bucket_for_logs" { source = "terraform-aws-modules/s3-bucket/aws" bucket = "my-s3-bucket-for-logs" force_destroy = true control_object_ownership = true object_ownership = "ObjectWriter" attach_elb_log_delivery_policy = true attach_lb_log_delivery_policy = true }

The addition of attach_lb_log_delivery_policy is required for ALB/NLB logs. The impact is support for both ELB and load balancer log delivery. The context shows module flexibility for different log sources.

Protecting Terraform State Files with a Deny Policy

Protect Terraform State Files with an S3 Bucket Policy. When working with Terraform, the state file is one of the most sensitive assets in your infrastructure stack. It does not just track resources, it often contains confidential details such as resource IDs, networking configurations, and even secrets or credentials that may have been provisioned. When using an S3 backend, and S3 bucket policy can add additional protection.

While IAM user and role policies already provide access control to the bucket, it is not uncommon to find organisations granting full S3 access to anyone who needs it. Ideally, this should not happen, but it unfortunately does.

The example policy makes sure only specific users can interact with objects in the bucket. Even if someone has allow s3:* on their IAM policy, the bucket policy will override that.

json { "Version": "2012-10-17", "Statement": [ { "Sid": "Statement", "Effect": "Deny", "Principal": "*", "Action": [ "s3:GetObject*", "s3:PutObject*", "s3:HeadObject*", "s3:DeleteObject*" ], "Resource": [ "arn:aws:s3:::terraform-bucket-name", "arn:aws:s3:::terraform-bucket-name/*" ], "Condition": { "ArnNotEquals": { "aws:PrincipalArn": [ "arn:aws:iam::100000000000:user/user1", "arn:aws:iam::100000000000:user/user2" ] } } } ] }

The policy denies object operations for all principals except those listed in the ArnNotEquals condition. The impact is defense in depth for state files even if IAM is overly permissive. The context is that Terraforming this policy adds code-level enforcement for state protection.

Cleanup and Destruction Workflow

Finally, if you are doing this exercise for learning purposes, you can clean up by destroying the created resource.

bash terraform destroy

Type yes, and hit enter. Once you hit enter, your resources get destroyed. Once done, this is how you see the destruction complete message.

The impact is cost avoidance and environment hygiene. The context is that destruction removes both bucket and policy resources created via Terraform.

Conclusion

In this article, we learnt How to Create S3 Bucket Policy using Terraform. You can use the standalone resource awss3bucketpolicy to create a policy or use policy parameter in resource awss3bucket. However, using policy parameter on bucket resource is deprecated and it is recommended to use awss3bucketpolicy resource to attach an s3 bucket policy to your bucket.

The material demonstrates creation of a public read-only policy using aws_iam_policy_document, attachment via aws_s3_bucket_policy, module-based bucket provisioning with log delivery policies, and state file protection via deny conditions. The patterns show how resource-based policies complement IAM, how ARN scoping differs for bucket vs object actions, and how Terraform can codify both permissive and restrictive access models for S3.

Sources

  1. How to Create S3 Bucket Policy using Terraform
  2. terraform-aws-modules/terraform-aws-s3-bucket
  3. Configure S3 Bucket Policies in Terraform
  4. Protect Terraform State Files with an S3 Bucket Policy

Related Posts