Kubernetes ExternalJWT 全解析:Service Account 外部签名与密钥管理(proto 契约 + kube-apiserver 集成原理)

发布时间:2026/9/8 22:48:40
Kubernetes ExternalJWT 全解析:Service Account 外部签名与密钥管理(proto 契约 + kube-apiserver 集成原理) Kubernetes ExternalJWT 全解析Service Account 外部签名与密钥管理proto 契约 kube-apiserver 集成原理【免费下载链接】kubernetesProduction-Grade Container Scheduling and Management项目地址: https://gitcode.com/GitHub_Trending/kuber/kubernetesExternalJWT 是 Kubernetes 面向服务账号Service Account令牌签发场景定义的对外 gRPC 接口契约它允许把 JWT 的签名动作与验签公钥的保管从 kube-apiserver 进程内部剥离出去交给一个运行在本地 Unix Domain Socket 上的外部进程ExternalJWTSigner完成。本文将以 staging/src/k8s.io/externaljwt/README.md 为核心骨架结合仓库内完整的.proto定义、kube-apiserver 侧的接入源码与 feature gate 演进记录讲清 ExternalJWT 的接口设计、调用链与配置方式。读完你将能理解该 proto API 每个字段的约束含义掌握--service-account-signing-endpoint的启用前提与校验规则并具备自行实现一个兼容外部签名器的能力。一、ExternalJWT 是什么1.1 仓库定位与作用ExternalJWT 是一个staged repositorystaging 区域仓库它服务于主 Kubernetes 仓库的模块化管理真实贡献issue、PR都发生在主仓库这里的内容由主仓库自动发布而来仓库本身只读、仅用于作为独立 module 被导入参见其 README.md 顶部说明及 CONTRIBUTING.md。其核心使命在 docs.go 中一句话概括Package externaljwt contains the proto definitions for the ExternalJWTSigner.也就是这一仓库承载了让 Kubernetes 接入「外部 JWT 签名与密钥管理」所需的全部 proto API 定义对应 Kubernetes Enhancement Proposal 中的 Service Account External Signing 主题SIG-Auth 下的子项目方向。社区沟通渠道包括#sig-authSlack 频道与kubernetes-sig-auth邮件列表参与受 code-of-conduct.md 约束。从源码结构看包内实际交付物非常聚焦staging/src/k8s.io/externaljwt/ ├── apis/ │ ├── v1/ # api.proto api.pb.go api_grpc.pb.go │ └── v1alpha1/ # api.proto api.pb.go api_grpc.pb.go ├── docs.go # 包级文档 ├── go.mod # 独立 modulek8s.io/externaljwt └── README.md / CONTRIBUTING.md / code-of-conduct.md / LICENSE ...v1与v1alpha1两套版本的 api.proto 内容当前完全一致仅在 proto 的package与go_packagek8s.io/externaljwt/apis/v1、k8s.io/externaljwt/apis/v1alpha1上区分体现「先 alpha 固化、再升 v1」的演进路径。生成代码由hack/update-codegen.sh protobindings生成api.pb.go、api_grpc.pb.go其 go.mod 以独立 module 发布依赖google.golang.org/grpc与google.golang.org/protobuf外部 signer 插件可直接以k8s.io/externaljwt为依赖引入这些生成的客户端/服务端桩代码。1.2 为什么需要外部签名从 proto 注释可以还原这一机制要解决的问题传统模式下kube-apiserver 直接持有服务账号签发私钥由--service-account-signing-key-file指定长期驻留进程内存并参与每次令牌签发而 ExternalJWT 把两件事外置JWT 签名signing——签名私钥不再进入 kube-apiserver 进程而是留在外部 signer 侧例如托管于 HSM/云 KMS/专用签名服务kube-apiserver 只负责组装 claims 并请求签名公钥管理与分发key management——验签公钥集由外部 signer 统一维护与轮换kube-apiserver 通过 FetchKeys 拉取既用于校验存量令牌也用于向 OIDC Discovery 的 JWKS 端点提供数据。由此获得集中式密钥治理、轮换不重启 apiserver、以及私钥最小暴露面等能力。需要强调的是以下所有字段语义与流程细节均出自本仓库源码可直接追溯验证。二、接口契约ExternalJWTSigner gRPC 服务整个模块只定义一个服务ExternalJWTSigner它由本地 Unix Domain Socket 上的进程提供源码注释明确 This service is served by a process on a local Unix Domain Socket。服务包含三个 RPCSign、FetchKeys、Metadata。syntax proto3; package v1; option go_package k8s.io/externaljwt/apis/v1; import google/protobuf/timestamp.proto; // This service is served by a process on a local Unix Domain Socket. service ExternalJWTSigner { rpc Sign(SignJWTRequest) returns (SignJWTResponse) {} rpc FetchKeys(FetchKeysRequest) returns (FetchKeysResponse) {} rpc Metadata(MetadataRequest) returns (MetadataResponse) {} }三者的调用时机与用途依据 proto 注释RPC调用方触发时机用途Sign每次需要签发新的 Service Account 令牌对 JWT payload 进行签名返回 header 与 signatureFetchKeys① kube-apiserver 校验来自 Service Account issuer、但key id 未知的 JWT 时②周期性调用拉取受信任的验签公钥集合为 OIDC JWKS 端点供数Metadata启动时调用一次向 kube-apiserver 共享 signer 的元信息如支持的最大令牌寿命用于 token lifetime 的 defaulting/validation下面逐个拆解消息体与其字段约束这是实现兼容 signer 时必须严格遵守的「契约」。2.1 Sign让外部签名器完成签名message SignJWTRequest { // URL-safe base64 wrapped payload to be signed. // Exactly as it appears in the second segment of the JWT string claims 1; } message SignJWTResponse { // header must contain only alg, kid, typ claims. // typ must be JWT. // kid must be non-empty, 1024 characters, and its corresponding public key should not be excluded from OIDC discovery. // alg must be one of the algorithms supported by kube-apiserver (currently RS256, ES256, ES384, ES512). // header cannot have any additional data that kube-apiserver does not recognize. // Already wrapped in URL-safe base64, exactly as it appears in the first segment of the JWT. string header 1; // The signature for the JWT. // Already wrapped in URL-safe base64, exactly as it appears in the final segment of the JWT. string signature 2; }请求claims是已经过URL-safe base64 编码的 JWT payload即 JWT 三段中的第二段原文signer 不需解析 payload 内容。响应header与signature均已 base64url 包装kube-apiserver 拿到后可直接拼接出header.payload.signature形式的完整令牌。签名对象约定为base64url(header) . base64url(payload)。响应 header 的约束是整份契约最严格之处逐条列出header 只允许含alg、kid、typ三个声明typ必须为JWTkid必须非空、长度≤1024字符且其对应的公钥不得被标记为从 OIDC Discovery 排除否则签发的令牌无法被标准 OIDC 依赖方验证alg必须是 kube-apiserver 当前支持的算法RS256、ES256、ES384、ES512header不得包含任何 kube-apiserver 无法识别的额外字段。2.2 FetchKeys公钥集合的获取与刷新message FetchKeysRequest {} message FetchKeysResponse { repeated Key keys 1; // The timestamp when this data was pulled from the authoritative source of // truth for verification keys. google.protobuf.Timestamp data_timestamp 2; // refresh interval for verification keys to pick changes if any. // any value 0 is considered a misconfiguration. int64 refresh_hint_seconds 3; } message Key { // A unique identifier for this key. // Length must be 1024. string key_id 1; // The public key, PKIX-serialized. // must be a public key supported by kube-apiserver (currently RSA 256 or ECDSA 256/384/521) bytes key 2; // Set only for keys that are not used to sign bound tokens. // eg: supported keys for legacy tokens. // If set, key is used for verification but excluded from OIDC discovery docs. // if set, external signer should not use this key to sign a JWT. bool exclude_from_oidc_discovery 3; }keys验签公钥集合。每个Key由key_id唯一标识≤1024 字符与keyPKIX 序列化的公钥字节当前须为 RSA 256 或 ECDSA 256/384/521 类型构成。exclude_from_oidc_discovery仅对不用于签发 bound token 的密钥例如仅用于校验 legacy 令牌的旧密钥置 true。置 true 后该密钥仍可用于验证但会被排除在 OIDC Discovery 文档之外且外部 signer不得再用它签发新 JWT。这一字段在下一节的签名校验逻辑中起着关键作用。data_timestamp这批密钥从权威来源拉取的时刻。proto 注释说明 kube-apiserver 可以把它导出为 metrics用于支撑端到端 SLO 观测。refresh_hint_seconds密钥刷新的提示间隔供 kube-apiserver 决定轮询节奏以尽快感知轮换任何 ≤0 的值都会被视作配置错误。2.3 Metadata启动期的一次性握手message MetadataRequest {} message MetadataResponse { // used by kube-apiserver for defaulting/validation of JWT lifetime while // accounting for configuration flag values: // 1. --service-account-max-token-expiration // 2. --service-account-extend-token-expiration int64 max_token_expiration_seconds 1; }MetadataResponse目前只有一个字段max_token_expiration_seconds即该 signer 支持签发的令牌最大寿命秒。proto 注释给出三条硬性规则若管理员显式设置的--service-account-max-token-expiration大于max_token_expiration_secondskube-apiserver 视为配置错误并退出若--service-account-max-token-expiration未显式设置kube-apiserver默认采用max_token_expiration_seconds若--service-account-extend-token-expirationtrue则延长后的令牌过期时间为min(1 年, max_token_expiration_seconds)。同时max_token_expiration_seconds必须至少为 600 秒proto 注释的显式下限约束。三、kube-apiserver 侧如何接入 ExternalJWT3.1 开关与启用前提ExternalJWT 能力由 feature gateExternalServiceAccountTokenSigner控制定义见 pkg/features/kube_features.go其演进记录清晰可见版本状态默认值v1.32Alphafalse需显式开启v1.34Betatruev1.36GALockToDefaulttrue不可关闭启用后即可为 kube-apiserver 传入--service-account-signing-endpoint参数其 flag 定义位于 pkg/controlplane/apiserver/options/options.go--service-account-signing-endpointPath to socket where an external JWT signer is listening.This flag is mutually exclusive with--service-account-signing-key-fileand--service-account-key-file.Requires enabling feature gate (ExternalServiceAccountTokenSigner).也就是说外部签名模式与传统本地私钥签名是二选一的关系一旦指定 endpoint就不能再给--service-account-signing-key-file/--service-account-key-file。3.2 启动初始化流程在 options.go 的配置完成逻辑中可以看到完整的初始化与自检链路互斥校验--service-account-signing-endpoint与--service-account-signing-key-file同时设置即报错两者 mutually exclusive。建立 gRPC 连接并填充密钥缓存调用plugin.New(...)以 issuer、socket path 为参数拨号并以keySyncTimeout60 秒完成首次密钥缓存填充initialFill。请求元数据并校验寿命以 10 秒超时调用GetServiceMetadata若返回的max_token_expiration_seconds小于允许的最小令牌寿命则报错退出随后执行前述 defaulting 规则——--service-account-max-token-expiration未设置时采用外部 signer 上报值、显式设置但超限时报错退出并同步收敛延长过期extended expiration的上限min(max_expiration, signer 上限)。装配组件将外部 signer 插件注册为 Service Account token generatorServiceAccountIssuer并把 key cache 注册为外部公钥获取器ExternalPublicKeysGetter供后续验签与 OIDC JWKS 供数使用。3.3 客户端实现plugin 包主仓库中真正的客户端实现位于 pkg/serviceaccount/externaljwt/plugin/plugin.go。其连接建立方式精确呼应了 proto 的 local Unix Domain Socket 定位conn, err : grpc.Dial( socketPath, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithAuthority(localhost), grpc.WithDefaultCallOptions(grpc.WaitForReady(true)), grpc.WithContextDialer(func(ctx context.Context, path string) (net.Conn, error) { return (net.Dialer{}).DialContext(ctx, unix, path) }), grpc.WithChainUnaryInterceptor(externaljwtmetrics.OuboundRequestMetricsInterceptor), )要点走unix协议拨号本地 socket传输层无 TLS本机可信进程间通信由文件系统权限隔离WaitForReady(true)保证 signer 暂时不可用时调用会等待而非立刻失败通过 gRPC 拦截器上报出站请求指标配合 token 生成成功/失败指标做可观测性见 metrics/metrics.go。Plugin结构体持有ExternalJWTSignerClient、issuer 与 keyCache实现 kube-apiserver 需要的 token generator 接口GenerateToken、GetServiceMetadata。四、运行时工作流从 claims 到完整 JWT4.1 签名主流程一次外部签名完整经过以下环节plugin.go 的signAndAssembleJWT合并 claims利用serviceaccount.GenerateToken与一个payload 抓取器payloadGrabber实现jose.Signer接口但只截取序列化后的 payload 字节得到完整 JSON payload编码对 payload 做base64.RawURLEncoding得到 JWT 第二段发起SignRPC将编码后的 claims 放入SignJWTRequest校验 header调用validateJWTHeader对返回的 header 做严格检查见下节拼装将response.Header . payloadBase64 . response.Signature拼成最终令牌字符串返回。生成的最终 JWT 与标准三段式结构header.payload.signature完全一致因此下游消费者API server 自身、OIDC 依赖方无需感知签名发生在进程外。4.2 返回 header 的强校验validateJWTHeaderkube-apiserver 不会盲信外部 signer 返回的任何 headervalidateJWTHeader 实现了与 proto 注释一一对应的校验用json.Decoder且DisallowUnknownFields()解析 header——任何多余字段都会导致解析失败落实了 header cannot have any additional datatyp必须等于JWTkid非空、且 ≤1024 字节alg必须在白名单RS256 / ES256 / ES384 / ES512内源码注释特别强调这份白名单必须与pkg/serviceaccount/jwt.go的signerFromRSAPrivateKey/signerFromECDSAPrivateKey以及测试镜像openidmetadata.go中的SupportedSigningAlgs保持同步——这是理解算法支持面的关键交叉引用点在allowSigningWithNonOIDCKeysfalsekube-apiserver 默认以false调用plugin.New时若kid对应的公钥带有ExcludeFromOIDCDiscovery标记则拒绝该签名——防止外部 signer 用仅供验签的 legacy 密钥签发新令牌。4.3 密钥缓存与轮换由于每次验签都实时问询 signer 代价过高plugin 内建了密钥缓存keycache.go启动填充initialFill在keySyncTimeout60s窗口内完成首次密钥同步失败则整个 apiserver 启动失败fail-fast周期同步scheduleSync在后台周期性刷新其节奏与 FetchKeys 响应中的refresh_hint_seconds语义呼应——该字段被设计为让 signer 主动告知多久轮换一次密钥以便缓存及时跟上按需触发当遇到未知kid的 JWT可能是轮换窗口内签发的新令牌时触发拉取保证轮换期间新旧令牌都能被验证。配合 OIDC Discovery 侧公开密钥未被排除的Key会被用于 JWKS 端点使标准 OIDC 客户端也能校验 Service Account 令牌。4.4 可观测性externaljwt 客户端接入独立的指标命名空间metrics/metrics.go涵盖出站 RPC经拦截器与 token 生成结果RecordTokenGenAttempt等观测点proto 注释还建议把 FetchKeys 返回的data_timestamp暴露为 metric用于端到端 SLO例如验签密钥数据新鲜度。相关行为均有单元测试与 mock 支撑可参见 plugin_test.go、keycache_test.go、metrics_test.go 及官方提供的 gRPC mock 桩 externalsigner_mock.go后者是实现端到端测试或自研插件联调时的现成参考。五、基于该契约实现外部 signer 的清单综合本文若要在仓库或k8s.io/externaljwt独立依赖之外实现一个兼容的 ExternalJWTSigner需要满足部署形态以本机进程 Unix Domain Socket 提供服务对应--service-account-signing-endpoint传入的 socket 路径无 TLS 但有本机访问控制实现三个方法Sign、FetchKeys、Metadata可复用k8s.io/externaljwt/apis/v1生成的 gRPC 桩代码签名的输出纪律header 仅含alg/kid/typtypJWTkid非空且 ≤1024alg属于 {RS256, ES256, ES384, ES512}header 与 signature 均为 URL-safe base64密钥管理返回 PKIX 序列化公钥RSA/ECDSA为仅验签的 legacy 密钥设置exclude_from_oidc_discoverytrue且保证绝不用其签发提供合理的refresh_hint_seconds0尽量返回可信的data_timestamp寿命宣告Metadata.max_token_expiration_seconds≥ 600 秒并与 kube-apiserver 的--service-account-max-token-expiration/--service-account-extend-token-expiration语义配合apiserver 超限会拒启调用方侧配置kube-apiserver 需启用ExternalServiceAccountTokenSignerv1.32v1.36 起默认 GA使用--service-account-signing-endpoint且不得与--service-account-signing-key-file/--service-account-key-file混用。六、小结ExternalJWT 通过一份极精简的 proto 契约v1 / v1alpha1把 Service Account 令牌的签名与密钥管理从 kube-apiserver 内部解耦到独立进程从而支持集中式密钥托管、无感轮换与私有密钥最小暴露。它不定义签发策略只定义通信边界——kube-apiserver 负责组装 claims 与对外输出外部 signer 负责签名与公钥治理二者在字段约束、寿命语义、密钥排除规则上互相校验环环相扣。无论是云厂商托管控制面、需要对接 HSM/KMS 的企业集群还是希望统一密钥生命周期的平台团队这套接口都给出了一个事实上的标准扩展点理解它等于理解了现代 Kubernetes 服务账号令牌体系的一个关键可选组件。【免费下载链接】kubernetesProduction-Grade Container Scheduling and Management项目地址: https://gitcode.com/GitHub_Trending/kuber/kubernetes创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考