# 双向 TLS（mTLS）原理：客户端证书怎么用、何时该用

> mTLS 如何在 TLS 1.3 握手中加入客户端证书认证、适用场景、为什么要用私有 CA，以及 2026 年 Chrome 和 Let's Encrypt 的变化有什么影响。

Source: https://ssl.sh/zh/learn/mtls/
Last updated: 2026-11-10

双向 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 取代了它，但消息流程不变。

```text
客户端                                                  服务器
------                                                  ------
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 被移除](https://ssl.sh/zh/errors/client-auth-eku-removed/)。

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

```text
公网 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](https://ssl.sh/zh/cookbook/private-ca/)。

## 在 nginx 中配置 mTLS

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

```nginx
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 之后才中止，所以错误出现在第一次读数据时：

```bash
openssl s_client -connect api.example.com:443 -servername api.example.com -brief
```

```text
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 的最快方法。带上证书和私钥：

```bash
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 命令](https://ssl.sh/zh/cookbook/pkcs12/)。确认客户端证书确实带有 clientAuth 用途：

```bash
openssl x509 -in client.crt -noout -ext extendedKeyUsage
```

```text
X509v3 Extended Key Usage:
    TLS Web Client Authentication
```

更多连接测试命令见[测试 TLS 连接](https://ssl.sh/zh/cookbook/test-connections/)。

## 常见故障

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 在负载均衡终止，后端看不到证书 | 在负载均衡上校验，用入口可控的请求头转发已验证的身份 |

客户端证书链出问题时，排查服务器证书链的工具同样适用，参见[验证证书链](https://ssl.sh/zh/cookbook/verify-chains/)。

## 轮换与吊销

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

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

握手细节见 [TLS 1.3 握手](https://ssl.sh/zh/learn/tls-handshake/)；EKU 和密钥用途字段见[扩展密钥用途](https://ssl.sh/zh/glossary/extended-key-usage/)和[客户端证书](https://ssl.sh/zh/glossary/client-certificate/)。
