<< BACK

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-1
++

All 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 notBreaching
++

The 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             |
+----+--------------------+-------------+--------------------+