--- 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-安全机制]] — 服务间认证与授权的完整体系