S3 Encryption Best Practices in Terraform: SSE-KMS vs SSE-S3

Last reviewed: 2026-05-27 · 9 min read

Rost Mironenko
Rost Mironenko·Founder, ArchGuard

5+ years AWS engineering · Open-source contributor

Last reviewed: 2026-05-27

S3 server-side encryption in Terraform means three distinct controls working together: default bucket-level encryption with a KMS key you control, a public access block that prevents accidental exposure, and a bucket policy that rejects unencrypted transport. Enabling only one of the three leaves gaps that are visible in Terraform HCL before deployment.

SSE-S3 vs SSE-KMS: which to use

FeatureSSE-S3 (AES-256)SSE-KMS (CMK)
Key managementAWS-managedYou control
CloudTrail audit log per decryptNoYes
Key rotationAutomatic (opaque)Configurable (annual recommended)
Cross-account accessNot controllableVia key policy
CostNo additional cost$1/key/month + API call costs
Well-Architected recommendation for sensitive dataMinimum baseline onlyRecommended

Source: Amazon S3 server-side encryption documentation

Step 1 — Configure SSE-KMS with a customer-managed key

The pattern below creates a KMS key with annual rotation enabled, then configures the S3 bucket to use it as the default encryption key. Every object uploaded to the bucket is encrypted at rest using this key, and every decrypt operation is logged to CloudTrail.

s3-encrypted.tf✓ After
resource "aws_kms_key" "s3_key" {  description             = "S3 bucket encryption key"  deletion_window_in_days = 30  enable_key_rotation     = true  tags = {    ManagedBy = "terraform"  }}resource "aws_kms_alias" "s3_key" {  name          = "alias/${var.env}-s3-data"  target_key_id = aws_kms_key.s3_key.key_id}resource "aws_s3_bucket" "data" {  bucket = "${var.env}-app-data"  tags = {    ManagedBy = "terraform"  }}resource "aws_s3_bucket_server_side_encryption_configuration" "data" {  bucket = aws_s3_bucket.data.id  rule {    apply_server_side_encryption_by_default {      sse_algorithm     = "aws:kms"      kms_master_key_id = aws_kms_key.s3_key.arn    }    bucket_key_enabled = true  }}

bucket_key_enabled = true reduces KMS API call costs by up to 99% for high-throughput buckets

Step 2 — Block all public access

S3 public access blocks are four independent controls, each required. A bucket with three of four set to true is still vulnerable to the fourth vector. The AWS S3 public access block documentation describes what each setting prevents.

s3-encrypted.tf✓ After
resource "aws_s3_bucket_public_access_block" "data" {  bucket = aws_s3_bucket.data.id  block_public_acls       = true  block_public_policy     = true  ignore_public_acls      = true  restrict_public_buckets = true}

Step 3 — Enforce TLS with a bucket policy

Encryption at rest does not protect data in transit. A bucket policy that denies requests where aws:SecureTransport is false rejects any API call made over HTTP — including AWS SDK calls that forget to set the HTTPS endpoint.

s3-encrypted.tf✓ After
data "aws_iam_policy_document" "s3_tls_only" {  statement {    sid     = "DenyNonTLS"    effect  = "Deny"    actions = ["s3:*"]    principals {      type        = "*"      identifiers = ["*"]    }    resources = [      aws_s3_bucket.data.arn,      "${aws_s3_bucket.data.arn}/*",    ]    condition {      test     = "Bool"      variable = "aws:SecureTransport"      values   = ["false"]    }  }}resource "aws_s3_bucket_policy" "data" {  bucket = aws_s3_bucket.data.id  policy = data.aws_iam_policy_document.s3_tls_only.json  depends_on = [aws_s3_bucket_public_access_block.data]}

The depends_on prevents a race condition between the public access block and the bucket policy

What a misconfigured bucket looks like

s3-bad.tf✗ Before
resource "aws_s3_bucket" "data" {  bucket = "my-app-data"  # No encryption configuration  # No public access block  # No bucket policy}

A bucket with no encryption configuration uses SSE-S3 only if the account-level default is set — which is not guaranteed

Related Security pillar articles

Frequently asked questions

What is the difference between SSE-S3 and SSE-KMS for S3 encryption?

SSE-S3 (AES-256) uses an AWS-managed key with no visibility into key use or rotation. SSE-KMS uses a KMS key you control: you get CloudTrail logs for every Decrypt call, can enforce key policies, and can rotate the key. The AWS Well-Architected Security pillar recommends SSE-KMS with a customer-managed key for sensitive data.

Does enabling default S3 encryption encrypt existing objects?

No. Default bucket encryption only applies to objects uploaded after the setting is enabled. Existing objects retain their original encryption state. Re-encrypting existing objects requires a copy operation.

What does the S3 bucket public access block do?

The S3 public access block (aws_s3_bucket_public_access_block) prevents four categories of public exposure: ACLs that grant public access, bucket policies that grant public access, existing public ACLs, and existing public policies. All four should be set to true for non-public buckets.

How do I force TLS on an S3 bucket in Terraform?

Add a bucket policy statement with Effect: Deny, Action: s3:*, Condition: aws:SecureTransport = false. This denies all S3 API calls that are not made over HTTPS, covering both the console and programmatic access.

Get an automated S3 encryption review

ArchGuard reviews your Terraform for S3 encryption gaps across the AWS Well-Architected Security pillar and delivers a branded PDF in 24 hours.

See how it works