Nginx limit_req 限制 WordPress 登录入口频率:阈值设定、白名单与误封回滚清单

枫少@KillBoy
枫少@KillBoy
枫少@KillBoy
管理员
399
文章
0
粉丝
安全运维1 17字数 2933阅读9分46秒阅读模式
AI智能摘要
给 WordPress 登录入口套上 Nginx limit_req,往往先误伤的是自己人而非攻击者。盲目收紧阈值会让后台卡顿、心跳中断,甚至把办公出口当成异常流量。本文拆解了阈值设定背后的漏桶逻辑,并给出了基于日志还原真实来源、配置白名单豁免及小步回滚的完整路径。在动手修改配置前,如何确保限流只压制高频尝试而不冻结业务?
— AI 生成的文章内容摘要

对 WordPress 站点来说,/wp-login.php 往往比首页更值得单独看管:它不能被缓存、每次请求都要拉起 PHP-FPM、失败尝试还会写入数据库。但登录入口同时也是办公出口、运维跳板和真实编辑者共用的路径,一旦限速过头,先受影响的往往不是攻击者,而是自己人。因此在动手写 limit_req 之前,先明确这次变更会影响到哪些业务路径:后台登录与退出、密码重置、wp-admin 内的 AJAX 心跳、媒体上传、插件更新,以及由 wp-cron.php 触发的计划任务。本文只讨论 Nginx 层的请求速率限制,不涉及 WAF 规则编写和插件验证码,也不承诺单靠限速就能阻断撞库;它的作用是压住单一来源的高频尝试,为其他防护手段争取时间。

运维人员在终端前查看 Nginx 访问日志并记录限速检查项

一、先界定影响范围:哪些请求会被同一条规则影响

limit_req 是按 key(通常是客户端 IP)计数,作用范围取决于你把它放在哪个 location。放错位置比参数写错更难排查。

请求路径实际用途套用严格限速的后果建议
POST /wp-login.php登录提交、密码重置提交高频失败尝试被压制,符合预期主目标,单独一条严格规则
GET /wp-login.php渲染登录表单反复刷新登录页的编辑者会被拦与 POST 共用同一 zone 一般可接受
/wp-admin/admin-ajax.php后台心跳、部分前台功能后台操作卡顿、编辑器提示连接中断不套用登录 zone
/wp-cron.php计划任务、备份与更新钩子备份中断、定时发布延迟单独温和限制或保持现状
/xmlrpc.php部分客户端与历史插件若有正常使用会被误伤先确认是否在用,再决定单独处理
静态资源图片、CSS、JS页面加载异常不要加限速

一个容易被忽略的前提:如果站点前面有 CDN 或反向代理,$binary_remote_addr 拿到的是边缘节点地址,此时限速会把所有访客当成同一个来源,等于全站限速。确认方法是在 http 块配置 set_real_ip_from 与 real_ip_header 后,看日志里记录的客户端 IP 是否真实;配置前先小范围验证一条请求的 $remote_addr 取值。

二、盘点:用现有日志还原真实登录来源

不要凭印象定阈值。执行位置是 Web 服务器的 shell,读取的是 Nginx access log,预期现象是能看出“哪些 IP 高频、集中在什么时段、状态码分布如何”。

先看登录路径的来源 IP 排名:

sudo awk '$7 ~ /^/wp-login.php/ {print $1}' /var/log/nginx/access.log 
  | sort | uniq -c | sort -rn | head -30

再看登录提交的结果分布。WordPress 登录成功后通常返回 302 跳转,失败则返回 200 并带回错误页,所以下面的状态码比例可粗略当作成功率:

sudo awk '$6 ~ /POST/ && $7 ~ /^/wp-login.php/ {print $9}' /var/log/nginx/access.log 
  | sort | uniq -c | sort -rn

需要注意的组合指标:

  • 单个 IP 在 1 分钟内对登录路径的请求数分布,决定 rate 的量级;
  • 有多少 IP 落在同一网段(办公出口、公司 NAT、云厂商机房),决定白名单粒度;
  • 真实用户从打开登录页到提交表单的间隔,决定 burst 至少要留几个位置;
  • 日志覆盖时长是否包含早高峰和内容发布时段,否则阈值会偏松。

如果日志里已经出现 429 或 503,先确认来源:503 可能来自 PHP-FPM 排队而非限速,两者混在一起会让后续判断失真。

三、判断:阈值和突发容量怎么取

limit_req 用的是漏桶模型:rate 是稳态放行速度,burst 是允许临时超出的队列长度,nodelay 决定超出部分立即处理还是延迟排队。三者要一起定,单独调 rate 意义不大。

参数含义起手取值思路依据
rate每 IP 稳态放行速率先按“正常用户最快操作速度”取,例如 10–20 次/分钟盘点中真实用户单页面的登录相关请求数
burst瞬时允许的超出量3–5,覆盖“刷新一次 + 提交一次 + 重试一次”浏览器重试与用户手滑的实际次数
nodelay超出 burst 立即拒绝而非排队登录路径建议加上排队会让用户看到长时间白屏,比直接报错更难解释
limit_req_status拒绝时返回的状态码显式设为 429默认 503 会与后端过载混淆
limit_req_log_level写入 error log 的级别warn默认 error,量大时噪音明显
zone 容量存储 key 状态的内存10m 通常足够中小站点每 MB 约存一万六千个状态

登录页的请求体量远小于接口,不需要照搬“每秒几十次”的通用值;对单人使用场景,每分钟个位数到十几次之间往往就够用。真正需要放宽的是多人共用一个出口 IP 的办公网络——这类地址更应该进白名单,而不是靠调大 rate 来迁就。

四、整改:白名单与小步上线

白名单的正确做法不是写一堆 allow 再 deny,而是让被豁免的请求“不进入计数”。把可信来源的 key 变成空字符串,limit_req_zone 就不会为这些请求建立状态。

在 http 块中定义(执行位置:/etc/nginx/nginx.conf 或 conf.d/ 下的独立文件):

geo $login_trusted {
    default 0;
    127.0.0.1 1;
    10.20.30.0/24 1;      # 办公出口
    198.51.100.7 1;       # 运维跳板
}

map $login_trusted $login_key {
    0     $binary_remote_addr;
    1     "";
}

limit_req_zone $login_key zone=wp_login:10m rate=12r/m;
limit_req_status 429;
limit_req_log_level warn;

在 server 块中作用于登录路径。如果站点里已经有 location = /wp-login.php,只加一行 limit_req;如果没有,新建的精确匹配块必须把你现有的 fastcgi_params、fastcgi_pass、SCRIPT_FILENAME 一并写全,否则会直接 404 或 502:

location = /wp-login.php {
    limit_req zone=wp_login burst=5 nodelay;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}

上线前固定三步:

  1. 备份待改文件,文件名带时间戳,例如 cp wp-login-limit.conf wp-login-limit.conf.20250101-0900.bak;
  2. sudo nginx -t,预期输出 syntax is ok 与 test is successful;
  3. sudo systemctl reload nginx,用 reload 而不是 restart,避免连接被硬切断。

如果使用的是较新版本的 NGINX,可以先用 limit_req_dry_run on; 跑一轮,只记录命中而不真正拒绝;老版本没有该指令,可先只对单个测试 server 生效。具体是否支持,用 nginx -V 看版本后对照官方 ngx_http_limit_req_module 文档确认,不要凭记忆。

几个容易踩的细节:

  • 白名单网段写错一位,等于把办公网推进限速;上线后务必从白名单 IP 和普通 IP 各测一次;
  • IPv6 下同一用户可以在一个 /64 内更换地址,若日志里 IPv6 占比高,需要考虑按前缀聚合 key,否则计数会被稀释;
  • xmlrpc.php 若仍在用,不要和登录路径共用同一 zone;
  • 修改 rate 或 burst 后需要 reload 才生效,观察窗口应重新起算,不要拿变更前后的数据直接对比。

五、验证:把正常业务路径跑一遍

在非高峰时段上线,逐项确认。下表每一行都应能被“看到现象”,而不是“觉得没问题”。

检查项操作预期现象
登录表单浏览器打开 /wp-login.php200,页面完整渲染
正常登录输入正确账号密码302 跳转后台,无 429
登录失败故意输错一次密码返回错误提示页,不是 429
密码重置走 ?action=lostpassword 发一封邮件正常发出,页面无报错
后台操作编辑文章、上传图片、切换页面无卡顿、无“连接已丢失”提示
计划任务触发一次 wp-cron.php备份与定时任务正常执行
限速生效从普通 IP 连续请求登录页 10 次前若干次正常,之后返回 429

连续请求可以用一条命令模拟,执行在任意能访问站点的机器上:

for i in $(seq 1 10); do
  curl -o /dev/null -s -w "%{http_code}n" https://example.com/wp-login.php
done

预期是前面几次返回 200,随后出现 429。若一次都不出现,说明 burst 太大或规则没生效;若第一次就出现,说明命中了你没预料到的计数来源,先回去看 error log 里的 limiting requests 记录。

对照日志状态码统计与浏览器登录结果进行限速验证

六、观察窗口:用指标判断要不要回退

建议观察窗口覆盖一个完整业务周期(24–72 小时),至少包含一次早高峰和一次内容发布时间。观察期内只盯四个指标:

指标采集方式处置参考
登录路径 429 占比按状态码统计登录请求持续高于 1%–2% 且伴随成功数下降,先放宽再看
登录成功率302 与 200 的比例变化相对基线明显下滑,视为误伤信号
白名单 IP 的 429单独过滤可信网段出现即为配置错误,立即修正
后端 5xxNginx 与 PHP-FPM 日志与限速无关,按原流程排查

error log 里的命中记录能直接回答“是谁被拦了”:

sudo grep "limiting requests" /var/log/nginx/error.log | tail -50

如果被拦的 IP 集中在少数几个网段,且这些网段有真实用户反馈,优先补白名单或放宽该 zone,而不是整体取消规则。如果被拦的 IP 极其分散、每个只出现一两次,说明阈值偏紧,把 rate 上调一档。

七、回滚清单:三档力度,从轻到重

出问题时不要凭紧张操作,按顺序执行,每一档都先 nginx -t 再 reload。

  1. 放宽:把 rate 上调一档(如 12r/m → 30r/m),burst 加 2,观察 30 分钟;
  2. 摘掉规则:注释掉 location 里的 limit_req 一行,保留 limit_req_zone 定义(不产生流量影响),reload 后限速立即消失;
  3. 整体禁用:把配置文件名改为 .disabled 后缀或整段注释,reload,回到变更前状态;
  4. 还原文件:用步骤四的备份覆盖,sudo nginx -t && sudo systemctl reload nginx;
  5. 回滚后确认:重复第五节的连续请求测试,预期全部返回 200;再核对一次 error log 中不再新增 limiting requests。

回滚过程中如果 nginx -t 报错,说明改动引入了语法问题,此时不要 reload,直接还原备份文件再测试。切记 systemctl reload nginx 失败时旧配置仍在运行,站点不会立刻中断,这给了你排查的时间。

八、复盘:把这次变更记录下来

限速规则不是一次配完就结束的东西,站点流量结构、办公出口 IP、CDN 接入方式都会变。建议在变更记录里留下四项内容:生效时间与观察窗口、最终的 rate/burst/zone 名、白名单网段及其负责人、以及回滚用的备份文件路径。下次调整时,这些就是判断依据,而不是重新从日志里猜一遍。

最后需要明确边界:按 IP 计数的限速对分布式来源效果有限,它压住的是单点高频,不是有组织的撞库。它的价值在于把明显的机器流量挡在 PHP 之外,同时把真实攻击留在日志里可追溯。登录安全仍需与强口令、双因素、失败锁定等措施配合,限速只是其中一层,且必须是可验证、可回滚的一层。

 
枫少@KillBoy
  • it2021
  • it2021.com
  • nginx
  • shell
  • test
  • WordPress
  • 登录安全
    • 堕落星神
      堕落星神 2

      办公出口IP真得先白名单,不然编辑先炸

    匿名

    发表评论

    匿名网友

    拖动滑块以完成验证