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:
- AWS WAF Bot Control — flags traffic matching known bot signatures and behavioral fingerprints (headless browsers, missing JS execution, known bad user agents).
- Rate-based rules — cap request volume per key (IP, or a custom aggregation key like a header or cookie).
- 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 type | Typical legitimate rate | Suggested WAF limit |
|---|---|---|
| Login / auth | Very low per user | 30–50 req / 5 min per IP |
| Password reset | Extremely low | 5–10 req / 5 min per IP |
| Public read API | High, bursty | 2,000+ req / 5 min per IP |
| Search endpoint | Medium, bursty | 300–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.