---
title: mTLS 与双向认证
url: https://doc.liz6.com/networking/05-tls-and-pki/04-mtls-and-mutual-authentication
locale: zh
area: networking
tags:
- networking
- TLS与PKI
date: 2026-06-30
modified: 2026-07-11
description: 服务端证明"我是谁"靠 TLS 证书,客户端证明"我是谁"靠 mTLS——双方都出示证书并验证。零信任架构里,mTLS 是服务间通信的身份基座,但证书分发和轮换的运维成本是它的主要阻力。
---

# mTLS 与双向认证

> 服务端证明"我是谁"靠 TLS 证书,客户端证明"我是谁"靠 mTLS——双方都出示证书并验证。零信任架构里,mTLS 是服务间通信的身份基座,但证书分发和轮换的运维成本是它的主要阻力。

## 概述

标准 TLS 只验证 server 身份。mTLS（mutual TLS）增加 client 证书验证：server 在握手中发送 CertificateRequest，client 必须提供证书。这实现了"零信任"网络的基石——两边互相证明身份而不依赖 IP 或网络位置。SPIFFE 标准化了 service identity，Istio/Linkerd 用 mTLS 为 service mesh 中的每个 pod 自动加密通信。自建 CA 是 mTLS 的必要前提——你需要完全控制证书签发和吊销。

## TLS vs mTLS: 握手差异

<svg viewBox="0 0 720 400" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="TLS 与 mTLS 握手差异对比">
  <defs><marker id="mtls-arr1" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker></defs>
  <rect width="720" height="400" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">TLS vs mTLS:单向信任变双向互验</text>

  <text x="44" y="70" font-size="12" font-weight="700" fill="#64748b">TLS</text>
  <rect x="80" y="48" width="150" height="36" rx="6" fill="#e2e8f0"/>
  <text x="155" y="70" text-anchor="middle" font-size="11" fill="#334155">Server 提供证书</text>
  <line x1="230" y1="66" x2="266" y2="66" stroke="#475569" stroke-width="1.6" marker-end="url(#mtls-arr1)"/>
  <rect x="270" y="48" width="160" height="36" rx="6" fill="#e2e8f0"/>
  <text x="350" y="70" text-anchor="middle" font-size="11" fill="#334155">Client 验证 Server</text>
  <line x1="430" y1="66" x2="466" y2="66" stroke="#475569" stroke-width="1.6" marker-end="url(#mtls-arr1)"/>
  <text x="474" y="70" font-size="12" font-weight="700" fill="#475569">→ 单向信任</text>

  <text x="44" y="122" font-size="12" font-weight="700" fill="#3730a3">mTLS</text>
  <rect x="80" y="100" width="225" height="36" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="192" y="122" text-anchor="middle" font-size="10.5" fill="#3730a3">Server 证书 + CertificateRequest</text>
  <line x1="305" y1="118" x2="341" y2="118" stroke="#475569" stroke-width="1.6" marker-end="url(#mtls-arr1)"/>
  <rect x="345" y="100" width="175" height="36" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="432" y="122" text-anchor="middle" font-size="11" fill="#3730a3">Client 也需提供证书</text>
  <line x1="520" y1="118" x2="556" y2="118" stroke="#475569" stroke-width="1.6" marker-end="url(#mtls-arr1)"/>
  <text x="564" y="122" font-size="12" font-weight="700" fill="#0f766e">→ 双方互验</text>

  <text x="360" y="170" text-anchor="middle" font-size="13" font-weight="700" fill="#1f2933">TLS 1.3 mTLS 简化握手</text>

  <rect x="60" y="182" width="600" height="32" rx="6" fill="#f1f5f9"/>
  <text x="360" y="203" text-anchor="middle" font-size="11.5" fill="#334155">Client → Server: ClientHello + key_share</text>
  <line x1="360" y1="214" x2="360" y2="228" stroke="#475569" stroke-width="1.6" marker-end="url(#mtls-arr1)"/>

  <rect x="60" y="230" width="600" height="36" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="360" y="248" text-anchor="middle" font-size="11" fill="#3730a3">Server → Client: ServerHello + key_share +</text>
  <text x="360" y="262" text-anchor="middle" font-size="11" fill="#3730a3">{EncryptedExtensions + CertificateRequest + ...}</text>
  <line x1="360" y1="268" x2="360" y2="282" stroke="#475569" stroke-width="1.6" marker-end="url(#mtls-arr1)"/>

  <rect x="60" y="284" width="600" height="36" rx="6" fill="#ccfbf1" stroke="#99f6e4"/>
  <text x="360" y="306" text-anchor="middle" font-size="11.5" font-weight="700" fill="#115e59">Client → Server: {Certificate + CertificateVerify + Finished}</text>

  <text x="360" y="332" text-anchor="middle" font-size="11" fill="#0f766e">↑ 比标准 TLS 1.3 多出 Certificate + CertificateVerify(由 client 提供)</text>

  <rect x="40" y="338" width="640" height="46" rx="7" fill="#eef2ff"/>
  <text x="360" y="357" text-anchor="middle" font-size="11" fill="#3730a3">CertificateRequest 携带:certificate_authorities(空=任意 CA)</text>
  <text x="360" y="373" text-anchor="middle" font-size="11" fill="#3730a3">signature_algorithms · certificate_extensions(如 SPIFFE OID)</text>
</svg>

## 私有 CA 管理

部署 mTLS 的第一步: 建立私有 CA:

```bash
# 1. 创建 Root CA (离线, 硬件安全模块 or air-gapped machine):
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out ca.key
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \
  -subj "/CN=Private CA" -extensions v3_ca

# 2. 签发服务端/客户端证书:
openssl genpkey -algorithm EC -out server.key
openssl req -new -key server.key -out server.csr \
  -subj "/CN=metrics-agent-001"
openssl x509 -req -days 90 -in server.csr -CA ca.crt -CAkey ca.key \
  -out server.crt -set_serial 0x01

# 3. 吊销 (如果有 compromised cert):
openssl ca -revoke server.crt -keyfile ca.key -cert ca.crt
```

## SPIFFE

标准化的服务身份框架，用于 mTLS 的证书 CN/SAN 中携带 SPIFFE ID:

```
SPIFFE ID: spiffe://trust-domain/path
  trust-domain: 组织/集群标识
  path: 服务/实例标识

例: spiffe://liz6.com/metrics-agent/li-cn-node-2

在 X.509 证书中:
  SAN URI: spiffe://liz6.com/metrics-agent/li-cn-node-2
  → 可以通过 URI SAN 验证 (不仅仅是 CN)

SVID (SPIFFE Verifiable Identity Document):
  = X.509 cert (短期: 默认 1 小时) + private key
  → 由 SPIRE agent 自动轮换
```

## Service Mesh (Istio/Linkerd)

<svg viewBox="0 0 720 270" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="Service Mesh 中 sidecar 自动完成 mTLS">
  <defs><marker id="mtls-arr2" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker></defs>
  <rect width="720" height="270" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">Service Mesh 中的 mTLS:应用代码无感知</text>

  <rect x="220" y="50" width="280" height="34" rx="6" fill="#4f46e5"/>
  <text x="360" y="71" text-anchor="middle" font-size="10.5" font-weight="700" fill="#ffffff">control plane (istiod / linkerd-identity)</text>
  <line x1="290" y1="84" x2="210" y2="132" stroke="#475569" stroke-width="1.6" marker-end="url(#mtls-arr2)"/>
  <line x1="430" y1="84" x2="470" y2="132" stroke="#475569" stroke-width="1.6" marker-end="url(#mtls-arr2)"/>
  <text x="360" y="108" text-anchor="middle" font-size="10" fill="#64748b">签发短期证书 · 自动轮换</text>

  <rect x="55" y="138" width="80" height="40" rx="6" fill="#e2e8f0"/>
  <text x="95" y="162" text-anchor="middle" font-size="12" font-weight="700" fill="#334155">Pod A</text>
  <line x1="135" y1="158" x2="153" y2="158" stroke="#475569" stroke-width="1.6" marker-end="url(#mtls-arr2)"/>

  <rect x="155" y="134" width="150" height="48" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="230" y="154" text-anchor="middle" font-size="11" font-weight="700" fill="#3730a3">sidecar proxy</text>
  <text x="230" y="169" text-anchor="middle" font-size="10" fill="#4f46e5">Envoy / linkerd-proxy</text>
  <line x1="305" y1="158" x2="323" y2="158" stroke="#475569" stroke-width="1.6" marker-end="url(#mtls-arr2)"/>

  <rect x="325" y="143" width="70" height="30" rx="15" fill="#0d9488"/>
  <text x="360" y="163" text-anchor="middle" font-size="12" font-weight="700" fill="#ffffff">mTLS</text>
  <line x1="395" y1="158" x2="413" y2="158" stroke="#475569" stroke-width="1.6" marker-end="url(#mtls-arr2)"/>

  <rect x="415" y="134" width="150" height="48" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="490" y="154" text-anchor="middle" font-size="11" font-weight="700" fill="#3730a3">sidecar proxy</text>
  <text x="490" y="169" text-anchor="middle" font-size="10" fill="#4f46e5">Envoy / linkerd-proxy</text>
  <line x1="565" y1="158" x2="583" y2="158" stroke="#475569" stroke-width="1.6" marker-end="url(#mtls-arr2)"/>

  <rect x="585" y="138" width="80" height="40" rx="6" fill="#e2e8f0"/>
  <text x="625" y="162" text-anchor="middle" font-size="12" font-weight="700" fill="#334155">Pod B</text>

  <rect x="40" y="200" width="640" height="50" rx="8" fill="#f0fdfa"/>
  <text x="360" y="220" text-anchor="middle" font-size="12" font-weight="700" fill="#115e59">应用代码不需要知道 mTLS 存在</text>
  <text x="360" y="238" text-anchor="middle" font-size="11" fill="#0f766e">每个 sidecar 从 control plane 获取短期证书,自动轮换、自动 mTLS</text>
</svg>

## 对比

| | mTLS | API Key | JWT (OAuth2) |
|---|---|---|---|
| 传输层 | L4 (TLS 握手阶段) | L7 (HTTP header) | L7 (HTTP header) |
| 身份绑定 | 证书 (CN/SAN + expiry) | key string | claims (sub, exp, aud) |
| 轮换 | CA 重新签发 | 生成新 key | 短 TTL + refresh |
| 吊销 | CRL / OCSP | 吊销 DB | 短 TTL (自然过期) |
| 中间件 overhead | 低 (TLS 本身) | 中 (验证 key) | 中 (验证 JWT) |
| 零信任兼容 | ✓ (SPIFFE) | ✗ | △ (需要 PKI for token signing) |

## 参考

- **RFC**: 8446, 8705
- **SPIFFE**: spiffe.io, github.com/spiffe/spire
- **Istio**: istio.io/latest/docs/concepts/security/#mutual-tls-authentication

*Keywords: mTLS, client certificate, SPIFFE, SPIRE, service mesh, private CA, certificate revocation, SVID*
