Key Takeaways

  • AWS Bot Control, a managed rule group in AWS WAF, classifies and blocks automated traffic (scrapers, credential-stuffing bots) using signature and behavioral detection.
  • AWS WAF rate limiting (rate-based rules) blocks high-volume abuse — brute force, credential stuffing — by capping requests per IP (or per custom key) in a rolling window.
  • Combining Bot Control + rate-based rules + Lambda@Edge gives layered defense: known-bad patterns, volumetric abuse, and custom logic all handled before requests reach your origin.
  • Rate limits must be tuned per endpoint — a login endpoint needs a far stricter threshold than a public content API.

Why Credential Stuffing Needs More Than a Simple Rate Limit

Credential stuffing attacks replay stolen username/password pairs against your login endpoint at scale, often from thousands of rotating IPs to evade naive per-IP rate limits. A flat "100 requests per 5 minutes per IP" rule misses distributed attacks entirely. You need layered defense:

  1. AWS WAF Bot Control — flags traffic matching known bot signatures and behavioral fingerprints (headless browsers, missing JS execution, known bad user agents).
  2. Rate-based rules — cap request volume per key (IP, or a custom aggregation key like a header or cookie).
  3. Lambda@Edge — custom logic AWS WAF can't express natively (e.g., blocking based on combined signals, device fingerprint cross-checks).

Step 1: Enable AWS WAF Bot Control

Bot Control ships as a managed rule group with two inspection levels: Common (free-tier-friendly signature detection) and Targeted (behavioral ML-based detection, higher cost, better accuracy for login/checkout endpoints).

{
  "Name": "AWS-AWSManagedRulesBotControlRuleSet",
  "Priority": 1,
  "Statement": {
    "ManagedRuleGroupStatement": {
      "VendorName": "AWS",
      "Name": "AWSManagedRulesBotControlRuleSet",
      "ManagedRuleGroupConfigs": [
        {
          "AWSManagedRulesBotControlRuleSet": {
            "InspectionLevel": "TARGETED"
          }
        }
      ]
    }
  },
  "OverrideAction": { "None": {} },
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "BotControlTargeted"
  }
}
aws wafv2 update-web-acl \
  --name api-protection-acl \
  --scope CLOUDFRONT \
  --id abcd-1234-efgh-5678 \
  --lock-token xyz-token \
  --rules file://bot-control-rule.json

Step 2: Add AWS WAF Rate Limiting for the Login Endpoint

Rate-based rules track a rolling 5-minute window per aggregation key. For credential-stuffing defense, aggregate by IP and layer in a stricter rule scoped to the login path specifically.

{
  "Name": "LoginRateLimit",
  "Priority": 2,
  "Statement": {
    "RateBasedStatement": {
      "Limit": 50,
      "AggregateKeyType": "IP",
      "ScopeDownStatement": {
        "ByteMatchStatement": {
          "SearchString": "/api/login",
          "FieldToMatch": { "UriPath": {} },
          "TextTransformations": [{ "Priority": 0, "Type": "LOWERCASE" }],
          "PositionalConstraint": "STARTS_WITH"
        }
      }
    }
  },
  "Action": { "Block": {} },
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "LoginRateLimit"
  }
}

This caps any single IP to 50 requests to /api/login per 5-minute window — far stricter than the general API rate limit, because login is the highest-value target.

Step 3: Lambda@Edge for Custom Signal Correlation

AWS WAF rules alone can't easily correlate "this IP passed Bot Control but failed a device-fingerprint check performed by your frontend." Lambda@Edge, running at viewer-request, fills that gap.

// Lambda@Edge: viewer-request trigger
exports.handler = async (event) => {
  const request = event.Records[0].cf.request;
  const headers = request.headers;

  const fingerprint = headers['x-device-fingerprint']?.[0]?.value;
  const suspiciousAgents = ['python-requests', 'curl', 'headlesschrome'];
  const ua = (headers['user-agent']?.[0]?.value || '').toLowerCase();

  if (!fingerprint && suspiciousAgents.some(a => ua.includes(a))) {
    return {
      status: '403',
      statusDescription: 'Forbidden',
      body: 'Request blocked by edge security policy',
    };
  }

  return request;
};

Deploy and associate it with your CloudFront distribution's viewer-request event, so it runs before the request even reaches your WAF-protected origin.

Layered Defense Architecture

<svg viewBox="0 0 760 320" xmlns="http://www.w3.org/2000/svg" style="width:100%;max-width:760px;font-family:sans-serif;color:currentColor">
  <style>
    .box { fill: none; stroke: currentColor; stroke-width: 1.5; }
    .lbl { fill: currentColor; font-size: 13px; }
    .sub { fill: currentColor; font-size: 11px; opacity: 0.7; }
    .arrow { stroke: currentColor; stroke-width: 1.5; marker-end: url(#a5); fill: none; }
  </style>
  <defs>
    <marker id="a5" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
      <path d="M0,0 L6,3 L0,6 Z" fill="currentColor" />
    </marker>
  </defs>

  <rect class="box" x="20" y="130" width="120" height="60" rx="6" />
  <text class="lbl" x="35" y="155">Client</text>
  <text class="sub" x="35" y="173">incl. bots</text>

  <rect class="box" x="190" y="130" width="160" height="60" rx="6" />
  <text class="lbl" x="205" y="155">CloudFront +</text>
  <text class="sub" x="205" y="173">Lambda@Edge</text>

  <rect class="box" x="400" y="60" width="160" height="60" rx="6" />
  <text class="lbl" x="415" y="85">Bot Control</text>
  <text class="sub" x="415" y="103">Targeted inspection</text>

  <rect class="box" x="400" y="140" width="160" height="60" rx="6" />
  <text class="lbl" x="415" y="165">Rate-Based Rule</text>
  <text class="sub" x="415" y="183">/api/login: 50 req/5min</text>

  <rect class="box" x="400" y="220" width="160" height="60" rx="6" />
  <text class="lbl" x="415" y="245">General WAF Rules</text>
  <text class="sub" x="415" y="263">SQLi, XSS, managed sets</text>

  <rect class="box" x="620" y="130" width="120" height="60" rx="6" />
  <text class="lbl" x="635" y="155">Origin API</text>

  <path class="arrow" d="M140,160 H190" />
  <path class="arrow" d="M350,150 Q375,120 400,90" />
  <path class="arrow" d="M350,160 H400" />
  <path class="arrow" d="M350,170 Q375,220 400,250" />
  <path class="arrow" d="M560,90 Q600,120 620,150" />
  <path class="arrow" d="M560,170 H620" />
  <path class="arrow" d="M560,250 Q600,200 620,170" />
</svg>

Tuning Rate Limits Per Endpoint

A single global rate limit is a mistake — endpoints have very different legitimate traffic patterns:

Endpoint typeTypical legitimate rateSuggested WAF limit
Login / authVery low per user30–50 req / 5 min per IP
Password resetExtremely low5–10 req / 5 min per IP
Public read APIHigh, bursty2,000+ req / 5 min per IP
Search endpointMedium, bursty300–500 req / 5 min per IP

FAQ

Does AWS WAF Bot Control stop all credential stuffing on its own? Not reliably alone — sophisticated attacks rotate IPs and mimic browser behavior. Combine it with endpoint-specific rate-based rules and, where needed, Lambda@Edge custom checks.

What's the difference between Common and Targeted inspection levels in Bot Control? Common uses signature-based detection (lower cost, catches known bad bots). Targeted adds machine-learning behavioral analysis, better at catching sophisticated bots mimicking humans, at a higher per-request cost.

Can rate-based rules block legitimate users behind a shared NAT or corporate proxy? Yes, if the aggregation key is plain IP. Use a custom aggregation key (e.g., a session cookie or header) for environments with many users sharing one egress IP.

Is Lambda@Edge necessary, or can AWS WAF managed rules do everything? Managed rules and rate-based statements cover most cases. Lambda@Edge is for custom correlation logic (e.g., cross-checking a client-side fingerprint) that WAF's rule language can't express.

Harden Your API Against Bots and Credential Stuffing

Layering Bot Control, rate limiting, and Lambda@Edge correctly takes careful tuning per endpoint. Talk to f9infotech's AWS security team about an API protection design built for your traffic patterns.