正在完善中
防火墙
系统防火墙
正确 action 名称
查看规则命令
iptables 旧版
iptables-multiport
iptables -L -n
nftables 新版
nftables-multiport
nft list ruleset
更新 手动更新 刷新软件包列表
这一步非常重要,因为你的源配置可能阻止了部分索引,强制刷新可以确保获取最新的(且被允许的)软件包信息。
1 sudo apt update --allow-releaseinfo-change
1 sudo apt install fail2ban
或
1 sudo apt upgrade fail2ban
二选一就可以了
自动更新 安装 PPA 依赖 1 2 # 18.04需先安装添加PPA的工具 sudo apt update -y && sudo apt install -y software-properties-common apt-transport-https ca-certificates
创建专属配置文件 /etc/apt/apt.conf.d/61-fail2ban-auto-upgrade 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 cat > /etc/apt/apt.conf.d/61-fail2ban-auto-upgrade <<'EOF' Unattended-Upgrade::Origins-Pattern { "origin=Ubuntu,archive=noble-security" ; "origin=Ubuntu,archive=noble-updates" ; }; Unattended-Upgrade::Package-Whitelist { "^fail2ban" ; }; Unattended-Upgrade::Automatic-Reboot "false" ; Unattended-Upgrade::Automatic-Restart-Services "true" ; EOF
校验文件是否生成成功 1 cat /etc/apt/apt.conf.d/61-fail2ban-auto-upgrade
重要配置 fail2ban ** 动作规则配置(触发封禁后的操作)**使用nano /etc/fail2ban/jail.d/nftables.conf打开这个文件,找到blocktype这个配置改成blocktype = drop,配置完成后,测试配置如果没有错误,就重启服务
1 2 3 4 5 6 7 nano /etc/fail2ban/jail.d/nftables.conf blocktype = drop fail2ban-client --test systemctl reload-or-restart fail2ban.service # 或者 fail2ban-client reload
fail2ban 优化配置nano /etc/fail2ban/fail2ban.conf即便重启服务器,屏蔽的IP还是处于屏蔽状态
1 2 3 4 5 6 # 开启持久化数据库(默认路径) dbfile = /var/lib/fail2ban/fail2ban.sqlite3 # 数据库清理周期:过期封禁记录自动清理,7天=604800秒 这个值一定要大于你设置封锁IP最长一个的时间,这里设置了90天 dbpurgeage = 90d # 每条IP最多保存几条匹配日志,减少库体积 dbmaxmatches = 5
按照如上设置,
dbpurgeage 含义:超过 90 天的历史记录,定期从 sqlite 里彻底删除
dbmaxmatches = 5 每个 IP 最多存 5 条触发封禁的日志记录,仅用来缩小 sqlite 体积,和重启恢复封禁无关。
测试fail2ban配置是否有问题
重启fail2ban服务 1 systemctl reload-or-restart fail2ban.service
查看底层配置防火墙
默认配置 创建一个defaults-debian.conf文件,存放以下内容;注意文件保存的格式,把这个文件放入到/etc/fail2ban/jail.d/中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 # ============================================================ # [DEFAULT] 段 - 全局默认配置 # ============================================================ # 此段配置会被所有 jail 继承,除非 jail 内单独覆盖 # ============================================================ [DEFAULT] # ------------------------------------------------------------ # 封禁动作配置(核心!) # ------------------------------------------------------------ # 封禁动作(针对指定端口) # 作用:当触发封禁时,使用此动作阻止攻击者 # 可选值: # - iptables-multiport → REJECT(发送拒绝响应) # - iptables-multiport-drop → DROP(直接丢弃,无响应)⭐推荐 # - nftables → 使用 nftables 防火墙 # - firewallcmd-ipset → 使用 firewalld # Ubuntu 20.04+ nftables 原生支持 banaction = nftables # banaction = iptables-multiport-drop # 封禁动作(针对所有端口) # 作用:当需要封禁所有端口时使用此动作 # 场景:某些 jail 配置了 allport = true # banaction_allports = iptables-allports-drop banaction_allports = nftables[type=allports] # IPv6 封禁动作(可选,如需要 IPv6 支持) # banaction_ipv6 = iptables-multiport-drop # banaction_allports_ipv6 = iptables-allports-drop banaction_ipv6 = nftables banaction_allports_ipv6 = nftables[type=allports] # ------------------------------------------------------------ # 日志后端配置 # ------------------------------------------------------------ # 日志读取后端 # 作用:指定如何读取系统日志 # 可选值: # - systemd → 使用 systemd journal(推荐,新版系统) # - auto → 自动检测 # - polling → 轮询日志文件 # - syslog → 使用 syslog backend = systemd # 轮询间隔,单位秒,默认1,数值越大CPU越低,日志捕获延迟越高 这个值与backend = polling有关 polltime = 60 # ------------------------------------------------------------ # 白名单配置 # ------------------------------------------------------------ # 忽略的 IP 地址(白名单) # 作用:这些 IP 永远不会被封禁 # 重要:务必添加自己的管理 IP,防止被锁在外面! # 格式:支持 CIDR 表示法 ignoreip = 127.0.0.1/8 ::1 100.104.0.0/16 # 启用递增封禁 bantime.increment = true # 递增因子 bantime.factor = 2 # 最大封禁时间 bantime.maxtime = 604800
action确认action_的定义位置
1 grep -r "^action_" /etc/fail2ban/jail.conf /etc/fail2ban/jail.local 2>/dev/null
配置action可以参考如下
1 2 3 4 5 6 # 在配置时候可以自己定义如下参数 filter = 这里可以自己定义 banaction = 底层防火墙 port = 端口号 protocol = 协议 action = %(banaction)s[name=%(filter)s-udp,port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
name= 后面的名字可以自定义,下面有这个参数的写法
chain 是防火墙规则链名称:
journalmatch配置(如果不想折腾,就直接跳过改章节)
只有在backend = systemd时候才会用到 journalmatch配置
如果同时配置了如下配置: journalmatch 这一行直接被 fail2ban 忽略,完全失效 ;
1 2 3 logpath = /var/log/auth.log backend = pyinotify journalmatch = _SYSTEMD_UNIT=sshd.service + _COMM=sshd
如果配置了 backend = systemd,就会执行下面这个功能,关于配置值按如下方式查找
1 journalmatch = _SYSTEMD_UNIT=sshd.service + _COMM=sshd
_SYSTEMD_UNIT 是 systemd-journal 日志自带的元标签(metadata),每条日志都会自动打上这个标记,记录这条日志是哪个 systemd 服务程序输出的。可以直接进入下面目录查看现在有哪些服务,这个就是写的服务名称
**+ ** 符号 含义
逻辑 AND(并且) ,代表前后两个条件必须同时满足 ,日志才会被保留,缺一不可。
**_COMM=**含义
_COMM 同样是 journal 内置元标签,代表进程启动的二进制程序文件名 (可理解为进程本名)。
_COMM的值查找使用如下命令
1 2 # 完整输出:PID / 内核进程名_COMM / 完整启动命令 | grep 查找相关的数据(只需要一个模糊的名字就可以,实在找不到就查百度) ps -eo pid,comm,cmd | grep "ssh"
pid:进程 ID
comm = journal 里的 _COMM 值 (内核短进程名,最多 15 字符)
cmd:完整启动命令路径 + 参数
在backend = systemd配置时候,可以添加如下值,如果backend不是配置的systemd时候也可以使用,
1 2 3 4 # 预过滤下放journalctl执行,配合journalmatch必开 prefilter = true # 公网服务器高频扫描设1000,抵御批量IP攻击;超大流量可上调至2000 maxmatches = 1000
详细工作逻辑 不开启 prefilter=false Fail2ban 先拉取该服务单元下全部原始日志(包含大量无关杂日志),再在 Python 内部逐条跑正则过滤,大量无用文本占用 CPU。开启 prefilter=true journalctl 依靠日志内置元数据索引,在把日志传给 fail2ban 之前,直接丢弃不满足 journalmatch 的日志; 最终只有目标服务(sshd/hbbr/nginx)的日志流入正则匹配环节。带来的效果 减少 90% 以上正则运算量,大幅降低 CPU 占用; 和 _SYSTEMD_UNIT + _COMM 组合是标准性能优化搭配; backend=pyinotify 读取文本日志时,此参数完全失效,不参与任何逻辑。 一句话总结 prefilter 是日志源头过滤开关,用来提前砍掉无关日志,减少后续计算压力。
_SYSTEMD_UNIT=sshd.service + _COMM=sshd作用 精准锁定进程,排除同名服务下的杂项子进程:
ssh配置添加完成后,要重启fail2ban服务
预认证洪水攻击检测配置规则 创建一个sshd-aggressive.conf文件,存放以下内容;注意文件保存的格式,把这个文件放入到/etc/fail2ban/filter.d/中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 [INCLUDES] before = common.conf [Definition] # 纯 Aggressive 攻击检测:暴力破解 / 扫描 / 无效用户 / 协议攻击 # 按威胁等级排序,高优先级规则在前 failregex = # 🔴 等级1:预认证洪水攻击(最高优先级) ^%(__prefix_line)sConnection (?:closed|reset) by authenticating user <F-USER>\S+</F-USER> <HOST>(?: port \d+)? \[preauth\]$ ^%(__prefix_line)sConnection closed by <HOST> port \d+ \[preauth\]$ # 🔴 等级2:root专项攻击 ^%(__prefix_line)sDisconnected from authenticating user root <HOST>(?: port \d+)? \[preauth\]$ ^%(__prefix_line)sFailed \w+ for root from <HOST>(?: port \d+)?(?: ssh\d*)?$ # 🟠 等级3:认证超限攻击 ^%(__prefix_line)s(?:error: )?maximum authentication attempts exceeded for (?:invalid user )?<F-USER>\S+</F-USER>(?: from \S+)? from <HOST>(?: port \d+)?$ # 🟠 等级4:无效用户扫描 ^%(__prefix_line)sInvalid user <F-USER>\S+</F-USER> from <HOST>(?: port \d+)?(?: ssh\d*)?$ # 🟡 等级5:密码暴力破解 ^%(__prefix_line)sFailed \w+ for (?:invalid user )?<F-USER>\S+</F-USER> from <HOST>(?: port \d+)?(?: ssh\d*)?$ # 🟡 等级6:协议协商攻击 ^%(__prefix_line)s(?:fatal: )?(?:Unable to negotiate with|Connection reset by) <HOST>.*: no matching (?:cipher|mac|key exchange|host key|kex) found # 🟢 等级7:未授权用户 ^%(__prefix_line)sUser <F-USER>\S+</F-USER> not allowed because not listed in AllowUsers from <HOST>(?: port \d+)?$ # 🟢 等级8:其他异常 ^%(__prefix_line)sReceived disconnect from <HOST>(?: port \d+)?:\s*3:\s*com\.juniper\.netscreen\.SSH2\.DisconnectReason\.ADMINISTRATIVELY_PROHIBITED ignoreregex = datepattern = {^LN-BEG} maxlines = 3 journalmatch = _SYSTEMD_UNIT=sshd.service + _COMM=sshd # 性能优化参数 maxmatches = 1000 prefilter = true encoding = auto
验证上面语法是否对日志生效 1 sudo fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd-aggressive.conf --print-all-matched
洪水攻击检测配置规则 创建一个sshd-maxstartups.conf文件,存放以下内容;注意文件保存的格式,把这个文件放入到/etc/fail2ban/filter.d/中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 # SSH MaxStartups/Connection Flood Protection # 检测:短时间内大量SSH连接请求 # 特征:Connection closed by authenticating user, MaxStartups exceeded [INCLUDES] before = common.conf [Definition] # 核心规则:按攻击严重程度排序 failregex = # 1. 真正的MaxStartups限制(最高优先级) ^%(__prefix_line)serror: maxstartups: <HOST> beyond limit, refused to connect\s*$ ^%(__prefix_line)serror: .*MaxStartups.*exceeded.*<HOST>.*$ # 2. 认证洪水攻击(高优先级) ^%(__prefix_line)s(?:error: )?maximum authentication attempts exceeded for .* from <HOST>.*$ ^%(__prefix_line)sDisconnecting: Too many authentication failures(?: for .*?)? from <HOST>.*$ # 3. 预认证连接洪水 ^%(__prefix_line)sConnection (?:closed|reset) by authenticating user <F-USER>\S+</F-USER> <HOST>(?: port \d+)? \[preauth\]\s*$ # ^%(__prefix_line)s(?:fatal: )?Connection closed by <HOST>.*\[preauth\]\s*$ # 4. 协议协商洪水 # ^%(__prefix_line)s(?:fatal: )?(?:Unable to negotiate with|Connection reset by) <HOST>.*: no matching (?:host|key|cipher|compression|MAC) found\s*$ # 5. 密码洪水攻击 ^%(__prefix_line)s(?:fatal: )?read_passwd:.*from <HOST>.*\s*$ ignoreregex = # 性能与兼容性优化 # datepattern = {^LN-BEG} 可以去除 datepattern = {^LN-BEG} maxlines = 3 journalmatch = _SYSTEMD_UNIT=sshd.service + _COMM=sshd # 检测模式优化 encoding = auto
验证上面语法是否对日志生效 1 2 # 检查过滤器语法 sudo fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd-maxstartups.conf --print-all-matched
兜底匹配规则 如果还有什么规则没有匹配到,直接写道这里面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 [INCLUDES] before = common.conf [Definition] failregex = ^%(__prefix_line)sbanner exchange: Connection from <HOST>.*invalid format ^%(__prefix_line)sConnection closed by <HOST> port \d+ ^%(__prefix_line)sbanner exchange: Connection from <HOST> port \d+: could not read protocol version ignoreregex = datepattern = {^LN-BEG} maxlines = 3 journalmatch = _SYSTEMD_UNIT=sshd.service + _COMM=sshd encoding = auto prefilter = true maxmatches = 200
验证上面语法是否对日志生效 1 2 sudo fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd-backstop.conf --print-all-matched
添加过滤器配置 创建一个customize-sshd.conf文件,存放以下内容;注意文件保存的格式,把这个文件放入到/etc/fail2ban/jail.d/中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 # ====================================================================== # 🔴 JAIL 1: sshd-aggressive (智能暴力破解防护) # 防御目标:高级暴力破解攻击、预认证洪水、协议攻击等 # 威胁等级:★★★★★ (最高) # ====================================================================== [sshd-aggressive] # 启用此 jail enabled = true # 保护端口 - SSH 默认端口 port = 10000,20000 # 使用的过滤器 - 对应 /etc/fail2ban/filter.d/sshd-aggressive.conf filter = sshd-aggressive # 日志文件路径 - systemd 系统使用 journal logpath = /var/log/auth.log # 后端类型 - 使用 systemd journal backend = systemd # ❗ 关键安全参数:最大重试次数 # 3次失败即封禁 - 因为这是高级攻击,需要零容忍 # 普通暴力破解通常5次,这里更严格因为威胁更大 maxretry = 3 # ❗ 关键安全参数:检测时间窗口(秒) # 60秒内累计3次失败 - 给予正常用户合理的时间窗口 # 同时能快速检测自动化攻击工具 findtime = 60 # ❗ 关键安全参数:基础封禁时长(秒) # 1209600秒 = 14天 - 长期封禁高级攻击者 # 防止攻击者等待短时间后继续攻击 bantime = 1209600 # ❗ 高级功能:智能递增封禁 # 同一IP多次违规,封禁时间会指数级增长 # 防止攻击者使用僵尸网络轮番攻击 bantime.increment = true # ❗ 递增因子:每次违规封禁时间乘以3 # 第1次:14天,第2次:42天,第3次:126天... bantime.factor = 3 # ❗ 最大封禁时长限制(秒) # 2592000秒 = 30天 - 防止无限增长,同时提供足够威慑 bantime.maxtime = 2592000 # ❗ 防火墙动作 - 使用 nftables # 比 iptables 更高效,支持更大规模的封禁列表 banaction = nftables # ❗ 防火墙链 - 在 input 链的最前面插入规则 # 确保最先匹配到封禁规则,提高性能 chain = input # ❗ 协议类型 - SSH 使用 TCP protocol = tcp # ❗ 动作配置 - 使用基础动作模板 # 这里会应用上面定义的 action_ 模板 action = %(action_)s # ====================================================================== # 🔵 JAIL 2: sshd-maxstartups (连接洪水防护) # 防御目标:连接洪水攻击、端口扫描、资源耗尽攻击 # 威胁等级:★★★★☆ (高) # ====================================================================== [sshd-maxstartups] # 启用此 jail enabled = true # 保护端口 - SSH 默认端口 port = 10000,20000 # 使用的过滤器 - 对应 /etc/fail2ban/filter.d/sshd-maxstartups.conf filter = sshd-maxstartups # 日志文件路径 - systemd 系统使用 journal logpath = /var/log/auth.log # 后端类型 - 使用 systemd journal backend = systemd # ❗ 关键安全参数:最大重试次数 # 5次连接尝试即封禁 - 比 aggressive 宽松一些 # 因为正常用户可能会有短暂的重连,需要平衡安全性与可用性 maxretry = 5 # ❗ 关键安全参数:检测时间窗口(秒) # 10秒内累计5次连接 - 非常短的时间窗口 # 专门检测快速连接洪水,正常用户不会在10秒内连接5次 findtime = 10 # ❗ 关键安全参数:封禁时长(秒) # 3600秒 = 1小时 - 短期封禁 # 连接洪水通常是短暂的,1小时足够让攻击者放弃 # 同时避免误封合法用户 bantime = 3600 # ❗ 防火墙动作 - 使用 nftables # 与 aggressive 使用相同的防火墙后端 banaction = nftables # ❗ 防火墙链 - 在 input 链 chain = input # ❗ 协议类型 - SSH 使用 TCP protocol = tcp # ❗ 动作配置 - 使用基础动作模板 # 不使用递增封禁,因为连接洪水通常是单次攻击 action = %(action_)s # ====================================================================== # 🔴 JAIL 3: 基础 SSH 防护 (保留你原有的配置) # 防御目标:常规密码暴力破解 # ====================================================================== [customize-sshd] # 启用该规则 enabled = true # 监控 SSH 端口(22) port = 10000,20000 # 使用 sshd 过滤规则 filter = sshd # 监控的日志文件 logpath = /var/log/auth.log backend = systemd # 5 次失败触发封禁 maxretry = 5 # 1 小时内统计失败次数 findtime = 3600 # 封禁 1 天 bantime = 86400 # IPv6 处理方式,auto 表示自动检测 allowipv6 = auto # ====================================================================== # 🔴 JAIL 4: 低频/预认证攻击防护 (新增配置) # 防御目标:你遇到的每10分钟试探一次的低密度攻击 # ====================================================================== [customize-sshd-low-freq] enabled = true port = 10000,20000 # 核心:使用 sshd 过滤器,并开启 aggressive 模式 filter = sshd[mode=aggressive] logpath = /var/log/auth.log backend = systemd # 1小时内累计3次预认证断开即封禁 maxretry = 3 findtime = 3600 # 封禁 1 天 bantime = 86400 [sshd-backstop-invalid-banner] # 是否启用该防护规则,true开启 false关闭 enabled = true # 日志读取源:auto自动识别journald或/var/log/auth.log logpath = auto # 使用上面我们自定义的过滤器文件名(无需带.conf后缀) filter = sshd-backstop # 最大允许触发次数:同一IP出现2次非法SSH握手即判定为攻击 maxretry = 2 # 封禁时长:单位秒,86400 = 1天 bantime = 86400 # 查找窗口:最近10分钟内的日志会参与计数(600秒) findtime = 3600 # 封禁动作,适配你的nftables防火墙 # banaction = nftables-multiport # 防护端口:修改过端口同步替换此处 port = 10000,20000
nginx 配置添加完成后,要重启fail2ban服务
可能没办法直接使用fail2ban提供的nginx防护能力,可以用作参考,需要手动添加自定义的文件
添加nginx错误日志检测规则 创建一个nginx-error-customize-banIP.conf文件,存放以下内容;注意文件保存的格式,把这个文件放入到/etc/fail2ban/filter.d/中
1 2 3 4 5 6 7 8 9 10 11 12 13 [INCLUDES] before = common.conf [Definition] # 修复1: 修正 datepattern 语法,使用单 %,并匹配日志中的完整时间格式 # datepattern = ^%%Y/%%m/%%d %%H:%%M:%%S # 规则1:nginx deny规则拦截 failregex = \[error\].*access forbidden by rule, client: <HOST> ignoreregex =
匹配日志格式如下:
1 2026/06/28 16:01:01 [error] 926#926: *845 access forbidden by rule, client: 2409:8c44:9400:1:0:4:100:8, server: www.goupos.cn, request: "GET /categories/Verilog%E8%AF%AD%E6%B3%95/ HTTP/2.0", host: "www.goupos.cn"
验证上面语法是否对日志生效 1 2 # 检查过滤器语法 sudo fail2ban-regex /var/log/nginx/error.log /etc/fail2ban/filter.d/nginx-error-customize-banIP.conf --print-all-matched
验证结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 Running tests ============= Use failregex filter file : nginx-error-customize-banIP, basedir: /etc/fail2ban Use datepattern : {^LN-BEG} : Default Detectors Use log file : /var/log/nginx/error.log Use encoding : UTF-8 Results ======= Failregex: 12 total |- #) [# of hits] regular expression | 1) [12] ^.*\[error\].* access forbidden by rule, client: <HOST> `- Ignoreregex: 0 total Date template hits: |- [# of hits] date format | [18] {^LN-BEG}ExYear(?P<_sep>[-/.])Month(?P=_sep)Day(?:T| ?)24hour:Minute:Second(?:[.,]Microseconds)?(?:\s*Zone offset)? `- Lines: 18 lines, 0 ignored, 12 matched, 6 missed [processed in 0.00 sec] |- Matched line(s): | 2026/06/28 01:28:38 [error] 1605418#1605418: *4624 access forbidden by rule, client: 119.0.68.61, server: www.goupos.cn, request: "GET /page/9/ HTTP/2.0", host: "www.goupos.cn"
添加ningx访问日志检测规则 创建文件 /etc/fail2ban/filter.d/nginx-protocol-customize-scan.conf,写入以下内容:加入customize好区分是不是自己写的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 [INCLUDES] before = common.conf [Definition] # 匹配包含 RDP 探测特征和 SSH 探测特征的 400 444错误日志 # 规则1: RDP探测包 \x03\x00\x00 + mstshash Cookie,返回400 failregex = ^<HOST> - \S+ \[.*?\] "\\x03\\x00\\x00.*Cookie: mstshash=.*" 400 .*$ # 规则2: SSH直连探测 SSH-2.0- 开头请求,返回400 ^<HOST> - \S+ \[.*?\] "SSH-2\.0-.*" 400 .*$ # 规则3: 同时满足状态码400/444 + 日志结尾 "-" "-" "-" ,第四段和第四段以后忽略 ^<HOST> - - \[.*?\] ".*?" (400|444) .* "-" "-" "-" # 规则4:HTTP/1.0 恶意请求 400/444,强制三段"-" ,第四段和第四段以后忽略 ^<HOST> - - \[.*?\] ".* HTTP\/1\.0" (400|444) .* "-" "-" "-" # 规则5:新增 —— 444状态码,XFF字段为"-" ,UA不限,第四段和第四段以后忽略 ^<HOST> - - \[.*?\] ".*" 444 \d+ ".*" ".*" "-" ignoreregex =
匹配日志格式如下:
1 2 45.227.254.155 - - [28/Jun/2026:05:58:31 +0800] "\x03\x00\x00/*\xE0\x00\x00\x00\x00\x00Cookie: mstshash=Administr" 400 150 "-" "-" "-" 139.59.78.146 - - [28/Jun/2026:12:35:33 +0800] "SSH-2.0-OpenSSH" 400 150 "-" "-" "-"
验证上面语法是否对日志生效 1 2 # 检查过滤器语法 sudo fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/nginx-protocol-customize-scan.conf --print-all-matched
验证结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 Running tests ============= Use failregex filter file : nginx-protocol-customize-scan, basedir: /etc/fail2ban Use datepattern : {^LN-BEG} : Default Detectors Use log file : /var/log/nginx/access.log Use encoding : UTF-8 Results ======= Failregex: 3 total |- #) [# of hits] regular expression | 1) [2] ^<HOST> - \S+ \[.*?\] "\\x03\\x00\\x00.*Cookie: mstshash=.*" 400 .*$ | 2) [1] ^<HOST> - \S+ \[.*?\] "SSH-2\.0-.*" 400 .*$ `- Ignoreregex: 0 total Date template hits: Lines: 332 lines, 0 ignored, 3 matched, 329 missed [processed in 0.04 sec] |- Matched line(s): | 194.165.16.167 - - [28/Jun/2026:00:51:28 +0800] "\x03\x00\x00/*\xE0\x00\x00\x00\x00\x00Cookie: mstshash=Administr" 400 150 "-" "-" "-" | 93.123.109.72 - - [28/Jun/2026:05:41:11 +0800] "\x03\x00\x00/*\xE0\x00\x00\x00\x00\x00Cookie: mstshash=Administr" 400 150 "-" "-" "-" | 139.59.78.146 - - [28/Jun/2026:12:46:06 +0800] "SSH-2.0-OpenSSH" 400 150 "-" "-" "-" `- Missed line(s): too many to print. Use --print-all-missed to print all 329 lines
添加过滤器配置 在/etc/fail2ban/jail.d目录下添加一个nginx-defense.conf文件,存放以下内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # 规则:匹配 access forbidden by rule 拒绝访问拉黑(自定义filter) [nginx-errorlog-customize-banIP] enabled = true logpath = /var/log/nginx/error.log # 自己写的filter,只匹配 access forbidden by rule filter = nginx-error-customize-banIP maxretry = 1 findtime = 3600 # 封禁30天 bantime = 2592000 # 启用递增封禁 bantime.increment = true # 递增因子 bantime.factor = 2 # 最大封禁时间 bantime.maxtime = 2592000 allowipv6 = auto backend = pyinotify action = nftables-multiport[name=nginx-customize-tcp, port="80,443,10000", protocol=tcp] nftables-multiport[name=nginx-customize-udp, port="80,443,10000", protocol=udp]
backend = polling 的作用是强制 Fail2ban 以轮询文件的方式读取日志,这样它就会严格使用您在 logpath 中指定的文件路径。
action = iptables-multiport[name=xxx, port="端口", protocol=tcp]
name=xxx:生成独立 iptables 链,区分限流 / 403 两种封禁来源,排查方便
port:封禁后该 IP 无法访问列表内所有 TCP 端口
底层动作固定为 DROP,服务器直接丢弃数据包,不回响应
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 [nginx-protocol-customize-scan] enabled = true port = 80,443,10000 filter = nginx-protocol-customize-scan logpath = /var/log/nginx/access.log findtime = 3600 maxretry = 1 bantime = 2592000 # 启用递增封禁 bantime.increment = true # 递增因子 bantime.factor = 2 # 最大封禁时间 bantime.maxtime = 2592000 allowipv6 = auto backend = pyinotify action = nftables-multiport[name=nginx-protocol-customize-scan-tcp, port="80,443,10000", protocol=tcp] nftables-multiport[name=nginx-protocol-customize-scan-udp, port="80,443,10000", protocol=udp]
RustDesk配置如果在调试rustdesk,不建议开启,以下配置会很严格,配置错误就会导致IP被封禁
添加过滤规则 创建一个rustdesk.conf文件,存放以下内容;注意文件保存的格式,把这个文件放入到/etc/fail2ban/filter.d/中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 [INCLUDES] before = common.conf [Definition] # 强制匹配核心特征,<HOST> 会自动识别并提取 IPv4 和 ::ffff:IPv6 格式的地址 # Relay authentication failed from [::ffff:217.146.80.105]:11392 - invalid key failregex = Relay authentication failed from \[<HOST>\]:\d+ - invalid key$ # Authentication failed from [::ffff:1.1.2.133]:43808 for peer 120724860 - invalid key Authentication failed from \[<HOST>\]:\d+ for peer \d+ - invalid key$ ignoreregex = # ====================== 日志读取性能优化 ====================== # maxlines:单次日志读取最多扫描连续行数,默认1 # 设置3是兼容多行报错日志(同一次攻击输出多条关联错误),兼顾识别不漏报与性能开销 maxlines = 3 # ====================== 编码兼容设置 ====================== # encoding:日志文件读取编码格式 # auto自动识别系统默认编码(UTF-8/ASCII),避免中文/特殊字符日志导致匹配失效 encoding = auto maxmatches = 200
添加过滤器配置 在/etc/fail2ban/jail.d目录下添加一个rustdesk.conf文件,存放以下内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 # Rustdesk HBBR HBBS 中继服务器防护监狱 [rustdesk] # 启用该防护规则,true 开启 / false 关闭 enabled = true # 绑定对应的自定义日志过滤规则文件 filter.d/rustdesk.conf filter = rustdesk # 监控 Rustdesk HBBR HBBS 程序输出的日志文件路径 logpath = /var/log/rustdesk-hbbr.log /var/log/rustdesk-hbbs.log # 需要封禁的端口:Rustdesk HBBR HBBS 默认业务端口,黑名单阻断这些端口入站 port = 21115,21116,21117,21118,21119 # 阈值:1小时内出现1次恶意行为,直接触发封禁, maxretry = 1 # 检测时间窗口:统计最近3600秒(1小时)内的异常记录 findtime = 3600 # 封禁时长:2592000秒 = 30天,拉黑IP一个月 bantime = 2592000 # 利用Linux内核inotify事件机制实时监听日志文件变更 backend = pyinotify # 启用递增封禁 bantime.increment = true # 递增因子 bantime.factor = 2 # 最大封禁时间 bantime.maxtime = 2592000 # IPv6处理策略:auto 自动跟随系统是否启用IPv6防火墙规则 allowipv6 = auto # 封禁动作,留空会使用全局[DEFAULT]段定义的banaction(nftables/iptables-ipset等) action = nftables-multiport[name=%(filter)s-tcp, port="%(port)s", protocol=tcp, chain="%(chain)s"] nftables-multiport[name=%(filter)s-udp, port="%(port)s", protocol=udp, chain="%(chain)s"]
ICMP洪水防御配置(未测试) 添加过滤规则 创建一个icmp-flood.conf文件,存放以下内容;注意文件保存的格式,把这个文件放入到/etc/fail2ban/filter.d/中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 [INCLUDES] before = common.conf [Definition] failregex = ^.*UFW_ICMP_FLOOD: .* SRC=<HOST> ignoreregex = # ====================== 日志读取性能优化 ====================== # maxlines:单次日志读取最多扫描连续行数,默认1 # 设置3是兼容多行报错日志(同一次攻击输出多条关联错误),兼顾识别不漏报与性能开销 maxlines = 3 # ====================== 编码兼容设置 ====================== # encoding:日志文件读取编码格式 # auto自动识别系统默认编码(UTF-8/ASCII),避免中文/特殊字符日志导致匹配失效 encoding = auto maxmatches = 200
添加过滤器配置 在/etc/fail2ban/jail.d目录下添加一个icmp-flood.conf文件,存放以下内容
1 2 3 4 5 6 7 8 9 10 [icmp-flood] enabled = true port = all filter = icmp-flood logpath = /var/log/syslog maxretry = 10 # 如果在 findtime 内出现 10 次上述日志 findtime = 60 # 时间窗口 60 秒 bantime = 3600 # 封禁 1 小时 backend = pyinotify action = ufw # 使用 ufw 动作进行封禁
工作原理:采用“令牌桶”机制。前 10 个包(burst)和之后每秒 5 个包(limit)会被直接 ACCEPT 放行。只有当攻击者发送的 Ping 频率超过了这个阈值,多出来的包才会命中后面的 LOG 和 DROP 规则。 对 Fail2ban 的影响:Fail2ban 监控的是 UFW_ICMP_FLOOD 前缀的日志。这意味着,只有真正的攻击者(高频 Ping)才会被 Fail2ban 封禁。正常的运维人员 Ping 一下服务器,不会产生日志,也不会被封禁。
待匹配日志数据 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 71.6.239.192 - - [1/Jun/2026:00:07:06 +0800] "" 400 0 "-" "-" "-" 71.6.239.192 - - [1/Jun/2026:00:07:08 +0800] "\x80\x00\x00(r\xFE\x1D\x13\x00\x00\x00\x00\x00\x00\x00\x02\x00\x01\x86\xA0\x00\x01\x97|\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 400 150 "-" "-" "-" 71.6.239.192 - - [1/Jun/2026:00:07:09 +0800] "\x00\x1E\x00\x06\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x07version\x04bind\x00\x00\x10\x00\x03" 400 150 "-" "-" "-" 71.6.239.192 - - [1/Jun/2026:00:07:09 +0800] "HELP" 400 150 "-" "-" "-" 71.6.239.192 - - [1/Jun/2026:00:07:10 +0800] "\x16\x03\x00\x00S\x01\x00\x00O\x03\x00?G\xD7\xF7\xBA,\xEE\xEA\xB2`~\xF3\x00\xFD\x82{\xB9\xD5\x96\xC8w\x9B\xE6\xC4\xDB<=\xDBo\xEF\x10n\x00\x00(\x00\x16\x00\x13\x00" 400 150 "-" "-" "-" 71.6.239.192 - - [1/Jun/2026:00:07:12 +0800] "\x16\x03\x01\x00\xEE\x01\x00\x00\xEA\x03\x03[Y\xE8\xA4#-\x93\xFB\x05uU&\xF4\xC1\x05\xC2b\xE4\xDE" 400 150 "-" "-" "-" 101.69.207.118 - - [27/Jun/2026:03:04:48 +0800] "GET /api/exclude/siteConfig/register HTTP/1.1" 444 0 "-" "okhttp/3.14.9" "-" 221.178.143.70 - - [27/Jun/2026:04:00:03 +0800] "GET / HTTP/1.1" 444 0 "-" "Mozilla/5.0 (Linux; U; Android 5.1.1; zh-CN; SM-C7000 Build/MMB29M; 720*1080) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/40.0.2214.89 UCBrowser/11.6.2.948 Mobile Safari/537.36" "-" 221.178.143.70 - - [27/Jun/2026:04:00:04 +0800] "GET /favicon.ico HTTP/1.1" 444 0 "-" "Dalvik/2.1.0 (Linux; U; Android 9.0; ZTE BA520 Build/MRA58K)" "-" 221.178.143.70 - - [27/Jun/2026:04:00:14 +0800] "GET / HTTP/1.1" 444 0 "-" "Dalvik/2.1.0 (Linux; U; Android 9.0; ZTE BA520 Build/MRA58K)" "-" 221.178.143.70 - - [27/Jun/2026:04:14:46 +0800] "GET / HTTP/1.1" 444 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11) AppleWebKit/601.1.27 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/601.1.27" "-" 124.117.194.104 - - [27/Jun/2026:08:54:05 +0800] "\x16\x03\x01\x05\xAC\x01\x00\x05\xA8\x03\x03X\xD7\xE6\xD8\x9B'\x8D\x02+Zi\xEC\xC4\x8F3&Q\xAB\x96w\xFB\xE8\xD5\xCE\x13\x90t\xF8\xFA9\xCAn |\xA7\x94\x1B\xA61M\xB1\x0E\xF5;\xCE\x1F\xF7j\xA2]^\xB2\x02\x90^W9Y\xCD\x8B\x81\xD5`\xA2\xAC\x00\x1A\xC0+\xC0/\xC0,\xC00\xCC\xA9\xCC\xA8\xC0\x09\xC0\x13\xC0" 400 150 "-" "-" "-" 222.176.200.74 - - [27/Jun/2026:08:54:06 +0800] "\x16\x03\x01\x05\xAC\x01\x00\x05\xA8\x03\x03o\xEA\x15\xED(\x1D\x0C{Y\xEF\x08\xECEY!q\x13\x02\x11]\xAF\x12\xD9wC\x86A#\x19\xBA\xEA\x95 \x80#\xBB\xB8Z\xA1(\xF8\xDD\xBE\xA0a\x91#&\x94i\xAF\xBEZ\xBB\x0B\xE9}I\xFEah\xAC\x10\xB7\xC3\x00\x1A\xC0+\xC0/\xC0,\xC00\xCC\xA9\xCC\xA8\xC0\x09\xC0\x13\xC0" 400 150 "-" "-" "-" 123.138.72.195 - - [27/Jun/2026:08:54:33 +0800] "USER anonymous" 400 150 "-" "-" "-" 185.132.176.32 - - [27/Jun/2026:20:22:40 +0800] "\x12\x01\x00/\x00\x00\x00\x00\x00\x00\x1A\x00\x06\x01\x00 \x00\x01\x02\x00!\x00\x01\x03\x00\x22\x00\x04\x04\x00&\x00\x01\xFF\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x01" 400 150 "-" "-" "-" 101.133.224.200 - - [27/Jun/2026:21:08:37 +0800] "" 400 0 "-" "-" "-" 101.133.224.200 - - [27/Jun/2026:21:08:38 +0800] "POST /sdk HTTP/1.1" 400 650 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36" "-" 101.133.224.200 - - [27/Jun/2026:21:08:38 +0800] "GET /nmaplowercheck1782565717 HTTP/1.1" 400 650 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36" "-" 101.133.224.200 - - [27/Jun/2026:21:08:38 +0800] "GET /HNAP1 HTTP/1.1" 400 650 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36" "-" 101.133.224.200 - - [27/Jun/2026:21:08:38 +0800] "GET /evox/about HTTP/1.1" 400 650 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36" "-" 101.133.224.200 - - [27/Jun/2026:21:08:58 +0800] "GET / HTTP/1.1" 400 248 "-" "-" "-" 171.36.7.191 - - [27/Jun/2026:08:54:06 +0800] "GET / HTTP/1.1" 444 0 "-" "-" "-" 101.133.224.200 - - [27/Jun/2026:21:08:38 +0800] "POST /sdk HTTP/1.1" 444 0 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36" "-" 101.133.224.200 - - [27/Jun/2026:21:08:38 +0800] "GET /nmaplowercheck1782565717 HTTP/1.1" 444 0 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36" "-" 101.133.224.200 - - [27/Jun/2026:21:08:38 +0800] "GET /HNAP1 HTTP/1.1" 444 0 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36" "-" 101.133.224.200 - - [27/Jun/2026:21:08:38 +0800] "GET /evox/about HTTP/1.1" 444 0 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36" "-" 101.133.224.200 - - [27/Jun/2026:21:08:58 +0800] "GET / HTTP/1.1" 444 0 "-" "-" "-" 223.90.190.188 - - [27/Jun/2026:00:19:48 +0800] "GET / HTTP/1.1" 444 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0" "-" 112.111.232.48 - - [27/Jun/2026:01:09:16 +0800] "GET / HTTP/1.1" 444 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0" "-" 42.234.195.196 - - [27/Jun/2026:01:21:20 +0800] "GET / HTTP/1.1" 444 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0" "-" 39.154.11.67 - - [27/Jun/2026:01:31:19 +0800] "GET / HTTP/1.1" 444 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0" "-" 116.196.103.2 - - [27/Jun/2026:04:38:31 +0800] "GET / HTTP/1.1" 444 0 "-" "BaiduSpider,Sogou,Yisou,Haosou,360Spider,BlackBerry,IEMobile,MQQBrowser,JUC,Fennec,wOSBrowser,Googlebot,AndroidBaiduSpider,Sogou,Yisou,Haosou,360Spider,BlackBerry,IEMobile,MQQBrowser,JUC,Fennec,wOSBrowser,Googlebot,Android JDCLOUD-SECURITY/ContentDetect" "-" 121.37.87.48 - - [27/Jun/2026:16:16:59 +0800] "GET /tags/IPV4/ HTTP/1.1" 444 0 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36" "-" 43.135.120.90 - - [27/Jun/2026:16:17:37 +0800] "GET /%E5%AF%86%E7%A0%81/ HTTP/1.1" 444 0 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36" "-" 129.226.207.99 - - [27/Jun/2026:16:17:42 +0800] "GET /pdf%E8%AE%BE%E7%BD%AE/ HTTP/1.1" 444 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36" "-" 185.132.176.32 - - [27/Jun/2026:19:33:16 +0800] "\x12\x01\x00/\x00\x00\x00\x00\x00\x00\x1A\x00\x06\x01\x00 \x00\x01\x02\x00!\x00\x01\x03\x00\x22\x00\x04\x04\x00&\x00\x01\xFF\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x01" 400 150 "-" "-" "-"
报错数据 查看 1 2 3 2026-06-29 20:49:17,894 fail2ban.filter [668]: WARNING [nginx-protocol-customize-scan] Simulate NOW in operation since found time has too large deviation None ~ 1782737357.8943663 +/- 60 2026-06-29 20:49:17,894 fail2ban.filter [668]: WARNING [nginx-protocol-customize-scan] Please check jail has possibly a timezone issue. Line with odd timestamp: x.x.x.x - - [29/Jun/2026:20:49:17 +0800] "GET /%E7%BC%96%E7%A8%8B/Verilog/list_of_terminal_names HTTP/1.1" 206 53225 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" "x.x.x.x,x.x.x.x"
修改/etc/fail2ban/jail.d目录下nginx-defense.conf文件,添加这个logtimezone = +0800
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 [nginx-protocol-customize-scan] enabled = true port = 80,443,10000 filter = nginx-protocol-customize-scan logtimezone = +0800 logpath = /var/log/nginx/access.log maxretry = 1 findtime = 3600 bantime = 2592000 # 启用递增封禁 bantime.increment = true # 递增因子 bantime.factor = 2 # 最大封禁时间 bantime.maxtime = 2592000 allowipv6 = auto backend = pyinotify action = nftables-multiport[name=nginx-protocol-customize-scan-tcp, port="80,443,10000", protocol=tcp] nftables-multiport[name=nginx-protocol-customize-scan-udp, port="80,443,10000", protocol=udp]
扩大时间偏差容错(如果有大量旧归档日志)
在 jail 增加 maxlines/timemaxoffset,允许更大时间差:
1 2 3 # 允许前后 3600 秒(1小时)时间偏差 timemaxoffset = 3600 logtimezone = +0800