ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Fail2Ban防SSH暴力破解:日志监控、阈值调优与排障实战

Fail2Ban防SSH暴力破解:日志监控、阈值调优与排障实战

1. 凌晨三点的登录日志,比任何告警都诚实

如果你维护过一台对公网开放的 SSH 服务器,大概率见过这种场面:早上打开终端,lastb输出刷了满满几屏,/var/log/auth.log里成片的Failed password for invalid user admin,用户名从root、oracle、test一路猜到pi。这不是针对你,扫描器是全互联网撒网的,任何一台开放 22 端口的机器上线几小时内就会被打上标记。默认配置下,密码登录开着、失败次数不限、日志没人看,等于把门虚掩着,然后祈祷没人拧把手。

Fail2Ban 就是解决这个问题的老牌工具。它不是防火墙,也不是入侵检测系统,它的定位可以概括成一句话:盯着日志看,发现某个 IP 在短时间内反复出错,就顺手把这个 IP 塞进防火墙黑名单里关一段时间。名字里的“Fail”指的是失败行为,“Ban”指的是封禁动作。它擅长处理 SSH 暴力破解、Web 登录爆破、邮件认证失败这类“单 IP 高频重试”的场景,配置门槛低,资源占用小,非常适合个人服务器、小团队项目机和内网跳板机。

这篇内容适合三类人看:刚接手一台云主机、只会ssh root@ip的新手;已经装了 Fail2Ban 但不确定参数是否合理、想知道为什么有时候封不住的中级运维;以及准备把防爆破策略标准化、需要一套可复现配置模板的团队负责人。我会从“为什么光改端口不够”讲到参数怎么算、配置怎么写、怎么测、怎么排查,最后给一份我自己在用的配置骨架,你可以直接抄。

2. 为什么只改端口、只换强密码,依然不够

2.1 暴力破解的真实成本已经低到可以忽略

很多人对暴力破解的印象还停留在“一个个试密码很慢”的阶段,实际上现在的扫描器根本不针对你。它们做的事情是:全网批量探测开放 22 端口的地址,拿到 banner 后,用一份包含几万条的“用户名 + 弱口令”字典做低频并发尝试,每个 IP 只试几次就换下一个目标。这种模式下,单看某一个 IP 的请求频率并不高,但累加起来,一台无人值守的机器一天被尝试几千次是常态。

我做过一个粗糙的统计:某台业务机在开放密码登录的 24 小时内,Failed password记录超过 8000 条,涉及 300 多个不同来源地址,尝试最多的用户名是root,其次是各种常见服务名和默认账户名。这个量级下,指望“密码足够复杂”就能高枕无忧,是把安全性完全押在单一维度上。密码强度、登录方式、暴露面收敛、失败响应,这几层应该是叠加的,而不是互相替代。

2.2 Fail2Ban 的工作模型:日志是唯一的信息源

理解 Fail2Ban,关键是理解它其实是一个“日志正则匹配器 + 动作执行器”的组合。它内部有三样东西:

  • Filter(过滤器):一组正则表达式,用来从日志行里抠出“攻击者 IP”和“这是一次失败尝试”这两个信息;
  • Jail(监狱):一个监控单元,绑定“哪个过滤器 + 看哪个日志 + 阈值多少 + 封多久 + 用什么动作封”;
  • Action(动作):封禁时具体执行的命令,通常是往防火墙里插一条规则,也可以是发邮件、写日志、调外部接口。

一次完整的判定流程是这样的:日志新增一行 → 过滤器命中 → 提取 IP → 该 IP 在findtime窗口内的失败次数加一 → 达到maxretry→ 执行action封禁bantime时长 → 到期自动解封。

这个模型的优点是通用性极强,任何会写日志的服务都能被纳管;缺点也同样明显:日志里没写的东西,它一概不知道。如果日志格式变了、日志路径错了、时区乱了、日志被轮转截断了,Fail2Ban 就变成了一个安静运行但什么也不做的进程。后面排查章节里八成的问题,根子都在这里。

2.3 它和防火墙、密钥登录之间是补充关系,不是替代关系

这里必须把定位说清楚,不然容易产生错误的安全感。

防护手段解决的问题局限
禁用密码登录、改用密钥从根上消除“猜密码”这条路径密钥管理不当、私钥泄露仍然有风险
改默认端口减少被无差别扫描命中的概率属于降低噪音,不是安全边界
防火墙白名单只允许可信来源访问移动办公、动态 IP 场景下维护成本高
Fail2Ban对高频失败来源做自动封禁,压缩攻击窗口依赖日志质量,对慢速分布式尝试基本无效

所以正确的组合姿势是:优先上密钥登录并把密码认证关掉,这是收益最大的一步;在此基础上升级 Fail2Ban 作为第二层,用来处理“密钥尝试失败”“非法用户探测”“协议层异常”这些残留噪音,同时把日志盯起来,让你知道有人在敲门。指望靠 Fail2Ban 单独扛住暴力破解,属于把补丁当承重墙。

还有一点经常被忽略:Fail2Ban 封的是新连接。当某个 IP 已经被封,它已有的 TCP 会话并不会立刻收到断开通知,而是后续数据包被拒绝,表现出来是会话卡死、输入无响应、最后超时断开。这个细节在排查“为什么封了对方还在线”时非常有用。

3. 动手之前:环境确认与几个关键选型

3.1 先搞清楚你的日志到底落在哪

Fail2Ban 的第一大坑就在这里。不同发行版、不同初始化系统的日志落点完全不同:

  • 传统 syslog 体系下,SSH 认证日志通常在/var/log/auth.log(Debian 系)或/var/log/secure(RHEL 系);
  • 纯 journald 体系下,可能根本没有上面这两个文件,日志只存在于journalctl里;
  • 部分精简系统同时开了 rsyslog 和 journald,两份日志都有,但要确认哪份是实时的。

先跑两条命令确认:

ls -l /var/log/auth.log /var/log/secure 2>/dev/null journalctl -u ssh --no-pager -n 5

第一条看文件在不在,第二条看 journal 里有没有 SSH 的认证记录。注意服务单元名:Debian/Ubuntu 上通常是ssh.service,RHEL 系是sshd.service,这个差异后面会直接影响 journal 后端的匹配。

确认完日志,再确认版本,因为 0.10 和 0.11 在配置项上差异不小:

fail2ban-client --version

0.11 及以上支持bantime.increment、bantime = -1(永久封禁)、nftables 原生动作,这些在 0.10 上要么没有,要么写法不同。心里有数,后面抄配置才不会踩空。

3.2 防火墙后端怎么选:iptables、nftables 还是 firewalld

Fail2Ban 的封禁动作最终要落到某种防火墙机制上,选错了会出现“日志显示已封禁,但规则没生效”的诡异现象。判断方法很简单,看你系统当前实际在用哪套:

# 看 nftables 有没有规则 nft list ruleset | head -20 # 看 iptables 有没有规则 iptables -S | head -20 # 看 firewalld 是否在跑 systemctl is-active firewalld

选型建议按这个顺序:

  • 系统已经在用firewalld(常见于 RHEL 系),就用banaction = firewallcmd-ipset,不要绕过它直接操作 iptables,否则 firewalld reload 会把你加的规则清掉;
  • 系统是nftables 原生(新版 Debian、部分容器宿主),用nftables-multiport或nftables-allports,比 iptables 兼容层干净;
  • 老系统或者不确定,用默认的iptables-multiport最稳,几乎所有发行版都能跑。

如果封禁频率高(比如每天上百次),建议额外启用 ipset 版本的动作。原因是纯 iptables 方案每封一个 IP 就往链里插一条规则,规则多了匹配开销线性增长;ipset 把 IP 放进哈希集合,匹配是常数级,规则数量再多也不影响性能。这是个在被打得比较狠的时候才会体会到差别的优化。

3.3 安装方式与最小依赖

主流发行版的仓库里都有现成包,直接装就行:

# Debian / Ubuntu apt update && apt install -y fail2ban # RHEL 系 dnf install -y fail2ban fail2ban-firewalld

装完先别急着改配置,看一眼默认服务状态和自带配置文件结构:

systemctl enable --now fail2ban ls /etc/fail2ban/

你会看到jail.conf、jail.d/、filter.d/、action.d/这几样。核心原则:永远不要直接改jail.conf,因为包升级会覆盖它,你所有的自定义配置会在一夜之间蒸发。正确做法是新建jail.local或往jail.d/里丢.local文件,加载顺序是jail.conf→jail.d/*.conf→jail.local→jail.d/*.local,后面的覆盖前面的。

我的习惯是全部写在/etc/fail2ban/jail.local一个文件里,结构清晰、迁移方便,换机器的时候复制一个文件就完事。

4. 配置逐项拆解:jail.local 里每一行到底在干什么

4.1 全局段 [DEFAULT]:三个时间参数怎么算才合理

先给一份可以直接用的骨架,然后逐项解释:

[DEFAULT] # 封禁时长:首次 1 小时 bantime = 1h # 统计窗口:10 分钟 findtime = 10m # 触发阈值:窗口内失败 5 次 maxretry = 5 # 白名单:本机回环 + 管理出口 ignoreip = 127.0.0.1/8 ::1 198.51.100.0/24 # 日志后端:使用 systemd journal backend = systemd

这三个参数不是拍脑袋定的,它们之间存在约束关系。核心逻辑是:maxretry次失败必须能在findtime内被观察到,否则永远不会触发。

举个反例:如果在findtime = 60(早期版本单位是秒)的情况下把它理解成 60 分钟,而bantime设成 10 分钟,就会出现“封禁时间比统计窗口还短”的荒谬情况——攻击者刚被封,窗口还没滑过去,人已经解封了。

我的经验取值是这样推的:先统计自己正常登录时输错密码的概率。绝大多数人一次登录最多错 1 到 2 次,所以maxretry = 5留了足够余量,不会误封自己;同时 5 次对于字典攻击来说又足够触发,因为扫描器通常是连续尝试几十上百次,5 次的门槛它们一定会撞上。

关于findtime,SSH 暴力破解的特点是短时间内密集尝试,10 分钟的窗口能覆盖绝大多数扫描行为,又不至于把“昨天下午输错两次、今天上午输错三次”这种跨天的偶发失败累加进去。

关于bantime,这里有个取舍。设短了(比如 10 分钟),攻击者等一会儿继续,封禁形同虚设;设长了(比如永久),万一误封了自己或者某个合作方出口 IP,处理起来麻烦。我的建议是首次封禁 1 小时起步,同时开启递增封禁,让反复作案的 IP 自动升级惩罚:

[DEFAULT] bantime.increment = true bantime.factor = 2 bantime.maxtime = 4w

含义是:同一个 IP 第二次被封禁时,时长按倍数放大,依次是 1h、2h、4h、8h……,最长不超过 4 周。这样既保留了误封的自愈能力,又让持续攻击者付出越来越高的代价。递增状态会记录在 Fail2Ban 的本地数据库里,重启不会丢失。

4.2 sshd 段:过滤器模式和日志来源的匹配

[sshd] enabled = true port = ssh filter = sshd logpath = %(sshd_log)s maxretry = 3 bantime = 24h

这里有几个点值得展开。

port = ssh是引用/etc/services里 ssh 对应的端口号。如果你改过 SSH 端口,这里必须写成具体数字,多个端口用逗号分隔,比如port = 2222,22。写错了会导致封禁规则只对默认端口生效,攻击者换个端口继续打,而你以为已经防住了。

filter = sshd用的是内置过滤器。这个过滤器其实有多个模式,通过方括号指定:sshd[normal]是默认级别,能覆盖最常见的失败密码、非法用户;sshd[ddos]更激进,会把连接层面的异常也算进去;sshd[aggressive]命中范围最广,包括“连接被关闭”“协议版本不匹配”这类行为,适合把扫描器也一起清掉,代价是误封概率略升。我在对外暴露的机器上一般用sshd[aggressive],因为在那台机器上,任何非预期连接都值得记一笔。

logpath = %(sshd_log)s是引用变量,实际值在paths-*.conf里定义。Debian 系是/var/log/auth.log,RHEL 系是/var/log/secure。用变量的好处是配置跨发行版可移植,不用手改路径。

如果你的系统只有 journald,没有日志文件,就要靠backend = systemd加journalmatch。这里有个非常容易翻车的细节:内置sshd过滤器的journalmatch默认写的是_SYSTEMD_UNIT=sshd.service,而 Debian/Ubuntu 上单元名是ssh.service,两者对不上,结果就是 Fail2Ban 运行正常、日志匹配零条、什么也不封。解决办法是在 jail 里显式覆盖:

[sshd] enabled = true backend = systemd journalmatch = _SYSTEMD_UNIT=ssh.service + _COMM=sshd

这个坑我至少在有那么几台机器上撞过,表现极具迷惑性:systemctl status fail2ban一片绿色,fail2ban-client status sshd显示Total failed: 0,只有把journalmatch改对之后数字才开始跳。

4.3 ignoreip 白名单:写法比你想的讲究

ignoreip支持单个 IP、CIDR 网段和 IPv6 地址,多条之间用空格分隔。写成逗号在某些版本上会被当成一个非法字符串,导致白名单整体失效——这是我在别人机器上见过最多的低级错误之一。

白名单里应该放什么:

  • 127.0.0.1/8和::1,本机回环,某些本地健康检查会走这里;
  • 你的固定办公出口 IP,或者跳板机的地址;
  • 监控探针、备份任务、自动化部署节点的来源地址;
  • 云平台的内网健康检查网段(如果你不确定,先别加,观察日志再说)。

如果你的管理出口是动态 IP,就别硬往白名单里塞了,改用“管理入口单独走一个非标准端口 + 密钥认证 + 只允许特定用户”这种更可控的方式。白名单写得越宽,等于防线的口子越大,这一点要时刻清醒。

4.4 封禁动作:从“只封”到“封 + 告警 + 留痕”

默认的action_是纯封禁,不产生额外信息。如果你想知道“谁在打、从哪来、什么时间”,可以换成带邮件或带 whois 的动作:

动作行为适用场景
%(action_)s仅封禁默认,最轻量
%(action_mw)s封禁 + whois 查询 + 邮件通知想有人盯着看的小规模环境
%(action_mwl)s上面两项 + 附带相关日志需要事后复盘的场景
%(action_allports)s封禁该 IP 的所有端口情节严重的重复攻击者

这里有个实打实的坑要提前说:带邮件的动作依赖本机 MTA。很多云主机没有装 sendmail 或 postfix,动作执行时会在发信这一步卡住甚至超时,表现出来是封禁延迟、日志里报错、严重时拖慢整个 Fail2Ban。我的建议是前期先用纯封禁动作,把日志盯住;确实需要告警,走“Fail2Ban 写 syslog → 日志采集端做匹配告警”这条链路,比在本机折腾 MTA 稳得多。

5. 把误封和漏封同时压下去的策略调优

5.1 先从日志里反推阈值,而不是抄别人的数字

参数没有普适最优解,得看你机器实际承受的流量。动手配之前,先做一次统计:

# 按小时统计失败次数,找出攻击高峰 grep "Failed password" /var/log/auth.log \ | awk '{print $1" "$2" "$3}' \ | uniq -c | sort -rn | head -20
# 统计攻击来源 IP 的分布 grep "Failed password" /var/log/auth.log \ | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' \ | sort | uniq -c | sort -rn | head -20

第一条命令告诉你攻击的时间分布:如果集中在某几个时段、每次几分钟内几百条,说明是典型的扫描器行为,findtime = 10m加maxretry = 5完全够用。第二条告诉你来源集中度:如果前 20 个 IP 占了总量的 80%,说明封禁策略会非常有效;如果 IP 极度分散、每个只试两三次,那 Fail2Ban 基本抓不到,这种情况下更该做的是关闭密码登录、上密钥、甚至只允许白名单访问。

这个“先看数据再定参数”的习惯,比任何配置模板都值钱。我见过太多人直接抄一份网上流传的配置,maxretry设成 10、bantime设成 600 秒,结果机器天天被打,还以为是工具不好用。

5.2 用 recidive 补上“屡教不改”的那一类

递增封禁解决的是“同一个 IP 反复触发同一个 jail”的问题,但有些攻击者会换着服务打——今天试 SSH,明天试 Web 登录,每个 jail 各自计数,永远到不了长期封禁的门槛。这时候可以启用recidive,它监控的是 Fail2Ban 自己的日志,专门抓“在多个 jail 里被反复封禁”的 IP,然后一次性封掉所有端口:

[recidive] enabled = true logpath = /var/log/fail2ban.log banaction = %(banaction_allports)s bantime = 1w findtime = 1d maxretry = 3

含义是:一天之内被任意 jail 封过 3 次的 IP,直接全端口封一周。

这个 jail 有个前置条件:Fail2Ban 的日志必须写到文件里(logtarget = FILE,这是默认值)。如果你为了省磁盘把日志改成了SYSLOG或NONE,recidive就会因为找不到日志文件而失效,而且它不会明显报错,只是安静地不工作。

5.3 和密钥登录、端口收敛配合,才是完整的组合拳

调参调到极致,也改变不了一个事实:Fail2Ban 是被动响应,攻击者总能先试几次。真正的防线应该是让“试”这件事本身失去意义。我的标准配置顺序是这样的:

  1. 生成密钥对,把公钥部署到目标账户的authorized_keys;
  2. 编辑/etc/ssh/sshd_config,设置PasswordAuthentication no、PermitRootLogin prohibit-password、PubkeyAuthentication yes;
  3. 改一个非默认端口(这一步主要是降噪,别当成安全措施);
  4. sshd -t校验配置语法,再systemctl reload ssh;
  5. 保留一个已登录的会话不要关,另开窗口验证新配置能连上,确认无误后再关旧会话;
  6. 最后上 Fail2Ban,把残留的探测行为收掉。

第 5 步是血泪教训。我早年改sshd_config的时候手快,把PubkeyAuthentication拼错成PubkeyAuthentcation,reload 之后所有连接被拒,机器直接锁死,最后靠控制台重置才救回来。从那以后我改 SSH 配置永远留一个活会话兜底,这个习惯救过我至少三次。

5.4 用一条命令看清当前到底封了谁

日常巡检不需要开日志翻,直接问 Fail2Ban:

fail2ban-client status fail2ban-client status sshd

第二条的输出大致长这样:

Status for the jail: sshd |- Filter | |- Currently failed: 3 | |- Total failed: 1247 | `- File list: /var/log/auth.log `- Actions |- Currently banned: 12 |- Total banned: 386 `- Banned IP list: 203.0.113.45 198.51.100.77 ...

这里几个数字的含义要分清:Currently failed是当前窗口内还没达到阈值的累计,Total failed是自启动以来的历史总数,Currently banned是当下正在封禁中的数量,Total banned是历史累计封禁次数。如果Total failed一直在涨但Total banned几乎不动,说明阈值设得太高或者过滤器没匹配上;如果Total banned涨得飞快但Currently banned很小,说明封禁时长短于攻击间隔,攻击者总能等到解封。

6. 从零跑通一遍:安装、测试、解封、持久化

6.1 最小可用配置的完整落地过程

假设是一台 Debian 系的新机器,完整流程如下。

第一步,安装并写入配置:

apt update && apt install -y fail2ban cat > /etc/fail2ban/jail.local <<'EOF' [DEFAULT] bantime = 1h findtime = 10m maxretry = 5 ignoreip = 127.0.0.1/8 ::1 backend = systemd bantime.increment = true bantime.factor = 2 bantime.maxtime = 4w [sshd] enabled = true port = ssh filter = sshd journalmatch = _SYSTEMD_UNIT=ssh.service + _COMM=sshd maxretry = 3 bantime = 24h EOF

第二步,校验配置语法。Fail2Ban 没有直接的configtest,但可以用-d把解析后的完整配置 dump 出来,任何语法错误都会在这一步暴露:

fail2ban-client -d > /tmp/f2b-dump.txt 2>&1 echo "exit: $?"

如果 dump 出来的内容里能看到sshd段的maxretry是你写的 3,说明配置被正确解析了。这一步能提前发现 90% 的配置问题,比启动后再排查省事得多。

第三步,重启并确认状态:

systemctl restart fail2ban systemctl status fail2ban --no-pager fail2ban-client status sshd

重点看输出里的File list或Journal matches是否指向你预期的位置,Total failed是否在几秒后开始增长。这两项都对了,才算真正跑通。

6.2 手动触发一次封禁,验证整条链路

光看状态不够,得实际验证“封禁动作真的写进防火墙了”。有两种验证方式。

第一种是手动调用接口,模拟一次封禁:

fail2ban-client set sshd banip 203.0.113.45

然后立刻检查防火墙:

# iptables 后端 iptables -S | grep f2b # nftables 后端 nft list ruleset | grep -A3 f2b

正常情况下能看到类似-A f2b-sshd -s 203.0.113.45/32 -j REJECT的规则。看到这条,说明从“配置解析 → 动作执行 → 防火墙写入”整条链路是通的。

第二种是真实触发。从另一台机器(不要用本机,本机 IP 在白名单里)故意用错误密码连几次:

ssh -o PreferredAuthentications=password wronguser@目标地址

连续输错 4 次(超过maxretry = 3),然后回目标机器看:

fail2ban-client status sshd

Banned IP list里应该出现那台测试机的地址,同时测试机再连就会超时或直接被拒。

验证完记得解封,不然测试机自己就被关在门外了:

fail2ban-client set sshd unbanip 203.0.113.45

6.3 解封、重载、重启,三者行为不一样

这几个命令经常被混用,但效果差别很大,值得单独说清楚:

命令作用是否保留封禁列表适用场景
fail2ban-client reload重新加载配置保留改完参数后首选
fail2ban-client restart重启服务进程保留(数据库恢复)配置大改或进程异常
systemctl restart fail2ban同上,走系统服务管理保留(数据库恢复)常规重启
fail2ban-client set <jail> unbanip <ip>解封单个 IP只影响该 IP误封处理
fail2ban-client unban --all清空所有封禁全部清空调试或误配后重置

“保留”依赖的是/var/lib/fail2ban/fail2ban.sqlite3这个数据库。如果你手动删了这个文件,或者目录权限不对导致写不进去,重启后封禁列表就会清空——这在容器化部署里特别常见,因为容器重建后这个路径往往是空的。所以容器方案要么把这个目录挂成持久卷,要么接受“重启后封禁清零”这个事实并做额外加固。

6.4 让规则在系统重启后依然生效

Fail2Ban 本身设了开机自启就够了吗?大多数情况下是的。但有两类情况需要额外确认:

一是防火墙服务启动顺序。如果系统同时启用了 firewalld 和 Fail2Ban,而 Fail2Ban 先于 firewalld 启动,它插入的规则可能被 firewalld 后续的初始化动作冲掉。确认方式很简单,重启后跑一次iptables -S | grep f2b或nft list ruleset | grep f2b,规则在就说明顺序没问题。不在的话,给 Fail2Ban 的服务单元加个After=firewalld.service的依赖覆盖。

二是云平台安全组与主机防火墙的叠加关系。云平台的安全组在主机外部生效,Fail2Ban 操作的是主机内防火墙,两者是串联的:安全组先过滤,放行的流量才轮到主机防火墙。这意味着你不能指望 Fail2Ban 去拦截那些被安全组已经挡掉的流量,反过来,安全组放行的来源如果被 Fail2Ban 封了,照样连不进来。排查“为什么封了还能连”的时候,先确认流量到底走到了哪一层。

7. 排查实录:那些日志看着正常但就是不封的时刻

7.1 状态显示正常,但 Total failed 永远是 0

这是最高频的问题,几乎全部指向“日志没读到”。按这个顺序排查:

第一步,确认日志源。先手动确认日志里确实有失败记录且是实时的:

tail -f /var/log/auth.log # 或 journalctl -u ssh -f

然后在另一个窗口故意输错一次密码,看日志有没有跟着刷新。如果日志本身不刷新,问题在 SSH 或日志服务,不在 Fail2Ban。

第二步,用 fail2ban-regex 单独验证过滤器。这个工具能直接告诉你正则到底命中了多少行:

fail2ban-regex /var/log/auth.log /etc/filter.d/sshd.conf

输出里的Lines和matched两个数字是关键。如果matched为 0,说明日志格式和你用的过滤器对不上,可能是 SSH 版本差异导致的措辞变化,也可能是日志经过了某种格式化处理。如果matched有数字但Failregex部分全是 0,说明命中了行但没提取出 IP,通常是正则的捕获组问题。

第三步,检查 journalmatch。就是前面说的单元名不匹配问题。验证方法:

journalctl _SYSTEMD_UNIT=sshd.service -n 5 journalctl _SYSTEMD_UNIT=ssh.service -n 5

哪条有输出,journalmatch就该写哪个。

第四步,检查 Fail2Ban 自身的日志。它的启动过程会打印每个 jail 的初始化结果,包括读了多少行、有没有报错:

journalctl -u fail2ban -n 50 --no-pager grep -i "error\|warning" /var/log/fail2ban.log | tail -20

常见的报错有:日志文件权限不足(Fail2Ban 以非 root 运行时)、过滤器文件语法错误、action 脚本找不到对应命令。

7.2 把自己封了怎么办

先别慌,也别急着重启服务器——重启很可能让事情更糟,因为封禁列表是持久化的,重启后照样封着。

如果你还能通过其他途径进入系统(控制台、VNC、云厂商的救援模式),直接解封:

fail2ban-client set sshd unbanip 你的IP fail2ban-client unban --all

如果进不去,就看有没有其他已建立的会话还活着。前面说过,封禁后已有连接会卡死但不会立刻断,如果那个会话还能响应,赶紧执行解封命令。

预防措施比补救重要。我的做法是三层保险:

  • ignoreip里放固定出口 IP;
  • 保留一个长期在线的管理通道(跳板机或控制台),不依赖单一入口;
  • 阈值不要设得太激进,maxretry低于 3 的时候,正常人手滑两次就危险了。

7.3 封禁规则在防火墙里看不到

分两种情况。

第一种是动作选错了后端。比如系统实际在用 nftables,但你配了banaction = iptables-multiport,Fail2Ban 会去操作 iptables 兼容层,规则确实写进去了,但nft list ruleset里看起来结构不一样,容易误判成没生效。用fail2ban-client get sshd actions能看到当前 jail 实际使用的 action 名称,对着系统实际情况核对一遍。

第二种是规则被其他组件清掉了。典型场景是 firewalld reload、Docker 重启、某些安全加固脚本重置防火墙。判断方法是看封禁时间和规则消失时间是否吻合。如果是,就改成与现有防火墙体系兼容的 action,或者干脆用 ipset 方案,把 IP 集合和防火墙规则解耦,减少被覆盖的概率。

7.4 容器环境里的特殊处理

在容器里跑 Fail2Ban 有几个绕不开的点:

  • 权限:需要NET_ADMIN能力才能操作网络规则,否则动作执行会失败;
  • 日志来源:容器里通常没有 SSH 日志,要么把宿主机日志目录挂进来,要么改用 journal 网关;
  • 规则可见性:容器内操作的是容器自己的网络命名空间,如果用的是 host 网络模式才能影响宿主机;用 bridge 模式时,封禁只对容器内部生效,对宿主机的 SSH 毫无作用;
  • 持久化:/var/lib/fail2ban/必须挂卷,否则每次重建容器封禁记录全丢。

坦白说,容器里跑 Fail2Ban 的性价比不高,除非你确实需要在容器边界做统一管控。更省心的做法是把 Fail2Ban 装在宿主机上,让它直接看宿主机日志、操作宿主机防火墙。

7.5 常见问题速查表

现象最可能原因快速验证处理方式
Total failed 恒为 0日志路径或 journalmatch 不对tail -f日志看是否刷新修正logpath或journalmatch
日志有记录但 matched 为 0过滤器正则不匹配当前日志格式fail2ban-regex跑一遍换 filter 模式或自定义 filter
显示已封禁但连不上的是自己白名单缺失fail2ban-client status sshd加ignoreip或临时 unban
封禁规则重启后消失数据库路径不可写或未持久化检查/var/lib/fail2ban/权限修正权限或挂载持久卷
封禁延迟很大动作里带邮件且无 MTA看 fail2ban.log 有无超时报错换纯封禁动作,告警外置
封了对方仍在尝试对方已建立会话,或换了源 IPipset list或iptables -S核对属正常现象,配合密钥登录加固
每天封几百个但攻击不停IP 分散,单点封禁收益低统计来源 IP 分布收敛暴露面,关闭密码登录

8. 我自己的配置习惯和几条不写在文档里的经验

配置写到最后,其实是在做取舍:封得狠一点,误封风险高一点;封得松一点,噪音多一点。我现在稳定用的一套是:findtime = 10m、maxretry = 3、首封1h、递增到4w封顶、filter = sshd[aggressive]、加recidive兜底、动作只用纯封禁不做邮件。这套参数在十几台机器上跑了两年多,误封自己的次数是两次,都是因为在咖啡厅用公共网络登录,后来把常用出口加进白名单就再没出现过。

有个小技巧值得分享:把fail2ban-client status sshd的输出做成一个定时任务写到本地文件里,每天扫一眼封禁数量的趋势。数量突然翻倍,通常意味着有新的扫描波段来了,或者你某个服务的暴露面变了。这个动作几乎零成本,但比任何告警系统都更早让你察觉到异常。

还有个容易忽略的点是日志清理。Fail2Ban 能正确跟随 logrotate 旋转,前提是轮转后新日志文件的属主和权限没变。有些自建的清理脚本用rm删日志再用touch新建,新建出来的文件属主是 root 且权限 644,Fail2Ban 以非 root 身份读不到,于是安静地停止工作。正确做法是用truncate -s 0原地清空,或者干脆别手动清理,交给 logrotate 管。

最后说一句实话:Fail2Ban 是个好东西,但它解决的是“自动化响应噪音”这个问题,不是“你的服务器安不安全”这个问题。真正的分水岭在于你有没有关掉密码登录、有没有收敛暴露端口、有没有人定期看日志。这三件事做到了,Fail2Ban 是锦上添花;做不到,Fail2Ban 只是一个让你安心的心理安慰。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进