fail2ban 十天只记 1 次失败:先验证它到底在读哪份日志
加固完一台 Ubuntu 22.04 的云主机(1.7G 内存,公网直连),隔了十天回去看 fail2ban 的统计:
fail2ban-client status sshd
Status for the jail: sshd
|- Filter
| |- Currently failed: 1
| |- Total failed: 1
| `- Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd
`- Actions
|- Currently banned: 0
|- Total banned: 0
`- Banned IP list:
十天,1 次失败,0 次封禁。同一台机器的 sshd journal 有 444 条条目,其中 47 行是"连接被断开"、5 行是"Invalid user"、34 行是协议协商阶段被对面直接掐断。
两个数字差了一个量级。这时候最容易做的两件事——把 maxretry 从 5 改成 3,或者怀疑服务没启动——都是猜。下面四步能把"它在读什么、认什么、封没封、会不会被冲掉"逐条钉死,而且每一步都能复现。
一、先确认它读的是哪条流
backend = systemd 的 jail 读的是 journal,不是文件。这一条决定了后面所有排查方向:日志里没有、/var/log/auth.log 里也翻不到,都是正常的。
# /etc/fail2ban/jail.local
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
backend = systemd
[sshd]
enabled = true
backend = systemd
判断依据不在配置文件里,而在 status 输出的那一行:
- 走 journal 的 jail 显示
Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd - 走文件的 jail 显示
File list: /var/log/auth.log
两个反直觉的地方,我在 0.11.2 上都撞到了:
fail2ban-client get sshd backend
# ERROR NOK: ('Invalid command (no get action or not yet implemented)',)
fail2ban-client get sshd logpath
# No file is currently monitored
第一行报错不是配置错,0.11.2 根本没实现这个 getter;第二行说的是实话——systemd 后端没有文件可监控。看到 No file is currently monitored 就跑去 tail -f /var/log/auth.log 找线索,是最常见的白跑一趟。
二、把"读什么"和"认什么"分开验证
fail2ban-client status 只给汇总数,fail2ban-regex 能给每一行的去向。用同一个 journalmatch 跑一遍:
fail2ban-regex systemd-journal /etc/fail2ban/filter.d/sshd.conf \
--journalmatch "_SYSTEMD_UNIT=sshd.service + _COMM=sshd"
我这台机器的真实输出(截关键部分):
Failregex: 142 total
|- #) [# of hits] regular expression
| 6) [5] ^[iI](?:llegal|nvalid) user <F-USER>.*?</F-USER> from <HOST>...
| 19) [8] ^<F-NOFAIL>Received <F-MLFFORGET>disconnect</F-MLFFORGET>...
| 20) [38] ^<F-NOFAIL><F-MLFFORGET>(Connection closed|Disconnected)</F-MLFFORGET>...
| 21) [91] ^<F-MLFGAINED>Accepted \w+</F-MLFGAINED>...
Lines: 1006 lines, 137 ignored, 5 matched, 864 missed
三个数字要连起来读:
- matched 5:真正被当成"失败事件"的行,全部来自第 6 条正则(Invalid user)。
- ignored 137:命中了正则,但正则里带
<F-NOFAIL>标记,直接扔掉。第 19(Received disconnect)、20(Connection closed)、21(Accepted publickey)都是这一类。 - missed 864:连正则都没碰到,包括
kex_exchange_identification这种协议层报错。
注意 Lines: 1006 和 journalctl -u ssh -o short-iso | wc -l 得到的 444 不是一回事:journal 里有些条目带多行字段,fail2ban 按行读,所以行数更多。做对照时别拿这两个数互相校验。
三、几十行扫描痕迹,为什么只算一两次失败
把 journal 拉成一行一条,再按模式点一遍数:
journalctl -u ssh --no-pager -o short-iso > /tmp/ssh.log
wc -l /tmp/ssh.log
for p in "Invalid user" "Failed password" "Connection closed by" \
"kex_exchange_identification" "Received disconnect" \
"Accepted publickey" "Unable to negotiate" "banner exchange"; do
printf "%-38s %s\n" "$p" "$(grep -c "$p" /tmp/ssh.log)"
done
实测结果:
444 (总行数)
Invalid user 5
Failed password 0
Connection closed by 47
kex_exchange_identification 34
Received disconnect 9
Accepted publickey 93
Unable to negotiate 2
banner exchange 4
Failed password 是 0,这一行是全文的关键。因为 PasswordAuthentication no 之后,扫描器连口令阶段都进不去——它枚举用户名、被拒、断开,全程不会出现一条"密码错误"。而 fail2ban 的 sshd 过滤器,主要认的就是"密码错误"和"非法用户"这两类。
对应到日志行的处理方式:
| 日志行 | 是否计入失败 | 原因 |
|---|---|---|
Invalid user ubuntu from 117.157.89.25 |
计入 | 命中 failregex #6,这也是唯一贡献计数的一类 |
Failed password for root from ... |
计入 | 但本机 PasswordAuthentication no,实际 0 行 |
Connection closed by authenticating user root ... [preauth] |
不计入 | 命中 #20,带 <F-NOFAIL>,被归入 ignored |
error: kex_exchange_identification: Connection closed by remote host |
不计入 | prefregex 剥掉 error: 前缀后无匹配,计入 missed |
Received disconnect from ...: 11: Bye Bye [preauth] |
不计入 | 命中 #19,<F-NOFAIL>,正常断开不惩罚 |
Accepted publickey for root from ... |
不计入 | 命中 #21,自己的登录成功记录 |
Unable to negotiate with ... no matching key exchange method |
不计入 | missed,过滤器不认协商失败 |
结论有点反常识:在只允许密钥登录的机器上,fail2ban 的 sshd jail 基本处于空转状态。它统计的是"认证失败",而绝大多数扫描在认证之前就被掐断了。它记到的那 1 次,是窗口内唯一一条真正走到用户名枚举的请求。
顺带说清它怎么算"一次":同一连接的多行日志用 [preauth] 和内部的连接标识串起来,只有最后判定失败的那一行进计数器,findtime 内累计到 maxretry 才触发 banaction。所以调 maxretry 之前先确认有行能命中——没有命中行,把数字调到 1 也不会封任何人。
另一个要提前想清楚的代价:封禁粒度是 IP,不是行为。办公出口是 NAT 的时候,一个人连错几次,整间办公室一起被 REJECT 一小时。要么把自己的固定出口写进 ignoreip,要么把 bantime 压到几分钟级别,别一上来就 24 小时。
这也解释了 Total failed: 1 与 Invalid user: 5 的差距:计数器是内存里的状态,跟着 jail 的生命周期走,不是历史累计。我执行 fail2ban-client reload --restart sshd 之后,它立刻回到 Total failed: 0、Total banned: 1(这个 1 是我下面测试封禁留下的)。所以这个数字既不能当扫描量看,也不能当防护效果看。
四、验证封禁真的落到了 iptables 上
不封一次人,你永远不知道 banaction 通不通。用一个保留网段的地址做靶子,测完立刻解封:
fail2ban-client set sshd banip 203.0.113.7
iptables -S | grep -i f2b
-N f2b-sshd
-A INPUT -p tcp -m multiport --dports 22 -j f2b-sshd
-A f2b-sshd -s 203.0.113.7/32 -j REJECT --reject-with icmp-port-unreachable
-A f2b-sshd -j RETURN
三个细节值得记下来:
- 跳转在 INPUT 的第 1 位。
iptables -L INPUT -n --line-numbers里,f2b-sshd排在ufw-before-logging-input之前,所以封禁优先于 ufw 对 22 端口的放行,不存在"ufw 先 ACCEPT 导致 fail2ban 拦不住"的问题。 - 链只在第一次封人时才创建。封禁前
iptables -S | grep -ci f2b返回 0,执行reload --restart sshd之后仍然是 0,直到banip才出现。所以"看不到 f2b 链"不能证明 fail2ban 没生效,只能证明它从没封过人。 - REJECT 而不是 DROP,
--reject-with icmp-port-unreachable。对方会立刻收到拒绝,不会挂在超时里。
测完记得收:
fail2ban-client set sshd unbanip 203.0.113.7
fail2ban-client status sshd | tail -4
五、ufw reload 会不会把规则冲掉
网上一直有"ufw reload 会清掉 fail2ban 规则"的说法。我在同一台机器上实测了一遍:
iptables -S | grep -ci f2b # 封禁后为 3
ufw reload # Firewall reloaded
iptables -S | grep -ci f2b # 仍为 3
iptables -S INPUT | head -3
f2b-sshd 链和 INPUT 里的跳转都还在。这台机器是 ufw 0.36 + iptables 1.8.7(nf_tables 后端)。老版本或 legacy 后端上确实有人复现过规则被清,所以这条结论只对当前组合负责——升级或换后端之后,请自己再跑一次这两行,别直接搬。
六、报错与现象对照表
| 现象 | 原因 | 处理 |
|---|---|---|
get sshd backend 报 Invalid command (no get action or not yet implemented) |
0.11.2 未实现该 getter,不是配置错误 | 看 status 里的 Journal matches / File list 行 |
get sshd logpath 返回 No file is currently monitored |
systemd 后端不监控文件 | 属正常,不用去找 auth.log |
iptables -S | grep f2b 为空 |
从未封禁过,链未创建 | 用 banip 打一发再确认 |
reload 后 Total failed 归零 |
计数器在内存,随 jail 生命周期重置 | 历史统计看 journal,不看这个数 |
大量扫描行,Total banned 一直是 0 |
那些行带 <F-NOFAIL> 或 missed;密码登录已关 |
属正常,别急着调 maxretry |
banip 后对方仍能连 |
命中 ignoreip(默认含 127.0.0.1/8、::1)或规则被其他工具 flush |
fail2ban-client get sshd ignoreip;再查 INPUT 位次 |
fail2ban.log 里找不到 Jail 'sshd' started |
日志按周轮转,启动记录已归档 | zgrep -h "started" /var/log/fail2ban.log.*.gz |
status 报 jail 不存在 |
jail.d/ 里的同名段覆盖了 jail.local,或 enabled 未置 true |
fail2ban-client -d | grep -A6 "jail 'sshd'" 看合并后的结果 |
七、把自证变成一条命令
每次改完配置,用这段跑一遍,四个数对上就算通了:
set -u
echo "--- jail 读哪条流"
fail2ban-client status sshd | sed -n '/Filter/,/Actions/p'
echo "--- 最近 24h 的候选失败行"
journalctl -u ssh --since "-24h" --no-pager -o short-iso \
| grep -cE "Invalid user|Failed password"
echo "--- 过滤器实跑"
fail2ban-regex systemd-journal /etc/fail2ban/filter.d/sshd.conf \
--journalmatch "_SYSTEMD_UNIT=sshd.service + _COMM=sshd" | tail -4
echo "--- 规则是否落地"
iptables -S | grep -ci f2b
八、边界
- 本文所有输出都在 Ubuntu 22.04 + fail2ban 0.11.2 + ufw 0.36 + iptables 1.8.7 nf_tables 上得到。fail2ban 1.x 的命令与输出有变化(1.x 提供了
get backend),0.10 的 journalmatch 行为也不完全一致,换版本请重新验证。 - 只测了
sshd这一个 jail。nginx-*、postfix等 jail 的 failregex 与 NOFAIL 标记各不相同,不能套用本文的命中数结论。 - 容器里跑 fail2ban 且没有挂载宿主 journal 时,systemd 后端会直接读不到东西,那种场景要用
backend = polling加文件日志。 - 更重要的边界:fail2ban 是"事后计数"的工具,不是边界防护。这台机器上真正挡住扫描的是安全组只放行 22/80/443、ufw 默认拒绝、以及关掉密码登录。指望它替你挡量,会把配置调得越来越激进,最后误封自己。
回到开头那个数字。Total failed: 1 不是故障信号,也不是防护成绩单,它只说明:十天里只有一个请求走到了用户名枚举这一步,其余全在更早的阶段被拦掉了。这个结论比"把 maxretry 改成 3"有用得多。
- 点赞
- 收藏
- 关注作者
评论(0)