This check flags AWS accounts that have no CloudWatch alarm watching for KMS customer-managed key disables or scheduled deletions. Without it, an attacker or a fat-fingered engineer can quietly kneecap your encryption and you won't know until data is unreadable. Fix it by creating a metric filter on your CloudTrail log group plus an alarm wired to SNS.
Encryption keys are the hinge that everything else swings on. If a KMS customer-managed key (CMK) gets disabled or scheduled for deletion, every resource that depends on it, EBS volumes, RDS snapshots, S3 objects, Secrets Manager secrets, starts failing or becomes permanently unrecoverable. CIS AWS Foundations Benchmark control 3.7 exists precisely because this kind of change is high-impact and easy to miss.
This Lensix check (account_alarm_cmkdisabled) verifies that your account has a CloudWatch alarm configured to detect DisableKey and ScheduleKeyDeletion events on KMS keys. If no such alarm exists, the check fails.
What this check detects
The check looks for a working detection pipeline made of three pieces:
- A CloudTrail trail delivering management events to a CloudWatch Logs group.
- A metric filter on that log group matching KMS key disable and deletion API calls.
- A CloudWatch alarm on that metric filter that pushes notifications to an SNS topic with at least one active subscriber.
If any link in that chain is missing, the alarm cannot fire, and the check reports a failure. The specific API events of interest are:
kms:DisableKey— turns off a key so it can no longer be used for crypto operations.kms:ScheduleKeyDeletion— queues a key for permanent deletion after a waiting period (7 to 30 days).
Note: A scheduled deletion is not instant. AWS enforces a minimum 7-day waiting window before a CMK is actually destroyed. That window is your grace period to cancel, but only if someone is watching for the event in the first place.
Why it matters
Disabling or deleting a CMK is one of the highest-blast-radius actions in an AWS account. Consider what breaks:
- Data at rest becomes unreadable. Any S3 object, EBS volume, or RDS instance encrypted with the key can no longer be decrypted. If the key is deleted, that data is gone for good.
- Running workloads fail. Lambda functions pulling secrets, EC2 instances mounting encrypted volumes, and applications reading from KMS-backed stores start throwing access-denied errors.
- Recovery is time-boxed. Once a key enters the deletion schedule, you have a finite window to call
CancelKeyDeletion. Miss it and there is no AWS support ticket that brings the data back.
From an attacker's perspective, disabling a key is a clean way to cause damage without exfiltrating anything. It is also a common move in ransomware-style extortion against cloud environments: lock the key, demand payment to re-enable it. A disgruntled insider with KMS permissions can do the same in seconds.
Even without malice, accidents happen. An engineer cleaning up "unused" keys during a cost review, an over-broad Terraform destroy, or a botched key rotation can all trigger a deletion schedule. The difference between a near-miss and an outage is whether someone gets paged in time.
Warning: AWS-managed keys (the ones with aws/ prefixes like aws/s3) cannot be disabled or deleted by you, so this check focuses on customer-managed keys. Those are the ones under your control, and therefore the ones at risk.
How to fix it
You need a CloudTrail trail that ships management events to CloudWatch Logs, then a metric filter and alarm on top. The steps below assume you already have a multi-region trail logging to a CloudWatch Logs group. If not, set that up first.
Step 1: Create the metric filter
This filter matches both disable and scheduled-deletion events. Replace your-cloudtrail-log-group with your actual log group name.
aws logs put-metric-filter \
--log-group-name "your-cloudtrail-log-group" \
--filter-name "CMKDisabledOrScheduledDeletion" \
--filter-pattern '{ ($.eventSource = "kms.amazonaws.com") && (($.eventName = "DisableKey") || ($.eventName = "ScheduleKeyDeletion")) }' \
--metric-transformations \
metricName=CMKDisabledOrScheduledDeletionCount,metricNamespace=CISBenchmark,metricValue=1,defaultValue=0
Step 2: Create an SNS topic and subscribe to it
aws sns create-topic --name cis-security-alerts
aws sns subscribe \
--topic-arn arn:aws:sns:us-east-1:111122223333:cis-security-alerts \
--protocol email \
--notification-endpoint [email protected]
Confirm the subscription from the email AWS sends, otherwise the alarm has nowhere to deliver.
Step 3: Create the alarm
aws cloudwatch put-metric-alarm \
--alarm-name "CIS-3.7-CMKDisabledOrScheduledDeletion" \
--alarm-description "Alert on KMS CMK disable or scheduled deletion (CIS 3.7)" \
--metric-name CMKDisabledOrScheduledDeletionCount \
--namespace CISBenchmark \
--statistic Sum \
--period 300 \
--threshold 1 \
--comparison-operator GreaterThanOrEqualToThreshold \
--evaluation-periods 1 \
--treat-missing-data notBreaching \
--alarm-actions arn:aws:sns:us-east-1:111122223333:cis-security-alerts
Tip: Set --treat-missing-data notBreaching. KMS disable events are rare, so the metric will report no data most of the time. Without this setting, some alarm configurations sit in an INSUFFICIENT_DATA state that can be mistaken for a problem.
What to do when the alarm fires
If the event was a ScheduleKeyDeletion and it was unintended, cancel it immediately:
Danger: Do not delete the key. The command below cancels a pending deletion and re-enables the key. If you instead let the schedule run out, the key and all data encrypted with it are permanently destroyed with no recovery path.
aws kms cancel-key-deletion --key-id 1234abcd-12ab-34cd-56ef-1234567890ab
aws kms enable-key --key-id 1234abcd-12ab-34cd-56ef-1234567890ab
Doing it with Infrastructure as Code
Clicking through the console once is fine, but the alarm should live in version control so it survives account rebuilds and applies to every region you care about. Here is the Terraform equivalent:
resource "aws_sns_topic" "cis_alerts" {
name = "cis-security-alerts"
}
resource "aws_sns_topic_subscription" "cis_alerts_email" {
topic_arn = aws_sns_topic.cis_alerts.arn
protocol = "email"
endpoint = "[email protected]"
}
resource "aws_cloudwatch_log_metric_filter" "cmk_disabled" {
name = "CMKDisabledOrScheduledDeletion"
log_group_name = var.cloudtrail_log_group_name
pattern = "{ ($.eventSource = \"kms.amazonaws.com\") && (($.eventName = \"DisableKey\") || ($.eventName = \"ScheduleKeyDeletion\")) }"
metric_transformation {
name = "CMKDisabledOrScheduledDeletionCount"
namespace = "CISBenchmark"
value = "1"
default_value = "0"
}
}
resource "aws_cloudwatch_metric_alarm" "cmk_disabled" {
alarm_name = "CIS-3.7-CMKDisabledOrScheduledDeletion"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = 1
metric_name = aws_cloudwatch_log_metric_filter.cmk_disabled.metric_transformation[0].name
namespace = "CISBenchmark"
period = 300
statistic = "Sum"
threshold = 1
treat_missing_data = "notBreaching"
alarm_description = "Alert on KMS CMK disable or scheduled deletion (CIS 3.7)"
alarm_actions = [aws_sns_topic.cis_alerts.arn]
}
Note: CloudWatch metric filters are regional. If you have a multi-region CloudTrail feeding a single log group, one filter covers all regions. If each region has its own trail and log group, you need this stack deployed per region.
How to prevent it from happening again
An alarm tells you after the fact. Combine it with controls that reduce the chance of the event happening at all, and with automation that keeps the alarm itself from drifting away.
Lock down who can disable or delete keys
Use a KMS key policy or an SCP to restrict the dangerous actions to a tiny set of break-glass roles. An organization-level SCP that denies these actions to everyone except a designated admin role is a strong backstop:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyKMSDestructiveActions",
"Effect": "Deny",
"Action": [
"kms:DisableKey",
"kms:ScheduleKeyDeletion"
],
"Resource": "*",
"Condition": {
"ArnNotLike": {
"aws:PrincipalArn": "arn:aws:iam::*:role/KmsBreakGlassAdmin"
}
}
}
]
}
Gate the alarm config in CI/CD
If the alarm lives in Terraform, add a policy-as-code check so nobody can merge a change that removes it. With Open Policy Agent and conftest, you can assert the resource exists in the plan:
terraform plan -out=plan.tfplan
terraform show -json plan.tfplan > plan.json
conftest test plan.json --policy policy/
A simple Rego rule that fails if the alarm is being destroyed:
package main
deny[msg] {
rc := input.resource_changes[_]
rc.type == "aws_cloudwatch_metric_alarm"
rc.change.actions[_] == "delete"
rc.change.before.alarm_name == "CIS-3.7-CMKDisabledOrScheduledDeletion"
msg := "CIS 3.7 CMK alarm must not be deleted"
}
Continuously verify with Lensix
Manual audits miss things, especially across dozens of accounts. Lensix runs account_alarm_cmkdisabled on a schedule, so if someone removes the alarm, deletes the trail, or kills the SNS subscription, you find out on the next scan instead of during an incident.
Tip: Pair this alarm with the related CIS monitoring controls (root account usage, IAM policy changes, CloudTrail config changes) so you are not building one-off filters. They all share the same trail, log group, and SNS topic, which keeps the setup tidy.
Best practices
- Route alerts somewhere a human sees fast. Email is fine for low-volume security alarms, but for something this critical, also wire SNS to PagerDuty, Opsgenie, or a monitored Slack channel.
- Enable key rotation on CMKs so the keys are healthy and actively managed, which reduces the temptation to "clean up" keys that look stale.
- Treat KMS deletions as a break-glass operation. Require a ticket, a second approver, and a documented reason before anyone runs
ScheduleKeyDeletion. - Test the alarm. In a non-production account, disable a throwaway key and confirm the notification actually lands. An untested alarm is a false sense of security.
- Standardize across accounts. Deploy the metric filter and alarm through your landing zone or AWS Organizations baseline so new accounts inherit it automatically.
The cost of this control is essentially zero: one metric filter, one alarm, one SNS topic. The cost of not having it is measured in lost data and outage hours. Set it up once, enforce it in code, and let Lensix confirm it stays in place.

