服务器暴露在公网,SSH 端口每天被尝试爆破几百上千次很正常。看日志就知道:
grep "Failed password" /var/log/auth.log | wc -l
密钥登录能挡住这些尝试(见 VPS 配置 SSH 密钥登录),但爆破流量本身还在占你的带宽、撑大日志、消耗 CPU。Fail2Ban 解决的就是这件事:发现某个 IP 反复失败,就把它封一段时间。
它是安全三件套里的最后一块 —— 密钥登录负责"进不来",防火墙负责"只开该开的端口",Fail2Ban 负责"把反复试探的踢出去"。
逻辑很简单:
- 读日志(或 systemd journal),用正则匹配失败记录
- 同一个 IP 在
findtime时间内失败超过maxretry次 - 调用防火墙把该 IP 封禁
bantime时长
注意第 3 步:Fail2Ban 自己不实现封禁,它只是调用 iptables(或 nftables、或者写入 hosts.deny)。 明白这一点,后面"为什么配了没生效"就好排查了 —— 往往是 action 那层出了问题。
装之前先看一眼真实的爆破规模,心里有个数:
sudo grep -c "Failed password" /var/log/auth.log
sudo grep "Failed password" /var/log/auth.log | awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | head -10
第二条会列出尝试次数最多的 IP。
sudo apt update
sudo apt install -y fail2ban
Ubuntu 自带的是 1.0.2(上游最新是 1.1.1,发布于 2026 年 8 月)。发行版版本偏旧是正常的 —— 它跟着安全更新走,稳定性比新特性重要。想用新版本得自己装,一般没必要。
sudo systemctl enable --now fail2ban
sudo systemctl status fail2ban
装完默认只启用了 sshd 这一个 jail,而且用的是相对宽松的默认值。要调整就得写自己的配置。
这是 Fail2Ban 最重要的一条规矩:
| 文件 | 作用 |
|---|---|
/etc/fail2ban/jail.conf | 发行版提供的默认值,升级时会被覆盖 |
/etc/fail2ban/jail.d/*.conf | 软件包提供的附加配置 |
/etc/fail2ban/jail.local | 你自己的配置,优先级最高 |
永远不要改 jail.conf。 升级一次你的改动就没了。所有自定义都写进 jail.local。
sudo tee /etc/fail2ban/jail.local > /dev/null <<'EOF'
[DEFAULT]
# 封禁时长(首次)
bantime = 1h
# 统计窗口
findtime = 10m
# 窗口内失败多少次就封
maxretry = 5
# 递增封禁:反复来的 IP 越封越久
bantime.increment = true
bantime.factor = 1
bantime.maxtime = 1w
# 白名单:自己的固定 IP 写这里,避免把自己封了
ignoreip = 127.0.0.1/8 ::1
[sshd]
enabled = true
backend = systemd
EOF
sudo systemctl restart fail2ban
sudo fail2ban-client status
| 参数 | 默认值 | 含义 |
|---|---|---|
bantime | 10m | 封禁多久 |
findtime | 10m | 在多长时间窗口内统计 |
maxretry | 5 | 窗口内失败几次就封 |
ignoreip | 127.0.0.1/8 ::1 | 永不封禁的地址 |
默认的 bantime = 10m 太短了。 封十分钟对自动化爆破脚本毫无威慑 —— 它们换个 IP 继续,或者十分钟后再来。建议至少 1h,配合递增封禁效果更好。
三种参数组合的取向:
| 取向 | 配置 | 效果 |
|---|---|---|
| 宽松(怕误伤) | maxretry=10, findtime=10m, bantime=30m | 只挡最明显的爆破 |
| 平衡(推荐) | maxretry=5, findtime=10m, bantime=1h | 挡住绝大多数自动化 |
| 严格 | maxretry=3, findtime=1h, bantime=1d | 挡得狠,也更容易误伤 |
如果用了动态 IP 或者经常换网络,别用严格档,很容易把自己封在外面。
bantime.increment = true
bantime.factor = 1
bantime.maxtime = 1w
开启后,同一个 IP 被封的次数越多,下次封得越久 —— 按 1、2、4、8、16 倍的指数增长,用 bantime.maxtime 设个上限(不设的话会一直翻倍下去)。
公式在官方配置里写得很清楚:
bantime.formula = ban.Time * (1<<(ban.Count if ban.Count<20 else 20)) * banFactor
这个功能很实用:偶然输错密码的真人被封一次 1 小时就够,而反复来试探的脚本会被逐渐封到一周,等于劝退。
还有个 bantime.rndtime 可以给封禁时间加随机抖动 —— 防止僵尸网络精确算出解封时间,卡着点回来。
ignoreip = 127.0.0.1/8 ::1 203.0.113.5 198.51.100.0/24
支持单个 IP、CIDR 网段,也支持域名。 多个值用空格分隔,也可以分行写。
几个该加进白名单的:
- 你自己的固定 IP(家里/公司有固定 IP 的话)
- 运维跳板机的 IP
- 监控系统(Uptime Kuma、Zabbix 这类)的 IP
- 内网网段
还有一个容易被忽略的选项 ignoreself,默认是 true —— 自动忽略本机自己的 IP,防止服务器自己访问自己触发封禁。保持默认开着就行。
已经把自己封了怎么办? 进 VNC 控制台执行 fail2ban-client set sshd unbanip 你的IP,完整自救流程见 VPS 被 Fail2Ban 误封怎么办。
[sshd]
backend = systemd
| backend | 读哪里 | 说明 |
|---|---|---|
systemd | systemd journal | 现代系统推荐 |
auto | 自动选择(默认) | 优先 pyinotify,否则轮询 |
pyinotify | 日志文件(inotify 监听) | 需要 python3-pyinotify |
polling | 日志文件(轮询) | 兼容性最好,有延迟 |
用 systemd 的理由: 现代 Ubuntu/Debian 上很多日志已经不在 /var/log/auth.log 里了(或者格式变了),journal 才是完整的来源。而且 auto 在文件不存在时会静默失效 —— 你以为什么都配好了,其实一条日志都没读到。
用了 systemd backend 就不需要写 logpath,改用 journalmatch。内置的 sshd filter 已经配好了 _SYSTEMD_UNIT=sshd.service,通常不用手动指定。
从文件换到 systemd 之后一定要验证能读到日志,方法见下面"验证"一节。
配完不验证,等于没配。
第一步:确认 jail 真的启用了
sudo fail2ban-client status
# 应该列出 sshd
sudo fail2ban-client status sshd
# 会显示当前封禁数量、总封禁数、以及被封的 IP 列表
第二步:确认能匹配到日志
这是最关键的一步。 用内置 filter 对着真实日志跑一遍,看能不能匹配上失败记录:
# 用 systemd journal
sudo fail2ban-regex systemd-journal /etc/fail2ban/filter.d/sshd.conf
# 或者指定日志文件
sudo fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf
输出里关注 Lines: ... matched 那部分 —— 匹配数是 0 就说明 filter 没生效,封禁永远不会触发。
想看得更细,把匹配到的行打出来:
sudo fail2ban-regex --print-all-matched systemd-journal /etc/fail2ban/filter.d/sshd.conf | head -30
第三步:确认封禁动作能执行
# 看当前的 iptables 规则里有没有 f2b 链
sudo iptables -L -n | grep -A5 f2b
正常应该能看到 f2b-sshd 这样的链。看不到就说明 banaction 那层有问题 —— 通常是没装 iptables,或者系统用的是 nftables 而 action 没配对。
第四步:故意触发一次
用一台测试机(或者自己的 IP,记得随后解封)连错几次密码:
# 在测试机上故意输错
for i in $(seq 1 6); do ssh wronguser@your-server; done
然后在服务器上看:
sudo fail2ban-client status sshd
# 应该能看到那个 IP 出现在 Banned IP list 里
第五步:看日志确认
sudo tail -f /var/log/fail2ban.log
journalctl -u fail2ban -f
会看到 Ban、Unban 的记录。
sshd 只是最基础的一个。Fail2Ban 自带了几十个 filter,常见的:
ls /etc/fail2ban/filter.d/ | head -40
[nginx-http-auth]
enabled = true
port = http,https
logpath = %(nginx_error_log)s
maxretry = 5
bantime = 1h
[nginx-botsearch]
enabled = true
port = http,https
logpath = %(nginx_error_log)s
[wordpress]
enabled = true
filter = wordpress
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 3
bantime = 2h
但 WordPress 更该用专门的插件(比如 Limit Login Attempts),它们在应用层拦截,比读日志反应更快。
自带的 filter 覆盖不到你的应用时,写一个。
先确认日志长什么样:
sudo tail -20 /var/log/myapp/auth.log
# 2026-09-29 10:23:11 FAILED login from 203.0.113.5 user=admin
写 filter(/etc/fail2ban/filter.d/myapp.conf):
[Definition]
failregex = ^.*FAILED login from <HOST>.*$
ignoreregex =
<HOST> 是特殊占位符,Fail2Ban 会自动把匹配到的部分解析成 IP 地址。这是写 filter 唯一需要记住的东西 —— 不用自己写 IP 的正则。
测试它:
sudo fail2ban-regex /var/log/myapp/auth.log /etc/fail2ban/filter.d/myapp.conf
匹配数为 0 就调整正则,直到能匹配上。先在 fail2ban-regex 里调通,再写进 jail —— 这样能省很多反复重启的时间。
加进 jail.local:
[myapp]
enabled = true
filter = myapp
port = http,https
logpath = /var/log/myapp/auth.log
maxretry = 5
findtime = 10m
bantime = 1h
如果你按 VPS 配置 UFW 防火墙 配了 UFW,这里要注意一个组合问题。
Fail2Ban 默认的 banaction 是 iptables-multiport(官方默认值),它直接往 iptables 插规则。而 UFW 也是 iptables 的前端 —— 两者能共存,但有几处要注意:
1. Fail2Ban 的规则会插入到 UFW 链之前,所以封禁优先级更高。这是想要的效果。
2. UFW 的 limit 和 Fail2Ban 功能重叠。 两个一起开不会有冲突,但会造成同一个 IP 被两层记录,排查时容易看晕。建议二选一 —— 用 Fail2Ban 就把 UFW 的 limit 换成普通 allow:
sudo ufw delete limit ssh
sudo ufw allow OpenSSH
3. 系统用 nftables 时要注意 action 匹配。 较新的发行版底层已经是 nftables,如果发现封禁不生效,检查一下:
sudo iptables -L -n | grep f2b # 有输出说明 iptables 兼容层在工作
sudo nft list ruleset | grep f2b # 或者看 nftables 里有没有
两个都没有,就要改用对应的 action(比如 nftables-multiport)。
4. 别用 ufw disable 来"重置防火墙" —— 那会连带清掉 Fail2Ban 的封禁规则,所有被封的 IP 立刻放出来。
和 UFW 一样,Fail2Ban 也管不住 Docker 直接暴露的端口。
原因相同:Docker 自己管理 iptables 规则,而且插在前面。如果你的服务跑在容器里、端口是 -p 8080:8080 暴露的,Fail2Ban 的封禁可能对走 Docker 端口进来的流量不生效。
解法还是那个:把端口绑定到 127.0.0.1,让流量统一经过宿主机上的 Nginx:
ports:
- "127.0.0.1:8080:8080"
这样 Nginx 处理外部请求、Fail2Ban 读 Nginx 日志来封禁,链路就完整了。
配了但一个 IP 都没封
按顺序查:
# 1. jail 启用了吗
sudo fail2ban-client status
# 2. 能匹配到日志吗(最可能的问题)
sudo fail2ban-regex systemd-journal /etc/fail2ban/filter.d/sshd.conf
# 3. server 和 jail 的日志
sudo tail -50 /var/log/fail2ban.log
fail2ban-regex 匹配数是 0 是最高频的原因 —— 换 backend、检查日志路径、或者日志格式和 filter 不匹配。
封了但立刻又解封
bantime 设得太短。Fail2Ban 日志里会看到 Unban。默认的 10m 在很多场景下太短。
规则在 iptables 里但 IP 还是能连
说明流量没走 iptables。Docker 端口是典型情况(见上一节);另外如果服务前面有 CDN,Fail2Ban 看到的是 CDN 的 IP 而不是真实访客 IP —— 这种情况要配 Nginx 的 real_ip 模块,不然你会把 CDN 节点封掉,导致大面积访问异常。
日志里报 Failed to execute ban jail 'sshd' action ... 'iptables'
缺少 iptables 命令。装一下:
sudo apt install -y iptables
封禁列表越来越长
正常现象,而且说明它在工作。要清理:
# 解封单个
sudo fail2ban-client set sshd unbanip 203.0.113.5
# 全部解封某个 jail
sudo fail2ban-client unban --all
重启后封禁记录还在吗
默认情况不在 —— 封禁数据在内存里,重启就清空了。想保留要开数据库:
[DEFAULT]
dbpurgeage = 1d
Fail2Ban 会把封禁记录写进 /var/lib/fail2ban/fail2ban.sqlite3,重启后能恢复。配合 bantime.increment 使用时这个几乎是必需的 —— 不然递增计数每次重启都归零。
会不会误伤真实用户
可能,尤其是共享出口 IP 的场景(公司、学校、机场 WiFi 后面所有人共用一个公网 IP)。一个人输错几次,整个出口被封。这种情况把 maxretry 调大、bantime 调短,平衡安全和误伤。
要不要同时装 CrowdSec
CrowdSec 是新一代的同类工具,支持社区共享威胁情报。个人 VPS 用 Fail2Ban 就够了 —— 它成熟、资料多、资源占用小。CrowdSec 的优势在于多机协同和更细的行为分析,单机场景用不上。
它占多少资源
很小。一个 Python 进程加 SQLite,常驻内存几十 MB。1G 内存的机器上完全无感。
- 只改了
jail.local,没动jail.conf -
bantime从默认的 10m 调长(建议 1h 以上) - 开了
bantime.increment,且配了dbpurgeage -
ignoreip里放了自己的固定 IP / 跳板机 / 监控 -
backend = systemd(或确认日志路径正确) - 跑过
fail2ban-regex确认匹配数不为 0 -
iptables -L -n | grep f2b能看到规则链 - 故意触发一次,确认真的会封
- UFW 的
limit和它做了取舍,没有重复 - Docker 端口绑在
127.0.0.1,流量经过 Nginx - 前面有 CDN 的话,配了
real_ip拿到真实访客 IP
- VPS 被 Fail2Ban 误封怎么办 — 把自己封了怎么救
- VPS 配置 UFW 防火墙 — 三件套里的端口管控
- VPS 配置 SSH 密钥登录 — 比封禁更根本的防护
- VPS 网站被 CC 攻击怎么办 — 应用层的限流和防护
- VPS 安全加固指南 — 完整的加固清单
