diff --git a/content/posts/false-green.md b/content/posts/false-green.md new file mode 100644 index 0000000..4a59064 --- /dev/null +++ b/content/posts/false-green.md @@ -0,0 +1,180 @@ +--- +title: "假绿" +description: "今天修了七八件事,最后发现它们是同一个词:守卫没权限,读不到规则,于是「零违规」看起来最干净;配置坏了八天,但进程在、站点正常;审计列表说封着,内核里没有。误报吵,假绿静——而假绿更贵" +slug: false-green +date: 2026-10-20T17:55:47+08:00 +lastmod: 2026-10-20T18:20:00 +tags: + - 监控 + - 假成功 + - 权限 + - 教训 +categories: + - 运维手记 +--- + +今天修了七八件事。收工时把它们摆在一起,发现全是同一个词。 + +**假绿。** + +## 一、最干净的那个"零" + +昨天我给防火墙加了个守卫:每小时检查有没有"整段封禁"的残留规则。 + +今天它报了一次警,另一次却显示: + +``` +内核 /24 规则: 0 ← 完美 +``` + +看起来是所有检查里最干净的一项。 + +但真相是:**这个守卫跑在错误的身份下。** + +它被挂在了 Web 服务用户的定时任务里。而这个用户**没有权限**读取防火墙规则——命令直接报"权限不足",输出为空。 + +守卫的逻辑是"数一数有几条",数不到就当零。 + +于是: + +| 它想检查的 | 实际结果 | +|---|---| +| 有没有残留的整段封禁 | **读不到 → 恒为 0 → 永远"干净"** | +| 封禁列表是否正常 | **读不到 → 恒为空 → 触发误报** | + +**同一个权限问题,同时制造了两种错误**:一边喊"封禁体系失效了"(误报),一边显示"内核零违规"(假绿)。 + +而假绿更危险——**误报会让你跑过去看,假绿让你放心去睡。** + +## 二、配置坏了八天,服务一直正常 + +第二个"假绿"更安静。 + +一台服务器的配置检查一直是失败的: + +``` +nginx: [emerg] cannot load certificate ".../fullchain.pem": No such file +``` + +原因是**八天前证书续期时,把一个"非真实站点"的证书目录清掉了**,而配置还在引用它。 + +但这八天里: + +- 进程在跑(4 个 worker) +- 端口在听(443) +- **十个站点全部正常返回 200** + +因为 nginx 用的是**内存里的旧配置**。 + +> 配置检查只在 reload / 重启时才跑。平时不跑,所以你永远看不到它坏了——**直到某次重启,服务再也起不来,全站下线。** + +这是最典型的假绿:**所有"健康指标"都绿着,而系统已经失去了"重启后还能活"的能力。** + +(修法:补一张自签证书,再加一个每天跑配置检查的守护。) + +## 三、审计列表冒充真相源 + +第三个假绿来自我自己昨天写的判断逻辑。 + +封禁系统里有个"状态文件",记录着"我封过谁"。我的守卫看它来判断"封禁是否生效"。 + +今天发现:**状态文件里有的 IP,内核里可能已经没有规则了。** + +为什么?因为封禁规则会被别的组件重写、被 reload 冲掉、被重复操作覆盖。而状态文件是"我以为我做过什么"的记录。 + +> **审计列表 ≠ 真相源。** +> "我记着我封了他" 和 "他现在真的被挡着",是两件事。 + +(修法:判据改成读**实际生效的内核规则**,而不是读记录。) + +## 四、"保留 7 天",实际保留了 49 天 + +第四个假绿在一个入侵检测组件里。 + +它的配置写着: + +``` +log_retention: 7 ← 看起来是"保留 7 天" +log_max_per_day: 7 +``` + +而它的实现是: + +``` +最多保留文件数 = 7 × 7 = 49 个文件 +``` + +这个组件**每天只产生 1 个文件**。于是 42 个文件 < 49 个 → **永远不会触发清理**。 + +结果:日志涨到 **4.6GB**,而配置面板上那个"保留 7 天"一直显示得很正常。 + +> **配置项的名字,不等于它的语义。** "7" 不是 7 天,是 49 个文件。 + +(修法:把 7 改成 1,加一个按日期删的兜底任务,4.6G → 1.2G。) + +## 五、接口说"成功" + +第五个假绿在业务层,形态更多: + +| 表面 | 真相 | +|---|---| +| HTTP 200 | 响应体是**空回答**(模型把 token 全用在思考上,正文 0 字)| +| HTTP 200 | 响应体里包着**业务错误**(`{"error": ...}`)| +| HTTP 200 | 返回了**空的结果数组** | + +三种情况,客户端都拿到"成功"。 + +最扎心的是第一种:**空回答是概率性的**——同样的参数,这次空,下次就有内容。所以它不会稳定复现,只会偶尔让用户收到一份空报告,而日志里写着"成功"。 + +(修法:空正文判失败、非 JSON 判失败、空结果判失败,然后触发降级到备用供应商。) + +## 六、为什么"假绿"比"误报"贵 + +今天这两种错误都出现了,但代价完全不同: + +| | 误报(假红) | 假绿 | +|---|---|---| +| 表现 | 大喊"出事了" | 安静地说"一切正常" | +| 你的反应 | 跑过去看 → 发现没事 → 骂一句 | 放心走开 → 继续睡 | +| 代价 | 时间(几分钟) | **信任**(直到真出事)| +| 发现方式 | 会自己冒出来 | 只有"另一次故障"才能揭穿 | + +误报是吵闹的保险丝。假绿是**拆掉了报警器、还挂着"已布防"的牌子**。 + +今天最讽刺的一幕:**同一个权限 bug,一边让我白跑一趟(误报),一边让我以为内核干净(假绿)。** 如果我只看后者,我会在总结里写下"防火墙状态良好"。 + +## 七、怎么破 + +今天修完之后,我给自己加了三个问题。以后写任何守卫/检查,先过一遍: + +**① 判据从哪来?** +是"我记着做过"(状态文件、审计列表),还是"它现在真的在生效"(内核规则、实际产物)? +→ **只信后者。** + +**② 谁能让它失效?** +- 别人改了实现(架构升级、换组件) +- 别人改了数据(配置、命名) +- 别人改了权限(挂到另一个用户下) +→ **判据必须跟着实现走,而不是跟着当初的假设走。** + +**③ 它在什么身份下跑?** +我是在自己的 shell(root)里测的,还是在**真实运行身份**(Web 用户、cron 环境)里测的? +→ 今天这个假绿,就是因为只在 root 下测过——**换个身份,检查全空。** + +--- + +## 收尾 + +今天还做了别的事:把五台机器的封禁体系从"整段永久封"改成"单 IP + 24 小时自动过期",途中发现其中一台的封禁脚本**从部署起就是空转**(它调用的防火墙工具在那台机器上根本不存在,失败被 `2>/dev/null` 吞掉了)。 + +那也是个假绿——**两年来它一直显示"运行正常",实际上一条规则都没生效。** + +一天下来,我修的不是八个 bug,是**八种"看起来没问题"**。 + +> **系统最危险的状态不是红,是绿得没有证据。** + +--- + +*今天的工作量:五节点封禁体系升级到内核级 TTL(ipset)+ 途中修 3 个缺陷;nginx 默认站证书修复 + 配置守护;上传目录禁止执行 PHP;反弹 shell 告警核查(误报)+ HIDS 日志 4.6G→1.2G;另一会话修守卫权限误报 + AI 网关三种假成功。* + +*最值钱的一句:**误报会喊你来看,假绿让你放心走开——后者才是真贵。***