This check flags CloudTrail trails that are not encrypted with a customer-managed KMS key (CMK). Without a CMK, you lose granular control over who can read your audit logs. Fix it by creating a KMS key with a tight policy and setting --kms-key-id on the trail.
CloudTrail is the system of record for what happens in your AWS account. Every API call, every console login, every IAM change ends up in a trail. That makes the log files themselves a high-value target. If an attacker can read or tamper with them, they can understand your environment and cover their tracks.
By default, CloudTrail encrypts log files at rest using SSE-S3, which is server-side encryption with keys managed entirely by AWS. That protects the bytes on disk, but it gives you no control over the key. You cannot restrict who decrypts the logs, you cannot audit key usage, and you cannot revoke access on demand. This check looks for trails that have not been upgraded to use a customer-managed KMS key (CMK).
What this check detects
The account_cloudtrailcmk check inspects every CloudTrail trail in your account and verifies that the KmsKeyId attribute is set to a customer-managed KMS key. A trail fails the check when:
- It uses the default SSE-S3 encryption (no
KmsKeyIdat all), or - The
KmsKeyIdfield is empty when you describe the trail.
You can see the current state with a single CLI call:
aws cloudtrail describe-trails \
--query 'trailList[].{Name:Name,KmsKeyId:KmsKeyId,Bucket:S3BucketName}' \
--output table
Any trail where KmsKeyId shows as None is the one this check is calling out.
Note: SSE-S3 and SSE-KMS both encrypt your logs at rest. The difference is control. With a CMK you own the key policy, you get CloudTrail entries for every decrypt operation, and you can disable the key to instantly cut off access to historical logs.
Why it matters
Audit logs are only useful if you can trust them. Encrypting CloudTrail with a CMK adds a second access control layer on top of S3 bucket policies. Even if someone gains read access to the log bucket, they still need kms:Decrypt permission on your key to make sense of the contents.
Attack scenario: the silent reader
An attacker compromises an IAM role with broad s3:GetObject permissions. With SSE-S3, the objects decrypt transparently because the bucket and the key are both owned by AWS on your behalf. The attacker pulls down months of CloudTrail logs, maps out your IAM users, service roles, and network topology, and plans a quiet privilege escalation, all without tripping a single alarm.
With a CMK, the same attacker hits a wall. Reading the objects requires kms:Decrypt on a key whose policy you control. If that role was never granted decrypt access, the logs are unreadable. And every successful or denied decrypt call is itself logged, so the attempt becomes evidence.
Compliance pressure
Several frameworks expect customer-managed encryption for audit data. CIS AWS Foundations Benchmark control 3.7 specifically requires CloudTrail logs to be encrypted with KMS CMKs. PCI DSS, HIPAA, and SOC 2 auditors routinely ask how you control access to encryption keys for sensitive log data. A CMK gives you a clean answer.
Warning: KMS is not free. Each CMK costs around 1 USD per month, plus a per-request charge for encrypt and decrypt operations. For a busy account with multi-region trails and many readers, the request volume can add up. It is almost always worth it for audit logs, but budget for it.
How to fix it
The fix has two parts: create a KMS key with a policy that lets CloudTrail use it, then point the trail at that key.
Step 1: Create a key policy for CloudTrail
CloudTrail needs permission to generate data keys and encrypt log files. Save this policy as cloudtrail-kms-policy.json and replace the account ID and region placeholders.
{
"Version": "2012-10-17",
"Id": "cloudtrail-key-policy",
"Statement": [
{
"Sid": "EnableRootAccountAdmin",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::111122223333:root" },
"Action": "kms:*",
"Resource": "*"
},
{
"Sid": "AllowCloudTrailEncrypt",
"Effect": "Allow",
"Principal": { "Service": "cloudtrail.amazonaws.com" },
"Action": "kms:GenerateDataKey*",
"Resource": "*",
"Condition": {
"StringLike": {
"kms:EncryptionContext:aws:cloudtrail:arn": "arn:aws:cloudtrail:*:111122223333:trail/*"
}
}
},
{
"Sid": "AllowCloudTrailDescribeKey",
"Effect": "Allow",
"Principal": { "Service": "cloudtrail.amazonaws.com" },
"Action": "kms:DescribeKey",
"Resource": "*"
},
{
"Sid": "AllowLogReadersDecrypt",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::111122223333:role/SecurityAuditRole" },
"Action": "kms:Decrypt",
"Resource": "*",
"Condition": {
"StringEquals": {
"kms:ViaService": "s3.us-east-1.amazonaws.com"
}
}
}
]
}
Step 2: Create the key
KEY_ID=$(aws kms create-key \
--description "CloudTrail log encryption" \
--policy file://cloudtrail-kms-policy.json \
--query 'KeyMetadata.KeyId' \
--output text)
aws kms create-alias \
--alias-name alias/cloudtrail-logs \
--target-key-id "$KEY_ID"
Step 3: Attach the key to your trail
Danger: Updating a trail is a live change to a production audit pipeline. If the key policy is wrong, CloudTrail can fail to deliver logs silently. Validate the policy in a non-production account first, and confirm log delivery resumes after the change.
aws cloudtrail update-trail \
--name my-org-trail \
--kms-key-id alias/cloudtrail-logs
Confirm the change took effect:
aws cloudtrail get-trail-status --name my-org-trail
aws cloudtrail describe-trails \
--query 'trailList[?Name==`my-org-trail`].KmsKeyId'
Note: Existing log files keep their original encryption. Only new objects written after the update use the CMK. There is no need to re-encrypt history, but be aware that older logs remain under SSE-S3.
Terraform example
If you manage infrastructure as code, define the key and trail together so encryption is never an afterthought.
resource "aws_kms_key" "cloudtrail" {
description = "CloudTrail log encryption"
enable_key_rotation = true
deletion_window_in_days = 30
policy = data.aws_iam_policy_document.cloudtrail_key.json
}
resource "aws_kms_alias" "cloudtrail" {
name = "alias/cloudtrail-logs"
target_key_id = aws_kms_key.cloudtrail.key_id
}
resource "aws_cloudtrail" "main" {
name = "my-org-trail"
s3_bucket_name = aws_s3_bucket.cloudtrail.id
kms_key_id = aws_kms_key.cloudtrail.arn
is_multi_region_trail = true
enable_log_file_validation = true
include_global_service_events = true
}
How to prevent it from happening again
Fixing one trail by hand is fine. Stopping the misconfiguration from coming back across dozens of accounts needs automation.
Block it in CI with policy-as-code
If you use Terraform, add a Checkov or OPA rule that rejects any aws_cloudtrail resource without a kms_key_id. Checkov ships with this rule built in (CKV_AWS_35), so enabling it is a one-liner in your pipeline:
checkov -d . --check CKV_AWS_35
For a custom OPA gate, fail the plan when the attribute is missing:
package terraform.cloudtrail
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_cloudtrail"
not resource.change.after.kms_key_id
msg := sprintf("CloudTrail %s must be encrypted with a KMS CMK", [resource.name])
}
Enforce org-wide with SCPs and Config
Use an AWS Config managed rule, cloudtrail-encryption-enabled, to continuously flag non-compliant trails. Pair it with an automatic remediation that runs the update-trail call, or at minimum sends an alert to your security channel.
Tip: Deploy CloudTrail centrally from your organization management account as an organization trail with a CMK already attached. Member accounts inherit the encrypted trail and cannot accidentally create a weaker one, which removes the problem at the source instead of catching it after the fact.
Best practices
- Enable key rotation. Turn on automatic annual rotation so the key material refreshes without any changes to your trail configuration.
- Keep the key policy tight. Grant
kms:Decryptonly to the specific roles that genuinely need to read logs, such as your SIEM ingestion role and security audit role. Avoid wildcards on the principal. - Pair encryption with log file validation. Set
enable_log_file_validation = trueso you can detect tampering, not just block reading. - Use a dedicated key for CloudTrail. Do not share the audit log key with application data. Separation keeps the key policy small and the blast radius contained.
- Monitor the key itself. Alert on
DisableKey,ScheduleKeyDeletion, and a spike in deniedDecryptcalls. An attacker who cannot read logs may try to disable the key instead. - Apply the same standard everywhere. Multi-region trails, organization trails, and per-account trails should all use a CMK. A single unencrypted trail undermines the whole posture.
Encrypting CloudTrail with a customer-managed key is a small change with outsized value. It turns your audit log from a passive record into a controlled, monitored, revocable asset. Run the describe-trails command above, find the trails without a KmsKeyId, and close the gap before someone else finds it for you.

