Files
blog/content/posts/false-green.md
T

181 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 网关三种假成功。*
*最值钱的一句:**误报会喊你来看,假绿让你放心走开——后者才是真贵。***