一、问题背景
生产环境里 SSH、FTP、网站登录、邮件服务被人扫描是常态。扫的形态一般是:
- 凌晨 3 点 SSH 被人用 root 弱口令试 1 万次
- WordPress xmlrpc.php 被扫 8 万次/小时
- 某业务登录接口被 6 万 IP 撞
- SMTP 被人拿去发垃圾邮件
- FTP 弱口令被人上传 WebShell
最直接的应对:
- 关弱协议(telnet、FTP 明文)
- 用强密码 + 公私钥
- 改端口
- WAF 拦
- 安全组封 IP
但所有这些加起来,仍需要一道"最后一道门"——Fail2ban。它干的事情很简单:扫描日志,发现异常就封禁 IP 一段时间。安装、配置好之后就能让 SSH 撞库、WordPress xmlrpc 撞库、FTP 弱口令、HTTP 401 风暴这些事直接消失。
这一篇讲:
- Fail2ban 原理与安装
- 核心配置 jail / filter / action
- 实战配置 SSH、SSH 慢速 DDoS、nginx-http-auth、nginx-bad-request、nginx-botsearch、apache-badbot、vsftpd、postfix
- 自定义 filter
- 自定义 action(邮件、Slack、钉钉)
- 与 firewalld / nftables / iptables / ufw 的协作
- 数据库、unban、白名单
- 排障与回滚
- 监控告警
二、适用场景
- SSH 22 端口防爆破
- SSH 慢速 DDoS 防护
- FTP / SFTP / vsftpd 弱口令防护
- SMTP / Postfix / Dovecot 认证失败防护
- nginx / apache / lighttpd 401、403、404 风暴防护
- WordPress xmlrpc.php、wp-login.php 撞库防护
- API 网关层 401 撞库防护
- 自定义业务系统登录失败防护
- 多服务器集中管理(与 fail2ban-client 远程)
- 邮件 / Slack / 钉钉告警
- 与云厂商安全组协同封禁
- 与 WAF 协同
- 与 Prometheus 监控集成
三、核心知识点
3.1 Fail2ban 原理
Fail2ban 本质是一个日志扫描 + 防火墙触发的守护进程:
- 启动时读取
/etc/fail2ban/jail.conf 与 /etc/fail2ban/jail.local
- 每个 jail 启动一个后台线程
- 后台线程使用
pyinotify(Linux inotify)监听日志文件变更
- 文件变更时读取新内容
- 用 filter 中定义的正则匹配异常
- 累计
maxretry 次后触发 action
- action 调用防火墙封禁 IP
bantime 时长
- 封禁到期后调用 unban action 解封
关键依赖:
- Python 3.7+(Fail2ban 1.0+)
pyinotify(Linux 上监控文件)
- 防火墙后端(iptables、nftables、firewalld、ufw)
- 日志后端(systemd、file、syslog)
3.2 核心组件
fail2ban-server :守护进程
fail2ban-client :命令行客户端
fail2ban-regex :测试正则
/etc/fail2ban/jail.conf :默认配置(升级时会被覆盖)
/etc/fail2ban/jail.local :用户配置(优先级最高)
/etc/fail2ban/filter.d/*.conf :filter 定义
/etc/fail2ban/action.d/*.conf :action 定义
/var/lib/fail2ban/fail2ban.sqlite3 :数据库(封禁历史)
/var/log/fail2ban.log :fail2ban 自身日志
/var/run/fail2ban/ :socket、PID
3.3 jail 配置结构
[DEFAULT]
backend = polling
bantime = 10m
findtime = 10m
maxretry = 5
ignoreip = 127.0.1/8
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/secure
参数:
enabled :是否启用
port :监听端口
filter :filter 名(不带 .conf)
logpath :日志路径
bantime :封禁时长
findtime :时间窗口
maxretry :最大失败次数
ignoreip :白名单
backend :polling、pyinotify、systemd
action :要执行的 action
usedns :是否解析 hostname
findtimeregex 、 datepattern :自定义时间戳正则
3.4 filter 配置
/etc/fail2ban/filter.d/sshd.conf :
[INCLUDES]
before = common.conf
[Definition]
_daemon = sshd
failregex = ^%(__prefix_line)s(?:error: PAM: )?[aA]uthentication (?:failure|error|failed) for .* from <HOST>(?:\s+port \d*)?(?: ssh\d*)?$
^%(__prefix_line)s(?:error: )?Received disconnect from <HOST>(?::|;)?\s+(?:[a-z]+\s+shutdown|reset|disconnect|connection closed)?\s*$
^%(__prefix_line)sUser .+ from <HOST> not allowed because not listed in AllowUsers$
^%(__prefix_line)sUser .+ from <HOST> not allowed because none of user's groups are listed in AllowGroups$
^%(__prefix_line)s(?:error: )?refused connect from \S+ \(<HOST>\)$
^%(__prefix_line)sInvalid user .* from <HOST>$
^%(__prefix_line)sFailed \S+ for .* from <HOST>(?:\s+port \d*)?\s+$
^%(__prefix_line)s(?:error: )?maximum authentication attempts exceeded for .* from <HOST>(?:\s+port \d*)?\s+.*$
ignoreregex =
关键点:
<HOST> 是占位符,会被替换为 (?:::f{4,6}:)?(?P<host>[\w\-.^_]*\w)
__prefix_line 是时间戳与进程名前缀
- 多条 failregex 用缩进写
- 调试时用
fail2ban-regex 测试
3.5 action 配置
/etc/fail2ban/action.d/iptables-multiport.conf 关键内容:
[Definition]
actionstart = iptables -N f2b-<name>
iptables -A f2b-<name> -j RETURN
iptables -I <chain> -p <protocol> -m multiport --dports <port> -j f2b-<name>
actionstop = iptables -D <chain> -p <protocol> -m multiport --dports <port> -j f2b-<name>
iptables -F f2b-<name>
iptables -X f2b-<name>
actioncheck = iptables -n -L <chain> | grep -q 'f2b-<name>[ \t]'
actionban = iptables -I f2b-<name> 1 -s <ip> -j DROP
actionunban = iptables -D f2b-<name> -s <ip> -j DROP
action 类型:
iptables-multiport :iptables 传统封禁
iptables-allports :所有端口
nftables-multiport :nftables
firewalld :firewalld
ufw :ufw
route :blackhole 路由
cloudflare :调用 Cloudflare API
slack-notify :发 Slack 消息
sendmail-* :发邮件
dshield :上报 DShield
shorewall :shorewall 防火墙
3.6 与 firewalld 配合
firewalld 后端有专属 action:
[DEFAULT]
banaction = firewallcmd-rich-rules
banaction_allports = firewallcmd-rich-rules
chain = INPUT
banaction 包括:
firewallcmd-ipset
firewallcmd-rich-rules
firewallcmd-new (已弃用)
注意:firewalld 后端不会立即封禁,要等 firewalld 重载;但 rich-rules 是热生效的。
3.7 与 nftables 配合
[DEFAULT]
banaction = nftables-multiport
chain = input
/etc/nftables.conf :
table inet filter {
chain input {
type filter hook input priority 0; policy accept;
ct state established,related accept
iif lo accept
tcp dport { 22, 80, 443 } accept
jump f2b-input
}
chain f2b-input {
return
}
}
注意:Fail2ban 的 nftables action 假设 nftables 中已存在同名 chain。
3.8 数据库结构
/var/lib/fail2ban/fail2ban.sqlite3 关键表:
bans :当前封禁列表
jails :jail 状态
logs :日志(可选)
查看数据库:
apt install -y sqlite3
sqlite3 /var/lib/fail2ban/fail2ban.sqlite3
sqlite> .schema
sqlite> SELECT * FROM bans;
sqlite> .headers on
sqlite> SELECT jail, ip, timeofban FROM bans;
3.9 findtime、maxretry、bantime 配合
举例:
findtime=10m 、 maxretry=5 、 bantime=1h :10 分钟内 5 次失败,封 1 小时
findtime=24h 、 maxretry=20 、 bantime=24h :24 小时内 20 次失败,封 24 小时
findtime=1h 、 maxretry=10 、 bantime=10m :1 小时内 10 次失败,封 10 分钟
实战经验:
- SSH:maxretry=5,bantime=1h 起步
- WordPress:maxretry=3,bantime=24h
- FTP:maxretry=3,bantime=24h
- 撞库:maxretry=10,bantime=24h
- 慢速 DDoS:用 recidive jail(重复犯案)
3.10 recidive 监狱
recidive 是"惯犯"监狱:把其他 jail 的 ban 行为记录进 recidive 的 logpath,多次被 ban 的人会进 recidive jail 长时间封禁。
[recidive]
enabled = true
filter = recidive
logpath = /var/log/fail2ban.log
bantime = 1w
findtime = 1d
maxretry = 5
action = iptables-multiport[name=recidive]
四、整体排查或实施思路
4.1 部署加固主线
- 目标:SSH、Web、FTP、SMTP 都被防爆破
- 环境假设:Linux 系统(CentOS / Ubuntu / Debian),有 root 权限
- 配置前检查:日志路径、防火墙类型、Python 版本
- 实施步骤:安装、配置 jail.local、配置 filter、配置 action
- 配置说明:每段 jail 的参数意义
- 生效方式:systemctl enable --now
- 验证方法:ssh 错几次密码看是否被 ban
- 风险点:误封、影响业务、占用 CPU
- 回滚方案:禁用 jail、删除 banaction、unban IP
- 维护建议:定期检查、logrotate、监控
4.2 故障排查主线
- 现象:Fail2ban 没起作用 / 误封 / 启动失败 / 日志报错
- 初步判断:版本、Python、日志路径、权限、防火墙
- 命令检查:journalctl、fail2ban-client status、iptables -L
- 关键指标:ban 数、unban 数、内存、CPU
- 根因定位:日志路径不对、权限不够、firewallcmd 冲突
- 修复方案:改配置、装依赖、关掉其他防火墙
- 验证结果:模拟异常,看是否 ban
- 回滚预案:unban、关 jail
- 复盘总结:写文档
五、实战步骤
5.1 安装
5.1.1 Debian / Ubuntu
apt update
apt install -y fail2ban
systemctl enable fail2ban
systemctl start fail2ban
systemctl status fail2ban
5.1.2 CentOS / RHEL
yum install -y epel-release
yum install -y fail2ban fail2ban-systemd
systemctl enable fail2ban
systemctl start fail2ban
systemctl status fail2ban
5.1.3 源码
git clone https://github.com/fail2ban/fail2ban.git
cd fail2ban
python3 -m pip install .
cp files/redhat-initd /etc/init.d/fail2ban
systemctl daemon-reload
systemctl enable fail2ban
systemctl start fail2ban
5.1.4 容器化
docker run -d --name fail2ban \
--net=host \
--cap-add=NET_ADMIN \
--cap-add=NET_RAW \
-v /var/log:/var/log:ro \
-v /etc/fail2ban:/etc/fail2ban \
-v /var/lib/fail2ban:/var/lib/fail2ban \
crazymax/fail2ban:latest
5.2 验证安装
# 版本
fail2ban-server --version
fail2ban-client version
# 状态
fail2ban-client status
# 启动的 jail
fail2ban-client status
# Number of jail: 0 (刚安装时为空,需要启用)
# 日志
tail -f /var/log/fail2ban.log
预期输出:版本 1.0.x,启动正常。
异常表现:fail2ban-client 报 connection refused,说明 fail2ban-server 未启动。
判断逻辑:检查 systemd 状态、socket 路径( /var/run/fail2ban/fail2ban.sock )。
下一步动作:systemctl start fail2ban 或 systemctl restart fail2ban 。
5.3 jail.local 基础配置
/etc/fail2ban/jail.local :
[DEFAULT]
# 白名单
ignoreip = 127.0.1/8 ::1 10.0.0.0/8 192.168.0.0/16 172.16.0.0/12
# 封禁时长
bantime = 1h
# 时间窗口
findtime = 10m
# 最大失败次数
maxretry = 5
# 后端
backend = systemd
# action
banaction = iptables-multiport
banaction_allports = iptables-allports
chain = INPUT
# 是否解析 hostname
usedns = warn
# 日志级别
loglevel = INFO
# 目的地(邮件)
destemail = ops@example.com
sender = fail2ban@example.com
mta = sendmail
# 收件人
# mta = smtp
# smtphost = smtp.example.com
# smtpport = 587
# smtpuser = alerts@example.com
# smtppass = YOUR_PASSWORD
# smtpfrom = fail2ban@example.com
# smtpto = ops@example.com
# smtptls = starttls
# 邮件 action
action = %(action_mwl)s
参数解释:
ignoreip :白名单,必须包含公司 IP、VPN 段、家庭 IP
bantime = -1 :永久封禁(慎用)
usedns = no :关闭 DNS 解析,避免 IP 反向解析慢导致 fail2ban 卡住
action_mwl :发邮件 + log + whois
5.4 启用 SSH jail
/etc/fail2ban/jail.local 中追加:
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = %(sshd_log)s
backend = %(sshd_backend)s
maxretry = 5
findtime = 10m
bantime = 1h
%(sshd_log)s 在 Debian 是 /var/log/auth.log ,在 RHEL 是 /var/log/secure 。
注意:
- CentOS 7 用
journalctl 的 systemd 后端,需要 backend = systemd
- CentOS 6 没有 systemd,用 polling
5.5 验证 SSH jail
# 启动
systemctl restart fail2ban
# 看状态
fail2ban-client status sshd
# 输出:
# Status for the jail: sshd
# |- Filter
# | |- Currently failed: 0
# | |- Total failed: 0
# | `- File list: /var/log/auth.log
# `- Actions
# |- Currently banned: 0
# |- Total banned: 0
# `- Banned IP list:
# 模拟失败
ssh -o ConnectTimeout=3 -o StrictHostKeyChecking=no -o PreferredAuthentications=password wronguser@127.0.0.1
# 故意输错 6 次密码
# 看是否被 ban
fail2ban-client status sshd
# Banned IP list: 127.0.0.1
# 看 iptables
iptables -L -n | grep f2b
# 解封
fail2ban-client set sshd unbanip 127.0.0.1
5.6 启用 nginx-http-auth
[nginx-http-auth]
enabled = true
filter = nginx-http-auth
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 5
findtime = 10m
bantime = 1h
/etc/fail2ban/filter.d/nginx-http-auth.conf (系统自带):
[Definition]
failregex = ^ \[error\] \d+#\d+: \*\d+ user "([^"]+):"? client: <HOST>, server: \S+, request: "\S+ \S+ HTTP/\d+\.\d+", host: "\S+"(?:, referrer: "\S+")?\s*$
ignoreregex =
验证:访问一个有 basic auth 的 URL,输错 6 次,看是否被 ban。
5.7 启用 nginx-bad-request
[nginx-bad-request]
enabled = true
filter = nginx-bad-request
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 10
findtime = 10m
bantime = 1h
/etc/fail2ban/filter.d/nginx-bad-request.conf :
[Definition]
failregex = ^<HOST> -[^"]* "[A-Z]+ /\S+ HTTP/\d+\.\d+" (4(04|03|44|13|14|51|93|95)) \d+ .*$
ignoreregex = ^<HOST> -[^"]* "[A-Z]+ /\S+ HTTP/\d+\.\d+" 200 \d+ .*$
5.8 启用 nginx-botsearch
[nginx-botsearch]
enabled = true
filter = nginx-botsearch
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 5
findtime = 10m
bantime = 24h
/etc/fail2ban/filter.d/nginx-botsearch.conf (系统自带)覆盖典型扫描路径:
/admin/
/wp-admin/
/xmlrpc.php
/phpmyadmin/
/.env
/.git/
/backup/
/.svn/
5.9 启用 sshd-ddos 慢速 SSH DDoS
[sshd-ddos]
enabled = true
filter = sshd-ddos
port = ssh
logpath = %(sshd_log)s
backend = %(sshd_backend)s
maxretry = 10
findtime = 1h
bantime = 24h
/etc/fail2ban/filter.d/sshd-ddos.conf (系统自带)匹配大量连接尝试。
5.10 启用 vsftpd
[vsftpd]
enabled = true
filter = vsftpd
port = ftp,ftp-data,ftps,ftps-data
logpath = /var/log/vsftpd.log
maxretry = 3
findtime = 10m
bantime = 24h
5.11 启用 postfix / dovecot
[postfix]
enabled = true
filter = postfix
port = smtp,465,submission
logpath = /var/log/mail.log
maxretry = 5
findtime = 10m
bantime = 1h
[dovecot]
enabled = true
filter = dovecot
port = pop3,pop3s,imap,imaps,submission
logpath = /var/log/mail.log
maxretry = 5
findtime = 10m
bantime = 1h
5.12 自定义 filter:WordPress xmlrpc 撞库
/etc/fail2ban/filter.d/wordpress-xmlrpc.conf :
[Definition]
failregex = ^<HOST> .* "POST /xmlrpc\.php HTTP/\d+\.\d+" \d+ .*$
^<HOST> .* "POST /wp-login\.php HTTP/\d+\.\d+" \d+ .*$
ignoreregex = ^<HOST> .* "(GET|HEAD) .*$
说明:ignoreregex 中的正则用于排除 GET/HEAD 请求,原文表达式未严格闭合引号,实际使用时建议用 fail2ban-regex 验证后再上线。
/etc/fail2ban/jail.local :
[wordpress-xmlrpc]
enabled = true
filter = wordpress-xmlrpc
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 5
findtime = 5m
bantime = 24h
5.13 自定义 action:钉钉 Webhook
/etc/fail2ban/action.d/dingtalk-webhook.conf :
[Definition]
actionstart =
actionstop =
actioncheck =
actionban = curl -s -X POST "<webhook_url>" \
-H "Content-Type: application/json" \
-d '{"msgtype":"text","text":{"content":"[Fail2ban] 封禁 IP <ip> 在 jail <name>,剩余时间 <bantime> 秒"}}'
actionunban = curl -s -X POST "<webhook_url>" \
-H "Content-Type: application/json" \
-d '{"msgtype":"text","text":{"content":"[Fail2ban] 解封 IP <ip> 在 jail <name>"}}'
[Init]
webhook_url = https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN
jail 中引用:
[wordpress-xmlrpc]
action = dingtalk-webhook[name=wordpress-xmlrpc]
5.14 自定义 action:Slack Webhook
/etc/fail2ban/action.d/slack-webhook.conf :
[Definition]
actionban = curl -s -X POST "<webhook_url>" \
-H "Content-Type: application/json" \
-d '{"text":"[Fail2ban] Banned IP <ip> in jail <name>"}'
actionunban = curl -s -X POST "<webhook_url>" \
-H "Content-Type: application/json" \
-d '{"text":"[Fail2ban] Unbanned IP <ip> in jail <name>"}'
[Init]
webhook_url = https://hooks.slack.com/services/YOUR/WEBHOOK
5.15 与云厂商安全组协同
5.15.1 AWS
pip install boto3
/etc/fail2ban/action.d/aws-waf.conf :
[Definition]
actionban = aws wafv2 update-ip-set --name <ipset_name> --scope REGIONAL --id <ipset_id> --addresses <ip>/32 --change-token <token>
actionunban = ...
更简单的方案:使用 iptables 封禁 1 小时后,云厂商安全组在大网段(AS)层做兜底。
5.15.2 Cloudflare
pip install cloudflare
/etc/fail2ban/action.d/cloudflare-ban.conf :
[Definition]
actionban = python3 /etc/fail2ban/cloudflare-ban.py ban <ip>
actionunban = python3 /etc/fail2ban/cloudflare-ban.py unban <ip>
/etc/fail2ban/cloudflare-ban.py :
#!/usr/bin/env python3
import sys
import CloudFlare
import os
def main():
cf = CloudFlare.CloudFlare(token=os.environ['CF_API_TOKEN'])
zone_id = os.environ['CF_ZONE_ID']
ip = sys.argv[2]
if sys.argv[1] == 'ban':
cf.zones.firewall.access_rules.rules.post(
zone_id,
data={'mode': 'block', 'configuration': {'target': 'ip', 'value': ip}, 'notes': 'fail2ban'}
)
else:
# 查找并删除
rules = cf.zones.firewall.access_rules.rules.get(zone_id)
for rule in rules:
if rule['configuration']['value'] == ip:
cf.zones.firewall.access_rules.rules.delete(zone_id, rule['id'])
if __name__ == '__main__':
main()
风险点:脚本中明文存储 token 必须改为环境变量或受保护配置文件。
5.16 操作失败时的保护
5.16.1 防止误封
[DEFAULT]
# 加 IP 白名单
ignoreip = 127.0.1/8 ::1 \
10.0.0.0/8 \
192.168.0.0/16 \
172.16.0.0/12 \
# 办公网段
203.0.113.0/24 \
# 公司 VPN
198.51.100.0/24 \
# 跳板机
192.0.2.10
5.16.2 防止 SSH 永久 ban
bantime 不要写 -1 ,避免被 ban 后只能进机房解封。
5.16.3 高风险时先关
- 服务器刚上线、白名单未配置时,临时禁掉所有 jail
- 配置确认后再启用
[DEFAULT]
enabled = false
5.17 排障:Fail2ban 不生效
现象:多次失败密码,但 IP 没被 ban。
初步判断:filter 不匹配、logpath 错、bantime 没生效、iptables 不存在。
命令检查:
# 看 fail2ban 日志
tail -f /var/log/fail2ban.log
journalctl -u fail2ban -f
# 看 jail 状态
fail2ban-client status sshd
# 测试 filter
fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf
# 看 iptables
iptables -L -n
# 看 fail2ban 是否能写入 iptables
iptables -N f2b-test
关键指标:
Currently failed :实时失败计数
Total failed :累计失败
Banned IP list :被 ban 的 IP
File list :监控的日志文件
Backend :监控方式
根因定位:
File list 列出空:logpath 不对或权限不足
Currently failed 不增加:filter 不匹配
Banned IP list 空:action 没成功
- iptables 中没有
f2b-sshd chain:banaction 错误
修复方案:
- logpath 改对
- 改 filter
- 检查 iptables / nftables 兼容性
- 检查 firewalld 是否启用
验证结果:模拟失败,确认能 ban。
5.18 排障:误封自己
现象:连了几次失败后自己被 ban,无法连回。
解决:
# 在另一台机器上 unban
ssh other-server "fail2ban-client set sshd unbanip YOUR_IP"
# 或者从 console 登录(云厂商 VNC)
fail2ban-client set sshd unbanip YOUR_IP
iptables -D f2b-sshd -s YOUR_IP -j DROP
预防:
- 永远先配 ignoreip
- 把跳板机 IP 加到所有 jail 的 ignoreip
- 不要在生产环境的 SSH jail 上做测试
5.19 排障:firewalld / nftables / iptables 冲突
现象:Fail2ban 启动失败,提示 banaction 错误。
判断:
# 看哪个防火墙在跑
systemctl status firewalld
systemctl status nftables
iptables -L -n
nft list ruleset
修复:
- 只保留一个防火墙
- 改 fail2ban 配置中的 banaction
[DEFAULT]
banaction = iptables-multiport
# 或
banaction = nftables-multiport
# 或
banaction = firewallcmd-rich-rules
5.20 排障:CPU 高
现象:Fail2ban 进程 CPU 高。
原因:
usedns = yes + DNS 服务器慢
- 大日志 + polling 后端
findtime 太大
修复:
usedns = warn 或 no
backend = pyinotify (如果用 polling 改 inotify)
- 拆 jail,每个 jail 单独监控一段日志
5.21 排障:磁盘写满
现象:fail2ban.log 巨大。
原因:DEBUG 日志级别。
修复:
[DEFAULT]
loglevel = INFO
清理旧日志:
truncate -s 0 /var/log/fail2ban.log
systemctl reload fail2ban
5.22 监控:Prometheus 集成
使用 fail2ban-exporter 或文本文件方式:
5.22.1 文本文件方式
/usr/local/bin/fail2ban-exporter.sh :
#!/bin/bash
TEXTFILE_DIR="/var/lib/node_exporter/textfile_collector"
TMP_FILE="$TEXTFILE_DIR/fail2ban.prom.tmp"
OUT_FILE="$TEXTFILE_DIR/fail2ban.prom"
# 先清空
> "$TMP_FILE"
# 遍历所有 jail
JAILS=$(fail2ban-client status | grep 'Jail list' | sed -E 's/^.*://;s/,//g')
for jail in $JAILS; do
STATUS=$(fail2ban-client status "$jail")
BANNED=$(echo "$STATUS" | awk '/Banned IP list/{print $NF}' | grep -oE '[0-9]+')
FAILED=$(echo "$STATUS" | awk '/Total failed/{print $NF}')
BANNED=${BANNED:-0}
FAILED=${FAILED:-0}
cat >> "$TMP_FILE" <<EOF
# HELP fail2ban_jail_banned_ip_count Number of banned IPs in jail
# TYPE fail2ban_jail_banned_ip_count gauge
fail2ban_jail_banned_ip_count{jail="$jail"} $BANNED
# HELP fail2ban_jail_total_failed Total failed login attempts in jail
# TYPE fail2ban_jail_total_failed counter
fail2ban_jail_total_failed{jail="$jail"} $FAILED
EOF
done
# 原子替换
mv "$TMP_FILE" "$OUT_FILE"
加 crontab:
chmod +x /usr/local/bin/fail2ban-exporter.sh
echo '* * * * * /usr/local/bin/fail2ban-exporter.sh' | crontab -
5.23 与 Prometheus 配合告警
groups:
- name: fail2ban
rules:
- alert: Fail2banJailHighBanCount
expr: fail2ban_jail_banned_ip_count > 100
for: 5m
labels:
severity: warning
annotations:
summary: "Jail {{ $labels.jail }} has banned {{ $value }} IPs"
- alert: Fail2banServiceDown
expr: absent(fail2ban_jail_banned_ip_count)
for: 5m
labels:
severity: critical
annotations:
summary: "Fail2ban exporter is down"
5.24 数据库清理
# 备份
cp /var/lib/fail2ban/fail2ban.sqlite3 /var/lib/fail2ban/fail2ban.sqlite3.bak
# 进入
sqlite3 /var/lib/fail2ban/fail2ban.sqlite3
# 看所有 ban
.mode column
.headers on
SELECT * FROM bans;
# 清空
DELETE FROM bans;
# 看 jail 状态
SELECT * FROM jails;
5.25 升级与回滚
# 升级
yum update fail2ban
systemctl restart fail2ban
fail2ban-client status
# 回滚
yum downgrade fail2ban-1.0.2-1.el7
systemctl restart fail2ban
5.26 卸载
# 停服
systemctl stop fail2ban
systemctl disable fail2ban
# 卸载
yum remove fail2ban
# 或
apt remove --purge fail2ban
# 清空规则
iptables -L -n
# 手动删除 f2b-* chain
for chain in $(iptables -L -n | grep '^Chain f2b' | awk '{print $2}'); do
iptables -F "$chain"
iptables -X "$chain"
done
六、常用命令
6.1 服务控制
# 启动
systemctl start fail2ban
systemctl enable fail2ban
systemctl status fail2ban
# 停止
systemctl stop fail2ban
# 重启
systemctl restart fail2ban
# 重新加载配置
systemctl reload fail2ban
6.2 jail 管理
# 查看所有 jail
fail2ban-client status
# 查看指定 jail
fail2ban-client status sshd
# 启用 jail
fail2ban-client set sshd addaction iptables-multiport
# 禁用 jail
fail2ban-client stop sshd
# 启动 jail
fail2ban-client start sshd
# reload jail
fail2ban-client reload sshd
6.3 IP 管理
# unban 单个 IP
fail2ban-client set sshd unbanip 1.2.3.4
# 列出被 ban 的 IP
fail2ban-client status sshd
# 列出所有 ban
iptables -L f2b-sshd -n
# nftables
nft list chain inet filter f2b-sshd
6.4 filter 测试
# 测试 logpath + filter
fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf
# 显示匹配行
fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf --print-all-matched
# 详细模式
fail2ban-regex -v /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf
# 调试新 filter
fail2ban-regex /var/log/access.log "POST /xmlrpc.php" "Date: <DATE>.*"
# 自带一个简单正则
6.5 日志
# fail2ban 自身日志
tail -f /var/log/fail2ban.log
journalctl -u fail2ban -f
# 慢日志
journalctl -u fail2ban --since "1 hour ago"
# 错误
journalctl -u fail2ban -p err
6.6 防火墙
# iptables
iptables -L -n
iptables -L f2b-sshd -n
iptables -D f2b-sshd -s 1.2.3.4 -j DROP
# nftables
nft list ruleset
nft list chain inet filter f2b-sshd
# firewalld
firewall-cmd --list-all
firewall-cmd --permanent --list-rich-rules
6.7 数据库
sqlite3 /var/lib/fail2ban/fail2ban.sqlite3
sqlite> .tables
sqlite> .schema bans
sqlite> SELECT * FROM bans;
sqlite> DELETE FROM bans;
七、配置示例
7.1 完整 jail.local 范例
[DEFAULT]
ignoreip = 127.0.1/8 ::1 10.0.0.0/8 192.168.0.0/16 172.16.0.0/12 203.0.113.0/24
bantime = 1h
findtime = 10m
maxretry = 5
backend = systemd
banaction = iptables-multiport
chain = INPUT
usedns = warn
loglevel = INFO
destemail = ops@example.com
sender = fail2ban@example.com
mta = sendmail
action = %(action_mwl)s
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = %(sshd_log)s
maxretry = 5
bantime = 1h
[sshd-ddos]
enabled = true
port = ssh
filter = sshd-ddos
logpath = %(sshd_log)s
maxretry = 10
findtime = 1h
bantime = 24h
[nginx-http-auth]
enabled = true
port = http,https
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
maxretry = 5
bantime = 1h
[nginx-bad-request]
enabled = true
port = http,https
filter = nginx-bad-request
logpath = /var/log/nginx/access.log
maxretry = 10
bantime = 1h
[nginx-botsearch]
enabled = true
port = http,https
filter = nginx-botsearch
logpath = /var/log/nginx/access.log
maxretry = 5
bantime = 24h
[vsftpd]
enabled = true
port = ftp,ftp-data,ftps,ftps-data
filter = vsftpd
logpath = /var/log/vsftpd.log
maxretry = 3
bantime = 24h
[postfix]
enabled = true
port = smtp,465,submission
filter = postfix
logpath = /var/log/mail.log
maxretry = 5
bantime = 1h
[dovecot]
enabled = true
port = pop3,pop3s,imap,imaps,submission
filter = dovecot
logpath = /var/log/mail.log
maxretry = 5
bantime = 1h
[recidive]
enabled = true
filter = recidive
logpath = /var/log/fail2ban.log
bantime = 1w
findtime = 1d
maxretry = 5
action = iptables-multiport[name=recidive]
7.2 自定义 filter:业务登录失败
/etc/fail2ban/filter.d/myapp-login.conf :
[Definition]
failregex = ^<HOST> -[^"]* "POST /api/login HTTP/\d+\.\d+" 401 \d+ .*$
^<HOST> -[^"]* "POST /api/login HTTP/\d+\.\d+" 403 \d+ .*$
ignoreregex = ^<HOST> -[^"]* "POST /api/login HTTP/\d+\.\d+" 200 \d+ .*$
jail:
[myapp-login]
enabled = true
port = http,https
filter = myapp-login
logpath = /var/log/nginx/access.log
maxretry = 10
findtime = 5m
bantime = 24h
7.3 自定义 filter:SSH 公私钥失败
[Definition]
failregex = ^%(__prefix_line)s(?:error: )?Authentication refused: bad ownership or modes for directory .*$
^%(__prefix_line)s(?:error: )?Authentication refused: too many authentication failures for .* from <HOST>(?:\s+port \d*)?\s*$
^%(__prefix_line)sUser .+ from <HOST> not allowed because not listed in AllowUsers$
^%(__prefix_line)sUser .+ from <HOST> not allowed because none of user's groups are listed in AllowGroups$
^%(__prefix_line)srefused connect from \S+ \(<HOST>\)$
7.4 自定义 action:解封后写入审计日志
/etc/fail2ban/action.d/audit-log.conf :
[Definition]
actionban = /usr/bin/logger -p auth.warning "Fail2ban banned <ip> in <name>"
actionunban = /usr/bin/logger -p auth.info "Fail2ban unbanned <ip> in <name>"
7.5 与 nftables 配合
/etc/fail2ban/jail.local :
[DEFAULT]
banaction = nftables-multiport
banaction_allports = nftables-allports
chain = input
/etc/nftables.conf :
table inet filter {
chain input {
type filter hook input priority 0; policy accept;
ct state established,related accept
iif lo accept
ip protocol tcp tcp dport { 22, 80, 443 } accept
ip protocol udp udp dport 53 accept
jump f2b-input
}
chain f2b-input {
return
}
}
八、日志或指标观察方法
8.1 fail2ban.log 关键内容
2026-06-29 10:23:45,123 fail2ban.filter [1234]: WARNING [sshd] Found 1.2.3.4
2026-06-29 10:23:45,456 fail2ban.actions [1234]: WARNING [sshd] Ban 1.2.3.4
2026-06-29 10:33:45,789 fail2ban.actions [1234]: NOTICE [sshd] Unban 1.2.3.4
关键字:
Found :检测到失败
Ban :封禁
Unban :解封
Restore / Save :规则变化
8.2 iptables 中看 ban 状态
iptables -L f2b-sshd -n -v
# pkts bytes target prot opt in out source destination
# 1234 876M DROP all -- * * 1.2.3.4 0.0.0.0/0
8.3 关键指标
| 指标 |
来源 |
健康范围 |
异常表现 |
| Total failed |
fail2ban-client |
业务相关 |
突增为被扫 |
| Currently banned |
fail2ban-client |
< 100 |
> 1000 为大规模扫 |
| Banned IP list |
fail2ban-client |
业务相关 |
单 IP 多次 ban 为顽固攻击 |
fail2ban_jail_banned_ip_count |
exporter |
业务相关 |
突增 |
fail2ban_jail_total_failed |
exporter |
业务相关 |
突增 |
process_cpu_seconds_total{name="fail2ban-server"} |
node_exporter |
< 5% |
> 30% 为 filter 不命中 |
process_open_fds{name="fail2ban-server"} |
node_exporter |
< 1024 |
持续高为 inotify 监控太多文件 |
8.4 与业务侧核对
业务侧应该把可疑 IP 加进 ignoreip ,把生产出口 IP 加进 ignoreip 。每月巡检一次。
九、排查路径
9.1 Fail2ban 不启动
journalctl -u fail2ban -xe
fail2ban-server -t :前台调试模式
- 检查 Python 版本、依赖
9.2 Filter 不匹配
fail2ban-regex 测试
- 看日志格式变化(升级后 sshd 日志格式可能变了)
- 看新版本 OS 的日志位置变化
9.3 IP 没被 ban
fail2ban-client status 看 Total failed 增长
- 看 iptables / nftables / firewalld 规则
- 看 banaction 是否被注释
9.4 IP 没被 unban
bantime = -1 :永久 ban
- 配置文件 reload 没生效
time 命令校准
9.5 fail2ban-server 内存持续增长
journalctl 看异常
- 升级到最新版本(修复内存泄漏)
- 减少 jail 数量
9.6 自定义 filter 不工作
- 用
fail2ban-regex --print-all-matched 验证
- 检查
__prefix_line 格式
- 检查
datepattern
十、风险提醒
ignoreip 没配会把自己 ban 出去
bantime = -1 永久 ban 风险极高(重启或 banaction 改了就解除,但中间没人能进)
usedns = yes + DNS 服务器慢会让 fail2ban 卡住
- 在 iptables / nftables / firewalld 三个防火墙间来回切换,规则会混乱
- 自定义 action 中明文存 token 风险
maxretry 设得太小会误封正常用户
banaction 与系统自带工具链(puppet、saltstack、ansible)的 iptables 模块可能冲突
- 与 K8s NetworkPolicy 冲突(封禁在主机层,K8s 内部流量不经过)
- 自定义 filter 写错正则会导致
Currently failed 飙高
findtime 太大 + maxretry 太小会让正常流量被 ban
- fail2ban 自身 log 写满磁盘
- 同时启用 iptables 和 firewalld 会让 fail2ban 启动失败
- recidive jail 的 bantime 要比普通 jail 长才有意义
- 切换到 IPv6 时 banaction 仍为 iptables-legacy,可能不生效
十一、验证方式
11.1 启动验证
systemctl status fail2ban
fail2ban-client status
# 至少显示 Number of jail
11.2 SSH jail 验证
# 模拟失败
for i in 1 2 3 4 5 6; do
sshpass -p "wrong" ssh -o ConnectTimeout=3 -o StrictHostKeyChecking=no -o PreferredAuthentications=password wronguser@127.0.0.1 2>/dev/null
done
# 看是否被 ban
fail2ban-client status sshd
iptables -L f2b-sshd -n
11.3 HTTP 401 验证
for i in 1 2 3 4 5 6; do
curl -s -o /dev/null -u wrong:wrong http://example.com/admin/
done
fail2ban-client status nginx-http-auth
11.4 通知验证
- 触发 ban,检查邮件/Slack/钉钉
- 触发 unban,检查通知
11.5 监控验证
curl http://127.0.0.1:9100/metrics | grep fail2ban_
十二、回滚方案
12.1 配置回滚
# 修改前
cp /etc/fail2ban/jail.local /etc/fail2ban/jail.local.bak.$(date +%Y%m%d_%H%M%S)
# 出问题
cp /etc/fail2ban/jail.local.bak.YYYYMMDD_HHMMSS /etc/fail2ban/jail.local
systemctl restart fail2ban
12.2 全部 ban 解封
# 停止 fail2ban
systemctl stop fail2ban
# 清理 iptables
for chain in $(iptables -L -n | grep '^Chain f2b' | awk '{print $2}'); do
iptables -F "$chain"
iptables -X "$chain"
done
# 清理数据库
sqlite3 /var/lib/fail2ban/fail2ban.sqlite3 "DELETE FROM bans;"
# 启动
systemctl start fail2ban
12.3 二进制回滚
yum downgrade fail2ban
systemctl restart fail2ban
12.4 临时关 jail
[DEFAULT]
enabled = false
systemctl reload fail2ban
十三、生产环境注意事项
- 必须先在
ignoreip 中加好公司所有出口 IP、跳板机 IP、监控 IP
- 必须保留对 sshd 的
match 与 bantime 不冲突的测试
- 必须给 fail2ban 自身配监控(CPU、内存、磁盘、日志)
- 必须 logrotate fail2ban.log
- 必须有 unban 流程文档
- recidive jail 必须开
- 邮件 / Slack / 钉钉 告警必须验证
- 自定义 filter 必须经过
fail2ban-regex 严格测试
- 自定义 action 中 token 必须用环境变量
- 必须定期巡检
iptables / nftables 规则数量
- 必须避免
bantime = -1
- 必须在多台机器上跑 fail2ban,相互解封通过 fail2ban-client 远程
- 必须在 K8s 场景下考虑 sidecar 方案或 DaemonSet
- 必须在云厂商场景下配置 VPC 防火墙兜底
- 必须配置 logrotate / 日志切割
- 必须把配置纳管到 git / 配置中心
十四、总结
Fail2ban 配置起来不难,但要跑稳,有 12 个关键点:
- ignoreip 必须全
- bantime 不能 -1
- usedns 不能 yes
- backend 用 systemd + pyinotify
- recidive 必开
- 自定义 filter 用 fail2ban-regex 测
- 自定义 action 不用明文 token
- 防火墙只能保留一个
- 数据库要备份要清理
- 监控要齐全
- unban 流程要文档化
- 配置版本化、可回滚
跑稳之后的实际收益是:SSH 撞库、网站爆破、FTP 弱口令、SMTP 撞库、WordPress xmlrpc 撞库这些常态化的攻击,全部被自动挡在门外。
最后留一个执行清单:
- 第 1 步:装包、启动、确认
fail2ban-client status 有 Number of jail
- 第 2 步:配置 jail.local,把公司 IP 全加 ignoreip
- 第 3 步:启用 sshd,模拟 6 次失败,看是否被 ban
- 第 4 步:启用 nginx-bad-request / nginx-botsearch,看是否能 ban
- 第 5 步:开 recidive
- 第 6 步:接告警(邮件 / Slack / 钉钉)
- 第 7 步:接监控(Prometheus 文本文件)
- 第 8 步:写 unban runbook
- 第 9 步:把配置纳管到 git
- 第 10 步:每月巡检规则数量、文件描述符、内存
附录 A:常见 Fail2ban 实战模板
A.1 阿里云 ECS 场景
阿里云服务器默认安全组只放行业务端口,Fail2ban 主要作用是减少业务层被撞的次数。注意:不要把 22 端口放在公网(用跳板机 + EIP + 安全组 0.0.0.0/0 放行 + jump host);让 Fail2ban 集中处理 Web 类撞库即可。
推荐配置:
[DEFAULT]
ignoreip = 127.0.1/8 ::1 10.0.0.0/8 172.16.0.0/12
bantime = 1h
findtime = 10m
maxretry = 5
backend = systemd
banaction = iptables-multiport
chain = INPUT
usedns = warn
loglevel = INFO
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = %(sshd_log)s
maxretry = 5
bantime = 1h
[nginx-botsearch]
enabled = true
port = http,https
filter = nginx-botsearch
logpath = /var/log/nginx/access.log
maxretry = 5
bantime = 24h
[recidive]
enabled = true
filter = recidive
logpath = /var/log/fail2ban.log
bantime = 1w
findtime = 1d
maxretry = 5
action = iptables-multiport[name=recidive]
A.2 自建 IDC 场景
[DEFAULT]
ignoreip = 127.0.1/8 ::1 \
10.0.0.0/8 \
192.168.0.0/16 \
172.16.0.0/12 \
203.0.113.0/24 \
198.51.100.0/24
bantime = 24h
findtime = 30m
maxretry = 5
backend = systemd
banaction = nftables-multiport
chain = input
usedns = warn
loglevel = INFO
destemail = ops@example.com
sender = fail2ban@example.com
mta = sendmail
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = %(sshd_log)s
maxretry = 3
bantime = 24h
[nginx-bad-request]
enabled = true
port = http,https
filter = nginx-bad-request
logpath = /var/log/nginx/access.log
maxretry = 10
bantime = 1h
[recidive]
enabled = true
filter = recidive
logpath = /var/log/fail2ban.log
bantime = 30d
findtime = 7d
maxretry = 5
action = nftables-multiport[name=recidive]
A.3 K8s 场景
K8s 中用 sidecar 模式跑 Fail2ban 不太合适(因为容器内 iptables 行为特殊)。一般用 DaemonSet + hostPath 共享日志 + privileged 模式:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fail2ban
spec:
selector:
matchLabels:
app: fail2ban
template:
metadata:
labels:
app: fail2ban
spec:
containers:
- name: fail2ban
image: crazymax/fail2ban:latest
securityContext:
privileged: true
capabilities:
add:
- NET_ADMIN
- NET_RAW
volumeMounts:
- name: var-log
mountPath: /var/log
readOnly: true
- name: fail2ban-config
mountPath: /etc/fail2ban
- name: fail2ban-data
mountPath: /var/lib/fail2ban
volumes:
- name: var-log
hostPath:
path: /var/log
- name: fail2ban-config
configMap:
name: fail2ban-config
- name: fail2ban-data
hostPath:
path: /var/lib/fail2ban
但实际生产中更多用:
- 主机层跑 Fail2ban(每台服务器一个)
- 监控数据用 node_exporter + 文本文件收集器
- 在 Calico / Cilium 场景用 NetworkPolicy 协同
A.4 Docker 场景
Docker 容器内一般不需要 Fail2ban,但 Docker daemon 暴露的端口仍然可能被撞。常见做法:
- 把 SSH / Web 跑在主机层
- 容器内只跑应用
- 主机层跑 Fail2ban 即可
容器化跑 Fail2ban 适用场景:需要统一管理多台机器 / 想用 fail2ban 触发外部 API。
docker run -d --name fail2ban \
--net=host \
--cap-add=NET_ADMIN \
--cap-add=NET_RAW \
-v /var/log:/var/log:ro \
-v /etc/fail2ban:/etc/fail2ban \
-v /var/lib/fail2ban:/var/lib/fail2ban \
crazymax/fail2ban:latest
附录 B:fail2ban-regex 实战
B.1 基础用法
# 用默认 filter 测试日志
fail2ban-regex /var/log/auth.log
# 指定 filter
fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf
# 显示所有匹配
fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf --print-all-matched
# 详细
fail2ban-regex -v /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf
# 详细 + 显示 unmatched
fail2ban-regex -v /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf --print-all-unmatched
# 用内置示例测试
fail2ban-regex -v "Jan 1 12:00:00 host sshd[1234]: Failed password for invalid user admin from 1.2.3.4" \
"Failed password for .* from <HOST>"
B.2 自定义 filter 调试步骤
- 抓 100 行真实日志
- 写 failregex 草稿
fail2ban-regex 跑一遍
- 调整正则
- 再跑一遍
- 重复直到匹配
调试技巧:
<HOST> 必须是第一个出现的 IP
__prefix_line 兼容 syslog、rsyslog、systemd 三种格式
- 时间戳格式要明确
- 调试时用
print-all-matched 看匹配是否合理
B.3 案例:自定义业务登录失败 filter
日志样例:
[2026-06-29 10:23:45] user_login FAILED user=alice ip=1.2.3.4 reason=password
[2026-06-29 10:23:46] user_login FAILED user=bob ip=1.2.3.4 reason=password
[2026-06-29 10:23:47] user_login OK user=carol ip=1.2.3.4
filter:
[Definition]
failregex = ^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\] user_login FAILED user=\S+ ip=<HOST> reason=.*$
ignoreregex = ^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\] user_login OK .*$
测试:
cat > /tmp/test.log <<'EOF'
[2026-06-29 10:23:45] user_login FAILED user=alice ip=1.2.3.4 reason=password
[2026-06-29 10:23:46] user_login FAILED user=bob ip=1.2.3.4 reason=password
[2026-06-29 10:23:47] user_login OK user=carol ip=1.2.3.4
EOF
fail2ban-regex /tmp/test.log /etc/fail2ban/filter.d/myapp-login.conf
输出:
Running tests
=============
Use failregex filter file : myapp-login, basedir: /etc/fail2ban
Use log file : /tmp/test.log
Use encoding : UTF-8
Results
=======
Failregex: 2 total
|- #) [# of matches] regular expression
| 1) [2] ^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\] user_login FAILED user=\S+ ip=<HOST> reason=.*$
| 1.2.3.4 Mon Jun 29 10:23:45 2026
| 1.2.3.4 Mon Jun 29 10:23:46 2026
`-
Ignoreregex: 0 total
Date template hits:
|- [# of hits] date format
| [3] Day-MONTH-Date Hour:Minute:Second.Year
`-
Lines: 3 lines, 0 ignored, 2 matched, 1 failed
附录 C:unban runbook
C.1 单 IP unban
# 找 jail
fail2ban-client status
# 看具体 jail
fail2ban-client status sshd
# 找 IP
fail2ban-client status sshd | grep 'Banned IP list'
# unban
fail2ban-client set sshd unbanip 1.2.3.4
# 验证
fail2ban-client status sshd | grep 'Banned IP list'
iptables -L f2b-sshd -n | grep 1.2.3.4
# 应该没有输出
C.2 批量 unban
# unban 所有 IP
for jail in $(fail2ban-client status | grep 'Jail list' | sed 's/.*://;s/,//g'); do
for ip in $(fail2ban-client status "$jail" | awk '/Banned IP list/{print $NF}' | tr ',' ' '); do
echo "Unbanning $ip from $jail"
fail2ban-client set "$jail" unbanip "$ip"
done
done
C.3 解封带白名单
# 先 unban
fail2ban-client set sshd unbanip 1.2.3.4
# 加进 ignoreip
echo "ignoreip = 1.2.3.4" >> /etc/fail2ban/jail.local
# 或加到全局
# /etc/fail2ban/jail.local
# [DEFAULT]
# ignoreip = ... 1.2.3.4
systemctl reload fail2ban
C.4 紧急 unban(fail2ban-server 不可用时)
# 直接操作 iptables
iptables -L f2b-sshd -n --line-numbers
iptables -D f2b-sshd <num>
# 直接操作 nftables
nft delete element inet filter f2b-sshd { 1.2.3.4 }
# 直接操作 firewalld
firewall-cmd --permanent --remove-rich-rule="rule family='ipv4' source address='1.2.3.4' reject"
firewall-cmd --reload
# 改数据库
sqlite3 /var/lib/fail2ban/fail2ban.sqlite3 "DELETE FROM bans WHERE ip='1.2.3.4';"
附录 D:常见误封案例与解决方案
D.1 案例 1:CI/CD 机器被 ban
现象:Jenkins / GitLab Runner 因为代码里 ssh user@host 错配密码,被 fail2ban 封。
解决:
- CI / CD 机器出口 IP 加 ignoreip
- 改用 SSH 私钥
D.2 案例 2:监控节点被 ban
现象:监控机器用 SSH 探测端口和服务,频繁触发 Connection closed by authentication 误匹配。
解决:
- 监控节点出口 IP 加 ignoreip
- 改用 SSH 私钥
D.3 案例 3:内部 NAT 网段被 ban
现象:办公网 NAT 后所有员工都从同一个出口 IP 出去,几个人错密码,全公司被 ban。
解决:
- 办公网出口 IP 加 ignoreip
bantime 缩短
- 启用基于用户的 ban 而非基于 IP
D.4 案例 4:scp 中断被误判
现象:scp 传输大文件时网络抖动导致中断,被 fail2ban 算作失败。
解决:
- 自定义 filter 中过滤 "connection reset" 类
- 加
maxretry 容忍度
D.5 案例 5:客户端心跳被误判
现象:客户端每 30 秒发心跳,断线重连时被算作新连接 + 失败。
解决:
- 改 filter
- 加
maxretry 容忍度
- 自定义
<HOST> 解析
附录 E:fail2ban-client 远程管理
Fail2ban 1.0+ 支持远程管理。配置服务端 socket 监听 TCP:
/etc/fail2ban/fail2ban.local :
[Definition]
socket = /var/run/fail2ban/fail2ban.sock
[socket]
[socktype=file]
[file=/var/run/fail2ban/fail2ban.sock]
[Definition]
# 或开 TCP socket(需谨慎)
[socket]
[socktype=tcp]
[address=0.0.0.0]
[port=8023]
[password=YOUR_STRONG_PASSWORD]
注意:开 TCP socket 必须配强密码 + 防火墙限制。
客户端连接:
fail2ban-client -s 10.0.0.1:8023 -p YOUR_PASSWORD status
但生产中更推荐:
- 用 SSH 远程登录到 fail2ban 主机执行 fail2ban-client
- 或用 Ansible / SaltStack 批量管理
- 或用 API(自建)
附录 F:日志切割
cat > /etc/logrotate.d/fail2ban <<'EOF'
/var/log/fail2ban.log {
daily
missingok
rotate 30
compress
delaycompress
notifempty
create 0640 root root
sharedscripts
postrotate
fail2ban-client flushlogs 2>/dev/null || true
endscript
}
EOF
fail2ban-client flushlogs 会重新打开 fail2ban.log 文件。
附录 G:与其它安全工具协同
G.1 与 WAF 协同
WAF(ModSecurity、OpenResty、Coraza)跑在 HTTP 层,Fail2ban 跑在主机层。两者可以独立工作:
- WAF:拦 SQL 注入、XSS、应用层 CC
- Fail2ban:拦 SSH 撞库、WordPress 撞库、FTP 弱口令
WAF 把 401/403 写到 nginx error log,Fail2ban 监听这个 log 自动 ban。
G.2 与 IDS 协同
Suricata、Snort 触发告警后,写到 syslog,Fail2ban 监听 syslog 自动 ban。
filter:
[Definition]
failregex = suricata: .* SRC=<HOST> .*$
G.3 与 SIEM 协同
ELK、Splunk 接收 fail2ban.log 写入,做威胁分析、报告、合规。
附录 H:故障复盘案例
H.1 案例 1:被大规模分布式扫 SSH
现象:1 小时内 5000 个不同 IP 尝试 SSH 登录。
根因:未启用 sshd jail。
修复:启用 sshd jail,maxretry=3,bantime=24h,recidive 必开。
复盘:
- 必须开 sshd jail
- 必须开 recidive
- 必须配监控
H.2 案例 2:被 CC 攻击导致 Fail2ban 占用大量内存
现象:fail2ban 进程内存占用 2GB,CPU 50%。
根因:nginx-bad-request filter 监听了 50GB 的大日志,pyinotify 跟不上节奏。
修复:
- 拆 jail,每个 jail 监控特定子日志
- logrotate 加大频率
- 改用 polling + 定时清理
复盘:
- 单个 logpath 不应超过 1GB
- logpath 必须 logrotate
- 监控 fail2ban 内存
H.3 案例 3:Fail2ban 升级后所有 jail 失效
现象:升级到 1.1 后所有 jail 报错,filter 不匹配。
根因:1.1 改了 failregex 语法,旧 filter 不兼容。
修复:使用官方最新 filter 模板,重新测试。
复盘:
- 升级前在测试环境跑一遍
- 自定义 filter 必须有备份
- 升级后必须
fail2ban-client status 验证
- 监控 fail2ban 自身存活
以上配置方案在云栈社区有更多实战讨论,欢迎交流分享你的 Fail2ban 落地经验。