Fail2ban 暴力封锁恶意扫描器
WordPress 站点上线后,访问日志中很快就会出现大量针对 xmlrpc.php、wp-login.php、wp-admin/install.php 等路径的扫描请求。这些请求来自自动化扫描器,频率高、IP 池大,手动封禁显然不现实。利用 Fail2ban 构建一套“露头就秒”的自动化封禁体系,是应对这类攻击的有效手段。
核心策略:maxretry = 1
对于不对外开放注册和登录的个人站点,所有对敏感路径的访问都来自扫描器或攻击者。将 maxretry 设置为 1,意味着任何 IP 只要访问一次被监控的敏感路径,就会被立即封禁。findtime 参数在这种配置下实际影响不大——无论统计窗口多长,第一次匹配就触发封禁。
bantime 设置为 604800 秒(7 天),足够阻断慢速扫描器的长期试探。
以下是一份基础的 jail.local 配置:
[DEFAULT]
ignoreip = 127.0.0.1/8 管理员公网IP
bantime = 604800
findtime = 60
maxretry = 1
[wordpress-attack]
enabled = true
filter = wordpress-scan
port = http,https
logpath = /var/log/nginx/example.com.log
maxretry = 1
findtime = 60
bantime = 604800
ignoreip 中需要添加管理员的公网 IP,防止误封。
自定义过滤器:覆盖 WordPress 攻击面
过滤器定义在 /etc/fail2ban/filter.d/wordpress-scan.conf 中,用于从访问日志中匹配恶意请求。以下是一份覆盖常见攻击路径的 failregex:
[Definition]
failregex = ^<HOST> [^”]* “(?:GET|POST) /+xmlrpc\.php[^”]*” (?:200|301|302|403|404|500|401|444)
^<HOST> [^”]* “GET /wp-admin/install\.php[^”]*” (?:200|301|302|403|404|500|401|444)
^<HOST> [^”]* “GET /wp-admin/upgrade\.php[^”]*” (?:200|301|302|403|404|500|401|444)
^<HOST> [^”]* “GET /wp-admin/setup-config\.php[^”]*” (?:200|301|302|403|404|500|401|444)
^<HOST> [^”]* “(?:GET|POST) /wp-login\.php[^”]*” (?:200|301|302|403|404|500|401|444)
^<HOST> [^”]* “(?:GET|POST|OPTIONS) /wp-json/wp/v2/users[^”]*” (?:200|301|302|403|404|500|401|444)
^<HOST> [^”]* “GET /+[^”]*\?[^”]*author=\d+[^”]*” (?:200|301|302|403|404|500|401|444)
^<HOST> [^”]* “GET /+[^”]*\?[^”]*author=[a-zA-Z][^”]*” (?:200|301|302|403|404|500|401|444)
^<HOST> [^”]* “GET /wp-includes/wlwmanifest\.xml[^”]*” (?:200|301|302|403|404|500|401|444)
^<HOST> [^”]* “GET /readme\.html[^”]*” (?:200|301|302|403|404|500|401|444)
^<HOST> [^”]* “GET /license\.txt[^”]*” (?:200|301|302|403|404|500|401|444)
^<HOST> [^”]* “GET /wp-config\.php[^”]*” (?:200|301|302|403|404|500|401|444)
^<HOST> [^”]* “GET /\.env[^”]*” (?:200|301|302|403|404|500|401|444)
^<HOST> [^”]* “GET /\.git[^”]*” (?:200|301|302|403|404|500|401|444)
^<HOST> [^”]* “GET /wp-content/[^”]*\.(?:sql|bak|log|tar\.gz|zip)[^”]*” (?:200|301|302|403|404|500|401|444)
^<HOST> [^”]* “GET /phpmyadmin[^”]*” (?:200|301|302|403|404|500|401|444)
^<HOST> [^”]* “GET /adminer[^”]*” (?:200|301|302|403|404|500|401|444)
^<HOST> [^”]* “GET /phpinfo\.php[^”]*” (?:200|301|302|403|404|500|401|444)
^<HOST> [^”]* “GET /backup\.zip[^”]*” (?:200|301|302|403|404|500|401|444)
^<HOST> [^”]* “GET /docker-compose\.yml[^”]*” (?:200|301|302|403|404|500|401|444)
ignoreregex =
状态码列表中包含了 200、301、302、403、404、500、401 和 444,覆盖了 Nginx 可能返回的各种响应。/+ 用于匹配路径中可能出现的多个斜杠(如 //xmlrpc.php),author= 规则分别匹配数字型和非数字型作者 ID 枚举。
配置完成后,通过 fail2ban-regex 验证过滤器能否正确匹配日志中的攻击请求。
自定义 Action:封禁 + 中断连接
默认的 iptables-multiport action 只会添加防火墙规则,不会中断已建立的 TCP 连接。攻击者如果复用了 Keep-Alive 长连接,即使 IP 被加入黑名单,已建立的连接仍可继续发送请求,导致封禁后日志仍在刷屏。
ss -K 强制中断。
解决方法是创建自定义 action,在封禁的同时强制中断该 IP 的所有 TCP 连接。
新建 /etc/fail2ban/action.d/iptables-killconn.conf:
[Definition]
actionban = iptables -I INPUT 1 -s <ip> -j REJECT –reject-with icmp-port-unreachable
ss -K dst <ip>
actionunban = iptables -D INPUT -s <ip> -j REJECT –reject-with icmp-port-unreachable
ss -K dst <ip> 会强制终止来自该 IP 的所有 TCP 连接。-I INPUT 1 确保规则插入到 INPUT 链最前面,优先于任何允许规则。
在 jail.local 中引用该 action:
[wordpress-attack]
action = iptables-killconn
Nginx 层前置拦截
在 Nginx 层对敏感路径直接返回 404 或 444,可以在请求到达后端之前就完成拦截,减少 PHP 资源消耗。
在站点配置的 server 块中添加:
# 静默丢弃 xmlrpc.php 和 install.php
location = /xmlrpc.php {
return 444;
access_log off;
log_not_found off;
}
location = /wp-admin/install.php {
return 444;
access_log off;
log_not_found off;
}
# 普通屏蔽
location = /wp-config.php {
return 404;
}
return 444 是 Nginx 特有状态码,直接关闭连接而不返回任何响应。配合 access_log off 和 log_not_found off,这些请求不会在访问日志和错误日志中留下痕迹。
对于 REST API 用户枚举和作者枚举等路径,使用 if 指令在 location 匹配之前拦截:
if ($request_uri ~* “^/wp-json/wp/v2/users”) {
return 404;
}
if ($args ~* “\bauthor=\d+”) {
return 404;
}
Nginx 层拦截与 Fail2ban 形成双重防护:Nginx 在应用层静默拒绝,Fail2ban 在网络层封禁 IP。
常用管理命令
| 命令 | 说明 |
|---|---|
sudo fail2ban-client status |
查看所有启用的 jail |
sudo fail2ban-client status <jail名> |
查看特定 jail 的封禁详情 |
sudo fail2ban-client set <jail名> banip <IP> |
手动封禁一个 IP |
sudo fail2ban-client set <jail名> unbanip <IP> |
手动解封一个 IP |
sudo fail2ban-client reload |
重载配置文件 |
sudo iptables -L INPUT -n | grep <IP> |
确认防火墙规则是否存在 |
效果与验证
配置完成后,访问日志中针对敏感路径的请求会显著减少。Fail2ban 日志(/var/log/fail2ban.log)中会显示类似以下的封禁记录:
already banned 表示该 IP 已被封禁且防火墙规则存在,后续请求虽然仍可能出现在访问日志中(因为 Nginx 在防火墙之前记录日志),但实际已被网络层拦截。
通过 iptables -L INPUT -n | grep <IP> 可以确认防火墙规则确实存在。若发现某个 IP 被封禁后仍持续产生大量日志,说明该 IP 在封禁前已建立了 TCP 连接,ss -K 可以强制中断这些连接。
ss -K 处理。
总结
这套方案的核心逻辑是:不给扫描器任何试探的机会。maxretry=1 确保任何对敏感路径的访问都会被立即封禁;自定义 action 在封禁的同时中断已有连接;Nginx 层前置拦截进一步降低后端负载。三者结合,形成了一套“露头就秒”的自动化防御体系。
对于不对外开放注册和登录的个人站点,这套配置几乎没有误报风险,能够在最小化运维成本的前提下,有效抵御自动化扫描攻击。
1321231231231231测试