13064 字
65 分钟
第 5 篇:Nginx HTTPS、证书与安全入口

第 5 篇:Nginx HTTPS、证书与安全入口——从 TLS 握手、自动续期到安全边界#

专栏:《Nginx 从入口代理到云原生流量治理》
文章序号:第 5 篇
Nginx 实验版本:Nginx Open Source 1.30.3(稳定版)
当前主线版本:1.31.2
后端实验版本:Spring Boot 4.1.0、Java 21
实验环境:Docker Compose、Linux、OpenSSL、curl
更新时间:2026-07-07


本文在专栏中的位置#

前四篇已经完成了入口代理的主体链路:

  1. 第一篇确定了 Nginx 在后端架构中的位置;
  2. 第二篇讲清了配置层级以及 serverlocation 匹配;
  3. 第三篇拆解了反向代理、Header、超时、缓冲、WebSocket 和 SSE;
  4. 第四篇将单实例代理扩展为多实例负载均衡与高可用。

到这里,系统已经能够通过 Nginx 接收请求、选择后端并完成故障转移,但如果入口仍然暴露明文 HTTP,客户端和 Nginx 之间的数据就可能被窃听、篡改或冒充。即使内部服务本身具备登录、JWT 和权限校验,攻击者仍可能在请求到达应用之前截获 Cookie、Token、请求体或响应内容。

因此,第五篇要解决的问题不是“如何加一行 listen 443 ssl”,而是构建一个完整的安全入口:

  • 客户端如何确认自己连接的确实是目标服务;
  • TLS 1.2/1.3 如何协商密钥并保护后续 HTTP 数据;
  • Nginx 如何根据 SNI 选择证书;
  • 证书链、私钥和中间证书分别承担什么职责;
  • HTTP 应该如何跳转到 HTTPS;
  • HSTS 为什么不能随意开启 preload
  • 证书如何通过 ACME 自动签发和续期;
  • TLS 应该在 CDN、云负载均衡器、Nginx 还是应用层终止;
  • 安全 Header、CORS、Host 校验和请求限制应该由谁负责;
  • HTTPS 出现证书错误、握手失败或重定向循环时如何分层排查。

需要先明确一个核心结论:

HTTPS 只解决传输加密、完整性和服务端身份验证,不自动解决用户认证、业务授权、注入攻击、越权、CSRF、SSRF 或数据泄露。

Nginx 可以成为安全入口,但它不能替代应用安全。

独立插图 01:HTTPS 安全入口架构
对应文件:01-https-entry-architecture.png

01 https entry architecture 下一篇将在安全入口基础上进入缓存、压缩与静态资源优化。


学习目标#

学完本文后,你应当能够:

  1. 解释 HTTP 明文传输面临的窃听、篡改和身份冒充风险;
  2. 区分 HTTPS、TLS、证书、CA、公钥、私钥和会话密钥;
  3. ClientHelloFinished 解释一次 TLS 1.3 完整握手;
  4. 说明 ECDHE、证书签名和对称加密分别解决什么问题;
  5. 理解 SNI 与 ALPN 在 Nginx HTTPS 虚拟主机中的作用;
  6. 判断 fullchain.pem、叶子证书、中间证书和根证书应该如何部署;
  7. 检查证书与私钥是否匹配、SAN 是否覆盖目标域名;
  8. 使用 listen 443 sslhttp2 onssl_certificatessl_certificate_key 配置 HTTPS;
  9. 说明 TLS 1.2 密码套件与 TLS 1.3 密码套件的配置边界;
  10. 解释 Session Cache、Session Ticket、会话恢复与 0-RTT 风险;
  11. 正确选择 301、302、307 和 308 跳转,并理解方法是否保留;
  12. 说明 HSTS 只在 HTTPS 响应中生效,以及 includeSubDomainspreload 的不可逆风险;
  13. 区分 ACME HTTP-01、DNS-01 和 TLS-ALPN-01;
  14. 使用 Certbot 或可选的 Nginx ACME 模块设计证书自动续期流程;
  15. 解释为什么续期成功后还需要 nginx -t、reload、到期监控和回滚;
  16. 比较 CDN、云 LB、Nginx 和应用层 TLS 终止;
  17. 判断哪些安全 Header 适合入口统一添加,哪些必须由应用动态生成;
  18. 解释 CORS 为什么不是鉴权,并正确处理预检、凭证与 Vary: Origin
  19. 使用 openssl s_clientcurl -vnginx -T 定位 HTTPS 故障;
  20. 完成一个 Docker Compose + Spring Boot + 本地 CA 证书的可运行实验。

一、HTTP 为什么不安全,TLS 到底解决什么#

1.1 明文 HTTP 的三个核心风险#

客户端直接通过 HTTP 请求后端时,请求行、Header、Cookie、查询参数和请求体默认以明文在网络上传输。网络路径中的恶意节点可能实施三类攻击。

风险一:窃听#

攻击者可以读取:

  • 登录 Cookie;
  • Bearer Token;
  • 用户名和密码;
  • 订单、地址和支付相关数据;
  • API 请求与响应;
  • URL 中的敏感查询参数。

即使密码在数据库中经过哈希处理,也无法阻止它在传输过程中被截获。

风险二:篡改#

攻击者不仅能读取数据,还可能修改:

  • 将下载文件替换为恶意程序;
  • 修改 API 请求参数;
  • 注入脚本;
  • 替换响应中的跳转地址;
  • 将用户导向仿冒登录页面。

风险三:身份冒充#

客户端无法仅凭一个 IP 地址确认对方是否真的是 api.example.com。DNS 污染、ARP 欺骗、恶意代理或错误路由都可能把请求导向攻击者控制的服务器。

1.2 TLS 提供的安全属性#

TLS 主要提供:

能力解决的问题依赖机制
机密性第三方不能直接读取通信内容对称加密
完整性数据被修改后能够被检测AEAD 认证加密
服务端身份验证客户端确认服务端证书属于目标域名CA 证书链与数字签名
可选客户端身份验证服务端验证客户端证书mTLS
前向保密长期私钥未来泄露时,历史会话不应被直接解密临时 ECDHE 密钥交换

HTTPS 可以理解为:

HTTP 语义
TLS 加密通道
TCP(HTTP/1.1、HTTP/2)或 QUIC/UDP(HTTP/3)

对于常见 Nginx HTTPS:

客户端 === TLS ===> Nginx -- HTTP 或 HTTPS --> Spring Boot

客户端与 Nginx 的 TLS 连接和 Nginx 与后端的连接是两条独立连接。入口启用 HTTPS,不代表 Nginx 到后端也自动加密。

1.3 TLS 不解决什么#

TLS 无法自动解决:

  • 用户密码过弱;
  • JWT 被应用日志泄露;
  • SQL 注入;
  • XSS;
  • CSRF;
  • 越权访问;
  • 服务端主动返回敏感数据;
  • 私钥在服务器磁盘上被窃取;
  • 已登录用户执行恶意操作;
  • Nginx 之后的内部链路被攻击。

因此不能把“网站已经是 HTTPS”当作系统已经安全。


二、TLS 1.3 握手:从非对称认证到对称加密#

2.1 为什么不能一直使用非对称加密#

非对称密码适合:

  • 身份认证;
  • 数字签名;
  • 密钥协商。

但它不适合直接加密大量业务数据,因为计算成本和数据长度限制都明显高于对称加密。

TLS 的基本思路是:

  1. 使用证书和数字签名确认服务端身份;
  2. 使用临时密钥交换协商共享秘密;
  3. 通过密钥派生函数生成会话密钥;
  4. 使用高效的对称 AEAD 算法保护 HTTP 数据。

2.2 TLS 1.3 完整握手主流程#

独立插图 02:TLS 1.3 完整握手时序
对应文件:02-tls13-handshake-sequence.png

02 tls13 handshake sequence 典型流程如下。

第一步:客户端发送 ClientHello#

客户端发送:

  • 支持的 TLS 版本;
  • 支持的密码套件;
  • 随机数;
  • 临时密钥份额;
  • SNI;
  • ALPN;
  • 支持的签名算法;
  • 可能的会话恢复信息。

SNI 告诉 Nginx 客户端希望访问哪个域名;ALPN 用于协商 h2http/1.1 等上层协议。

第二步:服务端发送 ServerHello#

Nginx 选择:

  • TLS 版本;
  • 密码套件;
  • 服务器临时密钥份额。

双方通过临时 ECDHE 参数得到相同共享秘密,但共享秘密本身不会直接在网络上传输。

常见安全组包括 X25519、secp256r1 等。实际选择由客户端、Nginx、OpenSSL 版本和配置共同决定。

第三步:服务端发送加密扩展#

在 TLS 1.3 中,后续握手消息更早进入加密状态。服务端会确认 ALPN 等扩展,例如选择 HTTP/2 的 h2

第四步:服务端发送证书链#

证书包含:

  • 域名身份;
  • 服务端公钥;
  • 有效期;
  • 签发者;
  • 用途约束;
  • CA 的数字签名。

第五步:CertificateVerify#

Nginx 使用与证书公钥对应的私钥对握手上下文签名。

客户端使用证书中的公钥验证签名,从而确认:

  1. 对端确实持有证书对应私钥;
  2. 当前握手没有被替换为另一个未经授权的服务端。

需要注意:证书私钥主要用于签名认证,不等于“用 RSA 私钥解密所有 HTTPS 数据”。现代 TLS 1.3 使用临时密钥交换产生会话秘密。

第六步:客户端验证证书链#

客户端检查:

  • SAN 是否包含访问域名;
  • 当前时间是否在证书有效期内;
  • 证书是否能够通过中间 CA 链到受信任根;
  • 数字签名是否有效;
  • 用途和策略是否允许;
  • 客户端生态是否认为该证书已被吊销。

第七步:双方发送 Finished#

Finished 消息用于确认握手消息完整性。完成后,双方使用派生的对称密钥传输 HTTP 数据。

2.3 单向陷门与 ECDHE 的核心#

ECDHE 的安全性依赖椭圆曲线离散对数问题。

客户端随机生成临时私钥 a,计算公开点:

A = aG

服务端生成临时私钥 b,计算:

B = bG

双方分别计算:

客户端:aB = abG
服务端:bA = abG

网络观察者能够获得 GAB,但无法在可行时间内从 A = aG 反推出 a,也无法从 B = bG 反推出 b

这里的“单向”不是数学上绝对不可逆,而是当前参数规模下计算成本不可接受。

因为 ab 是临时密钥,握手结束后应被销毁,所以即使服务端证书私钥未来泄露,攻击者通常也不能仅凭历史抓包还原过去的会话密钥,这就是前向保密。

2.4 TLS 1.2 与 TLS 1.3 的实践边界#

目前通用生产配置通常保留:

ssl_protocols TLSv1.2 TLSv1.3;

原因是:

  • TLS 1.3 更现代、握手更精简;
  • TLS 1.2 仍用于兼容部分旧客户端;
  • TLS 1.0 和 TLS 1.1 不应作为新的通用生产基线。

ssl_ciphers 主要控制 TLS 1.2 及更早版本的密码套件。TLS 1.3 密码套件由 OpenSSL 的 TLS 1.3 配置机制管理,不应误以为一条旧式 ssl_ciphers 就能控制所有版本。

不要直接复制多年以前包含 RC4、3DES、静态 RSA 密钥交换或 TLS 1.0 的“万能配置”。

2.5 0-RTT 为什么默认应谨慎#

TLS 1.3 会话恢复可以支持 Early Data,即客户端在握手完全确认前发送数据。

优势:降低延迟。

风险:Early Data 可能被重放。

因此:

  • 不要把订单创建、扣款、发券、修改密码等非幂等请求放入 0-RTT;
  • 应用必须能够识别和拒绝不安全的 Early Data;
  • 未明确理解重放防护前,应保持 ssl_early_data 关闭。

三、证书链、公钥、私钥与域名验证#

3.1 证书不是“加密文件”#

X.509 证书是一个经过 CA 签名的身份声明,主要包含:

  • Subject Alternative Name;
  • 公钥;
  • 签发者;
  • 序列号;
  • 有效期;
  • Key Usage / Extended Key Usage;
  • CA 数字签名。

证书通常可以公开发送。真正必须保密的是私钥。

3.2 叶子证书、中间证书与根证书#

独立插图 03:证书链与客户端信任验证
对应文件:03-certificate-chain-validation.png

03 certificate chain validation

叶子证书#

也叫服务器证书,直接对应你的域名,例如:

api.example.com

中间 CA 证书#

由根 CA 或更上级中间 CA 签发,用来签发叶子证书。服务端通常需要把中间证书随叶子证书一起发送。

根 CA 证书#

根证书通常已经预装在操作系统、浏览器或 Java TrustStore 中。服务端通常不需要发送根证书。

3.3 fullchain.pem 是什么#

常见 ACME 客户端生成:

cert.pem 叶子证书
chain.pem 中间证书链
fullchain.pem 叶子证书 + 中间证书链
privkey.pem 私钥

Nginx 通常配置:

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

如果只配置叶子证书,部分客户端可能无法构建完整信任链。

3.4 SAN 而不是只看 Common Name#

现代客户端主要根据 SAN 验证域名。

例如访问:

https://api.example.com

证书 SAN 至少需要包含:

DNS:api.example.com

通配符:

*.example.com

通常只能匹配一级子域,例如:

api.example.com 可匹配
v1.api.example.com 通常不可匹配
example.com 不匹配

3.5 证书与私钥必须匹配#

可以通过公钥摘要检查。

RSA 示例:

Terminal window
openssl x509 -in fullchain.pem -noout -pubkey \
| openssl pkey -pubin -outform pem \
| sha256sum
openssl pkey -in privkey.pem -pubout -outform pem \
| sha256sum

两个摘要应一致。

查看证书信息:

Terminal window
openssl x509 -in fullchain.pem -noout -subject -issuer -dates -ext subjectAltName

检查私钥:

Terminal window
openssl pkey -in privkey.pem -check -noout

3.6 私钥保护#

私钥保护至少包括:

  • 文件权限最小化;
  • 不提交到 Git;
  • 不打进公开镜像层;
  • 容器中只读挂载;
  • 不在日志中打印;
  • 备份应加密;
  • 轮换和吊销流程可执行;
  • 限制能读取证书 Secret 的账号;
  • 云环境优先使用受控证书服务或 Secret 管理系统。

私钥一旦泄露,攻击者可能冒充服务端。在发现泄露后,仅删除旧文件不够,还应吊销证书、轮换私钥并排查泄露范围。


四、Nginx HTTPS 配置与请求处理流程#

4.1 TLS 握手发生在 HTTP 路由之前#

独立插图 04:Nginx SNI、证书、ALPN 与 HTTP 请求处理流程
对应文件:04-nginx-tls-sni-alpn-flow.png

04 nginx tls sni alpn flow 基本顺序是:

TCP 连接
-> ClientHello
-> 根据监听地址和 SNI 选择 HTTPS 虚拟主机/证书
-> TLS 握手
-> ALPN 确定 HTTP/2 或 HTTP/1.1
-> 解密 HTTP 请求
-> server/location 匹配和业务处理

因此,证书错误发生时,应用接口通常还没有获得请求。

4.2 最小 HTTPS 配置#

server {
listen 443 ssl;
http2 on;
server_name api.example.com;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://backend_cluster;
}
}

listen 443 ssl#

表示该监听端口使用 TLS。

http2 on#

从 Nginx 1.25.1 开始,HTTP/2 使用独立 http2 on; 指令启用。新的文章和配置不应继续把:

listen 443 ssl http2;

作为推荐写法。

旧写法在某些版本仍可能兼容,但会产生弃用提示。

ssl_certificate#

指定发送给客户端的证书链。

ssl_certificate_key#

指定与叶子证书公钥匹配的私钥。

ssl_protocols#

限制允许的 TLS 协议版本。

4.3 SNI 与多个 HTTPS 域名#

同一个 IP 和 443 端口可以承载多个 HTTPS 域名:

server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /certs/api/fullchain.pem;
ssl_certificate_key /certs/api/privkey.pem;
}
server {
listen 443 ssl;
server_name admin.example.com;
ssl_certificate /certs/admin/fullchain.pem;
ssl_certificate_key /certs/admin/privkey.pem;
}

客户端在 ClientHello 中发送 SNI,Nginx 据此选择证书。

如果客户端没有发送 SNI,Nginx 只能使用该监听地址上的默认 HTTPS server 证书。这也是为什么错误的默认虚拟主机可能导致“访问 A 域名却收到 B 域名证书”。

4.4 ALPN 与 HTTP/2#

ALPN 是 TLS 扩展,用于协商上层协议。

客户端可能声明支持:

h2
http/1.1

Nginx 完成 TLS 握手时选择一个协议。可以记录:

log_format tls '$remote_addr $host $request '
'status=$status protocol=$server_protocol '
'tls=$ssl_protocol cipher=$ssl_cipher '
'sni=$ssl_server_name http2=$http2';

4.5 TLS Session Cache#

TLS 握手中的证书验证和密钥计算存在成本。Session Resumption 可以减少重复完整握手。

共享 Session Cache:

ssl_session_cache shared:TLS:20m;
ssl_session_timeout 10m;

官方文档说明,共享缓存每 1MB 大约可容纳数千个会话,实际容量受实现和会话数据影响。

共享缓存能够让不同 Worker 复用会话状态。

4.6 Session Ticket#

ssl_session_tickets on;

Session Ticket 把会话状态交给客户端保存,服务端使用 Ticket Key 加解密。

需要关注:

  • 多 Nginx 节点需要一致的 Ticket Key 才能跨节点恢复;
  • 长期静态 Ticket Key 会扩大密钥泄露影响;
  • Ticket Key 需要安全轮换;
  • 不具备轮换能力时可以关闭 Ticket,接受更多完整握手;
  • TLS 1.3 会话恢复和 Early Data 也需要理解 Ticket/PSK 语义。

简化基线可以使用:

ssl_session_cache shared:TLS:20m;
ssl_session_timeout 10m;
ssl_session_tickets off;

这不是所有场景的性能最优解,而是降低 Ticket Key 运维复杂度的选择。

4.7 OCSP Stapling 的当前边界#

Nginx 支持:

ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/certs/ca-chain.pem;
resolver 1.1.1.1 8.8.8.8 valid=300s;

但是否有意义取决于证书签发 CA 是否提供 OCSP Responder。

截至 2026 年,Let’s Encrypt 已停止 OCSP 服务,并改用 CRL 发布吊销信息。因此,对当前 Let’s Encrypt 证书照搬 ssl_stapling on 不会获得有效 OCSP 响应。

正确原则是:

  1. 检查证书 AIA 是否包含 OCSP URL;
  2. 检查签发 CA 当前是否仍提供 OCSP;
  3. 不要把 OCSP Stapling 当成所有证书的固定模板;
  4. 不要继续请求 Let’s Encrypt OCSP Must-Staple 证书。

4.8 HTTP/3 不是简单加一行配置#

Nginx 从 1.25.0 开始提供实验性的 ngx_http_v3_module

需要注意:

  • 模块并非所有构建默认包含;
  • 使用 QUIC/UDP 而不是 TCP;
  • 需要 UDP 443、防火墙和云 LB 支持;
  • 常通过 Alt-Svc 告知客户端;
  • OpenSSL 和 Nginx 构建方式影响 0-RTT 等能力;
  • 官方仍将该模块标为实验性。

因此,本篇稳定版实验只启用 HTTPS 与 HTTP/2,不把 HTTP/3 作为默认生产基线。


五、HTTP 跳转 HTTPS 与 HSTS#

5.1 HTTP 重定向和 HSTS 不是同一件事#

独立插图 05:HTTP 跳转 HTTPS 与 HSTS 请求流程
对应文件:05-http-redirect-hsts-flow.png

05 http redirect hsts flow HTTP 重定向处理当前请求:

客户端先发送 HTTP
-> 服务端返回 Location: https://...
-> 客户端重新发起 HTTPS

HSTS 改变浏览器未来请求:

浏览器已经通过 HTTPS 收到 HSTS
-> 在有效期内输入 http:// 时,本地直接升级为 https://

5.2 跳转配置#

生产环境常见:

server {
listen 80;
server_name example.com www.example.com;
return 308 https://$host$request_uri;
}

$request_uri 保留原始路径和查询参数。

5.3 301、302、307、308 的差异#

状态码永久性方法和请求体语义
301永久历史客户端可能把 POST 改成 GET
302临时历史客户端可能把 POST 改成 GET
307临时明确保留方法和请求体
308永久明确保留方法和请求体

对于网页 GET,301/308 都常见。

对于 API,如果不能接受 POST 被转换为 GET,应优先考虑 307/308。最稳妥的方式仍是让客户端直接使用 HTTPS API 地址,不依赖明文入口跳转。

5.4 HSTS 基础配置#

add_header Strict-Transport-Security "max-age=31536000" always;

HSTS 只能通过 HTTPS 响应发送。浏览器会忽略 HTTP 响应中的 HSTS。

5.5 includeSubDomains 的风险#

add_header Strict-Transport-Security \
"max-age=31536000; includeSubDomains" always;

这表示所有子域都必须支持 HTTPS。

如果以下任意子域仍只支持 HTTP:

legacy.example.com
printer.example.com
internal.example.com

浏览器可能强制使用 HTTPS,导致业务不可访问。

5.6 preload 的风险#

add_header Strict-Transport-Security \
"max-age=63072000; includeSubDomains; preload" always;

preload 不是普通运行参数,而是申请加入浏览器预加载列表的承诺。

启用前必须确认:

  • 主域和所有现有子域都支持 HTTPS;
  • 未来新子域也会默认支持 HTTPS;
  • 证书自动化成熟;
  • 不存在依赖明文 HTTP 的设备或旧系统;
  • 团队理解移除 preload 并不能立即影响所有客户端。

建议按阶段上线:

max-age=300
-> 1 天
-> 1 周
-> 1 月
-> 1 年
-> includeSubDomains
-> 最后才考虑 preload

5.7 重定向循环#

常见结构:

客户端 HTTPS
-> 云 LB 终止 TLS
-> HTTP 转发给 Nginx
-> Nginx 看到 $scheme=http,再跳转 HTTPS
-> 客户端重新访问 HTTPS
-> 循环

根因是 Nginx 没有正确理解原始协议。

修复思路:

  • 只信任明确的上游负载均衡器;
  • 使用受控的 X-Forwarded-Proto
  • 云 LB 与 Nginx 的转发协议配置一致;
  • 不信任任意客户端伪造的转发 Header。

六、ACME、Certbot 与证书自动续期#

6.1 为什么证书必须自动化#

证书有有效期。手工续期存在:

  • 忘记续期;
  • 私钥和证书路径写错;
  • 多节点只更新一台;
  • 更新后没有 reload;
  • reload 失败仍以为已经生效;
  • 证书文件更新但 Nginx 仍加载旧证书;
  • 到期前没有告警。

完整自动化应形成闭环。

独立插图 06:ACME 签发、部署、续期与 reload 闭环
对应文件:06-acme-certificate-lifecycle.png

06 acme certificate lifecycle

6.2 ACME 的作用#

ACME 协议让客户端自动完成:

  1. 创建 CA 订单;
  2. 证明域名控制权;
  3. 提交 CSR;
  4. 获取证书;
  5. 周期续期;
  6. 必要时吊销。

6.3 HTTP-01#

CA 要求在以下路径提供 Token:

http://example.com/.well-known/acme-challenge/<token>

特点:

  • 需要公网可访问 80 端口;
  • 不能签发通配符证书;
  • 多入口节点需要共享 Challenge 文件或统一路由;
  • 适合单域名和普通 Web 服务。

Nginx 常见配置:

server {
listen 80;
server_name example.com www.example.com;
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
default_type text/plain;
try_files $uri =404;
}
location / {
return 308 https://$host$request_uri;
}
}

不要先把所有 HTTP 请求无条件跳转,导致 Challenge 路径不可达。

6.4 DNS-01#

CA 要求在 DNS 中创建:

_acme-challenge.example.com TXT <value>

特点:

  • 可以签发通配符证书;
  • 不要求目标 Web 服务开放 80;
  • 需要 DNS API 自动化;
  • DNS API 凭证必须最小权限;
  • 可以把 _acme-challenge 委托到单独验证区域。

不要把拥有整个 DNS 账号全部权限的长期密钥直接放进公网 Nginx 容器。

6.5 TLS-ALPN-01#

通过 443 端口和专用 ALPN 协议证明控制权。

适合某些无法使用 80 的场景,但需要入口能够正确处理 Challenge。实际支持情况取决于 ACME 客户端、CA 和部署方式。

6.6 Certbot 自动续期#

Certbot 是常见 ACME 客户端。

Webroot 模式示例:

Terminal window
certbot certonly \
--webroot \
--webroot-path /var/www/certbot \
--domain example.com \
--domain www.example.com \
--email ops@example.com \
--agree-tos \
--no-eff-email

测试续期:

Terminal window
certbot renew --dry-run

续期成功后验证并 reload:

Terminal window
certbot renew \
--deploy-hook 'nginx -t && nginx -s reload'

生产中应确认:

  • Hook 运行环境能访问正确的 Nginx;
  • 容器模式下不能假设 Certbot 容器中的 nginx 是入口容器;
  • 可以通过共享证书卷、宿主机脚本或编排系统发送 reload;
  • reload 失败应告警而不是静默忽略。

6.7 Nginx 原生 ACME 模块的当前边界#

Nginx 已提供可选的 ngx_http_acme_module,实现 ACMEv2,并从 1.29.0 起提供预编译模块包。

需要注意:

  • 它是可选模块,不应假设所有 Nginx 安装或官方基础镜像默认包含;
  • 需要安装相应模块包或自行构建;
  • 需要持久化 state_path,避免重启丢失状态和触发 CA 限速;
  • 当前模块支持的 Challenge 类型和功能应以实际模块版本文档为准;
  • 迁移前需要评估与现有 Certbot、Secret 管理和发布流程的兼容性。

示意配置:

load_module modules/ngx_http_acme_module.so;
http {
resolver 1.1.1.1;
acme_issuer letsencrypt {
uri https://acme-v02.api.letsencrypt.org/directory;
contact ops@example.com;
state_path /var/cache/nginx/acme-letsencrypt;
accept_terms_of_service;
}
acme_shared_zone zone=ngx_acme_shared:1M;
server {
listen 443 ssl;
server_name example.com;
acme_certificate letsencrypt;
ssl_certificate $acme_certificate;
ssl_certificate_key $acme_certificate_key;
ssl_certificate_cache max=10;
}
}

本文实验仍使用静态本地证书,因为它更适合离线复现 TLS 原理;生产环境再接入 Certbot、Nginx ACME 模块、云证书服务或 Kubernetes cert-manager。

6.8 多 Nginx 节点的证书同步#

双机或多实例场景要解决:

  • 证书由谁申请;
  • 私钥在哪里生成;
  • 如何安全同步;
  • reload 是否逐节点执行;
  • 某节点更新失败如何回滚;
  • 证书和 Ticket Key 是否一致;
  • ACME Challenge 如何路由;
  • 是否使用云证书托管避免分发私钥。

不建议让每个节点对同一域名完全独立、无协调地频繁申请证书,容易触发 CA 限速并增加状态不一致。


七、TLS 应该在哪里终止#

独立插图 07:TLS 终止与再加密部署模式
对应文件:07-tls-termination-patterns.png

07 tls termination patterns

7.1 Nginx 终止 TLS#

Client === TLS ===> Nginx -- HTTP --> App

优点:

  • 配置直观;
  • 证书集中;
  • 应用无需承担 TLS 握手;
  • 易于统一安全 Header 和日志。

风险:

  • Nginx 到应用是明文;
  • 内部网络不可信时可能不满足要求;
  • 应用必须通过可信 Header 识别原始 HTTPS。

7.2 云 LB 终止 TLS#

Client === TLS ===> Cloud LB -- HTTP --> Nginx -- HTTP --> App

优点:

  • 托管证书;
  • 自动扩容;
  • 与云 WAF、DDoS 防护结合;
  • 降低自维护入口压力。

风险:

  • Nginx 看到的 $scheme 可能是 HTTP;
  • 真实 IP 和协议依赖可信代理 Header;
  • 错误配置可能造成重定向循环;
  • 云 LB 到 Nginx 的网络仍需评估。

7.3 Nginx 到后端再加密#

Client === TLS ===> Nginx === TLS ===> App

配置示意:

location /api/ {
proxy_pass https://backend_https;
proxy_ssl_server_name on;
proxy_ssl_name backend.internal.example;
proxy_ssl_verify on;
proxy_ssl_trusted_certificate /etc/nginx/ca/internal-root.pem;
proxy_ssl_verify_depth 2;
}

不要使用:

proxy_ssl_verify off;

来“解决”内部证书报错。关闭验证只获得加密,不获得可信身份验证,会把中间人攻击风险带回内部链路。

7.4 mTLS#

独立插图 12:mTLS 客户端证书与信任边界
对应文件:12-mtls-trust-boundary.png

12 mtls trust boundary Nginx 验证客户端证书:

ssl_client_certificate /etc/nginx/ca/client-ca.pem;
ssl_verify_client on;
ssl_verify_depth 2;

可以记录:

$ssl_client_verify
$ssl_client_s_dn
$ssl_client_serial

但要注意:

  • mTLS 识别的是证书身份,不自动等于业务用户权限;
  • 不要把证书 Subject 未经规范化直接当作管理员权限;
  • 转发到后端的身份 Header 必须由 Nginx 覆盖,不能让公网客户端自行注入;
  • 需要处理证书签发、轮换、吊销和根 CA 迁移;
  • 普通浏览器用户场景通常仍使用账号、OIDC、Cookie 或 Token,而不是客户端证书。

7.5 选择矩阵#

场景推荐方式
单机个人项目Nginx 终止 TLS
云上公网应用云 LB/CDN 终止或云 LB + Nginx 再加密
跨不可信网络端到端 TLS
强合规内部系统Nginx 到应用再加密,必要时 mTLS
KubernetesGateway/Ingress 终止,证书交给 cert-manager 或云托管
机器对机器高信任接口mTLS + 应用级授权

八、安全 Header:哪些适合 Nginx,哪些属于应用#

独立插图 08:安全 Header 的入口层与应用层职责边界
对应文件:08-security-headers-responsibility.png

08 security headers responsibility

8.1 add_header 的继承陷阱#

add_header X-Content-Type-Options nosniff always;

always 表示不仅对部分 2xx/3xx 响应添加,也对更多状态码添加。

需要注意:在子级上下文重新声明任何 add_header 时,父级的 add_header 可能不再按直觉继承。应使用统一 snippet,或检查 nginx -T 展开的最终配置。

8.2 X-Content-Type-Options#

add_header X-Content-Type-Options "nosniff" always;

作用是让浏览器尊重服务端声明的 MIME 类型,减少 MIME Sniffing。

前提是服务端自身必须返回正确 Content-Type,否则 nosniff 可能暴露原本被浏览器“猜测修复”的配置错误。

8.3 Referrer-Policy#

add_header Referrer-Policy "strict-origin-when-cross-origin" always;

控制浏览器跨页面请求时发送多少 Referer 信息。

选择策略需要考虑:

  • 是否依赖完整 Referer 做统计;
  • URL 中是否有敏感参数;
  • 跨域请求是否需要来源信息;
  • 前端分析和安全需求。

8.4 Permissions-Policy#

add_header Permissions-Policy \
"camera=(), microphone=(), geolocation=()" always;

用于限制页面或 iframe 使用浏览器能力。

不要盲目禁止业务确实需要的相机、麦克风或地理位置能力。

8.5 Frame 防护#

传统配置:

add_header X-Frame-Options "DENY" always;

更现代、粒度更高的方式是 CSP:

Content-Security-Policy: frame-ancestors 'none'

如果应用存在合法嵌入场景,不能全局设置 DENY。CSP 的 frame-ancestors 更适合按页面和业务来源控制。

8.6 Content-Security-Policy#

静态站点简单基线:

add_header Content-Security-Policy \
"default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'" always;

但复杂前端通常包含:

  • 动态脚本;
  • 第三方统计;
  • CDN;
  • WebSocket;
  • 图片和字体域名;
  • 内联脚本;
  • nonce 或 hash。

因此 CSP 往往需要应用为每次响应生成 nonce:

Content-Security-Policy: script-src 'self' 'nonce-<random>'

Nginx 静态配置不适合独立生成与页面模板同步的动态 nonce。

建议先使用:

Content-Security-Policy-Report-Only

观察违规,再逐步强制。

Cookie 的:

Secure
HttpOnly
SameSite
Path
Domain
Max-Age

更适合由应用或认证网关按具体 Cookie 语义生成。

Nginx 可以通过代理重写 Cookie 属性,但全局修改容易误伤:

  • OAuth 回调;
  • 跨站嵌入;
  • 多子域会话;
  • 第三方身份提供商;
  • 不同 Cookie 的 SameSite 要求。

8.8 HSTS 适合入口统一,但需要组织级确认#

HSTS 看起来适合 Nginx 全局添加,但 includeSubDomains 会影响整个域名体系。因此它不是单个服务团队随意决定的 Header,而是域名治理决策。


九、基础入口防护与 Host 边界#

独立插图 10:Nginx 安全入口的分层防护
对应文件:10-nginx-security-entry-layers.png

10 nginx security entry layers

9.1 默认 HTTPS server#

应该拒绝未知 Host/SNI:

server {
listen 443 ssl default_server;
http2 on;
server_name _;
ssl_certificate /etc/nginx/certs/default.crt;
ssl_certificate_key /etc/nginx/certs/default.key;
return 444;
}

注意:TLS 握手需要先选择某张证书,所以默认 HTTPS server 仍需要可用证书。使用什么默认证书取决于部署和客户端兼容策略。

9.2 Host 校验#

不要把任意客户端 Host 用于:

  • 密码重置链接;
  • OAuth 回调地址;
  • 邮件链接;
  • 绝对 URL;
  • 多租户映射。

入口应只接受预期域名:

server_name api.example.com;

应用生成绝对链接时还要使用可信配置,而不是直接信任请求 Host。

9.3 请求体大小#

client_max_body_size 10m;

作用:限制单个请求体大小。

它不是所有接口都应该统一为 10MB。上传接口、普通 JSON API、Webhook 和管理接口应有不同上限。

超限通常返回 413。

9.4 请求方法限制#

静态资源可以限制:

location /assets/ {
limit_except GET HEAD {
deny all;
}
}

API 方法应由应用路由和授权最终判断。不要在 Nginx 中维护一套与应用完全重复、容易漂移的方法矩阵。

9.5 隐藏文件和敏感路径#

location ~ /\. {
deny all;
}

但需要为 ACME Challenge 留出例外:

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

同时防止误暴露:

  • .git
  • .env
  • 私钥;
  • 数据库备份;
  • Spring Boot 配置;
  • 调试文件;
  • 管理接口。

9.6 Server Header#

server_tokens off;

可以减少响应中直接暴露精确版本,但它不是安全补丁,也不能隐藏所有实现特征。真正重要的是及时升级和减少攻击面。

9.7 WAF 边界#

Nginx 原生配置可以做基础路径、大小、方法、速率限制,但不等于完整 WAF。

复杂的:

  • SQL 注入检测;
  • XSS 特征;
  • Bot 管理;
  • 虚拟补丁;
  • 威胁情报;
  • 大规模 DDoS;

通常需要云 WAF、专用 WAF 模块或上层安全服务。


十、CORS:浏览器读取权限,不是接口鉴权#

独立插图 09:CORS 预检与凭证请求流程
对应文件:09-cors-preflight-credentials.png

09 cors preflight credentials

10.1 什么是 Origin#

Origin 由:

scheme + host + port

组成。

以下 Origin 不相同:

https://app.example.com
http://app.example.com
https://app.example.com:8443
https://api.example.com

10.2 CORS 控制什么#

CORS 控制浏览器中的脚本是否可以读取跨 Origin 响应。

它不阻止:

  • curl;
  • Postman;
  • 后端服务;
  • 恶意脚本直接向接口发送请求;
  • 已泄露 Token 被服务端调用。

所以 CORS 不能代替:

  • 身份认证;
  • 权限校验;
  • API Key;
  • CSRF;
  • 速率限制。

10.3 简单请求和预检请求#

当请求方法、Header 或 Content-Type 超出简单请求范围时,浏览器先发送:

OPTIONS /api/orders HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Authorization, Content-Type

服务端返回:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Allow-Credentials: true
Vary: Origin

预检成功后,浏览器才发送真实请求。

10.4 凭证与通配符不能组合#

错误配置:

Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

浏览器不会接受带凭证的通配符 Origin。

需要根据白名单返回明确 Origin:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Vary: Origin

10.5 使用 map 构造 Origin 白名单#

map $http_origin $cors_origin {
default "";
"https://app.example.com" $http_origin;
"https://admin.example.com" $http_origin;
}

预检示意:

location /api/ {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-Request-ID" always;
add_header Vary "Origin" always;
add_header Content-Length 0;
return 204;
}
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Vary "Origin" always;
proxy_pass http://backend;
}

这里 if 只执行 return,行为较可控。但仍应注意:

  • 非白名单 Origin 时变量为空,必须确认实际响应行为;
  • add_header 继承规则容易导致重复或丢失;
  • 应用也添加 CORS 时可能出现多个 Access-Control-Allow-Origin
  • 复杂租户、动态域名和不同接口策略更适合由应用或 API Gateway 统一处理;
  • 带 Cookie 的跨站请求还需要 CSRF 防护。

10.6 CORS 与缓存#

当响应根据 Origin 返回不同 Access-Control-Allow-Origin 时,应发送:

Vary: Origin

否则缓存可能把为 A Origin 生成的响应错误提供给 B Origin。


十一、最小可运行实验#

本实验完成:

  • 本地根 CA;
  • localhostdev.example.test 签发服务器证书;
  • Nginx 80 端口跳转到 8443;
  • Nginx 8443 完成 TLS;
  • HTTP/2 配置;
  • 反向代理到 Spring Boot;
  • 传递原始协议和客户端信息;
  • 安全 Header;
  • 使用 curl --cacertopenssl s_client 验证。

11.1 项目结构#

nginx-https-lab/
├── compose.yaml
├── backend/
│ ├── Dockerfile
│ ├── pom.xml
│ └── src/main/java/com/example/httpslab/
│ ├── HttpsLabApplication.java
│ └── InfoController.java
├── nginx/
│ ├── nginx.conf
│ ├── conf.d/
│ │ └── app.conf
│ ├── snippets/
│ │ ├── tls.conf
│ │ ├── proxy-headers.conf
│ │ └── security-headers.conf
│ └── html/
│ └── index.html
├── certs/
│ ├── rootCA.crt
│ ├── rootCA.key
│ ├── server.crt
│ └── server.key
└── scripts/
├── generate-local-cert.sh
└── verify.sh

11.2 Spring Boot pom.xml#

路径:backend/pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>nginx-https-lab</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

11.3 启动类#

路径:backend/src/main/java/com/example/httpslab/HttpsLabApplication.java

package com.example.httpslab;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class HttpsLabApplication {
public static void main(String[] args) {
SpringApplication.run(HttpsLabApplication.class, args);
}
}

11.4 Header 观察接口#

路径:backend/src/main/java/com/example/httpslab/InfoController.java

package com.example.httpslab;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.Map;
@RestController
public class InfoController {
@GetMapping("/api/info")
public ResponseEntity<Map<String, Object>> info(HttpServletRequest request) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("time", Instant.now().toString());
body.put("schemeSeenByApp", request.getScheme());
body.put("serverNameSeenByApp", request.getServerName());
body.put("remoteAddrSeenByApp", request.getRemoteAddr());
body.put("host", request.getHeader("Host"));
body.put("xForwardedProto", request.getHeader("X-Forwarded-Proto"));
body.put("xForwardedHost", request.getHeader("X-Forwarded-Host"));
body.put("xForwardedFor", request.getHeader("X-Forwarded-For"));
body.put("xRequestId", request.getHeader("X-Request-ID"));
return ResponseEntity.ok(body);
}
}

11.5 Dockerfile#

路径:backend/Dockerfile

FROM maven:3.9.11-eclipse-temurin-21 AS build
WORKDIR /workspace
COPY pom.xml .
RUN mvn -q -DskipTests dependency:go-offline
COPY src ./src
RUN mvn -q -DskipTests package
FROM eclipse-temurin:21-jre
WORKDIR /app
RUN useradd --system --uid 10001 appuser
COPY --from=build /workspace/target/nginx-https-lab-0.0.1-SNAPSHOT.jar app.jar
USER appuser
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

11.6 本地 CA 和服务器证书脚本#

路径:scripts/generate-local-cert.sh

#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
CERT_DIR="$ROOT_DIR/certs"
mkdir -p "$CERT_DIR"
openssl genpkey \
-algorithm EC \
-pkeyopt ec_paramgen_curve:P-256 \
-out "$CERT_DIR/rootCA.key"
openssl req \
-x509 \
-new \
-sha256 \
-days 3650 \
-key "$CERT_DIR/rootCA.key" \
-subj "/C=SG/O=Nginx Column Lab/CN=Nginx Column Local Root CA" \
-out "$CERT_DIR/rootCA.crt"
openssl genpkey \
-algorithm EC \
-pkeyopt ec_paramgen_curve:P-256 \
-out "$CERT_DIR/server.key"
cat > "$CERT_DIR/server.ext" <<'EOF'
basicConstraints=CA:FALSE
keyUsage=digitalSignature,keyEncipherment
extendedKeyUsage=serverAuth
subjectAltName=DNS:localhost,DNS:dev.example.test,IP:127.0.0.1
EOF
openssl req \
-new \
-sha256 \
-key "$CERT_DIR/server.key" \
-subj "/C=SG/O=Nginx Column Lab/CN=dev.example.test" \
-out "$CERT_DIR/server.csr"
openssl x509 \
-req \
-sha256 \
-days 365 \
-in "$CERT_DIR/server.csr" \
-CA "$CERT_DIR/rootCA.crt" \
-CAkey "$CERT_DIR/rootCA.key" \
-CAcreateserial \
-extfile "$CERT_DIR/server.ext" \
-out "$CERT_DIR/server.crt"
chmod 600 "$CERT_DIR/rootCA.key" "$CERT_DIR/server.key"
openssl verify \
-CAfile "$CERT_DIR/rootCA.crt" \
"$CERT_DIR/server.crt"
echo "Generated certificates in $CERT_DIR"

执行:

Terminal window
chmod +x scripts/generate-local-cert.sh
./scripts/generate-local-cert.sh

实验根 CA 只用于本地学习,不要用于生产。

11.7 Nginx 主配置#

路径:nginx/nginx.conf

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format tls_json escape=json
'{'
'"time":"$time_iso8601",'
'"remote_addr":"$remote_addr",'
'"host":"$host",'
'"request":"$request",'
'"status":$status,'
'"request_time":$request_time,'
'"tls_protocol":"$ssl_protocol",'
'"tls_cipher":"$ssl_cipher",'
'"sni":"$ssl_server_name",'
'"http2":"$http2",'
'"upstream_addr":"$upstream_addr",'
'"upstream_status":"$upstream_status"'
'}';
access_log /var/log/nginx/access.log tls_json;
sendfile on;
keepalive_timeout 65s;
include /etc/nginx/conf.d/*.conf;
}

11.8 TLS snippet#

路径:nginx/snippets/tls.conf

ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:TLS:10m;
ssl_session_timeout 10m;
ssl_session_tickets off;
ssl_certificate /etc/nginx/certs/server.crt;
ssl_certificate_key /etc/nginx/certs/server.key;

本地 CA 不提供 OCSP,因此不配置 Stapling。

11.9 代理 Header snippet#

路径:nginx/snippets/proxy-headers.conf

proxy_http_version 1.1;
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_set_header X-Forwarded-Host $host;
proxy_set_header X-Request-ID $request_id;
proxy_connect_timeout 3s;
proxy_send_timeout 15s;
proxy_read_timeout 30s;

11.10 安全 Header snippet#

路径:nginx/snippets/security-headers.conf

add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
# 本地实验默认不启用 HSTS,防止浏览器长期记住测试域名。
# 生产上线前应逐级增加 max-age,并确认全部子域支持 HTTPS。
# add_header Strict-Transport-Security "max-age=300" always;

11.11 站点配置#

路径:nginx/conf.d/app.conf

upstream backend {
server backend:8080;
}
server {
listen 80;
server_name localhost dev.example.test;
return 308 https://$host:8443$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name localhost dev.example.test;
include /etc/nginx/snippets/tls.conf;
include /etc/nginx/snippets/security-headers.conf;
root /usr/share/nginx/html;
index index.html;
location = / {
try_files /index.html =404;
}
location /api/ {
include /etc/nginx/snippets/proxy-headers.conf;
proxy_pass http://backend;
}
}

本实验容器内监听 443,但宿主机映射为 8443,因此 HTTP 跳转显式加入 :8443。生产环境使用标准 443 时应删除该端口。

11.12 静态页面#

路径:nginx/html/index.html

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Nginx HTTPS Lab</title>
</head>
<body>
<h1>Nginx HTTPS Lab</h1>
<p>如果能够看到此页面,说明 TLS 握手和静态资源返回成功。</p>
<p><a href="/api/info">查看代理 Header</a></p>
</body>
</html>

11.13 Docker Compose#

路径:compose.yaml

services:
backend:
build:
context: ./backend
expose:
- "8080"
networks:
- lab
restart: unless-stopped
nginx:
image: nginx:1.30.3
depends_on:
- backend
ports:
- "8080:80"
- "8443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./nginx/snippets:/etc/nginx/snippets:ro
- ./nginx/html:/usr/share/nginx/html:ro
- ./certs:/etc/nginx/certs:ro
networks:
- lab
restart: unless-stopped
networks:
lab:
driver: bridge

11.14 启动#

Terminal window
./scripts/generate-local-cert.sh
docker compose build
docker compose up -d

11.15 配置检查#

Terminal window
docker compose exec nginx nginx -t

预期:

syntax is ok
test is successful

11.16 验证 HTTP 跳转#

Terminal window
curl -I http://localhost:8080/api/info

预期:

HTTP/1.1 308 Permanent Redirect
Location: https://localhost:8443/api/info

11.17 验证 HTTPS#

Terminal window
curl \
--cacert certs/rootCA.crt \
https://localhost:8443/api/info

预期后端看到:

{
"schemeSeenByApp": "http",
"xForwardedProto": "https",
"host": "localhost",
"xForwardedHost": "localhost"
}

schemeSeenByApp 仍可能是 HTTP,因为 Nginx 到 Spring Boot 使用 HTTP;应用应通过可信转发 Header 或 Forwarded Header 策略恢复外部协议。

11.18 验证证书链和 SNI#

Terminal window
openssl s_client \
-connect localhost:8443 \
-servername dev.example.test \
-CAfile certs/rootCA.crt \
-alpn h2,http/1.1 \
-showcerts

关注:

Verification: OK
Protocol : TLSv1.3
ALPN protocol: h2

实际输出取决于 OpenSSL 和客户端支持。

11.19 验证错误域名#

证书 SAN 不包含 wrong.example.test

Terminal window
curl \
--resolve wrong.example.test:8443:127.0.0.1 \
--cacert certs/rootCA.crt \
https://wrong.example.test:8443/api/info

应出现域名不匹配错误。

如果使用 -k 跳过校验,请仅用于排障,不要把它当作修复方案。

11.20 查看日志#

Terminal window
docker compose logs -f nginx

进入容器查看结构化访问日志:

Terminal window
docker compose exec nginx tail -f /var/log/nginx/access.log

11.21 停止和清理#

Terminal window
docker compose down --remove-orphans

如不再使用本地 CA,应删除测试私钥:

Terminal window
rm -rf certs

十二、从开发配置升级到生产配置#

12.1 第一阶段:HTTPS 可用#

目标:

  • 证书与私钥正确;
  • TLS 1.2/1.3;
  • HTTP 跳转 HTTPS;
  • 反向代理正常。
server {
listen 443 ssl;
http2 on;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://backend;
}
}

12.2 第二阶段:会话与连接性能#

ssl_session_cache shared:TLS:20m;
ssl_session_timeout 10m;
ssl_session_tickets off;
keepalive_timeout 65s;

上线前通过压测观察:

  • 完整握手比例;
  • CPU;
  • TLS 握手耗时;
  • 连接复用;
  • Session Resumption 命中。

12.3 第三阶段:协议与证书日志#

log_format tls '$remote_addr $host "$request" $status '
'tls=$ssl_protocol cipher=$ssl_cipher '
'sni=$ssl_server_name h2=$http2 '
'rt=$request_time urt=$upstream_response_time';

用于识别:

  • 旧 TLS 客户端;
  • 异常 Cipher;
  • 未发送 SNI;
  • HTTP/2 协商率;
  • TLS 正常但 upstream 变慢。

12.4 第四阶段:自动续期#

必须包含:

定期 renew
-> 证书文件原子更新
-> nginx -t
-> reload
-> 远程握手验证
-> 到期时间监控
-> 失败告警

仅执行 certbot renew 不是完整上线流程。

12.5 第五阶段:安全 Header#

先添加低风险、通用 Header:

add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

再根据业务验证:

  • Permissions-Policy;
  • Frame 防护;
  • CSP Report-Only;
  • HSTS 短 max-age
  • 最后才是长期 HSTS 和 preload。

12.6 第六阶段:内部链路安全#

根据威胁模型决定:

  • Nginx 到后端 HTTP;
  • Nginx 到后端 HTTPS;
  • mTLS;
  • Service Mesh mTLS;
  • 云私有网络与安全组;
  • 证书托管和 Secret 管理。

不要只因为“内网”就默认可信,也不要在低风险场景盲目增加所有链路 mTLS,导致证书运维超过团队能力。

12.7 第七阶段:发布和回滚#

证书或 TLS 配置发布应:

  1. 在测试环境验证;
  2. 使用 nginx -t
  3. 分批 reload;
  4. 使用不同客户端验证 TLS 1.2、TLS 1.3、HTTP/2;
  5. 检查错误率和握手错误;
  6. 保留旧证书和配置回滚能力;
  7. 避免在证书到期当天才操作。

十三、常见错误与反例#

13.1 只配置叶子证书#

错误现象#

部分浏览器可以访问,部分 Java 客户端或旧设备提示证书链不完整。

错误配置#

ssl_certificate /certs/cert.pem;

根本原因#

服务端没有发送必要中间证书,客户端无法构建到受信任根的链。

修复#

ssl_certificate /certs/fullchain.pem;

验证#

Terminal window
openssl s_client -connect example.com:443 -servername example.com -showcerts

13.2 证书与私钥不匹配#

错误现象#

nginx -t 失败,出现 key values mismatch。

根本原因#

证书公钥和私钥不是一对。

修复#

使用公钥摘要比较,并重新选择正确私钥。

13.3 访问 IP,但证书只有域名#

错误现象#

证书链可信,但客户端提示 hostname mismatch。

根本原因#

证书 SAN 中没有该 IP。

修复#

使用证书覆盖的域名访问,或为需要的 IP 签发包含 IP SAN 的证书。

不要使用 -k 作为生产修复。

13.4 开启 HSTS preload 后仍有 HTTP 子域#

错误现象#

旧系统突然无法访问,浏览器自动尝试 HTTPS。

根本原因#

includeSubDomains; preload 将策略扩展到整个子域体系。

修复#

启用前完成域名资产盘点。已 preload 后的恢复需要长期兼容处理,不能指望立刻撤销。

13.5 HTTP 跳转循环#

错误现象#

浏览器提示 Too Many Redirects。

根本原因#

TLS 在云 LB 终止,Nginx 收到 HTTP 后再次跳转,未正确恢复原始协议。

修复#

只信任受控代理的 X-Forwarded-Proto,统一链路协议策略。

13.6 关闭后端证书验证#

错误配置#

proxy_ssl_verify off;

根本原因#

为绕过自签名或主机名错误,关闭了服务端身份验证。

风险#

内部攻击者可以冒充后端。

修复#

配置内部 CA、正确 SNI 和可信证书:

proxy_ssl_server_name on;
proxy_ssl_name backend.internal.example;
proxy_ssl_verify on;
proxy_ssl_trusted_certificate /etc/nginx/ca/internal-root.pem;

13.7 把 Let’s Encrypt OCSP Stapling 当固定模板#

错误现象#

Nginx 日志持续出现 OCSP Responder 相关错误,或配置没有任何实际效果。

根本原因#

Let’s Encrypt 已于 2025 年停止 OCSP 服务。

修复#

按实际 CA 能力决定,不对当前 Let’s Encrypt 证书启用无效的 OCSP Stapling。

13.8 证书续期成功但线上仍是旧证书#

可能原因#

  • Nginx 没有 reload;
  • reload 失败;
  • 实际加载的是另一条路径;
  • 云 LB/CDN 终止 TLS,更新错了层;
  • 多实例只更新一台;
  • 容器挂载的是旧卷;
  • symlink 更新但容器没有看到。

验证#

Terminal window
openssl s_client \
-connect example.com:443 \
-servername example.com \
</dev/null 2>/dev/null \
| openssl x509 -noout -serial -dates -issuer -subject

13.9 CORS 使用 * 和 Credentials#

错误配置#

Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

结果#

浏览器拒绝带凭证的跨域响应。

修复#

返回明确白名单 Origin,并设置 Vary: Origin

13.10 Nginx 和应用重复添加 CORS Header#

错误现象#

响应出现两个 Access-Control-Allow-Origin,浏览器拒绝。

修复#

明确只有一个层负责 CORS,或在代理层隐藏并重建后端 Header:

proxy_hide_header Access-Control-Allow-Origin;

是否隐藏需要结合整体策略,不应盲目添加。

13.11 全局 CSP 导致前端白屏#

根本原因#

静态 CSP 未覆盖脚本、字体、图片、API 或 WebSocket 来源。

修复#

先使用 Report-Only,收集违规;需要 nonce 时交给应用动态生成。

13.12 证书文件权限过宽#

错误配置#

Terminal window
chmod 777 privkey.pem

风险#

任何本机用户或容器进程都可能读取私钥。

修复#

使用最小权限、只读挂载、受控用户和 Secret 管理。


十四、HTTPS 故障排查流程#

独立插图 11:HTTPS 分层故障排查流程
对应文件:11-https-troubleshooting-flow.png

11 https troubleshooting flow

14.1 第一层:DNS#

Terminal window
dig +short example.com

确认:

  • 是否解析到正确入口;
  • A/AAAA 是否一致;
  • CDN 是否启用;
  • 本地缓存是否陈旧;
  • /etc/hosts 是否覆盖。

14.2 第二层:TCP 和端口#

Terminal window
nc -vz example.com 443
ss -lntp | grep ':443'

检查:

  • 安全组;
  • 防火墙;
  • 云 LB Listener;
  • Docker 端口映射;
  • Nginx 是否监听正确地址;
  • IPv4/IPv6。

14.3 第三层:TLS 握手#

Terminal window
openssl s_client \
-connect example.com:443 \
-servername example.com \
-alpn h2,http/1.1 \
-showcerts

关注:

  • 协议版本;
  • Cipher;
  • ALPN;
  • 证书 Subject/SAN;
  • Issuer;
  • Verify return code;
  • 证书链;
  • 有效期。

14.4 第四层:SNI 和证书选择#

对同一 IP 测试指定域名:

Terminal window
curl --resolve api.example.com:443:203.0.113.10 \
https://api.example.com/

如果不带正确 SNI,可能收到默认 server 证书。

14.5 第五层:HTTP 重定向#

Terminal window
curl -vI http://example.com/path?q=1
curl -vI https://example.com/path?q=1

检查:

  • Location 是否正确;
  • 路径和查询参数是否保留;
  • 是否循环;
  • 是否错误添加端口;
  • API 方法是否会被改变。

14.6 第六层:安全 Header 和 CORS#

Terminal window
curl -I https://example.com/
curl -i -X OPTIONS https://api.example.com/orders \
-H 'Origin: https://app.example.com' \
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: authorization,content-type'

检查:

  • 是否重复 Header;
  • Vary: Origin
  • 凭证与通配符;
  • 预检状态码;
  • CSP 是否阻止资源;
  • Header 是否在错误响应中仍存在。

14.7 第七层:upstream#

HTTPS 握手成功但 API 502/504,问题可能在 Nginx 到后端:

Terminal window
docker compose exec nginx curl -v http://backend:8080/api/info

检查:

  • upstream DNS;
  • 端口;
  • 后端证书;
  • proxy_ssl_name
  • 连接和读取超时;
  • 后端应用日志。

14.8 第八层:证书续期#

检查本地证书:

Terminal window
openssl x509 -in fullchain.pem -noout -dates -serial

检查线上实际证书:

Terminal window
openssl s_client \
-connect example.com:443 \
-servername example.com \
</dev/null 2>/dev/null \
| openssl x509 -noout -dates -serial

对比序列号,确认实际入口已经加载新证书。

14.9 Nginx 配置与日志#

Terminal window
nginx -t
nginx -T
nginx -s reload

容器:

Terminal window
docker compose logs nginx
docker compose exec nginx nginx -T

常见 error log:

  • SSL_CTX_use_PrivateKey failed
  • key values mismatch
  • no suitable key share
  • peer closed connection in SSL handshake
  • certificate verify failed
  • upstream SSL certificate verify error
  • too many redirects 通常在客户端观察。

十五、生产实践与能力边界#

15.1 什么规模下 Nginx 自管证书足够#

适合:

  • 少量域名;
  • 单机或少量虚拟机;
  • 团队具备 Linux 和证书运维能力;
  • 可以稳定执行 ACME、reload 和告警;
  • 证书私钥分发范围可控。

15.2 什么时候使用云证书服务#

  • 多地域;
  • 多云 LB;
  • 大量域名;
  • 需要托管续期;
  • 希望私钥不落到应用主机;
  • CDN/WAF 已经终止 TLS;
  • 需要统一审计和权限管理。

15.3 什么时候需要 mTLS#

  • 机器对机器;
  • 管理面;
  • 高价值内部 API;
  • 零信任网络;
  • 合规要求双向认证。

不适合:

  • 普通公网用户登录;
  • 团队没有证书生命周期管理;
  • 无法处理移动端证书分发;
  • 把证书 Subject 直接当业务权限。

15.4 什么时候需要 WAF#

  • 公网高风险业务;
  • 频繁遭受自动化攻击;
  • 需要托管规则和威胁情报;
  • 合规要求;
  • 应用修复周期较长,需要临时虚拟补丁。

Nginx 基础限制不能替代完整 WAF。

15.5 Nginx 不应承担的安全职责#

  • 密码哈希;
  • 用户会话最终判断;
  • 订单权限;
  • 数据脱敏;
  • SQL 参数化;
  • 业务级 CSRF Token;
  • 租户隔离;
  • 审批流程;
  • 密钥管理系统;
  • 漏洞修复。

15.6 安全配置不应成为复制粘贴清单#

TLS 和安全 Header 都存在上下文:

  • 客户端兼容性;
  • 域名结构;
  • CA 能力;
  • 浏览器行为;
  • 前端资源;
  • 云 LB 链路;
  • 合规要求;
  • 应用身份模型。

正确做法是基于威胁模型、实验和监控逐步收紧,而不是一次性复制“满分配置”。


十六、面试与复习问题#

16.1 十道核心问题#

1. HTTPS 与 TLS 是什么关系?#

HTTPS 是运行在 TLS 安全通道上的 HTTP。TLS 提供加密、完整性和身份验证,HTTP 仍负责请求方法、Header、状态码和业务语义。

2. 证书公钥和私钥分别做什么?#

证书公开携带公钥和域名身份;服务端使用私钥证明自己持有对应密钥。私钥不能发送给客户端。

3. TLS 1.3 中证书私钥是否直接加密全部业务数据?#

不是。证书私钥主要用于身份签名,业务数据使用握手派生出的对称会话密钥加密。

4. SNI 的作用是什么?#

客户端在 TLS ClientHello 中声明目标域名,使同一 IP/443 上的 Nginx 能在 HTTP 请求解密前选择正确证书和虚拟主机。

5. ALPN 的作用是什么?#

在 TLS 握手中协商上层协议,例如 HTTP/2 的 h2http/1.1

6. 为什么要使用 fullchain.pem#

它包含叶子证书和必要中间证书,帮助客户端构建到受信任根的完整证书链。

7. HSTS 为什么只能在 HTTPS 响应中设置?#

如果 HTTP 响应中的 HSTS 被接受,中间人可以伪造或删除策略。浏览器只信任通过安全连接获得的 HSTS。

8. HTTP-01 与 DNS-01 的主要区别?#

HTTP-01 通过 80 端口文件证明控制权,不能签发通配符;DNS-01 通过 TXT 记录验证,可签发通配符,但需要安全的 DNS API 自动化。

9. CORS 是否是鉴权?#

不是。CORS 主要控制浏览器脚本读取跨域响应,curl 和后端调用不受同样限制。服务端仍需认证和授权。

10. 为什么 HTTPS 正常仍可能返回 502?#

TLS 只覆盖客户端到 Nginx。握手成功后,Nginx 到后端仍可能 DNS 失败、连接拒绝、证书验证失败或超时。

16.2 五道场景分析题#

场景 1:云 LB 终止 TLS,Nginx 不断跳转 HTTPS#

原因:Nginx 看到内部 HTTP,未识别原始协议。应只信任云 LB 的 X-Forwarded-Proto,并调整跳转逻辑。

场景 2:某些客户端报证书错误,Chrome 正常#

可能是中间证书链不完整。Chrome 可能通过缓存补链,Java 或旧设备无法补链。检查 fullchain.pemopenssl s_client -showcerts

场景 3:续期脚本成功,但线上证书仍旧#

确认 TLS 是否在 CDN/云 LB 终止;检查 Nginx 实际证书路径、reload 结果、多节点同步和线上证书序列号。

浏览器会拒绝。必须返回明确 Origin、Allow-Credentials: trueVary: Origin,并处理 CSRF。

场景 5:内部 HTTPS 后端使用自签名证书,能否关闭验证?#

不应。应建立内部 CA、配置 proxy_ssl_trusted_certificate、正确 SNI 和主机名。

16.3 三道配置排错题#

排错题 1#

server {
listen 443 ssl http2;
}

问题:为什么新版本出现弃用提示?

答案:从 1.25.1 起推荐使用独立的 http2 on;

排错题 2#

ssl_certificate /certs/cert.pem;
ssl_certificate_key /certs/other-domain.key;

问题:Nginx 为什么无法启动?

答案:证书公钥与私钥不匹配。

排错题 3#

add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Credentials "true" always;

问题:为什么浏览器仍提示 CORS?

答案:凭证请求不能使用通配符 Origin。


十七、本文总结#

本文完成了 Nginx 从“流量入口”到“安全入口”的升级:

  1. HTTP 明文无法提供机密性、完整性和可靠身份验证;
  2. TLS 使用证书和签名验证服务端身份,使用 ECDHE 协商秘密,再使用对称 AEAD 加密业务数据;
  3. TLS 1.3 中证书私钥主要用于身份签名,不是直接加密全部 HTTP 内容;
  4. Nginx 在解密 HTTP 前先根据 SNI 选择证书,并通过 ALPN 协商 HTTP/2;
  5. fullchain.pem 应包含叶子证书和中间链,私钥必须最小权限保护;
  6. 新配置应使用 http2 on;,并默认仅允许 TLS 1.2/1.3;
  7. Session Cache、Session Ticket 和 0-RTT 都有性能与安全权衡;
  8. OCSP Stapling 依赖 CA 能力,当前 Let’s Encrypt 已停止 OCSP 服务;
  9. HTTP 重定向解决当前请求,HSTS 影响未来请求,includeSubDomainspreload 必须谨慎;
  10. ACME 自动化必须包括挑战验证、持久化、nginx -t、reload、远程验证和到期告警;
  11. Nginx ACME 模块是可选模块,不能假设所有安装默认具备;
  12. TLS 可以在 CDN、云 LB、Nginx 或应用终止,内部链路是否再加密取决于信任边界;
  13. 安全 Header 需要区分入口统一策略和应用动态策略;
  14. CORS 是浏览器访问控制,不是服务端鉴权;
  15. HTTPS 故障应按 DNS、TCP、TLS、虚拟主机、HTTP、Header、upstream 和续期链路逐层排查。

下一篇将进入缓存、压缩与静态资源优化,继续分析浏览器缓存、Nginx 静态资源、proxy_cache、gzip/Brotli 和 CDN 如何协作。


参考资料#

以下资料均于 2026-07-07 访问:

  1. Nginx 官方下载页:Mainline 1.31.2、Stable 1.30.3
    https://nginx.org/en/download.html
  2. Nginx 官方文档:Module ngx_http_ssl_module
    https://nginx.org/en/docs/http/ngx_http_ssl_module.html
  3. Nginx 官方文档:Configuring HTTPS servers
    https://nginx.org/en/docs/http/configuring_https_servers.html
  4. Nginx 官方文档:Module ngx_http_v2_module
    https://nginx.org/en/docs/http/ngx_http_v2_module.html
  5. Nginx 官方文档:Module ngx_http_v3_module
    https://nginx.org/en/docs/http/ngx_http_v3_module.html
  6. Nginx 官方文档:Support for QUIC and HTTP/3
    https://nginx.org/en/docs/quic.html
  7. Nginx 官方文档:Module ngx_http_acme_module
    https://nginx.org/en/docs/http/ngx_http_acme_module.html
  8. Nginx 官方文档:Module ngx_http_proxy_module
    https://nginx.org/en/docs/http/ngx_http_proxy_module.html
  9. Let’s Encrypt 官方文档:Challenge Types
    https://letsencrypt.org/docs/challenge-types/
  10. Let’s Encrypt 官方文档:Keep Port 80 Open
    https://letsencrypt.org/docs/allow-port-80/
  11. Let’s Encrypt 官方公告:OCSP Service Has Reached End of Life
    https://letsencrypt.org/2025/08/06/ocsp-service-has-reached-end-of-life
  12. Certbot 官方网站与使用说明
    https://certbot.eff.org/
  13. MDN:Strict-Transport-Security
    https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Strict-Transport-Security
  14. MDN:Content Security Policy
    https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP
  15. MDN:Cross-Origin Resource Sharing
    https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS
  16. MDN:Access-Control-Allow-Origin
    https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Allow-Origin
  17. MDN:X-Content-Type-Options
    https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Content-Type-Options
  18. MDN:Referrer-Policy
    https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Referrer-Policy
  19. MDN:Permissions-Policy
    https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Permissions-Policy
  20. Spring Boot 官方项目页
    https://spring.io/projects/spring-boot

文章验收清单#

内容完整性#

  • 解释 HTTP 风险与 TLS 安全属性;
  • 讲清 TLS 1.3 握手;
  • 讲清 ECDHE、签名和会话密钥;
  • 讲清证书链、SAN 和私钥;
  • 覆盖 Nginx HTTPS、SNI、ALPN 和 HTTP/2;
  • 覆盖 Session Cache、Ticket 和 0-RTT;
  • 覆盖 HTTP 跳转、HSTS 和 preload;
  • 覆盖 ACME、Certbot 和自动续期;
  • 覆盖 TLS 终止、再加密和 mTLS;
  • 覆盖安全 Header 与职责边界;
  • 覆盖 Host、请求体和基础入口防护;
  • 覆盖 CORS、预检和凭证;
  • 提供 Docker Compose + Spring Boot 完整实验;
  • 提供反例、排查流程和面试题。

技术准确性#

  • 区分 TLS 1.2 和 TLS 1.3 配置边界;
  • 未使用 TLS 1.0/1.1 作为生产基线;
  • 未使用旧版 listen ... http2 作为推荐写法;
  • 未把 HTTPS 描述为完整应用安全;
  • 未把证书私钥描述为直接加密全部数据;
  • 未把根证书要求服务端固定发送;
  • 标注 HTTP/3 实验性和构建边界;
  • 标注 Nginx ACME 为可选模块;
  • 标注 Let’s Encrypt 已停止 OCSP;
  • 未把 CORS 描述为鉴权;
  • 未使用通配符 Origin 搭配 Credentials;
  • 未通过关闭后端证书验证解决问题。

配置可运行性#

  • Docker 镜像固定为 Nginx 1.30.3;
  • 提供完整目录结构;
  • 提供本地 CA 和 SAN 证书脚本;
  • 提供 Nginx 主配置和 snippets;
  • 提供 Spring Boot 后端代码;
  • 提供 Dockerfile 与 Compose;
  • 提供启动、验证、日志和清理命令;
  • 提供 nginx -t
  • 提供 openssl s_clientcurl --cacert 验证;
  • 所有插图以独立 PNG 文件交付,不嵌入 Markdown。
第 5 篇:Nginx HTTPS、证书与安全入口
https://jupiter-ws.cn/posts/backend/nginx/05_nginx_https_certificate_security_entry/
作者
Jupiter
发布于
2026-07-07
许可协议
CC BY-NC-SA 4.0