Guide · AWS WAF
AWS WAF Bot Control for MCP Servers
AWS WAF Bot Control uses browser fingerprinting, TLS fingerprinting, and behavioral signals to classify requests as human or bot, then lets you apply CAPTCHA challenges, silent JavaScript challenges, or BLOCK actions. The fundamental problem for MCP servers: your legitimate callers are bots. Claude, GPT, and other agent frameworks call your MCP server programmatically — they have no browser, cannot complete a CAPTCHA, and don't run JavaScript. Applying Bot Control to MCP API paths without careful scoping will block every legitimate user of your server. The correct pattern is to scope Bot Control exclusively to browser-facing paths (landing page, OAuth flows, status dashboard) and protect the MCP API paths with rate-based rules instead. If your MCP server has both a browser UI and an API, use scope-down statements and label-match rules to separate the two.
TL;DR
Do NOT apply Bot Control with CAPTCHA or Challenge action to /mcp* or /api/* paths — it will block all API-based MCP clients (Claude, GPT, agent frameworks). Scope Bot Control to browser-facing paths only (/, /auth/*, /dashboard/*). Use label-match rules to log bot signals on API paths without blocking. Protect API paths with rate-based rules and IP reputation list instead.
CAPTCHA vs Challenge: what each action does
AWS WAF provides two distinct bot-mitigation actions. Understanding the difference is essential before applying either to MCP-adjacent paths.
| Action | Mechanism | Client requirement | Works for API clients? |
|---|---|---|---|
| CAPTCHA | Returns a visual CAPTCHA puzzle (image recognition, checkbox) embedded in an AWS-hosted page; on pass, sets a WAF token cookie valid for 5 minutes | JavaScript-capable browser; human can see and interact with the puzzle | No — API clients cannot solve visual puzzles |
| Challenge | Returns a JavaScript challenge (browser fingerprinting, proof-of-work); runs silently in background; on pass, sets a WAF token cookie valid for 300 seconds | JavaScript-capable browser; user sees nothing but experiences a brief delay | No — API clients do not run JavaScript |
| Block | Returns HTTP 403 (or custom response code) immediately | None — client just receives the rejection | Yes — returns a deterministic error that API clients can handle |
| Count | Logs the bot signal without taking any action; request continues normally | None | Yes — safe for API paths; use for audit logging of bot signals |
Token lifecycle: When a browser completes a CAPTCHA or Challenge, WAF sets an aws-waf-token cookie. Subsequent requests from the same browser include this cookie and WAF exempts them from the challenge for the token's validity period. API clients using Bearer tokens or API keys have no mechanism to acquire or send this cookie.
Correct scope-down: protect browser paths, not API paths
Use a scope-down statement in the Bot Control rule to restrict which requests it inspects. Scope it to browser-facing paths and explicitly exclude MCP API paths.
// CDK: Bot Control scoped to browser paths only — does NOT inspect /mcp/* or /api/*
import * as wafv2 from "aws-cdk-lib/aws-wafv2";
const botControlBrowserOnly: wafv2.CfnWebACL.RuleProperty = {
name: "BotControlBrowserPaths",
priority: 15,
overrideAction: { none: {} },
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: "BotControlBrowserPaths",
sampledRequestsEnabled: true,
},
statement: {
managedRuleGroupStatement: {
vendorName: "AWS",
name: "AWSManagedRulesBotControlRuleSet",
managedRuleGroupConfigs: [
{
awsManagedRulesBotControlRuleSet: {
inspectionLevel: "COMMON", // COMMON: free; TARGETED: $10/mo + $1/1M req
},
},
],
// Scope-down: only inspect requests to browser-facing paths
scopeDownStatement: {
andStatement: {
statements: [
// NOT /mcp paths (these are API calls from agent frameworks)
{
notStatement: {
statement: {
byteMatchStatement: {
fieldToMatch: { uriPath: {} },
searchString: "/mcp",
positionalConstraint: "STARTS_WITH",
textTransformations: [{ priority: 0, type: "LOWERCASE" }],
},
},
},
},
// NOT /api paths
{
notStatement: {
statement: {
byteMatchStatement: {
fieldToMatch: { uriPath: {} },
searchString: "/api/",
positionalConstraint: "STARTS_WITH",
textTransformations: [{ priority: 0, type: "LOWERCASE" }],
},
},
},
},
],
},
},
},
},
};
Allowlisting known MCP client user-agents
If Bot Control must inspect paths that might be called by both browsers and MCP clients, add an allow rule at a higher priority (lower number) that exempts requests from known MCP client user-agent prefixes before Bot Control evaluates them.
// CDK: Allowlist known AI agent user-agents before Bot Control inspects
const allowKnownMcpClients: wafv2.CfnWebACL.RuleProperty = {
name: "AllowKnownMcpClients",
priority: 14, // Must be lower number than Bot Control priority
action: { allow: {} }, // Allow terminates evaluation — Bot Control never runs
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: "AllowedMcpClients",
sampledRequestsEnabled: true,
},
statement: {
orStatement: {
statements: [
// Claude / Anthropic
{
byteMatchStatement: {
fieldToMatch: { singleHeader: { name: "user-agent" } },
searchString: "Claude-",
positionalConstraint: "STARTS_WITH",
textTransformations: [{ priority: 0, type: "NONE" }],
},
},
// OpenAI ChatGPT plugins / tools
{
byteMatchStatement: {
fieldToMatch: { singleHeader: { name: "user-agent" } },
searchString: "ChatGPT-User",
positionalConstraint: "STARTS_WITH",
textTransformations: [{ priority: 0, type: "NONE" }],
},
},
// MCP SDK standard user-agent (modelcontextprotocol/sdk)
{
byteMatchStatement: {
fieldToMatch: { singleHeader: { name: "user-agent" } },
searchString: "modelcontextprotocol",
positionalConstraint: "CONTAINS",
textTransformations: [{ priority: 0, type: "LOWERCASE" }],
},
},
// Bearer token present: authenticated API clients
{
byteMatchStatement: {
fieldToMatch: { singleHeader: { name: "authorization" } },
searchString: "Bearer ",
positionalConstraint: "STARTS_WITH",
textTransformations: [{ priority: 0, type: "NONE" }],
},
},
],
},
},
};
Caution on user-agent allowlisting: User-agent strings are trivially spoofed. An attacker can set User-Agent: Claude-Bot and bypass Bot Control. Use user-agent allowlisting as a false-positive prevention measure (so legitimate agents aren't blocked), not as a security control. The actual abuse protection comes from rate-based rules and IP reputation lists.
Using Bot Control labels for API audit logging (COUNT mode)
Bot Control attaches labels to requests it classifies as bots. You can use these labels in subsequent label-match rules to take different actions based on bot classification — including COUNT on API paths (log without block) for threat intelligence without impacting availability.
// CDK: Log bot signals on API paths without blocking (for threat intel)
// Requires Bot Control rule to run first (at lower priority number)
const logBotSignalsOnApi: wafv2.CfnWebACL.RuleProperty = {
name: "LogBotSignalsOnMcpApi",
priority: 30, // Must be higher number than Bot Control rule
action: { count: {} }, // Count, don't block — log the signal
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: "BotSignalsOnMcpApi",
sampledRequestsEnabled: true,
},
statement: {
andStatement: {
statements: [
// Only for API paths
{
byteMatchStatement: {
fieldToMatch: { uriPath: {} },
searchString: "/mcp",
positionalConstraint: "STARTS_WITH",
textTransformations: [{ priority: 0, type: "LOWERCASE" }],
},
},
// Only for requests Bot Control labeled as "verified_bot" or "common_bot"
{
labelMatchStatement: {
scope: "LABEL",
key: "awswaf:managed:aws:bot-control:bot:category:monitoring",
},
},
],
},
},
};
// Bot Control label categories (Common inspection level):
// awswaf:managed:aws:bot-control:bot:verified — known good bots (Googlebot, etc.)
// awswaf:managed:aws:bot-control:bot:category:monitoring — monitoring/uptime bots
// awswaf:managed:aws:bot-control:bot:category:scraping — web scrapers
// awswaf:managed:aws:bot-control:bot:category:searchengine — search engine crawlers
// awswaf:managed:aws:bot-control:signal:automated_browser — automation frameworks (Puppeteer, Playwright)
Bot Control cost model
Bot Control has a base cost plus a per-request charge. For MCP servers, the per-request charge can be significant because every API call from every agent client is inspected. Scope Bot Control carefully to avoid inspecting API traffic.
| Component | Cost | Notes |
|---|---|---|
| Bot Control managed rule group (Common) | $10/month | Per WebACL, not per request |
| Bot Control managed rule group (Targeted) | $10/month | Includes Common + TLS fingerprinting |
| Requests inspected by Bot Control | $1.00 per 1M requests | Each request that matches Bot Control scope-down is counted |
| Regular WAF WebACL | $5/month + $1/1M requests | Applies regardless of Bot Control |
| CAPTCHA challenges served | $0.40 per 1,000 challenges | Only for requests that receive CAPTCHA action |
Cost example: An MCP server receiving 10M requests/month with Bot Control scoped to all paths would pay $5 (WebACL) + $10 (BotControl) + $10 (10M inspected) = $25/month. The same server with Bot Control scoped to browser paths only (assuming 95% API traffic) would pay $5 + $10 + $0.50 (500K inspected) = $15.50/month — a 38% saving, and no false positives on API paths.
Common failure modes
| Symptom | Cause | Fix |
|---|---|---|
| All MCP API calls return 405 or redirect to CAPTCHA | Bot Control with CAPTCHA/Challenge action applied to /mcp paths | Add scope-down statement excluding /mcp* and /api/* from Bot Control inspection |
| Claude/GPT plugin calls return 403 after Bot Control enabled | AI agent user-agents classified as bots; no exemption rule in place | Add allowlist rule for known MCP client user-agents at priority lower than Bot Control |
| Unexpected WAF cost spike | Bot Control inspecting all API traffic, not just browser paths | Add scope-down statement; verify Bot Control's scope matches only browser-facing paths |
| Challenge token cookie not passed in API responses | API client stores session token in Authorization header, not cookie jar | Do not apply Challenge action to API paths; use rate-based rules for API protection |
| Monitoring bot (AliveMCP pings) classified as bot and blocked | Uptime monitoring services match bot signatures | Add uptime monitoring source IPs/CIDRs to an allow-IP-set rule at priority 0 |