Multi-Region S3 with Replication and Encryption: A Terraform Guide

IaC Genius Team2026-03-287 min read

S3 is deceptively simple to start with — create a bucket, upload objects, done. But a production-grade S3 setup for compliance or disaster recovery looks very different: versioning enabled, server-side encryption with KMS, cross-region replication with its own IAM role and destination bucket configuration, lifecycle policies to manage storage costs, public access blocked at every level, and access logging for audit trails.

Writing this in Terraform involves more moving parts than most people expect. IaC Genius generates the complete configuration with every piece connected correctly.

What Makes Multi-Region S3 Complex

Cross-Region Replication Prerequisites

CRR (Cross-Region Replication) has strict prerequisites that must all be in place before replication works:

  1. Versioning must be enabled on both buckets — source and destination. Enable it on just one and replication silently does nothing.
  2. Both buckets must exist before you configure replication — Terraform's dependency resolution handles this, but only if you write the depends_on correctly.
  3. An IAM role with specific S3 permissions must exist and be referenced in the replication configuration. The trust policy must allow s3.amazonaws.com to assume the role.
  4. The destination bucket's region must be different from the source — AWS will return an error if you accidentally point replication at the same region.

KMS Encryption Across Regions

If you encrypt the source bucket with a KMS key (as you should), replication needs permission to decrypt objects from the source key AND encrypt them with a destination key. This means:

  • Two KMS keys (one per region)
  • The replication IAM role needs kms:Decrypt on the source key and kms:GenerateDataKey on the destination key
  • The KMS key policies need to allow the replication role access

Miss any of these and encrypted objects silently fail to replicate with no obvious error in the S3 console.

Lifecycle Policies and Cost Management

Without lifecycle policies, versioned buckets accumulate non-current versions indefinitely. A bucket that receives frequent writes can balloon in storage costs within weeks. Lifecycle rules need to be attached to the right prefixes, apply to non-current versions specifically, and have transition rules (to Glacier) before expiration rules — in the wrong order, Glacier transition rules conflict with expiration rules and one silently wins.

Public Access Block

S3 has four separate public access block settings at both the bucket and account level. All four should be true for private buckets. AWS has been adding defaults here over time, but Terraform configurations that don't explicitly set these leave you dependent on account-level defaults that may change.

What IaC Genius Generates

Source Bucket with Versioning and Encryption

resource "aws_s3_bucket" "source" {
  bucket        = "${var.project}-${var.environment}-primary"
  force_destroy = var.force_destroy

  tags = var.tags
}

resource "aws_s3_bucket_versioning" "source" {
  bucket = aws_s3_bucket.source.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "source" {
  bucket = aws_s3_bucket.source.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = aws_kms_key.source.arn
    }
    bucket_key_enabled = true
  }
}

resource "aws_s3_bucket_public_access_block" "source" {
  bucket                  = aws_s3_bucket.source.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

bucket_key_enabled = true is an easy win that reduces KMS API calls by up to 99%, significantly cutting KMS costs on high-traffic buckets.

Replication IAM Role

resource "aws_iam_role" "replication" {
  name = "${var.project}-s3-replication"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Service = "s3.amazonaws.com" }
      Action    = "sts:AssumeRole"
    }]
  })
}

resource "aws_iam_role_policy" "replication" {
  name = "replication-policy"
  role = aws_iam_role.replication.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "s3:GetReplicationConfiguration",
          "s3:ListBucket"
        ]
        Resource = aws_s3_bucket.source.arn
      },
      {
        Effect = "Allow"
        Action = [
          "s3:GetObjectVersionForReplication",
          "s3:GetObjectVersionAcl",
          "s3:GetObjectVersionTagging"
        ]
        Resource = "${aws_s3_bucket.source.arn}/*"
      },
      {
        Effect = "Allow"
        Action = [
          "s3:ReplicateObject",
          "s3:ReplicateDelete",
          "s3:ReplicateTagging"
        ]
        Resource = "${aws_s3_bucket.destination.arn}/*"
      },
      {
        Effect = "Allow"
        Action = ["kms:Decrypt"]
        Resource = aws_kms_key.source.arn
      },
      {
        Effect = "Allow"
        Action = ["kms:GenerateDataKey"]
        Resource = aws_kms_key.destination.arn
      }
    ]
  })
}

This is the complete IAM policy for KMS-encrypted cross-region replication. Every permission is present, every resource ARN is correct, and the KMS decrypt/encrypt permissions are both included.

Replication Configuration

resource "aws_s3_bucket_replication_configuration" "main" {
  role   = aws_iam_role.replication.arn
  bucket = aws_s3_bucket.source.id

  rule {
    id     = "replicate-all"
    status = "Enabled"

    destination {
      bucket        = aws_s3_bucket.destination.arn
      storage_class = var.replica_storage_class

      encryption_configuration {
        replica_kms_key_id = aws_kms_key.destination.arn
      }
    }

    source_selection_criteria {
      sse_kms_encrypted_objects {
        status = "Enabled"
      }
    }
  }

  depends_on = [
    aws_s3_bucket_versioning.source,
    aws_s3_bucket_versioning.destination
  ]
}

The depends_on block ensures versioning is active on both buckets before replication is configured — the most common source of silent replication failures.

Lifecycle Policy

resource "aws_s3_bucket_lifecycle_configuration" "source" {
  bucket = aws_s3_bucket.source.id

  rule {
    id     = "manage-versions"
    status = "Enabled"

    noncurrent_version_transition {
      noncurrent_days = var.glacier_transition_days
      storage_class   = "GLACIER"
    }

    noncurrent_version_expiration {
      noncurrent_days = var.expiration_days
    }
  }
}

Old versions move to Glacier for cheap long-term retention, then expire after your configured retention period. Storage costs stay predictable.

Security Best Practices Built In

  • KMS encryption on both buckets: Customer-managed KMS keys give you full control over key rotation and access policies
  • Public access blocked at all four levels: No accidental public exposure
  • Access logging: S3 server access logs written to a dedicated logging bucket for audit trails
  • Versioning on both buckets: Enables point-in-time recovery and is required for replication
  • Delete marker replication: Deletes propagate to the replica, preventing replica drift

Customization Options

  • Storage class for replicas: STANDARD, STANDARD_IA, or GLACIER depending on access patterns
  • Replication scope: All objects or specific prefixes
  • Lifecycle retention periods: Glacier transition and expiration days
  • KMS key rotation: Automatic annual rotation enabled by default
  • Access logging: Enable/disable with dedicated logging bucket

Who Uses This

Multi-region S3 replication is most commonly needed for:

  • Compliance requirements: GDPR, HIPAA, and PCI-DSS often require data redundancy in separate geographic regions
  • Disaster recovery: RPO close to zero for critical data assets
  • Multi-region application architectures: Serving data to users from the region closest to them
  • Backup strategy: A separate region means infrastructure events in one region don't affect your backup

Try the Multi-Region S3 template for free at app.iacgenius.com. Generate a complete, validated S3 replication setup in minutes.