服务网格身份与 mTLS:证书轮换、撤销和双根迁移
client 到 orders 的请求突然大面积握手失败,监控却显示两边证书都“还有很久才过期”。值班同学重启业务 Pod 后短暂恢复,下一批实例又失败。最后发现并不是 leaf certificate 到期,而是节点时钟漂移让新证书的 NotBefore 尚未生效;重启只是把请求换到了时钟正常的节点。
还有一种事故发生在根证书迁移时。平台先让签发方切到新根,再把新 trust bundle 推给数据面,窗口内的新证书无法被旧代理验证。面板仍显示 mTLS 已开启,正确身份访问和错误身份访问甚至都成功,因为授权策略只按服务名放行。加密、身份、信任和授权被压成一个绿色图标,真正的故障与越权同时被隐藏了。
把身份、证书、加密和授权拆成四个问题
workload identity 回答“这个运行实例是谁”。SPIFFE ID 是 URI 形式的名字,例如 spiffe://example.test/ns/mesh-lab/sa/client;trust domain 是 URI authority example.test,代表一套身份管理边界,不等于 Kubernetes namespace、cluster 或业务租户。SVID 是证明某个 workload 可以使用该 SPIFFE ID 的材料,可以是 X.509-SVID,也可以是 JWT-SVID。
X.509-SVID 把 SPIFFE ID 放入 URI SAN,并由所属 trust domain 的 CA 链签名。trust bundle 保存验证对端所需的根或中间信任材料。私钥证明持有者,证书链证明签发关系,bundle 决定验证者信谁。SPIFFE 的X.509-SVID 规范和Workload API定义了这些合同,但 selector、注册、TTL、密钥落点和授权由 SPIRE或网格实现决定。
mTLS 在一次连接里让双方出示证书、验证链与主机时间,并协商加密。它提供对等认证、机密性和完整性,不回答“client 是否能执行 orders 的退款操作”。这个问题必须由网格授权策略或业务授权处理。即使身份完全正确,权限也可能应该被拒绝;反过来,只有 TLS 加密但未验证工作负载身份,也不能称为可信 mTLS。
先建立不会泄露密钥的实验环境
准备隔离的 mesh-lab namespace、已安装并锁定版本的网格、kubectl 和产品 CLI。实验账号需要创建 ServiceAccount、Deployment 与命名空间策略;读取代理证书、exec 进代理和查看配置转储属于高敏调试权限,应临时授予并审计。CA 私钥、issuer 凭据和 Kubernetes Secret 的读取只给安全平台 owner,应用开发者不需要获得它们。
部署 client、orders-v1、orders-v2,为它们分别使用 client 与 orders ServiceAccount,不使用 default ServiceAccount:
kubectl create namespace mesh-lab
kubectl -n mesh-lab create serviceaccount client
kubectl -n mesh-lab create serviceaccount orders
kubectl -n mesh-lab apply -f deploy/mesh-lab/orders-v1.yaml
kubectl -n mesh-lab apply -f deploy/mesh-lab/orders-v2.yaml
kubectl -n mesh-lab apply -f deploy/mesh-lab/client.yaml
kubectl -n mesh-lab get pod `
-o custom-columns=NAME:.metadata.name,SA:.spec.serviceAccountName,NODE:.spec.nodeName项目清单里的 serviceAccountName 会改变身份,绝不能把它当无关字段。automountServiceAccountToken 只控制 Kubernetes API token 是否自动挂载,不能直接关闭网格身份;某些 attestor 会读取投射 token,贸然禁用可能让身份签发失败。应按实现选择 audience 有限、短期的 projected token,并验证网格 agent 的实际依赖。
以 Istio sidecar 作为可执行示例时,先按安全模型确认目标版本如何从 Kubernetes ServiceAccount 构造 identity,再启用 namespace 注入并滚动工作负载。新实验集群可在安装时显式设置虚构 trust domain;已经存在身份和策略的集群不能原地照抄这个变更,因为它会改变 principal:
istioctl install --set profile=minimal `
--set meshConfig.trustDomain=example.test `
--set revision=mesh-lab-id1 --skip-confirmation
kubectl label namespace mesh-lab istio.io/rev=mesh-lab-id1
kubectl -n mesh-lab rollout restart deployment/client deployment/orders-v1 deployment/orders-v2
kubectl -n mesh-lab rollout status deployment/client
kubectl -n mesh-lab rollout status deployment/orders-v1
kubectl -n mesh-lab rollout status deployment/orders-v2
istioctl proxy-statusmeshConfig.trustDomain 进入 workload principal 与信任判断,改动它会联动授权、federation 和审计,不是显示名称;revision 选择负责注入和配置的数据面版本。若使用外部 SPIRE,按Istio SPIRE 集成核对 socket、CSI、attestation 与兼容版本;SPIRE 注册条目存在只能证明声明存在,仍要检查 Workload API 实际返回的 SVID。
读证书时要同时读身份、链和时间
先取得代理暴露的证书摘要。不同产品命令不同,Istio 可用 CLI 检查代理证书,SPIRE 可从受控调试进程调用 Workload API;不要把私钥或完整 Secret 输出到终端录屏和 CI:
$pod = kubectl -n mesh-lab get pod -l app=client `
-o jsonpath='{.items[0].metadata.name}'
istioctl proxy-config secret "$pod.mesh-lab"预期至少能读取到 leaf certificate 的 URI SAN、issuer、serial、NotBefore、NotAfter 与证书链,以及 ROOTCA/bundle 的有效期。正确证据要回答:URI SAN 是否真的是 client 身份;证书链是否连接到预期 trust domain 的根;当前时间是否落在有效窗口;私钥是否只在代理内存、SDS/Workload API 客户端进程或受控挂载中;代理是否已经使用新版本,而不是控制面“准备好了”。
不得在文章、工单或群聊粘贴生产私钥、完整证书转储、真实 SPIFFE ID、内部 trust domain、证书序列号与节点名。诊断时保留哈希、脱敏后的 SAN 结构、相对剩余寿命和发行者标识即可。证书本身通常不是秘密,但它会泄露组织结构、服务名和拓扑;私钥始终按凭证处理。
用正反握手证明 mTLS,而不是看开关
先让 orders 入站只接受 mTLS,再用独立授权对象允许 client 身份。把两者写成两个对象很重要:前者控制 TLS 接入模式,后者才决定通过认证的 principal 能做什么。
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
name: orders-strict-mtls
namespace: mesh-lab
spec:
selector:
matchLabels:
app: orders
mtls:
mode: STRICT
---
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: orders-allow-client
namespace: mesh-lab
spec:
selector:
matchLabels:
app: orders
action: ALLOW
rules:
- from:
- source:
principals:
- example.test/ns/mesh-lab/sa/client
to:
- operation:
paths: ["/version", "/admin"]把这两个对象保存为 deploy/mesh-lab/orders-security.yaml。selector.matchLabels 决定策略落在哪些目标 workload,标签错一个字符就可能让对象存在但没有执行点;STRICT 拒绝目标端口上的明文入站;action: ALLOW 一旦存在,就让被选择的目标进入“只允许规则命中”的语义;principals 匹配已认证来源身份,不能用可由应用伪造的 HTTP header 替代。paths 是 HTTP/L7 条件,纯 TCP 或没有 L7 解析的执行点无法按相同方式判断。应用前先审查选中对象,再等待配置到达目标代理:
kubectl diff -f deploy/mesh-lab/orders-security.yaml
kubectl apply -f deploy/mesh-lab/orders-security.yaml
istioctl analyze -n mesh-lab
istioctl proxy-status先让 client 以 request ID 调用 orders:
kubectl -n mesh-lab exec deploy/client -- sh -c `
'wget -S -O- --header="X-Request-Id: mtls-positive-1" http://orders:8080/version'正向判据包括应用返回成功、源代理看到目标身份、目标代理看到源身份、连接安全属性为双向认证,并且日志中的 request ID、Pod UID 与证书版本能关联。只看到 HTTP 200 不能排除 plaintext,也不能证明双方身份正确。
接着用 deploy/mesh-lab/intruder.yaml 创建 Deployment;清单必须显式设置 serviceAccountName: intruder。它也必须被 mesh,才能形成“mTLS 握手成功、授权随后拒绝”的反例:
kubectl -n mesh-lab create serviceaccount intruder
kubectl -n mesh-lab apply -f deploy/mesh-lab/intruder.yaml
kubectl -n mesh-lab rollout status deployment/intruder
kubectl -n mesh-lab exec deploy/intruder -- sh -c `
'wget -S -O- --header="X-Request-Id: mtls-deny-1" http://orders:8080/admin'预期 TCP/TLS 连接由代理以 intruder 身份建立,随后目标数据面返回授权拒绝,orders 应用不记录该 request ID 的业务处理。保存目标代理的 source principal、TLS 安全属性、拒绝原因和策略版本,才能证明失败发生在授权层;只保存 wget 的非零退出码不够。若 intruder 仍成功,检查策略 selector、source principal、执行点和 generation,而不是质疑证书加密。
再做一个未纳管来源反例。在迁移期的 permissive 模式中,它可能以 plaintext 成功,但没有 mTLS identity,不能据此应用身份授权。迁移完成后切换 strict,预期未纳管来源在到达应用前失败。Istio 中 PeerAuthentication 的 selector、port level 与 mode 会改变接收入站 TLS 的方式,字段语义应就近查PeerAuthentication 参考;PERMISSIVE 是过渡状态,不是长期“兼容性更好”的安全基线。
Workload API 与 SDS 为什么能热更新
把证书做成文件并让应用启动时读一次,会把轮换变成重启事务。SPIFFE Workload API 通常通过本地 Unix Domain Socket 向经过 attestation 的进程流式返回完整 SVID 集合、私钥、所属 trust domain bundle、federated bundle 和可选 CRL;后续响应代表当前完整状态,不是只追加一张新证书。客户端若在后续响应中失去某个身份,必须停止使用旧材料。网格常通过 SDS 或自己的 identity service 把材料推给代理,应用无需知道 CA token。
Workload API 并不意味着私钥永远不离开 agent:直接调用 X.509-SVID profile 的客户端会收到未加密 PKCS#8 私钥。因此 socket 文件权限、挂载范围、调用进程 UID、Linux namespace 和 workload attestation selector 共同构成凭证边界。反向实验应让错误 UID 或未命中 selector 的进程访问 socket,预期得到权限拒绝或没有可用身份;若同 Pod 任意进程都能取到同一 SVID,就要收紧 socket 挂载和进程隔离,而不是只依赖“短证书”。
热更新包含两个阶段:控制面签发并送达新材料,数据面把新证书原子地切入新的握手。既有 TCP/HTTP2 连接通常不会重新握手,所以“代理已加载新证书”不等于所有连接都已使用它。验证时既要观察 Secret/SVID 版本变化,也要建立新连接;必要时控制连接最大寿命或分批 drain,避免旧连接无限隐藏轮换结果。客户端更新必须把 leaf、匹配私钥和验证 bundle 视为一个不可拆分快照,不能先替换证书文件、稍后再替换私钥;否则会制造间歇性 key values mismatch 或只在部分握手出现 unknown authority。
SDS/Workload API 断开时,代理可能继续使用缓存中的现有证书,直到过期;新 workload、首次签发和 bundle 更新则可能立即失败。可用性预算必须小于证书剩余有效期,并包含告警、修复与安全回退时间。延长 leaf TTL 会减少签发压力,却扩大泄露凭证的可用窗口;缩短 TTL 会提高 CA、agent 和控制面的轮换峰值,必须用容量数据决定。
轮换实验要看到旧证书、新证书和新连接
先记录 T0 的证书指纹摘要、serial、有效窗口、bundle 摘要、代理配置版本和源/目标 Pod UID,然后触发产品支持的 leaf rotation;不要通过删除生产 Secret 猜测控制器行为。Istio 通常由数据面自动续期,Linkerd、Consul、Kuma 与 Cilium/SPIRE 各有不同 owner 和默认 TTL,动态值要从目标集群配置与证书读取,不能照抄教程默认值。
T0: old leaf loaded, new handshakes use old leaf
T1: issuer creates new leaf before old leaf expires
T2: proxy receives and activates new leaf
T3: new connection presents new leaf; old established connection may survive
T4: old leaf expires; no new handshake accepts it正向判据是 serial 或指纹发生变化,URI SAN 保持预期身份,链仍被对端 bundle 接受,新连接成功,业务错误率没有形成尖峰。反向实验可在隔离环境暂停 identity agent 或阻断它到 issuer 的连接:预期现有连接可能继续,新的续期开始失败,剩余寿命持续下降,并在到期前触发告警。恢复后要看到更新恢复,而不是只看 agent 进程 Ready。
还要做“旧节点缓存”反例:让一个目标副本在轮换期间与 identity service 断开,其余副本正常更新,然后连续建立短连接。预期失败只集中到旧副本,并能由目标 Pod UID、旧 bundle 摘要或旧 leaf serial 定位;若负载均衡把失败摊平,聚合错误率会掩盖单副本未收敛。修复后需要证明该副本加载新材料并让新连接成功,不能只把异常副本摘流后宣告轮换完成。
项目接入时,把证书剩余寿命、轮换成功/失败、Workload API/SDS 断连、CA 签发延迟与 TLS handshake error 纳入统一仪表盘。告警不要固定写“剩 24 小时”,而应根据 leaf TTL、修复时长、发布窗口和夜间值班能力设置,例如在剩余寿命小于“最大修复时间 + 一次完整轮换验证时间”时告警。
时钟是证书系统的隐藏依赖
X.509 验证依赖验证方本地时间。节点时钟落后可能把新证书判断为“尚未生效”,超前则会提前判断“已过期”;CA、agent、代理与排障终端的时间不一致还会制造互相矛盾的日志。
时钟反例只能在隔离 VM 或专用节点执行,不要修改共享 Kubernetes 节点时间。安全做法是使用可注入时钟的 TLS 测试程序或独立 VM:让验证方时间位于 NotBefore 之前,预期新握手失败并出现明确的 not yet valid;再置于 NotAfter 之后,预期出现 expired。恢复准确时间后,新连接应成功。证据保存证书时间窗口、验证方时间、NTP/chrony 同步状态与握手错误,不保存私钥。
生产上监控节点 clock offset、同步源、stratum/同步状态和时间跳变。证书故障出现时,先比较双方和 issuer 时间,再做重启;重启可能改变调度节点,却不会修复时钟治理。
撤销不是“删掉注册条目就立即断线”
短寿命 SVID 常用“停止续签并等待过期”缩短撤销系统复杂度,但它不是即时撤销。删除 SPIRE registration entry、移除 ServiceAccount 或撤销签发权限,只阻止后续获取材料;已经签发的证书仍可能在有效期内被信任,既有连接也可能继续传输。产品是否支持 CRL、OCSP、deny list 或连接主动排空,要按目标实现确认,不能假定传统 Web PKI 的撤销链自动存在。
凭证泄露时先判断影响层级:leaf key 泄露可冻结 identity、拒绝该 principal、停止续签并排空连接;intermediate/issuer key 泄露要切换签发者并缩短信任窗口;root key 泄露意味着 trust domain 的根基失效,需要紧急分发新根、迁移身份并移除旧根。授权 deny 可以比证书自然过期更快阻断业务,但前提是策略控制面和执行点仍可信。
撤销实验应记录四条时间线:注册/签发资格何时移除,旧 leaf 何时不再续期,新连接何时被策略拒绝,既有连接何时 drain 或结束。只有把这四条线都闭合,才知道“撤销”对真实业务意味着几分钟还是一个 TTL。
双根迁移必须先扩信任,再换签发,最后收旧根
根或 trust domain 迁移最危险的错误,是先让 issuer 使用新根。正确顺序使用重叠窗口:
第一阶段只扩展 bundle,不改变签发链。验证旧 leaf 在双根 bundle 下仍成功,同时确认所有代理都已加载新 bundle;控制面已更新、Secret 已修改或对象状态为 Ready 都不能替代数据面证据。第二阶段切换 issuer,让新 leaf 链到 Root B;验证新旧数据面交叉通信。第三阶段等待或主动轮换所有 Root A leaf,并处理仍持有旧连接的长连接。最后通过证书库存、握手抽样和代理 secret 状态证明不再使用 Root A,才从 bundle 移除它。
反向实验在隔离环境故意让一个 orders-v2 仍只信 Root A,再给 client 签 Root B leaf。预期只有命中该副本的握手失败,证据应能定位到目标 Pod 的 bundle 摘要,而不是归因于随机网络抖动。修复是先补发 bundle 并等待数据面生效,不是把所有服务改成 permissive。
如果同时迁移 trust domain,SPIFFE ID 本身也变化,授权策略、federation、审计和应用身份映射都要双读。跨 trust domain federation 是显式信任关系,不应通过复制根私钥实现。Consul 的CA rotation与 Kong Mesh 的mTLS 轮换提供各自状态机,执行前必须按目标版本确认 provider、cross-signing 和回滚支持,不能把一个产品的字段套给另一个产品。
不同实现的身份入口不能互换
Istio 通常把 Kubernetes ServiceAccount 映射为 workload identity,由 istiod 或集成 CA 提供证书;sidecar 和 ambient 的取证路径不同,但都要验证源、目标 identity 与实际执行点。Linkerd 的自动 mTLS要求通信两端都被 mesh,数据面 identity 通常与 ServiceAccount 关联;control-plane issuer 与 trust anchor 的轮换责任不同,操作时参考automatic mTLS和目标稳定发行线的凭据轮换文档。
Cilium mutual authentication 由 Cilium agent 代表 workload 使用 SPIRE 材料,能力仍应按目标 stable 版本的Mutual Authentication成熟度与限制决策。它不同于 WireGuard/IPsec 透明加密,也不同于 L7 授权。
Consul 为服务实例签发 SPIFFE-compatible X.509 证书,内置 CA 或 Vault 等 provider 管理签发;intentions 才决定服务是否可通信。Kuma 默认不因安装就自动开启 mTLS,启用 backend CA 后才签发身份材料,MeshTrafficPermission 又是独立授权层。Kuma/Kong Mesh 的新 MeshIdentity、SPIFFE matcher 与 provider 支持要按目标版本确认成熟度,不能从实验性 API 外推生产保证。
SPIRE 本身提供 node/workload attestation、registration、SVID 与 bundle 分发,不负责业务流量路由,也不自动提供服务网格授权。用 SPIRE 给本地应用直接提供 Workload API 时,应用必须支持 SVID 热更新、bundle 更新、TLS peer ID 验证和连接重建;否则“接入了 SPIFFE”仍可能在第一次轮换时中断。
按第一条失败证据排查
看到 certificate expired 或 not yet valid,先对照验证方时钟和证书窗口。看到 unknown authority,检查对端发送的 chain、验证方 bundle 与中间证书是否完整。看到 bad certificate 或 handshake reset,检查双方是否都要求 client certificate、私钥与证书是否匹配、协议/SNI 是否正确。握手成功后出现明确 RBAC deny,才转入身份映射和授权 selector。HTTP 503 但没有 TLS 错误时,还要检查 endpoint、listener 与连接池,避免所有 503 都归给证书。
控制面 Ready 而新 Pod 拿不到证书时,沿 Pod projected token、node/workload attestation、registration selector、Workload API socket 权限、issuer 可用性、SDS/xDS ACK 逐层检查。旧 Pod 正常、新 Pod 失败通常说明首次签发链路有问题;旧连接正常、新连接失败通常说明新 leaf、bundle 或时间有问题;只有某个副本失败通常说明局部 secret/bundle 未收敛。
排障结束必须用新的连接重跑正确身份允许、错误身份拒绝、未纳管来源拒绝和证书摘要检查。重启后一次 200 只能说明某个副本某次成功,不能关闭事故。
容量、成本与长期信任治理
身份系统的容量由 workload 数、leaf TTL、轮换提前量、重连风暴、CA 签名延迟、bundle 大小和多集群复制决定。若有 N 个工作负载、平均每 T 时间轮换一次,平稳签发速率约为 N/T;滚动发布、节点恢复或根迁移会把它变成突发峰值。压测需要同时观察 CA QPS/P99、agent 队列、SDS/Workload API 推送延迟、代理内存、TLS handshake CPU、失败率和控制面存储,而不是只测业务吞吐。
短 TTL 降低 leaf 泄露窗口,却提高 CA 与控制面成本;长 TTL 降低轮换频率,却让撤销更慢。每服务独立 trust domain 隔离更强,但 federation、bundle 和运维复杂度增加;全组织共享一个根最简单,却扩大根泄露与错误签发的影响半径。多数团队应按安全边界和运维责任划分 trust domain,再用 federation 建立最小跨域信任,而不是按 namespace 数量机械拆分。
长期治理至少维护 trust domain/CA owner、root 与 intermediate 保管位置、离线备份与恢复演练、issuer 权限、SVID TTL、轮换 SLO、时钟 SLO、federation 关系、授权 owner、证书库存、例外到期和退网流程。根私钥使用 KMS/HSM 或受控密钥系统,流水线只取得最小短期权限;CA、bundle、策略与网格升级都经过双人评审和 canary。
清理 mesh-lab 前先撤销临时 registration、token 和测试授权,再移除工作负载纳管并确认直连基线,最后删除 namespace:
kubectl config current-context
kubectl -n mesh-lab get serviceaccount,secret,authorizationpolicy
kubectl delete namespace mesh-lab若实验使用独立 SPIRE entry、外部 CA、KMS key、VM agent 或 DNS 记录,还要在对应系统按资源 owner 清理并核销。不要因为 namespace 已删除就假定外部注册、签发权限和密钥已消失,也不要在共享环境删除根或 CRD。
成熟的 mTLS 系统不是“证书自动续期”这一个功能,而是能持续回答:谁获得了哪个身份,谁签发并信任它,新旧材料如何重叠,失败连接停在哪一层,泄露后多久真正失效,以及加密之后谁仍然没有权限。把这些问题分开取证,绿色图标才不会替代安全事实。
