协议特性

双向 TLS(mTLS)原理:客户端证书怎么用、何时该用

作者: · 最后核实

双向 TLS(mutual TLS,mTLS)就是通信两端都出示证书的 TLS:服务器证明自己是 api.example.com,客户端证明自己是比如 billing-service,然后才开始处理请求。它是机器对机器认证的标准做法,服务网格、合作方 API、物联网设备都在用,因为凭据是一把永远不离开客户端的私钥。应使用自己掌控的私有 CA;公网证书正在被收窄为只能做服务器认证。

mTLS 在 TLS 1.3 握手中改了什么

mTLS 只是在普通 TLS 1.3 握手上多了一条服务器消息和两条客户端消息。TLS 1.3 最初由 RFC 8446 定义,2026 年 7 月发布的 RFC 9846 取代了它,但消息流程不变。

客户端                                                  服务器
------                                                  ------
ClientHello            (key_share、signature_algorithms)-->
                                              <-- ServerHello(key_share)
                                              <-- {EncryptedExtensions}
                                              <-- {CertificateRequest}     <- mTLS
                                              <-- {Certificate}            服务器证书链
                                              <-- {CertificateVerify}      服务器证明持有私钥
                                              <-- {Finished}
{Certificate}          客户端证书链                       -->              <- mTLS
{CertificateVerify}    对握手记录的签名                   -->              <- mTLS
{Finished}                                                -->
[应用数据]                                            <-->  [应用数据]

{ } = 用握手密钥加密        [ ] = 用应用数据密钥加密

三条 mTLS 消息分别携带什么:

消息 发送方 内容 作用
CertificateRequest 服务器 必带 signature_algorithms,可选 certificate_authorities(可接受 CA 的名称列表) 要求客户端出示证书,并说明接受哪些 CA 和签名算法
Certificate 客户端 客户端叶子证书和中间证书;没有证书时可以为空 出示身份
CertificateVerify 客户端 用客户端私钥对整个握手记录做的签名 证明持有私钥,并绑定到这一条连接

实践中要注意三点:

  • TLS 1.3 里客户端证书是加密的。 TLS 1.2 中它在加密开始前发送,被动监听者可以直接看到客户端身份;TLS 1.3 用握手密钥加密了它。
  • 协议允许发送空的 Certificate。 由服务器决定怎么处理:严格的服务器用 certificate_required 告警(编号 116)中止连接,宽松的服务器继续握手,交给应用判断。
  • 握手后认证有,但受限。 TLS 1.3 客户端带上 post_handshake_auth 扩展后,服务器可以在连接中途再索要证书。RFC 8740 禁止 HTTP/2 这么做,所以“只在 /admin 路径要求客户端证书”在 HTTP/2 下通常行不通,得改用单独的域名。

单向 TLS 与双向 TLS 对比

单向 TLS 只认证服务器,mTLS 用同一套机制认证双方。下表把它和最常见的替代方案(单向 TLS 加令牌)放在一起比较。

单向 TLS + API Key/令牌 双向 TLS
服务器认证 证书 证书
客户端认证 在连接里发送一个秘密值 握手中的证书加私钥签名
流量被记录或代理被攻破时泄露什么 令牌本身,吊销前可反复使用 没有可复用的东西,私钥从不离开客户端
在哪一层拒绝 应用层,TLS 之后 TLS 层,HTTP 请求还没解析
轮换 所有地方替换秘密值 签发新证书,CA 信任关系不变
运维成本 低 需要 CA、签发、续期和吊销

mTLS 不能代替授权。它告诉你“谁在连接”,至于这个客户端能做什么,还得由应用决定。

mTLS 用在哪里

凡是两端都是机器、且由同一组织或签约合作方运营的地方,基本都能看到 mTLS。

  • 服务网格。 Sidecar 代理给每个工作负载签发短期证书,服务之间的流量全部加密并互相认证。工作负载身份通常放在 URI 类型的 SAN 里,例如 spiffe://example.com/ns/prod/sa/billing。
  • 合作方与金融接口。 开放银行和支付类 API 常要求调用方出示由服务商或指定 CA 签发的证书。国内不少银行、支付和政务对接接口也采用双向证书。
  • OAuth 2.0。 RFC 8705 定义了两件事:客户端用证书向授权服务器认证(PKI 签发的用 tls_client_auth,自签名的用 self_signed_tls_client_auth);以及“证书绑定令牌”,令牌的 cnf 声明里带客户端证书的哈希(x5t#S256),令牌被偷了没有私钥也用不了。
  • 零信任接入。 公司设备装有设备证书,访问代理只放行出示公司 CA 证书的连接。
  • 物联网设备。 每台设备出厂时写入唯一证书,后端按证书识别设备,而不是靠烧在固件里的共享密码。
  • 数据库和消息队列。 PostgreSQL、MySQL、Kafka 等都原生支持客户端证书认证。

国密场景另有一套体系:TLCP(GB/T 38636-2020)使用签名证书和加密证书双证书,配置双向认证时要按所用国密网关或国密 SSL 库的文档准备客户端证书。

为什么 mTLS 应该用私有 CA

客户端证书请用私有 CA 签发。公网可信证书正在被限定为只做 TLS 服务器认证;即便在限制之前,它也不适合做客户端身份:公网 CA 验证的是域名,不是你的服务或设备,而且这家 CA 的任何客户都能拿到链到同一个根的证书。

截至 2026 年 11 月的政策变化:

日期 变化 出处
2025-10-01 Let's Encrypt 推出可选的 tlsclient 配置,保留 clientAuth EKU 供迁移过渡 Let's Encrypt 博客
2026-02-11 Let's Encrypt 默认 classic 配置不再包含 TLS 客户端认证 EKU Let's Encrypt 博客
2026-06-15 Chrome 根证书计划开始淘汰违反“专用服务器认证体系”规则的 PKI;此后向 CCADB 披露的中间 CA 只能声明 id-kp-serverAuth Chrome 根证书计划政策 1.8
2026-07-08 Let's Encrypt 停止 tlsclient 配置,不再签发带 clientAuth 的证书 Let's Encrypt 博客(2026-03-16 更新)
2027-03-15 Chrome 信任的根下签发的订户证书只能声明 id-kp-serverAuth Chrome 根证书计划政策 1.8

Let's Encrypt 的 Generation Y 证书完全不含客户端认证。其他公网 CA 在 Chrome 信任的体系上也在做同样调整。如果某套 mTLS 在证书续期后突然报“密钥用途”或“证书目的”相关错误,参见 客户端认证 EKU 被移除。

正确做法是把两件事拆开:

公网 Web PKI(浏览器信任)               私有 PKI(只有你的系统信任)
------------------------                ----------------------------
根证书(在浏览器根证书库中)            你自己的离线根 CA
  └─ 中间证书(仅 serverAuth)            └─ 签发 CA “Client CA”
       └─ api.example.com                      ├─ billing-service   (clientAuth)
          EKU: serverAuth                      ├─ device-000123     (clientAuth)
                                               └─ laptop-alice      (clientAuth)

服务器向客户端出示公网证书。            服务器校验客户端证书时只信任私有 Client CA。

服务器只信任一个专用的客户端 CA,本身也正是你想要的安全属性:任何其他 CA(无论公网还是私有)签发的证书都会被拒绝。具体命令见用 OpenSSL 搭建私有 CA。

在 nginx 中配置 mTLS

nginx 用 ssl_verify_client 加一个 CA 文件来校验客户端证书。ssl_client_certificate 里的 CA 也会在 CertificateRequest 中作为“可接受的 CA 名称”发给客户端。

server {
    listen 443 ssl;
    server_name api.example.com;

    ssl_certificate     /etc/ssl/api.example.com/fullchain.pem;   # 公网服务器证书
    ssl_certificate_key /etc/ssl/api.example.com/privkey.pem;

    ssl_client_certificate /etc/ssl/client-ca/ca-bundle.pem;      # 私有客户端 CA
    ssl_verify_client on;            # on | optional | optional_no_ca | off
    ssl_verify_depth  2;
    ssl_crl /etc/ssl/client-ca/client-ca.crl;                     # 可选:吊销列表

    location / {
        proxy_set_header X-Client-Verify $ssl_client_verify;      # SUCCESS / FAILED:原因 / NONE
        proxy_set_header X-Client-DN     $ssl_client_s_dn;
        proxy_set_header X-Client-Serial $ssl_client_serial;
        proxy_pass http://127.0.0.1:8080;
    }
}
  • on:没有有效证书就拒绝,nginx 返回 400 No required SSL certificate was sent 或 400 The SSL certificate error。
  • optional:请求证书但不强制,应用里检查 $ssl_client_verify。适合灰度上线阶段。
  • optional_no_ca:接受不链到你的 CA 的证书,把校验交给后端,一般不是你想要的。
  • 在入口处删掉客户端自带的 X-Client-* 头,防止伪造。

宝塔面板里同样是在“网站 → 设置 → 配置文件”的 443 server 块中加入上面几行。如果 TLS 在阿里云、腾讯云的负载均衡上终止,证书校验要在负载均衡上做:它们的 HTTPS 监听一般支持上传 CA 证书开启双向认证,后端只能通过负载均衡转发的请求头拿到客户端身份。

用 curl 和 openssl s_client 测试

成功和失败两种情况都要测。没有客户端证书时,要求证书的 TLS 1.3 服务器会在客户端 Finished 之后才中止,所以错误出现在第一次读数据时:

openssl s_client -connect api.example.com:443 -servername api.example.com -brief
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Peer certificate: CN=api.example.com
Verification: OK
Negotiated TLS1.3 group: X25519MLKEM768
...:error:0A00045C:SSL routines:ssl3_read_bytes:tlsv13 alert certificate required:...:SSL alert number 116

去掉 -brief 后,s_client 还会打印 Acceptable client certificate CA names,这是查看服务器期望哪个 CA 的最快方法。带上证书和私钥:

openssl s_client -connect api.example.com:443 -servername api.example.com \
  -cert client.crt -key client.key -brief

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

客户端证书是 PKCS#12 文件时,curl 可以用 --cert-type P12 --cert client.p12:密码,参见 PKCS#12 命令。确认客户端证书确实带有 clientAuth 用途:

openssl x509 -in client.crt -noout -ext extendedKeyUsage
X509v3 Extended Key Usage:
    TLS Web Client Authentication

更多连接测试命令见测试 TLS 连接。

常见故障

mTLS 故障大多是“服务器信任的”和“客户端发送的”对不上,而不是协议本身的问题。按现象对照原因:

现象 可能原因 处理
客户端报 alert certificate required(116) 客户端没发证书:路径错、缺私钥,或客户端库根本没加载 证书和私钥都要配置;浏览器要把 PKCS#12 导入系统或浏览器证书库
alert unknown ca(48) 客户端证书链不到服务器信任的 CA 把签发 CA(及中间证书)加入服务器的客户端 CA 文件,或让客户端一并发送中间证书
alert unsupported certificate 或“用途”类错误 客户端证书没有 clientAuth EKU,常见于续期后的公网证书 改用私有 CA 签发,带 extendedKeyUsage=clientAuth
浏览器不弹证书选择框 服务器的 certificate_authorities 列表和本机证书的颁发者都不匹配 看 s_client 输出里的 Acceptable client certificate CA names
HTTP/1.1 正常,HTTP/2 失败 应用依赖握手后(按路径)索要证书 改为握手时就要求证书,或使用独立域名
只有经过负载均衡才失败 TLS 在负载均衡终止,后端看不到证书 在负载均衡上校验,用入口可控的请求头转发已验证的身份

客户端证书链出问题时,排查服务器证书链的工具同样适用,参见验证证书链。

轮换与吊销

客户端证书分散在大量机器上,续期要从一开始就设计好。行之有效的做法:

  • 短有效期加自动化。 服务网格每几个小时到几天就轮换一次工作负载证书,几乎不再需要吊销。虚拟机可以用内部 ACME 服务或网格自带的 CA 达到同样效果。
  • CA 轮换时让新旧 CA 并存。 先把新的客户端 CA 加进 ssl_client_certificate,再开始用它签发,等所有旧证书过期后再移除旧 CA。
  • 长期设备证书要留吊销通道。 发布 CRL 并用 ssl_crl 加载,或在应用里按序列号、指纹维护黑名单。
  • 私钥在客户端生成。 让设备或服务自己生成密钥,再提交 CSR;不要通过邮件、聊天工具传私钥。
  • 监控到期时间。 客户端证书过期在客户端那边表现为故障,而且常常发生在一个没人记得归谁管的系统里。

握手细节见 TLS 1.3 握手;EKU 和密钥用途字段见扩展密钥用途和客户端证书。

常见问题

TLS 和 mTLS 有什么区别?

普通 TLS 只有服务器用证书证明身份。mTLS 中服务器还会发送 CertificateRequest,客户端必须出示自己的证书,并用 CertificateVerify 签名证明自己持有对应私钥。应用数据开始传输之前,双方身份都已确认。

Let's Encrypt 的证书还能当客户端证书用吗?

新证书不行了。Let's Encrypt 在 2026 年 2 月 11 日把客户端认证 EKU 从默认配置中去掉,2026 年 7 月 8 日之后彻底停止签发;新的 Generation Y 证书体系也不含客户端认证。客户端证书请用私有 CA 签发。

nginx 返回“No required SSL certificate was sent”是什么原因?

这是开启 ssl_verify_client on 后客户端没有出示证书时 nginx 返回的 400 错误。检查客户端是否同时配置了证书和私钥,以及证书是否由 ssl_client_certificate 里列出的 CA 签发。

mTLS 能和 HTTP/2 一起用吗?

能,只要在首次握手时就要求客户端证书。RFC 8740 禁止在 HTTP/2 连接上使用 TLS 1.3 的握手后认证,所以服务器没法等到访问某个路径时再临时索要证书。

资料来源

  1. RFC 9846: The TLS Protocol Version 1.3(取代 RFC 8446)
  2. RFC 8705: OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens
  3. RFC 8740: Using TLS 1.3 with HTTP/2
  4. Chrome Root Program Policy
  5. Let's Encrypt: Ending TLS Client Authentication Certificate Support in 2026
  6. nginx: ngx_http_ssl_module
  7. OpenSSL s_client 手册