在金融核心或支付清算类 Kubernetes 集群的日常运维中,服务间认证往往经历过一个“先跑通、再还债”的阶段:早期为了推进业务上线,很多团队直接使用一套静态的对称加密 JWT(即 HS256 共享 Secret)打通集群内外的微服务调用。
随着接入的服务从几个扩展到几十个,并涉及集群外的银行前置机、传统虚拟机和第三方合作机构,这套方案的安全隐患就会集中暴露。如果改用 Kubernetes 原生的 ServiceAccount,凭据到底是平台自动注入还是必须改业务代码?集群外的异构服务接进来时,能不能直接由网关统一认证、集群内无条件信任网关?相比单独部署一套 Keycloak 身份提供商(IdP),原生工作负载身份与专业授权服务各自的边界又在哪里?
本文结合金融级零信任安全规范与云原生前沿实践,逐一拆解这四个核心工程问题,给出可直接落地的架构选型建议。
一、为什么静态对称 JWT Secret 在金融场景下必须淘汰?
许多项目最初选择 HS256 静态密钥,是因为它实现最简单:网关和各个微服务挂载同一个 Kubernetes Secret 或配置项,发送方用这把密钥签发 Token,接收方用同一把密钥验签。但在高安全等级的生产环境中,这种设计存在三个结构性缺陷:
验签权等同于签发权(横向伪造零门槛):在
HS256对称算法下,验证签名与生成签名使用的是完全相同的密钥。假设集群内外有 30 个微服务需要校验调用方身份,这 30 个服务就必须同时持有这把共享 Secret。一旦某个边缘非核心服务(例如报表导出、消息通知)因为依赖漏洞或配置不当被攻破,攻击者拿到环境变量里的 Secret 后,就能直接以最高权限伪造出任意服务的 JWT,长驱直入调用核心账务接口。静态长效与轮转死锁(Rotation Paralysis):为了避免频繁修改配置,静态 Secret 往往长期不更换,甚至签发出的机器 Token 有效期长达数月。一旦需要应急换密,所有签发方与验签方必须同步更新并滚动重启;在严苛的金融变更窗口下,这种牵一发而动全身的操作极易演变成“几年不敢轮转一次密钥”。
缺乏持有者绑定(Bearer 令牌重放):传统静态 JWT 属于“谁持有谁就能用”的凭据。如果 Token 不慎被打印进网关访问日志、APM 链路追踪或经过未加密的内网链路,在过期前的整个窗口期内都可以被直接重放。
注意下图左侧红色警示栏与右侧蓝色高亮栏的结构对比,可以直观看到对称共享密钥与非对称短期工作负载凭证在权限边界上的本质差异:
图1:对称共享密钥(验签方即签发方)与非对称短期凭证(公私钥分离+受众绑定)的安全边界对比
要彻底消除“单点泄露即全网沦陷”的风险,金融级认证底座必须同时满足两条铁律:第一,签发私钥绝对不离开控制面,业务节点只持公钥验签(RS256 / ES256);第二,废除静态长效密码,所有凭据必须绑定具体工作负载实例与目标受众(aud),且具备分钟级自动轮转能力。
二、K8s ServiceAccount 认证:哪些是自动注入,哪些要改代码?
围绕 Kubernetes 原生 ServiceAccount(简称 SA)做服务间认证,运维与研发最容易混淆的问题是:“凭据到底是不是自动注入的?需不需要业务代码配合?”
准确的答案是:凭据的“申请、挂载与定期刷新”由 Kubelet 100% 自动完成,无需任何业务代码介入;但凭据的“读取携带与接收方验签”,则取决于你采用纯原生 SDK 模式还是服务网格下沉模式。
1. 平台自动完成的部分:Projected Volume 与 80% 周期轮转
早期的 Kubernetes(1.21 之前)会把永不过期的 SA Token 存进 etcd 的 Secret 对象里,现已彻底废弃。现代 Kubernetes 默认启用 BoundServiceAccountTokenVolume 机制(基于 TokenRequest API)。
当你在工作负载 YAML 中声明一个带特定受众(audience)和过期时间(expirationSeconds)的投射卷(Projected Volume)时,Kubelet 会自动接管整个凭据生命周期:
apiVersion: v1
kind: Pod
metadata:
name: payment-caller
namespace: finance-core
spec:
serviceAccountName: payment-caller-sa
containers:
- name: app
image: registry.internal/finance/payment-caller:v2.4
volumeMounts:
- mountPath: /var/run/secrets/tokens
name: ledger-api-token
readOnly: true
volumes:
- name: ledger-api-token
projected:
sources:
- serviceAccountToken:
path: ledger-token
expirationSeconds: 1800
audience: "https://ledger-svc.finance-core.svc"
在这个配置下,平台在后台自动完成了两件关键的事:
内存文件系统隔离:Kubelet 向 API Server 的
TokenRequest接口申请一枚带有RS256或ES256非对称签名的 JWT。这枚 Token 不会写入 etcd,而是直接挂载在节点内存盘(tmpfs)中,Pod 销毁即刻从内存抹除。静默自动轮转:Kubelet 会在 Token 达到 80% 生命周期(例如上例 1800 秒有效期的第 24 分钟,或默认 3600 秒有效期的第 48 分钟)或距过期不足 24 小时时,自动向 API Server 申请新 Token,并通过原子符号链接替换的方式更新
/var/run/secrets/tokens/ledger-token文件。
2. 需要业务代码或网格配合的部分:热加载与验签
虽然文件在磁盘上自动更新了,但 HTTP/gRPC 请求并不会自己把文件塞进请求头。注意下图纵向管道中橙色高亮的第 03 步与蓝色第 04 步,这里正是工程落地的分水岭:
图2:Kubernetes Projected ServiceAccount Token 从 Kubelet 动态签发、tmpfs 轮转到客户端携带与服务端验签的四步闭环
如果团队不引入服务网格,而是直接在微服务中基于原生 SA Token 认证,业务代码(或公共基础 SDK)必须处理好以下三个细节:
客户端严禁启动时单次读文件缓存:这是生产环境最常见的踩坑点。很多研发习惯在服务启动初始化时执行一次
os.ReadFile把 Token 读进内存变量,结果服务上线运行到第 31 分钟,旧 Token 过期,后续所有调用全部报401 Unauthorized。客户端 SDK 必须每分钟重新读取一次挂载路径,或者监听文件系统的更新事件(在 Go 语言中,官方client-go的transport.NewBearerAuthWithRefreshRoundTripper已内置该定时重读逻辑)。接收方优先采用 OIDC JWKS 离线验签:接收方(如账务服务
ledger-svc)收到Authorization: Bearer <token>后,如何验证真伪?Kubernetes API Server 原生内置了 OIDC 发现端点(/.well-known/openid-configuration与/openid/v1/jwks)。接收方只需在启动时拉取并定期缓存集群的公钥集合(JWKS),即可在本地内存中完成签名校验、过期时间检查,并严格核对iss(签发者)、sub(调用方身份,格式如system:serviceaccount:finance-core:payment-caller-sa)以及aud(是否等于自身服务标识https://ledger-svc.finance-core.svc)。这种离线验签耗时仅微秒级,且不会给 API Server 带来任何并发压力。高敏感操作叠加
TokenReview实时核验:离线 JWKS 验签有一个细微的时间窗口限制——如果调用方 Pod 被异常销毁,其已签发的短期 Token 在剩余十几分钟内依然能通过公钥签名校验。对于大额资金划拨等核心接口,接收方可以调用 Kubernetes 的TokenReviewAPI,由 API Server 实时核查该 Token 绑定的Pod UID是否仍然存活,并在服务端对校验结果做 10~30 秒的短缓存以保护 API Server。
3. 如何彻底免除代码改造?下沉至 SPIFFE 与服务网格
如果你希望业务代码完全不感知 Token 文件的读取与验签,业界标准做法是将工作负载身份下沉到基础设施层——即采用 SPIFFE(Secure Production Identity Framework for Everyone) 标准与服务网格(如 Istio),或使用在 Kubernetes 1.37 正式步入 GA 稳定态的原生 PodCertificateRequest(KEP-4317)与 ClusterTrustBundle(KEP-3257)机制。
在 Istio(无论是传统 Sidecar 模式,还是无 Sidecar 的 Ambient Mesh 节点级 ztunnel 模式)或 SPIRE 架构下:
数据面代理会自动读取本地 Projected Volume 中的 K8s SA Token,向控制面(如
istiod或 SPIRE Server)证明自己的容器身份,换取一张有效期极短的 X.509-SVID 证书(证书的 SAN 扩展字段中写入标准身份标识spiffe://cluster.local/ns/finance-core/sa/payment-caller-sa)。当服务 A 调用服务 B 时,底层的 Envoy 或
ztunnel代理自动建立双向 TLS(mTLS)加密通道,并在握手阶段完成双向证书验证。运维人员只需编写声明式的
AuthorizationPolicy,即可精确控制“哪个 Namespace 下的哪个 ServiceAccount 允许调用哪个微服务的哪个路径”,业务代码无需编写一行认证逻辑。
三、兼容集群外服务:“集群内无条件信任网关”可行吗?
在实际生产环境中,很少有金融系统是 100% 运行在单个 Kubernetes 集群内的。集群外的核心银行主机、遗留虚拟机应用以及外部合作机构的回调请求,都需要调用集群内部的服务。
这时很多架构师会产生一个直觉想法:“是不是让南北向 API 网关负责对接和认证集群外的服务,只要请求进了网关,集群内部的服务直接无条件信任网关转发过来的请求就行了?”
1. 为什么“边界网关验一次、内网裸奔”在金融级场景下极其危险?
这种“外紧内松”的边界安全模型(Perimeter Security),在零信任(Zero Trust)和金融合规审计中属于典型的反模式,主要面临两大攻击面:
内网横向旁路攻击(East-West Bypass):如果网关认证完外部调用方后,只是剥离外部凭证、往后端转发普通的明文请求(比如塞一个自定义的
X-Caller-Org: bank-partner-a请求头),而集群内部服务只要看到流量来自集群内网就无条件信任这个 Header——那么一旦集群内任何一个边缘 Pod(例如对外提供静态资源或营销活动的容器)被攻陷,攻击者就能直接从该 Pod 内部向核心账务服务发起请求,手工伪造一行X-Caller-Org: bank-partner-a,彻底绕过 API 网关的所有安全防线。混淆代理问题(Confused Deputy):即使内部服务通过网络策略或 mTLS 限制了“只允许 API 网关的 Pod 连接我”,如果网关不把原始外部调用方的密码学凭证或签名上下文传递进来,后端核心服务就只能看到“请求来自网关”,而无法独立核实“到底是哪个外部机构发起的请求、网关有没有因为路由配置错误而越权转发”。
2. 金融级标准解法:南北向强认证 + 网关令牌交换(Token Exchange)+ 东西向双层校验
正确的做法不是让内部服务“盲目信任网关”,而是建立 “网关边界认证与令牌交换 + 集群内传输层与应用层双重校验” 的纵深防御体系。
注意下图顶部的橙色外部域如何通过中间的边界网关完成令牌转换,并在底部绿色核心服务处执行传输层与应用层的双重校验:
图3:集群外部服务经由边界网关执行 RFC 8693 令牌交换,并在集群内部结合 SPIFFE mTLS 与 JWKS 实现双层零信任校验
整条跨边界调用链路由三个严密咬合的环节组成:
第一环(南北向接入认证):集群外的服务或前置机访问集群边界网关时,废除静态 API Key,优先采用 双向 mTLS 客户端证书认证(遵循 RFC 8705 证书绑定规范) 或 非对称私钥签名的 JWT 断言(遵循 RFC 7523
private_key_jwt规范)。外部系统自己保管私钥,网关或外部身份中心只保存其公钥证书。第二环(网关层令牌交换:RFC 8693 Token Exchange / 事务令牌 Txn-Token):API 网关验证外部请求合法后,绝不直接透传外部原始长效 Token,也不传无签名的裸 Header。网关利用自身的密钥(或调用内部令牌服务),执行标准的 OAuth 2.0 Token Exchange(RFC 8693) 或签发 IETF 事务令牌(Transaction Token),生成一枚有效期极短(例如 TTL 仅 30~60 秒)、仅供本次内部调用链使用的非对称签名 JWT。这枚内部令牌的载荷中明确包含三个关键字段: -
sub(Subject):记录最初发起调用的外部机构或外部服务真实身份; -act(Actor):记录当前执行代理转发的网关身份; -aud(Audience):锁定本次请求允许访问的集群内目标微服务。第三环(集群内双重零信任校验):当请求从网关到达集群内的核心账务服务时,核心服务执行两道独立校验: - 传输层校验(防内网旁路):通过服务网格 mTLS(或网络策略 + 节点证书)验证建立连接的对端 SPIFFE ID 必须是合法的网关身份(例如
spiffe://cluster.local/ns/ingress/sa/api-gateway)。任何其他 Pod 直接发来的请求在 TLS 握手阶段即被拒绝。 - 应用层校验(防越权与混淆代理):核心服务(或其 Sidecar 的RequestAuthentication策略)使用内部公钥(JWKS)验证网关换发出来的短期内部 JWT,核实sub外部调用方是否具备访问当前业务接口的权限。
这样一来,即便内网某个边缘 Pod 被黑客控制,它既没有网关的 SPIFFE mTLS 私钥,也没有签发内部事务令牌的私钥,内网横向伪造的路就被彻底堵死了。
四、原生 K8s SA / SPIFFE 对比 Keycloak:优势、限制与融合之道
很多团队在重构认证体系时会纠结另一个核心选型:既然有 Keycloak 这样成熟的开源身份与访问管理(IAM / IdP)系统,我们是应该全盘引入 Keycloak,还是直接用 Kubernetes 原生 SA 和 SPIFFE?
要做出正确选择,首先要看清这两类技术在零信任架构中处于完全不同的层级。
1. 本质差异:基础设施身份(我是谁) vs 业务授权与联邦(我能干什么)
Kubernetes SA / SPIFFE(工作负载身份层):解决的是 “这个进程或容器到底是谁(Who am I)”。它的核心优势是 消除了“初始密钥分发难题(Secret Zero Problem)”。因为容器就跑在 Kubelet 和操作系统内核眼皮底下,平台可以通过进程空间、cgroup、Pod UID 直接检验容器的真实性并自动注入短期凭据,不需要人为预置第一个密码。但它的限制也很明显:它签发的身份是基础设施维度的(仅包含 Namespace 和 ServiceAccount 名称),不包含业务级的细粒度权限范围(Scopes)、商业租户属性,也无法直接给集群外没有 Kubelet 的第三方合作方签发身份。
Keycloak(OAuth2 / OIDC 授权与联邦层):解决的是 “这个调用方具备哪些业务权限(What can it do)” 以及跨异构平台、人机混合调用的统一认证管理。Keycloak 提供了完善的 Client 管理、Scope 权限映射、多租户 Realm 隔离、令牌交换(Token Exchange)以及审计日志。但如果在 Kubernetes 内部微服务互调时生搬硬套传统的 Keycloak
Client Credentials(客户端凭证模式),会立刻踩中两个大坑: 1. 初始密钥死灰复燃(Credential Zero):传统模式下,每个微服务去向 Keycloak 申请 Token 时,必须先提供一个静态的client_secret。这把client_secret怎么安全地放进 Pod、怎么定期轮转?你又回到了静态密码管理的老路上。 2. 中心化状态组件的可用性瓶颈:Keycloak 是依赖关系型数据库和分布式缓存的重状态系统。如果集群内部成百上千个微服务每次东西向调用都去请求 Keycloak,或者高频调用实时内省接口,Keycloak 的抖动或数据库连接池耗尽就会瞬间演变成全集群瘫痪的单点故障(SPOF)。
2. 前沿最佳实践:分层自治 + 无密钥联邦认证(Workload Identity Federation)
在现代金融级云原生架构中,原生工作负载身份(K8s SA / SPIFFE)与 Keycloak 不是二选一的互斥关系,而是“内网用平台原生身份、跨边界用 Keycloak 联邦交换”的黄金组合。
特别是当集群内的某个服务确实需要向 Keycloak 申请一枚带有复杂业务 Scope 的 OAuth2 Access Token(例如去调用跨部门的集中式网关或外部合规接口)时,可以利用 Keycloak 原生支持的 Federated JWT Client Authentication(基于 RFC 7523 的联邦客户端认证),彻底消灭 client_secret:
配置 Keycloak 信任 K8s OIDC 公钥:在 Keycloak 的 Client 认证设置中,将客户端认证方式从传统的
Client Id and Secret改为Signed Jwt,并配置信任 Kubernetes 集群暴露的 JWKS 公钥端点。用 K8s SA Token 直接换取业务 Token:Pod 内部不需要存放任何 Keycloak 密码。业务容器直接读取 Kubelet 自动挂载并每小时轮转的 ServiceAccount JWT,将其作为
client_assertion参数发送给 Keycloak 的/token接口:
curl -X POST "https://idp.internal.bank/realms/finance/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \
-d "client_assertion=$(cat /var/run/secrets/tokens/keycloak-assertion-token)" \
-d "scope=settlement:write ledger:read"
全链路零静态密钥:Keycloak 收到请求后,用 K8s 的公钥验证这枚
client_assertion确实是由集群 API Server 为该特定 Pod 签发的,随后直接返回带有settlement:write等业务权限的 OAuth2 Access Token。整条链路没有一行静态密码落盘。
五、全场景选型对照与演进路线建议
为了便于结合团队当前的运维人力与架构阶段做决策,下图从密钥分发与轮转机制、代码侵入度及适用边界四个维度,对四种主流方案进行了完整对比(注意第 1 行红色标注的静态对称密钥反模式与后三行动态凭证方案的演进关系):
图4:金融级 Kubernetes 服务间认证四类方案在密钥轮转、代码侵入度与适用边界上的全景对照表
针对目前正在使用静态对称 JWT Secret、希望向金融级高安全架构演进的团队,建议采取以下分阶段平滑迁移路线,避免一次性推倒重来带来的业务中断风险:
第一阶段(止血与公私钥解耦,1~2 周):如果暂时无法全面改造调用链路,先把静态
HS256对称签名切换为RS256或ES256非对称签名。仅允许网关或专门的认证服务持有私钥签发 Token,所有下游业务微服务只挂载公钥(或拉取 JWKS)做只读验签,并强制开启aud(目标受众)校验。这一步只需改动配置与验签公钥加载逻辑,就能立刻斩断“边缘服务泄露密钥即可伪造全网身份”的最大威胁。第二阶段(集群内东西向切换为动态工作负载身份,2~4 周): - 若团队基础设施精简、服务数量在十几个以内:直接启用 Kubernetes Projected ServiceAccount Token,在公共 RPC/HTTP 客户端封装定期重读挂载文件的逻辑,接收端通过集群 OIDC JWKS 离线验签。 - 若服务数量众多、多语言异构,或希望彻底解放业务研发:引入 Istio(推荐评估资源开销更低的 Ambient 模式)或 SPIRE,通过底层自动签发的短期 X.509-SVID 证书实现全集群双向 mTLS 与
AuthorizationPolicy访问控制。第三阶段(南北向网关令牌交换与 Keycloak 无密钥联邦,按需推进):针对集群外接入的银行前置机、合作伙伴及跨集群调用,部署 Keycloak 作为边界授权中心。外部调用方通过双向 mTLS 或私钥 JWT 接入 API 网关,网关执行 RFC 8693 Token Exchange 换发短周期的内部事务令牌向集群内透传;集群内服务需要访问受 Keycloak 保护的外部资源时,统一使用 K8s SA Token 作为
client_assertion联邦换票,最终实现集群内外全链路“零静态密钥”的金融级安全闭环。

