Files
cs-note/hzh/MS/02-服务治理/09-网关鉴权策略/service-mesh实战.md
T
2026-05-24 11:42:38 +08:00

9.8 KiB
Raw Blame History

tags, create time
tags create time
service-mesh
mTLS
zero-trust
istio
certificate-management
2026-05-17 21:35

Service Mesh 中的 mTLS 配置详解

概述

mTLS(Mutual TLS)是服务网格实现零信任网络的核心机制。本文从 Istio 的三种 mTLS 模式入手,讲解双向证书认证的配置方法、迁移路径和日常排查技巧。

[!question] 为什么服务间通信需要 mTLS?

HTTP 请求在集群内裸奔是很危险的——一旦某个 Pod 被攻陷,攻击者可以直接监听同一 Namespace 内所有流量。mTLS 确保即使网络完全暴露,窃听者也无法解密通信内容。

这里有一个常见误解:mTLS 不是万能的。它只保护传输通道,不解决授权问题。一个合法的 A 服务仍然可以调用 B 服务的所有公开接口——只是它没法调 C 服务的接口了。这就是纵深防御的意义。

mTLS 的基本原理

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 做实验,验证流量不受影响后逐步推广到生产:

apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: production
spec:
  mtls:
    mode: PERMISSIVE

启用 PERMISSIVE 后,通过 Istio 自带的 metrics 找出未使用 mTLS 的流量来源:

# 查看各工作负载收到的明文连接数
kubectl exec -n istio-system deploy/istiod -- istioctl proxy-status

也可以从 Grafana 仪表盘中查看 istio_requests_total{response_code=~"503"} 的变化趋势——如果切到 STRICT 后某服务的 503 陡增,说明有非 mesh 流量在访问它。

修复完所有非 mesh 流量后,再升级到 STRICT:

apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: production
spec:
  mtls:
    mode: STRICT

DestinationRule 中的端口级控制

某些端口可能不适合 mTLS,比如健康检查端点由 kubelet 发起,没有 Sidecar 证书。可以用 DestinationRule 做细粒度排除:

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 代理负责整个证书的申请和刷新过程,应用层代码通常无需感知:

func main() {
    // Istio Sidecar 自动处理以下事宜:
    // 1. 向 Citadel 申请服务身份证书
    // 2. 在到期前自动完成轮转
    // 3. 热更新 Envoy 的 TLS 上下文
    
    // 你的业务代码照常写即可,不需要关心证书细节
    http.ListenAndServe(":8080", nilHandler{})
}

如果需要直接读取证书文件做自定义处理(比如某些不走 Sidecar 的老服务),要注意处理文件变更事件并重新加载配置:

// 直读证书目录时需要监听文件变更
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 签发证书,但又需要让它们之间能互相信任。关键是通过统一信任域实现跨集群身份对齐:

graph LR
    subgraph ClusterA["集群 A"]
        CA_A["CA A<br/>Signer ID: cluster-a.example.com"]
        svc_a["svc-a"]
    end

    subgraph ClusterB["集群 B"]
        CA_B["CA B<br/>Signer ID: cluster-b.example.com"]
        svc_b["svc-b"]
    end

    CA_A -.信任域互联.-> CA_B
    svc_a <-->|"mTLS 跨集群"| svc_b

配置跨集群信任的核心是让所有 Istio 实例共享同一个 trust-domain,同时在 meshConfig 中声明可信任的外部域:

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 相关的故障时,可以按以下步骤快速定位:

# Step 1: 查看当前 Pod 持有的证书信息
istioctl proxy-config secret <pod-name> -n <namespace>

# Step 2: 检查命名空间的 PeerAuthentication 策略
kubectl get peerauthentication -n <namespace>

# Step 3: 查看是否有冲突的 DestinationRule
kubectl get destinationrule -n <namespace> -o yaml | grep -A 5 tls

# Step 4: 确认 ServiceAccount 是否有证书签发权限
kubectl auth can-i create certificatesigningrequests \
  --as=system:serviceaccount:<ns>:<sa-name>

常见的三类故障及其应对思路:

症状 原因 解决方法
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://<trust-domain>/ns/<ns>/sa/<sa> 标准化标识,便于审计和自动化编排

[!note] 解释

关于 TLS 版本的取舍:TLS 1.3 在密码学上更优,但会与部分老版本的 gRPC 库和 Envoy 产生兼容性问题。如果你的技术栈较新(Go 1.18+、Envoy 1.24+),优先启用 TLS 1.3;否则保守选 TLS 1.2 更为稳妥。

总结

mTLS 是零信任架构中最值得投入的基础设施之一。它的核心价值不在于防御外部攻击——网关已经挡住了第一波流量——而在于限制内部横向移动。当某个 Pod 被入侵时,攻击者无法轻易监听或伪造其他服务间的通信。

记住一个简单的原则:先宽松后严格,先观察后行动;证书不是装了就完事,要定期审计和轮换。

关联笔记