CloudWatch Log Groups Should Be Encrypted With CMK
More Info:
Cloudwatch loggroups should be encrypted
Risk Level
High
Address
Security
Compliance Standards
HIPAA,PCIDSS,GDPR,CISAWS,CBP,NIST,SOC2,AWSWAF,SEBI,RBI_UCB
Triage and Remediation
- Remediation
Remediation
Using Console
Below are the exact console steps to ensure a CloudWatch Log Group is encrypted with a customer-managed KMS key (CMK).
Prerequisites: Create or identify a CMK
- Sign in to the AWS Management Console.
- Go to Key Management Service (KMS):
- In the search bar, type KMS, choose Key Management Service.
- Create a new CMK (if you don’t already have one for logs):
- In the left pane, choose Customer managed keys.
- Click Create key.
- Key type: Symmetric.
- Key usage: Encrypt and decrypt.
- Click Next and:
- Set an alias (e.g.,
alias/cloudwatch-logs-key). - Choose key administrators and key users (IAM roles/users that need to write/read logs).
- Set an alias (e.g.,
- Complete the steps and click Finish.
Note: Ensure the IAM roles/services that write to CloudWatch Logs (e.g., Lambda, ECS, EC2, etc.) are added as Key users so they can use the CMK.
Step-by-step: Encrypt an existing CloudWatch Log Group with CMK
- In the AWS console, go to CloudWatch.
- In the left navigation pane, select Log groups.
- Find and click the log group you want to encrypt.
- At the top right, choose Actions → Edit (or Edit encryption depending on UI).
- Under Encryption:
- Check/enable Encrypt log group (if shown).
- For KMS key, choose:
- Select KMS key and pick your customer-managed key (e.g.,
alias/cloudwatch-logs-key), notaws/logs(the AWS-managed key).
- Select KMS key and pick your customer-managed key (e.g.,
- Click Save changes.
CloudWatch Logs will now store new log data in that log group encrypted using your CMK.
Step-by-step: Set CMK encryption by default for new log groups (optional)
There is no global “default CMK for all log groups” setting in the console, but you can:
-
Create log groups manually (instead of auto-created), and during creation:
- In CloudWatch → Log groups → Create log group.
- Enter Log group name.
- Under Encryption, choose your CMK.
- Click Create.
-
Or enforce via automation (CloudFormation, Terraform, or a Lambda that:
- Monitors for new log groups.
- Calls
AssociateKmsKeyto attach your CMK to them.)
Validate encryption
- In CloudWatch → Log groups, click the log group.
- Check the Encryption section:
- It should show KMS with your CMK alias/ARN.
- Optionally, in KMS → Customer managed keys, select your key and:
- Check Key usage and CloudTrail logs to confirm encryption operations.
Using CLI
Below are AWS CLI steps to ensure CloudWatch Log Groups are encrypted with a customer-managed KMS CMK.
1. (Optional) Create a KMS CMK for CloudWatch Logs
If you don’t already have a CMK you want to use:
aws kms create-key \
--description "CMK for CloudWatch Log Group encryption" \
--key-usage ENCRYPT_DECRYPT \
--origin AWS_KMS
Note the KeyId from the output. You can also create an alias:
aws kms create-alias \
--alias-name alias/cloudwatch-logs-cmk \
--target-key-id <KEY_ID_FROM_PREVIOUS_COMMAND>
You can then use either the KeyId or the alias ARN as the --kms-key-id.
2. Identify Log Groups Without CMK Encryption
List all log groups:
aws logs describe-log-groups --output json
Filter those without a kmsKeyId using jq (recommended):
aws logs describe-log-groups --output json \
| jq -r '.logGroups[] | select(.kmsKeyId == null) | .logGroupName'
This will output the names of log groups that are not using CMK encryption.
3. Associate a CMK with a Single Log Group
Use associate-kms-key to enable CMK encryption:
aws logs associate-kms-key \
--log-group-name "<LOG_GROUP_NAME>" \
--kms-key-id "arn:aws:kms:<REGION>:<ACCOUNT_ID>:alias/cloudwatch-logs-cmk"
Or directly with the KeyId/KeyArn:
aws logs associate-kms-key \
--log-group-name "<LOG_GROUP_NAME>" \
--kms-key-id "<KEY_ID_OR_KEY_ARN>"
4. Apply CMK Encryption to All Unencrypted Log Groups (Batch)
Example Bash loop for all unencrypted log groups:
KMS_KEY_ARN="arn:aws:kms:<REGION>:<ACCOUNT_ID>:alias/cloudwatch-logs-cmk"
for lg in $(aws logs describe-log-groups --output json \
| jq -r '.logGroups[] | select(.kmsKeyId == null) | .logGroupName'); do
echo "Associating KMS key with log group: $lg"
aws logs associate-kms-key \
--log-group-name "$lg" \
--kms-key-id "$KMS_KEY_ARN"
done
5. (Important) KMS Key Policy Permissions
Ensure the CMK key policy allows CloudWatch Logs and any writers/readers to use it. Minimal example snippet in the KMS key policy:
{
"Sid": "Allow CloudWatch Logs to use the key",
"Effect": "Allow",
"Principal": {
"Service": "logs.<REGION>.amazonaws.com"
},
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
"kms:DescribeKey"
],
"Resource": "*"
}
Update policy via:
aws kms put-key-policy \
--key-id "<KEY_ID_OR_ARN>" \
--policy-name "default" \
--policy file://kms-policy.json
6. Verify Encryption
Check a specific log group:
aws logs describe-log-groups \
--log-group-name-prefix "<LOG_GROUP_NAME>" \
--output json \
| jq '.logGroups[] | {logGroupName, kmsKeyId}'
You should see kmsKeyId populated with the CMK ARN.
Using Python
Below is a concise, step‑by‑step way to remediate “CloudWatch Log Groups should be encrypted with CMK” using Python (boto3).
1. Prerequisites
- Python 3.x
boto3installed:pip install boto3- AWS credentials configured (via
~/.aws/credentials, environment variables, or instance profile). - Permissions:
logs:DescribeLogGroups,logs:AssociateKmsKeykms:CreateKey,kms:DescribeKey,kms:ListAliases(if creating/using CMK)
2. Option A – Use an Existing KMS CMK
If you already have a KMS CMK (recommended), you just need its ARN or alias.
2.1. Find CMK by alias (optional helper)
import boto3
def get_kms_key_arn_by_alias(alias_name: str) -> str:
"""
Returns the KMS key ARN for a given alias like 'alias/cloudwatch-logs'.
"""
kms = boto3.client("kms")
paginator = kms.get_paginator("list_aliases")
for page in paginator.paginate():
for alias in page.get("Aliases", []):
if alias.get("AliasName") == alias_name and "TargetKeyId" in alias:
key_id = alias["TargetKeyId"]
key = kms.describe_key(KeyId=key_id)
return key["KeyMetadata"]["Arn"]
raise ValueError(f"Alias {alias_name} not found or has no target key.")
3. Option B – Create a New CMK for CloudWatch Logs
import boto3
import json
def create_cloudwatch_logs_cmk(alias_name: str = "alias/cloudwatch-logs") -> str:
kms = boto3.client("kms")
# Optional: key policy allowing CloudWatch Logs to use this CMK
# Adjust principals and conditions for your org requirements.
account_id = boto3.client("sts").get_caller_identity()["Account"]
region = boto3.session.Session().region_name
key_policy = {
"Version": "2012-10-17",
"Statement": [
# Allow account root full access to the CMK
{
"Sid": "EnableRootPermissions",
"Effect": "Allow",
"Principal": {"AWS": f"arn:aws:iam::{account_id}:root"},
"Action": "kms:*",
"Resource": "*"
},
# Allow CloudWatch Logs to use the CMK for encryption/decryption
{
"Sid": "AllowCloudWatchLogsUseOfTheKey",
"Effect": "Allow",
"Principal": {"Service": "logs.amazonaws.com"},
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
"kms:DescribeKey"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"kms:EncryptionContext:aws:logs:arn": f"arn:aws:logs:{region}:{account_id}:*"
}
}
}
]
}
response = kms.create_key(
Policy=json.dumps(key_policy),
Description="CMK for encrypting CloudWatch Log Groups",
KeyUsage="ENCRYPT_DECRYPT",
Origin="AWS_KMS"
)
key_id = response["KeyMetadata"]["KeyId"]
# Create alias for easier future reference
kms.create_alias(AliasName=alias_name, TargetKeyId=key_id)
return response["KeyMetadata"]["Arn"]
Call this once to create the key:
cmk_arn = create_cloudwatch_logs_cmk("alias/cloudwatch-logs")
print("Created CMK:", cmk_arn)
4. Associate CMK with All (or Selected) Log Groups
This script:
- Lists all CloudWatch log groups.
- Identifies those without a
kmsKeyId(i.e., not using CMK). - Associates them with the specified CMK.
import boto3
def associate_cmk_with_log_groups(kms_key_arn: str, prefix_filter: str = None):
logs = boto3.client("logs")
paginator = logs.get_paginator("describe_log_groups")
for page in paginator.paginate():
for lg in page.get("logGroups", []):
log_group_name = lg["logGroupName"]
kms_key_id = lg.get("kmsKeyId")
# Optional: only act on log groups matching a prefix
if prefix_filter and not log_group_name.startswith(prefix_filter):
continue
# Skip if already encrypted with *some* CMK
if kms_key_id:
print(f"Skipping {log_group_name}: already encrypted with {kms_key_id}")
continue
print(f"Associating CMK with log group: {log_group_name}")
logs.associate_kms_key(
logGroupName=log_group_name,
kmsKeyId=kms_key_arn
)
if __name__ == "__main__":
# Option 1: use existing CMK ARN directly
# kms_key_arn = "arn:aws:kms:us-east-1:123456789012:key/xxxx-xxxx-xxxx-xxxx"
# Option 2: get by alias
kms_key_arn = get_kms_key_arn_by_alias("alias/cloudwatch-logs")
# Optionally, limit to specific app prefix: prefix_filter="/aws/lambda/"
associate_cmk_with_log_groups(kms_key_arn)
5. Verify Encryption
Programmatically:
import boto3
def verify_encryption():
logs = boto3.client("logs")
paginator = logs.get_paginator("describe_log_groups")
for page in paginator.paginate():
for lg in page.get("logGroups", []):
print(lg["logGroupName"], "->", lg.get("kmsKeyId", "NOT ENCRYPTED"))
verify_encryption()
Summary of flow:
- Create or choose a KMS CMK (steps 2–3).
- Run the association script (step 4).
- Verify all log groups show
kmsKeyIdset (step 5).
Using Terraform
resource "aws_kms_key" "cloudwatch_logs" {
description = "KMS CMK for CloudWatch Log Group encryption"
deletion_window_in_days = 30
# Optional: restrict key usage to CloudWatch Logs
policy = data.aws_iam_policy_document.cloudwatch_kms.json
}
data "aws_iam_policy_document" "cloudwatch_kms" {
statement {
sid = "AllowCloudWatchLogsUseOfTheKey"
effect = "Allow"
principals {
type = "Service"
identifiers = ["logs.${var.AWS_REGION}.amazonaws.com"]
}
actions = [
"kms:Encrypt",
"kms:Decrypt",
"kms:GenerateDataKey*",
"kms:DescribeKey",
]
resources = ["*"]
}
}
resource "aws_cloudwatch_log_group" "this" {
name = "/aws/my/app" # replace with your log group name
retention_in_days = 30 # optional
kms_key_id = aws_kms_key.cloudwatch_logs.arn
}
- Replace
var.AWS_REGIONwith your region variable or hard-code the region string as needed. - If the log group already exists and is imported into Terraform, adding
kms_key_idis an in-place update and does not force replacement.
Verification: terraform plan should show an in-place update on aws_cloudwatch_log_group.this with kms_key_id changing from null to the CMK ARN, and creation of aws_kms_key.cloudwatch_logs.