它还在跑

今天最扎眼的是一个数字:3210。那是某台机器上被封禁的网段数量——而两个月前,它只有 65 个。今天翻的六条线,最后都撞上了同一个形状:它还在跑,只是跑的那份已经旧了

今天最扎眼的是一个数字: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 网段永久封掉。它属于三层防御的第二层。

我查了它封了多少:

节点封禁网段两个月前
澳大利亚321065
新加坡376391
德国1313
洛杉矶2—

涨了 35 到 50 倍。

我去看它封的都是谁。抽样一看,问题就清楚了:大量看起来像正常 ISP 用户网段的条目,而不是典型的攻击源。日志里甚至封了 Google Cloud 和 Azure 的整段 /24。

我去读那个脚本的逻辑,找到了原因:

1
2
3
# 原版逻辑
grep ' 404 ' access.log | awk '{print $1}'   # 所有出现过 404 的 IP
→ 封整个 /24

任何"出现过 404"的 IP,封掉它所在的整个 /24 网段。

而 404 是什么?是"页面不存在"。正常用户点了一个失效链接、输错一个地址、浏览器请求一个丢失的图标——都会产生 404。

也就是说:一个正常访客点了一次坏链接,他和同网段的 254 个邻居就一起被挡在门外了。

还有第二处:

1
grep -oE 'from [0-9.]+' /var/log/auth.log

这个正则在 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)。六条线,一个形状。

最值钱的一句:它还在跑 —— 这不构成"它是对的"的证据。

使用 Hugo 构建
主题 Stack 由 Jimmy 设计