下午给一批子域名加统一拦截规则。
配置写完,nginx -t 通过,重载,随手测了一个随机域名,返回 000,符合预期。收工。
一小时后,我顺手测了另一个路径,也返回 000。
这个 000 就不对了。
起因:一批泛解析的子域名
服务器上有一批子域名,十几个,每个都在跑各自的服务。
域名是泛解析的,也就是 *.budingwz.cn 全部都解析到同一台服务器。这意味着随便什么域名 —— abc.budingwz.cn、test123.budingwz.cn —— 都能访问到主站内容。
对搜索引擎来说,「主域名之外还挂着一堆内容相同的站点」是站群判定的典型特征。之前主站被判定为垃圾站,这批子站也脱不了干系。
所以得治。
思路很简单:加一个兜底 server 块,把所有没匹配到具体配置的域名直接掐断。
server {
listen 80 default_server;
listen 443 ssl http2 default_server;
server_name _;
ssl_certificate /path/fullchain.pem;
ssl_certificate_key /path/privkey.pem;
return 444;
}
几点说明:
server_name _;是通配写法,表示「任何域名都匹配我」listen ... default_server把这个块标记为默认服务器,只有它才能接住未匹配的域名default_server是关键。 没有这个标记,nginx 会按配置文件的字典序挑第一个 server 块当默认 —— 那就变成「随机挑一个子站来服务未知域名」,比不处理还糟
至于 return 444:这是 nginx 特有的状态码,不回任何响应,直接关闭连接。
为什么用 444,不用 403 或 404
这一步我犹豫过,说下取舍逻辑。
| 方案 | 我认为的问题 |
|---|---|
403 Forbidden |
给搜索引擎一个明确的「存在但拒绝访问」信号。长期大量 403 可能被判定为抓取异常 |
404 Not Found |
告诉搜索引擎「这个域名存在,只是没内容」—— 反而可能被当成待收录页面排队 |
444 直接断连 |
不发任何响应头,连接直接关闭。请求在 HTTP 层面就结束了,不留下页面级信号 |
需要说明的是:这三条不是从某个官方文档里抄来的定论,是基于「泛解析站点的未知域名本来就不该被搜索引擎看到」这个判断做的取舍。 如果你有明确依据认为 403 更合适,用 403 也没问题 —— 关键是别让它变成 200。
444 还有个附带好处:它几乎不消耗服务器资源。 nginx 官方文档对它的定义是:
The non-standard code 444 closes a connection without sending a response header.
翻译过来就是「关闭连接,不发送响应头」。普通的 403/404 需要走完整个 HTTP 响应流程,组织响应头、生成响应体;444 直接关 socket,省下这些开销。
对泛解析站点来说,每天可能有大量扫描器、爬虫在轮询各种不存在的子域名,这笔省下来的资源很可观。
写完 nginx -t,通过。nginx -s reload,生效。测了一个随机域名,000,符合预期。
看起来完美。
一小时后发现问题
顺手测了一下证书续签路径。
宝塔的 Let's Encrypt 证书是自动续签的。续签时,验证机构会往 /.well-known/acme-challenge/ 放一个验证文件,然后 Let's Encrypt 的服务器来取这个文件,确认域名归你所有。
这条路径必须永远可达,否则新域名申请不了证书,老域名续签会失败。
问题在于:兜底块接管了所有域名,而 /.well-known/ 这条路径也被 return 444 一起掐了。所以得加放行:
server {
listen 443 ssl http2 default_server;
server_name _;
ssl_certificate /path/fullchain.pem;
ssl_certificate_key /path/privkey.pem;
location ^~ /.well-known/acme-challenge/ {
root /www/wwwroot/$host;
default_type "text/plain";
}
return 444; # ← 问题在这一行
}
^~ 是 location 匹配修饰符里优先级最高的(前缀匹配且一旦命中就停止后续正则匹配),按理说这条放行规则应该稳稳生效。
测试:
curl -sk -o /dev/null -w '%{http_code}' \
-H 'Host: brandnew.budingwz.cn' \
https://127.0.0.1/.well-known/acme-challenge/test
返回 000。
放行规则完全没生效。
排查
第一反应是 location 优先级搞错了,于是把 ^~ 换成 =(精确匹配,优先级更高),重载,再测 —— 还是 000。
第二反应是 root 拼错了。检查 $host 变量,没问题。
第三反应才回到最朴素的方向:这个请求到底有没有走到 location 匹配?
于是我把 return 444 临时注释掉,只留 location:
curl -> 404 ← 路径通了!
把 return 444 加回去:
curl -> 000 ← 又断了
结论明确:return 444 的执行时机,早于 location 匹配。
不是优先级问题,不是变量问题 —— 是顺序问题。location 根本没机会出场。
nginx 的请求处理顺序
这是问题的根源,值得单独说清楚。

nginx 官方文档里对 rewrite 模块的执行顺序写得很明确:
The
break,if,return,rewrite, andsetdirectives are processed in the following order:
- the directives of this module specified on theserverlevel are executed sequentially;
- repeatedly:
- alocationis searched based on a request URI;
- the directives of this module specified inside the found location are executed sequentially;
翻译过来就是:
- 先顺序执行写在
server层的 rewrite 模块指令 - 然后才根据 URI 去搜索
location - 再执行命中的
location里的 rewrite 指令
而 return 正是 ngx_http_rewrite_module 的指令之一。
对应到 nginx 内部的请求处理阶段,大致是这样的:
| 阶段 | 做什么 | 谁在这阶段执行 |
|---|---|---|
| 1. post-read | 读请求头 | — |
| 2. server-rewrite | 执行 server 块的 rewrite 指令 | return、rewrite、set |
| 3. find-config | 匹配 location | location 匹配逻辑 |
| 4. rewrite | 执行 location 内的 rewrite 指令 | location 里的 rewrite |
| 5. content | 执行内容处理器 | proxy_pass、fastcgi_pass、root+try_files |
关键在阶段 2 和阶段 3 的先后。
return 在阶段 2 执行,而 location 匹配是阶段 3 的事。
所以只要 return 写在 server 顶层,它下面的所有 location 都形同虚设 —— 请求在第 2 阶段就已经被 return 结束了,永远走不到第 3 阶段。
这也解释了为什么 nginx -t 检测不出来:它只检查语法,不检查语义。 这段配置语法完全合法,文件能被正常解析,只是逻辑上是错的。
补充一句:这个"阶段"的说法来自 nginx 源码里
ngx_http_core_module定义的 phase 枚举,官方文档没有专门展开。上面引用的那段执行顺序说明,是文档里能直接查到、且与实测结果一致的部分。
修法
把 return 444 从 server 顶层挪进 location / 里。

完整配置:
server {
listen 80 default_server;
listen 443 ssl http2 default_server;
server_name _;
# 用主站证书即可 —— 444 不返回内容,证书域名不匹配也无所谓
ssl_certificate /www/server/panel/vhost/cert/www.budingwz.cn/fullchain.pem;
ssl_certificate_key /www/server/panel/vhost/cert/www.budingwz.cn/privkey.pem;
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:!MD5;
ssl_prefer_server_ciphers on;
# ---- 证书续签校验目录必须放行 ----
# 用 $host 动态拼目录:$host 就是域名本身,与宝塔的目录命名一致。
# 这样 http://新域名/.well-known/acme-challenge/xxx 能正确命中,
# 新域名后续申请证书不会被兜底块挡住。
location ^~ /.well-known/acme-challenge/ {
root /www/wwwroot/$host;
default_type "text/plain";
try_files $uri @acme_main;
}
# 退化路径:万一按域名找不到,再试主站目录
location @acme_main {
root /www/wwwroot/www_budingwz_cn;
default_type "text/plain";
try_files $uri =404;
}
# 其余一切请求:断开,不返回任何内容。
# 【关键】444 必须写在 location 里,不能写在 server 顶层。
location / {
return 444;
}
}
改动就一处:return 444 现在挂在 location / 里。
这样一来,请求会先经过阶段 3 的 location 匹配:
- 命中
^~ /.well-known/acme-challenge/→ 走放行逻辑,正常返回文件 - 没命中 → 落到
location /→ 执行return 444
两条规则互相独立,互不干扰。
再测:
curl -sk -o /dev/null -w '%{http_code}' \
-H 'Host: brandnew.budingwz.cn' \
https://127.0.0.1/.well-known/acme-challenge/test
返回 404。
404 在这里是好结果。 它说明请求走到了阶段 5 的内容处理器,只是目录下确实没这个文件。路径是通的,证书能签。
对比一下三种结果的语义:
| 返回 | 含义 | 结论 |
|---|---|---|
000 |
连接被断,location 没执行 | ✗ 证书路径被拦,续签会失败 |
404 |
走完了流程,文件不存在 | ✓ 路径可达,符合预期 |
403 |
被某条规则拒绝 | ⚠ 要具体看规则,但至少说明请求走到了 nginx |
注意 404 和 403 都算「路径可达」—— 关键是不能是 000。
主站和子站的 /.well-known/ 由于各自的 server 块配置不同,返回码可能不一样(本站实测主站返回 403,走兜底块的新域名返回 404),但只要是正常 HTTP 响应,就说明没被兜底块掐断。
怎么确认改对了
改完配置只测一条路径是不够的。我写了个脚本,把相关路径一次性测完:
#!/bin/bash
# 验证兜底块:随机域名该断、正式站点该通、证书路径该可达
SITE="www.budingwz.cn"
echo "--- ① 随机子域名应被掐断(期望 000)---"
for d in zzztest random123 abc hello-world; do
code=$(curl -sk -o /dev/null -w '%{http_code}' \
-H "Host: $d.budingwz.cn" https://127.0.0.1/ --max-time 10)
printf " %-22s → %s\n" "$d.budingwz.cn" "$code"
done
echo "--- ② 正式子站应正常(期望 200)---"
for d in jinqun1 pdf xiufu; do
code=$(curl -sk -o /dev/null -w '%{http_code}' \
-H "Host: $d.budingwz.cn" https://127.0.0.1/ --max-time 10)
printf " %-22s → %s\n" "$d.budingwz.cn" "$code"
done
echo "--- ③ 主站不能受影响(期望 200)---"
code=$(curl -sk -o /dev/null -w '%{http_code}' \
-H "Host: $SITE" https://127.0.0.1/ --max-time 10)
printf " %-22s → %s\n" "$SITE" "$code"
echo "--- ④ 证书验证路径必须可达(不能是 000)---"
for d in $SITE jinqun1.budingwz.cn brandnew.budingwz.cn; do
code=$(curl -sk -o /dev/null -w '%{http_code}' \
-H "Host: $d" https://127.0.0.1/.well-known/acme-challenge/ --max-time 10)
printf " %-22s → %s\n" "$d" "$code"
done
四个检查项缺一不可:
- 随机域名断开 —— 确认兜底块生效
- 正式子站正常 —— 确认没误伤
- 主站正常 —— 确认没影响收录
- 证书路径不是 000 —— 就是这一条,漏了的话问题两个月后才炸
12 项全部符合预期,才算改完。
为什么这个错误特别危险
因为它不会立刻暴露。
配置写完,nginx -t 通过,站点访问正常,随机域名确实被掐断了,验证「通过」。看起来一切正常。
问题在于 /.well-known/ 这条路径平时没人访问。它只在证书续签时才被用到 —— 而 Let's Encrypt 证书是 90 天有效期,宝塔一般提前 30 天续签。
也就是说,这个错误会在 60 天后以「证书突然过期」的面貌出现。
到时候你查续签日志,看到的是 403 或 timeout,然后开始怀疑 DNS、怀疑防火墙、怀疑 Let's Encrypt 抽风 —— 而配置里那条导致问题的 return,早就不记得是什么时候加的了。
这次是运气好,写完一小时就顺手测了这条路径。如果没测,两个月后就要把这套排查流程重走一遍。
顺带一个教训:别被响应头骗了
排查这个问题时,我还犯了另一个错。
在改 sitemap 之前,我看 /sitemap.xml 的响应头是这样的:
Content-Type: text/xml
Content-Length: 1041
Last-Modified: ...
ETag: "6aba5fec-411"
Accept-Ranges: bytes
ETag、Accept-Ranges、Content-Length —— 全是 nginx 静态文件的标准特征。加上文件确实躺在站点根目录,我直接断定:这是个手动维护的静态文件,难怪每发文章都得重新生成。
于是花时间写了个插件去「接管」它。
部署完发现响应头一点没变,大小还是 1041。
这才去找原因 —— 根目录躺着一个 sitemap.php,是我之前写的(忘了),nginx 伪静态里有这么一条:
location = /sitemap.xml {
try_files $uri /sitemap.php?$args;
}
它每次被请求时检查缓存,超过 1 小时就查库重新生成,然后写回静态文件。我看到的那个"静态文件",只是它的缓存产物。
所以我把文件 mv 走后,sitemap.php 立刻又生成了一个 —— 我部署的插件根本没被触发。

教训:响应头的静态特征不等于文件是手动维护的。缓存产物会带一模一样的头。
正确做法是改之前先查一遍伪静态规则:
grep -rn 'sitemap' /www/server/panel/vhost/rewrite/
一行命令,两分钟的事。比误判后推翻重做划算得多。
同一个 URL 可能被三层同时接管 —— nginx rewrite、独立脚本、应用路由。漏看任何一层,都会做出重复甚至冲突的改动。
小结
四件事,按重要性排:
1. return 写在 nginx server 层会绕过该块内所有 location。
因为它在 rewrite 阶段(阶段 2)执行,早于 location 匹配(阶段 3)。需要 location 生效时,return 必须放进 location /。
2. nginx -t 只查语法,不查语义。
语法正确的配置可以逻辑上是错的,而且能通过所有静态检查。唯一的验证方式是实测。
3. 测配置不能只测你改的那一条,要测所有受影响的路径。
这次如果只测了「随机域名被掐断没有」,结论是「正常」。只有把「证书路径可达不可达」也测了,才能发现问题。
改完配置,把所有相关路径列出来,写个脚本一次跑完。比重载后随手点两下靠谱得多。
4. 改一个 URL 之前,先查清它由谁负责。
响应头的特征只是线索,不是结论。grep 一遍配置文件,两分钟的事。