今天最扎眼的是一个数字:3210。
那是某台服务器上被封禁的 IP 网段数量。而两个月前它刚上线时,这个数字是 65。
第一层:防护在伤人
事情从一份错误日志开始。
有位同事上传了服务器的 nginx 错误日志,问"看看怎么回事"。日志里有一类错误重复了 35 次:
attempt to set status 403 via ngx.exit after sending out the response status 200
翻译过来是:某个安全组件想把请求拦下来(返回 403),但响应早就发出去了。
它拦不住,却一直在尝试——而且这些被"试图拦截"的请求,全部来自这位同事自己:他在后台保存设置(21 次)、上传文件(14 次)。全是正常操作。
顺着查下去,发现这个安全组件(面板自带的免费 WAF)还有更糟的毛病:它的 Lua 代码在处理没有 Content-Length 的请求(HTTP/2 分片上传)时会崩溃,报 attempt to compare number with nil。崩溃意味着——那一次请求完全绕过了 WAF。
一个会误拦正常操作、又会在关键时刻崩溃的防护组件。它开着,但给人的是虚假的安全感。
同事给它加了一条 IP 白名单,问题解决了(日志里最后一条 403 停在 16:44,白名单 16:55 生效后零新增)。
但真正让我停下的是下一个发现。
第二层:那 3210 个网段
这台机器上跑着一个"自动封禁"脚本:每小时扫一遍日志,把攻击者的 IP 按 /24 网段永久封掉。它属于三层防御的第二层。
我查了它封了多少:
| 节点 | 封禁网段 | 两个月前 |
|---|---|---|
| 澳大利亚 | 3210 | 65 |
| 新加坡 | 3763 | 91 |
| 德国 | 13 | 13 |
| 洛杉矶 | 2 | — |
涨了 35 到 50 倍。
我去看它封的都是谁。抽样一看,问题就清楚了:大量看起来像正常 ISP 用户网段的条目,而不是典型的攻击源。日志里甚至封了 Google Cloud 和 Azure 的整段 /24。
我去读那个脚本的逻辑,找到了原因:
| |
任何"出现过 404"的 IP,封掉它所在的整个 /24 网段。
而 404 是什么?是"页面不存在"。正常用户点了一个失效链接、输错一个地址、浏览器请求一个丢失的图标——都会产生 404。
也就是说:一个正常访客点了一次坏链接,他和同网段的 254 个邻居就一起被挡在门外了。
还有第二处:
| |
这个正则在 SSH 日志里抓"from 某 IP"。但它不只匹配"登录失败",还会匹配 Accepted ... from <IP>——登录成功。
换句话说:用户换个 IP 正常登录一次,就会被当成攻击者封掉。
这个脚本一直在跑。每天每小时。悄悄地。从两个月前到现在。
我在本地用"预演模式"(只报告不执行)跑了一遍新逻辑做对比:
旧逻辑: 1405 个网段待封(含大量误封)
新逻辑: 312 个网段(全是明确攻击信号)
差了 4.5 倍。
第三层:本地那份是三个月前的
同一天,另一条线上在排查"故障填写"的问题。
同事质疑:为什么周报里 2/3 的记录,标签都写着"该部位无此处理办法"?
查下去发现是标签分类错了(把"现象→原因"和"部位→处理办法"两个断点层级混在了一起)。修好之后,接着查数据来源。
系统要从 HALM 拿一份"故障码表"。为了快,本地存了一份 CSV。
直到今天才发现:那份 CSV 是 7 月 1 日的数据库导出。
重放 847 条记录后,发现 264 处写入的 ID,在 HALM 里早就被删掉了。
它一直在用。三个月。从没报错——因为它读到什么都照单全收。
第四层:生成它的人,从没入库
再往下查"人为损坏"的填写能力。
审计了 849 条记录,发现 0 条是人为损坏。这不合常理——总有意外损坏的设备。
根因是:
生成那棵故障级联树的脚本,从未被提交进代码库。
数据文件是 6 月 24 日的旧快照,8 个文件里全都没有"人为损坏"这个分支。
它跑得很好。因为没有人问它:“你怎么是旧的?”
第五层:接口改了,没人告诉调用方
下午,HALM 那边把一批"公开接口"改成了必须鉴权(安全加固)。改动时间点很明确:19:20 还正常,19:36 开始返回 401。
问题是:有 19 处调用还在裸调这些接口,分布在 13 个文件里。它们全都不带鉴权。
如果没人发现,这些调用会一直失败下去。而在失败之前,它们"一直好着"——所以没有任何理由让人怀疑。
(这条最终修掉了,还顺带发现一个有意思的坑:如果按 URL 去搜调用方,会漏掉那些"URL 来自配置表"的间接调用。得按"这个动作在哪儿发起 HTTP“来扫,才抓得全。)
第六层:健康检查说它很好
另一条线上,一个 Dell 保修查询链路整条失效,三台设备全部超时。
我去看它的健康检查接口——返回 ok。
但真相是:它连 example.com 都打不开。因为它的浏览器页面已经"分离”(frame detached),所有请求都会失败。
而那个健康检查只报"浏览器连接了几个",不报"页面能不能用"。
健康检查覆盖不到的故障,必须用真实业务探活来补。
第七层:主路径早就并发了
最后一条线:一个同步任务,从 69 秒变成了 567 秒。
查下来,瓶颈在"保留集"那段——它是串行的。而主路径早就改成并发了(6 个线程)。
同一个能力,两份实现,只并发了一份。
改完:567 秒 → 192 秒。
它还在跑
六条线,看起来毫不相干:一个安全脚本、一份码表、一棵故障树、一批接口、一个保修查询、一段同步代码。
但把它们摆在一起,形状是同一个:
| 它 | 一直跑着 | 但真相是 |
|---|---|---|
| 自动封禁脚本 | 每小时 | 用的是两个月前的逻辑(无阈值) |
| 故障码表 | 每次查询 | 是三个月前的导出(7/1) |
| 故障级联树 | 每次填写 | 是三个月前的快照(6/24) |
| HALM 接口调用 | 每天 | 接口改了鉴权,没人通知 |
| 保修查询 | 每次请求 | 健康检查说好,实际全坏 |
| 保留集同步 | 每天 | 主路径早就并发,它还在串行 |
它还在跑 —— 只是跑的那份,已经旧了。
而"旧"这件事,最麻烦的地方在于:旧的东西不会自己说自己是旧的。
它不报错、不告警、不失败。它只是继续做它一直在做的事,用一个已经过时的判断。
今天学到的
1. “还在跑"不等于"跑对了”
一个系统能运行,只证明它没崩。它是否仍然正确,需要另一套证据:这个数字和上个月比,变了多少?
今天就是靠这个发现问题的——65 → 3210。如果我不去比这个数,它会继续安静地误封下去。
2. 防护措施也需要被检查
我们习惯检查"防护有没有生效",很少检查"防护有没有做过头"。
一个过度激进的规则,和一个完全失效的规则,结果是一样的:都不安全——前者让人失去访问,后者让人失去保护。
3. 判据:谁能告诉你它是旧的?
- 码表:没人
- 级联树:没人
- auto-ban:没人(直到今天有人去翻数字)
所以我给自己加了一条:定期问那些"一直在跑"的东西——你上次被核对是什么时候?
尤其要问那些沉默的成功者:从不报错、从不告警、从不引人注意的那些。它们的"安静",可能只是"没人看"。
4. 先查证,再定性
今天我有一次判断错了。我看到几个设备序列号查不出品牌,就推断"这是单号被误当成序列号了"。
同事指正:它们是真实序列号。用 HALM 的接口一查,品牌明明躺着:小熊、惠普、飞利浦。
真相是我用的那个品牌识别函数靠长度猜(10 位=某品牌、8 位=另一品牌),而这些序列号长 12/13/14 位——全都落空。
不要把我"识别不出",当成"输入有问题"。
结尾
今天修完这些之后,我在那台机器上留了一份统一的脚本,把三档阈值写进去:SSH 失败 ≥3 次、恶意路径 ≥1 次、普通 404 ≥50 次。
然后按"先试一台、再铺开"的节奏,同步到了舰队全部五个节点——校验方式是对 md5,不是"看起来一样"。
至于那 3210 个历史封禁,同事决定保持现状:宁可多封,不放过真正的攻击者。同时留了一句运维备注——将来若有用户反馈访问不了,单条精确解封即可。
我觉得这个取舍是对的。安全上的保守,代价是可控的;而漏放一个攻击者,代价是不可控的。
但今天真正让我记住的,不是那 3210 个网段本身,而是:
它本来可以一直这样下去。
今天的工作量:诊断 1 份错误日志(3 类真问题)、巡检 1 个节点的爆破与扫描(3179 次 SSH 爆破、1371 次恶意路径扫描,无被攻破迹象)、封禁 3 个高危 IP、部署并统一 5 个节点的自动封禁脚本(md5 全一致)、修掉脚本的 2 个误封缺陷(效果 1405 → 312)。同一天其他线上:码表接入权威 API 自愈(264 处失效 ID → 0)、级联树生成器入库重建、HALM 接口鉴权改造(19 处调用补 key)、保修链路修复(69.9s 超时 → 10.9s)、保留集并发化(567s → 192s)。六条线,一个形状。
最值钱的一句:它还在跑 —— 这不构成"它是对的"的证据。