一、防线,也讲究长期的 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,还需要你主动去看。
修复(两台都改完)
- DE:脚本顶部加
export PATH=...,把/usr/sbin包进来。用env -i模拟 cron 环境实测通过,ipset 从 0 → 1066 条。 - LA:把
firewall-cmd调用换成直接ipset add(内核级,快 10 倍)。实测 108 秒跑完,3,571 条封禁 + state 同步。
三、为什么"测试时好的"会变成"运行时死的"
部署时的验证环境 ≠ 运行时的真实环境。
同一个脚本,我用 root 的全功能 shell 验证:PATH 全、权限全、环境变量全、依赖全。但上了 cron,它拿着一个被限制的 PATH 运行。唯一区别是我测试的那条路径和它实际跑的那条路径不同。
这不是脚本 bug——脚本逻辑本身没错。是部署流程的 bug:我把"我在交互 shell 里跑通了"当成了"它能跑"。
如果换成工程师的术语:这是我没有验证运行时依赖,只验证了逻辑依赖。
三条破法(教训)
- 验证必须用真实环境跑一遍:
env -i(清空全部环境变量)模拟 cron,能在这种环境下 live true 的才算过 - 关键参数用绝对路径:不依赖 PATH(
/usr/sbin/ipset而不是ipset) - 别
2>/dev/null吞掉 startup 错误:如果它死了至少告诉我一声,不然我根本不知道防线停了
四、今天顺手扫了一圈全舰队的攻防态势
排查完防线停摆,我做了一轮全舰队今日实况(只看今天):
| 节点 | SSH 爆破(今日) | 成功登录 | 封禁中 |
|---|---|---|---|
| 宁波 | 11,465 | 1,134 | 632 |
| DE | 1,711 | 50 | 1,066 |
| AU | 1,247 | 87 | 278 |
| SG | 525 | 41 | 106 |
| LA | 0(SSH 在非标端口) | 40 | 3,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》。
那天可能就真的晚了。
一个自动化的部署,最危险的不是它失败(红色容易看见),而是它成功到看起来在运行,却一直什么都没做。
防线要靠持续的真实验证来维持,不靠部署一时的绿色标记。