-今日
-本周
-本月
-累计
项目工作台运维 个人项目与实践记录

变更记录 #006:部署 fail2ban(含两次自我纠错)

已修复

日期2026-09-13
环境Alibaba Cloud Linux 3.2104 / fail2ban 1.0.2
类型**加固部署**(非故障复盘)
风险等级中(涉及 iptables 写规则)
保护措施死人开关 + 一键回滚脚本 + 白名单前置
服务中断
处理耗时约 30 分钟
项目内容
日期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 探测连接

当前防护(密码认证已关闭 + 仅密钥登录)已经能挡住入侵,但有两个残留问题:

  1. 扫描消耗资源:占用 sshd 进程、刷认证日志
  2. 流量成本:本机有 20GB/月 的公网流量免费额度,

而网站一旦在 80 端口对公网开放,扫描器会持续请求,累积消耗流量

注:扫描器消耗的主要是入站流量(通常不计费), 但它们的请求会触发出站响应(429 错误页),这部分是计费的。 虽然单个响应只有 162 字节,但被持续刷仍会累积。

2. 风险分析:为什么这次敢做

改动 iptables 是高风险操作。我给自己定的规矩是 "改防火墙规则需要你我同时在线"。所以先做了严格的风险区隔:

动作风险决策
安装 fail2ban 包低(可 yum remove 回滚)✅ 做
配置 jail 对失败 IP 加 REJECT低(只影响被判定为恶意的具体 IP)✅ 做
修改 INPUT 策略为 DROP高(一条错规则即失联)不做
修改 sshd_config高(可能重启后无法登录)不做

关键区别:fail2ban 的做法是"对特定 IP 追加一条 REJECT", 而不是"把默认策略改成拒绝"。前者错了只影响一个 IP, 后者错了整台机器失联。

三重保护

  1. 死人开关:先用 at 排程 3 分钟后自动回滚,验证通过再撤销
  2. 一键回滚脚本fail2ban-rollback.sh 清理服务、封禁规则、iptables 链、ipset、软件包
  3. 白名单前置:启动服务之前先设好 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.1127.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 封禁,说明有异常情况——那才是真问题。