登录页报「连不上人机验证服务器」:一条藏在库里的 WAF 策略
所有人登录都报 Failed to fetch,后端日志里一条请求都没有。468 不是标准状态码,是 WAF 的挑战页;策略在库里躺了半个月,一次配置生成才让它真正落地。
早上八点半,群里同时冒出七八条消息:ERP 登录页打不开了。有人说页面能出来,但点了登录没反应;有人连页面都白屏,控制台上只有一行红字。
我先用自己的账号试了一次。页面确实加载出来了,但登录按钮点下去,弹出一行小字「无法连接到人机验证服务器」,控制台里是 TypeError: Failed to fetch。与此同时,后端服务的健康检查是 200,应用日志里干干净净,一条登录请求都没进来。
也就是说:请求根本没到应用,在更前面就被拦了。
一、先确认是谁返回的
第一反应是查后端和数据库,但日志里没有任何请求,说明问题在入口层。直接从命令行复现:
curl -sS -o /dev/null -w "%{http_code} %{size_download}\n" https://app.example.com/login
返回的是 468 1247。468 不是任何一个标准状态码,HTTP 里根本没有它。看到这种「野生状态码」,基本可以确定是网关或 WAF 自己加的。
再取一次响应体:
curl -sS https://app.example.com/login | head -20
返回的不是应用的登录页,而是一段压缩过的 JS。这就是 WAF 的「人机验证挑战页」——它用一个非常规状态码告诉浏览器「你得过一道验证」,再把验证脚本塞进响应体里。
把入口层会出现的几个码列一下,能省掉大量猜测:
| 状态码 | 谁返回的 | 含义 | 算不算故障 |
|---|---|---|---|
| 465 | WAF 等候室 | 并发满了,在排队 | 是 |
| 468 | WAF 挑战页 | 要求人机验证 | 是(如果业务不需要) |
| 403 | WAF / 规则引擎 | 命中拦截规则 | 分情况 |
| 502 | 网关 | 上游连不上 | 是 |
| 200 + 780 字节 | 默认后端 | 域名没匹配到任何站点 | 是 |
二、为什么提示「连不上验证服务器」
挑战页的脚本里有一段逻辑:优先从公网 CDN 加载验证组件,加载失败再回落到 WAF 本地。如果客户端出不了公网、或者 DNS 把那个域名解析到了黑洞,第一段就卡住,于是页面上显示「无法连接到人机验证服务器」。
这个提示很有欺骗性。它让人以为是 WAF 挂了、或者客户端网络有问题,实际上 WAF 活得好好的,只是它要求的东西客户端拿不到。我们这边的办公网出口是白名单制,那个 CDN 域名不在名单里,所以所有人都是同样的报错——这也解释了为什么「换个人试也一样」。
所以这条提示的正确读法是:有人在要求你做一次人机验证,而验证组件加载不到。要解决的不是网络,是「为什么要验证」。
三、策略是从哪冒出来的
WAF 的站点配置分两层:一层是控制台上能看见的规则(按路径、按 IP、按 UA),另一层是站点级的全局策略,比如人机验证、等候室、区域封禁。后者在控制台上往往被折叠,甚至被标记成隐藏,出了事根本想不到去看。
我们在配置库里查到了两条记录,都属于「站点级」:
- 一条人机验证策略,动作是「挑战」,绑定在出问题的站点上,处于启用状态;
- 一条等候室策略,最大并发 5、最大等待 0,也绑定在同一个站点上。
时间戳显示是两周前一次演练留下的。奇怪的是,这两周一直没出问题,为什么偏偏今天早上生效?
答案在 WAF 的下发机制上:站点级策略改了之后,并不会立刻写进数据面。只有当有人触发一次「站点配置重新生成」时,生成器才会把库里的策略整份渲染成配置文件,下发到处理流量的进程。也就是说,这两条策略在库里躺了半个月,一直没落地;今天早上有人在控制台改了一个无关的开关,触发了重新生成,两条策略跟着一起生效了。
这类问题最难查的地方在于:改 A 导致 B 生效。日志上看不到任何「策略变更」的痕迹,只有流量突然开始被挑战。
要确认这一点,把配置文件的修改时间和操作记录对一下就行。数据面配置文件的 mtime 就是最后一次生成的时间,如果它和「有人保存配置」的时间吻合,而策略的 updated_at 比它早半个月,那结论就很清楚了:策略早就存在,只是这次才落地。
stat -c '%y %n' /etc/waf/sites-enabled/*.conf | sort | tail -3
SELECT id, site_id, action, is_enabled, hidden, updated_at
FROM mgt_policy
WHERE site_id = <站点ID>;
hidden = 1 这一列是关键。它决定了这条策略在控制台列表里不显示,也决定了它不会出现在任何「最近变更」的视图里。排查时如果没有直接查库,光看控制台永远不会发现它。
四、怎么确认是它
三个证据互相咬合,就能定性:
第一,响应码是 468,说明是挑战;第二,挑战覆盖了整站所有路径,包括 /login 这种静态页,而站点级策略的特征就是「不带路径条件」;第三,配置库里那两条策略的 site_id 正是这个站点,is_enabled 是 1。
有一条弯路值得记下来:我们先去查了 WAF 的拦截日志,什么都没查到。因为挑战和等候室不记在「拦截」里,它们记在另外的统计维度。按「拦截数」找问题,永远找不到它。
五、处置:改库、重载,不动配置文件
处置分三步,全程没有改一行 nginx 配置,因为配置文件是生成出来的,改了下次生成照样被覆盖。
第一步,把库里两条策略置为禁用。注意是禁用不是删除——删除会丢审计痕迹,禁用可以立刻回滚:
UPDATE mgt_policy SET is_enabled = 0 WHERE id = 7;
UPDATE mgt_waiting_room SET is_enabled = 0 WHERE id = 1;
第二步,让数据面重新加载。策略由两个组件负责,一个是检测器,一个是排队器,各自以 Pod 形式跑着。删掉它们让控制器重建,比重启整个 WAF 影响面小:
kubectl -n waf delete pod -l app=detector
kubectl -n waf delete pod -l app=chaos
kubectl -n waf rollout status deploy/waf-engine --timeout=90s
第三步,按「从外到内」的顺序验:
curl -sS -o /dev/null -w "%{http_code}\n" https://app.example.com/ # 期望 302
curl -sS -o /dev/null -w "%{http_code}\n" https://app.example.com/login.html # 期望 200
curl -sS -o /dev/null -w "%{http_code}\n" -X POST https://app.example.com/api/login # 期望 200
curl -sS -o /dev/null -w "%{http_code}\n" "https://app.example.com/api/q?id=1%27%20or%201=1--" # 期望 403
前三个验证业务恢复,第四个验证防护还在。这一步不能省——把策略关干净很容易,关过头就把 WAF 变成了透明代理。
六、防复发
事后补了三件事。
一是巡检。原来只监控「站点是否 200」,现在加了 468/465 的计数,任意路径在五分钟内出现超过 20 次就告警。这两个码在正常业务里几乎不会出现,是很干净的信号。
巡检本身不用很复杂,从入口日志里按状态码聚合就够:
# 每 5 分钟统计一次非 2xx/3xx 的状态码分布,468/465 出现即告警
tail -n 20000 /var/log/waf/access.log \
| awk '{print $9}' | sort | uniq -c | sort -rn \
| grep -E '^( *)[0-9]+ (468|465)$' \
&& echo "challenge-or-waiting-room triggered" | mail -s "waf alert" ops@example.com
如果日志量大,用现成的采集器做一次 count by status_code 更省事,重点是把 468 和 465 单独列成指标,而不是混在「4xx 比例」里——混进去之后,正常业务里那点 404 会把它稀释掉,告警永远不响。
二是基线比对。把站点的「站点级策略」导出成一份清单,每次配置生成之后自动比对一次,有新增就通知。这类隐藏开关最大的问题是「人不知道它存在」,只能靠机器盯着。
三是出口白名单。如果确实要用挑战,挑战组件的域名必须放进办公网白名单,否则一旦启用就是全员不可用。我们现在的策略是「不用挑战」,但白名单还是加了,防止哪天有人又打开。
小结
- 页面提示「无法连接验证服务器」时,先看响应码。468、465 这类非标准码是 WAF 自己的语义,跟应用和网络都没关系。
- 挑战页的脚本常从公网 CDN 加载,客户端出不去就会报
Failed to fetch。这个报错说明「被要求验证」,不说明「网络故障」。 - 站点级策略(人机验证、等候室、区域封禁)在控制台上经常是隐藏的,而且改完不立即生效,要等一次配置生成才落地。查「改 A 导致 B 生效」的问题时,先怀疑它。
- 拦截日志里查不到挑战和排队,别在「拦截数」上找。
- 处置优先改库置禁用 + 重载组件,不要手改生成出来的配置文件。
- 验证要正反都验:业务恢复,同时防护仍然生效。
正在加载评论…