公网上的Ubuntu服务器,只要开了SSH端口,不出半小时就能在日志里看到大量暴力破解尝试。攻击者用自动化工具不断尝试root、admin等常见用户名和弱密码,一旦得手,服务器就成了肉鸡。解决这个问题不能靠运气,必须从两个维度下手:一是用系统自带的ufw限制连接频率,让攻击者的尝试成本急剧上升;二是在应用层用fail2ban动态识别并封禁恶意IP。两者配合,才能把SSH暴力破解的风险降到最低。

先理解ufw的rate-limiting机制

ufw是Ubuntu默认集成的防火墙前端,底层调用iptables。很多人只用ufw做简单的端口放行或拒绝,却不知道它内置了一个非常实用的功能——limit。这个limit指令不是简单的“限制”,而是调用了iptables的hashlimit模块,实现基于IP地址的速率限制。

具体来说,当你在ufw中配置一条limit规则时,iptables会为每个来源IP维护一个令牌桶。默认参数是:每个IP在6秒内最多发起6个连接请求。超过这个速率后,后续的连接包会被直接丢弃,直到该IP的请求速率降回阈值以下。这个默认值对于SSH服务来说相当合理——正常管理员即便快速重连,也不会在6秒内连6次,而暴力破解脚本的特征恰恰是高频连接。

配置方法极其简单。假设你的SSH监听在默认的22端口,执行以下命令即可:

sudo ufw limit ssh

这条命令等价于:

sudo ufw limit 22/tcp

执行后,可以用以下命令查看规则是否生效:

sudo ufw status verbose

输出中会看到类似这样的条目:

22/tcp                     LIMIT IN    Anywhere

这里的LIMIT标识说明rate-limiting已经启用。此时任何IP如果在短时间内向22端口发起大量TCP连接,超出的SYN包会被直接丢弃。攻击者的扫描器会收到大量超时,扫描速度被强制拖慢,原本几小时能跑完的字典,现在可能需要几天甚至几周。

需要注意一个细节:ufw的limit规则只对新连接生效,已经建立的连接不受影响。这意味着正常登录的用户在会话过程中不会因为limit规则而被踢掉。另外,limit规则默认作用于INPUT链,针对的是入站连接请求,出站流量完全不受限制。

调整ufw limit的默认参数

ufw本身没有提供直接修改limit参数的命令行选项,因为它的limit功能直接映射到iptables的hashlimit模块,而ufw在生成iptables规则时使用的是硬编码的默认值。如果你需要更严格的限制,比如把6秒6次改成30秒3次,就需要直接操作iptables。

查看ufw生成的iptables规则:

sudo iptables -L ufw-user-input -v -n

你会看到类似这样的规则:

Chain ufw-user-input (1 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 ACCEPT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:22 ctstate NEW recent: SET name: DEFAULT side: source mask: 255.255.255.255
    0     0 ufw-user-limit  tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:22 ctstate NEW recent: UPDATE seconds: 6 hit_count: 6 name: DEFAULT side: source mask: 255.255.255.255
    0     0 ufw-user-limit-accept  tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:22 ctstate NEW recent: UPDATE seconds: 6 hit_count: 6 name: DEFAULT side: source mask: 255.255.255.255

这里可以看到seconds: 6和hit_count: 6就是默认参数。要自定义这些值,最稳妥的做法是直接在/etc/ufw/before.rules文件中添加自定义的iptables规则。在filter段落的适当位置加入:

# 自定义SSH速率限制:30秒内最多3次新连接
-A ufw-before-input -p tcp --dport 22 -m state --state NEW -m recent --set --name SSH_RATE
-A ufw-before-input -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 30 --hitcount 3 --name SSH_RATE -j DROP

保存后执行sudo ufw reload让配置生效。这样就把限制收紧到了30秒内最多3次新连接,对暴力破解的压制效果更加明显。

ufw limit的局限性

ufw的rate-limiting虽然简单有效,但它有一个明显的短板:它只能根据连接速率做无差别限制,无法识别登录失败的行为模式。一个攻击者可以放慢扫描速度,比如每10秒尝试一次密码,这样就能轻松绕过6秒6次的阈值。ufw limit对这种“慢速暴力破解”完全无能为力。

另外,ufw limit只在网络层工作,它看不到SSH应用层发生了什么。一个IP可能只是建立了TCP连接,但并没有真正尝试登录,也可能成功登录了,ufw limit对这些情况一视同仁。这就需要一个能解析应用日志、根据登录失败次数动态封禁的工具来补充——fail2ban正是为此而生。

fail2ban的工作原理与安装

fail2ban是一个用Python写的入侵防御工具,核心逻辑很简单:它持续监控指定的日志文件,用正则表达式匹配你定义的“失败模式”,当一个IP在设定时间窗口内触发了足够多次失败模式,fail2ban就自动调用iptables(或ufw、firewalld等后端)封禁该IP一段时间。

对于SSH暴力破解,fail2ban监控/var/log/auth.log,匹配类似"Failed password for root from 192.168.1.100"这样的日志行,提取出IP地址,然后累加计数。默认配置下,10分钟内出现5次失败尝试,该IP就会被封禁10分钟。

在Ubuntu上安装fail2ban非常简单:

sudo apt update
sudo apt install fail2ban -y

安装完成后,fail2ban会自动启动并加载默认的jail配置。你可以用以下命令检查服务状态:

sudo systemctl status fail2ban
配置fail2ban的SSH防护策略

fail2ban的配置文件位于/etc/fail2ban/目录。主配置文件是jail.conf,但官方强烈建议不要直接修改它,因为软件更新时可能被覆盖。正确的做法是创建一个jail.local文件,fail2ban会优先读取.local后缀的配置,其中的设置会覆盖.conf中的默认值。

创建并编辑jail.local:

sudo nano /etc/fail2ban/jail.local

一个针对SSH的实用配置如下:

[DEFAULT]
# 封禁时长,单位秒,默认600秒即10分钟,这里改为3600秒即1小时
bantime = 3600

# 查找失败的时间窗口,单位秒
findtime = 600

# 在findtime内允许的最大失败次数
maxretry = 3

# 忽略的IP列表,自己的固定IP或内网IP可以加在这里
ignoreip = 127.0.0.1/8 ::1

[sshd]
# 启用这个jail
enabled = true

# 监控的端口
port = ssh

# 日志文件路径
logpath = /var/log/auth.log

# 后端使用ufw
banaction = ufw

这个配置的含义是:在10分钟内,任何IP如果有3次SSH登录失败,就会被ufw封禁1小时。注意banaction设置为ufw,这样fail2ban会通过ufw来添加和删除封禁规则,而不是直接操作iptables。这样做的好处是规则管理更统一,用sudo ufw status就能看到fail2ban添加的封禁条目。

保存配置文件后,重启fail2ban使配置生效:

sudo systemctl restart fail2ban
验证fail2ban是否正常工作

配置完成后,需要确认fail2ban确实在监控SSH日志。使用fail2ban-client工具查看状态:

sudo fail2ban-client status

输出会显示当前活跃的jail列表:

Status
|- Number of jail:      1
`- Jail list:   sshd

进一步查看sshd jail的详细状态:

sudo fail2ban-client status sshd

输出示例:

Status for the jail: sshd
|- Filter
|  |- Currently failed: 0
|  |- Total failed:     12
|  `- File list:        /var/log/auth.log
`- Actions
   |- Currently banned: 0
   |- Total banned:     2
   `- Banned IP list:

这里可以看到当前没有正在被封禁的IP,但历史上有2个IP被封禁过,总共记录了12次失败尝试。如果Currently banned不为空,被封禁的IP会列在Banned IP list后面。

你也可以直接查看ufw的状态来确认fail2ban的封禁是否生效:

sudo ufw status numbered

fail2ban添加的规则通常带有注释,可以看到类似"f2b-sshd"的标识。

ufw limit与fail2ban的协同逻辑

现在你的服务器上同时运行着ufw limit和fail2ban,它们的分工非常清晰:

ufw limit在网络层做第一道防线。任何IP只要在6秒内发起超过6个SSH连接请求,超出的包直接被丢弃。这能过滤掉那些高速扫描器——它们往往在几秒内发出几十上百个连接请求,ufw limit直接让它们撞墙。攻击者的扫描器会收到大量超时,扫描效率断崖式下降。

fail2ban在应用层做第二道防线。那些放慢了速度、绕过了ufw limit的“慢速扫描器”,每次连接后尝试登录,一旦密码错误,fail2ban就会在auth.log中捕获到失败记录。3次失败后,fail2ban调用ufw把这个IP彻底封禁1小时。此时攻击者不仅无法登录,连TCP连接都建立不了。

这两层防护形成了互补:ufw limit处理高频攻击,fail2ban处理低频但持续的尝试。单独使用其中任何一个都有盲区,两者叠加后,攻击者的操作空间被极大压缩。

进阶调优:自定义fail2ban过滤器

默认的sshd过滤器已经能匹配绝大多数SSH登录失败的场景,但有些边缘情况可能被遗漏。比如,有些攻击者会尝试使用不存在的用户名,日志中会出现"Invalid user"而不是"Failed password"。fail2ban默认的sshd过滤器实际上已经覆盖了这两种模式,你可以在/etc/fail2ban/filter.d/sshd.conf中看到具体的正则表达式。

如果你需要更激进的防护策略,可以自定义过滤器。比如,有些扫描器会在建立连接后不做任何认证尝试就断开,这在auth.log中表现为"Connection closed by authenticating user"或"Did not receive identification string"。虽然这些行为不构成直接威胁,但大量出现时说明有人在探测你的端口。你可以创建一个专门的过滤器来封禁这类行为:

创建/etc/fail2ban/filter.d/ssh-aggressive.conf:

[Definition]
failregex = ^%(__prefix_line)sConnection closed by authenticating user .* <HOST> port \d+ \[preauth\]$
            ^%(__prefix_line)sDid not receive identification string from <HOST>$
ignoreregex =

然后在jail.local中添加对应的jail:

[ssh-aggressive]
enabled = true
port = ssh
logpath = /var/log/auth.log
maxretry = 5
findtime = 300
bantime = 7200
banaction = ufw

这样在5分钟内出现5次这类探测行为,IP就会被封禁2小时。不过要提醒一点,这种策略可能误伤一些网络不稳定的正常用户,生产环境中需要根据实际情况权衡。

日常维护与监控

安全策略部署后,定期检查是必要的。以下几个命令值得加入日常巡检清单:

查看当前被封禁的IP列表:

sudo fail2ban-client status sshd

手动解封某个IP(比如误封了自己的IP):

sudo fail2ban-client set sshd unbanip 192.168.1.100

查看auth.log中最近的失败登录记录:

sudo grep "Failed password" /var/log/auth.log | tail -20

查看ufw的完整规则列表,确认limit和fail2ban的规则都在生效:

sudo ufw status verbose

如果你使用了日志轮转,fail2ban默认能够正确处理logrotate后的日志文件,不需要额外配置。但如果你修改了日志路径或使用了非标准的日志格式,记得同步更新fail2ban的logpath和过滤器。

总结一下防护层次

部署完成后,你的SSH服务前面竖起了三道屏障:第一道是ufw limit的速率限制,专门拦截高速扫描;第二道是fail2ban的动态封禁,针对慢速但持续的暴力破解;第三道是SSH服务本身的安全配置——虽然本文没展开讲,但禁用root登录、使用密钥认证、修改默认端口这些措施同样重要。三层防护叠加,攻击者想要突破的难度呈指数级上升。绝大多数自动化扫描工具在遇到这种配置时会直接放弃,转向更容易得手的目标。你的服务器日志会清净很多,安全事件的风险也降到可控范围内。