2412 字
约 8 分钟
16
S15-X11 · 反向代理与 TLS:证书、SNI 与转发头

nginx 前面的 TLS 和后面的转发头,是两个最容易被"配了就行"糊弄过去的地方。现象往往不是报错,而是:浏览器提示证书不属于这个域名、应用拿到的客户端 IP 全是 127.0.0.1、跳转链接变成 http://、上传大文件直接 413。这篇按握手、证书、转发、超时的顺序拆。


1. TLS 握手时,证书是怎么被选中的

客户端在 ClientHello 里带 SNI(要访问的主机名),nginx 拿它和所有 server_name 比对,选中最具体的那个 server 块,用该块的证书应答。比对不到任何 server_name 时,用的是默认 server——也就是该 listen 端口上第一个出现的 server 块,或者显式标了 default_server 的那个。

于是"没配 server_name"的结果是:访问 A 域名却收到 B 域名的证书。

# 兜底块:吞掉所有未知 SNI,避免串证书
server {
    listen 443 ssl default_server;
    server_name _;
    ssl_certificate     /etc/nginx/certs/default.crt;
    ssl_certificate_key /etc/nginx/certs/default.key;
    return 444;
}
客户端 SNI 匹配到的块 返回的证书
与某个 server_name 精确相同 该块 该块证书,正确
命中通配 *.example.com 通配块 该块证书
无匹配 default_server 或首个 server 默认块的证书,常见串证书
客户端不发 SNI(老客户端、IP 直连) 同上 同上

return 444 是 nginx 的"直接断连",比返回 404 更干净,也避免默认块被扫到内容。


2. 证书链:只配叶证书会缺中间证书

一张服务器证书背后是一条链:叶证书 → 中间证书 → 根证书。浏览器信任根,但中间证书必须由服务器一起下发。只配叶证书时,桌面浏览器可能靠缓存或 AIA 抓取蒙对,手机 App、curl、Java 客户端往往直接失败。

# 把叶证书与中间证书按顺序拼接成 fullchain
$ cat server.crt intermediate.crt > fullchain.crt
$ nginx -T 2>/dev/null | grep -E 'ssl_certificate'
$ openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | grep -E 'Verify return code|depth'

Verify return code: 0 (ok) 才是完整;出现 unable to get local issuer certificate 就是缺中间证书。签发方通常直接给 fullchain.pem,配置里用它,不要用 cert.pem。

配置 下发内容 典型表现
cert.pem(叶) 仅叶证书 桌面浏览器偶发正常,移动端/命令行链验证失败
fullchain.pem 叶 + 中间 正常

3. listen 443 ssl 与 http2 的位置

ssl 写在 listen 上,表示这个端口按 TLS 处理;ssl_certificate / ssl_certificate_key 是 server 级指令,写在 http 或 server 里都合法,写在 server 里更清晰。

http2 的位置在 1.25 有明确变化:1.25.1 起 listen 443 ssl http2 这种写法被标记为废弃,推荐独立的 http2 on; 指令。按 nginx 1.25 核对版本行为,两个写法在过渡期都还能跑,但会打 warning。

server {
    listen 443 ssl;          # 1.25:不要在这里写 http2
    http2 on;                # 独立指令
    server_name example.com;
    ssl_certificate     /etc/nginx/certs/fullchain.pem;
    ssl_certificate_key /etc/nginx/certs/privkey.pem;
}

http2 on 只在 server 级生效,写 http 级不生效;ssl_protocols 则可以放 http 级做全局默认。


4. 现代 TLS 配置的取舍

项 建议 取舍点
ssl_protocols TLSv1.2 TLSv1.3 留 1.1 是为极老客户端,代价是合规与套件范围变宽
ssl_ciphers 1.3 套件由 OpenSSL 定,1.2 侧留 ECDHE+AESGCM 过窄会挡掉旧 Android;过宽会引入 CBC
ssl_prefer_server_ciphers on 仅在 1.2 有效,1.3 由客户端定序
ssl_session_cache shared:SSL:10m 复用会话省一次完整握手,10m 约对应数万会话
ssl_session_timeout 1h 太长会让吊销信息滞后生效
ssl_stapling on + ssl_stapling_verify on 需 ssl_trusted_certificate 指向链,否则不生效
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1h;
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/certs/chain.pem;

stapling 配了不生效时先看错误日志:多数是 ssl_trusted_certificate 缺失或链不完整,拿不到响应就静默跳过。


5. 转发头:Host、X-Real-IP、X-Forwarded-For

应用在反代后面,REMOTE_ADDR 永远是 nginx 的地址。要让应用知道真实来源,得靠头。

头 语义 常见误用
Host 原始请求的主机名 不传则应用按 localhost:8080 拼跳转链接
X-Real-IP 单个客户端 IP,nginx 直接赋值 以为它是链路,其实是覆盖写
X-Forwarded-For IP 列表,追加语义 把入站值当可信导致伪造
X-Forwarded-Proto 原始协议 不传则应用生成 http:// 绝对地址
location /api/ {
    proxy_pass http://127.0.0.1:8080;
    proxy_set_header Host              $host;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

$proxy_add_x_forwarded_for 等于"入站的 X-Forwarded-For + , + $remote_addr"。所以这个头可以被客户端自带:直接请求时带 X-Forwarded-For: 1.2.3.4,nginx 会把 1.2.3.4 原样留在最左边。若应用取最左值做鉴权或限流,等于让任何人伪造来源 IP。

正确做法是明确信任边界:只信任你自己那层代理加进来的值,取从右往左第一个非可信地址。前面还有 Cloudflare 之类的 CDN 时,链会更长,需要按实际跳数取值,而不是一律取最左。

# 有 CDN 时:先信任 CDN 段,再用 real_ip 模块还原
set_real_ip_from 173.245.48.0/20;
real_ip_header CF-Connecting-IP;
real_ip_recursive on;

Spring 侧要显式打开:

server:
  forward-headers-strategy: framework   # 或 native

native 让容器(Tomcat)自己处理 X-Forwarded-*;framework 由 Spring 的 ForwardedHeaderFilter 处理。不配这一项,request.getScheme() 仍是 http,RedirectView 生成的绝对地址会掉协议。


6. proxy_pass 带不带 URI,路径改写完全不同

写法 请求 /api/users/1 转发到 规则
proxy_pass http://127.0.0.1:8080; /api/users/1 不带 URI,原样透传
proxy_pass http://127.0.0.1:8080/; /users/1 带 /,替换掉 location 匹配的前缀
proxy_pass http://127.0.0.1:8080/v2/; /v2/users/1 带 URI,前缀被替换

关键在 location /api/ 里的斜杠:location /api/ 配 proxy_pass .../ 会剥掉 /api,配 proxy_pass 不带斜杠会保留。这一对斜杠是"接口 404"最常见的来源,改之前先用 curl -v 看实际请求行。


7. 上传体积与超时三件套

默认 client_max_body_size 1m,超过直接 413,请求体根本不进应用。

指令 管的阶段 典型值
client_max_body_size 请求体大小 上传接口按业务定,如 50m
proxy_connect_timeout 与上游建连 5s,内网可更短
proxy_read_timeout 两次读之间的间隔 60s;长任务需放大
proxy_send_timeout 两次写之间的间隔 60s
proxy_buffering 是否缓冲上游响应 SSE/流式必须 off

注意 proxy_read_timeout 是两次读操作之间的间隔,不是整个请求的总时长。一个持续有数据输出的流式接口,60s 足够;一个 5 分钟才出第一个字节的同步任务,60s 一定断。

location /api/stream/ {
    proxy_pass http://127.0.0.1:8080;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_buffering off;
    proxy_cache off;
    proxy_read_timeout 3600s;
    client_max_body_size 50m;
}

proxy_http_version 1.1 与清空 Connection 是 SSE 的必要条件:默认的 HTTP/1.0 上游连接会在响应结束后关闭,事件流随之断掉。


8. 排错

现象 原因 处理
浏览器提示证书属于别的域名 该 SNI 没匹配到 server_name,走了默认块 补 server_name,加 default_server 兜底
curl 报链验证失败、浏览器正常 只配了叶证书 改用 fullchain.pem
应用里客户端 IP 全是 127.0.0.1 没传 X-Real-IP / X-Forwarded-For 补 proxy_set_header,应用侧读对应头
登录后跳转变成 http:// 缺 X-Forwarded-Proto 或 Spring 未开 forward-headers 传 $scheme,配 forward-headers-strategy
接口 404,上游收到少一段路径 proxy_pass 末尾斜杠与 location 不匹配 按第 6 节表核对,curl -v 看请求行
上传大文件 413 client_max_body_size 默认 1m 在对应 location 或 server 放大
SSE 几分钟后断开 缓冲未关、上游用 HTTP/1.0、读超时太短 proxy_buffering off + 1.1 + 放大读超时
改完配置没生效 只 reload 没 -t,或改的文件不是 -T 里显示的那个 先 nginx -t,再 nginx -T 核对实际加载内容

最后一条值得单说:nginx -t 通过了再 reload 是硬规矩;而"改了没生效"多数不是 reload 的问题,是改的文件没被 include——nginx -T 会把最终合并后的配置全量打印,比对着看比猜快得多。


9. 边界

场景 更合适的做法 理由
证书自动化续期 ACME 客户端 + reload 钩子 手工续期必然忘记
证书过期监控 外部探针按天 openssl s_client nginx 自己不会告警
多域名大量站点 一张多 SAN 证书或统一通配 减少 SNI 命中错块的概率
需要 L7 路由与重试策略 Ingress Controller / 服务网格 nginx 原生重试语义有限

10. 自测

  1. 客户端不带 SNI 时,nginx 用哪个 server 块的证书应答?
  2. 为什么"桌面浏览器正常、手机 App 报证书错误"通常指向证书链不完整?
  3. nginx 1.25 里 http2 应该写在哪一级?写在 http 级会怎样?
  4. $proxy_add_x_forwarded_for 的追加语义为什么会让来源 IP 可被伪造?
  5. location /api/ 配 proxy_pass http://up/; 与不带斜杠,转发后的路径差在哪?
  6. SSE 接口除了关 proxy_buffering,还需要补哪两项?
S15-X11 · 反向代理与 TLS:证书、SNI 与转发头
https://www.clxhxhhr.top/posts/5868/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。