Fail2ban 防爆破实战:从 SSH 默认规则到自定义 jail
任何一台开着公网 SSH 的机器,auth 日志里都不会缺席爆破流量:字典用户名、高频试密码,量大时既占连接数又吵告警。Fail2ban 是这类场景里最经久耐用的轻量解法——它不接管登录流程,只盯着日志,某个来源在时间窗内失败够多次,就调用防火墙把这个 IP 封一阵。本文把它的三段工作链路讲清,先把 sshd 的默认防护调到合理口径,再以「给 Nginx 写一个自定义 jail」为完整示例,把 filter 的正则写法与调试方法走通。示例环境为 Rocky Linux 9 + firewalld,Ubuntu 的差异主要是包名与默认 action。
工作原理:三段链路
Fail2ban 的结构可以拆成三个可独立替换的部件。第一段是日志来源:jail 指定要盯的日志,可以是文件(如 /var/log/secure、Nginx 的 access.log),也可以是 systemd journal(backend = systemd,RHEL 系上 sshd 建议用这个,免去路径与轮转的麻烦)。第二段是 filter:一组正则(failregex),逐行匹配日志,命中且捕捉到来源 IP(日志里的地址要写成 <HOST> 占位符)就记一次失败。第三段是 action:失败计数在 findtime 窗口内达到 maxretry,就执行封禁动作——本质是往 firewalld 或 iptables 里加一条拒绝规则,bantime 到期自动撤销。理解了这三段,配置项再多也只是给三段填参数。
前置条件
- 一台已开启 SSH 或 Nginx 并有真实日志产出的主机,具备 root 权限;
- 防火墙使用 firewalld(或 iptables,二选一,与后文 banaction 对应);
- 手边留一条不依赖 SSH 的管理通道(云控制台、VNC、带外管理口),防止把自己封在门外无人解封;
- 知道自家运维出口 IP 段,后面要加进白名单。
第一步:安装与配置组织
安装后先建立规矩:永远不改 /etc/fail2ban/jail.conf(包升级会覆盖它),定制全部写进 jail.local 或 jail.d/*.conf,后者的优先级高于前者。启动前先跑一遍配置自检,避免服务带错启动。
dnf install -y fail2ban fail2ban-firewalld
fail2ban-client -t # 配置语法自检,输出 OK 再启动
systemctl enable --now fail2ban
第二步:sshd 的 jail 口径
全局段先立三条底线:白名单(ignoreip)把本机与运维出口放进去;默认封禁动作选与本机防火墙一致的 firewalld 实现;开启递进封禁(bantime.increment),让反复试探的 IP 封禁时长指数增长,而不是每次都只封十分钟——这是默认配置里最值得改的一项。sshd 的 jail 本身参数克制即可:10 分钟内失败 5 次触发,首封 1 小时。
# /etc/fail2ban/jail.local
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 198.51.100.0/24
banaction = firewallcmd-ipset
bantime.increment = true
bantime.factor = 2
bantime.max = 1w
[sshd]
enabled = true
backend = systemd
maxretry = 5
findtime = 10m
bantime = 1h
三个时间参数的口径要分清:findtime 是计数窗口,maxretry 是窗口内允许的失败次数,bantime 是处罚时长。把 findtime 拉得很长、maxretry 设为 2 看似严格,实则会把输错两次密码的同事也送进去;生产口径宁可处罚时长递进,也不要把触发门槛压得过低。另外,Fail2ban 只对「失败留痕」的爆破有效;把 SSH 改成仅密钥登录、关掉密码认证,才是从根上消掉这类流量,两件事要一起做。
第三步:为 Nginx 写自定义 filter
默认 filter 覆盖不了业务日志,这才是 Fail2ban 真正的日常工作。假设要拦两类行为:扫描敏感路径(.env、.git、phpmyadmin 等,返回 404)和对登录接口的高频 401。新建 /etc/fail2ban/filter.d/nginx-bad-req.conf,正则里的日志地址位统一用 <HOST>,日期部分用 <SKIPLINES> 或直接从 IP 开始匹配,避免被各家日志格式的时间串绊住:
# /etc/fail2ban/filter.d/nginx-bad-req.conf
[Definition]
failregex = ^<HOST> - - \[.*\] "(GET|POST|HEAD) [^"]*/(\.env|\.git/|\.svn|phpmyadmin|wp-login\.php|xmlrpc\.php)[^"]*" 404
^<HOST> - - \[.*\] "POST /login[^"]*" 401
ignoreregex =
再在 jail.local 里启用它。Nginx 的 access.log 是文件日志,backend 用默认的 polling 即可(依靠 inotify,资源占用可控);阈值比 SSH 宽一些,因为正常用户也会偶发 404:
[nginx-bad-req]
enabled = true
filter = nginx-bad-req
logpath = /var/log/nginx/access.log
maxretry = 10
findtime = 10m
bantime = 1h
第四步:用 fail2ban-regex 把正则调到命中
自定义 filter 十有八九不是一次写对的,不要靠重启服务试错——fail2ban-regex 就是官方给的离线调试器:拿真实日志喂给 filter,它会逐条报告命中与漏检,还能打印被漏掉的行供你修正。用当天日志里已知有扫描流量的样本验证,命中数大于零且漏检行里没有目标行为,才算写对:
fail2ban-regex /var/log/nginx/access.log \
/etc/fail2ban/filter.d/nginx-bad-req.conf --print-all-matched
# 只看漏检行,针对性修正则:
fail2ban-regex /var/log/nginx/access.log \
/etc/fail2ban/filter.d/nginx-bad-req.conf --print-all-missed | head -50
调试时最常见的三个坑:日志格式与默认预期不一致(Nginx 改过 log_format、加了 X-Forwarded-For 后行首不再是 IP,正则要跟着改);转义偷懒(.env 的点没转义会误伤正常路径);贪婪匹配.* 跨过引号吃掉状态码。逐个对照漏检行修即可。改完重载对应 jail 生效,不必重启整个服务:
fail2ban-client reload nginx-bad-req
fail2ban-client status nginx-bad-req
第五步:封禁的查看、白名单与解封
日常运维就三个动作。看状态:fail2ban-client status 与带 jail 名的明细,能看到当前封禁列表与累计计数;查防火墙侧:firewalld 的 ipset 里应能找到对应地址,确认 action 真落了地而非只记在数据库里。解封:对误封走 unbanip,不要图省事去 flush 整个 ipset。白名单变更只改 jail.local 的 ignoreip 后 reload;注意 ignoreip 只对新触发的判定生效,已在封禁中的地址仍需手动解封。
fail2ban-client status sshd
fail2ban-client set sshd unbanip 203.0.113.99
# 人工封一个测试地址,验证 action 链路(验完即解封)
fail2ban-client set sshd banip 203.0.113.200
验证
用一台测试机(或本机别名地址)对 SSH 连续输错密码 5 次,观察:fail2ban-client status sshd 的 Banned IP list 出现该地址,firewalld ipset 中有对应条目,且该地址再连 SSH 直接被拒;bantime 到期后自动消失。再对 Nginx 用 curl 打两次敏感路径与若干次错误登录,确认自定义 jail 的计数在涨。两个 jail 都走通,链路才算完整。
常见问题
- filter 匹配不上、Failures 恒为 0:先用 fail2ban-regex 离线验证正则,不要在线上瞎猜;重点怀疑日志格式变化(特别是 access.log 加了自定义字段)和时间戳解析。journal 类日志则检查 backend 与 systemd 单元名是否对应。
- 把自己封了:多半是 ignoreip 漏写了当前出口 IP,或换了办公网络。用预留的控制台通道执行 unbanip 解封,再补白名单。切忌为了省事把 ignoreip 写成大段公网。
- 日志轮转后 jail 失效:文件型日志在轮转后 inode 变化,Fail2ban 一般能跟随,但若用了 copytruncate 且服务长期未重载,偶见跟丢。优先给关键 jail 用 backend = systemd(journal 无此问题),文件型则确保 logrotate 配置规范并定期看 status 里的日志路径状态。
- 封禁了但对方还能连:action 与实际防火墙不一致(系统跑的是 iptables 却配了 firewalld 的 banaction),或封禁规则被排在已有放行规则之后。用 status 看 Total banned 在涨、再去防火墙里实查规则是否存在,两段分别验证。
小结
Fail2ban 的价值不在默认那条 sshd 规则,而在「日志→正则→封禁」这条可复用的链路:任何把失败写进日志的服务,都能按本文的套路接进来。运维上守住三点即可长期受益:配置只写 jail.local、白名单先行且留带外通道、新 filter 一律先过 fail2ban-regex 再上线。它负责的是降噪与抬高爆破成本,不能替代密钥登录、最小暴露面这些基本功。







