Skip to main content

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

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

  1. Sign in to the AWS Management Console.
  2. Go to Key Management Service (KMS):
    • In the search bar, type KMS, choose Key Management Service.
  3. 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).
    • 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

  1. In the AWS console, go to CloudWatch.
  2. In the left navigation pane, select Log groups.
  3. Find and click the log group you want to encrypt.
  4. At the top right, choose ActionsEdit (or Edit encryption depending on UI).
  5. 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), not aws/logs (the AWS-managed key).
  6. 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:

    1. In CloudWatch → Log groups → Create log group.
    2. Enter Log group name.
    3. Under Encryption, choose your CMK.
    4. Click Create.
  • Or enforce via automation (CloudFormation, Terraform, or a Lambda that:

    • Monitors for new log groups.
    • Calls AssociateKmsKey to attach your CMK to them.)

Validate encryption

  1. In CloudWatch → Log groups, click the log group.
  2. Check the Encryption section:
    • It should show KMS with your CMK alias/ARN.
  3. 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
  • boto3 installed:
    pip install boto3
  • AWS credentials configured (via ~/.aws/credentials, environment variables, or instance profile).
  • Permissions:
    • logs:DescribeLogGroups, logs:AssociateKmsKey
    • kms: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:

  1. Create or choose a KMS CMK (steps 2–3).
  2. Run the association script (step 4).
  3. Verify all log groups show kmsKeyId set (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_REGION with 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_id is 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.