The 14 CIS CloudWatch Alarms Every AWS Account Needs
complete metric filter patterns, CDK stack, and alert fatigue strategies for satisfying CIS Benchmark CloudWatch controls in a production account.
- DATE:
- APR.02.2026
- READ:
- 20 MIN
The CIS AWS Foundations Benchmark specifies 14 CloudWatch metric filters and alarms that monitor security-relevant API activity in your account. These alarms watch CloudTrail logs for events like root account usage, IAM policy changes, unauthorized API calls, and network configuration modifications. When a matching event occurs, the metric filter increments a custom CloudWatch metric, the metric breaches its threshold, and an SNS notification fires.
For a SOC 2 audit, Security Hub flags each missing alarm as a separate CloudWatch.* finding — all 14 map to the CC7.1 (monitoring) and CC7.2 (incident detection) trust service criteria.
This post covers all 14 alarms with exact filter patterns, a CDK stack that creates them reproducibly, and the operational considerations for running them in production without drowning in noise.
Prerequisites
Before creating the metric filters, you need a CloudTrail trail delivering logs to a CloudWatch Logs log group, and an SNS topic for notifications.
# Create the SNS topic
aws sns create-topic --name security-alarms --region us-east-1
# Subscribe the security team email
aws sns subscribe
--topic-arn arn:aws:sns:us-east-1:ACCOUNT_ID:security-alarms
--protocol email
--notification-endpoint security-team@yourcompany.com
# Verify CloudTrail log group exists
aws logs describe-log-groups
--log-group-name-prefix "aws-cloudtrail-logs"
--region us-east-1All filters below share the same pattern: metric filter on the CloudTrail log group → custom metric in CISBenchmark namespace → alarm triggered when metric ≥ threshold.
The 14 alarms
1. Root account usage (CloudWatch.1)
Detects any API call made with root credentials. Root has unrestricted access to every resource — any activity outside of emergency break-glass scenarios is suspicious.
FILTER_PATTERN='{ $.userIdentity.type = "Root" && $.userIdentity.invokedBy NOT EXISTS && $.eventType != "AwsServiceEvent" }'
aws logs put-metric-filter
--log-group-name "aws-cloudtrail-logs-ACCOUNT_ID"
--filter-name "RootAccountUsage"
--filter-pattern "$FILTER_PATTERN"
--metric-transformations
metricName=RootAccountUsageCount,metricNamespace=CISBenchmark,metricValue=1
aws cloudwatch put-metric-alarm
--alarm-name "CIS-RootAccountUsage"
--metric-name RootAccountUsageCount
--namespace CISBenchmark
--statistic Sum --period 300 --evaluation-periods 1
--threshold 1 --comparison-operator GreaterThanOrEqualToThreshold
--alarm-actions arn:aws:sns:us-east-1:ACCOUNT_ID:security-alarms
--treat-missing-data notBreachingThe filter excludes AwsServiceEvent types because some AWS services make calls on behalf of the root account internally — these are not actual root usage.
2. Unauthorized API calls (CloudWatch.2)
Detects AccessDenied and UnauthorizedAccess errors. A spike may indicate stolen credentials being probed.
FILTER_PATTERN='{ ($.errorCode = "*UnauthorizedAccess*") || ($.errorCode = "AccessDenied*") }'3. Console sign-in without MFA (CloudWatch.3)
Detects successful console sign-ins that didn’t use MFA. A compromised password alone grants full console access.
FILTER_PATTERN='{ ($.eventName = "ConsoleLogin") && ($.additionalEventData.MFAUsed != "Yes") && ($.userIdentity.type = "IAMUser") && ($.responseElements.ConsoleLogin = "Success") }'Scoped to IAMUser type to exclude federated users and SSO sign-ins, which have different MFA mechanisms.
4. IAM policy changes (CloudWatch.4)
Detects creation, modification, or deletion of IAM policies and role/group/user policy attachments. Unauthorized policy changes can escalate privileges.
FILTER_PATTERN='{ ($.eventName = CreatePolicy) || ($.eventName = DeletePolicy) || ($.eventName = CreatePolicyVersion) || ($.eventName = DeletePolicyVersion) || ($.eventName = AttachRolePolicy) || ($.eventName = DetachRolePolicy) || ($.eventName = AttachGroupPolicy) || ($.eventName = DetachGroupPolicy) || ($.eventName = AttachUserPolicy) || ($.eventName = DetachUserPolicy) || ($.eventName = PutUserPolicy) || ($.eventName = PutGroupPolicy) || ($.eventName = PutRolePolicy) || ($.eventName = DeleteUserPolicy) || ($.eventName = DeleteGroupPolicy) || ($.eventName = DeleteRolePolicy) }'5. CloudTrail configuration changes (CloudWatch.5)
Detects changes to CloudTrail itself — stopping logging, deleting trails. The first thing an attacker does after gaining access is disable logging.
FILTER_PATTERN='{ ($.eventName = CreateTrail) || ($.eventName = UpdateTrail) || ($.eventName = DeleteTrail) || ($.eventName = StartLogging) || ($.eventName = StopLogging) }'6. Console authentication failures (CloudWatch.6)
Detects failed sign-in attempts. A burst indicates brute force against IAM user passwords.
FILTER_PATTERN='{ ($.eventName = ConsoleLogin) && ($.errorMessage = "Failed authentication") }'Threshold of 5 accounts for legitimate typos.
7. CMK disable or scheduled deletion (CloudWatch.7)
Detects KMS Customer Master Keys being disabled or scheduled for deletion. Disabling a CMK makes all data encrypted with it permanently inaccessible — this is the ransomware attack vector.
FILTER_PATTERN='{ ($.eventSource = kms.amazonaws.com) && (($.eventName = DisableKey) || ($.eventName = ScheduleKeyDeletion)) }'8. S3 bucket policy changes (CloudWatch.8)
Detects changes to S3 bucket policies, ACLs, CORS, and lifecycle rules. An attacker modifying a bucket policy can make it public or redirect data to their account.
FILTER_PATTERN='{ ($.eventSource = s3.amazonaws.com) && (($.eventName = PutBucketAcl) || ($.eventName = PutBucketPolicy) || ($.eventName = PutBucketCors) || ($.eventName = PutBucketLifecycle) || ($.eventName = PutBucketReplication) || ($.eventName = DeleteBucketPolicy) || ($.eventName = DeleteBucketCors) || ($.eventName = DeleteBucketLifecycle) || ($.eventName = DeleteBucketReplication)) }'9. AWS Config changes (CloudWatch.9)
Detects stopping the Config recorder or modifying delivery channels. Config provides continuous compliance monitoring — disabling it removes drift detection.
FILTER_PATTERN='{ ($.eventSource = config.amazonaws.com) && (($.eventName = StopConfigurationRecorder) || ($.eventName = DeleteDeliveryChannel) || ($.eventName = PutDeliveryChannel) || ($.eventName = PutConfigurationRecorder)) }'10. Security group changes (CloudWatch.10)
Detects creation, deletion, or modification of VPC security groups and their rules.
FILTER_PATTERN='{ ($.eventName = AuthorizeSecurityGroupIngress) || ($.eventName = AuthorizeSecurityGroupEgress) || ($.eventName = RevokeSecurityGroupIngress) || ($.eventName = RevokeSecurityGroupEgress) || ($.eventName = CreateSecurityGroup) || ($.eventName = DeleteSecurityGroup) }'This is one of the noisiest alarms during CDK deployments.
11. NACL changes (CloudWatch.11)
Detects changes to Network ACL entries. NACLs are the stateless perimeter before security groups.
FILTER_PATTERN='{ ($.eventName = CreateNetworkAcl) || ($.eventName = CreateNetworkAclEntry) || ($.eventName = DeleteNetworkAcl) || ($.eventName = DeleteNetworkAclEntry) || ($.eventName = ReplaceNetworkAclEntry) || ($.eventName = ReplaceNetworkAclAssociation) }'12. Network gateway changes (CloudWatch.12)
Detects creation or deletion of Internet Gateways, NAT Gateways, and Virtual Private Gateways. A new IGW attached to a VPC creates a path from the internet to previously isolated resources.
FILTER_PATTERN='{ ($.eventName = CreateCustomerGateway) || ($.eventName = DeleteCustomerGateway) || ($.eventName = AttachInternetGateway) || ($.eventName = CreateInternetGateway) || ($.eventName = DeleteInternetGateway) || ($.eventName = DetachInternetGateway) }'13. Route table changes (CloudWatch.13)
Detects changes to VPC route tables. Route table modifications can redirect traffic through an attacker-controlled instance or expose internal services.
FILTER_PATTERN='{ ($.eventName = CreateRoute) || ($.eventName = CreateRouteTable) || ($.eventName = ReplaceRoute) || ($.eventName = ReplaceRouteTableAssociation) || ($.eventName = DeleteRouteTable) || ($.eventName = DeleteRoute) || ($.eventName = DisassociateRouteTable) }'14. VPC changes (CloudWatch.14)
Detects creation, deletion, or modification of VPCs and VPC peering connections.
FILTER_PATTERN='{ ($.eventName = CreateVpc) || ($.eventName = DeleteVpc) || ($.eventName = ModifyVpcAttribute) || ($.eventName = AcceptVpcPeeringConnection) || ($.eventName = CreateVpcPeeringConnection) || ($.eventName = DeleteVpcPeeringConnection) || ($.eventName = RejectVpcPeeringConnection) || ($.eventName = AttachClassicLinkVpc) || ($.eventName = DetachClassicLinkVpc) || ($.eventName = DisableVpcClassicLink) || ($.eventName = EnableVpcClassicLink) }'CDK stack implementation
Instead of running 28 CLI commands once and hoping they stay in place, define the alarms in CDK:
from aws_cdk import (
Stack, Duration,
aws_cloudwatch as cloudwatch,
aws_cloudwatch_actions as cw_actions,
aws_logs as logs,
aws_sns as sns,
aws_sns_subscriptions as subs,
)
from constructs import Construct
CIS_ALARMS = [
{
"name": "RootAccountUsage",
"pattern": '{ $.userIdentity.type = "Root" && $.userIdentity.invokedBy NOT EXISTS && $.eventType != "AwsServiceEvent" }',
"threshold": 1,
},
{
"name": "UnauthorizedAPICalls",
"pattern": '{ ($.errorCode = "*UnauthorizedAccess*") || ($.errorCode = "AccessDenied*") }',
"threshold": 5,
},
{
"name": "ConsoleSignInWithoutMFA",
"pattern": '{ ($.eventName = "ConsoleLogin") && ($.additionalEventData.MFAUsed != "Yes") && ($.userIdentity.type = "IAMUser") && ($.responseElements.ConsoleLogin = "Success") }',
"threshold": 1,
},
{
"name": "IAMPolicyChanges",
"pattern": '{ ($.eventName = CreatePolicy) || ($.eventName = DeletePolicy) || ($.eventName = AttachRolePolicy) || ($.eventName = DetachRolePolicy) || ($.eventName = PutRolePolicy) || ($.eventName = DeleteRolePolicy) }',
"threshold": 1,
},
{
"name": "CloudTrailConfigChanges",
"pattern": '{ ($.eventName = CreateTrail) || ($.eventName = UpdateTrail) || ($.eventName = DeleteTrail) || ($.eventName = StopLogging) }',
"threshold": 1,
},
{
"name": "ConsoleAuthFailures",
"pattern": '{ ($.eventName = ConsoleLogin) && ($.errorMessage = "Failed authentication") }',
"threshold": 5,
},
{
"name": "CMKDisableOrDeletion",
"pattern": '{ ($.eventSource = kms.amazonaws.com) && (($.eventName = DisableKey) || ($.eventName = ScheduleKeyDeletion)) }',
"threshold": 1,
},
{
"name": "S3BucketPolicyChanges",
"pattern": '{ ($.eventSource = s3.amazonaws.com) && (($.eventName = PutBucketAcl) || ($.eventName = PutBucketPolicy) || ($.eventName = DeleteBucketPolicy)) }',
"threshold": 1,
},
{
"name": "AWSConfigChanges",
"pattern": '{ ($.eventSource = config.amazonaws.com) && (($.eventName = StopConfigurationRecorder) || ($.eventName = DeleteDeliveryChannel) || ($.eventName = PutDeliveryChannel)) }',
"threshold": 1,
},
{
"name": "SecurityGroupChanges",
"pattern": '{ ($.eventName = AuthorizeSecurityGroupIngress) || ($.eventName = AuthorizeSecurityGroupEgress) || ($.eventName = RevokeSecurityGroupIngress) || ($.eventName = RevokeSecurityGroupEgress) || ($.eventName = CreateSecurityGroup) || ($.eventName = DeleteSecurityGroup) }',
"threshold": 1,
},
{
"name": "NACLChanges",
"pattern": '{ ($.eventName = CreateNetworkAcl) || ($.eventName = CreateNetworkAclEntry) || ($.eventName = DeleteNetworkAcl) || ($.eventName = DeleteNetworkAclEntry) || ($.eventName = ReplaceNetworkAclEntry) }',
"threshold": 1,
},
{
"name": "NetworkGatewayChanges",
"pattern": '{ ($.eventName = AttachInternetGateway) || ($.eventName = CreateInternetGateway) || ($.eventName = DeleteInternetGateway) || ($.eventName = DetachInternetGateway) }',
"threshold": 1,
},
{
"name": "RouteTableChanges",
"pattern": '{ ($.eventName = CreateRoute) || ($.eventName = CreateRouteTable) || ($.eventName = ReplaceRoute) || ($.eventName = DeleteRouteTable) || ($.eventName = DeleteRoute) }',
"threshold": 1,
},
{
"name": "VPCChanges",
"pattern": '{ ($.eventName = CreateVpc) || ($.eventName = DeleteVpc) || ($.eventName = ModifyVpcAttribute) || ($.eventName = AcceptVpcPeeringConnection) || ($.eventName = CreateVpcPeeringConnection) || ($.eventName = DeleteVpcPeeringConnection) }',
"threshold": 1,
},
]
class CISCloudWatchAlarmsStack(Stack):
def __init__(
self,
scope: Construct,
construct_id: str,
cloudtrail_log_group_name: str,
alarm_email: str,
**kwargs,
):
super().__init__(scope, construct_id, **kwargs)
topic = sns.Topic(
self, "SecurityAlarmsTopic",
topic_name="security-alarms",
display_name="Security Alarms",
)
topic.add_subscription(subs.EmailSubscription(alarm_email))
log_group = logs.LogGroup.from_log_group_name(
self, "CloudTrailLogGroup", cloudtrail_log_group_name
)
alarm_action = cw_actions.SnsAction(topic)
for alarm_config in CIS_ALARMS:
metric_name = f"{alarm_config['name']}Count"
metric_filter = logs.MetricFilter(
self, f"{alarm_config['name']}Filter",
log_group=log_group,
metric_namespace="CISBenchmark",
metric_name=metric_name,
filter_pattern=logs.FilterPattern.literal(alarm_config["pattern"]),
metric_value="1",
default_value=0,
)
metric = cloudwatch.Metric(
namespace="CISBenchmark",
metric_name=metric_name,
statistic="Sum",
period=Duration.seconds(300),
)
alarm = cloudwatch.Alarm(
self, f"{alarm_config['name']}Alarm",
alarm_name=f"CIS-{alarm_config['name']}",
metric=metric,
evaluation_periods=1,
threshold=alarm_config["threshold"],
comparison_operator=cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
treat_missing_data=cloudwatch.TreatMissingData.NOT_BREACHING,
)
alarm.add_alarm_action(alarm_action)Usage in app.py:
CISCloudWatchAlarmsStack(
app, "CISAlarmsStack",
cloudtrail_log_group_name="aws-cloudtrail-logs-ACCOUNT_ID",
alarm_email="security-team@yourcompany.com",
env=env,
)Alert fatigue: keeping alarms actionable
The 14 alarms will generate a lot of noise during CDK deployments — IAM policy changes, security group modifications, and VPC changes are normal side effects of any infrastructure deploy. Without noise management, the security team will start ignoring everything.
Deployment windows: Before a CDK deploy, send a notification to the security channel: “Deploying stacks X, Y, Z — expect CIS alarms for IAM, SG, and VPC changes in the next 30 minutes.” This gives context to whoever is on-call.
Routing by alarm type: Not all alarms warrant a PagerDuty page. Root account usage and CloudTrail modification are always page-worthy. Security group changes during a deployment are not. Route through separate SNS topics if you have a paging integration:
# High-priority topic (triggers PagerDuty)
urgent_topic = sns.Topic(self, "UrgentAlarmsTopics", ...)
# Normal priority topic (email only)
normal_topic = sns.Topic(self, "NormalAlarmsTopic", ...)
# Root account usage → urgent
root_alarm.add_alarm_action(cw_actions.SnsAction(urgent_topic))
# Security group changes → normal (noisy during deploys)
sg_alarm.add_alarm_action(cw_actions.SnsAction(normal_topic))Cost: All 14 metric filters + alarms cost approximately $2/month in CloudWatch. Negligible.
Reference: all 14 alarms at a glance
+----+--------------------+-------------+--------------------+ | # | Alarm Name | CIS Control | Noisy During | | | | | Deploys? | +----+--------------------+-------------+--------------------+ | 1 | Root Account Usage | 1.7 | No | +----+--------------------+-------------+--------------------+ | 2 | Unauthorized API | 3.1 | Occasionally | | | Calls | | | +----+--------------------+-------------+--------------------+ | 3 | Console Sign-In | 1.14 | No | | | Without MFA | | | +----+--------------------+-------------+--------------------+ | 4 | IAM Policy Changes | 3.4 | Yes (CDK creates | | | | | roles) | +----+--------------------+-------------+--------------------+ | 5 | CloudTrail Config | 3.5 | No | | | Changes | | | +----+--------------------+-------------+--------------------+ | 6 | Console Auth | 3.6 | No | | | Failures | | | +----+--------------------+-------------+--------------------+ | 7 | CMK Disable or | 3.7 | No | | | Deletion | | | +----+--------------------+-------------+--------------------+ | 8 | S3 Bucket Policy | 3.8 | Yes (CDK updates | | | Changes | | S3 policies) | +----+--------------------+-------------+--------------------+ | 9 | AWS Config Changes | 3.9 | No | +----+--------------------+-------------+--------------------+ | 10 | Security Group | 3.10 | Yes (CDK creates | | | Changes | | SGs) | +----+--------------------+-------------+--------------------+ | 11 | NACL Changes | 3.11 | No | +----+--------------------+-------------+--------------------+ | 12 | Network Gateway | 3.12 | No | | | Changes | | | +----+--------------------+-------------+--------------------+ | 13 | Route Table | 3.13 | No | | | Changes | | | +----+--------------------+-------------+--------------------+ | 14 | VPC Changes | 3.14 | Rarely | +----+--------------------+-------------+--------------------+