Fail2ban 通常最先被装上,也通常就停在那里:包装好了,服务在跑,sshd 看上去已被覆盖。半年后才发现,那个 jail 正在读一个这套系统上根本不存在的文件,而其余的一切——邮件、控制面板、站点的登录表单——从来就没被覆盖过。

我们来看看在一台带站点的普通 VPS 上值得开启什么、该填什么数字,以及怎么确认一个 jail 是在工作而不只是存在于配置文件里。

先确认 sshd 是否抓到了任何东西

一条命令:

sudo fail2ban-client status sshd

输出里有 Currently failedTotal failedTotal banned 几行。如果一台已经在互联网上待了至少一天的服务器,Total failed 却是零,那说明这个 jail 没在工作。拿它跟现实比对一下:

sudo lastb | wc -l

lastb 里成千上万次失败尝试,对着 Fail2ban 里的零,只意味着一件事:过滤器在看错地方。

最常见的原因是 Debian 12。它默认不再安装 rsyslog,文件 /var/log/auth.log 干脆就不存在,而标准的 sshd jail 恰恰被配置成读那个文件。对此没有任何报错:服务启动了,状态显示出来了,计数器停在零。修法是改用 systemd 的日志:

[sshd]
enabled = true
backend = systemd

另一个选择是把 rsyslog 作为软件包装回来,如果纯文本的 auth.log 对其他工具是必需的。Ubuntu 24.04 的表现完全一样。

自己的设置放在哪里

别去动 /etc/fail2ban/jail.conf:包更新时它会被覆盖,你在那里做的每一处改动总有一天会悄无声息地消失。你自己的设置放进 /etc/fail2ban/jail.local——这个文件最后被读取,会压过公共的那个。

[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 203.0.113.25
bantime  = 1h
findtime = 10m
maxretry = 5
backend  = systemd

把自己的地址写进 ignoreip 不是奢侈而是保险:因为一个笔误把自己关在门外,比听上去要容易得多。如果没有固定地址,就在 SSH 之外留一条进门的路——服务商的控制台或者 VNC。

改变全局的那个 jail:recidive

普通 jail 的记性很短:十分钟内五次尝试,封一小时,一小时后一切从头开始。机器人对此活得很自在,明天后天照样回来。recidive 堵的正是这个缺口:它读的不是系统日志而是 Fail2ban 自己的日志,也就是说,它封的是 Fail2ban 已经封过的那些人。

[recidive]
enabled  = true
bantime  = 1w
findtime = 1d
maxretry = 5

可以这样读:「一天之内被封五次——消失一周」。这个 jail 需要文件 /var/log/fail2ban.log,所以如果你把 Fail2ban 自身的日志挪到了 syslog,那么 recidive 也会需要 backend = systemd

实践中这一个 jail 的效果,通常比把其余所有的都细调一遍还要明显:常客们消失了,日志里只剩下新鲜的背景噪声。

服务器上有站点时还该开启什么

按有用程度递减排列:

  • nginx-http-authapache-auth——针对基本认证的密码猜测。如果管理区或 staging 站点位于 Web 服务器口令之后,就需要它;
  • nginx-botsearch——对已知路径的扫描:/wp-login.php/phpmyadmin/.env。这不是入侵而是侦察,而它先于其他一切;
  • nginx-limit-req——只有在 Nginx 本身定义了 limit_req_zone 时才起作用;否则这个 jail 开着也是白开;
  • postfix-sasldovecot——如果你自己跑邮件,那是必需的。对邮箱的密码猜测持续不断,而通常根本没人盯着。
[nginx-botsearch]
enabled = true
logpath = /var/log/nginx/access.log

站点自身的登录表单是另一回事。应用没有自己的失败登录日志,而在 access.log 里,一次失败尝试看起来就是带 200 或 302 的普通 POST——任何通用过滤器都无法把它和成功的区分开。有两条路:要么让应用把失败写进 syslog(多数内容管理系统都有相应插件),要么限制到登录页的请求频率。第二种是限流器而不是探测器,这一点最好事先就知道。

那些数字

bantime = 10m 正是让人把 Fail2ban 判为无用的那个默认值:十分钟足够机器人回来。首次封禁一小时,再加上一周的 recidive,效果远好过永久封禁——后者久而久之会变成一份长得看不到头的规则清单。

SSH 用 maxretry = 3 是把自己关在门外的可靠办法。十分钟内五次尝试同样能很好地切断密码猜测。

缓慢的猜测——每五分钟一次——永远不会落进 findtime 里。这并不是把窗口拉长到一天的理由:你会得到针对自己同事的误封。对付缓慢猜测有效的不是阈值,而是关掉密码认证。

确认封禁真的作用到了数据包上

Fail2ban 只是调用一条外部命令。如果 banaction 与真正在过滤流量的东西不匹配,日志里会不断堆起欢快的 Ban 198.51.100.7,而数据包照样到达。Debian 12 默认使用 nftables,而在 UFW 开启时正确的选择是 banaction = ufw。检查方法:

sudo nft list ruleset | grep -c f2b
sudo iptables -S | grep f2b

两者中至少有一个应当显示出规则。而过滤器本身不必等到攻击就能测试:

sudo fail2ban-regex systemd-journal /etc/fail2ban/filter.d/sshd.conf

输出底部会写明匹配了多少行。零意味着过滤器和日志碰不到一起,再往下配置也没有意义。

Fail2ban 做不到的事

它防不住分布式猜测:一千个地址各试一次,无论怎么设置都到不了阈值。它不消除原因——被封的地址并不会让弱口令消失。它对信誉一无所知:昨天花了一整天去攻破别人服务器的地址,在它眼里依然干净,直到它开始对付你为止。由此才有了实践中的组合:用密钥代替密码,用 Fail2ban 当噪声限流器,用共享封禁清单(CrowdSec)当作别人经验的知识。

另有一个问题是,这一切偶尔总得有人看一眼。没有人会连着几周对每个 jail 敲 fail2ban-client status,而封禁数量的上升,往往在某个东西已经坏掉之后才被注意到。在下面的演示页面上,同样的数据被放在一个页面里:jail 列表、此刻谁被封着,以及这些尝试来自哪里。