变更记录 #006:部署 fail2ban(含两次自我纠错)
已修复
| 项目 | 内容 |
|---|---|
| 日期 | 2026-09-13 |
| 环境 | Alibaba Cloud Linux 3.2104 / fail2ban 1.0.2 |
| 类型 | 加固部署(非故障复盘) |
| 风险等级 | 中(涉及 iptables 写规则) |
| 保护措施 | 死人开关 + 一键回滚脚本 + 白名单前置 |
| 服务中断 | 无 |
| 处理耗时 | 约 30 分钟 |
1. 背景:为什么需要 fail2ban
安全审计发现服务器正在被互联网扫描:
外部扫描来源:
5.135.x.x 2 次 preauth 断开
64.62.x.x 1 次 Invalid user
118.196.x.x 1 次 Invalid user + 1 次 preauth
205.210.31.205 活跃的 SSH banner 探测连接
当前防护(密码认证已关闭 + 仅密钥登录)已经能挡住入侵,但有两个残留问题:
- 扫描消耗资源:占用 sshd 进程、刷认证日志
- 流量成本:本机有 20GB/月 的公网流量免费额度,
而网站一旦在 80 端口对公网开放,扫描器会持续请求,累积消耗流量
注:扫描器消耗的主要是入站流量(通常不计费), 但它们的请求会触发出站响应(429 错误页),这部分是计费的。 虽然单个响应只有 162 字节,但被持续刷仍会累积。
2. 风险分析:为什么这次敢做
改动 iptables 是高风险操作。我给自己定的规矩是 "改防火墙规则需要你我同时在线"。所以先做了严格的风险区隔:
| 动作 | 风险 | 决策 |
|---|---|---|
| 安装 fail2ban 包 | 低(可 yum remove 回滚) | ✅ 做 |
| 配置 jail 对失败 IP 加 REJECT | 低(只影响被判定为恶意的具体 IP) | ✅ 做 |
| 修改 INPUT 策略为 DROP | 高(一条错规则即失联) | ❌ 不做 |
| 修改 sshd_config | 高(可能重启后无法登录) | ❌ 不做 |
关键区别:fail2ban 的做法是"对特定 IP 追加一条 REJECT", 而不是"把默认策略改成拒绝"。前者错了只影响一个 IP, 后者错了整台机器失联。
三重保护
- 死人开关:先用
at排程 3 分钟后自动回滚,验证通过再撤销 - 一键回滚脚本:
fail2ban-rollback.sh清理服务、封禁规则、iptables 链、ipset、软件包 - 白名单前置:启动服务之前先设好
ignoreip
3. 执行过程
3.1 白名单采集(最关键的一步,必须在启动前完成)
# 识别当前管理连接的来源
ss -tnp | grep ':22 ' | awk '{print $5}' | sed 's/:.*//'
→ 100.104.242.116 (阿里云 Workbench 代理)
→ ADMIN_IP (管理员家庭出口)
# 构建白名单
ignoreip = 127.0.0.1/8 ::1 ADMIN_IP 100.64.0.0/10 172.16.0.0/12
为什么顺序不能反:如果先启动服务再配白名单, 在这个间隙里若有失败计数累积,可能把自己封掉。
3.2 死人开关
job 2 at Sun Sep 13 15:31:00 2026 a root ← 3 分钟后自动回滚
然后启动服务,立即从外部发起新 SSH 连接验证:
SSH_STILL_WORKS
确认后才拆除:
已删除作业 2
at 队列已空,死人开关拆除完成
✅ 无回滚日志,说明未触发
3.3 验证封禁真的生效(不只看配置)
用 RFC5737 保留测试地址 192.0.2.1 做真实封禁测试:
fail2ban-client set sshd banip 192.0.2.1
iptables 中确实出现了规则:
-N f2b-sshd
-A INPUT -p tcp --dport 22 -j f2b-sshd
-A f2b-sshd -s 192.0.2.1/32 -j REJECT --reject-with icmp-port-unreachable
-A f2b-sshd -j RETURN
解封后规则被正确清除。这证明防护不止是"配置写了",而是真的在网层生效。
4. 自我纠错一:jail 级 ignoreip 是覆盖而非追加
问题
我在两个 nginx jail 里显式写了 ignoreip = 127.0.0.1/8 ::1。
验证时发现白名单不完整:
[sshd] ← 继承 DEFAULT,白名单完整
|- 127.0.0.0/8
|- 100.64.0.0/10
|- 172.16.0.0/12
|- ::1
`- ADMIN_IP
[nginx-limit-req] ← 显式设置,白名单被覆盖成只剩 2 项
|- 127.0.0.0/8
`- ::1
根因:fail2ban 的 jail 级 ignoreip 是覆盖语义,不是追加。 写了它,[DEFAULT] 里的白名单就全部失效。
后果(如果不修)
网站开放到 80 端口后,管理员自己浏览站点时若触发限流, 会被自己的 fail2ban 封禁 80/443 端口—— 不影响 SSH,但你自己打不开自己的网站,而且排查时会很困惑。
修复
删除 jail 级 ignoreip,让它继承 [DEFAULT]:
[nginx-limit-req]
|- 127.0.0.0/8
|- 100.64.0.0/10
|- 172.16.0.0/12
|- ::1
`- ADMIN_IP
修复后三个 jail 的白名单全部完整。
并把这个要求写进了 KNOWN-DEVIATIONS.md 的 DEV-004, 同时在 f2b-status.sh 中加入自动检查——白名单缺失时会告警。
5. 自我纠错二:三处"输出格式假设"错误
这次的验证脚本又踩了同一个坑,而且是第三次:
| 次数 | 场景 | 我的假设 | 实际输出 | ||
|---|---|---|---|---|---|
| 1 | 流量归因 | ss 输出含 State 列 | 加了 state established 过滤后不含该列 | ||
| 2 | 偏差检查 | systemctl is-enabled 输出是 disabled | 输出带换行,与 `\ | \ | echo unknown 叠加成 "disabled\nunknown"` |
| 3 | 本次 | fail2ban 白名单里有 127.0.0.1 | 被规范化为 127.0.0.0/8 |
本次的假警报:
⚠️ sshd 缺 127.0.0.1 ← 假警报
✅ sshd 含 ADMIN_IP
而实际白名单里明明有 127.0.0.0/8。
根因
fail2ban 会对 IP 做规范化:127.0.0.1 → 127.0.0.0/8。 我的 grep 用精确地址匹配,所以匹配不到。
修复
改用网段正则:
grep -qE '127\.0\.0\.(0/8|1)'
教训(这是第三次,必须形成检查习惯)
验证脚本的匹配逻辑,必须考虑命令输出可能被规范化/改写。 具体做法: 1. 先打印原始输出看一眼,再写匹配规则 2. 用正则容纳多种等价表示(127.0.0.1/127.0.0.0/8) 3. 假警报和漏报一样有害 —— 假警报会让人对告警脱敏
这个错误我在复盘 #004 里刚批评过("假告警会破坏对监控的信任"), 结果自己的验证脚本又制造了假警报。
6. 最终状态
服务: sshd=active nginx=active crond=active docker=active fail2ban=active
失败服务: 0
启用的 jail:
sshd — SSH 认证失败防护(aggressive 模式,含扫描器特征)
nginx-limit-req — 大量触发限流的 IP 封禁(保护流量额度)
nginx-botsearch — 探测不存在路径的扫描器
防护验证:
✅ 白名单完整(3 个 jail 均含本机回环 + 管理 IP + 内网段)
✅ 封禁功能实测有效(iptables 规则真实写入并可清除)
✅ INPUT 策略未被修改(仍为 ACCEPT)
✅ 站点正常(HTTP 200)
✅ SSH 连接正常
资源占用:
fail2ban 内存 ≈ 25.3 MB(RSS,含 Python 解释器)
cgroup 计 12.3 MB(更接近独占开销)
已知偏差登记:
DEV-003 INPUT 策略保持 ACCEPT(fail2ban 只做 IP 级封禁)
DEV-004 jail 级 ignoreip 必须留空(继承 DEFAULT)
7. 沉淀的经验
7.1 区分"追加"与"覆盖"语义
配置系统中"子级覆盖父级"很常见,但容易被忽略:
| 系统 | 语义 |
|---|---|
fail2ban ignoreip | 覆盖(jail 级会替换 DEFAULT) |
| systemd unit | 覆盖(drop-in 也是覆盖,但可分段) |
nginx add_header | 覆盖(子级声明会清掉父级) |
| sysctl | 覆盖(后加载的文件赢) |
实践建议:改任何"继承式配置"时,先验证继承是否真的发生了—— 不要假设。
7.2 验证必须用"行为"而非"配置"
本次如果只看 fail2ban-client status,会以为一切正常。 只有做了真实的封禁测试,才确认防护在网层生效。
这与复盘 #004 的教训一致: nginx -t 说语法对 ≠ 配置生效;systemctl reload 返回 0 ≠ 配置生效。
7.3 白名单必须前置
启动任何"会自动封禁来源"的服务之前, 白名单必须先配好。顺序反了就有自锁窗口。
7.4 危险变更的三件套
1. 死人开关 —— 排程自动回滚,验证通过再撤销
2. 回滚脚本 —— 一键完全恢复(含清理副作用)
3. 外部验证 —— 从另一台机器发起连接,不能只在本机测
第 3 条最容易被忽略也最重要: 本机 systemctl is-active sshd 返回 active 不代表外部连得进来。
7.5 假警报与漏报同样有害
三次"输出格式假设"错误中,两次造成了假警报。
假警报的实际危害:
- 让人对告警脱敏,进而忽略真告警
- 浪费排查时间
- 侵蚀对监控体系的信任 —— 这比一次漏报更严重
做法:写匹配规则前先看原始输出;用正则容纳等价表示; 把"验证脚本本身"也当作需要测试的对象。
附:fail2ban 运维速查
# 总览
fail2ban-client status
# 查看某 jail
fail2ban-client status sshd
# 手动封禁/解封
fail2ban-client set sshd banip 1.2.3.4
fail2ban-client set sshd unbanip 1.2.3.4
fail2ban-client unban --all
# 查看生效的白名单
for j in sshd nginx-limit-req nginx-botsearch; do
echo "[$j]"; fail2ban-client get $j ignoreip
done
# 查看 iptables 中的封禁规则
iptables -S | grep f2b
# 本项目工具
/root/ops-lab/bin/f2b-status.sh # 状态摘要 + 异常检测
/root/ops-lab/bin/fail2ban-rollback.sh # 完全回滚
# 日志
journalctl -u fail2ban -f
tail -f /var/log/fail2ban.log
注意:因为使用密钥登录,正常运维操作永远不会触发 sshd jail。 如果发现自己被 sshd jail 封禁,说明有异常情况——那才是真问题。