4. 服务网格中的身份认证:mTLS 原理
身份认证,说白了就是解决“你是谁”的问题。在服务网格里,这个问题尤其棘手。你想,几百个服务互相调来调去,如果没有可靠的认证机制,那跟开门揖盗有什么区别?
我刚开始做微服务安全那会儿,大家还在用 JWT + API Gateway 那套老路子。认证逻辑散落在各个业务代码里,改一次策略,恨不得把所有服务都重新部署一遍。直到后来接触到服务网格的 mTLS,我才真正觉得,这事有了标准解法。
4.1 为什么需要 mTLS?
传统的 TLS 只解决一个问题——客户端验证服务端的身份。比如你访问银行网站,浏览器会检查银行服务器的证书是否合法。但服务器并不验证你是谁(只要你有正确的 Cookie 就行)。
在服务网格的内部拓扑中,这种单向信任是不够的。你想想看,如果某个 Pod 被攻破了,它完全可以假装成另一个服务,去窃取敏感数据。所以我们需要双向验证——不仅仅是 Sidecar 验证对端,对端也要验证我们。
这就是 mTLS 的核心价值:通信双方都要出示证书,且互相验证。说白了,就是“我认识你,你也认识我,咱俩才能说话”。
关键点:mTLS 不是 TLS 的替代品,而是 TLS 的安全增强版。它复用了 TLS 1.2/1.3 的加密套件和握手协议,只是在证书验证环节做了双向扩展。
4.2 mTLS 的握手过程
这个握手过程,说复杂也复杂,说简单也简单。我习惯把它拆成四个阶段来看:
- Client Hello:客户端(源 Sidecar)发起连接,告诉服务端它支持哪些 TLS 版本和加密套件。
- Server Hello + 服务端证书:服务端(目标 Sidecar)回应,选定加密套件,并发送自己的证书。
- 客户端证书验证 + 客户端证书发送:客户端验证服务端证书的有效性(CA 签名、过期时间、域名等)。验证通过后,客户端也发送自己的证书给服务端。
- 服务端验证 + 密钥协商:服务端验证客户端证书。双向都通过后,双方用非对称加密交换对称密钥,之后整个会话都用对称加密传输。
嗯,这里要注意一个细节。在服务网格里,证书的颁发和管理是由 Citadel(Istio 的控制面组件,负责证书签发轮换)完成的。每个 Sidecar 启动时,都会向 Citadel 请求一个 SPIFFE 格式的身份证书。这个证书绑定了服务账户(ServiceAccount),所以你无法伪造不属于你的身份。
避坑指南:我曾经遇到过一个问题——两个服务位于不同的集群,却通过 mTLS 通信。当时死活连不上,最后发现是跨集群的根证书没有做统一信任域配置。所以记住,mTLS 的信任域必须在所有参与方之间达成一致。
下图展示了 mTLS 在服务网格中的完整握手流程:
4.3 Istio 中的 mTLS 实现
在 Istio 中,mTLS 的配置超级简单。你不需要修改一行业务代码,只需要在控制面声明策略就行。我个人习惯按以下层级来配置:
| 策略层级 | 作用范围 | 典型场景 |
|---|---|---|
DISABLE |
命名空间 / 服务 | 灰度迁移期,兼容旧服务 |
PERMISSIVE |
命名空间 / 服务 | 逐步启用 mTLS,新旧共存 |
STRICT |
命名空间 / 网格全局 | 生产环境,强制所有通信走 mTLS |
举个具体的例子。如果你想在整个网格内强制执行 mTLS,只需要创建一个 PeerAuthentication 资源:
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: istio-system
spec:
mtls:
mode: STRICT
这段配置的意思很简单:所有 Sidecar 之间的通信都必须使用 mTLS。如果某个请求没有提供合法的客户端证书,连接会被直接拒绝。我在生产环境就是这么干的,效果很好。
注意:不要盲目开启 STRICT 模式!我曾经在迁移期直接切了 STRICT,结果一堆遗留服务(没有注入 Sidecar 的服务)全部断联。建议先走 PERMISSIVE 模式,等确认所有服务都正常接入网格后,再切到 STRICT。
4.4 证书轮换与过期处理
mTLS 的安全性依赖于证书的时效性。如果证书永久有效,被泄露的风险会大大增加。Istio 默认的证书有效期是 24 小时,并且会在过期前自动轮换。
这个机制其实挺巧妙的。Sidecar 会定期向 Citadel 发起 CSR(证书签名请求),拿到新证书后立即替换旧证书。而且这个过程是热切换的——不会中断现有连接。嗯,这里有个细节:新证书只能用于新建连接,已建立的连接会继续使用旧证书直到会话结束。这也是为什么短连接(HTTP/1.1 默认 TCP 长连接)需要特别注意。
最佳实践:如果你的服务使用了 gRPC 长连接,建议在应用层也加上连接到期重连的逻辑。否则证书轮换了,但连接还绑着旧证书,会出现奇怪的身份验证错误。
4.5 性能开销到底有多大?
很多人担心 mTLS 会拖慢性能。我直接说结论:影响微乎其微。
为什么?因为 mTLS 的握手只发生在连接建立时。一旦握手完成,后续的数据传输使用的是对称加密(AES-GCM 之类的),这个开销在硬件指令集的支持下几乎可以忽略不计。我在线上一台 2C4G 的 Pod 上做过压测,开启 mTLS 后的吞吐下降不到 3%。
但有一个例外——如果你的服务间建立了大量短连接(比如每次请求都新建 TCP 连接),那握手开销就会成为瓶颈。解决方法是:启用连接池复用。Istio 的 DestinationRule 可以配置连接池的大小和空闲超时,把长连接维护起来。
- 连接数:限制到目标服务的最大连接数(默认 1024)。
- 超时时间:空闲连接保持存活的时间(默认 30 分钟)。
- 重试策略:连接失败时的自动重试次数。
举个例子:
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: my-svc
spec:
host: my-svc.default.svc.cluster.local
trafficPolicy:
connectionPool:
tcp:
maxConnections: 100
connectTimeout: 30ms
http:
http2MaxRequests: 1000
maxRequestsPerConnection: 10
这样配置后,mTLS 的握手只会发生在连接池初始化阶段,后续的请求都复用同一加密通道,性能几乎无损耗。
个人经验:我建议在压测时就带上 mTLS 一起测,别等上了生产才发现性能瓶颈。曾经有个团队告诉我“mTLS 太慢”,结果发现是他们没开连接池,每条请求都握手一次。调整后,性能完全恢复正常。
4.6 信任域与多集群 mTLS
如果你管理着多个 Kubernetes 集群,mTLS 的信任域就需要统一。说白了,所有集群的 Citadel 必须使用同一个根证书。否则,集群 A 签发的证书在集群 B 看来就是无效的。
Istio 支持通过 MeshConfig 配置信任域:
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
meshConfig:
trustDomain: my-cluster.local
把主集群的根证书分发给其他集群的 Citadel,让它们共享同一个信任链。这样跨集群的服务调用就能正常进行 mTLS 认证了。
我在一个多集群项目里就是通过这种方式实现的。你想想看,两个集群可能分布在不同的机房甚至不同的云服务商,但它们之间的 Sidecar 依然可以互相验证身份。这就是统一的信任域带来的好处。