Nginx 的 TLS 排错:SNI、证书链与 443 是谁在听
一台机器上同时跑三四个站点是很常见的事。为了省一个 IP,大家都挤在 443 上,靠 SNI 分流。这时候如果某个域名握手报错,第一反应通常是去看证书有没有过期——但十次里有七八次,问题不在这里。
先确认站在哪块 server 里
server_name 匹配不上时,Nginx 会把请求交给该端口的 default_server。如果 default_server 是一块 return 444 的兜底配置,你看到的现象就是连接被直接掐断,而不是一个像样的错误页。
# 看 Nginx 自己怎么理解这些配置(会展开 include)
nginx -T | grep -nE '^\s*(listen|server_name|ssl_certificate) '
# 用目标 SNI 握手,看实际返回哪张证书
echo | openssl s_client -connect 127.0.0.1:443 \
-servername example.com 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
如果 subject 不是你期望的域名,说明要么 SNI 没传到,要么这块 server 没匹配上。前者多半是中间还有一层转发(CDN、负载均衡、或者另一个监听 443 的进程),后者通常是拼写、通配符优先级,或者 server_name 后面多了个点。
listen 443 ssl 又有 listen 443 ssl http2,它们的协议能力并不一致。http2 写在 listen 行里的老语法在新版本里已经弃用,混用时行为很难预期。证书链:单张证书不等于能验证
把 fullchain.pem 写成了 cert.pem,浏览器有时能过(因为它会去补中间证书),但 curl、Go、Java 这些严格的客户端会直接报 unable to get local issuer certificate。
# 看服务端到底发了几张证书
echo | openssl s_client -connect example.com:443 -servername example.com \
-showcerts 2>/dev/null | grep -c 'BEGIN CERTIFICATE'
# 应该是 2 张(站点证书 + 中间证书),1 张就是链不全
链不全的修法很简单:签发工具一般都会输出 fullchain.pem,ssl_certificate 指向它就行,不要指向只有叶子证书的那个文件。
ALPN 与 HTTP/2 的坑
有些客户端(尤其是自己写的健康检查)只支持 HTTP/1.1。如果 server 只协商 h2,握手会失败在 ALPN 阶段,日志里连一条 access 记录都没有。用 openssl s_client -alpn http/1.1 手动指定一次,能快速区分是「协议没谈拢」还是「证书不对」。
echo | openssl s_client -connect example.com:443 \
-servername example.com -alpn http/1.1 2>/dev/null | grep 'ALPN protocol'
反向代理场景下的 421
Nginx 1.19 之后对 Host 和 SNI 不一致的连接会返回 421 Misdirected Request。前面挂了一层 CDN、回源时 Host 被改写,很容易命中。要么让回源保持同一个域名,要么在明确知道来源可信时关掉这项检查:
server {
listen 8443 ssl;
server_name example.com;
# 回源由固定的一层代理发起,可以放宽
http2 on;
# 旧版本没有这个指令,靠 http2 指令控制
}
小结
- 握手失败先看
nginx -T,确认请求落在哪块 server。 - 证书问题优先怀疑链不完整,而不是过期。
- 用
openssl s_client分别试 SNI 和 ALPN,能把问题切得很细。