🔒 第03章 · Nginx TLS 配置

ssl_certificate 配置块解析

03 | Nginx TLS 配置

证书签好了,接下来就是让 Nginx 把这套 TLS 真正跑起来。这一章逐行拆解本机真实配置文件:certbot 往 server 块里写了什么、options-ssl-nginx.conf 里的安全参数是干嘛的、80 -> 443 跳转块怎么来的、以及反向代理场景里 TLS 究竟在哪一层终止。

Nginx 在 TLS 架构里的位置

Nginx 通常扮演 TLS 终止(TLS Termination) 的角色:对外终结加密连接、解密出明文 HTTP,再转发给后端应用。只要明确"加密到 Nginx 为止",后面的 proxy_pass 走的是内网明文,配置思路就清晰了。

客户端 --443/TLS--> Nginx(终止 TLS, 解密) --http--> 后端应用(127.0.0.1:3000)

对比另一种模式 TLS 透传(Pass-through):Nginx 只做转发不解密,把加密流原样交给后端(如 stream 模块). 普通 Web 应用几乎都用"终止"模式,因为 Nginx 要读请求头做路由、加安全头、做日志,必须先解密。本机所有站点都是终止模式。

一个完整 server 块的 TLS 部分(本机真实配置)

server {
    server_name bangdream.xyz www.bangdream.xyz;

    # 业务逻辑:代理到后端(被 /location/ 匹配)
    location / {
        proxy_pass http://127.0.0.1:3000;
        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;   # 告诉后端"原始是 https"
    }

    # —— 下面是 certbot 自动写入的 TLS 块(managed by Certbot)——
    listen 443 ssl;                                  # 监听 443 并启用 SSL
    ssl_certificate     /etc/letsencrypt/live/bangdream.xyz/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/bangdream.xyz/privkey.pem;
    include             /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam         /etc/letsencrypt/ssl-dhparams.pem;
}

逐行拆解 certbot 写的四条指令

指令 作用 指向(本机)
listen 443 ssl; 让 Nginx 在 443 端口监听并启用 SSL listen 443 ssl;
ssl_certificate 指定证书链(含叶证书+中间证书) /etc/letsencrypt/live/bangdream.xyz/fullchain.pem
ssl_certificate_key 指定私钥 /etc/letsencrypt/live/bangdream.xyz/privkey.pem
include options-ssl-nginx.conf 引入 certbot 维护的密码套件/协议安全参数 /etc/letsencrypt/options-ssl-nginx.conf
ssl_dhparam 指定 DH 参数,用于(旧式)DHE 密钥交换 /etc/letsencrypt/ssl-dhparams.pem

关键理解:ssl_certificate 必须指向 fullchain.pem(整条链),而不是 cert.pem(只有叶证书)。回想第一章的证书链——客户端需要"叶子+中间"才能验证到根。只发 cert.pem 会 unable to get local issuer certificate。

options-ssl-nginx.conf 拆解(certbot 的安全参数)

这个文件基于 Mozilla 的 SSL 配置建议生成,长期由 certbot 更新。本机内容:

ssl_session_cache shared:le_nginx_SSL:10m;   # 会话缓存,复用握手结果
ssl_session_timeout 1440m;                   # 会话缓存 24 小时
ssl_session_tickets off;

ssl_protocols TLSv1.2 TLSv1.3;               # 只开安全的两个版本
ssl_prefer_server_ciphers off;               # 交给客户端偏好(表意) 
ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:...:DHE-RSA-AES128-GCM-SHA256:..."; 
  • ssl_protocols TLSv1.2 TLSv1.3:明确禁用老旧的 TLS 1.0 / 1.1(有 POODLE / BEAST 漏洞)。
  • ssl_session_cache shared:le_nginx_SSL:10m:缓存 TLS 会话,避免每个连接重新握手,提升性能。
  • 那条很长的 ssl_ciphers 是 密码套件(cipher suite)白名单,都是现代 GCM/CHACHA20 系,能借"前向保密"(Forward Secrecy,ECDHE)让历史会话不被未来私钥泄漏破解。

易错点:不要手动改这个文件——文件开头就写了"如果你改它,Certbot 将无法自动提供后续安全更新,只会报错并把路径告诉你"。真想调 TLS 参数,就在自己的 server 块里额外覆盖指令,或者在 ssl-config.mozilla.org 生成一份自己的 include 文件替换 include 指向。

80 -> 443 的跳转 server 块

certbot 会把"纯 HTTP 的 80 端口"改成一个 301 跳转块(本机真实内容):

server {
    if ($host = bangdream.xyz) {
        return 301 https://$host$request_uri;
    } # managed by Certbot

    listen 80 default_server;
    listen [::]:80 default_server;
    server_name bangdream.xyz www.bangdream.xyz;
    return 404; # managed by Certbot
}

作用:用户访问 http://bangdream.xyz 时,被 301 重定向到 https://bangdream.xyz,浏览器的地址栏因此永远是 HTTPS。return 404 兜底,防止无 TLS 的裸 80 被直接命中。

易错点 1:这里的 listen 80 default_server; 和 server_name bangdream.xyz ... 需要和某个具体域名匹配,否则普通 HTTP 到这台机器会落到错误的 server(默认会走 listen 80 default_server),产生诡异的 404 或跳错域名。

易错点 2:return 301 不是 302。301 是永久重定向,浏览器和搜索引擎都会记住并缓存,更利于 SEO 和防止 "http 版本仍在被收录"。

自定义一个"理想"的 server 块

如果你不想完全依赖 certbot,也可以手写一个(注意别和 certbot 的 # managed by Certbot 行冲突):

server {
    listen 443 ssl;
    http2 on;                                    # 开启 HTTP/2(比 http1 更快,防队头阻塞)

    server_name example.com;
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_session_cache shared:le_nginx_SSL:10m;
    ssl_session_timeout 1440m;
    ssl_session_tickets off;
    ssl_stapling on;                             # OCSP stapling(第五章详述)
    ssl_stapling_verify on;
    resolver 127.0.0.11 valid=30s;               # 供 OCSP 解析用

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;   # 后端以此判断是否 https
    }
}

易错点:ssl_stapling 需要 resolver 才能查 OCSP;如果没有 resolver 或网络不通,staple 就起不来。另外新版 Nginx http2 on; 是单独指令,旧版用 listen 443 ssl http2;——版本不同写法不同,别照抄。

场景:反向代理必须把协议传给后端

TLS 终止在 Nginx,所以后端看到的是明文 HTTP。后端要生成安全的 https:// 绝对地址、正确处理 cookie 的 Secure 标记,就必须知道原始协议。这就是 X-Forwarded-Proto 的职责:

location / {
    proxy_pass http://127.0.0.1:3000;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;   # $scheme 在 https 连接下就是 "https"
    proxy_redirect off;
}

本机所有站点(含 GitLab)的代理块里都写了 proxy_set_header X-Forwarded-Proto $scheme;。丢了它,后端会误以为自己是 http,生成错误的重定向或强制跳回 http,造成"页面无限重定向"。

server_name 与 SNI

现代客户端发起 TLS 握手时,会在 ClientHello 里带一个 SNI(Server Name Indication) 字段告诉服务器"我要访问哪个域名",Nginx 据此挑选 server_name 对应的证书。所以一台机上挂多个 HTTPS 域名,每域名一张证书,靠 SNI 区分:

域名 证书(live/ 下) Nginx server_name
bangdream.xyz bangdream.xyz bangdream.xyz www.bangdream.xyz
dsh.bangdream.xyz dsh.bangdream.xyz dsh.bangdream.xyz
gitlab.bangdream.xyz gitlab.bangdream.xyz gitlab.bangdream.xyz
lovelive.top lovelive.top lovelive.top www.lovelive.top

易错点:若同一 IP 的 server 块没有 listen ssl,或 server_name 没匹配,客户端会用第一个有 SSL 的默认 server 的证书——导致明明访问了 gitlab.bangdream.xyz,浏览器却弹出 "证书是 bangdream.xyz 的" 警告。

检查 Nginx 配置

# 语法检查(必做,配完先测)
nginx -t

# 热加载(不中断服务)
nginx -s reload

易错点:任何修改 nginx 配置后,先 nginx -t,再 nginx -s reload。-t 报错就说明有语法或路径问题,直接 reload 会起不来服务。

延伸:多证书与后续升级

  • certbot --nginx -d 在续期时会自动保留并更新你 server 块里的 TLS 行——只要别动 # managed by Certbot 标记。
  • 想整体换安全参数,用 https://ssl-config.mozilla.org 生成现代配置,替换 include 指向并另存,再 nginx -t && nginx -s reload。

下一章:04-cert-manage → 用 openssl 查看、校验、排查证书问题。