编程语言

ERR_TLS_CERT_ALTNAME_INVALID:Node.js 证书域名不匹配

作者: · 最后核实

报错信息ERR_TLS_CERT_ALTNAME_INVALID

ERR_TLS_CERT_ALTNAME_INVALID 的意思是:你的 Node.js 代码连接的域名或 IP,不在服务器证书的“使用者可选名称”(SAN)扩展里。 证书可能是可信的、也没过期,只是它属于别的名字。要修的是访问地址、SNI 名称或者证书本身,而不是关闭校验。

这条报错是什么意思

这条报错说明证书可信,但证书里的名字和你连接的地址对不上。证书链校验通过之后,Node.js 会调用 tls.checkServerIdentity(hostname, cert) 比对请求的主机名和证书。全都对不上时,就抛出 code 为 'ERR_TLS_CERT_ALTNAME_INVALID' 的错误,附带 reason、host、cert 属性,消息类似:

Error [ERR_TLS_CERT_ALTNAME_INVALID]: Hostname/IP does not match certificate's altnames: Host: example.com. is not in the cert's altnames: DNS:www.example.com

冒号后面那段就是 reason,Node.js 会给出这几种:

  • Host: <域名>. is not in the cert's altnames: DNS:...:证书里的 DNS 名称不包含这个主机名。
  • IP: <地址> is not in the cert's list: ...:你用 IP 访问,但证书里没有这个 IP。
  • Host: <域名>. is not cert's CN: ...:证书没有 DNS 类型的 SAN,Node.js 退回去比对 Common Name,也没对上。
  • Cert does not contain a DNS name:既没有 DNS SAN,也没有 Common Name 可比。

浏览器里对应的报错是 NET::ERR_CERT_COMMON_NAME_INVALID,原理和修法相同。

常见原因(按出现频率排序)

最常见的是访问地址不在证书覆盖范围内,比如访问裸域名而证书只签了 www。

1. 访问地址不在证书覆盖范围内

最典型的是用 https://example.com 访问,而证书只签了 www.example.com;或者测试环境用了生产环境的证书。通配符还有一个限制:*.example.com 只覆盖一级,既不匹配 example.com,也不匹配 a.b.example.com。

  • 确认: 列出服务器实际出示的证书里有哪些名字:
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -ext subjectAltName

也可以把证书粘贴到证书解析工具里查看。

  • 修复: 改用证书覆盖的名字,或者重新签发包含所有域名的证书,例如 certbot certonly -d example.com -d www.example.com。在阿里云、腾讯云等平台申请证书时,提交前先确认这张证书会覆盖哪些名字,裸域名和 www 是否都包含在内。

2. 没发正确的 SNI,服务器返回了默认证书

一台服务器托管多个网站时,会根据 ClientHello 里的 SNI 选择证书。客户端用 IP 连接、走隧道、或者自定义了 DNS 解析,却没有发送正确的 SNI,就会拿到默认站点的证书。

  • 确认: 用 openssl s_client 分别带和不带 -servername 连接,证书主题不同就说明是 SNI 在起作用。
  • 修复: 显式传入真实域名:
const tls = require('node:tls');
const socket = tls.connect({ host: '203.0.113.10', port: 443, servername: 'example.com' }, () => {
  console.log('authorized:', socket.authorized);
  socket.end();
});

3. 直接用 IP 访问

https://203.0.113.10/ 除非证书里有这个 IP,否则必然失败。通过 IP 访问的内网服务、Docker 或 Kubernetes 的服务名(https://api:8443)、localhost 都常遇到这个问题。

  • 修复: 用证书里有的域名访问;内网证书则把 IP 或服务名加进 SAN 列表重新签发。

4. CDN、负载均衡或云平台返回了平台自己的证书

自定义域名已经解析到 CDN、对象存储或云托管平台,但还没在控制台上传这个域名的证书,平台就会返回它自己的默认证书,SAN 里是平台的域名。国内用对象存储绑定自定义域名、CDN 加速域名时很常见。

  • 确认: SAN 列表里是云厂商的域名,而不是你的。
  • 修复: 在对应产品的控制台里为这个域名上传或申请证书,然后重新测试。

5. 新增域名后没更新证书

服务器还在用新增域名之前的旧证书。按完整的域名列表续期或重签,并重载服务器。用 certbot 的话,certbot certificates 可以列出每张证书包含的域名;新证书签好后别忘了重载 Nginx,否则进程里还是旧证书。负载均衡和 CDN 上的证书要单独更新,它们不会自动跟着源站变。

推荐的排查顺序

排查从报错里的 reason 开始,大多数情况 3 步就能定位。先看它是 Host: 开头还是 IP: 开头,后面列出的名字是不是你的域名。名字完全陌生(比如是云厂商或另一个网站的域名),优先怀疑 SNI 和默认证书;名字是你的,只是少了裸域名或 www,就是证书覆盖范围的问题;你访问的是 IP,就改成域名。然后用上面的 openssl s_client 命令带上 -servername 再看一次,确认服务器在正确 SNI 下出示的证书。

分平台修复命令

修复分两步:先查看服务器实际发来的证书,再按真实域名连接和校验。

Linux / macOS(查看服务器发来的证书):

openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName

Windows PowerShell(用 Node.js 做同样的检查):

node -e "const s=require('tls').connect(443,'example.com',{servername:'example.com'},()=>{console.log(s.getPeerCertificate().subjectaltname);s.end()})"

连接 IP,但按真实域名校验(校验完整保留):

const https = require('node:https');
const tls = require('node:tls');
https.get({
  host: '203.0.113.10',
  servername: 'internal.example.com',
  headers: { Host: 'internal.example.com' },
  checkServerIdentity: (_host, cert) => tls.checkServerIdentity('internal.example.com', cert),
}, res => console.log(res.statusCode));

Nginx 托管多个站点(含宝塔面板): 确认每个 server 块的 server_name 和 ssl_certificate 对应正确,默认站点不会抢走你的域名。

不推荐:关闭证书校验

rejectUnauthorized: false、NODE_TLS_REJECT_UNAUTHORIZED=0、checkServerIdentity: () => undefined 都能让报错消失,代价是失去对服务器身份的验证。前两个接受任何证书;第三个虽然保留了证书链校验,却去掉了域名检查,危害同样大:攻击者只要拥有任何一个域名的任何受信任证书,就能冒充你的服务器,读取 token、密码和接口数据。

安全的替代办法:用正确的域名访问;设置 servername 让 SNI 选中正确的证书;签发证书时把要用的名字都写进去;必须用 IP 连接时,像上面那样用 tls.checkServerIdentity(expectedName, cert) 按预期域名校验。

怎么确认已经修好

下面的命令输出状态码和 true,就说明证书校验已经通过。

node -e "require('https').get('https://example.com', r => { console.log(r.statusCode, r.socket.authorized); r.resume(); }).on('error', e => console.error(e.code, e.reason || e.message))"

应当输出状态码和 true。再把裸域名和 www 分别放进 SSL 检测工具确认都能匹配;规划新证书要包含哪些名字时,可以先了解使用者可选名称(SAN)。

常见问题

*.example.com 的通配符证书能用于 example.com 吗?

不能。通配符只匹配最左边的一级,*.example.com 能覆盖 api.example.com,但覆盖不了 example.com,也覆盖不了 v1.api.example.com。裸域名要作为单独的名字写进证书,更深一级的子域名要单独申请。

能用 IP 地址走 HTTPS 吗?

只有证书里包含这个 IP(IP 类型的 subjectAltName)才行;否则 Node.js 会报另一种原因 IP: 203.0.113.10 is not in the cert's list。大多数公开证书只包含域名,所以请用域名访问,或者用能签发 IP 证书的内网 CA。

自定义 checkServerIdentity 安全吗?

只要它仍然在校验就安全。无条件返回 undefined 等于关掉了域名检查,任何域名的任何受信任证书都会被接受。安全的写法是用你预期的名字调用 tls.checkServerIdentity(expectedName, cert),比如连接的是 IP,但你知道服务的真实域名。

为什么同一张证书 Node.js 能用,Go 却报错?

证书没有任何 DNS 类型的 SAN 时,Node.js 会退回比对主题里的 Common Name;Go 1.15 起忽略 Common Name,只有 CN 的证书在 Go 里会失败,见 Go x509 legacy Common Name。重新签发带 SAN 的证书,各种客户端的结果才会一致。

资料来源

  1. Node.js docs: Errors — ERR_TLS_CERT_ALTNAME_INVALID
  2. Node.js docs: tls.checkServerIdentity() and tls.connect() servername
  3. Node.js source: lib/tls.js (checkServerIdentity)
  4. RFC 9525: Service Identity in TLS