协议特性
双向 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 的握手后认证,所以服务器没法等到访问某个路径时再临时索要证书。
资料来源
- RFC 9846: The TLS Protocol Version 1.3(取代 RFC 8446)
- RFC 8705: OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens
- RFC 8740: Using TLS 1.3 with HTTP/2
- Chrome Root Program Policy
- Let's Encrypt: Ending TLS Client Authentication Certificate Support in 2026
- nginx: ngx_http_ssl_module
- OpenSSL s_client 手册