Security Pillar

S3 Encryption in Terraform: What the AWS Well-Architected Framework Actually Requires

AWS has encrypted S3 by default since January 2023. That does not mean your Terraform configuration meets the Well-Architected Security pillar. This post explains the three encryption types, exactly what the framework requires for sensitive workloads, and how to implement each option in Terraform.

May 20, 20268 min read#terraform#s3#kms#encryption#well-architected

Since January 2023, AWS automatically applies SSE-S3 encryption to every new S3 bucket. That means if you create a bucket in Terraform today without any encryption configuration, the objects in it are still encrypted at rest. Checkov rule CKV_AWS_19 passes. Your linter score is green.

But passing CKV_AWS_19 is not the same as meeting the AWS Well-Architected Security pillar requirements for S3. The WAF Security pillar (SEC 3: Data Protection) distinguishes between encryption that protects data from AWS infrastructure failures and encryption that protects data from unauthorised access — including from AWS employees, compromised IAM credentials, or misconfigured access policies. SSE-S3 addresses the first. Only SSE-KMS with a customer-managed key (CMK) addresses the second.

This post explains the three S3 encryption types, what the Well-Architected framework actually requires, why the Checkov rule is a floor not a ceiling, and how to implement four levels of compliance in Terraform — from the minimum to the configuration that satisfies PCI-DSS, HIPAA, and SOC 2 Type II audit requirements.

SSE-S3 vs SSE-KMS vs SSE-C — what is the difference?

AWS S3 supports three server-side encryption mechanisms. The choice between them determines who controls the encryption keys, what audit trail is available, and which compliance frameworks you can satisfy.

TypeKey controlCloudTrail auditWAF fit
SSE-S3AWS manages keys — no visibilityNo key-level audit entriesMinimum only
SSE-KMS (AWS key)AWS key — no policy controlKMS API calls loggedPartial
SSE-KMS (CMK)You own key policy + rotationFull per-operation audit trailFully compliant
SSE-CYou provide key per requestNo key logged (client secret)Not typical for IaC

SSE-S3 uses AES-256 with keys that AWS generates, stores, and rotates automatically. You have no control over the key lifecycle and no dedicated audit trail for key use — you can see S3 API calls in CloudTrail, but you cannot see which key was used for which object or revoke access to historical objects by disabling a key. This is adequate for data that has no regulatory classification.

SSE-KMS with an AWS-managed key (the aws/s3 key) uses AWS KMS, which means KMS API calls appear in CloudTrail. The limitation: you cannot modify the key policy, you cannot set a rotation schedule beyond the AWS-managed default, and you cannot delete the key or revoke access to historical data by key disablement. It is one step up from SSE-S3 in auditability but still falls short of the WAF Security pillar recommendation for sensitive workloads.

SSE-KMS with a customer-managed key (CMK) gives you full key policy control: you decide which IAM principals can use the key, you set the deletion window, you enable automatic annual rotation, and you get a per-operation CloudTrail audit entry for every encrypt and decrypt event. If an IAM credential is compromised, you can disable the CMK immediately and revoke the attacker’s ability to read historical objects — a capability that does not exist with SSE-S3 or the AWS-managed KMS key. This is what the Well-Architected Security pillar requires for any bucket holding sensitive, regulated, or personally identifiable data.

SSE-C requires the caller to supply the encryption key with every API request. The key is never stored by AWS. This creates a very high operational burden — every application that reads or writes the bucket must manage and transmit the key securely — and is rarely the right choice for Terraform-managed infrastructure. It has no dedicated Checkov rule and is not a pattern the Well-Architected framework promotes for standard workloads.

What the Well-Architected Security pillar says about S3 encryption

The AWS Well-Architected Security pillar — SEC 3: How do you protect your data at rest? establishes the standard. The best practice “Encrypt data at rest” calls for using an AWS KMS customer-managed key with automatic key rotation enabled for data classified as sensitive. The supporting best practices add:

  • 1.Enforce encryption using bucket policies — a bucket default does not prevent a caller from explicitly requesting no encryption on a PUT.
  • 2.Use separate CMKs per data classification tier — a single shared key across all buckets means a key compromise exposes all tiers simultaneously.
  • 3.Enable CloudTrail data events for S3 buckets holding sensitive data — object-level API calls are not logged by default in CloudTrail management events.
  • 4.Enforce HTTPS (aws:SecureTransport) via bucket policy — encryption at rest is only part of the picture; data in transit must also be protected.

CIS AWS Foundations Benchmark 2.1.1 and the AWS Foundational Security Best Practices (FSBP) standard — both of which AWS Security Hub can check — align with this. FSBP control S3.4 requires that any S3 bucket storing sensitive data use SSE-KMS. Security Hub does not enforce CMK vs. AWS-managed key by default, but the WAF pillar documentation is explicit: customer-managed keys are required where you need independent key lifecycle control.

Why passing Checkov CKV_AWS_19 is not enough

Checkov rule CKV_AWS_19 checks that an aws_s3_bucket_server_side_encryption_configuration resource block exists. That is the entirety of the check. It passes for SSE-S3 withAES256, for SSE-KMS with the AWS-managed key, and for SSE-KMS with a CMK — the check cannot distinguish between them.

Three things that CKV_AWS_19 does not check:

Key type

Whether the encryption uses an AWS-managed key or a CMK. A bucket using AES256 passes CKV_AWS_19 identically to one using a CMK with key rotation — from the linter's perspective, they are equivalent.

Bucket policy enforcement

Whether a bucket policy denies PutObject requests that omit the encryption header. Without enforcement, the bucket default applies as a fallback — not a guarantee. An application misconfiguration or a misconfigured SDK can upload unencrypted objects and the bucket will still accept them.

Transport enforcement

Whether HTTPS is enforced via the aws:SecureTransport condition. Encryption at rest without encryption in transit means data can be intercepted between the client and S3. CKV_AWS_20 and CKV_AWS_54 cover related public-access and ACL checks but not this condition directly.

This is a specific instance of the architectural context gap that linters cannot close by design. Checkov evaluates individual resources in isolation. It cannot determine whether the encryption configuration is appropriate for the data classification of the workload that uses the bucket. That judgement requires context the linter does not have — context that an architectural review provides. For the full picture on where linters stop and architecture reviews begin, see What Checkov Catches — and What It Misses.

Implementing Well-Architected S3 encryption in Terraform

Four implementation levels, from minimum to fully compliant. Use the table above and your data classification to decide which level applies to each bucket.

Starting point: no explicit encryption block

A bucket with no encryption configuration block relies on the AWS account default (SSE-S3 since January 2023). This pattern fails CKV_AWS_19 and documents nothing about your encryption intent in the Terraform code.

s3_bucket.tf✗ Before
# ❌ No explicit encryption block — relying on AWS default# Passes no Checkov rules. If the account default ever changes, this bucket changes with it.# Nothing in your IaC documents or enforces the encryption expectation.resource "aws_s3_bucket" "data" {  bucket = "my-company-data"}

No encryption block. Fails CKV_AWS_19. Relies on AWS account default — if the default ever changes, your bucket silently changes with it.

Option 1 — SSE-S3: explicit AES-256 (minimum)

Adding an explicit SSE-S3 configuration documents the intent in your Terraform code and passes CKV_AWS_19. This is the minimum for any bucket — but is not sufficient for buckets that hold PII, payment card data, health records, or any data under a regulatory framework.

s3_bucket.tf✓ After
# ✅ Option 1: Explicit SSE-S3 (minimum — satisfies Checkov CKV_AWS_19)# AWS manages the keys using AES-256. You have no key policy control or audit trail.# Adequate for non-sensitive data; not sufficient for PII, financial, or regulated workloads.resource "aws_s3_bucket" "data" {  bucket = "my-company-data"}resource "aws_s3_bucket_server_side_encryption_configuration" "data" {  bucket = aws_s3_bucket.data.id  rule {    apply_server_side_encryption_by_default {      sse_algorithm = "AES256"    }  }}

Explicit SSE-S3. Passes CKV_AWS_19. AWS manages keys. No policy control, no CMK audit trail. Suitable for non-sensitive data only.

Option 2 — SSE-KMS with AWS-managed key

Switching to aws:kms with the aws/s3 managed key adds KMS API call visibility in CloudTrail — you can now see when the key was used for encryption and decryption operations. Note bucket_key_enabled = true: this instructs S3 to use a bucket-level key as an intermediary, reducing per-object KMS API calls by approximately 99% and cutting KMS costs for high-volume buckets significantly.

s3_bucket.tf✓ After
# ✅ Option 2: SSE-KMS with AWS-managed key (aws/s3)# Checkov passes. KMS API calls visible in CloudTrail.# Limitation: you cannot attach a key policy, cannot rotate on demand, cannot revoke access.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/s3"  # AWS-managed key — no policy control    }    bucket_key_enabled = true  # Reduces per-request KMS API calls by ~99%  }}

SSE-KMS with AWS-managed key. Passes CKV_AWS_19. KMS API calls logged in CloudTrail. No key policy control. Suitable for internal workloads without compliance requirements.

Option 3 — SSE-KMS with customer-managed key (recommended for sensitive workloads)

A CMK gives you three capabilities that AWS-managed keys do not: key policy control (define exactly which IAM principals can use the key), automatic rotation (enable_key_rotation = true triggers annual rotation), and key disablement (if a credential is compromised, you can disable the key immediately, blocking decryption of historical objects). The deletion window of 30 days prevents accidental key deletion — a deleted CMK means encrypted objects become permanently unreadable.

s3_kms_cmk.tf✓ After
# ✅ Option 3: SSE-KMS with customer-managed key (CMK) — Well-Architected recommended# Gives you: key policy control, on-demand rotation, deletion window, full CloudTrail audit trail.# Required by: PCI-DSS, HIPAA, SOC 2 Type II for buckets holding sensitive data.data "aws_caller_identity" "current" {}resource "aws_kms_key" "s3" {  description             = "CMK for S3 bucket encryption — sensitive workloads"  deletion_window_in_days = 30  enable_key_rotation     = true  # Automatic annual rotation  policy = jsonencode({    Version = "2012-10-17"    Statement = [      {        Sid    = "RootAccountFullAccess"        Effect = "Allow"        Principal = {          AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"        }        Action   = "kms:*"        Resource = "*"      },      {        Sid    = "AllowS3Service"        Effect = "Allow"        Principal = { Service = "s3.amazonaws.com" }        Action   = ["kms:GenerateDataKey", "kms:Decrypt"]        Resource = "*"      },    ]  })}resource "aws_kms_alias" "s3" {  name          = "alias/my-company-s3-data"  target_key_id = aws_kms_key.s3.key_id}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.arn    }    bucket_key_enabled = true  }}

SSE-KMS with CMK. enable_key_rotation = true. bucket_key_enabled = true for cost efficiency. Required for PCI-DSS, HIPAA, SOC 2 Type II, and WAF Security pillar compliance on sensitive workloads.

Option 4 — Bucket policy: enforce encryption and HTTPS

Encryption configuration is a default — it applies when the caller does not specify otherwise. A misconfigured application, a legacy SDK, or an operator running a manual command can upload objects with explicitly no encryption header and the bucket default applies as a fallback. The bucket policy below turns that fallback into a hard enforcement: any PutObject that does not specify x-amz-server-side-encryption: aws:kms is denied. The second statement enforces HTTPS for all S3 operations on the bucket.

s3_policy.tf✓ After
# ✅ Option 4: Bucket policy enforcement — deny unencrypted transport and unencrypted PUTs# This is the Well-Architected policy layer on top of the encryption configuration.# Without this, a caller can upload an object without specifying an encryption header# and the bucket default applies — but there is no enforcement, just a fallback.resource "aws_s3_bucket_policy" "data" {  bucket = aws_s3_bucket.data.id  policy = jsonencode({    Version = "2012-10-17"    Statement = [      {        Sid       = "DenyUnencryptedTransport"        Effect    = "Deny"        Principal = "*"        Action    = "s3:*"        Resource  = [          aws_s3_bucket.data.arn,          "${aws_s3_bucket.data.arn}/*",        ]        Condition = {          Bool = { "aws:SecureTransport" = "false" }        }      },      {        Sid       = "DenyNonKMSEncryptedPuts"        Effect    = "Deny"        Principal = "*"        Action    = "s3:PutObject"        Resource  = "${aws_s3_bucket.data.arn}/*"        Condition = {          StringNotEquals = {            "s3:x-amz-server-side-encryption" = "aws:kms"          }        }      },    ]  })}

Bucket policy enforcement: denies any PutObject without an explicit KMS encryption header, and denies all S3 operations over plain HTTP. Apply on top of Option 3 for full WAF Security pillar coverage.

Which level is right for each of your S3 buckets depends on the data classification of the workload using that bucket — not on the bucket configuration in isolation. A linter sees the bucket. An architectural review sees what the bucket holds, which services write to it, and whether the encryption level is consistent with the data classification your organisation has assigned to that data. For the full process of evaluating your Terraform S3 configuration against the Well-Architected Security pillar alongside IAM, network, and observability findings, see the Terraform Architecture Review: A Complete Guide.

Frequently asked questions

Is S3 default encryption sufficient for compliance?

AWS enabled default SSE-S3 encryption for all new S3 buckets in January 2023, but SSE-S3 is not sufficient for most compliance frameworks. PCI-DSS, HIPAA, SOC 2 Type II, and the AWS Well-Architected Security pillar all recommend or require customer-managed KMS keys (CMKs) for buckets holding sensitive, regulated, or personally identifiable data. With AWS-managed keys you cannot enforce key policy, audit key usage independently of S3 access logs, or revoke access to historical data by disabling the key. CMKs give you all three capabilities.

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

SSE-S3 (algorithm: AES256) uses keys that are generated, stored, and managed entirely by AWS — you have no visibility into key operations. SSE-KMS uses AWS Key Management Service and has two variants: AWS-managed keys (the aws/s3 key AWS creates automatically in your account) and customer-managed keys (CMKs you create and control). With CMKs, you define the key policy, enable automatic rotation, control which IAM principals can use the key, and get detailed CloudTrail audit entries for every encryption and decryption operation. For any workload classified above 'public', SSE-KMS with a CMK is the Well-Architected recommendation.

What does Checkov CKV_AWS_19 check?

Checkov rule CKV_AWS_19 checks that an aws_s3_bucket_server_side_encryption_configuration resource exists for the S3 bucket. It passes when any encryption configuration block is present — SSE-S3 or SSE-KMS, AWS-managed or customer-managed key. It does not verify whether the key type is appropriate for the data classification, whether bucket_key_enabled is set, or whether a bucket policy enforces that all PutObject requests must specify encryption headers. Passing CKV_AWS_19 means you have some form of encryption configured; it does not mean your configuration meets the Well-Architected Security pillar requirements for sensitive workloads.

What is bucket_key_enabled in Terraform and should I use it?

bucket_key_enabled = true instructs S3 to use a bucket-level key as an intermediary rather than calling KMS for every individual object operation. This reduces the number of direct KMS API calls by approximately 99%, significantly cutting KMS request costs for high-volume buckets and reducing operation latency. The security properties are equivalent — the object data encryption key is still derived from the CMK. For any bucket handling more than a few thousand object operations per day, bucket_key_enabled = true is the recommended setting.

How do I enforce that all S3 uploads use KMS encryption?

Configure an aws_s3_bucket_policy resource with a Deny statement on s3:PutObject that applies when the condition StringNotEquals "s3:x-amz-server-side-encryption" = "aws:kms" is true. Any PutObject request without the x-amz-server-side-encryption: aws:kms header is then denied by the bucket policy regardless of the bucket default. Add a second statement denying all s3:* actions when aws:SecureTransport = false to enforce HTTPS. Together these two statements form the enforcement layer that the Well-Architected Security pillar expects alongside encryption configuration.

Architecture Review

See How Your Terraform S3 Configuration Scores

Upload your Terraform and get a Well-Architected review covering all four pillars — S3 encryption, IAM, network, and observability — with findings mapped by severity and a PDF report you can share with your team.

Findings include WAF pillar references, severity by workload context, and HCL remediation examples.