生产环境里 SSH、FTP、网站登录、邮件服务被人扫是常态。扫的形态一般是:
最直接的应对:
但所有这些加起来,仍需要一道“最后一道门”——Fail2ban。它干的事情很简单:扫描日志,发现异常就封禁 IP 一段时间。安装、配置好之后就能让 SSH 撞库、WordPress xmlrpc 撞库、FTP 弱口令、HTTP 401 风暴这些事直接消失。
这一篇讲:
Fail2ban 本质是一个日志扫描 + 防火墙触发的守护进程:
/etc/fail2ban/jail.conf 与 /etc/fail2ban/jail.localpyinotify(Linux inotify)监听日志文件变更maxretry 次后触发 actionbantime 时长关键依赖:
pyinotify
(Linux 上监控文件)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、PIDini
[DEFAULT] backend = polling bantime = 10m findtime = 10m maxretry = 5 ignoreip = 127.0.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、systemdaction
:要执行的 actionusedns
:是否解析 hostnamefindtimeregex
、datepattern:自定义时间戳正则
/etc/fail2ban/filter.d/sshd.conf:
ini
[INCLUDES] before = common.conf [Definition] _daemon = sshd failregex = ^%(__prefix_line)s(? PAM: )?[aA]uthentication (?:failure|error|failed) for .* from (?:s+port d*)?(?: sshd*)?$ ^%(__prefix_line)s(? )?Received disconnect from (?::| ;)?s+(?:[a-z]+s+shutdown|reset|disconnect|connection closed)?s*$ ^%(__prefix_line)sUser .+ from not allowed because not listed in AllowUsers$ ^%(__prefix_line)sUser .+ from not allowed because none of user's groups are listed in AllowGroups$ ^%(__prefix_line)s(? )?refused connect from S+ ()$ ^%(__prefix_line)sInvalid user .* from $ ^%(__prefix_line)sFailed S+ for .* from (?:s+port d*)?s+$ ^%(__prefix_line)s(? )?maximum authentication attempts exceeded for .* from (?:s+port d*)?s+.*$ ignoreregex = 关键点:
是占位符,会被替换为 (?:::f{4,6}:)?(?P[w-.^_]*w) __prefix_line
是时间戳与进程名前缀fail2ban-regex 测试
/etc/fail2ban/action.d/iptables-multiport.conf 关键内容:
ini
[Definition] actionstart = iptables -N f2b- iptables -A f2b- -j RETURN iptables -I -p -m multiport --dports -j f2b- actionstop = iptables -D -p -m multiport --dports -j f2b- iptables -F f2b- iptables -X f2b- actioncheck = iptables -n -L | grep -q 'f2b-[ ]' actionban = iptables -I f2b- 1 -s -j DROP actionunban = iptables -D f2b- -s -j DROP action 类型:
iptables-multiport
:iptables 传统封禁iptables-allports
:所有端口nftables-multiport
:nftablesfirewalld
:firewalldufw
:ufwroute
:blackhole 路由cloudflare
:调用 Cloudflare APIslack-notify
:发 Slack 消息sendmail-*
:发邮件dshield
:上报 DShieldshorewall
:shorewall 防火墙firewalld 后端有专属 action:
ini
[DEFAULT] banaction = firewallcmd-rich-rules banaction_allports = firewallcmd-rich-rules chain = INPUT banaction 包括:
firewallcmd-ipsetfirewallcmd-rich-rulesfirewallcmd-new
(已弃用)注意:firewalld 后端不会立即封禁,要等 firewalld 重载;但 rich-rules 是热生效的。
ini
[DEFAULT] banaction = nftables-multiport chain = input
/etc/nftables.conf:
nft
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。
/var/lib/fail2ban/fail2ban.sqlite3 关键表:
bans
:当前封禁列表jails
:jail 状态logs
:日志(可选)查看数据库:
bash
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; 举例:
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 分钟实战经验:
recidive 是“惯犯”监狱:把其他 jail 的 ban 行为记录进 recidive 的 logpath,多次被 ban 的人会进 recidive jail 长时间封禁。
ini
[recidive] enabled = true filter = recidive logpath = /var/log/fail2ban.log bantime = 1w findtime = 1d maxretry = 5 action = iptables-multiport[name=recidive] bash
apt update apt install -y fail2ban systemctl enable fail2ban systemctl start fail2ban systemctl status fail2ban bash
yum install -y epel-release yum install -y fail2ban fail2ban-systemd systemctl enable fail2ban systemctl start fail2ban systemctl status fail2ban bash
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 bash
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 bash
# 版本 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。
/etc/fail2ban/jail.local:
ini
[DEFAULT] # 白名单 ignoreip = 127.0.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 段、家庭 IPbantime = -1
:永久封禁(慎用)usedns = no
:关闭 DNS 解析,避免 IP 反向解析慢导致 fail2ban 卡住action_mwl
:发邮件 + log + whois
/etc/fail2ban/jail.local 中追加:
ini
[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。
注意:
journalctl 的 systemd 后端,需要 backend = systemdbash
# 启动 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 ini
[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(系统自带):
ini
[Definition] failregex = ^ [error] d+#d+: *d+ user "([^"]+):"? client: , server: S+, request: "S+ S+ HTTP/d+.d+", host: "S+"(?:, referrer: "S+")?s*$ ignoreregex = 验证:访问一个有 basic auth 的 URL,输错 6 次,看是否被 ban。
ini
[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:
ini
[Definition] failregex = ^ -[^ "]* "[A-Z]+ /S+ HTTP/d+.d+" (4(04|03|44|13|14|51|93|95)) d+ .*$ ignoreregex = ^ -[^" ]* "[A-Z]+ /S+ HTTP/d+.d+" 200 d+ .*$ ini
[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/ini
[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(系统自带)匹配大量连接尝试。
ini
[vsftpd] enabled = true filter = vsftpd port = ftp,ftp-data,ftps,ftps-data logpath = /var/log/vsftpd.log maxretry = 3 findtime = 10m bantime = 24h ini
[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
/etc/fail2ban/filter.d/wordpress-xmlrpc.conf:
ini
[Definition] failregex = ^ .* "POST /xmlrpc.php HTTP/d+.d+" d+ .*$ ^ .* "POST /wp-login.php HTTP/d+.d+" d+ .*$ ignoreregex = ^ .* "(GET|HEAD) .*$
/etc/fail2ban/jail.local:
ini
[wordpress-xmlrpc] enabled = true filter = wordpress-xmlrpc port = http,https logpath = /var/log/nginx/access.log maxretry = 5 findtime = 5m bantime = 24h
/etc/fail2ban/action.d/dingtalk-webhook.conf:
ini
[Definition] actionstart = actionstop = actioncheck = actionban = curl -s -X POST "" -H "Content-Type: application/json" -d '{"msgtype":"text","text":{"content":"[Fail2ban] 封禁 IP 在 jail ,剩余时间 秒"}}' actionunban = curl -s -X POST "" -H "Content-Type: application/json" -d '{"msgtype":"text","text":{"content":"[Fail2ban] 解封 IP 在 jail "}}' [Init] webhook_url = https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN jail 中引用:
ini
[wordpress-xmlrpc] action = dingtalk-webhook[name=wordpress-xmlrpc]
/etc/fail2ban/action.d/slack-webhook.conf:
ini
[Definition] actionban = curl -s -X POST "" -H "Content-Type: application/json" -d '{"text":"[Fail2ban] Banned IP in jail "}' actionunban = curl -s -X POST "" -H "Content-Type: application/json" -d '{"text":"[Fail2ban] Unbanned IP in jail "}' [Init] webhook_url = https://hooks.slack.com/services/YOUR/WEBHOOK bash
pip install boto3
/etc/fail2ban/action.d/aws-waf.conf:
ini
[Definition] actionban = aws wafv2 update-ip-set --name --scope REGIONAL --id --addresses / 32 --change-token actionunban = ...
更简单的方案:使用 iptables 封禁 1 小时后,云厂商安全组在大网段(AS)层做兜底。
bash
pip install cloudflare
/etc/fail2ban/action.d/cloudflare-ban.conf:
ini
[Definition] actionban = python3 /etc/fail2ban/cloudflare-ban.py ban actionunban = python3 /etc/fail2ban/cloudflare-ban.py unban
/etc/fail2ban/cloudflare-ban.py:
python
#!/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 必须改为环境变量或受保护配置文件。
ini
[DEFAULT] # 加 IP 白名单 ignoreip = 127.0.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
bantime 不要写 -1,避免被 ban 后只能进机房解封。
ini
[DEFAULT] enabled = false 现象:多次失败密码,但 IP 没被 ban。
初步判断:filter 不匹配、logpath 错、bantime 没生效、iptables 不存在。
命令检查:
bash
# 看 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 的 IPFile list
:监控的日志文件Backend
:监控方式根因定位:
File list
列出空:logpath 不对或权限不足Currently failed
不增加:filter 不匹配Banned IP list
空:action 没成功f2b-sshd chain:banaction 错误修复方案:
验证结果:模拟失败,确认能 ban。
现象:连了几次失败后自己被 ban,无法连回。
解决:
bash
# 在另一台机器上 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 预防:
现象:Fail2ban 启动失败,提示 banaction 错误。
判断:
bash
# 看哪个防火墙在跑 systemctl status firewalld systemctl status nftables iptables -L -n nft list ruleset 修复:
ini
[DEFAULT] banaction = iptables-multiport # 或 banaction = nftables-multiport # 或 banaction = firewallcmd-rich-rules 现象:Fail2ban 进程 CPU 高。
原因:
usedns = yes
+ DNS 服务器慢findtime
太大修复:
usedns = warn
或 nobackend = pyinotify
(如果用 polling 改 inotify)现象:fail2ban.log 巨大。
原因:DEBUG 日志级别。
修复:
ini
[DEFAULT] loglevel = INFO 清理旧日志:
bash
truncate -s 0 /var/log/fail2ban.log systemctl reload fail2ban
使用 fail2ban-exporter 或文本文件方式:
/usr/local/bin/fail2ban-exporter.sh:
bash
#!/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:
bash
chmod +x /usr/local/bin/fail2ban-exporter.sh echo '* * * * * /usr/local/bin/fail2ban-exporter.sh' | crontab - yaml
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" bash
# 备份 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; bash
# 升级 yum update fail2ban systemctl restart fail2ban fail2ban-client status # 回滚 yum downgrade fail2ban-1.0.2-1.el7 systemctl restart fail2ban bash
# 停服 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 bash
# 启动 systemctl start fail2ban systemctl enable fail2ban systemctl status fail2ban # 停止 systemctl stop fail2ban # 重启 systemctl restart fail2ban # 重新加载配置 systemctl reload fail2ban bash
# 查看所有 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 bash
# 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 bash
# 测试 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: .*" # 自带一个简单正则 bash
# fail2ban 自身日志 tail -f /var/log/fail2ban.log journalctl -u fail2ban -f # 慢日志 journalctl -u fail2ban --since "1 hour ago" # 错误 journalctl -u fail2ban -p err bash
# 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 bash
sqlite3 /var/lib/fail2ban/fail2ban.sqlite3 sqlite> .tables sqlite> .schema bans sqlite> SELECT * FROM bans; sqlite> DELETE FROM bans; ini
[DEFAULT] ignoreip = 127.0.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]
/etc/fail2ban/filter.d/myapp-login.conf:
ini
[Definition] failregex = ^ -[^ "]* "POST /api/login HTTP/d+.d+" 401 d+ .*$ ^ -[^" ]* "POST /api/login HTTP/d+.d+" 403 d+ .*$ ignoreregex = ^ -[^ "]* "POST /api/login HTTP/d+.d+" 200 d+ .*$ jail:
ini
[myapp-login] enabled = true port = http,https filter = myapp-login logpath = /var/log/nginx/access.log maxretry = 10 findtime = 5m bantime = 24h ini
[Definition] failregex = ^%(__prefix_line)s(? )?Authentication refused: bad ownership or modes for directory .*$ ^%(__prefix_line)s(? )?Authentication refused: too many authentication failures for .* from (?:s+port d*)?s*$ ^%(__prefix_line)sUser .+ from not allowed because not listed in AllowUsers$ ^%(__prefix_line)sUser .+ from not allowed because none of user's groups are listed in AllowGroups$ ^%(__prefix_line)srefused connect from S+ ()$
/etc/fail2ban/action.d/audit-log.conf:
ini
[Definition] actionban = /usr/bin/logger -p auth.warning "Fail2ban banned in " actionunban = /usr/bin/logger -p auth.info "Fail2ban unbanned in "
/etc/fail2ban/jail.local:
ini
[DEFAULT] banaction = nftables-multiport banaction_allports = nftables-allports chain = input
/etc/nftables.conf:
nft
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 } } text
2026-06-29 1045,123 fail2ban.filter [1234]: WARNING [sshd] Found 1.2.3.4 2026-06-29 1045,456 fail2ban.actions [1234]: WARNING [sshd] Ban 1.2.3.4 2026-06-29 1045,789 fail2ban.actions [1234]: NOTICE [sshd] Unban 1.2.3.4 关键字:
Found
:检测到失败Ban
:封禁Unban
:解封Restore
/Save:规则变化bash
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 | 指标 | 来源 | 健康范围 | 异常表现 |
|---|---|---|---|
| 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 监控太多文件 |
业务侧应该把可疑 IP 加进 ignoreip,把生产出口 IP 加进 ignoreip。每月巡检一次。
journalctl -u fail2ban -xefail2ban-server -t
:前台调试模式fail2ban-regex
测试fail2ban-client status
看 Total failed 增长bantime = -1
:永久 bantime
命令校准journalctl
看异常fail2ban-regex --print-all-matched 验证__prefix_line 格式datepatternignoreip
没配会把自己 ban 出去bantime = -1
永久 ban 风险极高(重启或 banaction 改了就解除,但中间没人能进)usedns = yes
+ DNS 服务器慢会让 fail2ban 卡住maxretry
设得太小会误封正常用户banaction
与系统自带工具链(puppet、saltstack、ansible)的 iptables 模块可能冲突Currently failed 飙高findtime
太大 + maxretry 太小会让正常流量被 banbash
systemctl status fail2ban fail2ban-client status # 至少显示 Number of jail bash
# 模拟失败 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 bash
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 bash
curl http://127.0.0.1:9100/metrics | grep fail2ban_ bash
# 修改前 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 bash
# 停止 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 bash
yum downgrade fail2ban systemctl restart fail2ban ini
[DEFAULT] enabled = false bash
systemctl reload fail2ban ignoreip 中加好公司所有出口 IP、跳板机 IP、监控 IPmatch 与 bantime 不冲突的测试fail2ban-regex 严格测试iptables / nftables 规则数量bantime = -1Fail2ban 配置起来不难,但要跑稳,有 12 个关键点:
跑稳之后的实际收益是:SSH 撞库、网站爆破、FTP 弱口令、SMTP 撞库、WordPress xmlrpc 撞库这些常态化的攻击,全部被自动挡在门外。
最后留一个执行清单:
fail2ban-client status 有 Number of jail阿里云服务器默认安全组只放行业务端口,Fail2ban 主要作用是减少业务层被撞的次数。注意:不要把 22 端口放在公网(用跳板机 + EIP + 安全组 0.0.0.0/0 放行 + jump host);让 Fail2ban 集中处理 Web 类撞库即可。
推荐配置:
ini
[DEFAULT] ignoreip = 127.0.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] ini
[DEFAULT] ignoreip = 127.0.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] K8s 中用 sidecar 模式跑 Fail2ban 不太合适(因为容器内 iptables 行为特殊)。一般用 DaemonSet + hostPath 共享日志 + privileged 模式:
yaml
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 但实际生产中更多用:
Docker 容器内一般不需要 Fail2ban,但 Docker daemon 暴露的端口仍然可能被撞。常见做法:
容器化跑 Fail2ban 适用场景:需要统一管理多台机器 / 想用 fail2ban 触发外部 API。
bash
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 bash
# 用默认 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 1200 host sshd[1234]: Failed password for invalid user admin from 1.2.3.4" "Failed password for .* from " fail2ban-regex
跑一遍调试技巧:
必须是第一个出现的 IP__prefix_line
兼容 syslog、rsyslog、systemd 三种格式print-all-matched 看匹配是否合理日志样例:
text
[2026-06-29 1045] user_login FAILED user=alice ip=1.2.3.4 reason=password [2026-06-29 1046] user_login FAILED user=bob ip=1.2.3.4 reason=password [2026-06-29 1047] user_login OK user=carol ip=1.2.3.4 filter:
ini
[Definition] failregex = ^[d{4}-d{2}-d{2} d{2}:d{2}:d{2}] user_login FAILED user=S+ ip= reason=.*$ ignoreregex = ^[d{4}-d{2}-d{2} d{2}:d{2}:d{2}] user_login OK .*$ 测试:
bash
cat > /tmp/test.log <<'EOF' [2026-06-29 1045] user_login FAILED user=alice ip=1.2.3.4 reason=password [2026-06-29 1046] user_login FAILED user=bob ip=1.2.3.4 reason=password [2026-06-29 1047] user_login OK user=carol ip=1.2.3.4 EOF fail2ban-regex /tmp/test.log /etc/fail2ban/filter.d/myapp-login.conf 输出:
text
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= reason=.*$ | 1.2.3.4 Mon Jun 29 1045 2026 | 1.2.3.4 Mon Jun 29 1046 2026 `- Ignoreregex: 0 total Date template hits: |- [# of hits] date format | [3] Day-MONTH-Date HourSecond.Year `- Lines: 3 lines, 0 ignored, 2 matched, 1 failed bash
# 找 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 # 应该没有输出 bash
# 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 bash
# 先 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 bash
# 直接操作 iptables iptables -L f2b-sshd -n --line-numbers iptables -D f2b-sshd # 直接操作 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';"
现象:Jenkins / GitLab Runner 因为代码里 ssh user@host 错配密码,被 fail2ban 封。
解决:
现象:监控机器用 SSH 探测端口和服务,频繁触发 Connection closed by authentication 误匹配。
解决:
现象:办公网 NAT 后所有员工都从同一个出口 IP 出去,几个人错密码,全公司被 ban。
解决:
bantime
缩短现象:scp 传输大文件时网络抖动导致中断,被 fail2ban 算作失败。
解决:
maxretry 容忍度现象:客户端每 30 秒发心跳,断线重连时被算作新连接 + 失败。
解决:
maxretry 容忍度 解析Fail2ban 1.0+ 支持远程管理。配置服务端 socket 监听 TCP:
/etc/fail2ban/fail2ban.local:
ini
[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 必须配强密码 + 防火墙限制。
客户端连接:
bash
fail2ban-client -s 10.0.0.1:8023 -p YOUR_PASSWORD status 但生产中更推荐:
bash
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 文件。
WAF(ModSecurity、OpenResty、Coraza)跑在 HTTP 层,Fail2ban 跑在主机层。两者可以独立工作:
WAF 把 401/403 写到 nginx error log,Fail2ban 监听这个 log 自动 ban。
Suricata、Snort 触发告警后,写到 syslog,Fail2ban 监听 syslog 自动 ban。
filter:
ini
[Definition] failregex = suricata: .* SRC= .*$ ELK、Splunk 接收 fail2ban.log 写入,做威胁分析、报告、合规。
现象:1 小时内 5000 个不同 IP 尝试 SSH 登录。
根因:未启用 sshd jail。
修复:启用 sshd jail,maxretry=3,bantime=24h,recidive 必开。
复盘:
现象:fail2ban 进程内存占用 2GB,CPU 50%。
根因:nginx-bad-request filter 监听了 50GB 的大日志,pyinotify 跟不上节奏。
修复:
复盘:
现象:升级到 1.1 后所有 jail 报错,filter 不匹配。
根因:1.1 改了 failregex 语法,旧 filter 不兼容。
修复:使用官方最新 filter 模板,重新测试。
复盘:
fail2ban-client status 验证
全部0条评论
快来发表一下你的评论吧 !