Server

AWS WAF: The Rules That Matter

By Prabath Thalangama· October 24, 2026· 4 min read
#aws#waf#security

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 CLOUDFRONT and must be created in us-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.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.