给站点配好 HTTPS,浏览器地址栏出现小锁头,很多人就以为搞定了。但拿去 SSL Labs(ssllabs.com/ssltest)一测,评分可能只有 B——证书链不全导致部分客户端报错、支持了早已被攻破的 TLS 1.0、握手慢得肉眼可见。这篇按「先能用、再安全、后提速」的顺序,把一份 Nginx HTTPS 配置从「有锁头」调到 SSL Labs A+,每一项都讲清楚为什么。
假设证书已经签好(Let’s Encrypt 或商业 CA),最小配置长这样:
|
1 2 3 4 5 6 7 8 9 |
server { listen 443 ssl; server_name example.com; ssl_certificate /etc/nginx/ssl/example.com.crt; ssl_certificate_key /etc/nginx/ssl/example.com.key; location / { root /var/www/html; } } |
nginx -t 通过、nginx -s reload,浏览器能打开 https 了。但这份配置有一堆问题:HTTP 还能访问、可能用了不全的证书链、默认协议里带着 TLS 1.0/1.1。下面逐个修。
最隐蔽的坑是证书链缺中间证书。你在自己浏览器打开好好的,但某些安卓机、Java 客户端、curl 却报 unable to verify the first certificate。因为浏览器会缓存或自动补全中间证书,而很多客户端不会——它们需要你在服务端就把完整链发下去。
关键点:ssl_certificate 指向的文件必须是「站点证书 + 中间证书」拼在一起的全链文件(顺序是站点证书在前、中间证书在后),不是只有站点证书那一张。
|
1 2 3 |
# Let's Encrypt 的 fullchain.pem 已经是全链,直接用它 # 商业 CA 通常给你 your_domain.crt 和 intermediate.crt 两个文件,需手动拼: cat your_domain.crt intermediate.crt > /etc/nginx/ssl/example.com.crt |
|
1 2 3 |
# 指向全链文件,不是单张站点证书 ssl_certificate /etc/nginx/ssl/example.com.crt; # fullchain ssl_certificate_key /etc/nginx/ssl/example.com.key; |
验证链是否完整,用 openssl 连一下看 Verify return code:
|
1 2 3 4 |
# 加 -servername 触发 SNI,否则多站点服务器可能返回错证书 echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \ | grep -i "verify" # 期望看到 "Verify return code: 0 (ok)" |
Verify return code: 0 (ok) 才算链完整。如果是 21 (unable to verify...),就是中间证书没拼上。
HTTP/2 能多路复用、头部压缩,同一域名大量小资源的站点提速明显。老写法是 listen 443 ssl http2;,但从 Nginx 1.25.1 起推荐用独立的 http2 指令(写在 listen 后面那种在新版会告警):
|
1 2 3 4 5 6 |
server { listen 443 ssl; http2 on; # Nginx 1.25.1+ 推荐写法 server_name example.com; # ... } |
如果你的 Nginx 是 1.25 之前的版本,还得用 listen 443 ssl http2;。用 nginx -v 看版本决定写哪种。注意 HTTP/2 强依赖 HTTPS——浏览器只在 TLS 上启用 HTTP/2,所以它必须和 ssl 一起配。
SSL Labs 给 A 以下评分,十有八九是因为还开着 TLS 1.0/1.1(已被 PCI DSS 淘汰)或弱加密套件。安全基线配置:
|
1 2 3 4 5 6 7 8 9 10 |
# 只留 TLS 1.2 和 1.3,砍掉 1.0/1.1 ssl_protocols TLSv1.2 TLSv1.3; # 加密套件:优先前向保密(ECDHE)的强套件 ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305; # TLS 1.3 的套件由客户端优选更优,1.2 场景保留服务端优先 ssl_prefer_server_ciphers off; # 复用握手结果,减少重复握手开销 ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; ssl_session_tickets off; # 关掉 session ticket,避免前向保密被削弱 |
几个要点:ssl_protocols 去掉 1.0/1.1 是上 A 的硬门槛;ECDHE 开头的套件提供前向保密(即使私钥泄露,历史流量也解不开),这是 A+ 必备;ssl_session_cache 让重复访问的客户端跳过完整握手,直接提速。
浏览器为了确认你的证书没被吊销,默认要自己去 CA 的 OCSP 服务器查一次,这一步既慢又泄露用户访问了哪个站。OCSP stapling 让 Nginx 代替浏览器提前查好,握手时直接把「证书有效」的凭证一起发下去,省掉浏览器那次外网请求:
|
1 2 3 4 5 6 7 |
ssl_stapling on; ssl_stapling_verify on; # 验证 stapling 响应需要 CA 的根+中间证书链 ssl_trusted_certificate /etc/nginx/ssl/ca-chain.pem; # Nginx 要能解析 OCSP 服务器域名,配个 DNS resolver 223.5.5.5 8.8.8.8 valid=300s; resolver_timeout 5s; |
验证 stapling 是否生效:
|
1 2 3 |
echo | openssl s_client -connect example.com:443 -servername example.com -status 2>/dev/null \ | grep -A 3 "OCSP Response Status" # 生效会看到 "OCSP Response Status: successful" |
注意 ssl_stapling_verify on 需要 ssl_trusted_certificate 指向包含根证书+中间证书的链文件(和站点证书那个 fullchain 不完全一样,这个要带根)。漏了 resolver,Nginx 解析不了 OCSP 域名,stapling 会静默失败。
即使你把 HTTP 跳转到了 HTTPS,用户第一次输 http:// 的那一跳仍是明文,存在中间人降级攻击的窗口。HSTS 让浏览器记住「这个站以后一律用 HTTPS」,连第一跳都省了:
|
1 2 |
# max-age 一年;includeSubDomains 覆盖子域;preload 可申请进浏览器预置名单 add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; |
always 参数很关键——不加的话,4xx/5xx 错误响应里不会带这个头。上 HSTS 前务必确认所有子域都已支持 HTTPS,否则 includeSubDomains 会把没配证书的子域也锁死成只能 HTTPS,直接打不开。这是不可逆的坑,先小范围 max-age 测试再拉长。
最后把 80 端口的明文请求 301 到 HTTPS,别让用户有机会走明文:
|
1 2 3 4 5 6 |
server { listen 80; server_name example.com; # 全部 301 到 https,HSTS 才有意义 return 301 https://$host$request_uri; } |
改完 nginx -t 确认语法、nginx -s reload 生效,再去 SSL Labs 重测,证书链、协议、前向保密、HSTS、OCSP 全绿就能拿到 A+。