Administrator
发布于 2026-10-01 / 0 阅读
0
0

nginx 反向代理的 7 个坑:从 ACME 校验失败到 reload 假象

这 7 个坑有个共同点:它们都不会在 nginx -t 时报错。语法正确、能正常启动、能正常服务——只是在某个特定请求路径上悄悄做错事。

坑 1:server 级 return 301 会抢在 location 前面执行

这是最隐蔽的一个。

nginx 的处理阶段顺序是:rewrite 阶段 → location 匹配 → content 阶段。而写在 server 块顶层的 return,属于 rewrite 阶段——也就是说,它会在 nginx 去匹配 location 之前就执行完毕。

后果:你写了这样一份"HTTP 跳 HTTPS"的配置,同时还留了个 ACME 校验的例外:

server {
    listen 80;
    server_name your-domain.com;

    return 301 https://$host$request_uri;     # 罪魁祸首

    location ^~ /.well-known/acme-challenge/ {
        root /var/www/html;                    # 永远走不到这里
    }
}

certbot 来取挑战文件时,请求被 301 重定向走了,于是报错:

Fetching http://your-domain.com/.well-known/acme-challenge/xxx:
Invalid response ... 301

正确写法:把 return 下沉进 location /:

server {
    listen 80;
    server_name your-domain.com;

    location ^~ /.well-known/acme-challenge/ {
        root /var/www/html;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

location ^~ 的优先级高于普通前缀匹配,所以 ACME 请求会命中第一个 location,其余请求才被 301。

坑 2:default_server 是个"什么都收"的口袋

如果你在 443 上设了 listen 443 ssl default_server; 配 server_name _;,那么任何没有被其他 server 块显式声明的域名,都会落到这个兜底站点。

这个设计本身有用(比如处理 IP 直连),但它的副作用是:你新加了一个域名、DNS 也指过来了,访问却显示另一个服务——因为新域名没有对应的 server_name,被兜底站点接走了。

排查方法:

nginx -T | grep -n 'server_name'

把所有 443 站点列出来,确认目标域名确实出现在某个 server_name 里。

坑 3:map 在整个配置里只能定义一次

HTTP→WebSocket 升级要用到 map $http_upgrade $connection_upgrade { ... }。如果你有两个站点配置各自内联了一份这个 map,nginx 会直接拒绝启动:

nginx: [emerg] duplicate map name "connection_upgrade"

解法:抽到 conf.d/ 下单独一个文件:

/etc/nginx/conf.d/00-websocket-map.conf

conf.d/*.conf 会被 nginx.conf 在 http 块里 include,位置在 sites-enabled 之前,所以各站点都能用到,且只定义一次。

坑 4:nginx -t 的退出码会被管道吞掉

nginx -t | sed 's/^/  /' && systemctl reload nginx

这里的 && 判断的是 sed 的退出码,而 sed 几乎总是成功。于是配置有语法错误时,reload 照样执行。

解法:

nginx -t > /tmp/t.log 2>&1
[ $? -eq 0 ] || { cat /tmp/t.log; exit 1; }

坑 5:reload 是异步的,"立刻验证"会骗你

systemctl reload nginx 发出信号后立即返回,但旧 worker 进程要处理完手头的连接才退出。在这个窗口期内,新连接可能被旧 worker 接走。

表现就是:nginx -T 明明显示新配置已加载,curl 却返回旧行为。

解法:sleep 2 后再验证。这不是"玄学等待",是给 worker 优雅退出的时间。

坑 6:证书自动续期,不会自动重载 nginx

Let's Encrypt 证书 90 天到期,certbot 的 timer 会帮你续。但是——续完之后 nginx 还在用内存里的旧证书。

三个月的某一天,你会突然收到"证书已过期"的告警,而磁盘上的证书其实是新的。

解法:在 /etc/letsencrypt/renewal/<cert-name>.conf 里加一行:

renew_hook = systemctl reload nginx

然后用 certbot renew --dry-run 演练一遍,确认钩子真的会被调用。

坑 7:TLS 参数在每个站点复制粘贴

ssl_protocols、ssl_ciphers、ssl_session_cache……这些参数每个 HTTPS 站点都要写一遍。一旦要调整(比如禁用某个旧协议),你得改 N 个文件,还容易漏。

解法:抽成片段,各站点 include:

# /etc/nginx/snippets/tls-common.conf
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;

站点里只用一行:

include snippets/tls-common.conf;

一处修改,处处生效。

小结

这 7 个坑之所以难查,是因为它们的"错误表现"离"错误原因"很远:证书签发失败的原因在一个 return 语句;页面显示错服务的原因在一个 server_name。

所以 nginx 的改动必须用真实请求验证——看到 reload 成功就收工,等于没验证。


评论