Table of Contents
Introduction
S3 is one of the most used AWS services, but misconfigured buckets remain a top cloud security risk. This guide covers how to create properly secured, optimized S3 buckets with Terraform following AWS best practices for encryption, access control, lifecycle management, and cross-region replication.
Secure Bucket Foundation
resource "aws_s3_bucket" "main" {
bucket = "mycompany-${var.environment}-data"
tags = {
Environment = var.environment
ManagedBy = "terraform"
}
}
# Block ALL public access
resource "aws_s3_bucket_public_access_block" "main" {
bucket = aws_s3_bucket.main.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# Enable versioning
resource "aws_s3_bucket_versioning" "main" {
bucket = aws_s3_bucket.main.id
versioning_configuration {
status = "Enabled"
}
}
# Server-side encryption with KMS
resource "aws_s3_bucket_server_side_encryption_configuration" "main" {
bucket = aws_s3_bucket.main.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.s3.arn
}
bucket_key_enabled = true
}
}
Lifecycle Rules
resource "aws_s3_bucket_lifecycle_configuration" "main" {
bucket = aws_s3_bucket.main.id
rule {
id = "transition-to-ia"
status = "Enabled"
transition {
days = 30
storage_class = "STANDARD_IA"
}
transition {
days = 90
storage_class = "GLACIER_IR"
}
transition {
days = 180
storage_class = "DEEP_ARCHIVE"
}
}
rule {
id = "cleanup-old-versions"
status = "Enabled"
noncurrent_version_transition {
noncurrent_days = 30
storage_class = "STANDARD_IA"
}
noncurrent_version_expiration {
noncurrent_days = 90
}
}
rule {
id = "abort-multipart"
status = "Enabled"
abort_incomplete_multipart_upload {
days_after_initiation = 7
}
}
rule {
id = "expire-temp"
status = "Enabled"
filter {
prefix = "tmp/"
}
expiration {
days = 7
}
}
}
Access Logging
resource "aws_s3_bucket" "logs" {
bucket = "mycompany-${var.environment}-access-logs"
}
resource "aws_s3_bucket_logging" "main" {
bucket = aws_s3_bucket.main.id
target_bucket = aws_s3_bucket.logs.id
target_prefix = "s3-access-logs/"
}
Cross-Region Replication
resource "aws_s3_bucket" "replica" {
provider = aws.eu_west_1
bucket = "mycompany-${var.environment}-data-replica"
}
resource "aws_s3_bucket_replication_configuration" "main" {
bucket = aws_s3_bucket.main.id
role = aws_iam_role.replication.arn
rule {
id = "replicate-all"
status = "Enabled"
destination {
bucket = aws_s3_bucket.replica.arn
storage_class = "STANDARD_IA"
encryption_configuration {
replica_kms_key_id = aws_kms_key.s3_replica.arn
}
}
source_selection_criteria {
sse_kms_encrypted_objects {
status = "Enabled"
}
}
}
depends_on = [aws_s3_bucket_versioning.main]
}
Bucket Policy
data "aws_iam_policy_document" "bucket_policy" {
# Enforce TLS
statement {
sid = "EnforceTLS"
effect = "Deny"
actions = ["s3:*"]
resources = [
aws_s3_bucket.main.arn,
"${aws_s3_bucket.main.arn}/*"
]
principals {
type = "*"
identifiers = ["*"]
}
condition {
test = "Bool"
variable = "aws:SecureTransport"
values = ["false"]
}
}
# Enforce encryption
statement {
sid = "EnforceEncryption"
effect = "Deny"
actions = ["s3:PutObject"]
resources = ["${aws_s3_bucket.main.arn}/*"]
principals {
type = "*"
identifiers = ["*"]
}
condition {
test = "StringNotEquals"
variable = "s3:x-amz-server-side-encryption"
values = ["aws:kms"]
}
}
}
resource "aws_s3_bucket_policy" "main" {
bucket = aws_s3_bucket.main.id
policy = data.aws_iam_policy_document.bucket_policy.json
}
Event Notifications
resource "aws_s3_bucket_notification" "main" {
bucket = aws_s3_bucket.main.id
lambda_function {
lambda_function_arn = aws_lambda_function.processor.arn
events = ["s3:ObjectCreated:*"]
filter_prefix = "uploads/"
filter_suffix = ".csv"
}
topic {
topic_arn = aws_sns_topic.s3_events.arn
events = ["s3:ObjectRemoved:*"]
}
}
Best Practices
- Always block public access unless explicitly needed
- Enable versioning for data protection and recovery
- Use KMS encryption with bucket keys for cost-effective encryption
- Set lifecycle rules to optimize storage costs automatically
- Enable access logging for audit compliance
- Enforce TLS via bucket policy
- Use cross-region replication for disaster recovery
- Abort incomplete multipart uploads to avoid hidden costs
- Tag everything for cost allocation
Conclusion
A properly configured S3 bucket with Terraform goes far beyond just creating storage. By implementing encryption, access controls, lifecycle policies, replication, and monitoring, you build a secure and cost-optimized storage foundation that meets enterprise compliance requirements.

