1. 问题背景与现象

在单台服务器上使用 Nginx 部署多个站点,并通过 Cloudflare(开启小黄云代理)进行 CDN 加速与安全防护。

  • 初始状态:服务器上仅运行站点 markdown.18401949.xyz,配置如下:

    1
    2
    3
    4
    5
    6
    
    server {
        listen 443 ssl;
        http2 on; # 使用了 Nginx 1.25.1+ 的新版写法
        server_name markdown.18401949.xyz;
        # ... SSL 证书及反代配置
    }
    

    此时站点访问完全正常。

  • 异常现象:当新增第二个站点 claw.oopses.de,并使用 Certbot 默认生成的旧版 HTTP/2 写法后:

    1
    2
    3
    4
    5
    
    server {
        listen 443 ssl http2; # 使用了旧版的混合写法
        server_name claw.oopses.de;
        # ... SSL 证书及反代配置
    }
    

    故障表现:保存并重载 Nginx 后,原本正常的 markdown 站点立刻无法访问,Cloudflare 直接抛出 525 SSL Handshake Failed 错误。抓包分析显示,Cloudflare 尝试访问 markdown 时,Nginx 错误地返回了 claw 的 SSL 证书,导致域名不匹配,握手被强制断开。


2. 原因详细分析

导致这一诡异现象的根本原因,在于 Nginx 在 1.25.1 版本前后对 HTTP/2 指令的设计变更,以及旧版指令在多虚拟主机(Virtual Host)环境下的“全局污染”特性。

2.1 Nginx 官方对 HTTP/2 指令的废弃与调整

在 Nginx 1.25.1(2023年6月发布)及后续版本中,官方重构了 HTTP/2 配置架构:

  • 旧版写法(已废弃)listen 443 ssl http2;
  • 新版写法(推荐)listen 443 ssl; http2 on;

2.2 旧版 listen ... http2 的全局污染机制

SSL 握手发生在应用层协议(HTTP/2)协商之前。旧版指令具有端口级别的全局侵入性,导致 Nginx 内部状态机发生冲突:

  1. SNI 协商干预:在标准 HTTPS 握手中,客户端通过 SNI 告知目标域名,Nginx 匹配 server_name 并提供正确证书。
  2. 配置上下文飘移:当 listen 443 ssl http2; 存在时,Nginx 在高并发下倾向于在完成 SNI 匹配前,提前应用第一个加载的 HTTP/2 SSL 上下文缓存。
  3. Cloudflare 回源触发冲突:Cloudflare 节点回源采用高并发连接池。Nginx 内部因新旧状态机冲突,错误地将先加载的 claw 证书返回。Cloudflare 检测到回源证书域名与请求域名不匹配,安全机制触发,直接中断握手并抛出 525 错误。

3. 解决办法

核心逻辑:彻底淘汰旧版写法,将全站 Nginx 配置指令集对齐。

3.1 修改 claw.oopses.de 配置

listen 后面的 http2 关键字移除,改为独立指令:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
server {
    listen 80;
    server_name claw.oopses.de;
    return 301 https://$host$request_uri;
}

server {
    # 1. 移除了 listen 末尾的 http2
    listen 443 ssl;
    
    # 2. 显式使用新版指令开启 HTTP/2
    http2 on; 

    server_name claw.oopses.de;

    ssl_certificate /etc/letsencrypt/live/claw.oopses.de/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/claw.oopses.de/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    location / {
        proxy_pass http://127.0.0.1:18789;
        # ... 其他反代配置
    }
}