← Back to blog
Cloud / AWSMar 28, 2026· 9 min

AWS WAF and Shield: Real Application Protection Beyond Default Rules

Default managed rules and Shield Standard are table stakes, not a strategy. Here is how to design a WAF web ACL that actually blocks attacks without breaking your app or your budget.

AWS WAF and Shield: Real Application Protection Beyond Default Rules

Most teams enable AWS WAF the same way: attach the AWSManagedRulesCommonRuleSet to a web ACL, flip everything to block, and declare the application protected. Weeks later they are either fielding tickets because a legitimate POST whose JSON body contains the word "select" got blocked, or they are breached anyway because the managed rules never inspected the parameter the attacker actually abused. WAF is a powerful traffic-inspection layer, but it rewards deliberate design and punishes the default-everything approach. This article is about the design.

Web ACL design: managed groups, custom rules, and count mode

A web ACL is an ordered list of rules evaluated by priority until a terminating action fires. AWS-managed rule groups give you broad, maintained coverage (CommonRuleSet, KnownBadInputsRuleSet, SQLiRuleSet, AmazonIpReputationList, plus platform-specific sets) and consume WCUs from your 1,500-WCU default budget, so you cannot stack everything; custom rules then encode what is true about your application: this URI only accepts GET, that header is required, this endpoint should never see foreign traffic. Managed groups handle the generic internet, custom rules handle your app. The single most important habit: never deploy a new rule group straight to block. Add it in count mode, ship it, and read the logs for days first. The override below keeps the group blocking but demotes only the two noisy rules: OverrideAction None preserves each rule's own action while RuleActionOverrides surgically drops the named ones to count, the precise alternative to disabling a group wholesale.

{
  "Name": "AWS-CommonRuleSet",
  "Priority": 10,
  "Statement": {
    "ManagedRuleGroupStatement": {
      "VendorName": "AWS",
      "Name": "AWSManagedRulesCommonRuleSet",
      "RuleActionOverrides": [
        { "Name": "SizeRestrictions_BODY", "ActionToUse": { "Count": {} } },
        { "Name": "CrossSiteScripting_BODY", "ActionToUse": { "Count": {} } }
      ]
    }
  },
  "OverrideAction": { "None": {} },
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "AWS-CommonRuleSet"
  }
}

Rate-based rules, geo, and IP sets

A rate-based rule tracks request counts per aggregation key over a rolling window (60, 120, 300, or 600 seconds) and blocks keys that exceed your threshold. The default key is source IP, but you can aggregate on a header, cookie, query argument, JA3/JA4 TLS fingerprint, or a composite; aggregating on something more specific than IP is what makes rate limiting survive shared NATs and botnets that rotate addresses. Pair the rate statement with a scope-down so it only counts traffic to the endpoint you care about, such as /login, rather than rate-limiting your whole site on one global counter, as the rule below does. Geo matching and IP sets are blunt but useful: reference an IPSet for partner allowlists or known-bad ranges, and prefer geo as a scope-down condition (lock the admin console to your operating regions) over a blanket country block that a VPN trivially defeats. JSON body inspection deserves the same care: use a JsonBody FieldToMatch so WAF parses the structure and inspects specific keys with an explicit fallback for malformed or oversized bodies, rather than a fragile match against the raw body.

{
  "Name": "RateLimit-Login",
  "Priority": 5,
  "Statement": {
    "RateBasedStatement": {
      "Limit": 200,
      "EvaluationWindowSec": 300,
      "AggregateKeyType": "IP",
      "ScopeDownStatement": {
        "ByteMatchStatement": {
          "SearchString": "/login",
          "FieldToMatch": { "UriPath": {} },
          "TextTransformations": [ { "Priority": 0, "Type": "LOWERCASE" } ],
          "PositionalConstraint": "STARTS_WITH"
        }
      }
    }
  },
  "Action": { "Block": {} },
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "RateLimit-Login"
  }
}

The most powerful WAF pattern is the label: write a rule that does not block but attaches a label when it sees something interesting (an admin role in a JSON body, say), then have a later rule decide what to do based on that label combined with other conditions. Labels turn WAF from a pile of independent rules into a small decision pipeline, and they are also how you tune false positives. AWS managed rules emit labels such as awswaf:managed:aws:sql-database:SQLi_Body, so when a managed rule fires on legitimate traffic you block on its label except where the request matches the one endpoint that genuinely needs SQL-looking content, tuned narrowly by path and method rather than by switching the rule off. AWS WAF Bot Control adds a managed group on top, classifying verified bots, crawlers, and suspected automation; its Targeted tier adds challenge and CAPTCHA actions (non-terminating, far friendlier to real users than a hard block) plus client telemetry. It carries a per-request cost, so scope it to the routes that actually face scraping or credential stuffing. The labeling rule below inspects a specific JSON key and emits a label instead of blocking.

{
  "Name": "Flag-AdminJsonRole",
  "Priority": 20,
  "Statement": {
    "ByteMatchStatement": {
      "SearchString": "admin",
      "FieldToMatch": {
        "JsonBody": {
          "MatchPattern": { "IncludedPaths": [ "/role" ] },
          "MatchScope": "VALUE",
          "InvalidFallbackBehavior": "MATCH"
        }
      },
      "TextTransformations": [ { "Priority": 0, "Type": "LOWERCASE" } ],
      "PositionalConstraint": "CONTAINS"
    }
  },
  "Action": { "Count": {} },
  "RuleLabels": [ { "Name": "shieldsync:body:role-admin" } ],
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "Flag-AdminJsonRole"
  }
}

The hidden cost of running in block mode blindly is not the WAF bill, it is the incident you cause yourself. A single over-eager managed rule blocking a legitimate checkout flow is a self-inflicted outage. Count mode plus logging is not optional caution; it is the only way to know what block mode will do to your traffic before it does it in production.

Shield Standard versus Advanced, and logging the attack

Shield Standard is automatic, free, and always on across CloudFront, Global Accelerator, Route 53, and the AWS edge, defending the common L3/L4 volumetric and state-exhaustion attacks with no configuration; for most apps it is enough at the network layer while your real L7 work happens in WAF. Shield Advanced is a paid subscription (1-year commitment, roughly 3,000 USD per month plus data transfer) and a different product class: 24/7 Shield Response Team access, cost-protection credits that refund attack-driven scaling charges, near-real-time diagnostics, automatic application-layer DDoS mitigation, and centralized management via Firewall Manager. It is worth it when an hour of downtime costs more than the subscription, when you carry a contractual uptime obligation, or when you have already been targeted; for an internal tool, Standard plus a tuned WAF is the honest answer. Underneath all of it, enable full web ACL logging to S3, CloudWatch Logs, or Firehose with redaction on authorization headers. The logs carry the matched rule, the labels, the terminating action, and a request sample, exactly what you need to tune count-mode rules and reconstruct an attack (Athena over S3 is the common query path). You cannot tune what you cannot see. And remember WAF is defense-in-depth, not a fix: a rule that blocks a SQL-injection string buys you time and noise reduction but does not patch the unparameterized query underneath, so route every WAF block to the engineers who own that code as a signal that an exploitable path exists. Build the ACL as a deliberate pipeline (rate and IP rules first, tuned managed groups next, custom labeling and scope-down to encode what your app does, Bot Control where automation is the threat, logging beneath all of it), start in count mode, promote to block only what you have verified. The web ACL is the outer wall, not the lock on the door.

Learn it by doing

Spin up a real AWS security lab, or explore our training tracks.

24 people viewing now