1.1 背景介绍

暴力破解是公网服务器面临的最常见攻击之一。SSH、Web 登录页面、API 接口是主要目标,攻击者使用 Hydra、Medusa、Burp Suite 等自动化工具,以每秒数百次的频率尝试用户名密码组合。一台暴露 22 端口的服务器,上线几小时内就会收到大量暴力破解请求,这在 /var/log/auth.log 中随处可见。

Fail2ban 是 Linux 环境下成熟的入侵防御工具,工作原理直观:监控日志文件 → 正则匹配失败记录 → 达到阈值触发封禁动作(通过 iptables/nftables 封锁 IP)→ 到期自动解封。整个过程无需人工干预,资源消耗极低。

核心架构由三大组件构成:

  • Jail(监狱):定义监控目标,包括日志路径、匹配规则、封禁参数

  • Filter(过滤器):定义正则表达式,从日志中提取失败的 IP 地址

  • Action(动作):定义触发封禁后的具体操作,如 iptables 规则、邮件通知、Webhook 调用

三者关系:Jail 引用 Filter 和 Action,Filter 负责"发现",Action 负责"执行",Jail 负责"编排"。

同类工具对比

特性

Fail2ban

DenyHosts

CrowdSec

支持服务

SSH/Web/邮件/自定义

仅 SSH

SSH/Web/自定义

封禁方式

iptables/nftables/firewalld

hosts.deny

iptables/nftables/云 API

分布式能力

无(单机)

有限(同步黑名单)

原生分布式,社区情报共享

社区威胁情报

有(Community Blocklists)

自定义 Filter

强,正则灵活

中等,YAML 场景定义

资源消耗

极低

中等(Go 编写,需 Agent)

成熟度

非常成熟,生态丰富

停止维护

较新,快速发展中

Fail2ban 在单机防护场景下仍然是最佳选择:配置灵活、生态成熟、资源消耗低。如果需要分布式情报共享,可以考虑 CrowdSec,但运维复杂度更高。

1.2 技术特点

  • 日志驱动:基于日志分析而非流量嗅探,不影响服务性能,不依赖内核模块

  • 正则灵活:Filter 使用 Python 正则表达式,可匹配任意日志格式

  • 多后端支持:支持 iptables、nftables、firewalld、tcpwrapper 等多种封禁后端

  • 渐进式封禁:通过 bantime.increment 实现累犯加重,首次短封、多次永封

  • 低资源消耗:Python 编写,常驻内存约 30-50MB,CPU 占用可忽略

  • 热加载:修改配置后 fail2ban-client reload 即可生效,无需重启服务

1.3 适用场景

  • SSH 暴力破解防护:公网服务器的基本安全底线

  • Nginx/Apache 登录页面防护:Web 应用的认证接口保护

  • 邮件服务防护:Postfix、Dovecot 等邮件服务的暴力破解防御

  • 自定义应用防护:任何能输出日志的应用都可以接入 Fail2ban

  • CC 攻击缓解:通过限制单 IP 高频请求,缓解应用层 DDoS

1.4 环境要求

组件

版本要求

说明

操作系统

Ubuntu 22.04+ / Debian 12+ / CentOS 8+

推荐 Ubuntu 22.04 LTS

Fail2ban

1.1.x

2025 最新稳定版,支持 nftables 和 bantime.increment

Python

3.8+

Fail2ban 1.1.x 已移除 Python 2 支持

防火墙

iptables / nftables / firewalld

Ubuntu 22.04 默认 nftables,CentOS 8+ 默认 firewalld

systemd

必须

Fail2ban 1.1.x 依赖 systemd 管理服务


二、详细步骤

2.1 安装与基础配置

2.1.1 安装 Fail2ban
# Ubuntu / Debian
sudo apt update && sudo apt install -y fail2ban

# CentOS 8+ / RHEL(EPEL 源)
sudo dnf install -y epel-release && sudo dnf install -y fail2ban

# 验证版本
fail2ban-client version
# 预期输出:Fail2Ban v1.1.x
2.1.2 配置文件层级

Fail2ban 的配置文件有明确的优先级层级,理解这一点非常重要:

/etc/fail2ban/
├── jail.conf            # 默认配置,升级时会被覆盖,永远不要直接修改
├── jail.local           # 本地覆盖配置,优先级高于 jail.conf
├── jail.d/              # 分文件配置目录,优先级最高
│   ├── sshd.conf
│   └── nginx.conf
├── filter.d/            # 过滤器定义目录
│   ├── sshd.conf
│   └── nginx-http-auth.conf
└── action.d/            # 动作定义目录
    ├── iptables-multiport.conf
    └── nftables-multiport.conf

优先级:jail.d/*.conf > jail.local > jail.conf。所有自定义配置写在 jail.local 或 jail.d/ 下,确保系统升级不会覆盖。

2.1.3 全局基础配置
# /etc/fail2ban/jail.local - 全局配置
[DEFAULT]
# 封禁时间,单位秒(默认 10 分钟)
bantime = 3600

# 检测时间窗口:在 findtime 秒内达到 maxretry 次失败则封禁
findtime = 600

# 最大失败次数
maxretry = 5

# 白名单 IP,多个用空格分隔,支持 CIDR
# 务必加入运维跳板机和监控服务器 IP
ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8 192.168.1.0/24

# 封禁后端选择:
# Ubuntu 22.04+ 推荐 nftables-multiport
# CentOS 8+ 使用 firewallcmd-ipset
# 传统环境使用 iptables-multiport
banaction = nftables-multiport
banaction_allports = nftables-allports

# 渐进式封禁(Fail2ban 1.1.x 特性)
# 累犯每次封禁时间翻倍,上限 4 周
bantime.increment = true
bantime.factor = 2
bantime.formula = ban.Time * math.exp(float(ban.Count) * banFactor) / math.exp(1 * banFactor)
bantime.maxtime = 4w

# 邮件通知(可选)
# destemail = admin@example.com
# sender = fail2ban@example.com
# mta = sendmail

# 后端选择:systemd 环境用 systemd,否则用 auto
backend = systemd
2.1.4 banaction 选择指南

banaction

适用环境

特点

iptables-multiport

传统 iptables 环境

兼容性最好,性能一般

nftables-multiport

Ubuntu 22.04+、Debian 12+

性能更好,规则管理更清晰

firewallcmd-ipset

CentOS 8+ / RHEL 8+

通过 firewalld 管理,使用 ipset 提升大量 IP 封禁性能

2.1.5 动作配置:iptables 与 firewalld

2.1.6 iptables-multiport 动作

Fail2ban 默认使用 iptables-multiport 动作。封禁时在 iptables 的 INPUT 链中添加一条 REJECT 规则:

# 查看当前 fail2ban 创建的 iptables 规则
iptables -L INPUT -n -v | grep -A 5 FAIL2BAN

# 手动查看 fail2ban 链
iptables -L f2b-sshd -n -v

# 示例输出:
# Chain f2b-sshd (1 references)
# target     prot opt in     out     source               destination
# REJECT     tcp  --  *      *       1.2.3.4             0.0.0.0/0            tcp dpt:22 reject-with tcp-reset

注意这里用的是 REJECT 而非 DROPREJECT 会向攻击者返回 ICMP 端口不可达或 TCP RST,拒绝明确、响应快速;DROP 则直接丢弃数据包,攻击者需要等待超时才能意识到被封禁。使用 REJECT 可以减少攻击者的等待时间,变相减少攻击资源消耗。

2.1.7 切换到 firewalld

如果服务器使用 firewalld 作为防火墙,可以切换 Fail2ban 的动作:

# 安装 firewalld(如果尚未安装)
yum install -y firewalld
systemctl start firewalld
systemctl enable firewalld

# 配置 firewalld(开放 SSH 的同时启用 rich rule)
firewall-cmd --permanent --add-service=ssh
firewall-cmd --reload

# 修改 jail.local 使用 firewalld 动作
# /etc/fail2ban/jail.local
[sshd]
enabled = true
filter = sshd
logpath = /var/log/secure
bantime = 1800
maxretry = 5
# 使用 firewalld rich rule 动作(支持更精细的控制)
action = firewallcmd-ipset[name=sshd-all, port="ssh", protocol=tcp]
# 重新加载 Fail2ban 配置
fail2ban-client reload

# 验证 firewalld 中有 fail2ban 的规则
firewall-cmd --list-all | grep -i fail2ban
# 或
firewall-cmd --direct --get-rules ipv4 filter f2b-sshd-all

2.1.8 动作脚本详解

Fail2ban 的动作本质上是一个 shell 脚本。iptables-multiport 的动作脚本位于 /etc/fail2ban/action.d/iptables-multiport.conf。核心逻辑:

# 封禁 IP(ban)
iptables -I f2b-<name> -s <ip> -p tcp --dport <port> -j REJECT

# 解封 IP(unban)
iptables -D f2b-<name> -s <ip> -p tcp --dport <port> -j REJECT

# 检查 IP 是否已被封禁
iptables -C f2b-<name> -s <ip> -j REJECT

了解这个逻辑的意义在于:当你需要排查"为什么 Fail2ban 没有封禁某个 IP"时,可以手动执行这些 iptables 命令来验证防火墙规则是否正确添加。

2.2 SSH 暴力破解防护

2.2.1 SSH Jail 配置
# /etc/fail2ban/jail.d/sshd.conf
[sshd]
enabled = true
port = ssh,22222                # 如果修改了 SSH 端口,这里一并添加
filter = sshd
# 使用 systemd 后端直接读取 journal,无需指定 logpath
backend = systemd
# 如果不用 systemd 后端,指定日志路径:
# logpath = /var/log/auth.log

# SSH 防护策略:比全局更严格
maxretry = 3                    # 3 次失败即封禁
findtime = 300                  # 5 分钟内
bantime = 600                   # 首次封禁 10 分钟(渐进式封禁会自动加重)

# SSH 专用白名单(追加到全局 ignoreip)
ignoreip = 127.0.0.1/8 ::1 10.0.0.100
2.2.2 Filter 正则解读

Fail2ban 1.1.x 内置的 sshd filter 已经覆盖了绝大多数场景,位于 /etc/fail2ban/filter.d/sshd.conf。核心匹配规则:

# /etc/fail2ban/filter.d/sshd.conf 关键部分(内置,通常不需要修改)
[Definition]
# 匹配模式:aggressive 模式覆盖更多攻击特征
mode = aggressive

# 常见匹配的日志行:
# "Failed password for root from 192.168.1.100 port 52312 ssh2"
# "Invalid user admin from 192.168.1.100 port 52312"
# "Connection closed by authenticating user root 192.168.1.100 port 52312 [preauth]"
# "Unable to negotiate with 192.168.1.100 port 52312: no matching key exchange method found"

设置 mode = aggressive 可以匹配更多攻击特征,包括连接重置、协议探测等行为。在公网服务器上推荐开启:

# /etc/fail2ban/jail.d/sshd.conf 中追加
[sshd]
filter = sshd[mode=aggressive]
2.2.3 渐进式封禁策略

渐进式封禁通过 bantime.increment 实现,已在全局 [DEFAULT] 中配置。实际效果:

封禁次数

封禁时长

说明

第 1 次

10 分钟

基础 bantime

第 2 次

~30 分钟

指数增长

第 3 次

~2 小时

持续加重

第 4 次

~8 小时

接近永封

第 5 次+

4 周(上限)

bantime.maxtime 封顶

封禁记录存储在 Fail2ban 的数据库中(/var/lib/fail2ban/fail2ban.sqlite3),重启服务不会丢失。

Fail2ban 内置一个特殊的 jail 叫 recidive,用于封禁被多次封禁的"惯犯"IP。当一个 IP 因为 SSH 暴力破解被封禁后,如果它之后再次被封禁,recidive 会给它一个更长的封禁时间。

# /etc/fail2ban/jail.local

[recidive]
enabled = true
filter = recidive
logpath = /var/log/fail2ban.log
banaction = iptables-allports
bantime = 604800      # 7 天
findtime = 86400      # 过去 1 天内
maxretry = 3           # 被封禁 3 次则触发 recidive

启用 recidive 前,需要先创建对应的 filter(默认已存在):

# 确认 recidive 过滤规则存在
ls /etc/fail2ban/filter.d/recidive.conf

# 启用并重启
fail2ban-client reload
fail2ban-client status recidive

2.3 Nginx/Apache Web 服务防护

2.3.1 HTTP 基础认证防护

Nginx 的 HTTP Basic Auth 失败会记录在 error.log 中,Fail2ban 内置了对应的 filter:

# /etc/fail2ban/jail.d/nginx.conf
[nginx-http-auth]
enabled = true
port = http,https
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
maxretry = 5
findtime = 300
bantime = 600
2.3.2 自定义 Nginx 登录页面防护

对于应用自身的登录接口(如返回 401/403 的 REST API),需要编写自定义 filter。假设登录失败时 Nginx access.log 记录如下:

192.168.1.100 - - [15/Mar/2025:10:23:45 +0800] "POST /api/login HTTP/1.1" 401 52

编写自定义 filter:

# /etc/fail2ban/filter.d/nginx-login.conf
[Definition]
# 匹配 POST 登录接口返回 401 或 403 的请求
failregex = ^<HOST> -.*"POST /api/login.*" (401|403)
# 忽略正常请求和健康检查
ignoreregex = ^<HOST> -.*"GET /health"
              ^<HOST> -.*"HEAD /"

对应的 jail 配置:

# /etc/fail2ban/jail.d/nginx-login.conf
[nginx-login]
enabled = true
port = http,https
filter = nginx-login
logpath = /var/log/nginx/access.log
maxretry = 10                   # 登录接口允许更多重试
findtime = 300
bantime = 1800                  # 首次封禁 30 分钟
2.3.3 WordPress 登录防护

WordPress 的 wp-login.php 是暴力破解的重灾区:

# /etc/fail2ban/filter.d/wordpress-login.conf
[Definition]
# 匹配 wp-login.php 的 POST 请求返回 200(WP 登录失败也返回 200)
# 因此需要结合频率判断,而非状态码
failregex = ^<HOST> -.*"POST /wp-login\.php
# 排除 wp-cron 和正常的 GET 请求
ignoreregex = ^<HOST> -.*"GET /wp-login\.php
              ^<HOST> -.*wp-cron\.php
# /etc/fail2ban/jail.d/wordpress.conf
[wordpress-login]
enabled = true
port = http,https
filter = wordpress-login
logpath = /var/log/nginx/access.log
maxretry = 5
findtime = 120                  # 2 分钟内 5 次 POST 即封禁
bantime = 3600
2.3.4 CC 攻击防护(高频请求限制)

针对单 IP 高频访问的 CC 攻击:

# /etc/fail2ban/filter.d/nginx-cc.conf
[Definition]
# 匹配所有请求(通过 maxretry 控制频率阈值)
failregex = ^<HOST> -.*"(GET|POST|PUT|DELETE) /
ignoreregex = ^<HOST> -.*"GET /static/
              ^<HOST> -.*"GET /favicon\.ico
# /etc/fail2ban/jail.d/nginx-cc.conf
[nginx-cc]
enabled = true
port = http,https
filter = nginx-cc
logpath = /var/log/nginx/access.log
maxretry = 300                  # 5 分钟内 300 次请求(根据业务调整)
findtime = 300
bantime = 600

2.4 自定义 Jail 开发

2.4.1 Filter 编写规范

自定义 filter 的核心是 failregex,使用 Python 正则表达式语法。关键规则:

  • <HOST> 是 Fail2ban 的特殊标记,匹配 IPv4/IPv6 地址,提取为封禁目标

  • 每行一个正则,多个正则之间是 OR 关系

  • ignoreregex 用于排除误报,格式与 failregex 相同

2.4.2 使用 fail2ban-regex 测试

编写 filter 后,务必用 fail2ban-regex 工具验证,避免上线后才发现正则不匹配:

# 测试 filter 是否能匹配日志
fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/nginx-login.conf

# 输出示例:
# Results
# =======
# Failregex: 42 total
# Ignoreregex: 3 total
# Date template hits: ...

# 用单条日志测试(调试用)
echo '192.168.1.100 - - [15/Mar/2025:10:23:45 +0800] "POST /api/login HTTP/1.1" 401 52' | \
  fail2ban-regex - /etc/fail2ban/filter.d/nginx-login.conf
2.4.3 自定义 Action 开发

除了封禁 IP,还可以在触发时执行自定义动作。Action 定义文件位于 /etc/fail2ban/action.d/,支持以下钩子:

  • actionstart:Fail2ban 启动时执行

  • actionstop:Fail2ban 停止时执行

  • actionban:封禁 IP 时执行

  • actionunban:解封 IP 时执行

# /etc/fail2ban/action.d/webhook-notify.conf
[Definition]
# 封禁时发送 Webhook 通知
actionban = /etc/fail2ban/scripts/webhook-notify.sh ban <ip> <name> <bantime>
# 解封时也通知(可选)
actionunban = /etc/fail2ban/scripts/webhook-notify.sh unban <ip> <name>

在 jail 中引用多个 action(封禁 + 通知):

[sshd]
action = nftables-multiport[name=sshd, port="ssh", protocol=tcp]
         webhook-notify
2.4.4 多日志源监控

一个 jail 可以监控多个日志文件,用换行分隔:

[nginx-login]
logpath = /var/log/nginx/site-a-access.log
          /var/log/nginx/site-b-access.log
          /var/log/nginx/site-c-access.log

也支持通配符:

logpath = /var/log/nginx/*-access.log

三、示例代码和配置

3.1 企业级 jail.local 完整配置

# /etc/fail2ban/jail.local
# 企业级 Fail2ban 配置 - 覆盖 SSH / Nginx / 邮件 / 自定义应用
# 适用 Fail2ban 1.1.x + Ubuntu 22.04

[DEFAULT]
# === 全局封禁参数 ===
bantime = 3600                  # 默认封禁 1 小时
findtime = 600                  # 检测窗口 10 分钟
maxretry = 5                    # 默认 5 次失败触发封禁

# === 白名单 ===
# 运维跳板机、监控服务器、办公网出口 IP
ignoreip = 127.0.0.1/8 ::1
           10.0.0.0/8
           172.16.0.0/12
           192.168.1.100/32

# === 封禁后端 ===
banaction = nftables-multiport
banaction_allports = nftables-allports
backend = systemd

# === 渐进式封禁 ===
bantime.increment = true
bantime.factor = 2
bantime.formula = ban.Time * math.exp(float(ban.Count) * banFactor) / math.exp(1 * banFactor)
bantime.maxtime = 4w
bantime.overalljails = true     # 跨 jail 累计封禁次数

# === 邮件通知(可选) ===
# destemail = sre-team@example.com
# sender = fail2ban@example.com
# mta = sendmail
# action = %(action_mwl)s

# ============================================
# SSH 防护
# ============================================
[sshd]
enabled = true
port = ssh
filter = sshd[mode=aggressive]
maxretry = 3
findtime = 300
bantime = 600

# ============================================
# Nginx HTTP 基础认证防护
# ============================================
[nginx-http-auth]
enabled = true
port = http,https
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
maxretry = 5
findtime = 300
bantime = 600

# ============================================
# Nginx 自定义登录接口防护
# ============================================
[nginx-login]
enabled = true
port = http,https
filter = nginx-login
logpath = /var/log/nginx/access.log
maxretry = 10
findtime = 300
bantime = 1800

# ============================================
# Nginx CC 攻击防护
# ============================================
[nginx-cc]
enabled = true
port = http,https
filter = nginx-cc
logpath = /var/log/nginx/access.log
maxretry = 300
findtime = 300
bantime = 600

# ============================================
# Postfix 邮件服务防护
# ============================================
[postfix]
enabled = true
port = smtp,465,submission
filter = postfix[mode=aggressive]
logpath = /var/log/mail.log
maxretry = 5
findtime = 300
bantime = 3600

# ============================================
# Dovecot IMAP/POP3 防护
# ============================================
[dovecot]
enabled = true
port = pop3,pop3s,imap,imaps
filter = dovecot[mode=aggressive]
logpath = /var/log/mail.log
maxretry = 5
findtime = 300
bantime = 3600

# ============================================
# 自定义应用防护(示例:内部管理后台)
# ============================================
[app-admin]
enabled = true
port = http,https
filter = app-admin
logpath = /var/log/myapp/access.log
maxretry = 5
findtime = 120
bantime = 3600

3.2 自定义 Nginx 登录防护完整示例

3.2.1 Filter 定义
# /etc/fail2ban/filter.d/nginx-login.conf
# 自定义 Nginx 登录接口防护过滤器
# 匹配登录接口返回 401/403 的请求

[Definition]
# 匹配 POST 到登录相关接口,返回 401 或 403
failregex = ^<HOST> -.*"POST /api/v[0-9]+/auth/login.*" (401|403)
            ^<HOST> -.*"POST /api/login.*" (401|403)
            ^<HOST> -.*"POST /admin/login.*" (401|403)
            ^<HOST> -.*"POST /user/signin.*" (401|403)

# 排除健康检查、静态资源、内部监控
ignoreregex = ^<HOST> -.*"GET /health"
              ^<HOST> -.*"GET /api/ping"
              ^<HOST> -.*Prometheus.*
              ^<HOST> -.*UptimeRobot.*

[Init]
# 日期格式(Nginx 默认格式)
datepattern = %%d/%%b/%%Y:%%H:%%M:%%S %%z
3.2.2 Jail 配置
# /etc/fail2ban/jail.d/nginx-login.conf
[nginx-login]
enabled = true
port = http,https
filter = nginx-login
logpath = /var/log/nginx/access.log
maxretry = 10
findtime = 300
bantime = 1800
# 同时封禁 + Webhook 通知
action = nftables-multiport[name=nginx-login, port="http,https", protocol=tcp]
         webhook-notify
3.2.3 验证 Filter
# 用实际日志测试匹配效果
fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/nginx-login.conf --print-all-matched

# 用模拟日志测试
echo '10.20.30.40 - - [15/Mar/2025:14:22:01 +0800] "POST /api/login HTTP/1.1" 401 45 "-" "curl/7.88"' | \
  fail2ban-regex - /etc/fail2ban/filter.d/nginx-login.conf

3.2.4 多端口防护:FTP、HTTP 基本认证、Nginx

Fail2ban 不只保护 SSH,它可以为任何有日志记录认证失败的服務创建 jail。

vsftpd 防护

[vsftpd]
enabled = true
filter = vsftpd
logpath = /var/log/vsftpd.log
bantime = 3600
maxretry = 5
action = iptables-multiport[name=vsftpd, port="ftp,ftp-data", protocol=tcp]

对应的 /etc/fail2ban/filter.d/vsftpd.conf(通常默认已存在)需要验证正则:

fail2ban-regex /var/log/vsftpd.log 'WARNING.*authentication failure.*rhost=<HOST>'

Nginx HTTP Basic Auth 防护

# /etc/fail2ban/filter.d/nginx-http-auth.conf
# 通常已存在,验证正则:
[Definition]
failregex = ^\s*<ADDR>\s+-\s+-\s+\S+\s+\[.*\]\s+\"HTTP/\d\.\d\"\s+40[13]\s+\d+\s+\".*\"\s+\".*\"\s*$|nginx-http-auth: wrong password

Nginx 防护:防止目录遍历和 SQL 注入/XSS 探测

[nginx-bad-requests]
enabled = true
filter = nginx-bad-requests
logpath = /var/log/nginx/access.log
bantime = 600
maxretry = 10
action = iptables-multiport[name=nginx-bad-req, port="http,https", protocol=tcp]

对应的 /etc/fail2ban/filter.d/nginx-bad-requests.conf

[Definition]
# 匹配目录遍历、SQL 注入、XSS 等恶意请求
failregex = ^<ADDR> - .*"(GET|POST).*\.\./.*" .*$
           ^<ADDR> - .*"(GET|POST).*(union|select|insert|drop|update|exec|script|javascript).*" .*$
           ^<ADDR> - .*"(eval\(|base64_decode|shell_exec).*" .*$

3.3 Webhook 通知脚本

封禁事件推送到企业微信/钉钉/Slack,便于安全团队实时感知攻击态势。

#!/bin/bash
# /etc/fail2ban/scripts/webhook-notify.sh
# Fail2ban 封禁事件 Webhook 通知脚本
# 支持企业微信、钉钉、Slack
# 用法:webhook-notify.sh <ban|unban> <ip> <jail_name> [bantime]

set -euo pipefail

# === 配置区域 ===
# 企业微信 Webhook(按需启用)
WECHAT_WEBHOOK="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY_HERE"
# 钉钉 Webhook(按需启用)
# DINGTALK_WEBHOOK="https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN"
# Slack Webhook(按需启用)
# SLACK_WEBHOOK="https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK"

# 通知开关:wechat / dingtalk / slack
NOTIFY_CHANNEL="wechat"

# === 参数解析 ===
ACTION="${1:-}"
IP="${2:-}"
JAIL="${3:-}"
BANTIME="${4:-N/A}"
HOSTNAME=$(hostname -f)
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')

if [[ -z "${ACTION}" || -z "${IP}" || -z "${JAIL}" ]]; then
    echo "用法: $0 <ban|unban> <ip> <jail_name> [bantime]" >&2
    exit 1
fi

# === IP 归属地查询(可选,依赖 curl + 公共 API) ===
get_ip_location() {
    local ip="$1"
    # 使用免费 API 查询,超时 3 秒
    local location
    location=$(curl -s --max-time 3 "https://ipinfo.io/${ip}/json" 2>/dev/null | \
        python3 -c "import sys,json; d=json.load(sys.stdin); print(f\"{d.get('country','未知')}/{d.get('region','未知')}/{d.get('org','未知')}\")" 2>/dev/null) || true
    echo "${location:-未知}"
}

IP_LOCATION=$(get_ip_location "${IP}")

# === 构造消息 ===
if [[ "${ACTION}" == "ban" ]]; then
    TITLE="🚨 Fail2ban 封禁告警"
    COLOR="warning"
    MSG_BODY="主机: ${HOSTNAME}\n监狱: ${JAIL}\n封禁IP: ${IP}\n归属地: ${IP_LOCATION}\n封禁时长: ${BANTIME}s\n时间: ${TIMESTAMP}"
else
    TITLE="✅ Fail2ban 解封通知"
    COLOR="info"
    MSG_BODY="主机: ${HOSTNAME}\n监狱: ${JAIL}\n解封IP: ${IP}\n归属地: ${IP_LOCATION}\n时间: ${TIMESTAMP}"
fi

# === 发送通知 ===
send_wechat() {
    local payload
    payload=$(cat <<EOJSON
{
    "msgtype": "markdown",
    "markdown": {
        "content": "### ${TITLE}\n${MSG_BODY}"
    }
}
EOJSON
)
    curl -s --max-time 5 -H "Content-Type: application/json" \
        -d "${payload}" "${WECHAT_WEBHOOK}" > /dev/null 2>&1 || true
}

send_dingtalk() {
    local payload
    payload=$(cat <<EOJSON
{
    "msgtype": "markdown",
    "markdown": {
        "title": "${TITLE}",
        "text": "### ${TITLE}\n${MSG_BODY}"
    }
}
EOJSON
)
    curl -s --max-time 5 -H "Content-Type: application/json" \
        -d "${payload}" "${DINGTALK_WEBHOOK}" > /dev/null 2>&1 || true
}

send_slack() {
    local payload
    payload=$(cat <<EOJSON
{
    "text": "*${TITLE}*\n\`\`\`${MSG_BODY}\`\`\`"
}
EOJSON
)
    curl -s --max-time 5 -H "Content-Type: application/json" \
        -d "${payload}" "${SLACK_WEBHOOK}" > /dev/null 2>&1 || true
}

# 根据配置发送
case "${NOTIFY_CHANNEL}" in
    wechat)   send_wechat ;;
    dingtalk) send_dingtalk ;;
    slack)    send_slack ;;
    *)        echo "未知通知渠道: ${NOTIFY_CHANNEL}" >&2; exit 1 ;;
esac

echo "[${TIMESTAMP}] ${ACTION} ${IP} (${JAIL}) 通知已发送至 ${NOTIFY_CHANNEL}"

设置脚本权限:

sudo chmod +x /etc/fail2ban/scripts/webhook-notify.sh

自定义告警:企业微信 / 钉钉集成

默认情况下 Fail2ban 通过邮件告警,但邮件容易被忽略。实际生产环境更推荐与企业微信或钉钉的机器人 webhook 集成:

# 创建告警脚本:/etc/fail2ban/action.d/wechat.sh
#!/bin/bash
# 企业微信机器人告警脚本

# 参数:
# $1 = fail2ban jail 名称(如 sshd)
# $2 = 被封禁的 IP
# $3 = 攻击尝试次数(maxretry)
# $4 = 封禁时长(秒)

JAIL_NAME="$1"
IP="$2"
RETRY_COUNT="$3"
BANTIME_SEC="$4"
BANTIME_HOUR=$(echo"scale=2; $BANTIME_SEC/3600" | bc)

# 获取 IP 的归属地信息(可选,帮助判断是否为云服务器/数据中心)
# 使用 ip-api.com 免费接口(限制 45 次/分钟)
IP_INFO=$(curl -s "http://ip-api.com/json/${IP}?fields=country,countryCode,org,status" 2>/dev/null)
COUNTRY=$(echo"$IP_INFO" | jq -r '.country // "Unknown"')
ORG=$(echo"$IP_INFO" | jq -r '.org // "Unknown"')

# 企业微信 Webhook 地址(替换为实际的 webhook 地址)
WEBHOOK_URL="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=<YOUR_WEBHOOK_KEY>"

# 构造告警消息
MESSAGE="🚨 Fail2ban 安全告警

- **攻击类型**: ${JAIL_NAME}
- **恶意 IP**: ${IP}
- **IP 归属地**: ${COUNTRY} (${ORG})
- **尝试次数**: ${RETRY_COUNT} 次
- **封禁时长**: ${BANTIME_HOUR} 小时
- **服务器**: $(hostname)
- **时间**: $(date '+%Y-%m-%d %H:%M:%S')"

# 发送告警(企业微信要求 markdown 格式)
curl -X POST "${WEBHOOK_URL}" \
  -H "Content-Type: application/json" \
  -d "{
    \"msgtype\": \"markdown\",
    \"markdown\": {
      \"content\": \"${MESSAGE}\"
    }
  }" 2>/dev/null

exit $?
# 设置执行权限
chmod +x /etc/fail2ban/action.d/wechat.sh

在 jail.local 中使用自定义动作:

[sshd]
enabled = true
filter = sshd
logpath = /var/log/auth.log
bantime = 1800
maxretry = 5
action = iptables-multiport[name=sshd, port="ssh", protocol=tcp]
         /etc/fail2ban/action.d/wechat.sh[name=sshd, destemail=ops@yourdomain.com]

3.4 启动与验证

# 检查配置语法
fail2ban-client -t

# 启动服务
sudo systemctl start fail2ban
sudo systemctl enable fail2ban

# 查看所有 jail 状态
fail2ban-client status

# 查看指定 jail 详情
fail2ban-client status sshd

# 手动封禁/解封测试
fail2ban-client set sshd banip 203.0.113.100
fail2ban-client set sshd unbanip 203.0.113.100

# 查看当前 nftables 封禁规则
nft list ruleset | grep -A 5 f2b

四、最佳实践和注意事项

4.1 最佳实践

4.1.1 封禁策略设计

渐进式封禁(递增 bantime)

一刀切的封禁时长并不合理——偶尔输错密码的合法用户和持续扫描的攻击者,不应该受到相同的惩罚。Fail2ban 0.11+ 提供了 bantime.increment 机制:

# /etc/fail2ban/jail.local - 渐进式封禁配置
[DEFAULT]
# 启用递增封禁
bantime.increment = true
# 递增因子:每次封禁时长 = bantime * (1 * factor^(banCount))
bantime.factor = 24
# 封禁次数记忆窗口(7天内的封禁历史都会计入)
bantime.maxtime = 4w
# 首次封禁基础时长
bantime = 10m

实际效果:首次封禁 10 分钟 → 第二次 4 小时 → 第三次 4 天 → 最终触顶 4 周。对于偶发误触的用户影响极小,对持续攻击者则自动升级为长期封禁。

永久封禁黑名单

对于已确认的恶意 IP,直接写入持久化黑名单比依赖 Fail2ban 的动态封禁更可靠:

# /etc/fail2ban/action.d/blacklist.local
# 自定义 action:将累计封禁超过 5 次的 IP 写入永久黑名单
[Definition]
actionban = if [ $(grep -c '<ip>' /etc/fail2ban/ip.blacklist 2>/dev/null) -eq 0 ]; then
             echo '<ip>' >> /etc/fail2ban/ip.blacklist
            fi
            ipset add f2b-blacklist <ip> -exist

配合 ipset 的 timeout 0(永不过期),即使 Fail2ban 服务重启,黑名单依然生效。

GeoIP 国家级封禁

如果业务明确只面向国内用户,可以在 Fail2ban 之前用 GeoIP 做第一层过滤:

# 安装 GeoIP 模块(以 nftables 为例)
sudo apt install -y geoip-bin geoip-database

# 在 nftables 中按国家代码放行/拒绝
# /etc/nftables.d/geoip-filter.nft
table inet geoip_filter {
    set cn_allowed {
        type ipv4_addr
        flags interval
        # 通过 geoip 数据库生成中国 IP 段
    }
    chain input {
        type filter hook input priority -10; policy accept;
        tcp dport { 22, 80, 443 } ip saddr != @cn_allowed drop
    }
}

封禁数量上限控制

当封禁列表膨胀到数万条时,iptables 的逐条匹配会显著拖慢网络性能。建议设置上限:

# jail.local
[DEFAULT]
# 单个 jail 最大封禁数量
bantime.maxcount = 50000

慢速攻击检测

传统阈值(5 次/10 分钟)可以检测高频攻击,但无法检测慢速攻击——攻击者每 10 分钟尝试一次密码,永远不会触发 5 次阈值。应对慢速攻击的方案:

方案一:降低 maxretry 并延长 bantime。当封禁时长足够长时,即使攻击频率低,累计的封禁时长也会让攻击成本变高。

方案二:使用 pzkar 或类似工具分析 auth.log 中"未失败但可疑"的行为(如同一 IP 使用了大量不同用户名尝试登录)。这类工具检测的是"枚举用户名"行为——攻击者不知道服务器上有哪些账号,于是用常见用户名列表枚举。

# 使用 lastb 命令查看失败的登录记录(包含 SSH 和终端)
lastb -adn 100 | awk '{print $3}' | sort | uniq -c | sort -rn | head -20
# 统计最近 100 条失败登录记录中每个 IP 的出现次数

方案三:使用 sshguard 或配置 OpenSSH 的 MaxAuthTries 和 LoginGraceTime 参数做基础防护:

# /etc/ssh/sshd_config
MaxAuthTries 3
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2

MaxAuthTries 3 表示每个连接最多允许 3 次密码尝试(而非默认的 6 次),超过后直接断开。如果配合 Fail2ban 的 maxretry=3,即使攻击者只尝试了 3 次也会被封禁。

防止误封:白名单与可信 IP

误封的主要场景是:运维工程师从某个 IP 频繁登录跳板机,该 IP 因为多次输错密码(或者同事代输时输错)而被封禁。

白名单配置:在 jail.local 的 [DEFAULT] 段配置 ignoreip

[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 \
           10.0.0.0/8 \
           172.16.0.0/12 \
           192.168.0.0/16 \
           <公司办公网络出口IP/32> \
           <运维跳板机出口IP/32>

动态白名单机制:可以使用 ignorecommand 动态判断是否忽略某个 IP,例如查询数据库中的可信 IP 列表:

[DEFAULT]
ignorecommand = /usr/local/bin/check_trusted_ip.sh <ip>
#!/bin/bash
# /usr/local/bin/check_trusted_ip.sh
IP="$1"
# 查询公司的 IP 管理数据库或配置文件
if grep -q "^${IP}$" /etc/fail2ban/trusted_ips.txt 2>/dev/null; then
  exit 0  # 0 表示忽略(白名单)
else
  exit 1  # 非 0 表示不在白名单
fi
4.1.2 性能优化

banaction 选择对性能的影响

封禁动作的选择直接决定了高并发攻击下的系统开销:

banaction

封禁方式

万级 IP 性能

适用场景

iptables-multiport

逐条 -A 规则

差,线性匹配 O(n)

封禁量 < 500 的小规模场景

nftables-multiport

nft 规则集

中等,集合优化

中等规模,已迁移 nftables 的系统

firewallcmd-ipset

ipset 哈希集合

优秀,O(1) 查找

CentOS/RHEL firewalld 环境

nftables-allports

 + set

nft set 原生集合

优秀,内核态匹配

推荐,现代 Linux 首选方案

生产环境强烈建议使用 ipset 或 nftables set,当封禁列表达到 5000+ 条时,iptables 逐条匹配的 CPU 开销会变得不可忽视。

ipset 批量封禁配置

# /etc/fail2ban/jail.local
[DEFAULT]
banaction = iptables-ipset-proto6
# ipset 使用 hash:ip 类型,内核态 O(1) 匹配
# 相比 iptables 逐条规则,万级封禁下 CPU 开销降低 95%+

日志轮转与 Fail2ban 的配合

日志轮转后如果 Fail2ban 仍然持有旧文件描述符,会导致新日志无法被监控。正确的 logrotate 配置:

# /etc/logrotate.d/auth-fail2ban
/var/log/auth.log {
    daily
    rotate 30
    compress
    delaycompress
    missingok
    notifempty
    # 关键:轮转后通知 Fail2ban 重新打开日志文件
    postrotate
        /usr/bin/fail2ban-client flushlogs >/dev/null 2>&1 || true
    endscript
}

flushlogs 比 reload 更轻量——它只重新打开文件句柄,不会重新加载配置和重置计数器。

4.1.3 高可用部署

单机 Fail2ban 的封禁列表无法跨节点共享,攻击者换一台目标服务器就能绕过封禁。多台服务器间同步封禁状态有以下方案:

方案

实现方式

优点

缺点

数据库后端

dbpurgeage

 + SQLite/MySQL 共享

原生支持,配置简单

SQLite 不支持并发写入,MySQL 需额外维护

Redis 共享

自定义 action 写入 Redis Set

高性能,天然支持 TTL 过期

需要自行编写 action 脚本

CrowdSec

分布式安全引擎,API 共享决策

社区威胁情报、自动同步

架构较重,学习成本高

自定义同步脚本

rsync/scp 定时同步封禁列表

实现简单

存在同步延迟,不适合实时场景

对于 10 台以内的服务器集群,Redis 方案性价比最高:

#!/bin/bash
set -euo pipefail
# /etc/fail2ban/action.d/redis-ban.sh
# 将封禁 IP 写入 Redis,所有节点共享

REDIS_HOST="10.0.0.100"
REDIS_PORT="6379"
REDIS_KEY="fail2ban:banned"
IP="$1"
BANTIME="$2"

# 写入 Redis 并设置过期时间
redis-cli -h "${REDIS_HOST}" -p "${REDIS_PORT}" SET "${REDIS_KEY}:${IP}" 1 EX "${BANTIME}"
4.1.4 与其他安全工具联动

Fail2ban + CrowdSec 社区情报

CrowdSec 维护了一个全球共享的恶意 IP 情报库。将 Fail2ban 的封禁数据上报到 CrowdSec,同时拉取社区情报做预防性封禁:

# 安装 CrowdSec 并注册
sudo apt install -y crowdsec
sudo cscli hub update
sudo cscli collections install crowdsecurity/linux

# 将 Fail2ban 日志接入 CrowdSec 作为数据源
# /etc/crowdsec/acquis.yaml 追加:
# ---
# filename: /var/log/fail2ban.log
# labels:
#   type: fail2ban

与 WAF 联动

Fail2ban 检测到的攻击 IP 可以通过 API 推送到 Nginx ModSecurity 或云 WAF 的黑名单:

# /etc/fail2ban/action.d/waf-notify.conf
[Definition]
actionban = curl -s -X POST https://waf-api.internal/blocklist \
            -H "Authorization: Bearer <token>" \
            -d '{"ip":"<ip>","duration":<bantime>,"source":"fail2ban"}'
actionunban = curl -s -X DELETE https://waf-api.internal/blocklist/<ip> \
              -H "Authorization: Bearer <token>"

与 SIEM 集成

通过 syslog 或 JSON 格式日志将 Fail2ban 事件推送到 ELK/Splunk/Graylog:

# /etc/fail2ban/fail2ban.local
[Definition]
logtarget = SYSLOG
syslogsocket = /dev/log

4.2 注意事项

4.2.1 配置注意事项

⚠️ 警告:ignoreip 配置不当会导致管理员自锁

这是 Fail2ban 运维中最常见的事故——管理员忘记将自己的 IP 加入白名单,调试时触发封禁规则把自己锁在门外。

# /etc/fail2ban/jail.local - 白名单配置(必须优先配置)
[DEFAULT]
# 本机回环 + 内网段 + 管理员固定 IP + VPN 出口
ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 <管理员公网IP>
# 白名单支持 DNS 域名(会在启动时解析)
# ignoreip = office.example.com

关键注意事项:

  • bantime 设置过长(如 bantime = -1 永久封禁)时,一旦误封将无法通过等待自动解封来恢复,必须通过带外管理(IPMI/KVM/云控制台)登录处理

  • IPv6 支持需要确认 banaction 兼容性:iptables-multiport 不支持 IPv6,需使用 iptables-multiport 配合 ip6tables-multiport,或直接使用 nftables(原生双栈支持)

  • findtime 窗口过大会消耗更多内存,因为 Fail2ban 需要在内存中维护该时间窗口内的所有失败记录

4.2.2 常见错误

错误现象

原因分析

解决方案

Jail 启动后不封禁任何 IP

filter 正则与实际日志格式不匹配

fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf

 测试匹配

fail2ban-client status

 显示 jail 未运行

jail.local 中 enabled = true 未设置或语法错误

检查配置语法:fail2ban-client -d 2>&1 | grep -i error

封禁后攻击者仍可访问

banaction 规则插入位置在 ACCEPT 规则之后

iptables -L INPUT --line-numbers

 检查规则顺序,确保 f2b 链在前

日志轮转后 Fail2ban 停止检测

文件句柄指向已归档的旧日志

logrotate postrotate 中添加 fail2ban-client flushlogs

服务启动报 NOK: ("no section",)

jail.local 文件编码异常或缺少 section header

确认文件为 UTF-8 无 BOM 编码,首行必须是 [DEFAULT] 或 [sshd] 等 section

数据库锁定 database is locked

SQLite 并发写入冲突(多 jail 同时触发)

设置 dbpurgeage = 1d 减小数据库体积,或迁移到 MySQL 后端

4.2.3 兼容性问题

systemd journal 后端 vs 传统日志文件

现代发行版(Ubuntu 22.04+、RHEL 8+)默认使用 systemd journal,部分服务不再写入传统日志文件:

# 使用 systemd journal 作为日志源
[sshd]
backend = systemd
# journalmatch 过滤条件(替代 logpath)
journalmatch = _SYSTEMD_UNIT=sshd.service + _COMM=sshd

注意:backend = systemd 模式下 logpath 参数会被忽略,两者不能混用。

容器环境中的 Fail2ban

Docker/Kubernetes 环境下,Fail2ban 面临两个核心问题:容器网络的 NAT 导致源 IP 丢失,以及 iptables 规则与容器运行时冲突。

# Docker 环境:Fail2ban 需要操作 DOCKER-USER 链而非 INPUT 链
# /etc/fail2ban/action.d/docker-action.conf
[Definition]
actionban = iptables -I DOCKER-USER -s <ip> -j DROP
actionunban = iptables -D DOCKER-USER -s <ip> -j DROP

Cloud 环境 NAT 后的真实 IP 获取

在 AWS ALB/NLB、Cloudflare 等反向代理后面,日志中记录的是代理 IP 而非真实客户端 IP。需要确保上游服务正确传递 X-Forwarded-For 或使用 PROXY Protocol,并在应用日志中记录真实 IP 后再交给 Fail2ban 分析。


五、故障排查和监控

5.1 故障排查

5.1.1 日志查看
# 实时跟踪 Fail2ban 运行日志
sudo tail -f /var/log/fail2ban.log

# 查看所有 jail 的汇总状态
sudo fail2ban-client status

# 查看指定 jail 的详细信息(当前封禁列表、累计封禁/检测次数)
sudo fail2ban-client status sshd
# 输出示例:
# Status for the jail: sshd
# |- Filter
# |  |- Currently failed: 3
# |  |- Total failed:     1247
# |  `- File list:        /var/log/auth.log
# `- Actions
#    |- Currently banned: 12
#    |- Total banned:     389
#    `- Banned IP list:   203.0.113.5 198.51.100.23 ...

# 临时调高日志级别用于调试(无需重启服务)
sudo fail2ban-client set loglevel DEBUG
# 调试完成后恢复
sudo fail2ban-client set loglevel INFO
5.1.2 常见问题排查

问题一:Jail 已启动但 filter 不匹配任何日志行

这是最高频的问题,通常是日志格式变化导致正则失效。

# 使用 fail2ban-regex 离线测试 filter 匹配情况
fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf

# 输出关键信息解读:
# Lines: 15234 lines, 0 ignored, 847 matched, 14387 missed
# ↑ matched 为 0 说明正则完全不匹配,需要检查日志格式

# 用单行日志测试特定正则
fail2ban-regex \
  "Jun 15 08:23:01 server sshd[12345]: Failed password for root from 203.0.113.5 port 22 ssh2" \
  /etc/fail2ban/filter.d/sshd.conf

# 常见原因:系统升级后 sshd 日志格式变化(如 OpenSSH 9.x 调整了日志前缀)
# 解决:更新 filter.d/sshd.conf 或升级 Fail2ban 到最新版本

问题二:封禁规则未生效(IP 已在封禁列表但仍可访问)

# 检查 iptables 规则链顺序
sudo iptables -L INPUT --line-numbers -n
# 确认 f2b-sshd 链的跳转规则在 ACCEPT 规则之前
# 如果使用 nftables:
sudo nft list ruleset | grep -B2 -A5 f2b

# 检查封禁 IP 是否确实在防火墙规则中
sudo iptables -L f2b-sshd -n
# 或
sudo ipset list f2b-sshd 2>/dev/null

# 常见原因:Docker 的 FORWARD 链或 firewalld 的 zone 优先级覆盖了 Fail2ban 规则
# 解决:调整 banaction 使用 DOCKER-USER 链或 firewallcmd-rich-rules

问题三:误封合法用户的紧急处理

# 紧急解封指定 IP
sudo fail2ban-client set sshd unbanip 10.20.30.40

# 批量解封某个网段(需要逐个处理)
for ip in $(sudo fail2ban-client status sshd | grep "Banned IP" | sed 's/.*://'); do
    if echo "$ip" | grep -q "^10\.20\."; then
        sudo fail2ban-client set sshd unbanip "$ip"
    fi
done

# 解封后立即加入白名单防止再次封禁
sudo fail2ban-client set sshd addignoreip 10.20.30.40

问题四:Fail2ban 服务崩溃或无响应

# 检查服务状态和最近的错误日志
sudo systemctl status fail2ban
sudo journalctl -u fail2ban --since "1 hour ago" --no-pager

# 常见原因1:SQLite 数据库损坏
# 症状:日志中出现 "database disk image is malformed"
sudo systemctl stop fail2ban
sudo mv /var/lib/fail2ban/fail2ban.sqlite3 /var/lib/fail2ban/fail2ban.sqlite3.bak
sudo systemctl start fail2ban
# Fail2ban 会自动创建新数据库,但历史封禁记录会丢失

# 常见原因2:内存泄漏(长期运行 + 大量 jail)
# 检查 Fail2ban 进程内存占用
ps aux | grep fail2ban-server | grep -v grep
# 如果 RSS 持续增长超过 500MB,考虑设置定期重启
5.1.3 fail2ban-client 调试命令集
# 检查配置文件语法(不启动服务)
fail2ban-client -d 2>&1 | head -50

# 查看当前生效的所有配置(含默认值)
fail2ban-client -d 2>&1 | grep -E "^\['set'"

# 重新加载单个 jail(不影响其他 jail 的封禁状态)
fail2ban-client reload sshd

# 重新加载全部配置
fail2ban-client reload

# 查看指定 jail 的 filter 正则表达式
fail2ban-client get sshd failregex

# 查看指定 jail 的当前配置值
fail2ban-client get sshd bantime
fail2ban-client get sshd findtime
fail2ban-client get sshd maxretry

# 运行时动态修改参数(立即生效,重启后失效)
fail2ban-client set sshd bantime 7200
fail2ban-client set sshd maxretry 10

# 获取 Fail2ban 服务器 ping 状态
fail2ban-client ping
# 正常输出:Server replied: pong

5.2 性能监控

5.2.1 关键指标

日常运维需要关注的核心指标:

# 各 jail 当前封禁 IP 数量
for jail in $(fail2ban-client status | grep "Jail list" | sed 's/.*://;s/,//g'); do
    count=$(fail2ban-client status "$jail" | grep "Currently banned" | awk '{print $NF}')
    echo "${jail}: ${count} banned"
done

# 最近 24 小时封禁/解封事件统计
grep "$(date +%Y-%m-%d)" /var/log/fail2ban.log | grep -c "Ban "
grep "$(date +%Y-%m-%d)" /var/log/fail2ban.log | grep -c "Unban "

# 被封禁次数最多的 Top 10 IP
grep "Ban " /var/log/fail2ban.log | awk '{print $NF}' | sort | uniq -c | sort -rn | head -10






5.2.2 性能问题:日志文件过大导致扫描缓慢
如果 /var/log/auth.log 或 /var/log/secure 体积过大(超过数 GB),Fail2ban 每次扫描可能消耗大量 I/O 和 CPU。可以配合 logrotate 做日志轮转:

# /etc/logrotate.d/syslog(Debian/Ubuntu)
/var/log/auth.log {
    daily
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    create 0640 root adm
    sharedscripts
    postrotate
        # 通知 fail2ban 日志文件已轮转(避免 fail2ban 从轮转后的空文件重新开始计数)
        /usr/bin/fail2ban-client set logtarget /var/log/fail2ban.log 2>/dev/null || true
    endscript
}
对于 CentOS/RHEL:

# /etc/logrotate.d/secure
/var/log/secure {
    daily
    rotate 30
    compress
    delaycompress
    missingok
    notifempty
    create 0600 root root
    sharedscripts
    postrotate
        /bin/kill -HUP $(cat /var/run/syslogd.pid 2>/dev/null) 2>/dev/null || true
    endscript
}



5.2.3 性能问题:频繁封禁导致 iptables 规则过多
如果被封禁的 IP 数量过多(成千上万条规则),iptables 的线性匹配会变慢。解决方案:

方案一:使用 ipset 替代纯 iptables。ipset 是一个内核级哈希表,查找复杂度是 O(1),远比 iptables 链式查找快:

# jail.local 中使用 ipset 动作
[sshd]
enabled = true
filter = sshd
logpath = /var/log/auth.log
bantime = 1800
maxretry = 5
action = iptables-ipset-proto-port[name=sshd]
# 手动创建 ipset 集合(Fail2ban 会自动创建,但手动创建可以预先设置大小)
ipset create f2b-sshd hash:ip timeout 3600
ipset list f2b-sshd
方案二:定期清理已过期的封禁记录,防止 iptables 规则持续累积(即使 IP 已解封,iptables 规则在重启前不会自动删除)。Fail2ban 通常会自动清理,但可以手动验证:

# 查看 f2b-sshd 链中的规则数量
iptables -L f2b-sshd -n --line-numbers | tail -n +3 | wc -l

# 如果规则数量异常多(如超过 10000),可能是清理机制失效
# 检查 fail2ban 日志中是否有清理记录
grep -i "unban\|unbanned\|delete.*chain" /var/log/fail2ban.log | tail -20
5.2.2 监控指标说明

指标名称

正常范围

告警阈值

说明

当前封禁 IP 总数

< 500

> 2000

过高可能遭受大规模扫描或 banaction 性能下降

单小时新增封禁数

< 50

> 200

突增说明正在遭受集中攻击

filter 匹配失败率

< 5%

> 30%

过高说明日志格式可能已变化

Fail2ban 进程内存

< 200MB

> 500MB

持续增长提示内存泄漏

fail2ban-client ping 延迟

< 1s

> 5s

过高说明服务负载过重或数据库锁定

5.2.3 Prometheus + Grafana 监控

使用 fail2ban_exporter 将指标暴露给 Prometheus:

# 安装 fail2ban_exporter(Go 二进制,无额外依赖)
wget https://github.com/hectorj/fail2ban-prometheus-exporter/releases/latest/download/fail2ban_exporter_linux_amd64
chmod +x fail2ban_exporter_linux_amd64
sudo mv fail2ban_exporter_linux_amd64 /usr/local/bin/fail2ban_exporter

# 创建 systemd 服务
sudo tee /etc/systemd/system/fail2ban-exporter.service > /dev/null <<'EOF'
[Unit]
Description=Fail2ban Prometheus Exporter
After=fail2ban.service

[Service]
ExecStart=/usr/local/bin/fail2ban_exporter --web.listen-address=:9191
Restart=always
User=root

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload && sudo systemctl enable --now fail2ban-exporter

Prometheus 告警规则:

# /etc/prometheus/rules/fail2ban.yml
groups:
  - name: fail2ban
    rules:
      - alert: Fail2banHighBanRate
        expr: rate(fail2ban_banned_total[5m]) > 1
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "Fail2ban 封禁速率异常 ({{ $labels.jail }})"
          description: "jail {{ $labels.jail }} 在过去 5 分钟内封禁速率超过 1 IP/s"

      - alert: Fail2banServiceDown
        expr: up{job="fail2ban"} == 0
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "Fail2ban exporter 不可达"

5.3 备份与恢复

5.3.1 配置备份脚本
#!/bin/bash
set -euo pipefail
# fail2ban-backup.sh - Fail2ban 配置与数据完整备份
# 建议通过 crontab 每日执行:0 2 * * * /opt/scripts/fail2ban-backup.sh

BACKUP_DIR="/backup/fail2ban"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/fail2ban-backup-${DATE}.tar.gz"
RETAIN_DAYS=30

# 创建备份目录
mkdir -p "${BACKUP_DIR}"

# 创建临时工作目录
TMPDIR=$(mktemp -d)
trap 'rm -rf "${TMPDIR}"' EXIT

# 备份核心配置文件
cp -a /etc/fail2ban/jail.local "${TMPDIR}/" 2>/dev/null || true
cp -a /etc/fail2ban/jail.d/ "${TMPDIR}/" 2>/dev/null || true
cp -a /etc/fail2ban/filter.d/*.local "${TMPDIR}/filter.d/" 2>/dev/null || true
cp -a /etc/fail2ban/action.d/*.local "${TMPDIR}/action.d/" 2>/dev/null || true
cp -a /etc/fail2ban/fail2ban.local "${TMPDIR}/" 2>/dev/null || true

# 备份数据库(包含封禁历史和递增计数)
if [ -f /var/lib/fail2ban/fail2ban.sqlite3 ]; then
    # 使用 sqlite3 的 .backup 命令确保一致性(避免直接 cp 导致损坏)
    sqlite3 /var/lib/fail2ban/fail2ban.sqlite3 ".backup '${TMPDIR}/fail2ban.sqlite3'"
fi

# 备份自定义黑名单
cp -a /etc/fail2ban/ip.blacklist "${TMPDIR}/" 2>/dev/null || true

# 打包压缩
tar -czf "${BACKUP_FILE}" -C "${TMPDIR}" .

# 清理过期备份
find "${BACKUP_DIR}" -name "fail2ban-backup-*.tar.gz" -mtime +${RETAIN_DAYS} -delete

echo "[$(date)] 备份完成: ${BACKUP_FILE} ($(du -h "${BACKUP_FILE}" | awk '{print $1}'))"
5.3.2 恢复流程
#!/bin/bash
set -euo pipefail
# fail2ban-restore.sh - 从备份恢复 Fail2ban 配置
# 用法: ./fail2ban-restore.sh /backup/fail2ban/fail2ban-backup-20260225_020000.tar.gz

BACKUP_FILE="${1:?用法: $0 <备份文件路径>}"

if [ ! -f "${BACKUP_FILE}" ]; then
    echo "错误: 备份文件不存在: ${BACKUP_FILE}"
    exit 1
fi

# 1. 停止服务
echo "[1/4] 停止 Fail2ban 服务..."
sudo systemctl stop fail2ban

# 2. 备份当前配置(防止恢复失败时无法回退)
echo "[2/4] 备份当前配置..."
sudo cp -a /etc/fail2ban /etc/fail2ban.pre-restore.$(date +%s)

# 3. 解压恢复
echo "[3/4] 恢复配置文件..."
TMPDIR=$(mktemp -d)
trap 'rm -rf "${TMPDIR}"' EXIT
tar -xzf "${BACKUP_FILE}" -C "${TMPDIR}"

# 恢复配置文件(仅覆盖 .local 文件,不动默认配置)
sudo cp -f "${TMPDIR}"/jail.local /etc/fail2ban/ 2>/dev/null || true
sudo cp -rf "${TMPDIR}"/jail.d/ /etc/fail2ban/ 2>/dev/null || true
sudo cp -f "${TMPDIR}"/filter.d/*.local /etc/fail2ban/filter.d/ 2>/dev/null || true
sudo cp -f "${TMPDIR}"/action.d/*.local /etc/fail2ban/action.d/ 2>/dev/null || true

# 恢复数据库
if [ -f "${TMPDIR}/fail2ban.sqlite3" ]; then
    sudo cp -f "${TMPDIR}/fail2ban.sqlite3" /var/lib/fail2ban/
fi

# 4. 验证并启动
echo "[4/4] 验证配置并启动服务..."
sudo fail2ban-client -d > /dev/null 2>&1 && echo "配置语法检查通过" || { echo "配置语法错误,请检查"; exit 1; }
sudo systemctl start fail2ban
sudo fail2ban-client status

echo "恢复完成。"

六、生成环境实战配置模板

以下是一个生产环境可用的完整配置模板,兼顾安全性和可用性:

# /etc/fail2ban/jail.local

[DEFAULT]
# 封禁时间:默认 1 小时,首次封禁 1 小时,重复违规递增
bantime = 3600
bantime.increment = true
bantime.multipliers = 1 2 4 8 16 32 64
bantime.rndtime = 30

# 计数窗口:5 分钟内超过阈值则封禁
findtime = 300
maxretry = 5

# 白名单(必须根据实际情况配置)
ignoreip = 127.0.0.1/8 ::1 \
           10.0.0.0/8 \
           172.16.0.0/12 \
           192.168.0.0/16 \
           <跳板机出口IP>

# 日志配置
loglevel = INFO
logtarget = /var/log/fail2ban.log

# sshd jail
[sshd]
enabled = true
filter = sshd
logpath = /var/log/auth.log
bantime = 3600
maxretry = 5
findtime = 300
action = iptables-multiport[name=sshd, port="ssh", protocol=tcp]
         /etc/fail2ban/action.d/wechat.sh[name=sshd]

# nginx-http-auth jail(如果有使用 Nginx HTTP Basic Auth 的管理后台)
[nginx-http-auth]
enabled = false
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
bantime = 600
maxretry = 5
findtime = 600
action = iptables-multiport[name=nginx-http-auth, port="http,https", protocol=tcp]

# nginx-bad-requests jail(可选,防止目录遍历和 SQL 注入探测)
[nginx-bad-requests]
enabled = false
filter = nginx-bad-requests
logpath = /var/log/nginx/access.log
bantime = 600
maxretry = 15
findtime = 60
action = iptables-multiport[name=nginx-bad-req, port="http,https", protocol=tcp]

# recidive jail(封禁惯犯,7 天封禁)
[recidive]
enabled = true
filter = recidive
logpath = /var/log/fail2ban.log
bantime = 604800
findtime = 86400
maxretry = 3
action = iptables-allports[name=recidive]

7 验证与监控

7.1 功能验证

配置完成后,需要验证 Fail2ban 是否能正确工作:

# 方法一:手动模拟攻击(从另一台机器尝试用错误密码 SSH 登录)
# 注意:仅在测试环境中执行,不要在生产环境自我攻击

# 方法二:使用 fail2ban-client 手动触发封禁测试
fail2ban-client set sshd banip 192.0.2.1
# 然后检查
iptables -L f2b-sshd -n -v | grep 192.0.2.1
# 应该看到 REJECT 规则

# 方法三:解封并查看日志
fail2ban-client set sshd unbanip 192.0.2.1
tail /var/log/fail2ban.log | grep 192.0.2.1

附录

A. 命令速查表

# === 服务管理 ===
sudo systemctl start fail2ban          # 启动服务
sudo systemctl stop fail2ban           # 停止服务
sudo systemctl restart fail2ban        # 重启服务
sudo systemctl status fail2ban         # 查看服务状态

# === 状态查询 ===
fail2ban-client status                 # 查看所有 jail 汇总
fail2ban-client status sshd            # 查看指定 jail 详情
fail2ban-client ping                   # 检测服务是否响应

# === 封禁操作 ===
fail2ban-client set sshd banip 1.2.3.4       # 手动封禁 IP
fail2ban-client set sshd unbanip 1.2.3.4     # 手动解封 IP
fail2ban-client unban --all                   # 解封所有 jail 的所有 IP

# === 白名单管理 ===
fail2ban-client set sshd addignoreip 10.0.0.1    # 运行时添加白名单
fail2ban-client set sshd delignoreip 10.0.0.1    # 运行时移除白名单
fail2ban-client get sshd ignoreip                 # 查看当前白名单

# === 配置管理 ===
fail2ban-client reload                 # 重新加载全部配置
fail2ban-client reload sshd            # 重新加载单个 jail
fail2ban-client -d                     # 输出当前生效配置(调试用)
fail2ban-client set loglevel DEBUG     # 临时开启调试日志
fail2ban-client set loglevel INFO      # 恢复正常日志级别
fail2ban-client flushlogs              # 重新打开日志文件句柄

# === 运行时参数调整 ===
fail2ban-client get sshd bantime       # 查看当前 bantime
fail2ban-client set sshd bantime 3600  # 动态修改 bantime
fail2ban-client get sshd maxretry      # 查看当前 maxretry
fail2ban-client set sshd maxretry 5    # 动态修改 maxretry

# === Filter 测试 ===
fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf    # 测试 filter 匹配
fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf --print-all-matched  # 显示所有匹配行

B. 配置参数详解

jail.local 全局参数([DEFAULT] 段)

参数

默认值

说明

ignoreip 127.0.0.1/8 ::1

白名单 IP/CIDR/域名,空格分隔,永不封禁

bantime 10m

封禁持续时间,支持 s/m/h/d/w 后缀,-1 为永久

findtime 10m

检测时间窗口,在此窗口内累计失败次数

maxretry 5

触发封禁的失败次数阈值

backend auto

日志监控后端:auto/pyinotify/systemd/polling

banaction iptables-multiport

封禁动作,决定使用哪种防火墙机制

banaction_allports iptables-allports

全端口封禁动作(用于 recidive 等 jail)

action %(action_)s

封禁时执行的动作组合(封禁/通知/上报)

destemail root@localhost

告警邮件接收地址

sender root@<fq-hostname>

告警邮件发送地址

mta sendmail

邮件发送程序:sendmail/mail

protocol tcp

封禁协议:tcp/udp/icmp/all

chain INPUT

iptables 链名称

dbpurgeage 1d

数据库清理周期,超过此时间的记录自动删除

dbfile /var/lib/fail2ban/fail2ban.sqlite3

SQLite 数据库路径,设为 None 禁用持久化

jail.local 单 jail 参数

参数

说明

enabled

是否启用此 jail:true/false

port

监控的端口,支持数字或服务名(如 sshhttp,https

filter

使用的 filter 名称,对应 filter.d/<name>.conf

logpath

监控的日志文件路径,支持通配符(如 /var/log/nginx/access*.log

journalmatch

systemd journal 过滤条件(backend = systemd 时使用)

maxretry

覆盖全局 maxretry

findtime

覆盖全局 findtime

bantime

覆盖全局 bantime

bantime.increment

是否启用递增封禁:true/false

bantime.factor

递增因子,每次封禁时长乘以此系数

bantime.maxtime

递增封禁的最大时长上限

ignoreip

覆盖全局白名单(追加而非替换)

C. 术语表

术语

英文

解释

暴力破解

Brute Force Attack

通过穷举用户名/密码组合尝试登录的攻击方式

封禁

Ban

将恶意 IP 加入防火墙黑名单,阻断其网络访问

Jail

Jail

Fail2ban 的核心概念,一个 jail 定义了一组完整的检测+封禁规则

Filter

Filter

日志匹配规则,包含用于识别攻击行为的正则表达式

Action

Action

封禁/解封时执行的具体操作(如添加防火墙规则、发送邮件)

递增封禁

Incremental Ban

根据历史封禁次数自动延长封禁时间的机制

ipset

IP Set

Linux 内核的 IP 地址集合框架,支持 O(1) 复杂度的地址匹配

nftables

nftables

Linux 新一代包过滤框架,iptables 的替代方案

误封

False Positive

将合法用户的正常行为误判为攻击并封禁

带外管理

Out-of-Band Management

通过独立于业务网络的管理通道(IPMI/KVM/云控制台)访问服务器

CrowdSec

CrowdSec

开源分布式安全引擎,通过社区共享威胁情报实现协同防护

eBPF

Extended Berkeley Packet Filter

Linux 内核态可编程框架,可用于高性能网络过滤

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐