Guide · AWS WAF
AWS WAF Geo Restriction for MCP Servers
AWS WAF GeoMatchStatement blocks or allows requests based on the originating country code, using AWS's built-in MaxMind GeoIP database. The two design choices that matter most: blocklist vs. allowlist (blocklist blocks named countries; allowlist blocks everyone except named countries — for compliance-restricted products an allowlist is mandatory, for abuse mitigation a blocklist of a few known-bad-traffic sources is usually sufficient) and ALB vs. CloudFront placement (WAF on an ALB resolves the country from the source IP after VPC flow; WAF on CloudFront uses CloudFront's viewer IP, which is generally more accurate because CloudFront terminates the TLS connection at the edge close to the user — ALB in a single AWS region may see traffic routed through regional proxies). For most MCP servers serving a global developer audience, geo restriction is best used as a signal amplifier for rate limits rather than hard blocking — apply stricter rate limits to high-abuse-traffic regions instead of blocking entire countries.
TL;DR
Use GeoMatchStatement with a NOT operator to allowlist your target markets if your MCP server has geographic compliance requirements. For abuse mitigation without a compliance mandate, use geo as a scope-down in rate rules (apply 10× stricter rate limits to historically high-abuse regions) rather than hard blocking. CloudFront WAF has more accurate geo detection than ALB WAF for globally distributed traffic.
GeoMatchStatement syntax and country codes
WAF geo match uses ISO 3166-1 alpha-2 two-letter country codes. The GeoMatchStatement matches a single country or list of countries. Use the NOT statement wrapper to invert the logic for allowlist (permit-only) approaches.
// CDK: Block requests from a specific list of countries (blocklist approach)
import * as wafv2 from "aws-cdk-lib/aws-wafv2";
const geoBlocklist: wafv2.CfnWebACL.RuleProperty = {
name: "GeoBlockHigh AbuseRegions",
priority: 3,
action: { block: {} },
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: "GeoBlockHighAbuse",
sampledRequestsEnabled: true,
},
statement: {
geoMatchStatement: {
countryCodes: ["KP", "CU", "IR", "SY"], // OFAC-sanctioned; block for compliance
},
},
};
// CDK: Allowlist approach — allow ONLY specific countries (block everyone else)
// Useful for MCP servers with geographic compliance requirements
const geoAllowlist: wafv2.CfnWebACL.RuleProperty = {
name: "GeoAllowlistOnly",
priority: 3,
action: { block: {} },
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: "GeoAllowlistBlock",
sampledRequestsEnabled: true,
},
statement: {
// NOT (US OR CA OR GB OR AU OR EU countries) = block all others
notStatement: {
statement: {
geoMatchStatement: {
countryCodes: [
"US", "CA", "GB", "AU", "NZ", // English-speaking
"DE", "FR", "ES", "IT", "NL", "SE", // EU core
"JP", "KR", "SG", "IN", // APAC developer markets
],
},
},
},
},
};
Geo-amplified rate limits instead of hard blocking
Hard blocking entire countries is a blunt instrument — it blocks legitimate developers who happen to be in high-abuse regions and does nothing against attackers using VPNs. A more surgical approach: apply the same rate limit structure for all traffic but reduce the threshold for regions with disproportionate abuse-to-legitimate-use ratios.
// CDK: Stricter rate limit for high-abuse regions (not a hard block)
// Normal limit: 1,000 POST/5min. High-abuse regions: 200 POST/5min.
const strictGeoRateLimit: wafv2.CfnWebACL.RuleProperty = {
name: "McpStrictGeoRateLimit",
priority: 4,
action: { block: {} },
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: "McpStrictGeoRateLimit",
sampledRequestsEnabled: true,
},
statement: {
rateBasedStatement: {
limit: 200, // 5× stricter than normal 1,000/5min limit
aggregateKeyType: "IP",
scopeDownStatement: {
andStatement: {
statements: [
// Only applies to MCP POST requests
{
byteMatchStatement: {
fieldToMatch: { uriPath: {} },
searchString: "/mcp",
positionalConstraint: "STARTS_WITH",
textTransformations: [{ priority: 0, type: "LOWERCASE" }],
},
},
{
byteMatchStatement: {
fieldToMatch: { method: {} },
searchString: "POST",
positionalConstraint: "EXACTLY",
textTransformations: [{ priority: 0, type: "UPPERCASE" }],
},
},
// Only applies to requests from high-abuse-traffic regions
{
geoMatchStatement: {
countryCodes: ["CN", "RU", "BR", "VN"], // Adjust based on your WAF logs
},
},
],
},
},
},
},
};
This rule and the normal 1,000/5min rule coexist in the same WebACL. Requests from the high-abuse regions hit the 200/5min limit at priority 4; requests from other regions only hit the 1,000/5min limit at whatever priority you assigned it. A Chinese developer using your MCP server normally will never trigger either limit; a Chinese bot generating 300 requests in 5 minutes will hit the strict limit and get blocked, while the same traffic from a US IP would only hit the broader limit.
CloudFront vs ALB geo accuracy
The accuracy of geo restriction depends on where WAF is placed in the request path. CloudFront WAF is generally more accurate for globally distributed clients.
| Placement | Source IP used | Accuracy | Notes |
|---|---|---|---|
| CloudFront WAF (CLOUDFRONT scope, us-east-1) | CloudFront viewer IP (real client from CF's perspective) | High — CloudFront sees the TLS connection from the actual client at the edge PoP closest to them | Most MCP server deployments should use this if they have CloudFront already |
| ALB WAF (REGIONAL scope) | Source IP from ALB access log (the IP that connected to the ALB) | Medium — traffic transiting through AWS Transit Gateway, Direct Connect, or VPC peering may appear with AWS IPs | Sufficient for abuse mitigation; not suitable for strict compliance geo-blocking |
| API Gateway WAF | API Gateway source IP from $context.identity.sourceIp | Medium — same caveats as ALB; API Gateway sits in a single region | Acceptable for developer-facing MCP APIs where strict geo compliance isn't required |
ForwardedIP with geo: For ALB-fronted servers, you can improve accuracy by using the X-Forwarded-For header to extract the client IP, then doing geo lookup on that. WAF's GeoMatchStatement on ForwardedIpConfig is available on rate-based rules but not on standalone GeoMatchStatement. To do geo lookup on the XFF header IP, use a rate-based rule with aggregateKeyType: FORWARDED_IP and add a scope-down geoMatchStatement — WAF evaluates the geo based on the aggregation key IP in that context.
Combining geo restriction with the full WAF stack
Geo rules fit naturally between IP reputation (priority 1) and rate limits (priority 2) in the rule evaluation order. Hard compliance blocks go at the top; soft geo-amplified rate limits sit just after the general rate rules.
// Rule priority order for MCP server WAF:
// Priority 0 — Allow IP set (uptime monitors, CI/CD IPs — skip all checks)
// Priority 1 — IP Reputation List (block known bad IPs)
// Priority 2 — OFAC compliance blocklist (block sanctioned countries — hard block)
// Priority 3 — Post rate limit global (1,000 POST/5min per IP)
// Priority 4 — Post rate limit geo-amplified (200 POST/5min from high-abuse regions)
// Priority 5 — SSE rate limit (10,000 GET/5min per IP on /mcp paths)
// Priority 10 — KnownBadInputsRuleSet (Log4j, SSRF, path traversal)
// Priority 20 — CommonRuleSet (XSS, SQLi — with MCP exclusions)
// DefaultAction: ALLOW
// OFAC compliance geo block (hard block, priority 2):
const ofacGeoBlock: wafv2.CfnWebACL.RuleProperty = {
name: "OFACGeoBlock",
priority: 2,
action: { block: {} },
visibilityConfig: { cloudWatchMetricsEnabled: true, metricName: "OFACGeoBlock", sampledRequestsEnabled: true },
statement: {
geoMatchStatement: {
countryCodes: ["KP", "CU", "IR", "SY"], // https://ofac.treasury.gov/sanctions-programs-and-country-information
},
},
};
Limitations and bypass vectors
Geo restriction is a speed bump, not a wall. Sophisticated attackers bypass it with VPNs, proxies, and Tor. For MCP servers, treat geo restriction as a cost-raising measure against opportunistic abuse, not as a security control against determined adversaries.
| Limitation | Impact | Mitigation |
|---|---|---|
| VPN bypass | Any attacker using a VPN exit node in an allowed country bypasses geo rules | Add AnonymousIpList managed rule group (blocks known VPN/proxy exit IPs); costs 50 WCU, free |
| GeoIP database inaccuracy | MaxMind database has ~1-3% city-level error rate; some IPs misclassified by country | Use allowlist rather than blocklist for compliance; log misclassifications via WAF sampling |
| Cloud exit IPs | Attackers using AWS/GCP/Azure instances in allowed countries appear domestic | Combine with rate limits; AWS IPs from same region as your service may be legitimate (CI/CD) |
| Blocking legitimate remote workers | Developer in "blocked" country with business VPN to an allowed country gets blocked at WAF level (WAF sees the country of the VPN exit) | For strict allowlist deployments, provide an API key bypass path that skips geo rules at priority 0 |
| IPv6 addresses less accurate | IPv6 GeoIP databases are less mature; some IPv6 blocks misclassified | Log and monitor IPv6 geo matches separately; escalate misclassifications to AWS WAF feedback |
Common failure modes
| Symptom | Cause | Fix |
|---|---|---|
| Developer in a target market gets blocked | GeoIP database misclassified their IP; or they're using a VPN with an exit node in a blocked country | Add an IP set allow rule for their IP at priority 0; provide instructions for affected users |
| Geo block has no effect on abuse traffic | Attackers using VPNs with exit nodes in allowed countries | Combine geo with rate limits; add AnonymousIpList managed rule group |
| Allowlist blocks uptime monitoring | AliveMCP/Pingdom/StatusCake monitor IPs in countries not in allowlist | Add monitoring service IPs to an allow IP set at priority 0 |
| CloudFront geo shows different country than ALB WAF for same client | Different GeoIP lookup points; CloudFront and ALB WAF use different source IPs | Standardize on one WAF placement; prefer CloudFront WAF for accuracy |