S3 Encryption Best Practices in Terraform: SSE-KMS vs SSE-S3
Last reviewed: 2026-05-27 · 9 min read
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
| Feature | SSE-S3 (AES-256) | SSE-KMS (CMK) |
|---|---|---|
| Key management | AWS-managed | You control |
| CloudTrail audit log per decrypt | No | Yes |
| Key rotation | Automatic (opaque) | Configurable (annual recommended) |
| Cross-account access | Not controllable | Via key policy |
| Cost | No additional cost | $1/key/month + API call costs |
| Well-Architected recommendation for sensitive data | Minimum baseline only | Recommended |
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.
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.
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.
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
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
KMS Key Rotation in Terraform →
IAM Least Privilege in Terraform →
VPC Security Groups Audit in Terraform →
AWS Security pillar overview →
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