AWS WAF: The Rules That Matter
Introduction¶
AWS WAF inspects HTTP requests before they reach your ALB, CloudFront distribution, API Gateway, or AppSync, and allows/blocks/counts them per rule. It's not a substitute for fixing your app, but it blunts common attacks and absorbs abusive traffic.
Structure¶
- Web ACL — attached to a resource (ALB / CloudFront / API GW). Contains an ordered list of rules, evaluated top to bottom; first terminating action (Allow/Block) wins. Has a default action (Allow, usually).
- Rule — a statement (match condition) + action (Allow / Block / Count / CAPTCHA / Challenge) + priority.
- Rule group — a reusable bundle of rules. AWS-managed ones, marketplace ones, or your own.
A baseline Web ACL¶
Priorities low → evaluated first:
10 AWSManagedRulesAmazonIpReputationList (block) - known bad IPs
20 AWSManagedRulesCommonRuleSet (CRS) (block, with some rules in count) - broad OWASP-ish
30 AWSManagedRulesKnownBadInputsRuleSet (block) - exploit strings, Log4j, etc.
40 AWSManagedRulesSQLiRuleSet (block) - SQL injection patterns
50 RateLimit-PerIP (block) - rate-based, 2000 req / 5 min per IP
60 (optional) AWSManagedRulesLinuxRuleSet / PHPRuleSet / WordPressRuleSet - if relevant
Add app-specific rules above the managed ones:
- Allow-list your own health-check / monitoring IPs so they're never rate-limited.
- Block or CAPTCHA specific abused paths.
- Geo-block regions you don't serve (
GeoMatchStatement), or geo-allow only the regions you do.
Rate-based rules¶
Statement: RateBasedStatement
Limit: 2000 (requests per 5-minute sliding window)
AggregateKeyType: IP (or FORWARDED_IP behind a CDN, or a custom key)
ScopeDownStatement: (optional — only rate-limit /login, or only unauthenticated)
The window is fixed at 5 minutes; the limit is the count in that window. Use a
ScopeDownStatement to only rate-limit the paths that need it (login, search,
API) rather than the whole site.
Tune with Count mode¶
Never deploy managed rule groups straight to Block. Deploy with the group (or its noisy rules) in Count mode, enable logging, and watch for a week:
- Which rules are matching legitimate traffic (false positives)?
- Override just those rule actions to Count (or exclude them) within the group.
- Then flip the group to Block.
CRS rules that commonly false-positive: SizeRestrictions_BODY (large uploads,
file endpoints), CrossSiteScripting_BODY / _QUERYARGUMENTS (rich text
editors, markdown, code snippets in forms), GenericRFI_* (URLs in query
params). Exclude these for the specific paths that need them, not globally.
Logging¶
Web ACL logging -> Kinesis Firehose -> S3 (and/or CloudWatch Logs / an S3 bucket directly)
Each log line has the request, the matching rule, and the action. Query with Athena:
SELECT terminatingruleid, httprequest.uri, count(*) c
FROM waf_logs
WHERE action = 'BLOCK' AND date = '2026-08-27'
GROUP BY 1, 2 ORDER BY c DESC LIMIT 50;
This is how you find both real attacks and your own blocked users.
CAPTCHA and Challenge¶
- Challenge — a silent browser check (JS + proof of work); bots without a real browser fail. Good default for "probably a bot" rules — no user friction.
- CAPTCHA — an actual puzzle; use for high-value abused endpoints (signup, checkout) when Challenge isn't enough.
Verification and troubleshooting¶
aws wafv2 get-web-acl --scope REGIONAL --name my-acl --id ...
aws wafv2 get-sampled-requests --web-acl-arn ... --rule-metric-name RateLimit-PerIP \
--scope REGIONAL --time-window StartTime=...,EndTime=... --max-items 100
# CloudWatch: per-rule AllowedRequests / BlockedRequests / CountedRequests metrics
- Legit users blocked — a managed rule false-positive. WAF logs
terminatingRuleId; override that specific rule to Count, or add a scope-down/exclusion for the affected path. Don't disable the whole group. - WAF not blocking anything — not associated with the resource, rules in
Count mode, default action Allow with no Block rules matching, or (CloudFront)
the scope is
CLOUDFRONTand must be created inus-east-1. - Rate limit not triggering during an obvious flood — the limit is per-IP
and the attack is distributed (many IPs). Use a lower limit,
FORWARDED_IP(behind CloudFront), a custom aggregate key (a header, cookie), or AWS Shield Advanced for volumetric DDoS. - Rate limit blocking a shared NAT / office — many users behind one IP. Exempt known corporate ranges, or key on something more specific than IP.
- Blocked requests still hitting the origin — WAF is on CloudFront but the ALB is publicly reachable directly. Restrict the ALB SG to CloudFront's prefix list, or put WAF on the ALB too.
- Cost — per Web ACL + per rule + per million requests + per managed rule group. A stack of managed groups on a high-traffic site adds up; keep the set lean.
Related tools and reading¶
- On-site: HTTP Header Analyzer, Security Headers Checker.
- Related posts: Rate limiting in Nginx, GuardDuty and Security Hub, explained.
Stuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.More in Cloud & AWS
An S3 Static Site Behind CloudFront
The right way to host a static site on AWS: a private S3 bucket, CloudFront with OAC in front, HTTPS via ACM, and SPA fallback routing. Plus the 403s that trip everyone.
October 24, 2026CloudWatch Logs and Metric Filters
CloudWatch Logs is cheap to write and expensive to forget about. Here's retention, turning error patterns into metrics and alarms, and Logs Insights for ad-hoc queries.
October 24, 2026ALB vs NLB (and When to Use Each)
ALB is an L7 HTTP router with rich features; NLB is an L4 pass-through with static IPs and raw throughput. Here's the decision, feature by feature.
October 24, 2026