Blog
OpenResty 限流:给站点加一层薄 WAF
完整商用 WAF 对个人站过重,但限流加几条死路径规则非常实在。五月在 1Panel 的 OpenResty 上调了一周参数,目标不是「拦下所有扫描」,而是:脚本变慢,真人几乎无感,日志可解释。 下面是按时间线写下的调参手记,方便以后自己回看「为什么当时把 burst 调到这个数」。参数没有放之四海而皆准的正确答案,只有在你自己的流量形状下反复核对过的区间。
起点配置:过严的第一版
第一版抄了偏保守的数字,只设一个 zone:
limit_req_zone $binary_remote_addr zone=perip:10m rate=5r/s;
server {
location / {
limit_req zone=perip burst=10 nodelay;
}
location = /api/login {
limit_req zone=perip burst=3 nodelay;
}
}
上线当天 429 暴涨。复盘 access log:某公司 NAT 出口后多名读者共享一个 IP;RSS 订阅抓取也在同一 IP 上短时间打满。每秒五请求对单人浏览够用,对 NAT 与爬虫不够。更糟的是,静态资源与 HTML 共用一个桶,打开一篇图多的文章就会误伤。此后我改成「先放宽全站、再单独收紧登录」的顺序,而不是一上来全局拧死。
调参过程:按状态码说话
改规则后固定观察二十四小时,只看状态码分布与少数真实投诉,不凭感觉。每次只改一个变量,否则你不知道是 burst 救了场还是拆 zone 救了场:
| 版本 | rate / burst | 429 占比(约) | 体感 |
|---|---|---|---|
| 第一版 | 5r/s,burst 10 | 偏高 | 公司网读者投诉 |
| 第二版 | 10r/s,burst 20 | 可接受 | 静态页正常 |
| 第三版 | 全站 10r/s;登录 2r/s burst 5 | 集中在登录 | 符合预期 |
| 第四版 | 静态资源单独 zone 更松 | 再降 | 图片多的页不误伤 |
关键思路:全站一个 zone 不够。登录、API、静态、管理后台拆开,各自用不同的 rate 与 burst。登录宁可严,静态宁可松。
limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:5m rate=2r/s;
limit_req_zone $binary_remote_addr zone=static:10m rate=30r/s;
location /assets/ {
limit_req zone=static burst=50 nodelay;
}
location = /api/login {
limit_req zone=login burst=5 nodelay;
}
location / {
limit_req zone=perip burst=20 nodelay;
}
IPv6 与 IPv4 都要落进你的 key 设计;只测了 v4 就上线,会被「看起来没限流」的 v6 扫描打脸。nodelay 是否使用也要结合业务:对登录我倾向更狠地挤掉突发;对静态页可以更宽容。zone 内存大小(如 10m)按并发 IP 量级估算即可,个人站很少先撞这个天花板。
死路径:比限流更便宜
个人站被扫最多的是别人的 CMS 路径。对这些直接切断,比让它们占用 limit 计数更干净:
location ~* ^/(\.env|\.git|wp-admin|wp-login\.php|phpmyadmin) {
return 444;
}
444 是 Nginx 与 OpenResty 直接关连接,少废话。配合「管理路径仅 VPN 或白名单 IP」,薄 WAF 的大部分收益就在这里。异常 UA 可以先记日志再决定是否拦——盲目黑名单误伤真实浏览器的情况我遇到过一次,之后改为只拦明确的利用套件特征,而不是凡是名字奇怪就杀。
扫描路径的列表不必追求完备:从自己 access log 的 Top 路径长出来,每月看一眼是否有新面孔,比抄一份巨型规则却从不敢改要健康。规则变更同样要观察:有没有把正常深度链接误伤成 444。
和真实用户共存的细节
办公 NAT 后很多人共享一个 IP,限流 key 只靠地址会误伤,所以 burst 要留余量。对已登录会话用更细的 key 需要 Lua 或应用层配合,个人站往往不划算。友链爬虫与 Feed 可放行已知 UA,或给它们单独更高配额。Cloudflare 在前面时,源站看到的是回源地址,限流 key 与真实攻击面要对齐:边缘已拦一批,源站再兜一层,两边规则语义别互相打架。
每次改完盯 limit_req 相关日志和 429、444 分布至少一天。盲抄别人的每秒十请求没有意义——你的读者构成、是否经 CDN、是否有 API,都不同。我后来把「改配置 → 观察窗口 → 写一句结论」记进变更日志,三个月后回看比记忆靠谱。
纵深位置
这层解决的是脚本噪声与低级利用路径。它不替代应用鉴权、依赖更新、最小暴露面、管理入口进隧道。个人站的对手大多是自动化脚本:你不需要无限军备,只需要参数是用自己的日志调出来的,并且改完敢看第二天的状态码。薄而可解释,比厚而神秘更适合一个人维护的站点。
调参手记能复用的部分,其实只有方法:按状态码说话、拆 zone、先松后紧、改完观察。具体数字抄走意义不大;把你自己的二十四小时分布记下来,下一次改规则才有参照系。
若站点前面还有 CDN:先分清边缘已经挡掉了什么,再决定源站 zone 的松紧,避免两边重复计数或互相误伤。个人站把限流当成「薄而可解释的一层」就够了,不必把它写成第二套防火墙规则集。