一、从一次通信被拒的现场说起
上周接了个客户的问题:他们的电商平台里,订单服务调用商品服务的时候突然报错,提示“连接被拒绝”。一开始以为是商品服务挂了,或者网络出问题,但是查了K8s的pod状态,两个服务都运行正常,网络也通,用curl直接在商品服务的pod里访问自己的/api/info接口,是好的,但是订单服务的pod访问就不行,而且日志里的错误是RBAC相关的,不是网络不通的那种。
二、拆解问题:从证书到授权的全链路走一遍
2.1 mTLS证书轮换的中间状态
服务网格里,默认都是用mTLS加密服务之间的通信,简单说就是两个服务“见面”的时候要互相看对方的身份证(证书),确认身份后才敢通信。这个身份证(证书)是服务网格的控制平面Istio的Citadel组件自动签发和轮换的,到期前自动生成新的证书下发给所有sidecar代理。 但是证书轮换不是瞬间完成的,当Citadel签发了新证书,sidecar代理要从控制平面拉取新证书,这个过程需要一点时间,少则几毫秒,多则几十秒。如果刚好在这个时间窗口里,新的请求用旧证书发,而对方已经要求用新证书验证,就会出现证书不匹配的问题,导致通信被拒,不过这个阶段的问题排查起来,看证书的过期时间就能发现,而且这个问题是临时的,过一会儿就好。
我们用一个简单的命令查看sidecar里的证书状态,确认是不是证书轮换的问题:
# 查看test命名空间下,serviceA这个pod挂载的sidecar证书secret
kubectl -n test get secret $(kubectl -n test get pod -l app=serviceA -o jsonpath='{.items[0].spec.volumes[*].secret.secretName}') -o yaml
这个命令会输出secret的yaml内容,里面的tls.crt和tls.key就是证书和私钥,看里面的NotAfter字段,确认是否过期,还有证书的签发者是不是Istio的Citadel,如果没问题,就可以排除证书的问题。
2.2 授权策略的“延迟陷阱”
刚才客户的问题里,证书是正常的,那为什么还是被拒?再看订单服务调用商品服务的日志,提示的是RBAC: access denied,这是Istio的授权策略拦截的。 服务网格里,授权策略是控制谁能访问服务的,比如要让订单服务(serviceA)能访问商品服务(serviceB)的/api接口,必须配置一个授权策略允许这个访问。很多人写了这个策略,以为部署下去就所有服务都能访问,但其实不是,授权策略是下发到每个服务的sidecar代理里的,Istio的控制平面(Istiod)要把这个策略同步到所有节点上的sidecar,这个同步过程是需要时间的,当集群节点多、服务多的时候,同步的延迟可能达到几十秒,甚至更久。
举个授权策略的示例,这个策略就是允许test命名空间里的serviceA访问serviceB的所有/api开头的路径:
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: serviceA-access-serviceB
namespace: test
spec:
# 动作是允许,符合我们的需求
action: ALLOW
rules:
- from:
# 来源是serviceA的服务账号,确保是指定的服务
- source:
principals: ["cluster.local/ns/test/sa/serviceA"]
to:
# 目标路径是/api开头的,匹配我们需要的接口
- operation:
paths: ["/api/*"]
这个策略看起来没问题,但是如果部署之后,有新的订单pod调度到了还没同步到这个策略的worker节点,这个新pod的sidecar里就没有这个授权策略,那它访问serviceB的时候,serviceB的sidecar就会拦截,认为没有权限,导致通信被拒,这就是策略下发不一致的坑。
三、实际场景中的典型案例
我们再把客户的场景具体化,更清晰地看到这个问题: 客户的集群有3个worker节点,部署了订单服务(serviceA)和商品服务(serviceB),都在test命名空间,用Istio的mTLS STRICT模式(所有服务通信都需要证书验证)。某天运营要求给商品服务加一个新的接口,所以运维修改了授权策略,把serviceA的访问权限调整为允许/api/v2/*的路径,新策略部署后,Istiod开始向3个节点同步策略,其中节点1和节点2在10秒内同步完成,节点3因为网络问题,花了25秒才同步到。 这时候,K8s的调度器要启动一个新的订单pod,它被调度到了节点3,因为节点3的sidecar还没拿到新的授权策略,这个新pod发请求到serviceB的/api/v2/order接口,serviceB的sidecar检查授权策略,发现没有允许这个访问,就返回403,也就是通信被拒,这就是当时客户遇到的问题。
排查的时候,先看证书,发现所有pod的证书都是正常的,过期时间还有28天;然后查授权策略,用istioctl命令查看所有sidecar的授权策略:
# 查看test命名空间下,serviceB的所有sidecar生效的授权策略
istioctl experimental authorization check serviceB.test.svc.cluster.local -n test --all
这个命令会输出所有节点上serviceB的sidecar的授权策略,发现节点3上的sidecar里,还是旧的授权策略,只允许/api/*,没有新的/api/v2/*的路径,所以问题就找到了,是策略下发延迟导致的不一致。
四、技术优缺点分析
4.1 mTLS证书轮换的优缺点
优点:1. 自动加密服务之间的流量,不需要开发者手动处理证书,减少了安全风险;2. 证书自动轮换,不需要运维手动更新,保障了安全性。 缺点:1. 证书同步过程有延迟,可能导致临时的通信问题;2. 证书中间状态难排查,非专业人员很难判断证书是否在轮换中,容易误判为其他问题;3. 当控制平面故障时,证书签发会中断,影响服务通信。
4.2 Istio授权策略的优缺点
优点:1. 基于K8s的服务账号和标签,授权规则灵活,可以控制到具体的路径、方法;2. 和K8s生态集成,不需要额外的组件,部署简单;3. 支持不同的授权动作,比如ALLOW、DENY,还有自定义的条件。 缺点:1. 策略下发一致性依赖控制平面,同步延迟可能导致部分服务权限缺失;2. 大集群下,策略多,排查权限问题的时候需要检查每个sidecar的策略,比较麻烦;3. 默认是STRICT模式,没有策略的话,所有流量都被拒绝,容易导致服务不可用。
五、注意事项
- 策略下发后的验证:当修改授权策略或者mTLS配置后,不要立刻发布新的服务,要等至少30秒,确保所有sidecar都同步到新策略,然后可以用istioctl命令检查所有sidecar的策略是否一致;
- 证书监控:要监控sidecar的证书过期时间,还有Istio Citadel的签发状态,设置告警,当证书即将过期或者签发失败的时候,及时处理;
- 避免STRICT模式的坑:如果服务网格刚部署,不要直接用STRICT模式,可以先用PERMISSIVE模式(既允许mTLS,也允许明文),等所有服务都适配后再切换到STRICT模式,减少临时问题的影响;
- 新pod调度后的测试:发布新的授权策略后,要测试新pod调度到不同节点的情况,比如手动删除一个serviceA的pod,让它重新调度,看是否能正常访问serviceB,确保策略下发一致;
- 不要用太严格的策略:刚开始的时候,可以先设置一个宽松的策略,允许大部分服务访问,等调试好后再细化规则,避免因为策略缺失导致全服务不可用。
六、解决和优化方案
针对策略下发不一致的问题,我们可以做几个优化:
- 增加策略同步的超时时间:在Istiod的配置里,调整策略同步的超时时间,确保所有节点都能及时拿到新策略;
- 用分布式策略存储:比如把授权策略存储在Etcd里,让所有sidecar直接从Etcd拉取策略,避免依赖Istiod的单节点同步,提高一致性;
- 增加策略一致性检查的工具:自己写一个小脚本,定期检查所有sidecar的授权策略是否一致,如果发现有节点的策略不一致,就自动触发重新同步;
- 部署新策略前做流量切换:发布新授权策略的时候,先把流量切到一个测试节点,等确认没问题再全量发布,避免影响所有服务;
- 用灰度发布的方式修改策略:当调整授权规则的时候,先只让一部分流量走新策略,另一部分用旧策略,逐步验证,直到没问题再全量替换。
七、总结
回到最开始的问题,服务网格里的通信被拒,很多时候不是因为网络或者服务本身的问题,而是因为两个核心安全环节的不一致:mTLS证书的同步延迟,或者授权策略的下发延迟。证书轮换的中间状态可能导致临时的问题,但是更多的是授权策略的不一致,比如策略还没同步到所有节点,新的服务请求就来了,导致权限被拒。排查这类问题的时候,要一步步来,先排除服务本身和网络的问题,再看证书状态,最后检查授权策略的下发情况,还要结合K8s的调度情况,确保所有pod都在正确的节点上拿到了最新的配置。服务网格的安全不是一劳永逸的,需要关注每个环节的同步和一致性,才能保障服务的稳定运行。
评论
围绕“服务网格安全策略下发不一致导致通信被拒?从mTLS证书轮换到授权策略传播全链路剖析”参与讨论