AWS 替你创建的 WAF 把你的编辑挡在门外:CloudFront 默认的 300 次/5 分钟限速
发布于 2026年6月20日

症状:编辑在工作中途被卡住
晚上 9 点左右,一个 WordPress 门户的编辑们开始在保存文章和上传图片时收到错误。这不是"网站宕机"——公开站点响应正常。问题出在后台:admin-ajax.php 失败、媒体上传报错、区块编辑器在编辑中途卡住。
CloudFront 的 WAF 正在拦截编辑本人。更糟的是:这是一个没人有意识地配置过的 WAF。
沉默的默认值:AWS 替你创建的 WebACL
当你在控制台为某个 CloudFront 分发启用保护时,AWS 可能会自动附加一条带默认限速规则的 WebACL:每个 IP 每 5 分钟 300 次请求,且没有任何 scope-down。它的名字暴露了来源——类似 AWS-RateBasedRule-IP-300-CreatedByCloudFront。
300 次/5 分钟听起来很宽裕,直到你想起 wp-admin 的行为方式。WordPress 心跳每 15–60 秒打一次 admin-ajax.php。每个编辑页面加载 20–40 个静态资源。同一个办公室里两三个编辑通过同一个公网 IP(NAT)出口。几分钟内,这个共享 IP 就冲破 300 次请求——而无法区分编辑与攻击者的 WAF,把他们全部拦下。
诊断:读 WAF 日志,不要猜
第一步是确认到底是谁、被哪条规则拦截。把 WebACL 日志送到 CloudWatch 后,Logs Insights 几秒钟就能回答:
fields @timestamp, httpRequest.clientIp, terminatingRuleId, httpRequest.uri
| filter action = "BLOCK"
| stats count(*) as blocks by terminatingRuleId
| sort blocks desc结果毫不含糊:窗口内共 1,408 次拦截,其中 1,284 次来自 AWS-RateBasedRule-IP-300-CreatedByCloudFront 规则。按 IP 和 URI 拆分,被拦的都是真实编辑——其中一人单独在 admin-ajax.php 和 media-new.php 上就有 111 次拦截,外加五个巴西住宅 IP 在编辑和上传媒体。另外那 124 次呢?那才是该拦的:一个来自境外云段的扫描器发起 934 次请求寻找 ALFA webshell,以及来自数十个分散 IP 的 xmlrpc.php 攻击。
限速本身存在是对的;错在把一个已登录的编辑与一个匿名扫描器同等对待。
显而易见方案的陷阱
两条诱人的出路,都是错的:
调高上限(300 → 2,000):为所有人削弱了 DDoS 和暴力破解防护,而且 NAT 后的大型办公室仍可能冲破。你只是用一个问题换了另一个。
把动作改成 Count:实际上禁用了防护——WAF 只计数、不再拦截。通过杀死功能来消除症状。
正确的问题不是"理想的上限是多少?",而是"这个上限到底应不应该作用于已认证用户?"。不应该。
修复:用 scope-down 豁免持有登录 Cookie 的人
WAFv2 允许在 RateBasedStatement 内嵌一个 ScopeDownStatement:限速规则只统计匹配 scope-down 的请求。再用 NotStatement 包一层,就反转了:统计所有人,除了发送 wordpress_logged_in_ Cookie 的请求。已认证用户不再计入上限;匿名流量仍受 300 次/5 分钟保护。
{
"Name": "RateLimit-300-anon-only",
"Priority": 0,
"Action": { "Block": {} },
"Statement": {
"RateBasedStatement": {
"Limit": 300,
"AggregateKeyType": "IP",
"ScopeDownStatement": {
"NotStatement": {
"Statement": {
"ByteMatchStatement": {
"SearchString": "wordpress_logged_in_",
"FieldToMatch": {
"Cookies": {
"MatchPattern": { "All": {} },
"MatchScope": "VALUE",
"OversizeHandling": "MATCH"
}
},
"PositionalConstraint": "CONTAINS",
"TextTransformations": [{ "Priority": 0, "Type": "NONE" }]
}
}
}
}
}
},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "RateLimit300AnonOnly"
}
}一个耗费时间的细节:通过 aws wafv2 ... --cli-input-json 提交这条规则时,SearchString 必须用 base64(wordpress_logged_in_ 对应 d29yZHByZXNzX2xvZ2dlZF9pbl8=)。在控制台你输入纯文本;通过 CLI 的 JSON 则不行。
第二个发现:有一个分发根本没有 WAF 日志
把修复复制到第二个分发时,查询命令以 WAFNonexistentItemException 失败:那条 WebACL 从未配置过日志。也就是说:它在盲目拦截,没有任何关于"拦了谁"的记录。在任何规则之前,我们先打开了日志:
aws wafv2 put-logging-configuration \
--logging-configuration \
ResourceArn=arn:aws:wafv2:us-east-1:123456789012:global/webacl/EXAMPLE/EXAMPLEID,\
LogDestinationConfigs=arn:aws:logs:us-east-1:123456789012:log-group:aws-waf-logs-cf经验法则:没有日志的 WAF 是你无法运维的 WAF。你不知道它拦了什么,分不清误报与攻击,最后只能用最昂贵的方式发现两者——客户来投诉。
结果
限速拦截(已记录) : 1,284 -> 0 (24 小时后,两个分发上)
卡在 wp-admin 的编辑 : 是 -> 否
对匿名的防护(300/5 分钟) : 开启 -> 开启(不变)
第二个分发的 WAF 日志 : 缺失 -> 已启用24 小时内,两个分发上的合法拦截归零,对匿名流量的防护依然在线。webshell 扫描器和 xmlrpc 攻击继续被拦——现在只拦它们。
经验教训
1. CloudFront 的默认值对已登录的 WordPress 是敌对的。自动创建的、300 次/5 分钟且无 scope-down 的 WebACL 把编辑当作攻击者。如果你在控制台启用过保护,去检查这条规则是否存在。
2. 限速的修复是 scope-down,不是那个数字。用 NotStatement 豁免已认证流量(wordpress_logged_in_ Cookie),对匿名保持紧的上限。
3. 没有日志的 WAF 无法运维。在信任任何规则之前先启用 put-logging-configuration。没有日志,你分不清编辑和扫描器。
4. 办公室 NAT 会骗过按 IP 的限速。多个编辑从同一个公网 IP 出口;300 次/5 分钟很快就没了。按 IP 的 AggregateKeyType 必须考虑这一点。
另一个限速惩罚合法用户的案例(在 ALB 上、由 Gutenberg 暴露)见 一天 36.3 万次误报 429——那里的触发点不同,但教训重复:缺乏"客户是谁"这一上下文的限速会变成搬起石头砸自己的脚。
结论
云替你自动开启的保护不是配置过的保护——它是一个可能为"平均情况"而非"你的情况"调优的默认值。边缘 WAF 需要知道:一个猛敲 xmlrpc.php 的匿名 IP,和一个保存草稿的已登录编辑,是两回事。CloudFront 默认并不知道;你用一条规则的 scope-down 教会它。