fail2ban 本身不会丢弃任何数据包:它会调用一个 action,而协议和端口正是由那个 action 指定的。当 jail 报告已封禁、守护进程却仍在接受登录时,这次封禁通常是真实存在的,只是范围比日志暗示的更窄。通过另一种传输方式重连的客户端,永远碰不到已经安装的那条规则。在改动任何东西之前,先比较 action 装进去的东西与守护进程实际监听的东西,因为证据存在于 action 写下的防火墙链里,而不在 jail 列表里。

读防火墙链,而不是 jail 摘要

fail2ban 会把自己的链插在服务链之前,所以计数器列表显示的正是实际存在的东西。一条以 jail 命名的链,里面只放了一条协议列写着 udp 的规则,就足以完整解释为何 TCP 登录还在源源不断地到来。被拒绝的只有来自被封禁来源的 UDP 数据包。服务有一个 TCP 监听器,而那条链里没有任何东西碰到它。

也要留意相邻 jail 效应。一台主机看起来配置得完美无缺,可能只是因为另一个 jail 补上了缺失的那一半;而在没有安装那个 jail 的地方,同一份构建什么也不做。

action 只指定一种协议,问题就出在这里

每个 banaction 都是一个防火墙命令模板,协议放在其中一个占位符里。这种狭窄是有意为之,因为 Web jail 和 DNS jail 没有理由封禁相同的端口。当服务监听不止一种传输方式时,它就成了陷阱,而 VPN 守护进程经常如此。过滤器可能同时监视来自两种传输方式的失败登录,而 action 只覆盖其中一种。

SoftEther 和 OpenVPN 部署通常同时监听 UDP 与 TCP,而两种情况下日志行看起来一样,所以过滤器两边都监视。这种不对称从不出现在 jail 自己的报告里。fail2ban 正确统计了失败次数,随后却安装了一条只适用于其中一半流量的规则,而失败正是从那一半流量上来的。

让封禁覆盖两种传输方式

改在 jail 定义里修;手写的规则会被下一次重载打乱。写出两个 action,每种协议一个:

action = %(banaction)s[name=%(__name__)s-tcp, actionname=%(__name__)s-tcp, protocol="tcp", port="%(port)s", chain="%(chain)s"]
         %(banaction)s[name=%(__name__)s-udp, actionname=%(__name__)s-udp, protocol="udp", port="%(port)s", chain="%(chain)s"]

不要试图用 protocol = all 来缩短它。默认的 banaction 是一个 multiport action,它会展开成 -p <protocol> -m multiport --dports <port>;没有 L4 协议,端口匹配就无从检验,于是 iptables 会拒绝这条规则,而 jail 仍然报告封禁成功。像 iptables-allports 或 nftables-allports 这样的全端口 action 确实接受单条条目,但它会封禁该来源上的所有端口,那是另一个决定,需要自己的理由。把 protocol 保持在 tcp 或 udp,接受这两条条目。

要证明规则确实存在,而不是相信设置本身,因为在一条链承载它之前,设置描述的只是一种意图:

iptables -S f2b-vpn-tcp
iptables -S f2b-vpn-udp

每条链应当列出一条规则,第一条里是 -p tcp,第二条里是 -p udp,两条都指向被封禁的地址。然后确认同一个地址在两种传输方式上都被拒绝。一条你没在链里见过的封禁只是希望,下一次同样的症状会用掉同样的一小时。

封禁仍然漏掉的东西,以及下一个案例

action 所做的是拒绝单一来源,而且只限于其中列出的端口。它对不断更换地址的攻击者无能为力,也无法把一台恶意主机与数百名用户共用的运营商级 NAT 出口区分开来——在那里,一次封禁就会把什么也没做的人锁在外面。确保过滤器读取的是 客户端真正拥有的地址,而不是转发的头或代理地址。把时间窗口设得足够短,让错误自己过期。

这些都不构成跳过协议修复的理由;它恰恰说明要同时握住两个想法。封禁比 jail 摘要所暗示的更窄,而即便一次完整的封禁,也比威胁本身更窄。把防火墙链放在守护进程的监听列表旁边,逐协议对照,因为这两份列表决定了一次封禁究竟能否到达服务。

把重载命令和链的列表写进 jail 旁边的运行手册。下一个人两者都会需要,而 jail 摘要两个都不会告诉他。

相关文章