Back to blog
AWSCloud SecurityMonitoring & LoggingNetworkingOperations & Compliance

No Alarm for VPC Changes: Fixing CIS 3.14 on AWS

Learn why a missing CloudWatch alarm for VPC changes (CIS 3.14) is a security risk, and how to fix it with CLI and Terraform remediation steps.

TL;DR

This check flags AWS accounts that lack a CloudWatch alarm for VPC changes (CIS 3.14). Without it, attackers or careless operators can alter network routing, peering, or attach gateways without anyone noticing. Fix it by creating a metric filter on a CloudTrail log group and wiring it to an SNS-backed CloudWatch alarm.

Your VPC is the wiring closet of your AWS account. Route tables, internet gateways, peering connections, and NAT configurations all decide where traffic can flow and, more importantly, where it cannot. When someone modifies that wiring, you want to know about it. The No Alarm for VPC Changes check looks for a missing CloudWatch alarm that should fire whenever a VPC-level API call happens, as recommended by CIS AWS Foundations Benchmark control 3.14.


What this check detects

Lensix inspects your account for a CloudWatch metric filter and alarm pair that watches CloudTrail logs for VPC modification events. Specifically, the alarm should trigger on API calls like:

  • CreateVpc / DeleteVpc
  • CreateVpcPeeringConnection / AcceptVpcPeeringConnection / DeleteVpcPeeringConnection
  • AttachClassicLinkVpc / DetachClassicLinkVpc
  • EnableVpcClassicLink / DisableVpcClassicLink
  • ModifyVpcAttribute

If no metric filter matches these events, or one exists but has no alarm attached, the check fails. It is a detective control, not a preventive one. The goal is visibility: making sure a network change cannot happen silently.

Note: This control depends on a working CloudTrail trail that delivers events to a CloudWatch Logs group. If CloudTrail is not logging to CloudWatch Logs in the first place, the metric filter has nothing to read and the alarm will never fire. Confirm CloudTrail-to-CloudWatch delivery before relying on this alarm.


Why it matters

VPC changes are high blast radius. A single route table edit can redirect outbound traffic through an attacker-controlled instance, and a new peering connection can quietly bridge your isolated production environment to an untrusted account. These are not loud, obvious actions. They look like ordinary API calls in a sea of thousands per hour.

Consider a few realistic scenarios:

  • Data exfiltration via peering. An attacker with compromised credentials creates a VPC peering connection to an external account and routes sensitive subnet traffic out through it. No alarm means the connection can sit live for weeks.
  • Routing hijack. A modified route sends traffic destined for an internal service through a malicious NAT instance, enabling a man-in-the-middle position.
  • Accidental exposure. An engineer attaches an internet gateway to a subnet that was supposed to stay private, putting internal databases one security group rule away from the public internet.

From a compliance angle, CIS 3.14 is part of the monitoring section that auditors check directly. A failing control here usually shows up in SOC 2 and PCI DSS assessments as a gap in change detection.

Network changes are among the few events where the time-to-detect directly determines the size of the breach. An alarm that fires in seconds turns a multi-week compromise into a same-day incident.


How to fix it

The fix has three pieces: an SNS topic for notifications, a metric filter on the CloudTrail log group, and a CloudWatch alarm tied to that metric. The steps below assume you already have a CloudTrail trail shipping to a CloudWatch Logs group.

Step 1: Create an SNS topic and subscribe to it

aws sns create-topic --name cis-vpc-changes-alarm

aws sns subscribe \
  --topic-arn arn:aws:sns:us-east-1:111122223333:cis-vpc-changes-alarm \
  --protocol email \
  --notification-endpoint [email protected]

Confirm the subscription from the email you receive before moving on, otherwise no notifications will be delivered.

Step 2: Create the metric filter

Point this at your CloudTrail CloudWatch Logs group. The filter pattern matches all the VPC events CIS 3.14 cares about.

aws logs put-metric-filter \
  --log-group-name "CloudTrail/DefaultLogGroup" \
  --filter-name "CIS-VpcChanges" \
  --filter-pattern '{ ($.eventName = CreateVpc) || ($.eventName = DeleteVpc) || ($.eventName = ModifyVpcAttribute) || ($.eventName = AcceptVpcPeeringConnection) || ($.eventName = CreateVpcPeeringConnection) || ($.eventName = DeleteVpcPeeringConnection) || ($.eventName = RejectVpcPeeringConnection) || ($.eventName = AttachClassicLinkVpc) || ($.eventName = DetachClassicLinkVpc) || ($.eventName = DisableVpcClassicLink) || ($.eventName = EnableVpcClassicLink) }' \
  --metric-transformations \
      metricName=VpcChangeCount,metricNamespace=CISBenchmark,metricValue=1,defaultValue=0

Step 3: Create the alarm

aws cloudwatch put-metric-alarm \
  --alarm-name "CIS-3.14-VpcChanges" \
  --alarm-description "Alarm on VPC configuration changes (CIS 3.14)" \
  --metric-name VpcChangeCount \
  --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-vpc-changes-alarm

Tip: CIS has a whole family of these alarms (3.1 through 3.16) covering root login, IAM policy changes, security group changes, and more. Rather than creating them one at a time, loop over an array of {filter-name, pattern, metric-name} tuples in a shell script, or better, define them once in Terraform so the entire benchmark is enforced as a unit.

Console steps (if you prefer the UI)

  1. Open CloudWatch > Log groups and select your CloudTrail log group.
  2. Choose Metric filters > Create metric filter and paste the filter pattern from Step 2.
  3. Assign it to the CISBenchmark namespace with metric name VpcChangeCount.
  4. After creation, choose Create alarm from the metric filter, set the threshold to >= 1 over a 5-minute period, and select your SNS topic for the action.

Doing it as code

Manual fixes drift. The durable approach is to define the metric filter and alarm in Terraform so the control is version-controlled and reapplied on every plan.

resource "aws_sns_topic" "cis_alarms" {
  name = "cis-vpc-changes-alarm"
}

resource "aws_cloudwatch_log_metric_filter" "vpc_changes" {
  name           = "CIS-VpcChanges"
  log_group_name = aws_cloudwatch_log_group.cloudtrail.name

  pattern = "{ ($.eventName = CreateVpc) || ($.eventName = DeleteVpc) || ($.eventName = ModifyVpcAttribute) || ($.eventName = AcceptVpcPeeringConnection) || ($.eventName = CreateVpcPeeringConnection) || ($.eventName = DeleteVpcPeeringConnection) || ($.eventName = RejectVpcPeeringConnection) || ($.eventName = AttachClassicLinkVpc) || ($.eventName = DetachClassicLinkVpc) || ($.eventName = DisableVpcClassicLink) || ($.eventName = EnableVpcClassicLink) }"

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

resource "aws_cloudwatch_metric_alarm" "vpc_changes" {
  alarm_name          = "CIS-3.14-VpcChanges"
  comparison_operator = "GreaterThanOrEqualToThreshold"
  evaluation_periods  = 1
  metric_name         = aws_cloudwatch_log_metric_filter.vpc_changes.metric_transformation[0].name
  namespace           = "CISBenchmark"
  period              = 300
  statistic           = "Sum"
  threshold           = 1
  treat_missing_data  = "notBreaching"
  alarm_actions       = [aws_sns_topic.cis_alarms.arn]
}

Warning: Metric filters only evaluate logs in the region where the log group lives, and CloudWatch alarms are regional. If you operate in multiple regions, you need this stack in each one, or you need an organization trail that aggregates events into a single log group. Assuming one alarm covers your whole footprint is a common and costly mistake.


Preventing regressions

Detecting the gap once is easy. Keeping it closed across dozens of accounts is the real work.

  • Deploy through a StackSet or Terraform across all accounts. Package the SNS topic, metric filter, and alarm into a single module and apply it to every account in your AWS Organization. New accounts inherit the control automatically.
  • Gate IaC in CI/CD. Run a policy-as-code scan on every pull request. With Open Policy Agent or Checkov, fail the build if a CloudTrail trail exists without the corresponding CIS metric filters.
  • Use AWS Config conformance packs. The CIS conformance pack includes a managed rule that continuously evaluates whether these monitoring alarms exist, and reports drift in near real time.
  • Run Lensix on a schedule. Continuous scanning catches the case where someone deletes an alarm or detaches the SNS action months after deployment.

Here is a minimal Checkov-style custom check concept for catching a missing alarm reference in Terraform:

# Fail the pipeline if no metric alarm references VpcChangeCount
grep -rq "VpcChangeCount" ./terraform/ \
  || { echo "Missing CIS 3.14 VPC change alarm"; exit 1; }

Best practices

An alarm that no one reads is theater. Tie the SNS topic into the channel your on-call engineers actually watch, whether that is PagerDuty, Slack, or an incident tool, not just an inbox that collects dust.

  • Route alarms to a real workflow. Subscribe a Lambda or an EventBridge rule that posts to your incident channel and tags the originating IAM principal from the CloudTrail event.
  • Enrich the notification. A bare "VPC changed" alert forces a manual investigation. Include the eventName, userIdentity, and source IP so responders can triage in seconds.
  • Reduce expected noise at the source. If your infrastructure team makes frequent legitimate VPC changes through a pipeline, suppress alerts originating from the known automation role rather than raising the threshold, which would blind you to manual changes.
  • Pair detection with prevention. Service control policies that deny VPC peering to unapproved accounts stop the bad change before it happens. The alarm is your backstop for what the SCP does not cover.
  • Test the alarm. Deliberately make a benign change, like setting a VPC attribute, in a sandbox and confirm the alert lands where you expect. An untested detective control is an assumption, not a safeguard.

Note: CIS 3.14 is one piece of a broader monitoring posture. If this control is missing, it is worth checking the rest of the 3.x family at the same time, since accounts that lack one CIS alarm usually lack several. Fixing them together is far cheaper than fixing them one incident at a time.