---
tags: [service-mesh, mTLS, zero-trust, istio, certificate-management]
create time: 2026-05-17 21:35
---
# Service Mesh 中的 mTLS 配置详解
## 概述
mTLS(Mutual TLS)是服务网格实现零信任网络的核心机制。本文从 Istio 的三种 mTLS 模式入手,讲解双向证书认证的配置方法、迁移路径和日常排查技巧。
> [!question] 为什么服务间通信需要 mTLS?
>
> HTTP 请求在集群内裸奔是很危险的——一旦某个 Pod 被攻陷,攻击者可以直接监听同一 Namespace 内所有流量。mTLS 确保即使网络完全暴露,窃听者也无法解密通信内容。
>
> 这里有一个常见误解:mTLS 不是万能的。它只保护传输通道,不解决授权问题。一个合法的 A 服务仍然可以调用 B 服务的所有公开接口——只是它没法调 C 服务的接口了。这就是纵深防御的意义。
## mTLS 的基本原理
```mermaid
sequenceDiagram
participant C1 as CL
participant S1 as SV
participant CA1 as CA
Note right of CA1: 签名机构
C1->>S1: TCP Connect
activate S1
C1->>S1: ClientHello
S1->>C1: ServerHello + 证书
S1->>C1: CertificateRequest
C1->>C1: 验签服务端证书
C1->>S1: 客户端证书
S1->>S1: 验签客户端证书
deactivate S1
Note over C1,S1: TLS 握手完成
C1->>S1: 加密数据
S1->>C1: 加密数据
```
上面展示了完整的 mTLS 四次握手过程。与普通 HTTPS 不同,mTLS 要求双方都验证对方证书——服务端不仅确认自己连接的是合法客户端,客户端也确认自己连的是目标服务而非中间人。每个参与方通过证书标识自己的 SPIFFE ID,形成机器级别的信任链。
> [!note] 解释
>
> 这段流程的关键在于两次独立的证书验证:第一次是客户端验证服务端证书(类似 HTTPS),第二次是服务端验证客户端证书(mTLS 特有)。只有两边都通过后,TLS 会话密钥才会被用于后续所有通信的加解密。
## Istio 中的 mTLS 模式
Istio 提供三种 PeerAuthentication 模式,决定了 Sidecar 如何处理入站流量:
| 模式 | 行为 | 安全性 | 适用阶段 |
|------|------|--------|---------|
| UNSET | 继承全局或父命名空间配置 | — | — |
| PERMISSIVE | 接受明文和 TLS 两种流量 | 中等 | 迁移过渡期 |
| STRICT | 仅接受 TLS 连接 | 最高 | 生产就绪态 |
### 从 PERMISSIVE 到 STRICT 的迁移路径
> [!tip] 核心原则
>
> 不要一步到位切换到 STRICT。正确的做法是先用 PERMISSIVE 观察哪些流量没走 mTLS,逐一修复后再升级。
先在一个非核心 Namespace 做实验,验证流量不受影响后逐步推广到生产:
```yaml
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: production
spec:
mtls:
mode: PERMISSIVE
```
启用 PERMISSIVE 后,通过 Istio 自带的 metrics 找出未使用 mTLS 的流量来源:
```bash
# 查看各工作负载收到的明文连接数
kubectl exec -n istio-system deploy/istiod -- istioctl proxy-status
```
也可以从 Grafana 仪表盘中查看 `istio_requests_total{response_code=~"503"}` 的变化趋势——如果切到 STRICT 后某服务的 503 陡增,说明有非 mesh 流量在访问它。
修复完所有非 mesh 流量后,再升级到 STRICT:
```yaml
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: production
spec:
mtls:
mode: STRICT
```
### DestinationRule 中的端口级控制
某些端口可能不适合 mTLS,比如健康检查端点由 kubelet 发起,没有 Sidecar 证书。可以用 DestinationRule 做细粒度排除:
```yaml
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: my-service
namespace: production
spec:
host: my-service.default.svc.cluster.local
trafficPolicy:
portLevelSettings:
- port:
number: 8080
tls:
mode: ISTIO_MUTUAL
- port:
number: 9090
tls:
mode: DISABLE
```
> [!note] 设计决策
>
> 这里的逻辑是:8080 端口接收来自其他服务的流量,必须走 mTLS;9090 端口只接收 kubelet 的 readiness probe,不需要额外的传输加密。这样既覆盖了服务间的安全需求,又避免了健康检查中断。
## 证书生命周期管理
Service Mesh 自动管理证书轮换,但你需要注意几个关键参数来平衡安全性和可用性:
| 参数 | 推荐值 | 说明 |
|------|--------|------|
| 证书有效期 | 24 小时 | 短生命周期降低泄露风险,过期后自动失效 |
| 轮转提前量 | 1 小时 | 提前签发新证书避免新旧交接时的中断 |
| 根 CA 轮换 | 按需手动触发 | 根密钥应长期保存、极少变动,每次轮换都是高风险操作 |
Sidecar 代理负责整个证书的申请和刷新过程,应用层代码通常无需感知:
```go
func main() {
// Istio Sidecar 自动处理以下事宜:
// 1. 向 Citadel 申请服务身份证书
// 2. 在到期前自动完成轮转
// 3. 热更新 Envoy 的 TLS 上下文
// 你的业务代码照常写即可,不需要关心证书细节
http.ListenAndServe(":8080", nilHandler{})
}
```
如果需要直接读取证书文件做自定义处理(比如某些不走 Sidecar 的老服务),要注意处理文件变更事件并重新加载配置:
```go
// 直读证书目录时需要监听文件变更
certFile := "/var/run/secrets/tls/tls.crt"
keyFile := "/var/run/secrets/tls/tls.key"
fsnotify.Watch(certFile, func(e fsnotify.Event) {
cert, _ := tls.LoadX509KeyPair(certFile, keyFile)
reloadTLSConfig(cert)
})
```
> [!note] 解释
>
> 上面的示例展示了当服务不能依赖 Sidecar 时,如何自行管理证书生命周期。`fsnotify` 监控证书文件变化,检测到更新后用新的证书对重新初始化 TLS 配置,保证服务不会因证书过期而中断。
## 多集群与跨域场景
不同集群可能需要不同的 CA 签发证书,但又需要让它们之间能互相信任。关键是通过统一信任域实现跨集群身份对齐:
```mermaid
graph LR
subgraph ClusterA["集群 A"]
CA_A["CA A
Signer ID: cluster-a.example.com"]
svc_a["svc-a"]
end
subgraph ClusterB["集群 B"]
CA_B["CA B
Signer ID: cluster-b.example.com"]
svc_b["svc-b"]
end
CA_A -.信任域互联.-> CA_B
svc_a <-->|"mTLS 跨集群"| svc_b
```
配置跨集群信任的核心是让所有 Istio 实例共享同一个 `trust-domain`,同时在 meshConfig 中声明可信任的外部域:
```yaml
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
values:
global:
trustDomain: example.com
meshConfig:
trustDomains:
- example.com
- other-cluster.example.com
```
> [!summary] 跨集群 mTLS 的三点注意事项
>
> 1. **SPIFFE ID 的一致性**:证书的 SAN 字段必须包含正确的 trust-domain,否则对端会拒绝证书
> 2. **网络可达性**:跨集群的 mTLS 要求 Pod CIDR 之间网络互通,还需要正确配置 Service Entry
> 3. **CA 互信**:要么使用同一个根 CA,要么建立交叉信任链让两边都能验证对方的证书
## 常见问题排查
遇到 mTLS 相关的故障时,可以按以下步骤快速定位:
```bash
# Step 1: 查看当前 Pod 持有的证书信息
istioctl proxy-config secret -n
# Step 2: 检查命名空间的 PeerAuthentication 策略
kubectl get peerauthentication -n
# Step 3: 查看是否有冲突的 DestinationRule
kubectl get destinationrule -n -o yaml | grep -A 5 tls
# Step 4: 确认 ServiceAccount 是否有证书签发权限
kubectl auth can-i create certificatesigningrequests \
--as=system:serviceaccount::
```
常见的三类故障及其应对思路:
| 症状 | 原因 | 解决方法 |
|------|------|---------|
| Pod 间调用返回 503,日志显示 certificate verify failed | 证书签发者不被信任 | 检查 PeerAuthentication 是否在正确的 Namespace 生效 |
| 切到 STRICT 后部分服务超时 | 链路中有未注入 Sidecar 的跳板机 | 回到 PERMISSIVE,定位缺口后补装 Sidecar |
| 证书过期后批量失败 | Istiod 异常导致部分 Sidecar 未能续约 | 重启受影响 Pod,排查 Istiod 日志 |
## 安全加固建议
除了基础配置外,以下几个维度也能进一步提升 mTLS 的安全性:
| 维度 | 建议 | 理由 |
|------|------|------|
| 证书长度 | RSA 2048 或 ECDSA P-256 | 性能与安全性的平衡点 |
| 加密套件 | 仅允许 ECDHE + AES-GCM / ChaCha20 | 禁用 CBC 模式防 BEAST 等历史漏洞 |
| 最小 TLS 版本 | TLS 1.2 | 旧版本协议存在已知的侧信道攻击 |
| OCSP Stapling | 启用 | 加快证书吊销状态检查,减少握手延迟 |
| SPIFFE ID 格式 | `spiffe:///ns//sa/` | 标准化标识,便于审计和自动化编排 |
> [!note] 解释
>
> 关于 TLS 版本的取舍:TLS 1.3 在密码学上更优,但会与部分老版本的 gRPC 库和 Envoy 产生兼容性问题。如果你的技术栈较新(Go 1.18+、Envoy 1.24+),优先启用 TLS 1.3;否则保守选 TLS 1.2 更为稳妥。
## 总结
mTLS 是零信任架构中最值得投入的基础设施之一。它的核心价值不在于防御外部攻击——网关已经挡住了第一波流量——而在于限制内部横向移动。当某个 Pod 被入侵时,攻击者无法轻易监听或伪造其他服务间的通信。
记住一个简单的原则:**先宽松后严格,先观察后行动;证书不是装了就完事,要定期审计和轮换。**
## 关联笔记
- [[09-网关鉴权策略]] — 网关层的 JWT/mTLS 分层鉴权设计
- [[RBAC权限模型实战]] — 从 RBAC 到 ABAC 的演进路径
- [[02-服务治理/02-安全机制]] — 服务间认证与授权的完整体系