This check flags AWS accounts that lack a CloudWatch alarm for internet and customer gateway changes (CIS 3.12). Without it, an attacker or careless engineer can alter your network egress path unnoticed. Fix it by creating a metric filter on your CloudTrail log group and wiring it to an SNS-backed alarm.
Gateways are the doors between your VPC and the outside world. Internet gateways (IGWs) move traffic to and from the public internet, and customer gateways anchor your VPN connections back to on-premises networks. When someone attaches, detaches, creates, or deletes one of these, the routing reality of your account shifts. The No Alarm for Gateway Changes check looks for a CloudWatch alarm that fires on exactly these events, and it maps to CIS AWS Foundations Benchmark control 3.12.
If no alarm exists, those changes happen in silence. You find out when traffic stops flowing, or worse, when it starts flowing somewhere it should not.
What this check detects
The check inspects your account for a CloudWatch metric filter and alarm pair that watches CloudTrail events related to gateway changes. Specifically, it expects monitoring for these API calls:
CreateCustomerGatewayandDeleteCustomerGatewayAttachInternetGatewayandDetachInternetGatewayCreateInternetGatewayandDeleteInternetGateway
For the check to pass, three things need to line up: an active multi-region CloudTrail trail delivering logs to a CloudWatch Logs group, a metric filter on that group matching the events above, and a CloudWatch alarm on the resulting metric with an SNS topic that actually has a subscriber.
Note: CloudWatch alarms cannot read CloudTrail directly. The chain is CloudTrail to CloudWatch Logs to a metric filter to a metric to an alarm to SNS. If any link is missing, you get no alert even though every individual piece looks configured.
Why it matters
Gateway changes are high-impact, low-frequency events. In a stable account, nobody should be detaching an internet gateway on a Tuesday afternoon. That rarity is exactly what makes them worth alarming on, because a change is almost always either a deliberate maintenance action or something that needs investigating.
The attack scenario
Consider an attacker who has compromised an IAM principal with EC2 networking permissions. One quiet way to exfiltrate data or pivot is to manipulate gateways. Detaching an internet gateway from a subnet and reattaching a controlled one, or creating a new gateway to support a rogue route, lets traffic leave your VPC down a path you never approved. Without an alarm, this is invisible until an audit or an incident review surfaces it weeks later.
The accidental scenario
The more common case is human error. An engineer running a Terraform destroy against the wrong workspace tears down an internet gateway, and every public-facing service in that VPC loses connectivity. An alarm here turns a multi-hour outage investigation into a two-minute confirmation: you see the alarm, you see who made the call in CloudTrail, and you know exactly where to look.
Warning: Detaching an internet gateway breaks all public ingress and egress for the attached VPC immediately. There is no grace period. The alarm does not prevent the change, it just tells you it happened, so pair it with restrictive IAM on gateway actions.
How to fix it
You need a CloudWatch metric filter on the CloudWatch Logs group that receives your CloudTrail events, plus an alarm and an SNS topic. The steps below assume you already have a multi-region trail writing to a log group. If you do not, set that up first, since the gateway alarm has nothing to read without it.
Step 1: Create an SNS topic and subscribe to it
aws sns create-topic --name lensix-gateway-changes
aws sns subscribe \
--topic-arn arn:aws:sns:us-east-1:111122223333:lensix-gateway-changes \
--protocol email \
--notification-endpoint [email protected]
Confirm the subscription from the email you receive. An alarm with an unconfirmed or absent subscriber will not notify anyone, and the check treats that as a fail.
Step 2: Create the metric filter
Point this at the log group your trail delivers to. The filter pattern matches all six gateway events.
aws logs put-metric-filter \
--log-group-name "/aws/cloudtrail/lensix-org-trail" \
--filter-name "GatewayChanges" \
--filter-pattern '{ ($.eventName = CreateCustomerGateway) || ($.eventName = DeleteCustomerGateway) || ($.eventName = AttachInternetGateway) || ($.eventName = CreateInternetGateway) || ($.eventName = DeleteInternetGateway) || ($.eventName = DetachInternetGateway) }' \
--metric-transformations \
metricName=GatewayEventCount,metricNamespace=CISBenchmark,metricValue=1,defaultValue=0
Step 3: Create the alarm
aws cloudwatch put-metric-alarm \
--alarm-name "CIS-3.12-GatewayChanges" \
--alarm-description "Alarm for internet and customer gateway changes (CIS 3.12)" \
--metric-name GatewayEventCount \
--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-gateway-changes
Once the alarm transitions out of INSUFFICIENT_DATA, the check passes. You can verify with:
aws cloudwatch describe-alarms --alarm-names "CIS-3.12-GatewayChanges"
Terraform version
If you manage infrastructure as code, define the whole chain so it stays in place across rebuilds:
resource "aws_sns_topic" "gateway_changes" {
name = "lensix-gateway-changes"
}
resource "aws_sns_topic_subscription" "gateway_changes_email" {
topic_arn = aws_sns_topic.gateway_changes.arn
protocol = "email"
endpoint = "[email protected]"
}
resource "aws_cloudwatch_log_metric_filter" "gateway_changes" {
name = "GatewayChanges"
log_group_name = "/aws/cloudtrail/lensix-org-trail"
pattern = "{ ($.eventName = CreateCustomerGateway) || ($.eventName = DeleteCustomerGateway) || ($.eventName = AttachInternetGateway) || ($.eventName = CreateInternetGateway) || ($.eventName = DeleteInternetGateway) || ($.eventName = DetachInternetGateway) }"
metric_transformation {
name = "GatewayEventCount"
namespace = "CISBenchmark"
value = "1"
}
}
resource "aws_cloudwatch_metric_alarm" "gateway_changes" {
alarm_name = "CIS-3.12-GatewayChanges"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = 1
metric_name = "GatewayEventCount"
namespace = "CISBenchmark"
period = 300
statistic = "Sum"
threshold = 1
treat_missing_data = "notBreaching"
alarm_actions = [aws_sns_topic.gateway_changes.arn]
}
Tip: The CIS benchmark defines roughly a dozen of these alarm controls (root usage, unauthorized API calls, IAM policy changes, and so on). Build one reusable Terraform module that takes an event filter pattern and a name, then call it once per control. You get consistent alarms and a single place to maintain the SNS wiring.
How to prevent it from recurring
Manual fixes drift. An engineer deletes a log group during cleanup, or someone spins up a new account without the baseline, and the alarm quietly disappears. Lock it in instead.
- Bake it into your account baseline. Whether you use Control Tower, Account Factory for Terraform, or a custom landing zone, the CIS alarm set should be part of the same module that gets applied to every new account on day zero.
- Gate it in CI/CD. Run a policy-as-code scan in your pipeline so a pull request that removes the metric filter or alarm fails before merge. Tools like Checkov, tfsec, or OPA/Conftest can assert that the resources exist.
- Continuously monitor with Lensix. A pipeline gate only catches changes that flow through the pipeline. Console edits and out-of-band changes need a detective control, which is where ongoing scanning closes the gap.
A simple Conftest policy to assert the alarm exists in your plan:
package main
deny[msg] {
not gateway_alarm_present
msg = "CIS 3.12: a CloudWatch alarm for gateway changes is required"
}
gateway_alarm_present {
some i
input.resource.aws_cloudwatch_metric_alarm[i].alarm_name == "CIS-3.12-GatewayChanges"
}
Best practices
Getting the alarm to pass the check is the floor, not the ceiling. A few habits make this control genuinely useful rather than just compliant:
- Route alarms somewhere people read. An email topic with one subscriber who left the company is technically a pass and practically useless. Send these to a monitored channel, a PagerDuty service, or your SIEM.
- Centralize CloudTrail in an org trail. A single organization-wide trail delivering to one log group means you configure the alarm once and cover every account, instead of per-account drift.
- Restrict the underlying permissions. Alarms are detective controls. Pair them with SCPs or IAM policies that limit who can call gateway APIs so the alarm fires rarely and meaningfully.
- Tune the noise. If a particular team legitimately rebuilds gateways during deploys, exclude their automation role from triggering pages, but keep logging it. An alarm that cries wolf gets muted.
Danger: Do not delete or disable the CloudTrail trail or its log group to silence noisy alarms. Doing so blinds every CIS monitoring control at once, not just this one, and is itself a finding under CIS 3.1. Adjust the filter or alarm action instead.
Gateway changes sit at the boundary of your network. Watching them costs almost nothing, and the one time it matters, it turns a confusing outage or a quiet breach into a timestamped, attributable event you can act on in minutes.

