Kubernetes ExternalJWT 全解析:Service Account 外部签名与密钥管理(proto 契约 + kube-apiserver 集成原理)
【免费下载链接】kubernetesProduction-Grade Container Scheduling and Management项目地址: https://gitcode.com/GitHub_Trending/kuber/kubernetes
ExternalJWT 是 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 repository(staging 区域仓库),它服务于主 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 # 独立 module:k8s.io/externaljwt └── README.md / CONTRIBUTING.md / code-of-conduct.md / LICENSE ...v1与v1alpha1两套版本的 api.proto 内容当前完全一致,仅在 proto 的package与go_package(k8s.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")。服务包含三个 RPC:Sign、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 与 signature |
FetchKeys | ① 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必须为"JWT";kid必须非空、长度≤1024字符,且其对应的公钥不得被标记为从 OIDC Discovery 排除(否则签发的令牌无法被标准 OIDC 依赖方验证);alg必须是 kube-apiserver 当前支持的算法:RS256、ES256、ES384、ES512;- header不得包含任何 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 字符)与key(PKIX 序列化的公钥字节,当前须为 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_seconds,kube-apiserver 视为配置错误并退出; - 若
--service-account-max-token-expiration未显式设置,kube-apiserver默认采用max_token_expiration_seconds; - 若
--service-account-extend-token-expiration=true,则延长后的令牌过期时间为min(1 年, max_token_expiration_seconds)。
同时,max_token_expiration_seconds必须至少为 600 秒(proto 注释的显式下限约束)。
三、kube-apiserver 侧如何接入 ExternalJWT
3.1 开关与启用前提
ExternalJWT 能力由 feature gateExternalServiceAccountTokenSigner控制(定义见 pkg/features/kube_features.go,其演进记录清晰可见):
| 版本 | 状态 | 默认值 |
|---|---|---|
| v1.32 | Alpha | false(需显式开启) |
| v1.34 | Beta | true |
| v1.36 | GA(LockToDefault) | true(不可关闭) |
启用后,即可为 kube-apiserver 传入--service-account-signing-endpoint参数,其 flag 定义位于 pkg/controlplane/apiserver/options/options.go:
--service-account-signing-endpoint:Path 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 为参数拨号,并以keySyncTimeout(60 秒)完成首次密钥缓存填充(initialFill)。 - 请求元数据并校验寿命:以 10 秒超时调用
GetServiceMetadata,若返回的max_token_expiration_seconds小于允许的最小令牌寿命则报错退出;随后执行前述 defaulting 规则——--service-account-max-token-expiration未设置时采用外部 signer 上报值、显式设置但超限时报错退出,并同步收敛延长过期(extended expiration)的上限min(max_expiration, signer 上限)。 - 装配组件:将外部 signer 插件注册为 Service Account token generator(
ServiceAccountIssuer),并把 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 到完整 JWT
4.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 的强校验(validateJWTHeader)
kube-apiserver 不会盲信外部 signer 返回的任何 header,validateJWTHeader 实现了与 proto 注释一一对应的校验:
- 用
json.Decoder且DisallowUnknownFields()解析 header——任何多余字段都会导致解析失败,落实了 "header cannot have any additional data"; typ必须等于"JWT";kid非空、且 ≤1024 字节;alg必须在白名单RS256 / ES256 / ES384 / ES512内;源码注释特别强调这份白名单必须与pkg/serviceaccount/jwt.go的signerFromRSAPrivateKey/signerFromECDSAPrivateKey以及测试镜像openidmetadata.go中的SupportedSigningAlgs保持同步——这是理解"算法支持面"的关键交叉引用点;- 在
allowSigningWithNonOIDCKeys=false(kube-apiserver 默认以false调用plugin.New)时,若kid对应的公钥带有ExcludeFromOIDCDiscovery标记,则拒绝该签名——防止外部 signer 用仅供验签的 legacy 密钥签发新令牌。
4.3 密钥缓存与轮换
由于每次验签都实时问询 signer 代价过高,plugin 内建了密钥缓存(keycache.go):
- 启动填充:
initialFill在keySyncTimeout(60s)窗口内完成首次密钥同步,失败则整个 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/typ,typ="JWT",kid非空且 ≤1024,alg属于 {RS256, ES256, ES384, ES512},header 与 signature 均为 URL-safe base64; - 密钥管理:返回 PKIX 序列化公钥(RSA/ECDSA);为仅验签的 legacy 密钥设置
exclude_from_oidc_discovery=true且保证绝不用其签发;提供合理的refresh_hint_seconds(>0);尽量返回可信的data_timestamp; - 寿命宣告:
Metadata.max_token_expiration_seconds≥ 600 秒,并与 kube-apiserver 的--service-account-max-token-expiration/--service-account-extend-token-expiration语义配合(apiserver 超限会拒启); - 调用方侧配置:kube-apiserver 需启用
ExternalServiceAccountTokenSigner(v1.32+,v1.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),仅供参考