通过之前的操作,我已经搭建好了wordpress个人网站,但没过多久,就收到了服务器提供商告警:CPU资源占用过高。通过日志排查(进入“my_website”文件夹后,输入命令:docker logs wordpress_nginx 2>&1 | tail -n 500
),锁定为:黑客暴力攻击式刷 xmlrpc.php 接口(日志中能看到很多个ip,不停的重复 POST,类似这样:POST //xmlrpc.php HTTP/1.1...)。
经查询,xmlrpc.php是 WordPress 提供的一个早期 API 接口,用于允许远程发布(例如通过手机App发文章),黑客利用这个接口,通过一次 POST 请求就可以尝试成百上千个用户名和密码的组合,这比攻击登录页面 wp-login.php 效率高得多。
我的网站虽然设置了每秒10个请求的限制,但攻击者用更低的频率攻击,且采用大量ip分布式攻击的方式,并掌握了管理员用户名(通过访问 https://anjir.top/?author=1获取),理论上,只要时间充足,密码一定会被攻破。又因为网站设置主要缓存静态资源,不缓存动态请求,每一次 POST 请求,Nginx 都会把它转发给后面的 WordPress (PHP-FPM) 处理。WordPress 需要连接数据库、验证信息,这个过程会消耗大量的 CPU 和内存,所以导致CPU资源占用过高告警。
要解决这个问题,其实有个很简单的解决方案:直接在 Nginx 层拒绝任何对xmlrpc.php这个文件的访问,然后只通过WordPress后台发布文章就行了。但我个人还是有用到xmlrpc接口的场景,所以不能完全封堵了。通过咨询AI,我找到了“Nginx + Fail2ban”方案,完美解决了现阶段的需求。本篇文章主要记录:使用“Nginx + Fail2ban”方案,解决黑客利用xmlrpc.php接口、wp-login.php页面暴力破解WordPress网站的问题。
简单5个步骤
- 启用并配置防火墙 UFW
- 修改 Nginx 配置,记录更详细的日志
- 配置 Fail2ban
- 设置白名单并重启 Fail2ban
- 检查封禁效果
第一步:启用并配置防火墙 UFW
-
安装: apt update && apt install ufw
-
设置默认规则: ufw default deny incoming ufw default allow outgoing
-
开放必要端口: ufw allow ssh ufw allow http ufw allow https
-
启用 (请确保已放行SSH): ufw enable
-
检查状态: ufw status
第二步:修改 Nginx 配置,记录更详细的日志
为了让 Fail2ban 能更好地工作,我们需要让 Nginx 在日志中记录 POST
请求的具体内容。这会稍微增加日志文件的大小,但对于安全来说是值得的。
- 修改 Nginx 主配置文件
nginx.conf
我们需要修改 Nginx 的主配置文件,而不是我们站点的
app.conf
。由于我们在 Docker 中运行,这个文件在容器内部。最简单的办法是把它复制出来,修改,然后通过卷映射挂载回去。从容器中复制
nginx.conf
到宿主机cd ~/my_website # 为主配置文件创建一个目录 mkdir -p ./nginx/main-conf # 从正在运行的 nginx 容器中复制文件 docker cp wordpress_nginx:/etc/nginx/nginx.conf ./nginx/main-conf/nginx.conf
编辑复制出来的
nginx.conf
nano ./nginx/main-conf/nginx.conf
找到
log_format main
,修改为(即末尾添加$request_body
变量,让它记录 POST 的内容):log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for" "$request_body"';
- 修改 docker-compose.yml 挂载配置
在 my_website 目录下创建日志文件夹:
mkdir -p ./nginx/logs
修改 docker-compose.yml,为 nginx 服务添加日志卷映射,和挂载我们修改后的
nginx.conf
nano docker-compose.yml
在 nginx 服务的
volumes
部分,新增两行:volumes: -./nginx/main-conf/nginx.conf:/etc/nginx/nginx.conf:ro# <--- 新增 -./nginx/logs:/var/log/nginx# <--- 新增 -./nginx/conf.d:/etc/nginx/conf.d
- 切换网络模式为 host
(删除 ports,添加 network_mode) 我们让 Nginx 容器直接使用宿主机的网络。这样,Nginx 就能直接看到来自外部的、未经 Docker NAT 转换的原始请求,从而获取到真实的客户端公网 IP。
继续对docker-compose.yml中的 nginx 服务进行如下修改: a. 删除 ports 部分:在 host 模式下,容器直接监听宿主机的端口,不再需要端口映射。 b. 添加 network_mode: "host"。 c. 修改 wordpress 的连接方式:由于 Nginx 现在和宿主机在同一个网络命名空间,它不能再通过服务名 wordpress 来连接了。它需要通过宿主机的 localhost (127.0.0.1) 和 WordPress 容器映射出来的端口来连接。所以我们也要修改 wordpress 服务。
请将 docker-compose.yml 文件中的 nginx 和 wordpress 服务部分,修改为以下内容:
wordpress: image:wordpress:php8.2-fpm container_name:wordpress_fpm restart:always # 新增 ports 映射,让 fpm 的 9000 端口暴露给宿主机 ports: -"127.0.0.1:9000:9000" depends_on: -db volumes: -./wordpress/html:/var/www/html environment: WORDPRESS_DB_HOST:db:3306 WORDPRESS_DB_USER:'wordpress_user' WORDPRESS_DB_PASSWORD:'YOUR_STRONG_USER_PASSWORD' WORDPRESS_DB_NAME:'wordpress' # wordpress 仍然在 app-network 中,以便连接 db networks: -app-network nginx: image:nginx:1.27.0-alpine container_name:wordpress_nginx restart:always # 关键修改:使用 host 网络模式 network_mode:"host" # 不再需要 ports 映射 # ports: # - "80:80" # - "443:443" volumes: -./nginx/main-conf/nginx.conf:/etc/nginx/nginx.conf:ro -./nginx/logs:/var/log/nginx -./nginx/conf.d:/etc/nginx/conf.d -./nginx/cache:/var/cache/nginx -./wordpress/html:/var/www/html:ro -./certbot/conf:/etc/letsencrypt -./certbot/www:/var/www/certbot # 在 host 模式下,nginx 不再属于 app-network # networks: # - app-network
- 修改 Nginx 站点配置文件 app.conf
由于 Nginx 现在需要通过 localhost 来连接 WordPress,我们需要修改 PHP-FPM 的转发地址。
nano ./nginx/conf.d/app.conf
找到 location ~ .php$ { ... } 块,将 fastcgi_pass wordpress:9000; 修改为 fastcgi_pass 127.0.0.1:9000;
location ~ .php$ { # ... (其他配置) ... # 连接到宿主机的 9000 端口,该端口已映射到 WordPress 容器 fastcgi_pass 127.0.0.1:9000; # ... (其他配置) ... }
- 重启
: docker compose down docker compose up -d
第三步:配置 Fail2ban
-
安装: apt install fail2ban -y
-
创建 jail 配置: nano /etc/fail2ban/jail.d/wordpress-zerotolerance.conf
将以下定制好的内容粘贴进去:
[wordpress-xmlrpc] enabled = true port = http,https filter = wordpress-xmlrpc logpath = /root/my_website/nginx/logs/access.log # 任何一次匹配都立即触发 maxretry = 1 # 封禁时间为30天 bantime = 30d # findtime 在 maxretry=1 时意义不大,但最好保留 findtime = 1m [wordpress-login] enabled = true port = http,https filter = wordpress-login logpath = /root/my_website/nginx/logs/access.log # 任何一次匹配都立即触发 maxretry = 1 # 封禁时间为1天 bantime = 1d findtime = 1m
-
创建对应的 filter
(过滤器) 文件为
xmlrpc.php
创建 filternano /etc/fail2ban/filter.d/wordpress-xmlrpc.conf
粘贴以下内容:
[Definition] failregex = ^<HOST> - .* "POST /+xmlrpc.php HTTP/.* ignoreregex =
以及为
wp-login.php
创建 filternano /etc/fail2ban/filter.d/wordpress-login.conf
粘贴以下内容:
[Definition] failregex = ^<HOST> - .* "POST /+wp-login.php.* ignoreregex =
第四步:设置白名单并重启 Fail2ban
这一步至关重要,防止你被自己锁在外面。
-
编辑 jail.local 添加你自己的 IP nano /etc/fail2ban/jail.local
找到或添加
ignoreip
这一行,并加上你自己的本机 IP段(百度“ip”,家用宽带动态ip前3段一般不变,最后一段全包含即可,比如百度查询到你此时的ip是:117.100.100.100,那么加白的ip段可以填:117.100.100.0/24)。[DEFAULT] ignoreip = 127.0.0.1/8 ::1117.100.100.0/24
-
重启 Fail2ban 使所有配置生效 systemctl restart fail2ban
-
检查 Fail2ban 状态 fail2ban-client status
你应该能看到
Jail list:
下面有wordpress-login
和wordpress-xmlrpc
这两个活动的 jail。
第五步:检查封禁效果
wp-login.php封禁验证:
a. 进行测试:
-
用你的手机网络(不要用常用 Wi-Fi)访问 https://anjir.top/wp-login.php。
-
只输入一次错误的密码。
-
然后尝试刷新页面或再次访问。你应该会发现网站已经无法访问了。
-
回到服务器,执行 fail2ban-client status wordpress-login,你会看到你手机的 IP 已经被列入了封禁列表。
b. 练习解封:
既然测试成功了,就练习一下解封自己的手机IP吧。
-
1 -
2
# 假设你手机IP是 222.222.222.222
fail2ban-client unban 222.222.222.222
xmlrpc.php封禁验证:
打开2个SSH 终端连接服务器窗口,一个用来操作登录,另一个用来看日志。
-
监控日志:
tail -f /root/my_website/nginx/logs/access.log
-
等待新的攻击日志出现。当下一条类似 5.101.157.127 ... "POST /xmlrpc.php ..." 的日志出现时,Fail2ban 就会立刻匹配到它。
-
检查 Fail2ban 状态:
稍等片刻后,再次执行:fail2ban-client status wordpress-xmlrpc
这一次,你应该会看到 Currently failed 和 Total failed 的计数增加了,并且 Banned IP list 中出现了新的被封禁的 IP。