news 2026/10/1 4:18:52

Go 1.15证书校验变化:从CN到SAN,解决x509报错与自签证书问题

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Go 1.15证书校验变化:从CN到SAN,解决x509报错与自签证书问题

先讲个真实场景:早上刚到工位,组里同事就甩过来一条报错,说 Go 写的内部工具连不上新部署的服务,日志里就这么一句话:

verify certificate: x509: certificate relies on legacy Common Name field, use SANs instead

这条报错现在越来越常见,尤其当你用 Go 1.15 之后的版本,去访问一些“历史遗留”的 HTTPS 服务,或者自签证书没配好 SAN 的内部系统。它的大白话意思是:你证书里的域名信息只写在 Common Name(CN)字段里,而 Go 现在默认只看 Subject Alternative Name(SAN,主题备用名称),找不到就直接拒绝连接。

这篇文章我打算把这个错误彻底讲透:为什么 Go 突然翻脸、怎么确认证书里到底有没有 SAN、四种修复方案怎么选,以及我在实际排障中踩过的坑和总结的排查套路。不管你是维护老系统的运维、写 Go 服务端的开发,还是刚接触自签证书的新手,这篇文章都能帮你少走弯路。

1. 这个报错的来龙去脉:Go 1.15 为什么和 CN 过不去

1.1 CN 与 SAN 到底差在哪

要理解这个报错,先得搞清楚 X.509 证书里两个字段的分工。Common Name 是证书里的“常用名”,在 SSL/TLS 证书里通常填域名,比如CN=example.com。而 Subject Alternative Name 是证书的扩展字段,里面可以列一堆备用名称,包括域名(DNS)、IP 地址(IP)、邮箱(email)等。

你可能会想:既然 CN 都写了域名,为什么还要 SAN?问题就出在“CN 能写的东西太不严谨”。一个 CN 只能填一个值,虽然老浏览器也支持通过通配符匹配,但它本质上是个“给人看的名称”,不是专门给机器做域名校验用的。SAN 才是标准体系里真正用来做“主机名验证”的扩展字段,一个证书可以同时列DNS:example.com、DNS:www.example.com、IP:127.0.0.1,清清楚楚。

打个比方:CN 像快递单上“收件人”一栏,SAN 像“收件地址列表”。以前快递员只看收件人名字就敢送,现在规则改了,必须核对地址列表里的详细地址,地址列表为空就拒收。这个变化在浏览器圈早就完成了,但到了 2020 年才轮到 Go。

1.2 浏览器和 Go 的“分道扬镳”

早期的 SSL 证书标准里,CN 是校验主机名的主要依据,RFC 6125 等标准出来后,正式把主机名校验挪到了 SAN。浏览器厂商跟进得比较早,主流浏览器从 2017 年左右就开始强制要求证书必须包含 SAN,否则直接提示不安全。

Go 语言则是在 1.15 版本(2020 年 8 月发布)做出了一个破坏性变更:x509 包在校验证书时不再回退到 CN 字段,如果证书里没有 SAN,就返回x509: certificate relies on legacy Common Name field, use SANs instead这个错误。

这个变更影响面很大,因为很多内部系统的自签证书、老一代 CA 签发的证书都是只填 CN 不填 SAN 的。更麻烦的是,你写好的 Go 程序在 Go 1.14 及更早版本跑得好好的,一升级到 1.15+,连不上旧服务,而且报错信息对新手很不友好。我当时就遇到过客户环境里 Docker Registry 连不上旧仓库证书的问题,原因就是 Docker 的 Go 运行时升级后,对证书的要求变严了。

1.3 你会踩到它的真实场景

这个报错最常出现在几类场景:

  • Kubernetes 集群里组件之间通过 HTTPS 通信,证书只用了 CN 没配 SAN,kubelet 升级后就报错。
  • Docker 配置了私有镜像仓库,仓库证书是自签的且没有 SAN,pull/push 镜像时失败。
  • Go 微服务调用内部 API,对方用的是老 CA 签发的证书,只写了 Common Name。
  • 各种调用外部 HTTPS 接口的 Go CLI 工具,比如grpcurl、go-resty类客户端。
  • 内部监控系统(Prometheus、Grafana)抓取 HTTPS 端点时发现证书不合法。

这类报错的特点就是:你从浏览器访问这个服务可能一切正常,因为浏览器要求的 SAN 校验规则早已普及;但换成 Go 写的程序一访问就挂。原因就是 Go 的运行时策略跟浏览器不完全一样,而且 Go 1.15 之后变得异常严格。

2. 先诊断再动手:如何确认证书里有没有 SAN

2.1 一条命令看清证书全貌

收到报错先别急着改代码,第一步肯定是确认服务端证书到底长什么样。用 OpenSSL 看证书详细信息:

openssl x509 -in server.crt -noout -text

重点看输出里的这两段:

Subject: C=CN, O=Internal CA, CN=myserver.example.com ... X509v3 extensions: X509v3 Subject Alternative Name: DNS:myserver.example.com, DNS:www.myserver.example.com

如果X509v3 extensions下面没有X509v3 Subject Alternative Name这一段,那就说明证书里确实没有 SAN,报错必然会出现。

如果只想快速看 SAN,可以精简成一行:

openssl x509 -in server.crt -noout -ext subjectAltName

输出类似:

X509v3 Subject Alternative Name: DNS:myserver.example.com, IP Address:127.0.0.1

有输出说明证书带 SAN,没输出说明证书没带。这是排查这类问题的“第一板斧”,建议牢牢记住。

2.2 pem、crt、key 文件怎么区分

排查时经常被文件后缀搞晕,这里顺带说清楚。.pem、.crt、.cer通常都是 PEM 格式的证书文件,里面是 Base64 编码的证书内容,开头长这样:

-----BEGIN CERTIFICATE-----

.key是私钥文件,开头通常是:

-----BEGIN PRIVATE KEY-----

有的老系统会把证书和私钥放在同一个.pem文件里,中间用不同的BEGIN标记区分。查看这类文件时,用openssl x509会报错,因为 OpenSSL 默认只解析第一个BEGIN CERTIFICATE块。可以用awk先拆分:

awk 'BEGIN {c=0} /BEGIN CERTIFICATE/{c++} {print > ("cert_" c ".pem")}' combined.pem

不过实际排查中,更常用的手法是用openssl s_client直接连接远程服务,把线上证书拉下来看:

openssl s_client -connect myserver.example.com:443 -servername myserver.example.com 2>/dev/null | openssl x509 -noout -text

这里-servername参数会带上 SNI,确保拿到的是对应域名的证书。这个方法在排查线上问题时非常有用,不用登录服务器就能看到对方证书的 SAN 情况。

2.3 确认“谁的证书”出了问题

有一点容易混淆:报错可能来自客户端校验服务端证书,也可能来自服务端校验客户端证书(双向 TLS)。判断方法看报错出现在哪一侧。

  • 客户端日志报x509: certificate relies on legacy Common Name field,说明是客户端不信任服务端证书,查服务端的证书。
  • 服务端日志报这个错,说明是服务端不信任客户端证书,查客户端证书。
  • 代理或网关(nginx、Envoy)报类似错误,则要看代理和后端之间的 TLS 配置。

另外,Go 报错信息里的verify certificate:前缀一般来自 gRPC 的 TLS 握手,或者 、http.Client的 HTTPS 请求。这时候先确认你访问的域名和证书里的 SAN 是否匹配,因为即使证书有 SAN,只要 SAN 里没有你要访问的域名,Go 也会拒绝。

3. 四种修复方案:从最快到最彻底

3.1 方案一:OpenSSL 重新生成带 SAN 的证书

如果你能控制服务器证书,最推荐的做法是直接用 OpenSSL 重新生成一份带 SAN 的证书。自签证书一条命令就能搞定:

openssl req -x509 -newkey rsa:2048 -nodes \ -keyout server.key -out server.crt \ -days 365 \ -subj "/CN=myserver.example.com" \ -addext "subjectAltName=DNS:myserver.example.com,DNS:www.myserver.example.com,IP:127.0.0.1,IP:192.168.1.10"

解释一下参数:

  • -x509表示直接生成自签证书,而不是生成证书签名请求。
  • -newkey rsa:2048生成新的 2048 位 RSA 私钥,-nodes表示私钥不加密。
  • -subj "/CN=myserver.example.com"设置主题信息,这里 CN 可以先填一个主域名。
  • -addext "subjectAltName=..."是 OpenSSL 1.1.1 及以上版本支持的直接添加 SAN 扩展的参数,非常方便。

生成后可以再次用第一板斧确认:

openssl x509 -in server.crt -noout -ext subjectAltName

如果你用的是旧版 OpenSSL 或者需要通过 CSR 交给 CA 签发,那么需要准备一个 OpenSSL 配置文件。比如server.cnf:

[req] distinguished_name = req_distinguished_name req_extensions = v3_req prompt = no [req_distinguished_name] C = CN O = Internal CN = myserver.example.com [v3_req] subjectAltName = @alt_names [alt_names] DNS.1 = myserver.example.com DNS.2 = www.myserver.example.com IP.1 = 127.0.0.1 IP.2 = 192.168.1.10

然后生成 CSR 并让 CA 签名:

openssl req -new -key server.key -out server.csr -config server.cnf openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ -out server.crt -days 365 -extensions v3_req -extfile server.cnf

这里最容忽略的是签名时也要带上-extensions v3_req -extfile server.cnf,否则即便 CSR 里带了 SAN,最终签出的证书也未必会保留 SAN。我见过不少人栽在这个细节上。

3.2 方案二:临时用 GODEBUG 绕过(只适合调试)

如果证书暂时改不了,又急着联调,Go 留了一个后门。Go 1.15 引入变更的同时,也提供了环境变量开关:

GODEBUG=x509ignoreCN=0 ./your_app

x509ignoreCN=0表示恢复旧的证书校验行为,即当证书没有 SAN 时,回退到用 CN 字段匹配主机名。这个开关仅对 Go 1.15+ 有效,到 Go 1.17 时虽然仍可设置,但官方明确说后续版本会移除。

注意,这个方案仅适合本地调试或临时应急,千万不要在正式环境长期依赖它。原因很简单:CN 匹配不如 SAN 严格,容易遭受中间人攻击的变体。而且如果服务端证书属于“没有 SAN 的旧证书”,长期依赖GODEBUG开关等于把整个服务的证书安全策略拖回十年前。

如果程序是编译好的二进制,也可以用环境变量方式临时设置:

GODEBUG=x509ignoreCN=0 docker run ...

在容器编排和 systemd 服务里也可以注入这个环境变量。但再强调一次,这只是应急止血,修复证书才是正路。

3.3 方案三:更新程序里的 CA 信任链

有些情况下证书本身没问题,而是程序里指定的根证书或中间证书不对,导致校验链不完整。这时报错信息可能混着其他错误,比如x509: certificate signed by unknown authority。

排查思路分三步:

第一步,确认你发起 HTTPS 请求的客户端是否指定了自定义 CA 文件。Go 的http.Client默认使用系统根证书,如果你的服务端证书是由内部 CA 签发的,且这个 CA 没有被加入系统信任库,就需要在客户端代码里显式加载:

pool := x509.NewCertPool() caCert, err := os.ReadFile("internal-ca.crt") if err != nil { log.Fatal(err) } pool.AppendCertsFromPEM(caCert) client := &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{RootCAs: pool}, }, }

第二步,确认服务端是否把完整的证书链发给了客户端。检查方式用:

openssl s_client -connect myserver.example.com:443 -showcerts

正常情况应该能看到“Certificate chain”里有至少两三个证书,最后一个是根证书。如果只有服务器证书一张,说明中间证书没有配置完整,需要在服务器上把链补齐。

第三步,确认证书过期时间。检查证书有效期最快捷的方式:

openssl x509 -in server.crt -noout -dates

输出:

notBefore=Sep 26 00:00:00 2023 GMT notAfter=Sep 25 23:59:59 2026 GMT

如果证书已经过期,Go 会报x509: certificate has expired or is not yet valid,这种就老老实实重新签发。

3.4 方案四:用 mkcert 一站式解决本地证书

如果你是本地开发环境需要自签证书,强烈推荐mkcert这个工具。它会自动生成本地根证书,并把它安装到系统信任库,然后一条命令生成带 SAN 的服务器证书。

安装方式(macOS 上用 Homebrew):

brew install mkcert

初始化并安装本地根证书:

mkcert -install

生成证书:

mkcert myserver.example.com localhost 127.0.0.1

执行后会生成myserver.example.com+2.pem和myserver.example.com+2-key.pem两个文件,前者是证书,后者是私钥。这个证书天然带 SAN,既包含域名又包含 IP,完美解决 Go 1.15 的校验要求。

mkcert 在我们团队内部用得很多,尤其是给前端本地起 HTTPS、给 gRPC 服务联调、给 Docker 私有仓库配本地证书,省掉了手工维护 OpenSSL 命令的麻烦。不过它也有局限:生成的根证书只在当前机器受信任,如果要分发给其他同事或服务器,需要把根证书导出并在目标机器上安装信任。

4. 现场实录:那些年被证书坑过的场景

4.1 curl 访问自签名站点报 SSL 错误

网上经常搜到这种求助,形如:

curl https://esign.example.edu.cn:9100/ curl: (60) SSL certificate problem: self-signed certificate

这类报错其实不带 Go 的 SAN 提示,因为它来自 curl/OpenSSL 的校验逻辑。但排障思路很像。如果你只是临时测接口,可以用-k跳过校验:

curl -k https://myserver.example.com:9100/

但长期方案依然是让服务端配上受信任的证书,或者把自签根证书放进本机信任库。在 macOS/Linux 上可以用:

# macOS sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ca.crt # Linux (Debian/Ubuntu) sudo cp ca.crt /usr/local/share/ca-certificates/ sudo update-ca-certificates

配完再用 curl 访问,就能正常校验通过。另外一个小技巧:用curl -v可以看到完整握手过程,里面会输出“SSL certificate verify result”,对定位“unable to get local issuer certificate”帮助很大。

4.2 nginx 双向认证时报 no required ssl certificate was sent

还有一类常见报错是服务端开了双向 TLS(mTLS),但客户端没带证书,nginx 会报:

no required ssl certificate was sent

这时你需要在 nginx 配置里加上:

ssl_verify_client on; ssl_client_certificate /etc/nginx/certs/ca.crt;

客户端访问时需要带上证书和私钥:

curl --cert client.crt --key client.key https://myserver.example.com/

如果客户端证书也出现 SAN 缺失的问题,服务端 Go 程序在解析时同样可能报x509: certificate relies on legacy Common Name field。处理方式和前面一样,要么重新签发客户端证书,要么在服务端临时开GODEBUG=x509ignoreCN=0。

这里有个容易被忽略的点:ssl_client_certificate填的是签发客户端证书的 CA,而不是客户端证书本身。很多新手配错,拿客户端证书去配,结果握手永远失败。

4.3 openssl verify 提示 unable to get local issuer certificate

这个错误是 OpenSSL 在校验证书链时找不到根证书导致的。如果你本地没有安装签发该证书的根 CA,那么即使证书本身没错,也过不了openssl verify校验。

测试方式:

openssl verify -CAfile ca.crt server.crt

如果只写:

openssl verify server.crt

默认用的是系统信任库,看不到内部 CA 就会报unable to get local issuer certificate。处理方式很简单:把内部 CA 加入系统信任库。

另外有个细节,OpenSSL 校验链时,对server.crt文件里包含的证书顺序有要求:先从叶子证书开始,再到中间证书,最后到根证书。如果顺序倒了,OpenSSL 也会报错。可以把证书链按顺序合并成一个文件:

cat leaf.crt intermediate.crt root.crt > fullchain.crt

然后配置到 nginx 的ssl_certificate指令里。这个“fullchain”习惯要养成,很多线上事故就是只配了叶子证书。

4.4 TLS 握手 fatal alert: bad_certificate

还有一个比较吓人的报错:

fatal alert: bad_certificate - a corrupt or unusable certificate was received

这个错误通常出现在握手阶段,原因多种多样:证书格式不对、证书链不完整、证书私钥不匹配、客户端和服务端支持的加密套件不一致。

我的排查顺序是:

  1. 确认证书私钥是否匹配:
openssl x509 -in server.crt -noout -pubkey | openssl md5 openssl pkey -in server.key -pubout | openssl md5

两个 MD5 值一致才是同一对,否则证书和私钥不匹配。

  1. 确认证书链顺序:
openssl s_client -connect myserver.example.com:443 -showcerts

看服务端是否把中间证书也发下来了。

  1. 确认证书格式是否正确,nginx 通常要求 PEM 格式,个别系统要求 PFX/P12。

如果服务端是 Java 系的,还可能遇到 keystore 里证书过期而 JVM 信任库没更新的情况,报错形态五花八门。遇到bad_certificate别慌张,按上面顺序排查基本能定位到问题。

5. 避坑清单与我的实操心得

5.1 证书生命周期的三个坑

第一个坑是证书有效期太短。内部系统经常签发一年期甚至更短的自签证书,导致一年后服务突然连不上。建议在 Go 服务里做证书过期监控,或者在 CI 里周期性地用脚本检查证书有效期:

openssl x509 -in server.crt -noout -enddate

第二个坑是通配符证书的 SAN 写法。*.example.com只能匹配a.example.com,不能匹配a.b.example.com。需要多级泛域名时,SAN 里要写两个:

DNS.1 = *.example.com DNS.2 = *.api.example.com

第三个坑是多域名证书重启服务时只挂了一张老证书。有的生产环境里 Nginx 配置和证书文件分离,更新证书时忘了重启或 reload,导致服务实际用的还是旧证书。排查时注意看服务进程的运行时间,别只看磁盘上的文件。

5.2 我总结的排查口诀

踩过几次坑之后,我给自己总结了一套排查流程,遇到证书相关报错基本能十分钟内定位。

第一句:先看报错来自哪一层。Go 报x509:是 Go 运行时校验失败;curl 报(60)是 OpenSSL 校验失败;浏览器报“您的连接不是私密连接”是浏览器内置校验失败。不同层级的报错信息差别很大,但都能用 OpenSSL 的s_client和x509 -text来检查服务端证书。

第二句:确认证书 SAN 齐不齐。用openssl x509 -in server.crt -noout -text | grep -A1 "Subject Alternative Name"看有没有、对不对。没有就是老的 CN-only 证书,直接重签;有但是域名对不上,说明匹配的是其他域名,检查访问地址。

第三句:确认链和信任库。openssl s_client -connect host:port -showcerts看服务端发了几张证书,openssl verify -CAfile ca.crt server.crt看本地能否校验证书链。

第四句:确认是不是版本问题。如果代码在旧环境能跑、新环境不行,重点看 Go 版本。Go 1.15 前和 1.15 后的证书行为变化很大,这个版本分界线值得每个 Go 开发者记住。

第五句:实在急用,GODEBUG=x509ignoreCN=0先顶着,但记录工单、限期换证书,别让这个开关上生产长期运行。

我个人在实际操作中,现在生成任何自签证书都会条件反射地加上 SAN,甚至在 CSR 模板里默认就写好subjectAltName。因为这已经不是“可选项”,而是 Go、浏览器、以及越来越多现代工具的硬性要求。多写一行 SAN,省掉的是一个团队一下午的排障时间,这笔账怎么算都划算。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 4:18:46

STM32F103开发全栈指南:从烧录失败到外设精准控制

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 4:17:58

基于MPC的混合储能微电网双层能量管理:从原理到Matlab实现

这两年做微电网能量管理系统,我最大的感受是:储能配置不是电池越多越好,运行策略再复杂也架不住现场工况多变,而遇到带约束、多目标、时间耦合的优化问题,模型预测控制(MPC)确实比传统PID和规则…

作者头像 李华
网站建设 2026/10/1 4:15:23

最小可用MPC钱包实战:Rust内核与Java编排实现两方签名闭环

做MPC钱包的人,第一句想对同行说的往往是:钱包不是钱包,是把私钥拆了。真正动手实现之后,你还会发现另一件事——把协议讲清楚的人很多,把工程竖起来的人很少。这篇实战文章就是用 Rust 做密码学核心、用 Java 做业务协…

作者头像 李华
网站建设 2026/10/1 4:15:20

石墨烯钙钛矿太阳能电池COMSOL光电热耦合仿真建模全解析

去年我接了一个钙钛矿太阳能电池的仿真项目,一开始只做了单纯的半导体光电模型,J-V曲线算出来看着还行,但把器件放到65度环境下再测,效率掉得比实验快很多,我当时以为是边界条件没设对,反复调了很久也没改善…

作者头像 李华
网站建设 2026/10/1 4:14:59

操作系统设备管理探究:从设备分类到中断与DMA的I/O原理

操作系统四大资源管理——CPU、内存、文件、设备——前三个都有相对统一的抽象模型,唯独设备管理这一块,每次学都感觉像在跟一堆“脾气各异”的硬件打交道。这也是为什么我把I/O设备原理单独放在OS笔记的第38篇。设备管理的核心是搞清楚CPU怎么和键盘、磁…

作者头像 李华
网站建设 2026/10/1 4:14:39

AI工程从零开始:模型之外的关键工程实践与避坑指南

说实话,第一次看到“ai-engineering-from-scratch”这个项目名时,我脑子里最先浮现的不是某条提示词,而是过去一年多带着团队从一个“能跑通的Notebook”走到“敢让客户直接使用”的线上AI系统的全过程。很多人以为大模型时代的工程起点是写P…

作者头像 李华