AWS WAF and Shield

By Vishvesh Patel · DevOps EngineerUpdated September 16, 2026

Application-layer protection that does not block your users. Count mode before block, rate limiting, managed rule false positives and Shield Advanced.

Two different products at two different layers.

Shield Standard is automatic, free, and defends every AWS account against network and transport layer attacks. SYN floods, reflection attacks, volumetric traffic. You are already protected and there is nothing to configure.

WAF inspects HTTP requests and decides what to allow, block, count or challenge. It handles the application layer: SQL injection, cross-site scripting, bad bots, and the request floods that look like legitimate traffic.

Shield Advanced is a paid subscription adding response support, cost protection and enhanced detection.

The failure mode that matters is not being attacked. It is blocking your own users, which is why count mode below is the most important section on this page.

A web ACL

aws wafv2 create-web-acl --name prod-acl --scope REGIONAL \
  --region eu-west-1 \
  --default-action Allow={} \
  --visibility-config 'SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=prodAcl' \
  --rules file://rules.json

--scope REGIONAL for an ALB, API Gateway or AppSync. --scope CLOUDFRONT for CloudFront, and that one must be created in us-east-1, the same rule as CloudFront certificates.

aws wafv2 associate-web-acl --web-acl-arn "$ACL_ARN" \
  --resource-arn "$ALB_ARN" --scope REGIONAL --region eu-west-1

A web ACL that exists but is associated with nothing inspects nothing. That is an easy state to leave things in after a Terraform apply that created the ACL and not the association.

Rules evaluate in priority order

[
  {
    "Name": "AllowHealthChecks",
    "Priority": 0,
    "Statement": { "IPSetReferenceStatement": { "ARN": "arn:aws:wafv2:...:ipset/monitoring" } },
    "Action": { "Allow": {} },
    "VisibilityConfig": { "SampledRequestsEnabled": true, "CloudWatchMetricsEnabled": true, "MetricName": "AllowHealthChecks" }
  },
  {
    "Name": "RateLimit",
    "Priority": 10,
    "Statement": {
      "RateBasedStatement": { "Limit": 2000, "AggregateKeyType": "IP" }
    },
    "Action": { "Block": {} },
    "VisibilityConfig": { "SampledRequestsEnabled": true, "CloudWatchMetricsEnabled": true, "MetricName": "RateLimit" }
  },
  {
    "Name": "CommonRuleSet",
    "Priority": 20,
    "OverrideAction": { "None": {} },
    "Statement": {
      "ManagedRuleGroupStatement": {
        "VendorName": "AWS",
        "Name": "AWSManagedRulesCommonRuleSet",
        "RuleActionOverrides": [
          { "Name": "SizeRestrictions_BODY", "ActionToUse": { "Count": {} } }
        ]
      }
    },
    "VisibilityConfig": { "SampledRequestsEnabled": true, "CloudWatchMetricsEnabled": true, "MetricName": "CommonRuleSet" }
  }
]

Lowest priority number first, and evaluation stops at the first terminating action. An Allow at priority 0 for your monitoring IPs means nothing below can block them, which is what you want, and equally what makes a careless broad Allow dangerous, since it skips every rule beneath it.

Put explicit allows for known-good traffic first, then rate limiting, then managed rule groups, then your own custom rules.

Count mode first. Always.

Deploying a managed rule group straight to Block is how you take your own site down.

AWSManagedRulesCommonRuleSet blocks request bodies over 8 KB by default, so a file upload, a large JSON payload or a long form submission is rejected with a 403 that looks nothing like a WAF problem. SQLiRule matches on patterns that legitimate content contains: a blog post about SQL, a password containing --, a search for O'Brien.

"OverrideAction": { "Count": {} }

Set the whole group to count, leave it for a week, and look at what it would have blocked:

aws logs start-query \
  --log-group-name aws-waf-logs-prod \
  --start-time "$(date -d '7 days ago' +%s)" --end-time "$(date +%s)" \
  --query-string 'fields @timestamp, httpRequest.uri, terminatingRuleId
                  | filter action = "COUNT"
                  | stats count() by terminatingRuleId'

Then switch the group to block and override the individual rules that produced false positives, as SizeRestrictions_BODY is overridden above. Overriding one rule is far better than disabling the group.

Rate limiting

The rule that stops most real problems. Credential stuffing, scraping, accidental client loops:

{
  "Name": "LoginRateLimit",
  "Priority": 5,
  "Statement": {
    "RateBasedStatement": {
      "Limit": 100,
      "EvaluationWindowSec": 300,
      "AggregateKeyType": "IP",
      "ScopeDownStatement": {
        "ByteMatchStatement": {
          "SearchString": "/api/login",
          "FieldToMatch": { "UriPath": {} },
          "TextTransformations": [{ "Priority": 0, "Type": "LOWERCASE" }],
          "PositionalConstraint": "STARTS_WITH"
        }
      }
    }
  },
  "Action": { "Block": {} }
}

The ScopeDownStatement is what makes this usable: a strict limit applies only to the login endpoint while normal browsing is untouched. A single global rate limit either allows too much on sensitive endpoints or blocks legitimate heavy users elsewhere.

Aggregating by IP is crude when clients sit behind corporate NAT or a mobile carrier. AggregateKeyType can instead use a header, a cookie or a JA3 fingerprint, which handles that far better.

Logging

aws wafv2 put-logging-configuration --logging-configuration '{
  "ResourceArn": "'"$ACL_ARN"'",
  "LogDestinationConfigs": ["arn:aws:logs:eu-west-1:123456789012:log-group:aws-waf-logs-prod"],
  "RedactedFields": [{"SingleHeader": {"Name": "authorization"}}]
}'

The log group name must begin with aws-waf-logs-, which is an easy thing to get wrong and produces a validation error rather than silence.

Redact sensitive headers. Without it, authorization and cookie values land in your logs, which turns a debugging aid into a credential store.

Without logging you cannot tune rules, cannot investigate a complaint, and cannot tell whether a 403 came from WAF or your application. Enable it before you enable blocking.

What Shield Advanced actually buys

At roughly $3,000 a month with a one-year commitment, plus data transfer, the question is what you get beyond the free Shield Standard.

Cost protection. If an attack scales your infrastructure, Auto Scaling launching instances, CloudFront serving attack traffic, AWS credits the charges. On a large estate this alone can justify it.

The Shield Response Team. Direct access during an attack, and with proactive engagement enabled they contact you rather than waiting.

Health-based detection. Attaching a Route 53 health check lets Shield distinguish an attack from a traffic spike, which reduces false positives significantly.

Free WAF. WAF charges are included, which offsets part of the cost for a heavy user.

For most organisations Shield Standard plus a well-tuned WAF is the right answer. Shield Advanced makes sense when downtime is measured in large sums per hour, when you are a plausible target, or when the cost protection covers the subscription on its own.

Bot Control and account takeover

{
  "Name": "BotControl",
  "Priority": 30,
  "Statement": {
    "ManagedRuleGroupStatement": {
      "VendorName": "AWS",
      "Name": "AWSManagedRulesBotControlRuleSet",
      "ManagedRuleGroupConfigs": [{ "AWSManagedRulesBotControlRuleSet": { "InspectionLevel": "COMMON" } }]
    }
  }
}

Bot Control identifies known bots and distinguishes verified crawlers from impersonators. It costs meaningfully more than standard WAF rules and is priced per request, so scope it down to the paths that need it.

AWSManagedRulesATPRuleSet targets credential stuffing specifically, inspecting login requests for stolen credentials and unusual patterns. Both are worth count mode before blocking.

Common errors

WAFInvalidParameterException: Error reason: The ARN isn't valid.
A valid ARN begins with arn: and includes other information separated by colons or slashes.

Usually a CLOUDFRONT scope ACL created outside us-east-1, or a regional ACL being associated with a CloudFront distribution.

WAFLimitsExceededException

A web ACL has a capacity budget in WCUs, and managed rule groups consume a lot. Remove unused groups or request an increase.

WAFNonexistentItemException

The resource is in a different region, or the ACL was never associated.

Legitimate users getting 403. A managed rule false positive. Check the WAF logs for terminatingRuleId before anything else.

Quick reference

TaskCommand
Create an ACLaws wafv2 create-web-acl --scope REGIONAL
Associate itaws wafv2 associate-web-acl --resource-arn <arn>
List managed groupsaws wafv2 list-available-managed-rule-groups --scope REGIONAL
Enable loggingaws wafv2 put-logging-configuration
Sample blocked requestsaws wafv2 get-sampled-requests --rule-metric-name <m>
Update rulesaws wafv2 update-web-acl --lock-token <t>
Check capacityaws wafv2 describe-managed-rule-group

Key takeaways

  • Shield Standard is free and already on. It covers layers 3 and 4.
  • A CloudFront-scope web ACL must be created in us-east-1.
  • An unassociated web ACL protects nothing.
  • Rules evaluate lowest priority first and stop at the first terminating action.
  • Always deploy managed rule groups in count mode first. Blocking your own users is the real risk.
  • Override individual rules, do not disable whole groups.
  • Scope down rate limits so login endpoints get strict limits and browsing does not.
  • Log group names must start with aws-waf-logs-, and redact authorization and cookie.
  • Shield Advanced is mainly cost protection and the response team.

Check your understanding

Exercise

Uploads broken by a new WAF

After enabling AWSManagedRulesCommonRuleSet in block mode, file uploads started failing with 403 while everything else works. What is the cause and the right fix?

Exercise

A rate limit that hurts real users

A single rate-based rule of 2,000 requests per five minutes per IP is applied to the whole site. Credential stuffing on /api/login continues, while some corporate customers behind NAT are being blocked. What should change?


Frequently Asked Questions

What is the difference between AWS WAF and AWS Shield?

They operate at different layers. Shield defends against network and transport attacks, SYN floods, UDP reflection, volumetric traffic, and Shield Standard is automatic and free on every account. WAF inspects HTTP requests and enforces rules about their content: SQL injection patterns, cross-site scripting, bot signatures, request rates. Shield Standard handles the traffic that never reaches your application; WAF handles the requests that do. Shield Advanced is a paid tier adding cost protection, response team access and health-based detection.

Why is my WAF blocking legitimate traffic?

Almost certainly a managed rule group deployed straight to block. AWSManagedRulesCommonRuleSet rejects request bodies over 8 KB, which breaks uploads, and its injection rules match patterns that ordinary content contains: an apostrophe in a name, a double dash in a password, a blog post discussing SQL. Check the WAF logs for terminatingRuleId to identify which rule fired, then use RuleActionOverrides to set just that rule to Count. Deploy new groups in count mode for a week before blocking.

What is WAF count mode and why does it matter?

Count mode evaluates rules and records matches without blocking anything, so you can see exactly what a rule would have rejected before it affects users. It is the single most important WAF practice, because the realistic risk is not an attacker getting through. It is a managed rule quietly returning 403 to your own customers while your application logs show nothing. Run new rule groups in count for a week, query the logs for what matched, override the false positives, then switch to block.

How do I rate limit only my login endpoint?

Add a ScopeDownStatement to the rate-based rule, matching the URI path, so the limit applies only to requests to that path. A strict limit of around 100 requests per five minutes per IP is reasonable on /api/login and would be absurd applied site-wide. For clients sharing an address through corporate NAT or mobile carriers, change AggregateKeyType from IP to a header, cookie or JA3 fingerprint so one shared address does not lock out an entire office.

Is Shield Advanced worth the cost?

At roughly $3,000 a month on an annual commitment, it depends on what an hour of downtime costs you. The strongest argument is cost protection, if an attack drives Auto Scaling or CloudFront charges up, AWS credits them, which on a large estate can exceed the subscription. Beyond that you get the Shield Response Team during an incident, proactive engagement, health-based detection that distinguishes attacks from traffic spikes, and WAF charges included. For most organisations, Shield Standard plus a well-tuned WAF is sufficient.

Why does my WAF web ACL not seem to do anything?

Three things to check. It may never have been associated with a resource. Creating a web ACL and associating it are separate operations, and infrastructure code frequently does the first without the second. Its scope may be wrong: CLOUDFRONT ACLs must be created in us-east-1, and a REGIONAL ACL cannot protect a distribution. Or the rules may be in count mode, recording matches without blocking. aws wafv2 get-web-acl shows the rules and actions, and the CloudWatch metrics show whether it is evaluating anything at all.

Continue Learning

Explore Related Topics

When it breaks

Related Resources