Back to blog
AWSCloud SecurityMonitoring & LoggingNetworkingOperations & Compliance

No Alarm for Security Group Changes (CIS 3.10): Why It Matters and How to Fix It

Learn why missing a CloudWatch alarm for AWS security group changes (CIS 3.10) is risky, and how to fix it with CLI, Terraform, and policy-as-code.

TL;DR

This check flags AWS accounts that lack a CloudWatch alarm for security group changes (CIS 3.10). Without it, an attacker or careless engineer can open ports to the internet and nobody gets notified. Fix it by creating a CloudWatch metric filter on a CloudTrail log group and wiring it to an SNS-backed alarm.

Security groups are the front door to almost everything you run on AWS. They decide which traffic reaches your EC2 instances, RDS databases, load balancers, and Lambda functions inside a VPC. A single rule change can expose a database port to the entire internet, and that change takes seconds to make. The question is whether you find out about it.

The No Alarm for Security Group Changes check looks for a specific piece of detective control wiring: a CloudWatch alarm that fires when someone modifies a security group. If that alarm does not exist, your account passes security group changes silently, which is exactly what an attacker wants.


What this check detects

The check verifies that your account has a CloudWatch alarm tied to a metric filter that matches security group change events in CloudTrail. Specifically, it expects a metric filter on a CloudTrail log group that matches these API calls:

  • AuthorizeSecurityGroupIngress
  • AuthorizeSecurityGroupEgress
  • RevokeSecurityGroupIngress
  • RevokeSecurityGroupEgress
  • CreateSecurityGroup
  • DeleteSecurityGroup

This maps directly to CIS AWS Foundations Benchmark recommendation 3.10, which calls for a metric filter and alarm covering security group changes. The check fails if any link in the chain is missing: no CloudTrail trail feeding CloudWatch Logs, no metric filter, or a metric filter with no alarm attached.

Note: CloudTrail records the API call, but the event sits in an S3 bucket or log group doing nothing on its own. The metric filter turns matching log events into a CloudWatch metric, and the alarm turns that metric into a notification. All three pieces have to be present for the control to work.


Why it matters

Security group changes are one of the most common ways environments get exposed, and they rarely look malicious at the moment they happen. Consider how these play out:

  • Accidental exposure. An engineer debugging a connection issue temporarily opens 0.0.0.0/0 on port 22 or 3306, plans to revert it, and forgets. The instance is now reachable from the internet and you have no record that anyone noticed.
  • Credential compromise. An attacker who gets hold of IAM credentials with EC2 permissions will often modify a security group to open a path to a resource they want to reach. With no alarm, this blends into normal activity until the damage is done.
  • Lateral movement. After landing on one compromised host, attackers frequently loosen egress rules to reach internal services or set up command and control channels. Egress changes are easy to overlook because most teams only watch ingress.

The business impact is straightforward. Exposed database ports lead to data theft and ransom demands. Open SSH or RDP ports lead to brute force attacks and cryptominers. And because security group changes are routine, they hide well in audit logs that nobody reads. An alarm flips the model from "find it later during an investigation" to "know within minutes."

Warning: This is a detective control, not a preventive one. The alarm tells you a change happened, it does not stop the change. Pair it with restrictive IAM permissions and guardrails (covered below) so that fewer people can make these changes in the first place.


How to fix it

You need three components: a CloudTrail trail that delivers events to a CloudWatch Logs group, a metric filter on that group, and an alarm that publishes to an SNS topic. If you already have a multi-region trail wired to CloudWatch Logs, skip to step 2.

Step 1: Make sure CloudTrail delivers to CloudWatch Logs

Confirm you have a trail sending events to a log group:

aws cloudtrail describe-trails \
  --query 'trailList[?CloudWatchLogsLogGroupArn!=`null`].[Name,CloudWatchLogsLogGroupArn]' \
  --output table

If nothing comes back, you need to point a trail at a log group first. The metric filter cannot work without it.

Step 2: Create the SNS topic and subscription

This is where the alarm sends notifications. Create a topic and subscribe an email address (or, better, a chat or paging endpoint).

# Create the topic
aws sns create-topic --name lensix-security-alarms

# Subscribe an email address (confirm the subscription from your inbox)
aws sns subscribe \
  --topic-arn arn:aws:sns:us-east-1:111122223333:lensix-security-alarms \
  --protocol email \
  --notification-endpoint [email protected]

Step 3: Create the metric filter

This filter matches all six security group API calls and increments a metric whenever one appears in the logs. Replace the log group name with yours.

aws logs put-metric-filter \
  --log-group-name "aws-cloudtrail-logs" \
  --filter-name "SecurityGroupChanges" \
  --filter-pattern '{ ($.eventName = AuthorizeSecurityGroupIngress) || ($.eventName = AuthorizeSecurityGroupEgress) || ($.eventName = RevokeSecurityGroupIngress) || ($.eventName = RevokeSecurityGroupEgress) || ($.eventName = CreateSecurityGroup) || ($.eventName = DeleteSecurityGroup) }' \
  --metric-transformations \
    metricName=SecurityGroupChangeCount,metricNamespace=CISBenchmark,metricValue=1

Step 4: Create the alarm

The alarm watches the metric and notifies your SNS topic when one or more changes occur in a period.

aws cloudwatch put-metric-alarm \
  --alarm-name "CIS-3.10-SecurityGroupChanges" \
  --alarm-description "Alarm on any security group change (CIS 3.10)" \
  --metric-name SecurityGroupChangeCount \
  --namespace CISBenchmark \
  --statistic Sum \
  --period 300 \
  --evaluation-periods 1 \
  --threshold 1 \
  --comparison-operator GreaterThanOrEqualToThreshold \
  --treat-missing-data notBreaching \
  --alarm-actions arn:aws:sns:us-east-1:111122223333:lensix-security-alarms

Once the subscription is confirmed, make a harmless test change (add and remove a tag, or add a self-referencing rule) and verify the notification lands.

Tip: Set --treat-missing-data notBreaching as shown above. Security group changes are infrequent, so the metric reports no data most of the time. Without this setting, the alarm can flap into an INSUFFICIENT_DATA state and generate noise.


Doing it with infrastructure as code

Click-ops drifts. Define the control in code so it is consistent across accounts and survives a teardown. Here is the same setup in Terraform.

resource "aws_sns_topic" "security_alarms" {
  name = "lensix-security-alarms"
}

resource "aws_cloudwatch_log_metric_filter" "sg_changes" {
  name           = "SecurityGroupChanges"
  log_group_name = var.cloudtrail_log_group_name

  pattern = "{ ($.eventName = AuthorizeSecurityGroupIngress) || ($.eventName = AuthorizeSecurityGroupEgress) || ($.eventName = RevokeSecurityGroupIngress) || ($.eventName = RevokeSecurityGroupEgress) || ($.eventName = CreateSecurityGroup) || ($.eventName = DeleteSecurityGroup) }"

  metric_transformation {
    name      = "SecurityGroupChangeCount"
    namespace = "CISBenchmark"
    value     = "1"
  }
}

resource "aws_cloudwatch_metric_alarm" "sg_changes" {
  alarm_name          = "CIS-3.10-SecurityGroupChanges"
  comparison_operator = "GreaterThanOrEqualToThreshold"
  evaluation_periods  = 1
  metric_name         = aws_cloudwatch_log_metric_filter.sg_changes.metric_transformation[0].name
  namespace           = "CISBenchmark"
  period              = 300
  statistic           = "Sum"
  threshold           = 1
  treat_missing_data  = "notBreaching"
  alarm_description   = "Alarm on any security group change (CIS 3.10)"
  alarm_actions       = [aws_sns_topic.security_alarms.arn]
}

Note: CIS 3.10 is one of roughly a dozen monitoring alarms in the benchmark (root usage, IAM policy changes, network ACL changes, route table changes, and more). Rather than build each by hand, wrap a Terraform module that takes a list of filter patterns and produces a filter plus alarm for each one. You configure the whole CIS monitoring section in a single place.


How to prevent it from happening again

An alarm you create once and forget can quietly disappear when someone refactors a CloudTrail setup or spins up a new account. Build the control into your pipeline so absence becomes a hard failure.

  • Scan in CI/CD. Run a policy-as-code check on every Terraform plan. Tools like Checkov, tfsec, and OPA/Conftest have rules for CIS monitoring alarms. Fail the build if the metric filter or alarm is missing from the plan.
  • Enforce it across accounts. In AWS Organizations, deploy the trail, log group, metric filters, and alarms through a CloudFormation StackSet or a shared Terraform module that every account inherits. New accounts get the control automatically.
  • Monitor for tampering. Add a second metric filter for DeleteMetricFilter and DeleteAlarms calls so you are alerted if someone removes the monitoring itself. Detective controls are a target during an attack.
  • Continuously verify. Let Lensix run this check on a schedule so configuration drift surfaces within hours, not at the next audit.

A quick Conftest example you can drop into a pipeline to require the metric filter:

package main

deny[msg] {
  not has_sg_change_filter
  msg := "Missing CloudWatch metric filter for security group changes (CIS 3.10)"
}

has_sg_change_filter {
  some r
  input.resource.aws_cloudwatch_log_metric_filter[r].name == "SecurityGroupChanges"
}

Best practices

Getting the alarm in place is the minimum. A few habits make the control genuinely useful instead of another ignored email.

  1. Route alarms somewhere people read. An SNS email that lands in a shared inbox nobody checks is worthless. Send to Slack, PagerDuty, or your SIEM so the alert reaches a human who can act.
  2. Include context in the notification. Use EventBridge with the CloudTrail event or a Lambda formatter so the message shows who made the change, which security group, and what rule. "A security group changed" is far less useful than "alice@ opened 0.0.0.0/0:22 on sg-0abc in prod."
  3. Cover all CIS monitoring recommendations together. Security group changes are one signal. Pair them with alarms for root account usage, unauthorized API calls, IAM policy changes, and network gateway changes so you see the full picture of a possible compromise.
  4. Reduce the blast radius with prevention. Limit who can call the security group APIs with tight IAM policies, and use Service Control Policies to block the most dangerous rules outright (for example, denying ingress rules that allow 0.0.0.0/0 on sensitive ports). The fewer people who can make a change, the more meaningful each alarm becomes.
  5. Test your alarms. A control you never exercise is a control you cannot trust. Periodically trigger a benign change and confirm the alert flows end to end.

Danger: Do not delete an existing metric filter or alarm just to "clean up" before you have a replacement in place. Removing a CIS monitoring control leaves a blind spot during exactly the window an attacker would exploit. Replace first, remove second, and alert on the removal itself.

Security group changes will keep happening, every day, across every account. The goal is not to stop all of them, it is to make sure none of them happen in silence. A CloudWatch alarm wired to a real notification channel turns an invisible change into a five-minute heads-up, and that head start is often the difference between a near miss and an incident.