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.
In one of my previous posts, I shared with you “How to Create an S3 Bucket using Terraform”. In this post, I will show you how to create S3 bucket policy using one of the most popular IaC tools called Terraform. You will also see how the policy is created and attached to the bucket.
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.
Writing bucket policies in Terraform is straightforward once you understand the policy structure. Let's go through the common patterns you'll need.
What an S3 Bucket Policy Is in the Terraform Context
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 meaning of resource-based policy is, that instead of applying the policy to a principal like user/group/role, you apply the policy to the s3 bucket itself on which access is required
The impact of this design choice is visible in day-to-day operations. Because the policy lives on the bucket, any principal that matches the Principal element can act without needing an individual IAM policy attachment. For cross-account access this removes the need to modify IAM policies in the caller account. For internal governance this means the bucket owner retains centralized control over who can read or write.
In Terraform the policy is not a separate file edited by hand. It is generated from HCL and rendered as JSON at apply time. That means version control, peer review, and automated validation become possible for what was previously a manual console edit.
Resource-Based IAM Policy Fundamentals
A bucket policy consists of one or more statements, each with an Effect (Allow/Deny), Principal (who), Action (what), and Resource (which objects).
This four-part structure drives all Terraform examples. The Effect determines permission direction. The Principal identifies the caller, which can be a wildcard, an AWS account ARN, or a service principal. The Action enumerates S3 API operations. The Resource points to the bucket ARN and the object ARN pattern.
The real-world consequence is that mis-scoping Resource causes silent failures. ListBucket requires the bucket ARN without a trailing /* while GetObject requires the object ARN with /*. Terraform code that mixes these will produce a policy that appears correct but denies actual access.
Creating S3 Bucket Policy Using Terraform Standalone Resource
The recommended pattern is to use the standalone resource awss3bucketpolicy to create a policy or use policy parameter in the resource awss3_bucket.
However, using policy parameter on bucket resource is deprecated and it is recommended to use awss3bucket_policy resource to attach an s3 bucket policy to your bucket.
Final Configuration File to Create S3 Bucket Policy using Terraform
```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.27"
}
}
}
provider "aws" {
profile = "default"
region = "ap-south-1"
}
S3 Bucket on Which we will add policy
resource "awss3bucket" "demo-bucket"{
bucket = "ck-demo-bucket-04th"
}
Resource to add bucket policy to a bucket
resource "awss3bucketpolicy" "publicreadaccess" {
bucket = awss3bucket.demo-bucket.id
policy = data.awsiampolicydocument.publicreadaccess.json
}
```
The provider block with profile = "default" and region = "ap-south-1" determines where Terraform will create resources. The awss3bucket resource creates the bucket named ck-demo-bucket-04th. The awss3bucketpolicy resource binds the policy to that bucket by referencing awss3_bucket.demo-bucket.id.
The separation of bucket creation and policy attachment mirrors AWS console behavior where you click on your bucket name and click on the Permissions tab as shown below screenshot. Scroll down to the Bucket policy section and you will see our public read-only policy got attached to our bucket.
Data Source awsiampolicy_document Pattern
DataSource to generate a policy 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 awsiampolicy_document builds the JSON policy document from HCL. The principals block with type = "" and identifiers = [""] creates a public principal. The actions list includes s3:GetObject and s3:ListBucket. The resources list includes the bucket ARN and the bucket ARN with /*.
Using a data source instead of jsonencode gives Terraform validation of statement fields and avoids manual JSON syntax errors. The output .json is then consumed by the awss3bucket_policy resource.
The impact layer is operational safety. If the bucket name changes, the ARN references update automatically. If the policy document is rendered incorrectly, Terraform plan will fail before any AWS API call is made.
Public Read-Only Policy Example Walkthrough
The public read-only policy example shows a common first use case. The bucket is created private by default. The policy then opens read access to anyone.
The workflow in practice is:
- Create bucket with awss3bucket
- Define data.awsiampolicy_document with public principal and GetObject, ListBucket
- Attach via awss3bucketpolicy with bucket = awss3_bucket.demo-bucket.id and policy = data...json
After apply, the Permissions tab in the S3 console shows the attached policy. The bucket remains writable only by the owning account unless additional statements are added.
Clean Up
Finally, if you are doing this exercise for learning purposes, you can clean up by destroying the created resource.
hcl
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.
Destruction removes both the policy attachment and the bucket. With force_destroy disabled Terraform will refuse to delete a non-empty bucket, preventing accidental data loss.
Updating an Existing S3 Bucket Policy Without Replacement Loss
Here's how you can approach this:
First, retrieve the existing bucket policy using a data source or by importing the existing policy into your Terraform state.
Then, define the awss3bucket_policy resource in your Terraform configuration, referencing the existing bucket.
In the policy attribute of this resource, include both the existing policy statements and the new policy you want to add.
Here's an example of how your Terraform code might look:
hcl
resource "aws_s3_bucket_policy" "bucket_policy" {
bucket = aws_s3_bucket.your_bucket.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{ // Existing policy statements here },
{ // New policy statement to add
Sid = "NewPolicyStatement"
Effect = "Allow"
Principal = {
Service = "some-aws-service.amazonaws.com"
}
Action = ["s3:SomeAction"]
Resource = ["${aws_s3_bucket.your_bucket.arn}/*"]
}
]
})
}
In this example, you would replace the placeholder comments with your actual existing policy statements and the new policy statement you want to add.
Remember that Terraform will replace the entire bucket policy when you apply this configuration
The impact of full replacement is critical. AWS does not merge policies incrementally. If you define a new awss3bucket_policy with only the new statement, the existing statements are removed. The mitigation is to read the current policy into state via import or data source, then construct a merged Statement array that contains both old and new entries.
Terraform AWS Modules S3 Bucket Module Policy 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 policy attachment for log delivery use cases. Instead of writing a custom awss3bucket_policy for ELB, ALB/NLB, and WAF logs, the module exposes boolean flags that generate the correct policy documents.
Module usage examples from the reference material show:
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
}
}
This creates a bucket with private ACL, object ownership controlled by the account, and versioning enabled.
```hcl
module "s3bucketfor_logs" {
source = "terraform-aws-modules/s3-bucket/aws"
bucket = "my-s3-bucket-for-logs"
acl = "log-delivery-write"
Allow deletion of non-empty bucket
forcedestroy = true
controlobjectownership = true
objectownership = "ObjectWriter"
attachelblogdeliverypolicy = true
}
```
The log delivery bucket uses acl = "log-delivery-write" and forcedestroy = true to allow automated cleanup. The attachelblogdelivery_policy flag injects the required policy for ELB logs.
```hcl
module "s3bucketfor_logs" {
source = "terraform-aws-modules/s3-bucket/aws"
bucket = "my-s3-bucket-for-logs"
Allow deletion of non-empty bucket
forcedestroy = true
controlobjectownership = true
objectownership = "ObjectWriter"
attachelblogdeliverypolicy = true # Required for ALB logs
attachlblogdeliverypolicy = true # Required for ALB/NLB logs
}
```
Both ELB and LB log delivery policies can be enabled together. The module handles the Principal and Action values required by AWS load balancers.
hcl
module "s3_bucket_for_waf_logs" {
source = "terraform-aws-modules/s3-bucket/aws"
bucket =
The pattern repeats for WAF logs with a dedicated module invocation.
To keep bucket policy with correct S3 bucket and AWS account properties, you can use the placeholders S3BUCKETID, S3BUCKETARN, and AWSACCOUNTID in the policy document. Those values will be replaced with the actual values during the policy attachment. This is especially useful when using bucket prefixes.
Sometimes you need to have a way to create S3 resources conditionally but Terraform does not allow to use count inside module block, so the solution is to specify argument create_bucket.
This S3 bucket will not be created
```hcl
module "s3bucket" {
source = "terraform-aws-modules/s3-bucket/aws"
createbucket = false
..
```
Setting create_bucket = false prevents the module from creating the bucket while still allowing other module outputs to be evaluated. This is useful for conditional deployments.
Basic Bucket Policy Pattern for IAM Role Read Access
A bucket policy consists of one or more statements, each with an Effect (Allow/Deny), Principal (who), Action (what), and Resource (which objects).
```hcl
resource "awss3bucket" "data" {
bucket_prefix = "app-data-"
}
Basic bucket policy allowing a specific IAM role to read objects
resource "awss3bucketpolicy" "data" {
bucket = awss3bucket.data.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowAppRoleRead"
Effect = "Allow"
Principal = {
AWS = awsiamrole.app.arn
}
Action = [
"s3:GetObject",
"s3:ListBucket"
]
Resource = [
awss3bucket.data.arn, # For ListBucket
"${awss3_bucket.data.arn}/*" # For GetObject
]
}
]
})
}
```
Notice that ListBucket applies to the bucket ARN (without /) while GetObject applies to the objects (with /)
The bucket_prefix usage creates a unique name with a prefix. The policy restricts access to a single IAM role ARN, avoiding wildcard principals. The Resource array explicitly separates bucket-level and object-level permissions.
The contextual layer connects this pattern to the public read example. The only difference is Principal. Public uses type = "" identifiers = [""]. Role-specific uses Principal = { AWS = awsiamrole.app.arn }. The Action and Resource scoping remains identical.
ListBucket Versus GetObject Resource Scoping
The distinction between bucket ARN and object ARN is a recurring source of errors.
ListBucket applies to the bucket ARN without /*. This permission allows enumeration of objects within the bucket.
GetObject applies to the objects with /*. This permission allows retrieval of object contents.
If a policy grants GetObject only on the bucket ARN, no objects can be read. If a policy grants ListBucket on the /* ARN, the list operation fails.
Terraform configurations that use interpolation "${awss3bucket.data.arn}/*" ensure the correct pattern is generated even when bucket names change.
Cleanup and Destruction Workflow
The article concludes with cleanup instructions. After learning, destroy resources with terraform destroy. Type yes, and hit enter. Once you hit enter, your resources get destroyed.
This step is important for cost control and security hygiene. Orphaned buckets with public policies can remain billable and expose data. Destroying via Terraform ensures both the bucket and its attached policy are removed in a single operation.
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 the resource awss3_bucket.
However, using policy parameter on bucket resource is deprecated and it is recommended to use awss3bucket_policy resource to attach an s3 bucket policy to your bucket.
I hope you were able to work up with me and able to create the s3 bucket policy. If you get stuck at any time feel free to add a comment. I will reply to your query asap.
Well, That was my take on “How to Create S3 Bucket Policy using Terraform“. Please feel free to share your feedback.
Enjoyed the content?
Subscribe to our newsletter below to get awesome AWS learning materials delivered straight to your inbox.
If you liked reading my post, you can motivate me by-
- Adding a comment below on what you liked and what can be improved.
- Follow us on Facebook, Twitter, LinkedIn, Instagram
- Share this post with your friends and colleagues.
The deep drilling through reference facts shows that S3 bucket policy creation in Terraform is not a single command but a set of interlocking patterns: data source generation, standalone awss3bucket_policy attachment, module-driven log delivery policies, placeholder substitution for dynamic names, and careful merging when updating existing policies. Each pattern preserves the core resource-based IAM semantics while adding IaC safety, version control, and repeatability.