AWS WAF Security Analysis with CloudWatch
AWS WAF generates detailed JSON logs for every web request evaluated by a web ACL. When you send these logs to Amazon CloudWatch Logs, you unlock three complementary analysis capabilities: CloudWatch Logs Insights for ad-hoc investigation, metric filters for near-real-time alerting, and Contributor Insights for continuous top-N threat identification. Together, these capabilities form a complete security operations workflow for detecting, investigating, and responding to web application threats.
This guide covers best practices for configuring WAF logging to CloudWatch Logs, building effective security queries, creating metric filters and alarms for automated threat detection, and using Contributor Insights rules to continuously identify your top unauthorized actors and most-targeted endpoints.
Setup WAF Logging for CloudWatch
WAF log groups must be prefixed with aws-waf-logs-. For example: aws-waf-logs-production. This is an AWS-enforced naming convention, and log delivery will fail silently if the prefix is missing. See Sending web ACL traffic logs to a CloudWatch Logs log group for details.
Before analyzing WAF logs in CloudWatch, you must configure your web ACL to send logs to a CloudWatch Logs log group. The recommended approach is to use CloudWatch telemetry enablement rules (part of CloudWatch Unified Data Sources) to automatically enable WAF logging across your accounts and organization.
Enable WAF Log Ingestion via CloudWatch Telemetry Enablement Rules (Console)
Telemetry enablement rules automatically discover WAF web ACLs in your account or organization and enable log delivery to CloudWatch Logs. This is the preferred method for consistent, scalable WAF log collection. The rule will enable logging for both existing web ACLs that do not already have CloudWatch Logs logging configured, and any new web ACLs created in the future.
- Open the CloudWatch console.
- In the navigation pane, choose Ingestion.
- Choose the Enablement rules tab.
- Choose Add rule.
- Choose Configure telemetry for AWS WAFV2.
- Under the Specify scope screen, for Rule name, enter a descriptive name (for example,
WAFv2-WebACL-TurnOnLogging-20260708-2119). Note that the rule name cannot be modified once created. - For Select source accounts, choose one of the following:
- All accounts in your organization: Applies the rule across your entire AWS Organizations
- Filter accounts: Applies the rule to specific accounts
- (Optional) Under Select data source scope, add tags to filter which web ACLs the rule affects. For example, add
Environment = prodto only enable logging for production web ACLs. CloudWatch will automatically enable telemetry for existing and new resources that match your tag criteria. - Under Target regions, select the Regions where you want this rule to apply. The home Region is included by default. Toggle All regions to automatically include new Regions when you opt in to them.
- Choose Next.
- Under the Specify destination screen, for Log group name pattern, enter a pattern for the log group name. The pattern must begin with
aws-waf-logs-. You can use the<resourceId>and<accountId>macros for flexibility (for example,aws-waf-logs-<resourceId>). - For Retention setting, choose the retention period for newly created log groups. If an existing log group with the same name exists, CloudWatch will not update its retention.
- (Optional) Under Assign tag to rule, add key-value pairs to help identify, organize, or search for data sources.
- Choose Next.
- Under the Select data options screen, for Redacted fields, select any data fields you want to omit from the logs (HTTP method, Query string, URI path, or Single header).
- (Optional) Under Filter logs, add filters to control which web requests are logged. If you add multiple filters, AWS WAF evaluates them starting from the top.
- Choose Next.
- Under the Review and create screen, review the configuration summary showing your scope, destination, and data options. The review page displays the data source type (AWS WAFV2), telemetry type (Logs), rule name, enablement status, source accounts, destination log group, and any redacted fields or filters.
- Choose Create rule.
- The log group created will always be prefixed with
aws-waf-logs-(this is enforced by the rule) - Rule updates only affect new web ACLs or existing web ACLs that do not already have logging enabled to CloudWatch Logs
- If you need a specific log group name, pre-create the log group before creating the enablement rule
- Rules are evaluated hierarchically: organization → OU → account. Higher-level rules provide the baseline; lower-level rules can add additional telemetry but cannot reduce it
Understanding WAF Log Structure
Each WAF log event is a JSON object containing the request details, rule evaluation outcomes, and metadata. Understanding the key fields enables effective query construction and metric filter design.
| Field | Description | Security Use |
|---|---|---|
action | ALLOW, BLOCK, CAPTCHA, CHALLENGE | Filter by enforcement outcome |
httpRequest.clientIp | Source IP of the request | Identify unauthorized source IPs |
httpRequest.country | Geo-location country code | Geo-based threat detection |
httpRequest.uri | Requested URI path | Identify targeted endpoints |
terminatingRuleId | Rule that terminated evaluation | Which rules are firing most |
terminatingRuleType | RATE_BASED, REGULAR, MANAGED_RULE_GROUP | Category of enforcement |
ruleGroupList[].terminatingRule | Specific rule within a managed group | Drill into managed rule triggers |
labels[].name | Labels applied by rules | Categorize and correlate events |
httpRequest.headers[] | Request headers (User-Agent, etc.) | Bot and tool fingerprinting |
timestamp | Event time (epoch ms) | Time-based correlation |
For the complete field reference, see Log fields for web ACL traffic.
The rule IDs used in this guide (such as SQLInjectionRule, XSSRule, GeoBlockRule, and BlockAdminPathsRule) are example placeholders. Your actual terminatingRuleId values will depend on how you named your custom rules and which managed rule groups you use. AWS Managed Rules use names like AWS-AWSManagedRulesSQLiRuleSet. Check your WAF web ACL configuration or run the Security Posture Overview query to discover the rule IDs active in your environment.
View sample WAF log event
{
"timestamp": 1719936000000,
"formatVersion": 1,
"webaclId": "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/my-webacl/abc123",
"terminatingRuleId": "SQLInjectionRule",
"terminatingRuleType": "REGULAR",
"action": "BLOCK",
"httpSourceName": "ALB",
"httpSourceId": "123456789012-app/my-alb/abc123",
"ruleGroupList": [],
"rateBasedRuleList": [],
"nonTerminatingMatchingRules": [],
"httpRequest": {
"clientIp": "203.0.113.42",
"country": "US",
"headers": [
{"name": "host", "value": "app.example.com"},
{"name": "user-agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
],
"uri": "/search?q=' OR '1'='1",
"args": "q=' OR '1'='1",
"httpVersion": "HTTP/1.1",
"httpMethod": "GET",
"requestId": "1-abc-123"
},
"labels": [{"name": "awswaf:managed:aws:sql-database:SQLi_Body"}]
}
CloudWatch Logs Insights Queries for WAF Security Analysis
CloudWatch Logs Insights enables ad-hoc investigation and forensic analysis of WAF events. Use these queries in the CloudWatch console by selecting your aws-waf-logs-* log group and setting an appropriate time range.
For production WAF log groups with high volume, narrow your time range to control query cost. Logs Insights charges per GB of data scanned. A 1-hour window is usually sufficient for active investigations.
Security Posture Overview
Get a quick snapshot of how many requests are being blocked versus allowed. This is the first query to run when you open a WAF investigation, and it tells you at a glance whether your WAF is actively blocking threats or if something has changed in your traffic pattern.
fields @timestamp, action, terminatingRuleId
| stats count(*) as requestCount by action
| sort requestCount desc
Top Blocked IPs: Unauthorized Source Identification
Identify source IPs generating the most blocked requests, potentially from unauthorized actors or compromised hosts. Use this to build IP block lists or to cross-reference with threat intelligence feeds for attribution. The country field helps identify whether attacks are concentrated geographically or distributed.
fields httpRequest.clientIp, httpRequest.country, terminatingRuleId
| filter action = "BLOCK"
| stats count(*) as blockCount by httpRequest.clientIp, httpRequest.country
| sort blockCount desc
| limit 20
SQL Injection Attack Analysis
Deep-dive into SQLi attempts to see exact payloads, source IPs, and targeted URIs. Examining the args field reveals the actual injection strings unauthorized actors are using, which helps you understand whether they are automated scanners or targeted attacks against your specific application logic.
fields @timestamp, httpRequest.clientIp, httpRequest.uri, httpRequest.args
| filter terminatingRuleId = "SQLInjectionRule"
or terminatingRuleId like /(?i)sql/
| sort @timestamp desc
| limit 50
Rule Effectiveness Analysis
Understand which rules carry the heaviest blocking load to validate rule configuration. If a single rule is responsible for the majority of blocks, it may indicate a highly effective rule, or a rule that is too broad and generating false positives that need investigation.
fields terminatingRuleId, terminatingRuleType
| filter action = "BLOCK"
| stats count(*) as triggers by terminatingRuleId, terminatingRuleType
| sort triggers desc
Managed Rule Group Drill-Down
See which specific rules within AWS Managed Rule Groups are triggering blocks. This is essential for tuning. AWS Managed Rules contain dozens of individual rules, and knowing which specific signatures fire helps you decide whether to set exceptions or adjust rule action overrides.
fields @timestamp, httpRequest.clientIp, httpRequest.uri
| parse @message '"terminatingRule":{"ruleId":"*"' as managedRuleId
| filter terminatingRuleType = "MANAGED_RULE_GROUP"
| stats count(*) as hits by managedRuleId, terminatingRuleId
| sort hits desc
Geographic Threat Analysis
Identify which countries generate the most unwanted traffic. This data informs geo-blocking decisions and helps you understand whether attacks originate from regions where you have no legitimate users, making IP reputation and geo-restriction rules lower risk to implement.
fields httpRequest.country, action
| filter action = "BLOCK"
| stats count(*) as blockedRequests by httpRequest.country
| sort blockedRequests desc
| limit 15
Rate-Limited IPs: DDoS and Brute-Force Detection
Find IPs that were rate-limited, indicating potential DDoS or brute-force activity. The firstSeen and lastSeen timestamps reveal whether rate limiting is catching short bursts or sustained campaigns, which influences whether you should escalate to permanent IP blocking.
fields @timestamp, httpRequest.clientIp, httpRequest.uri
| filter terminatingRuleType = "RATE_BASED"
| stats count(*) as rateLimited, earliest(@timestamp) as firstSeen, latest(@timestamp) as lastSeen
by httpRequest.clientIp
| sort rateLimited desc
XSS Attack Patterns
Analyze cross-site scripting attempts to identify payload patterns and targeted endpoints. XSS attacks often target forms, search fields, and user input endpoints, and correlating by HTTP method helps distinguish GET-based reflected XSS from POST-based stored XSS attempts.
fields @timestamp, httpRequest.clientIp, httpRequest.uri, httpRequest.httpMethod
| filter terminatingRuleId = "XSSRule"
or @message like /(?i)xss/
| stats count(*) as attempts by httpRequest.clientIp, httpRequest.httpMethod
| sort attempts desc
Admin Path Probing Detection
Detect reconnaissance activity, with unauthorized actors scanning for admin panels, config files, and sensitive paths. Path probing is typically an early-stage attack indicator; seeing it from a specific IP suggests the unauthorized actor is mapping your application before launching a targeted attempt.
fields @timestamp, httpRequest.clientIp, httpRequest.uri, action
| filter httpRequest.uri like /admin/
or httpRequest.uri like /wp-admin/
or httpRequest.uri like /phpmyadmin/
or httpRequest.uri like /\.env/
| stats count(*) as probeCount by httpRequest.clientIp, httpRequest.uri, action
| sort probeCount desc