This check flags S3 buckets whose default encryption uses AES256 or an AWS-managed KMS key instead of a customer-managed key (CMK). Without a CMK you lose granular access control, key rotation policies, and full audit visibility over who can decrypt your data. Fix it by setting bucket default encryption to aws:kms with a CMK you own and a bucket key to control cost.
Server-side encryption on S3 is one of those settings that almost everyone assumes is "handled." Buckets have been encrypted by default since January 2023, so a quick glance at the console shows a green checkmark and most teams move on. The problem is that the default encryption uses an AWS-managed key, and that key gives you almost none of the control that compliance frameworks and serious threat models actually require.
This check, s3_nocmk, looks at the default encryption configuration of each bucket and reports any bucket that relies on AES256 (SSE-S3) or the AWS-managed KMS key (aws/s3) rather than a customer-managed KMS key.
What this check detects
S3 supports several flavors of server-side encryption, and they are not equal from a security and governance standpoint:
- SSE-S3 (AES256): Amazon manages the keys entirely. You cannot see, control, or audit them.
- SSE-KMS with the AWS-managed key (
aws/s3): AWS creates and manages a KMS key on your behalf. You get CloudTrail visibility into decrypt calls, but you cannot edit the key policy, control rotation, or restrict which principals may use it. - SSE-KMS with a customer-managed key (CMK): You create and own the key. You define the key policy, rotation schedule, and can revoke access independently of bucket policies and IAM.
The check inspects each bucket's GetBucketEncryption result. If the SSEAlgorithm is AES256, or it is aws:kms but the KMSMasterKeyID points at the AWS-managed alias rather than one of your own keys, the bucket is flagged.
Note: A customer-managed key (CMK) is just a KMS key that you create in your account, as opposed to the AWS-managed keys that services spin up automatically. The capability difference is in control, not cryptographic strength. The actual data encryption uses AES-256 in all three cases.
Why it matters
The headline reason is control over the decryption boundary. With SSE-S3, encryption protects against one specific scenario: someone walking out of an AWS data center with a physical disk. It does essentially nothing against the threats that actually breach cloud data, like over-permissive IAM, leaked credentials, or a compromised application role.
A CMK adds a second, independent gate. To read an object, a principal needs both S3 permissions and permission on the KMS key policy. Consider a common breach pattern:
- An attacker compromises an EC2 instance role that has
s3:GetObjecton a sensitive bucket. - With SSE-S3 or the AWS-managed key, the data decrypts transparently. The attacker downloads everything.
- With a CMK whose key policy does not grant that role
kms:Decrypt, theGetObjectcall fails. The data stays opaque.
That second gate is the whole point. It lets your security team scope decryption access narrowly and revoke it instantly by editing one key policy, without touching dozens of bucket and IAM policies.
Beyond the threat model, several compliance regimes effectively require customer-managed keys for regulated data. PCI DSS, HIPAA aligned controls, and many internal data classification policies expect you to demonstrate key ownership, rotation, and access logging. The AWS-managed key cannot satisfy those because you cannot edit its policy or attach your own rotation evidence.
Warning: Switching to a CMK does not retroactively re-encrypt existing objects. Default encryption only applies to objects written after the change. Objects already in the bucket keep their original encryption until you rewrite them. Plan a copy or batch operation if you need every object under the CMK.
How to fix it
The fix has two steps: create or pick a CMK, then point the bucket's default encryption at it. Enable an S3 Bucket Key while you are at it to keep KMS costs sane.
Step 1: Create a customer-managed key
aws kms create-key \
--description "S3 default encryption key - app-data" \
--key-usage ENCRYPT_DECRYPT \
--key-spec SYMMETRIC_DEFAULT \
--tags TagKey=purpose,TagValue=s3-encryption
# Give it a friendly alias
aws kms create-alias \
--alias-name alias/s3-app-data \
--target-key-id <key-id-from-previous-output>
Enable automatic annual rotation so you do not have to manage it manually:
aws kms enable-key-rotation --key-id <key-id>
Step 2: Set bucket default encryption to the CMK
aws s3api put-bucket-encryption \
--bucket my-sensitive-bucket \
--server-side-encryption-configuration '{
"Rules": [
{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": "arn:aws:kms:us-east-1:111122223333:key/<key-id>"
},
"BucketKeyEnabled": true
}
]
}'
Verify the result:
aws s3api get-bucket-encryption --bucket my-sensitive-bucket
Tip: BucketKeyEnabled: true is not optional in practice. It tells S3 to use a short-lived bucket-level data key instead of calling KMS for every single object operation. On a busy bucket this cuts KMS API calls (and the bill) by up to 99 percent. There is no security downside.
Step 3: Lock down the key policy
A CMK only earns its keep if its key policy is restrictive. Grant decrypt access only to the roles that genuinely need it:
{
"Sid": "AllowAppRoleDecrypt",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111122223333:role/app-data-reader"
},
"Action": [
"kms:Decrypt",
"kms:GenerateDataKey"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"kms:ViaService": "s3.us-east-1.amazonaws.com"
}
}
}
The kms:ViaService condition ensures the key can only be used through S3, not directly by an attacker who steals the role's credentials and tries to call KMS on their own.
Terraform example
If you manage infrastructure as code, encode all of this so it cannot drift:
resource "aws_kms_key" "s3" {
description = "S3 default encryption key - app-data"
enable_key_rotation = true
deletion_window_in_days = 30
}
resource "aws_kms_alias" "s3" {
name = "alias/s3-app-data"
target_key_id = aws_kms_key.s3.key_id
}
resource "aws_s3_bucket_server_side_encryption_configuration" "this" {
bucket = aws_s3_bucket.app_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
}
}
Danger: If you ever schedule a CMK for deletion, every object encrypted with it becomes permanently unreadable once the deletion completes. There is no recovery. Always use a long deletion_window_in_days (the maximum is 30) and confirm no buckets reference the key before deleting it.
How to prevent it from happening again
Fixing one bucket by hand is fine. Stopping the next ten buckets from launching with the wrong encryption is what actually moves the needle.
Enforce it with an SCP or bucket policy
Deny any PutObject that does not specify your KMS encryption. This blocks uploads that would land under SSE-S3 or no encryption header:
{
"Sid": "DenyNonCmkUploads",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-sensitive-bucket/*",
"Condition": {
"StringNotEquals": {
"s3:x-amz-server-side-encryption": "aws:kms"
}
}
}
Gate it in CI/CD with policy-as-code
Catch misconfigured buckets before they ever reach an account. A Checkov or OPA rule in your pipeline can reject Terraform plans that set sse_algorithm = "AES256" or omit a CMK ARN:
checkov -d ./terraform \
--check CKV_AWS_145 # Ensure S3 buckets are encrypted with KMS by default
Continuous detection
Pipeline gates only cover resources you create through the pipeline. Manual changes, third-party tools, and console clicks will eventually create drift. A continuous scan across every account closes that gap, which is exactly what the Lensix s3_nocmk check does on each run, so a bucket that slips through still gets flagged within hours rather than at the next audit.
Best practices
- One CMK per data domain, not per bucket. A key per sensitivity tier or application keeps key policies meaningful without exploding into hundreds of keys you cannot reason about.
- Always enable a Bucket Key. It is the single best lever for keeping KMS costs predictable on high-traffic buckets.
- Turn on automatic key rotation. Annual rotation is free for symmetric CMKs and removes a manual chore from your compliance evidence.
- Use the
kms:ViaServicecondition. Constrain key usage to S3 so leaked credentials cannot decrypt data through a direct KMS call. - Review CloudTrail KMS events. Decrypt calls are logged, so unusual decrypt volume from an unexpected principal is an early signal of a credential compromise.
- Re-encrypt legacy objects deliberately. Run an S3 Batch Operations copy job to bring pre-existing objects under the new CMK when the data warrants it.
Encryption at rest is not a checkbox, it is an access control boundary. SSE-S3 satisfies the checkbox. A customer-managed key satisfies the boundary.
Moving from AES256 to a CMK is a low-effort change with a high-value payoff: an independent decryption gate, real key rotation, and the audit trail your compliance team keeps asking for. Make the switch on sensitive buckets first, then enforce it everywhere so the question never comes up again.

