为什么光改 SSH 端口远远不够
几乎每个刚拿到 VPS 的站长,第一件事都是把 SSH 端口从 22 改成某个高位端口,然后觉得万事大吉。改端口确实能挡掉一部分无脑扫描脚本,但只要你把服务器放上公网几天,打开 journalctl -u sshd 或者 lastb,就会看到密密麻麻的失败登录记录——攻击者用的是全端口扫描,端口改到 59213 一样会被找到,只是时间问题。
真正稳定的做法是引入主动封禁机制。fail2ban 就是干这个的:它读取 SSH 的认证日志,发现某个 IP 在短时间内连续失败达到阈值,就调用防火墙把该 IP 加进黑名单一段时间。本文不讲「装个 fail2ban 就完事」,而是把 nftables 后端、自定义 jail、白名单防自锁、以及「封了但没生效」的排查完整讲一遍。
fail2ban 的三个核心概念
在动手之前,先把术语理顺,不然后面看配置文件会晕。
- filter(过滤器):一段带正则的规则,定义「什么样的日志行算一次失败」。fail2ban 自带
sshdfilter,通常不用自己写。 - jail(监狱):把 filter + 动作 + 阈值参数绑定在一起的一个监控单元。一个 fail2ban 实例可以跑多个 jail,比如 sshd、nginx-http-auth、nginx-botsearch。
- action(动作):命中阈值后执行什么。默认是调用 iptables/nftables 加规则,也可以发邮件、调 webhook。
日志来源方面,Debian/Ubuntu 的 sshd 走 systemd-journald,fail2ban 可以通过 backend = systemd 直接读 journal;CentOS/RHEL 走 /var/log/secure。这个区别很重要,写错 backend 会导致 fail2ban 完全读不到日志、静默不工作。
安装与选择 nftables 后端
Debian 11/12 与 Ubuntu 22.04 之后,系统默认防火墙已经是 nftables,iptables 只是兼容层。这时候让 fail2ban 直接用 nftables 后端更干净,避免规则混在两套体系里。
apt update
apt install -y fail2ban nftables
# 确认系统实际使用哪个防火墙后端
nft list ruleset | head
# 如果输出正常,说明 nft 在用;sbin/iptables 可能是 nft 的 wrapper
Debian 12 的 fail2ban 默认 banaction 是 iptables-multiport,但有 nftables-multiport 可用。建议显式指定,省得踩到兼容层的坑。
写一份干净的 jail.local
规矩:永远不要改 jail.conf,那是包管理器管的分发文件,升级会被覆盖。所有本地改动写在 /etc/fail2ban/jail.local。
[DEFAULT]
# 读 systemd journal 而不是文件
backend = systemd
# 用 nftables 后端
banaction = nftables-multiport
banaction_allports = nftables-allports
# 封禁时长(秒),这里 1 小时
bantime = 1h
# 统计窗口
findtime = 10m
# 窗口内失败次数阈值
maxretry = 5
# 忽略本机与内网,防止把自己锁死
ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8 172.16.0.0/12
[sshd]
enabled = true
port = ssh
logpath = %(sshd_log)s
# 更激进的楼:3 次失败就封
maxretry = 3
bantime = 24h
几个容易忽略的点:
ignoreip一定要包含你自己的固定办公 IP,否则某天连续输错密码就把自己关在门外,只能去控制台 VNC 救。bantime建议对 sshd 往长了设。真正在暴力破解的 IP 大多是肉鸡,封 24 小时比封 10 分钟划算得多。findtime与maxretry是一对。maxretry=3、findtime=10m 意思是十分钟内错三次就封。
启用并验证真的在跑
systemctl enable --now fail2ban
systemctl status fail2ban --no-pager
# 查看当前所有 jail 状态
fail2ban-client status
# 查看 sshd jail 详情:当前封了几个、封了谁
fail2ban-client status sshd
如果 fail2ban-client status sshd 返回「Sorry but the jail 'sshd' does not exist」,说明 jail 没加载成功,多半是 jail.local 语法错了,看 journalctl -u fail2ban 里的报错。
验证封禁是真的生效,而不是只写进日志
最坑的一步。fail2ban 显示「Banned IP list: 1.2.3.4」,不代表防火墙真加上了规则。用 nftables 时要查对应的 set:
# 查看 nft 里 fail2ban 的表
nft list table inet f2b-table 2>/dev/null
# 或者查 set 内容
nft list set inet f2b-table addr-set-sshd
正常应该能看到被封的 IP 在 set 里。如果看不到,检查两点:一是 banaction 名字拼写是否与 /etc/fail2ban/action.d/ 下实际存在的 action 文件名一致(去掉 .conf);二是 fail2ban 是否有权限操作 nft。用 fail2ban-client get sshd actions 可以看到当前 jail 实际用的是哪个 action。
手动测试:别等到真被攻击才发现没配好
可以主动触发一次封禁做验证。从另一台机器(或用手机热点)反复输错 SSH 密码 3 次,然后:
fail2ban-client status sshd
# 应看到 1.2.3.4 出现在 Banned IP list
# 手动解封(别忘了,不然那台机器一直进不来)
fail2ban-client set sshd unbanip 1.2.3.4
如果触发后没被封,八成是 filter 没匹配上日志。用 fail2ban-regex 直接测:
# 用 systemd journal 测
fail2ban-regex systemd-journal /etc/fail2ban/filter.d/sshd.conf
输出里会告诉你匹配了多少行、命中了哪条正则。如果 total 是 0,说明日志格式与 filter 不匹配,常见于改了 sshd 的 LogLevel、或者系统把日志写到了别处。
recidive:给惯犯加长封禁
普通 jail 封完就放,攻击者过一小时又来。fail2ban 自带一个 recidive jail,专门监控 fail2ban 自己的封禁日志,反复被封的 IP 会被封更久。
[recidive]
enabled = true
logpath = /var/log/fail2ban.log
# 一天内被 ban 3 次,就封一周
bantime = 1w
findtime = 1d
maxretry = 3
对长期在线的个人站来说,recidive 是性价比最高的一个附加 jail。
五个必踩的坑
- backend 写错导致静默不工作。 Debian/Ubuntu 上用文件 backend 读
/var/log/auth.log,而新系统该文件可能存在也可能空,导致永远 total=0。用backend = systemd最省心。 - ignoreip 漏掉自己的 IP 把自己锁死。 上线前一定把固定 IP 加进去,没有固定 IP 就至少确认有控制台可以救。
- 改了 banaction 却忘了动作文件里也写死了 iptables。 部分老 action 内部锁死 iptables,切 nftables 后要确认用的是 nftables-* 系列 action。
- 只封 SSH,忘了 Nginx 层面的爆破。 WordPress/Nginx 的登录爆破同样需要
nginx-http-auth、nginx-botsearch这些 jail 覆盖,否则网站后台照样被撞库。 - 把 fail2ban 当万能。 它只封固定 IP 的攻击。对分布式代理池、IPv6 海量地址效果有限。真正的底线仍是:禁用密码登录、只用密钥、禁用 root 直登。
最根本的一条:先关掉密码认证
fail2ban 是「事后封禁」,而禁用密码登录、强制公钥认证才是「事前堵死」。在 sshd_config 里:
PermitRootLogin prohibit-password
PasswordAuthentication no
PubkeyAuthentication yes
改完 sshd -t 校验语法,确认公钥能登录后再 systemctl reload sshd。配置了密钥认证之后,密码爆破在协议层面就被拒了,fail2ban 主要是用来处理「连密钥都没有还在硬撞」的骚扰流量,把噪声从日志里清掉。
把 nftables 后端、白名单、recidive 加长封禁三件事配齐,一台公网 VPS 的 SSH 面基本就安静了。剩下的精力,才轮到去折腾网站本身。