防线是会自己停摆的

三天内两处防线静默失效,都是我自己部署时验证过的——交互 shell 里全绿,cron 里悄悄死。是它们变心了吗?不,是我从没在真实的环境里验证过。

一、防线,也讲究长期的 commitment

5 台机器,我把"自动封禁脚本"当成给机器装的免疫系统—它们24/7运行在公网,每天几千到上万次的爆破/扫描,我总不能每次都手动去封吧。

所以几个月前,我在每台机器上部署了 auto-ban 脚本:定时扫描日志、识别爆破/扫描 IP、按照阈值封禁。

今天检查发现:其中一台的"免疫系统",两年来一直空转。

原因:它运行依赖的防火墙工具,在那台机器上根本不存在(cron 环境的 PATH 里找不到)。脚本的设计是"失败都不失败"——报错被 2>/dev/null 吞掉了,永久静默。

它每天按时启动,按时退出,日志里写"今天运行正常"。但实际上,一条规则都没生效。

不是防线变坏了,是我从没在真实的环境里验证过它能跑。

这周连续三次同类事故:

  • DE 节点:auto-ban 突然 FATAL,说的是"ipset 不可用"
  • LA 节点:auto-ban 卡死,另一个变体的问题
  • 更早的一台:guard 脚本跑在错的用户身份下,读都读不到自己的数据

二、两种"防线静默失效"的病理

病例 1:FATAL PATHShock 🫠

DE 的 auto-ban 脚本,每天 09:30 开始报:

[Oct 20 09:30:01] FATAL ipset 不可用

每小时一次,共 51 次。

根因:ipset 这个工具装在 /usr/sbin,但 cron 的默认 PATH 里没有 /usr/sbin。脚本里 ipset create ... 找不到命令,直接 FATAL 退出。

为什么没发现?因为部署那天我是在交互 shell(root 全环境 PATH)里验证的——一切正常。一旦走 cron(受限 PATH),当场死。

这是第三次踩 “cron PATH” 这个坑。前面两次是宝塔的 fail2ban 和另一个 guard 脚本。同一个坑,我摔了三次。

病例 2:锁死锁上的 Douglas 阻塞 🔒

LA 的 auto-ban 更隐蔽:进程没报错,但卡住了。

它调用 firewall-cmd --ipset --add-entry 来封 IP。这个调用每条要走一次 D-Bus。正常几条无所谓,但 LA 一天被扫描出 3571 个 IP,要一条条走 D-Bus——几百秒跑不完 → 进程挂住 → flock 锁不释放 → 之后每小时的定时任务都直接 skip(“另一个实例在跑”)。

结果:BANNED 0 条,而日志看起来还在"正常"输出。

Hook 是:卡住比报错更难发现。报错至少有信号,卡住只在日志末尾留一句 skip,还需要你主动去看。

修复(两台都改完)

  1. DE:脚本顶部加 export PATH=...,把 /usr/sbin 包进来。用 env -i 模拟 cron 环境实测通过,ipset 从 0 → 1066 条。
  2. LA:把 firewall-cmd 调用换成直接 ipset add(内核级,快 10 倍)。实测 108 秒跑完,3,571 条封禁 + state 同步。

三、为什么"测试时好的"会变成"运行时死的"

部署时的验证环境 ≠ 运行时的真实环境。

同一个脚本,我用 root 的全功能 shell 验证:PATH 全、权限全、环境变量全、依赖全。但上了 cron,它拿着一个被限制的 PATH 运行。唯一区别是我测试的那条路径和它实际跑的那条路径不同。

这不是脚本 bug——脚本逻辑本身没错。是部署流程的 bug:我把"我在交互 shell 里跑通了"当成了"它能跑"。

如果换成工程师的术语:这是我没有验证运行时依赖,只验证了逻辑依赖。

三条破法(教训)

  1. 验证必须用真实环境跑一遍:env -i(清空全部环境变量)模拟 cron,能在这种环境下 live true 的才算过
  2. 关键参数用绝对路径:不依赖 PATH(/usr/sbin/ipset 而不是 ipset)
  3. 别 2>/dev/null 吞掉 startup 错误:如果它死了至少告诉我一声,不然我根本不知道防线停了

四、今天顺手扫了一圈全舰队的攻防态势

排查完防线停摆,我做了一轮全舰队今日实况(只看今天):

节点SSH 爆破(今日)成功登录封禁中
宁波11,4651,134632
DE1,711501,066
AU1,24787278
SG52541106
LA0(SSH 在非标端口)403,571

所有成功登录都来自我自己的 IP(除了一次奇怪但无害的 root 密码登录,同网段有一个我们的客户端,判断是用户自己)。

Web 扫描:宁波主力站点 404 靠 WAF 全是 0;LA 没有 WAF,全靠 auto-ban 硬封 3571 IP 撑着。

结论:有,而且很活跃;但没有被攻破。 一个风险信号都没有。

但注意这个数字——如果防线的停摆没被发现,这些"封禁中"的数字,有的会是 0。


五、最让我后怕的一点:

“防线在工作的证据” 和 “防线在工作” 是两回事

回过头看,我靠着两样东西了解防线状态:

  • 脚本跑得欢:日志里有输出(“今天运行正常”)
  • 封禁列表有数字:DE 显示还残留着几条历史的封禁

但这两样都是**“防线在工作的证据”,而不是“防线在工作”本身。证据可以是旧的(残留的历史数字),也可以是假的(主动打印的成功字样)。防线本身,只有在真实封一次攻击者**时才被证明工作。

所以我今天给自己加了一条新判据:

封禁数长期为 0,本身就是一个警报——不是"日志安静"的平静,而是"防线悄悄停了"。

封禁数从 3000 掉到 5、从 5 掉到 0 这样的级差变化,也应该被当成独立信号触发告警。


六、本周还有一个让我后怕的假设

回过头看,今天五台机器的封禁数是:632 / 1,066 / 278 / 106 / 3,571。

如果防线的停摆再晚一周被发现,DE 的封禁数会从 948 自然衰减到 0(24 小时的 TTL 到期),而日志还在每天写"运行正常"。

那将成为我博客的又一篇——《它显示运行正常,其实是 0》。

那天可能就真的晚了。

一个自动化的部署,最危险的不是它失败(红色容易看见),而是它成功到看起来在运行,却一直什么都没做。

防线要靠持续的真实验证来维持,不靠部署一时的绿色标记。

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