服务器和代理

unsupported certificate purpose:公网证书已不含 clientAuth

作者: · 最后核实

报错信息client SSL certificate verify error: (26:unsupported certificate purpose)

这个报错的意思是:要求客户端证书的服务器收到了一张只允许“服务器认证”用途的证书。 2026 年起,Let's Encrypt 等公网 CA 签发的 TLS 证书不再包含“TLS 客户端认证”扩展密钥用途(EKU),所以原来用公网证书做客户端一侧的双向 TLS(mTLS),续期之后就会失败。

快速修复

修法是改用私有 CA 签发带 clientAuth 用途的客户端证书。先确认客户端出示的证书有哪些扩展用途:

openssl x509 -in client.pem -noout -ext extendedKeyUsage

如果只显示 TLS Web Server Authentication,说明证书缺少 clientAuth。也可以把证书粘贴到证书解析里查看。改用私有 CA 签发客户端证书,并让服务器信任这个 CA:

# 一次性:创建私有 CA(ca.key 要离线保存或放进 HSM/KMS)
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
  -keyout ca.key -out ca.pem -days 3650 -subj "/CN=Example Internal Client CA"

# 每个客户端:私钥、CSR,再签发带 clientAuth 的证书
openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
  -keyout client.key -out client.csr -subj "/CN=billing-service"
printf "extendedKeyUsage=clientAuth\nkeyUsage=digitalSignature\n" > client.ext
openssl x509 -req -in client.csr -CA ca.pem -CAkey ca.key -CAcreateserial \
  -out client.pem -days 90 -extfile client.ext
ssl_client_certificate /etc/nginx/client-ca.pem;   # 只放私有 CA
ssl_verify_client on;

原因

这个报错有 3 种原因,绝大多数是公网证书续期后丢了 clientAuth。

1. 公网证书续期后丢了 clientAuth

  • 发生了什么: Chrome 根证书计划政策(v1.8)要求公网 CA 把服务器认证和客户端认证拆成两套体系:2026 年 6 月 15 日及以后向 CCADB 披露的中间 CA 证书只能含 serverAuth;2027 年 3 月 15 日及以后签发的用户证书必须带 EKU 且只声明 serverAuth。各家 CA 按自己的时间表更早移除了 clientAuth:Let's Encrypt 在 2026 年 2 月 11 日把 clientAuth 从默认的 classic 配置中移除,可选的 tlsclient 配置保留到 2026 年 7 月 8 日;新的 Generation Y 证书只含服务器认证。
  • 确认: 出错的证书来自公网 CA,最近刚续期,上面的命令只显示服务器认证。
  • 修复: 按上文把客户端一侧换成私有 CA 签发的证书。

2. 服务器之间互相认证

有些系统让同一张证书“两头用”:服务 A 调服务 B 时出示自己的网站证书;XMPP、邮件服务器在主动外连时出示服务器证书;数据库或消息队列的复制节点之间用公网证书互相认证。

  • 确认: 报错出现在接收方服务器的日志里,发起方用的是自己的公网网站证书作为客户端身份。
  • 修复: 给每个节点再配一张私有 CA 签发的“客户端角色”证书;协议允许的话,也可以改用共享密钥、OAuth 2.0 客户端凭据、签名令牌等认证方式。

3. 私有 CA 签发时没加对用途

如果已经在用私有 CA,仍然报错 26,说明客户端证书或某一级中间 CA 签发时没有包含 clientAuth。

  • 确认: 对客户端证书和每一级中间证书都执行上面的检查;中间 CA 如果带 EKU 扩展,也必须包含 clientAuth。
  • 修复: 用 extendedKeyUsage=clientAuth 重新签发。

各环境修法

各环境的修法相同:客户端证书改由私有 CA 签发,服务器的客户端信任库只放这个私有 CA。

Nginx / 宝塔: 设置 ssl_verify_client on,ssl_client_certificate 只指向私有 CA 证书文件,把公网根证书从这个文件里删掉。宝塔面板里在站点配置文件中手动加这两行即可。

Apache httpd: SSLVerifyClient require,SSLCACertificateFile /etc/httpd/client-ca.pem。

Java 客户端: 把私有 CA 签发的客户端私钥和证书导入 PKCS#12 密钥库(-Djavax.net.ssl.keyStore=client.p12)。只含 serverAuth 的证书会被拒绝,提示类似 “Extended key usage does not permit use for TLS client authentication”。

Go: 服务端用 tls.RequireAndVerifyClientCert 时会报 x509: certificate specifies an incompatible key usage。ClientCAs 只放私有 CA。

Kubernetes 和服务网格: 工作负载证书用 cert-manager 的 CA 或 Vault Issuer 签发;Istio、Linkerd 等服务网格本来就用自己的私有 CA 签发 mTLS 证书。

对接银行、支付、政企接口: 这类接口通常由对方指定 CA 或由对方签发客户端证书,按对方要求申请,不要拿自己网站的公网证书去对接。

不要这样做

以下做法都能让报错消失,但 mTLS 也就不再起认证作用。

  • 关闭用途检查(比如把 OpenSSL 的 purpose 强制设成 any,或改验证代码)。服务器会接受并非用来标识客户端的证书。
  • 用 ssl_verify_client optional_no_ca 让报错消失。 Nginx 将接受任何客户端证书而不验证签发者,等于取消了认证。
  • 让服务器用公网根证书库验证客户端。 该 CA 的任何证书持有人都能通过认证,客户端信任锚必须是私有的。
  • 到处找还在混发服务器/客户端用途的 CA 来维持旧方案。 就算现在还有,按 Chrome 根证书计划政策,2027 年 3 月 15 日起签发的用户证书必须只声明 serverAuth,这条路有明确的终点。

常见问题

我的网站用的是 Let's Encrypt 证书,会受影响吗?

不会。浏览器和普通 HTTPS 客户端只检查“服务器认证”用途,这一项仍然保留。只有当某个系统把你的公网证书当作客户端证书,拿去连接另一台要求双向 TLS 的服务器时,才会受影响。

现在还能买到带 clientAuth 的公网证书吗?

Let's Encrypt 已经不行了,tlsclient 配置在 2026 年 7 月 8 日停止。其他公网 CA 也在按各自的时间表移除 clientAuth;按 Chrome 根证书计划政策,2027 年 3 月 15 日及以后签发的用户证书必须只声明 serverAuth,所以即使还有过渡选项,最晚也到那时为止,具体以各 CA 的官方公告为准。长期方案只有私有 CA。

为什么变更之后过了一段时间才出问题?

变更前签发的证书在到期前一直保留原来的扩展用途。Let's Encrypt 证书默认有效期 90 天,所以故障出现在变更后第一次续期、拿到只含 serverAuth 的新证书时。截至 2026 年 11 月,所有含 clientAuth 的 Let's Encrypt 证书都已过期。

国内云厂商的私有 CA 服务能用吗?

可以。阿里云、腾讯云等都提供私有 CA(私有证书)类服务,签发时选择客户端认证用途即可。也可以自建 step-ca、HashiCorp Vault PKI,小规模场景直接用 OpenSSL。关键是服务器端只信任这个私有 CA。

用公网 CA 做客户端认证本来就有问题吗?

通常是的。服务器如果信任公网根证书来验证客户端,任何能从该 CA 拿到任意域名证书的人,都能出示一张“合法”的客户端证书。私有 CA 由你决定给谁发证、随时吊销。

资料来源

  1. Let's Encrypt: Ending TLS Client Authentication Certificate Support in 2026
  2. Let's Encrypt: Profiles
  3. Chrome Root Program Policy
  4. OpenSSL X509_STORE_CTX_get_error (verification error codes)
  5. nginx ngx_http_ssl_module