Best Practice: Encryption Strategy
Context
Encryption is the last line of defense: even if access control, network, and all other layers fail, encryption protects the data.
Common encryption mistakes:
-
S3 buckets with SSE-S3 (AES256) instead of CMK – AWS manages the key, no real key ownership
-
EBS volumes without encryption-by-default configuration
-
Load balancers that still accept TLS 1.0 and 1.1
-
Self-signed certificates in production without rotation
-
No distinction between data categories (PII vs. operational)
Related Controls
-
WAF-SEC-030 – Encryption at Rest with CMK
-
WAF-SEC-040 – Encryption in Transit – TLS Enforcement
Encryption Strategy: Which Data Requires CMK?
| Data Category | Minimum Requirement | Recommendation | Examples |
|---|---|---|---|
PII (personal data) |
CMK |
CMK with key policy |
Customer data, user profiles, email addresses |
Financial data |
CMK |
CMK or BYOK |
Transactions, account data, payment information |
Health data |
CMK |
CMK with HSM backing |
Patient data, diagnostic information |
Internal audit logs |
CMK |
CMK |
CloudTrail, application logs |
Operational data |
SSE-KMS (CMK recommended) |
CMK |
Metrics, monitoring data |
Public assets |
SSE-S3 acceptable |
SSE-S3 |
Static website assets, public documents |
KMS Key Hierarchy
KMS Key Hierarchy:
Master Keys (CMK) – one per data category:
├── cmk-pii → RDS customer database, S3 PII bucket
├── cmk-financial → RDS financial data, DynamoDB transactions
├── cmk-logs → CloudTrail S3, CloudWatch log groups
├── cmk-secrets → Secrets Manager keys
├── cmk-backup → AWS Backup vault
└── cmk-general → All other production resources
Envelope Encryption (automatically by AWS):
CMK (KMS)
└── encrypts: Data Encryption Key (DEK, 256-bit AES)
└── DEK encrypts: actual data in S3/RDS/EBS
Envelope Encryption Explained
Envelope encryption is the concept where a data encryption key (DEK) is encrypted with another key (CMK):
-
Application requests DEK from KMS
-
KMS generates DEK and an encrypted copy (DEK encrypted with CMK)
-
Application encrypts data with DEK
-
Encrypted DEK is stored together with the data
-
CMK never leaves KMS – only DEKs are handed to applications
Advantage: When the CMK rotates, only DEKs are re-encrypted – not all data. When the CMK is deleted, all data protected by it immediately becomes undecryptable (cryptographic erasure).
Technical Implementation
KMS CMK with Correct Key Policy
# CMK for PII data
resource "aws_kms_key" "pii" {
description = "CMK for PII data (customer data, authentication)"
deletion_window_in_days = 30 # At least 14 days
# Automatic annual rotation of key material
enable_key_rotation = true
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "EnableIAMUserPermissions"
Effect = "Allow"
Principal = {
AWS = "arn:aws:iam::${var.account_id}:root"
}
Action = "kms:*"
Resource = "*"
},
{
Sid = "AllowAppUsage"
Effect = "Allow"
Principal = {
AWS = aws_iam_role.app.arn
}
Action = [
"kms:Decrypt",
"kms:GenerateDataKey"
]
Resource = "*"
},
{
Sid = "AllowCloudTrailUsage"
Effect = "Allow"
Principal = {
Service = "cloudtrail.amazonaws.com"
}
Action = [
"kms:GenerateDataKey*",
"kms:DescribeKey"
]
Resource = "*"
}
]
})
tags = {
data-class = "pii"
pillar = "security"
managed-by = "terraform"
}
}
resource "aws_kms_alias" "pii" {
name = "alias/cmk-pii"
target_key_id = aws_kms_key.pii.key_id
}
S3 with CMK (SSE-KMS)
resource "aws_s3_bucket" "customer_data" {
bucket = "myorg-customer-data-prod"
}
resource "aws_s3_bucket_server_side_encryption_configuration" "customer_data" {
bucket = aws_s3_bucket.customer_data.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.pii.arn # CMK, not AES256
}
bucket_key_enabled = true # Reduces KMS API calls and costs
}
}
# Non-Compliant: SSE-S3 (AES256) for PII data
# resource "aws_s3_bucket_server_side_encryption_configuration" "bad" {
# rule {
# apply_server_side_encryption_by_default {
# sse_algorithm = "AES256" # AWS-managed key – no CMK!
# }
# }
# }
RDS with Encryption at Rest
resource "aws_db_instance" "main" {
identifier = "prod-db"
engine = "postgres"
engine_version = "15.4"
instance_class = "db.t3.medium"
storage_encrypted = true # Encryption enabled
kms_key_id = aws_kms_key.pii.arn # CMK (not default KMS)
# Additional security settings
publicly_accessible = false # Never public
deletion_protection = true # Protection against accidental deletion
backup_retention_period = 7 # 7-day backup retention
# IAM authentication instead of password (for apps)
iam_database_authentication_enabled = true
}
TLS Configuration
ALB: Modern TLS Security Policy
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.main.arn
port = "443"
protocol = "HTTPS"
# Modern TLS policy: TLS 1.2 and 1.3, strong cipher suites
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = aws_acm_certificate.main.arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.app.arn
}
}
# HTTP → HTTPS redirect (no plain HTTP!)
resource "aws_lb_listener" "http_redirect" {
load_balancer_arn = aws_lb.main.arn
port = "80"
protocol = "HTTP"
default_action {
type = "redirect"
redirect {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
}
CloudFront: Minimum TLS Policy
resource "aws_cloudfront_distribution" "main" {
viewer_certificate {
acm_certificate_arn = aws_acm_certificate.cdn.arn
ssl_support_method = "sni-only"
minimum_protocol_version = "TLSv1.2_2021" # No TLS 1.0 or 1.1
}
}
Recommended Cipher Suites
For ALB security policies (as of 2025):
-
Recommended:
ELBSecurityPolicy-TLS13-1-2-2021-06(TLS 1.2 and 1.3) -
Minimum:
ELBSecurityPolicy-2016-08(TLS 1.2 only) -
Prohibited: Policies that allow TLS 1.0 or 1.1
TLS 1.3 cipher suites (automatically negotiated):
* TLS_AES_128_GCM_SHA256
* TLS_AES_256_GCM_SHA384
* TLS_CHACHA20_POLY1305_SHA256
Certificate Management
AWS Certificate Manager (ACM)
ACM manages TLS certificates fully automatically (issuance, renewal, deployment on ALB/CloudFront):
# ACM certificate with automatic renewal
resource "aws_acm_certificate" "main" {
domain_name = "*.example.com"
subject_alternative_names = ["example.com"]
validation_method = "DNS"
lifecycle {
create_before_destroy = true # Zero-downtime renewal
}
}
# DNS validation via Route 53
resource "aws_route53_record" "cert_validation" {
for_each = {
for dvo in aws_acm_certificate.main.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
zone_id = aws_route53_zone.main.zone_id
name = each.value.name
type = each.value.type
records = [each.value.record]
ttl = 60
}
Let’s Encrypt (for internal endpoints)
For internal services that do not use ACM (e.g., HashiCorp Vault, internal APIs):
-
Certbot or cert-manager (Kubernetes) for automatic Let’s Encrypt certificates
-
Wildcard certificates via DNS-01 challenge (for internal domains)
-
ACME protocol for automatic renewal
Anti-Patterns
-
AES256 for PII: SSE-S3 (AES256) means AWS manages the key – no CMK ownership.
-
Single KMS key for everything: Loss of one key should not affect all data categories.
-
No
bucket_key_enabled: Without bucket key, high KMS API costs are incurred. -
Manage certificates manually: ACM for all AWS services; no manual upload without rotation.
-
TLS 1.0/1.1 for "compatibility": TLS 1.2+ has been universally supported since 2022.
Metrics
-
Share of PII/financial data stores with CMK: Target 100%
-
KMS key rotation enabled: Target 100% of all CMKs
-
TLS 1.2+ on all external endpoints: Target 100%
-
ACM certificates with expiry > 30 days: Target 100% (ACM renews automatically)
-
EBS encryption by default: Target: Enabled in all accounts