公网上的Ubuntu服务器,只要开了22端口,不出半天日志里就会堆满来自世界各地的SSH登录失败记录。这些不是偶然的试探,而是自动化脚本在持续进行暴力破解。放任不管的话,轻则系统资源被大量占用,重则一旦弱口令被猜中,整台机器就成了别人的肉鸡。处理这个问题最直接、最有效的工具就是fail2ban。它通过监控日志文件,匹配预设的失败模式,当某个IP在短时间内失败次数达到阈值,就自动调用iptables或ufw将其封禁一段时间。下面直接讲具体的配置思路和细节,不绕弯子。

安装与基础环境确认

在Ubuntu上安装fail2ban只需要一条命令:sudo apt update && sudo apt install fail2ban -y。安装完成后服务会自动启动,但默认配置只对SSH做了最基本的防护,阈值设置得比较宽松,建议根据实际情况调整。先确认一下服务状态:sudo systemctl status fail2ban。如果显示active (running),说明已经在工作了。fail2ban的核心配置文件是/etc/fail2ban/jail.conf,但官方强烈建议不要直接修改这个文件,因为软件包更新时会覆盖它。正确的做法是创建一个本地配置文件,fail2ban会优先读取.local后缀的文件。所以第一步就是复制一份:sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local。之后所有自定义配置都写在jail.local里,既安全又清晰。

SSH防护的核心参数调优

打开/etc/fail2ban/jail.local,找到[sshd]段落。默认配置下,bantime是10分钟,findtime是10分钟,maxretry是5次。这意味着10分钟内同一个IP失败5次,就会被封10分钟。对于暴露在公网的服务器来说,这个策略太温和了。攻击脚本的轮询间隔往往在几秒到几十秒之间,10分钟的封禁几乎不影响它们的工作节奏。建议把bantime提升到3600秒也就是1小时,甚至86400秒也就是24小时。findtime可以缩短到600秒,让监控窗口更紧凑。maxretry降低到3次,减少被试探的空间。修改后的配置段落大致如下:

[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
findtime = 600
bantime = 3600

这里有一个容易被忽略的点:logpath的路径必须和系统实际日志路径一致。Ubuntu默认的SSH认证日志写在/var/log/auth.log,如果你的系统做了日志集中采集或者使用了systemd-journald,需要相应调整。fail2ban也支持backend = systemd模式,直接从journald读取日志,对于高负载场景效率更高。在[sshd]段里加上backend = systemd,同时注释掉logpath即可。但要注意,使用systemd后端时,fail2ban对日志的解析依赖journalctl的权限,确保fail2ban服务有足够的权限读取日志。

自定义封禁动作与防火墙联动

fail2ban默认使用iptables-multiport作为封禁动作,这在大多数情况下够用。但如果服务器上同时运行着ufw,直接用ufw来管理封禁会更统一。在jail.local的[DEFAULT]段里,把banaction改为ufw:

[DEFAULT]
banaction = ufw
banaction_allports = ufw

改完之后,fail2ban封禁IP时就会调用ufw insert 1 deny from <IP>,解封时则删除对应规则。这样做的好处是,所有防火墙规则集中在ufw里管理,排查问题时一目了然。如果服务器上有Docker,需要注意Docker默认会绕过ufw直接操作iptables,可能导致fail2ban的封禁对容器端口不生效。这种情况下要么在Docker启动参数里加上--iptables=false,要么在fail2ban里使用更底层的iptables-allports动作,并在chain参数里指定DOCKER-USER链。这是一个比较进阶的场景,但实际部署中经常遇到,提前了解可以少走弯路。

白名单与忽略IP的精细化设置

封禁策略再激进,也不能把自己封在外面。在jail.local的[DEFAULT]段里有一个ignoreip参数,用来设置白名单。默认值是127.0.0.1/8 ::1,也就是本机回环地址。实际使用中,至少要把公司或家里的固定公网IP加进去,多个IP用空格分隔。如果有跳板机或者VPN网段,也一并加入。格式如下:

[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 你的固定公网IP 办公网段/24

这里有一个细节:ignoreip支持CIDR格式,所以可以把整个办公网段加进去。但要注意,如果IP是动态变化的,比如家庭宽带每次拨号IP不同,白名单就起不到作用。这种情况建议配置一个备用的远程管理通道,比如通过云服务商的控制台VNC登录,或者配置fail2ban的邮件通知功能,一旦被封还能收到告警。另外,ignoreip的匹配是在filter阶段之前进行的,也就是说白名单IP即使触发了maxretry也不会被封,这在调试阶段非常有用。

进阶filter定制与正则优化

默认的sshd filter位于/etc/fail2ban/filter.d/sshd.conf,里面定义了一系列正则表达式,用来匹配auth.log中的认证失败记录。大多数情况下默认规则够用,但如果遇到一些变种的暴力破解工具,它们可能在日志里留下不同的失败信息,导致默认filter漏报。可以通过自定义filter来增强匹配能力。在/etc/fail2ban/filter.d/下创建一个sshd-local.conf文件,内容可以继承默认filter并追加自己的规则:

[INCLUDES]
before = sshd.conf

[Definition]
failregex = ^%(__prefix_line)sFailed password for .* from  port \d+ ssh2$
            ^%(__prefix_line)sConnection closed by authenticating user .*  port \d+ \[preauth\]$
            ^%(__prefix_line)sUser .+ from  not allowed because not listed in AllowUsers$

然后在jail.local的[sshd]段里指定filter = sshd-local。这样既保留了原有规则,又扩展了新的匹配模式。编写正则时一定要在fail2ban-regex工具里测试,避免因为正则错误导致整个jail失效。测试命令:fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd-local.conf。输出结果会详细列出匹配到的行和未匹配的行,根据结果反复调整直到覆盖所有异常登录模式。

多服务联动防护与递归封禁

SSH只是入口之一,攻击者在暴力破解SSH失败后,往往会转向同一台服务器上的其他服务,比如MySQL、Postfix、Apache的登录入口。fail2ban内置了大量服务的jail配置,在jail.local里把对应服务的enabled设为true即可激活。常见的包括:

[proftpd]
enabled = true
port = ftp,ftp-data,ftps,ftps-data

[postfix]
enabled = true

[dovecot]
enabled = true

[mysqld-auth]
enabled = true
port = 3306

更进一步的策略是递归封禁。fail2ban支持在多个jail之间共享封禁信息,一旦某个IP在SSH上被封,同时也会在HTTP、FTP等其他服务上被封。这需要在jail.local里配置一个统一的封禁动作,或者使用fail2ban的recidive jail。recidive jail专门用来监控fail2ban自身的日志,如果一个IP在短时间内被多次封禁又解封,说明它是持续性攻击源,recidive会将其拉入更长时间的黑名单。配置如下:

[recidive]
enabled = true
logpath = /var/log/fail2ban.log
banaction = ufw
bantime = 604800
findtime = 86400
maxretry = 5

这个配置的含义是:如果一个IP在一天之内被fail2ban封禁了5次,就封它一周。这种阶梯式的封禁策略能有效对抗那些不断更换攻击节奏的顽固IP。

日志监控与效果评估

配置完成后,不能一设了之,需要持续观察fail2ban的工作状态。sudo fail2ban-client status sshd可以查看SSH jail的当前状态,包括封禁列表和统计信息。sudo fail2ban-client status会列出所有活跃的jail。如果想看实时封禁日志,tail -f /var/log/fail2ban.log即可。日志里会清晰记录每条封禁和解封动作的时间、IP和原因。通过分析这些日志,可以评估当前阈值设置是否合理。如果发现大量IP在解封后立刻再次被封,说明bantime设置太短;如果误封了正常用户的IP,说明maxretry太低或者ignoreip没配置全。根据实际数据做微调,而不是凭感觉设参数。

另外,fail2ban默认会把封禁信息记录在内存里,重启服务后封禁列表会丢失。如果服务器需要频繁重启,可以考虑启用fail2ban的数据库存储功能,将封禁状态持久化到SQLite中。在jail.local的[DEFAULT]段加上dbfile = /var/lib/fail2ban/fail2ban.sqlite3,并确保该目录可写。这样即使服务重启,之前的封禁记录依然有效,不会给攻击者留下重启后的真空窗口。

常见故障排查思路

fail2ban配置中最容易出现的问题集中在三个方面。第一是时区问题,fail2ban解析日志时默认使用系统时区,如果日志时间戳和系统时区不一致,会导致findtime计算偏差,该封的没封。检查系统时区用timedatectl命令,确保和日志中的时间戳一致。第二是日志轮转问题,logrotate在切割auth.log之后,fail2ban如果还在读旧的文件句柄,就会停止监控。现代版本的fail2ban已经通过pyinotify机制解决了这个问题,但如果用的是非常老的版本,需要确认一下。第三是防火墙规则冲突,尤其是同时运行iptables、ufw和Docker时,规则的插入顺序和链的归属容易混乱。排查时用sudo iptables -L -n -v查看当前规则列表,确认fail2ban添加的规则确实在正确的位置并且有数据包匹配计数。

还有一个容易被忽视的安全细节:fail2ban本身的管理端口。fail2ban默认不开启远程管理,但如果你通过fail2ban-client -s <socket>的方式暴露了管理接口,一定要做好访问控制,否则攻击者可能通过管理接口解封自己的IP。生产环境建议保持默认的本地socket通信方式,不要额外开启网络监听。

fail2ban不是一劳永逸的安全方案,它解决的是暴力破解这个特定场景的问题。对于SSH安全,还应该配合密钥登录、禁用root密码登录、修改默认端口等措施形成纵深防御。但就防暴力破解本身而言,一套调校得当的fail2ban配置,能把99%的自动化攻击挡在门外,让你的auth.log从每分钟几十条失败记录降到几乎为零。这种立竿见影的效果,正是它成为Linux服务器标配工具的原因。