必须设clientauth为tls.requireandverifyclientcert,否则服务端不校验客户端证书而静默降级为单向;clientcas需非空且含可信ca根证书;verifypeercertificate中须手动调用cert.verify()执行链验证。

ClientAuth 必须设为 tls.RequireAndVerifyClientCert
服务端不设 ClientAuth 或设错值,客户端证书就完全不会被校验——连接看似成功,实则降级为单向。默认值是 tls.NoClientCert,哪怕你填了 ClientCAs 也没用。
正确做法只有这一种:
-
ClientAuth: tls.RequireAndVerifyClientCert:强制客户端提供证书,并用ClientCAs中的根证书验证其签发链 -
ClientCAs必须是非空的*x509.CertPool,且内容是 CA 根证书(不是客户端证书本身) - 若
ClientCAs为空或加载失败,错误表现是tls: client didn't provide a certificate,但真实原因是池没建起来
客户端证书必须完整传入 Certificates 字段
Go 的 tls.Config 在客户端侧靠 Certificates 字段传证书链,不是靠单独的 cert/key 路径参数。漏掉这个,握手直接失败,日志里只显示 transport: authentication handshake failed,没更多线索。
常见配置错误:
- 只调用
tls.LoadX509KeyPair(certFile, keyFile),但没把返回的tls.Certificate塞进Certificates: []tls.Certificate{...} -
certFile里只放了终端证书,没拼上中间 CA;某些服务端(如 Nginx 前置)会因此校验失败 - 私钥是密码保护的 PEM ——
tls.LoadX509KeyPair不支持自动解密,会静默返回空tls.Certificate
RootCAs 和 ServerName 缺一不可(客户端侧)
客户端验证服务端证书时,RootCAs 和 ServerName 是硬性依赖。不设 RootCAs 就等于信任所有证书;不设 ServerName 会导致 SNI 不发、域名比对失败,报错类似 x509: certificate is valid for example.com, not 127.0.0.1。
关键细节:
- 别指望系统 CA:Alpine、scratch 镜像里没有
/etc/ssl/certs,必须显式加载根证书 PEM 到RootCAs -
InsecureSkipVerify: true只能用于本地调试,上线前必须删除,连注释都不能留 - 用
http.Transport.TLSClientConfig时,ServerName要设在tls.Config里,不是传给http.Get
自定义 VerifyPeerCertificate 时必须手动做链验证
一旦设置了 VerifyPeerCertificate 回调,Go 就不再自动执行证书链验证。如果你只检查 OU 或 SAN,却忘了调用 roots.Verify(),攻击者可能伪造一个格式合法但无法追溯到可信 CA 的证书绕过校验。
安全写法模板:
VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
if len(rawCerts) == 0 {
return errors.New("no client certificate provided")
}
cert, err := x509.ParseCertificate(rawCerts[0])
if err != nil {
return err
}
// 手动触发链验证(必须!)
opts := x509.VerifyOptions{
Roots: c.caPool,
CurrentTime: time.Now(),
DNSName: "", // 客户端证书一般不校验 DNSName
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}
if _, err := cert.Verify(opts); err != nil {
return err
}
// 再加业务规则,比如 OU 必须是 "service"
if len(cert.OCSPServer) == 0 || cert.Subject.OU[0] != "service" {
return errors.New("OU mismatch")
}
return nil
}
最常被忽略的是第一层链验证——它不是可选项,是防线基石。











