mTLS 必须设 ClientAuth 为 tls.RequireAndVerifyClientCert,ClientCAs 非空,Certificates 仅含服务端证书;客户端需正确加载私钥(权限0600、无密码)、设置 ServerName 和 RootCAs,并用 VerifyPeerCertificate 实现零信任校验。

服务端必须设 ClientAuth 为 tls.RequireAndVerifyClientCert
不设这个值,mTLS 就根本没生效——哪怕你传了 ClientCAs、客户端也带了证书,连接照样成功,只是退化成单向 TLS。Go 默认的 http.ListenAndServeTLS 构造的 tls.Config 中,ClientAuth 是 tls.NoClientCert,等于关掉了客户端校验开关。
正确做法是手动构造 http.Server,并在 TLSConfig 中显式设置:
-
ClientAuth: tls.RequireAndVerifyClientCert(唯一能强制双向的选项) -
ClientCAs必须是非空的*x509.CertPool,不能是nil或空池 -
Certificates字段只放服务端证书+私钥,和单向 HTTPS 完全一样,别混进ClientCAs
客户端证书加载失败的三个典型原因
Go 客户端不会自动发证书,也不默认信任任何 CA;常见报错如 400 no required ssl certificate was sent 或 remote error: tls: bad certificate,往往不是服务端问题,而是客户端配置漏项。
- 私钥文件权限不是
0600:Go 会静默返回空tls.Certificate,后续握手直接失败 - 私钥带密码(比如 PEM 头含
DEK-Info):tls.LoadX509KeyPair()直接 panic 报crypto/tls: failed to parse key -
RootCAs没加载服务端 CA:错误提示模糊,常是x509: certificate signed by unknown authority或 EOF
ServerName 和 SAN 必须匹配,否则证书验证直接失败
Go 的 TLS 客户端默认不发 SNI 扩展,除非你显式设 ServerName。而服务端证书若没包含请求域名或 IP 的 SAN(Subject Alternative Name),就会在链验证阶段报 x509: certificate is valid for xxx, not yyy。
立即学习“go语言免费学习笔记(深入)”;
- 用
openssl req -new -x509 -addext "subjectAltName = DNS:my-service.internal,IP:127.0.0.1"生成带 SAN 的自签名证书 - 客户端
http.Transport.TLSClientConfig.ServerName必须和实际请求 Host 一致 - gRPC 场景下同样要设
ServerName,否则 HTTP/2 握手失败
VerifyPeerCertificate 不是可选功能,是零信任落地关键
仅靠 ClientCAs 只能验证“证书是不是这个 CA 签的”,但无法控制“签给谁”“能不能用”。业务上需要 OU 过滤、序列号黑名单、SAN 匹配等策略时,必须启用 VerifyPeerCertificate 回调。
- 一旦设置了该回调,Go 就不再自动做链验证,你得先调用
roots.Verify()补基础校验 - 参数
rawCerts是 DER 格式字节,需手动x509.ParseCertificate()解析 -
verifiedChains不可信——它只是候选链,最终是否接受由你代码决定
真正难的不是写通 mTLS,而是让每张客户端证书都承载明确身份语义,并在校验环节精确执行;漏掉 ServerName、私钥权限、RootCAs 加载或 VerifyPeerCertificate 中的手动链验证,都会让安全链条出现不可见的断点。


















