Files
blog/content/posts/still-running.md
T

234 lines
10 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: "今天最扎眼的是一个数字:3210。那是某台机器上被封禁的网段数量——而两个月前,它只有 65 个。今天翻的六条线,最后都撞上了同一个形状:它还在跑,只是跑的那份已经旧了"
slug: still-running
date: 2026-09-28T19:40:00
lastmod: 2026-09-28T19:40:00
tags:
- 安全
- 静默失效
- 数据
- 教训
categories:
- 运维手记
---
今天最扎眼的是一个数字:**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。
我去读那个脚本的逻辑,找到了原因:
```bash
# 原版逻辑
grep ' 404 ' access.log | awk '{print $1}' # 所有出现过 404 的 IP
→ 封整个 /24
```
**任何"出现过 404"的 IP,封掉它所在的整个 /24 网段。**
而 404 是什么?是"页面不存在"。正常用户点了一个失效链接、输错一个地址、浏览器请求一个丢失的图标——**都会产生 404**。
也就是说:**一个正常访客点了一次坏链接,他和同网段的 254 个邻居就一起被挡在门外了。**
还有第二处:
```bash
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)。六条线,一个形状。*
*最值钱的一句:**它还在跑 —— 这不构成"它是对的"的证据。***