Skip to main content

CloudWatch Route Table Changes Alarm

Overview

This check verifies that your AWS account has a CloudWatch Logs metric filter and alarm configured to monitor VPC route table changes. Route tables control how network traffic flows within and outside your VPC. Monitoring changes to these tables helps you detect unauthorized modifications or misconfigurations quickly.

The check looks for monitoring of these CloudTrail API events:

  • CreateRoute / DeleteRoute
  • CreateRouteTable / DeleteRouteTable
  • ReplaceRoute / ReplaceRouteTableAssociation
  • DisassociateRouteTable

Risk

Without monitoring route table changes, attackers or accidental misconfigurations can:

  • Redirect traffic to malicious destinations for data interception
  • Bypass security controls like firewalls or inspection appliances
  • Cause outages by blackholing routes or breaking connectivity
  • Enable data exfiltration by routing traffic outside your trusted network
  • Go undetected for hours or days while exploiting compromised routing

Route tables are foundational to your VPC security architecture. Changes should be rare and always intentional.

Remediation Steps

Prerequisites

You need:

  • AWS Console access with permissions to create CloudWatch alarms and metric filters
  • An existing CloudTrail trail sending logs to CloudWatch Logs
  • An SNS topic for alarm notifications (or permission to create one)
Required IAM permissions (for administrators)

Your IAM user or role needs these permissions:

  • logs:PutMetricFilter
  • logs:DescribeMetricFilters
  • cloudwatch:PutMetricAlarm
  • cloudwatch:DescribeAlarms
  • sns:CreateTopic
  • sns:Subscribe
Prerequisite: Ensure CloudTrail logs to CloudWatch

Before creating the metric filter, you need a CloudTrail trail that sends logs to CloudWatch Logs. If you do not have this configured:

  1. Go to CloudTrail in the AWS Console
  2. Select your trail (or create one)
  3. Under CloudWatch Logs, enable logging and note the log group name

For detailed instructions, see the cloudtrail_cloudwatch_logging_enabled remediation guide.

AWS Console Method

  1. Open CloudWatch in the AWS Console

  2. Create a metric filter

    • In the left sidebar, expand Logs and click Log groups
    • Find and click on your CloudTrail log group (often named /aws/cloudtrail/<trail-name>)
    • Click the Metric filters tab
    • Click Create metric filter
  3. Define the filter pattern

    • In the Filter pattern field, paste:
    { ($.eventName = CreateRoute) || ($.eventName = CreateRouteTable) || ($.eventName = ReplaceRoute) || ($.eventName = ReplaceRouteTableAssociation) || ($.eventName = DeleteRouteTable) || ($.eventName = DeleteRoute) || ($.eventName = DisassociateRouteTable) }
    • Click Next
  4. Configure the metric

    • Filter name: RouteTableChanges
    • Metric namespace: CloudTrailMetrics
    • Metric name: RouteTableChangeCount
    • Metric value: 1
    • Default value: 0
    • Click Next, then Create metric filter
  5. Create an alarm from the metric filter

    • On the metric filter you just created, click Create alarm
    • Configure the alarm:
      • Statistic: Sum
      • Period: 5 minutes
      • Threshold type: Static
      • Condition: Greater than or equal to 1
    • Click Next
  6. Configure notifications

    • Under Notification, select In alarm
    • Choose an existing SNS topic or create a new one
    • If creating new, enter a name and your email address
    • Click Next
  7. Name and create the alarm

    • Alarm name: RouteTableChangesAlarm
    • Alarm description: Alerts when VPC route table changes are detected
    • Click Next, review settings, then Create alarm
  8. Confirm your subscription

    • If you created a new SNS topic, check your email and confirm the subscription
AWS CLI (optional)

Step 1: Create the metric filter

Replace <cloudtrail-log-group> with your CloudTrail log group name:

aws logs put-metric-filter \
--log-group-name <cloudtrail-log-group> \
--filter-name RouteTableChanges \
--filter-pattern '{ ($.eventName = CreateRoute) || ($.eventName = CreateRouteTable) || ($.eventName = ReplaceRoute) || ($.eventName = ReplaceRouteTableAssociation) || ($.eventName = DeleteRouteTable) || ($.eventName = DeleteRoute) || ($.eventName = DisassociateRouteTable) }' \
--metric-transformations metricName=RouteTableChangeCount,metricNamespace=CloudTrailMetrics,metricValue=1,defaultValue=0 \
--region us-east-1

Step 2: Create an SNS topic for notifications

aws sns create-topic \
--name RouteTableChangeAlerts \
--region us-east-1

Note the TopicArn from the output.

Step 3: Subscribe your email to the topic

Replace <topic-arn> and <your-email>:

aws sns subscribe \
--topic-arn <topic-arn> \
--protocol email \
--notification-endpoint <your-email> \
--region us-east-1

Check your email and confirm the subscription.

Step 4: Create the CloudWatch alarm

Replace <topic-arn> with your SNS topic ARN:

aws cloudwatch put-metric-alarm \
--alarm-name RouteTableChangesAlarm \
--alarm-description "Alerts when VPC route table changes are detected" \
--metric-name RouteTableChangeCount \
--namespace CloudTrailMetrics \
--statistic Sum \
--period 300 \
--evaluation-periods 1 \
--threshold 1 \
--comparison-operator GreaterThanOrEqualToThreshold \
--alarm-actions <topic-arn> \
--treat-missing-data notBreaching \
--region us-east-1
CloudFormation (optional)

This template creates the metric filter, SNS topic, and CloudWatch alarm:

AWSTemplateFormatVersion: '2010-09-09'
Description: CloudWatch alarm for VPC route table changes

Parameters:
CloudTrailLogGroupName:
Type: String
Description: Name of the CloudTrail CloudWatch Logs log group
Default: /aws/cloudtrail/my-trail

NotificationEmail:
Type: String
Description: Email address for alarm notifications

Resources:
RouteTableChangesTopic:
Type: AWS::SNS::Topic
Properties:
TopicName: RouteTableChangeAlerts
DisplayName: Route Table Change Alerts

RouteTableChangesSubscription:
Type: AWS::SNS::Subscription
Properties:
TopicArn: !Ref RouteTableChangesTopic
Protocol: email
Endpoint: !Ref NotificationEmail

RouteTableChangesMetricFilter:
Type: AWS::Logs::MetricFilter
Properties:
LogGroupName: !Ref CloudTrailLogGroupName
FilterName: RouteTableChanges
FilterPattern: >-
{ ($.eventName = CreateRoute) || ($.eventName = CreateRouteTable) ||
($.eventName = ReplaceRoute) || ($.eventName = ReplaceRouteTableAssociation) ||
($.eventName = DeleteRouteTable) || ($.eventName = DeleteRoute) ||
($.eventName = DisassociateRouteTable) }
MetricTransformations:
- MetricName: RouteTableChangeCount
MetricNamespace: CloudTrailMetrics
MetricValue: '1'
DefaultValue: 0

RouteTableChangesAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: RouteTableChangesAlarm
AlarmDescription: Alerts when VPC route table changes are detected
MetricName: RouteTableChangeCount
Namespace: CloudTrailMetrics
Statistic: Sum
Period: 300
EvaluationPeriods: 1
Threshold: 1
ComparisonOperator: GreaterThanOrEqualToThreshold
AlarmActions:
- !Ref RouteTableChangesTopic
TreatMissingData: notBreaching

Outputs:
AlarmArn:
Description: ARN of the CloudWatch alarm
Value: !GetAtt RouteTableChangesAlarm.Arn

TopicArn:
Description: ARN of the SNS topic
Value: !Ref RouteTableChangesTopic

Deploy with:

aws cloudformation deploy \
--template-file route-table-alarm.yaml \
--stack-name route-table-changes-alarm \
--parameter-overrides \
CloudTrailLogGroupName=/aws/cloudtrail/my-trail \
NotificationEmail=your-email@example.com \
--region us-east-1
Terraform (optional)
variable "cloudtrail_log_group_name" {
description = "Name of the CloudTrail CloudWatch Logs log group"
type = string
default = "/aws/cloudtrail/my-trail"
}

variable "notification_email" {
description = "Email address for alarm notifications"
type = string
}

# SNS Topic for notifications
resource "aws_sns_topic" "route_table_changes" {
name = "RouteTableChangeAlerts"
display_name = "Route Table Change Alerts"
}

resource "aws_sns_topic_subscription" "email" {
topic_arn = aws_sns_topic.route_table_changes.arn
protocol = "email"
endpoint = var.notification_email
}

# Metric filter for route table changes
resource "aws_cloudwatch_log_metric_filter" "route_table_changes" {
name = "RouteTableChanges"
log_group_name = var.cloudtrail_log_group_name
pattern = "{ ($.eventName = CreateRoute) || ($.eventName = CreateRouteTable) || ($.eventName = ReplaceRoute) || ($.eventName = ReplaceRouteTableAssociation) || ($.eventName = DeleteRouteTable) || ($.eventName = DeleteRoute) || ($.eventName = DisassociateRouteTable) }"

metric_transformation {
name = "RouteTableChangeCount"
namespace = "CloudTrailMetrics"
value = "1"
default_value = "0"
}
}

# CloudWatch alarm
resource "aws_cloudwatch_metric_alarm" "route_table_changes" {
alarm_name = "RouteTableChangesAlarm"
alarm_description = "Alerts when VPC route table changes are detected"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = 1
metric_name = "RouteTableChangeCount"
namespace = "CloudTrailMetrics"
period = 300
statistic = "Sum"
threshold = 1
treat_missing_data = "notBreaching"

alarm_actions = [aws_sns_topic.route_table_changes.arn]

depends_on = [aws_cloudwatch_log_metric_filter.route_table_changes]
}

output "alarm_arn" {
description = "ARN of the CloudWatch alarm"
value = aws_cloudwatch_metric_alarm.route_table_changes.arn
}

output "topic_arn" {
description = "ARN of the SNS topic"
value = aws_sns_topic.route_table_changes.arn
}

Deploy with:

terraform init
terraform plan -var="notification_email=your-email@example.com"
terraform apply -var="notification_email=your-email@example.com"

Verification

After creating the alarm, verify it is working correctly:

  1. In the AWS Console:

    • Go to CloudWatch > Alarms > All alarms
    • Find RouteTableChangesAlarm and confirm its state is OK (not "Insufficient data" after a few minutes)
    • Go to CloudWatch > Logs > Log groups and select your CloudTrail log group
    • Click Metric filters and verify RouteTableChanges exists
  2. Test the alarm (optional):

    • Create a temporary route table in a test VPC
    • Wait 5-10 minutes for CloudTrail to log the event
    • Check that the alarm triggers and you receive a notification
CLI verification commands

Check the metric filter exists:

aws logs describe-metric-filters \
--log-group-name <cloudtrail-log-group> \
--filter-name-prefix RouteTableChanges \
--region us-east-1

Check the alarm configuration:

aws cloudwatch describe-alarms \
--alarm-names RouteTableChangesAlarm \
--region us-east-1

Verify the alarm state:

aws cloudwatch describe-alarms \
--alarm-names RouteTableChangesAlarm \
--query 'MetricAlarms[0].StateValue' \
--output text \
--region us-east-1

The state should be OK (not INSUFFICIENT_DATA after some time has passed).

Additional Resources

Notes

  • CloudTrail prerequisite: This check requires CloudTrail logs to be sent to CloudWatch Logs. If that integration is not configured, set it up first using the cloudtrail_cloudwatch_logging_enabled remediation guide.

  • Timing: CloudTrail events can take 5-15 minutes to appear in CloudWatch Logs. The alarm evaluates every 5 minutes, so expect some delay between a route table change and receiving an alert.

  • Costs: CloudWatch metric filters are free, but CloudWatch alarms have a small monthly cost (approximately $0.10 per alarm). SNS email notifications are free.

  • False positives: Legitimate infrastructure changes (deployments, scaling) will trigger this alarm. Consider documenting expected changes or using maintenance windows. You can also adjust the threshold if your environment has frequent planned changes.

  • Multiple alarms: This alarm monitors all route table changes. For high-security environments, consider separate alarms for different event types (e.g., delete operations only).

  • Alarm actions: The examples use email notifications. For production environments, consider also triggering Lambda functions, PagerDuty, or other incident response systems.