Kubernetes 1.38 镜像拉取凭据身份化:KEP-4412 投射 ServiceAccount Token 落地

要解决的问题

私有镜像仓库的凭据一直是 Kubernetes 里一个不太优雅的地方。常见做法有两种:把仓库凭据做成 Secret 放在集群里,通过 imagePullSecrets 引用;或者把凭据直接配置到节点上,让整个节点共用一份。前者的问题是密钥长期存在、轮换麻烦、容易被滥用;后者的问题是凭据与节点绑定而不是与工作负载绑定,任何在这个节点上调度起来的 Pod 都共享同一份权限。

KEP-4412 想改的就是这件事。它的做法是:让 kubelet 在拉取镜像前,向 API Server 申请一枚短生命周期、限定受众、与 Pod 绑定的 ServiceAccount Token,再把这枚 token 交给一个凭据提供者插件。插件可以用它去换取真正的仓库凭据,也可以直接把它当作拉取凭据使用(来源:Kubernetes 官方周报与 KEP-4412 文档,2026-10)。

功能状态是:v1.33 进入 Alpha,v1.34 进入 Beta,目标在 v1.38 转 GA,并锁定两个特性门控。

请求流程与受众限制

理解这个特性,关键是理解"受众(audience)"这层约束。

完整的时序是这样的:kubelet 准备拉取镜像时,先识别这个 Pod 使用的 ServiceAccount 与相关注解,然后向 API Server 发起 token 请求,请求中带上目标受众。API Server 会校验这个节点是否有权请求该受众的 token——这一步是安全边界所在,节点不能随意索取任意受众的 token。拿到 token 后,kubelet 把它和选定的 ServiceAccount 注解一起传给凭据提供者插件。插件完成换取或直接使用,返回给容器运行时。

几个关键点:

第一,token 是绑定 Pod 的。它不像节点级凭据那样被整台机器共享,而是只对应当前这个工作负载身份。

第二,受众受限。API Server 会限制节点可以请求哪些受众,这阻断了拿 token 去访问其他服务的可能。

第三,短生命周期。这是安全收益,但也是最大的运维坑——后面会展开。

第四,容器运行时只在拉取开始时拿到凭据。这意味着凭据不需要在节点上持久化,但反过来也带来一个限制,见下文。

关键配置项

管理员通过 tokenAttributes 让某个提供者加入这个机制,并决定凭据缓存策略。

tokenAttributes 的作用是声明这个凭据提供者需要 token,以及需要哪些 ServiceAccount 注解一起传递。凭据提供者可以据此判断该给谁换什么权限。配置样例形态如下:

apiVersion: kubelet.config.k8s.io/v1beta1kind: CredentialProviderConfigproviders:  - name: registry-credential-plugin    matchImages:      - "registry.internal.example.com"    defaultCacheDuration: "10m"    apiVersion: credentialprovider.kubelet.k8s.io/v1    tokenAttributes:      # 需要一并传入的 ServiceAccount 注解,插件据此决定换取哪套凭据      - serviceAccountToken:          audience: registry.internal.example.com          expirationSeconds: 600          requireServiceAccount: true    cacheType: Token

cacheType 是第二个关键旋钮,它决定凭据的缓存粒度:

cacheType缓存粒度适用场景代价
Token按 token 缓存高安全要求、身份隔离严格缓存命中率低,换取次数多
ServiceAccount按 ServiceAccount 缓存同一 SA 下大量 Pod,追求换取效率同一 SA 的 Pod 共享缓存条目

选 Token 意味着每次拿到新 token 都可能触发一次换取,安全边界更紧;选 ServiceAccount 则在同一个 ServiceAccount 下的所有 Pod 之间复用缓存,换取次数大幅下降。这两个选项之间没有绝对优劣,取决于你的仓库是否对换取接口有速率限制。

凭据提供者的实现要点

凭据提供者是一个通过 Unix socket 与 kubelet 通信的插件,实现 CredentialProvider 接口。它的核心逻辑是:拿到 token 与注解后,去换取真实的仓库凭据。

概念上,一个最小实现要处理三件事:解析传入的凭据请求、用 ServiceAccount Token 向仓库的合法换取端点请求真实凭据、把结果按 Kubelet 约定的 JSON 格式返回。

// 简化示意:凭据提供者的核心处理逻辑type credRequest struct {    Image              string            `json:"image"`    ServiceAccountToken string           `json:"serviceAccountToken"`    ServiceAccountAnnotations map[string]string `json:"serviceAccountAnnotations"`    CacheKeyType       string            `json:"cacheKeyType"`}type credResponse struct {    Username      string `json:"username"`    Password      string `json:"password"`    CacheKeyType  string `json:"cacheKeyType"` // Token 或 ServiceAccount}func (p *provider) handle(req credRequest) (credResponse, error) {    // 1) 用 token 去仓库侧换取短期凭据;仓库侧需校验 token 的受众与签名    cred, err := p.exchange(req.ServiceAccountToken, req.Image)    if err != nil {        return credResponse{}, fmt.Errorf("exchange failed: %w", err)    }    // 2) 明确返回缓存键类型,必须与 CredentialProviderConfig 里的设置一致    return credResponse{        Username:     cred.User,        Password:     cred.Pass,        CacheKeyType: "Token",    }, nil}

实现侧最容易出问题的地方是返回值里的 CacheKeyType 与配置里的 cacheType 不一致。kubelet 会据此判断缓存条目归属,不一致会导致缓存行为不符合预期,表现为频繁换取或者该隔离的没隔离。

灰度迁移步骤

从 imagePullSecret 迁移过来,建议分四步走,每一步都可回滚。

第一步,保留现有的 imagePullSecrets 不动,同时部署新的凭据提供者和 CredentialProviderConfig。此时两条路径并存,新机制只在 matchImages 命中的仓库上生效。

第二步,挑一个非关键工作负载,去掉它的 imagePullSecrets,观察镜像能否正常拉取、拉取耗时是否可接受。重点看 kubelet 日志里是否有凭据换取失败但被回退掩盖的情况。

第三步,把仓库访问日志与凭据提供者日志对齐,统计换取频次。如果换取频次远超预期,说明 cacheType 选错了,改成 ServiceAccount 试试。

第四步,全量切换,并保留一次快速回退的准备——把删掉的 imagePullSecrets 存在版本控制里,出问题能立刻恢复。

常见问题与排查

第一个限制,也是最需要提前知道的:容器运行时只在拉取开始时拿到凭据。如果一次拉取持续的时间超过凭据的有效期,拉取会失败。对于超大镜像或者带宽受限的环境,这是一个真实风险。缓解办法是把 expirationSeconds 设得比最长拉取耗时长,或者在拉取策略上做分批。

第二个限制,懒加载镜像流式传输(lazy image streaming)不被支持。如果你的平台依赖按需拉取镜像层来加速启动,这条路径和凭据身份化不兼容,需要评估是否值得切换。

第三个问题,Pod 卡在 ImagePullBackOff,kubelet 日志报 token 请求被拒。检查 API Server 是否允许该节点请求目标受众的 token,这是最常见的原因。受众配置错误在症状上看起来像仓库认证失败,但根因在 API Server 侧的授权。

第四个问题,凭据提供者 socket 不可用导致拉取全部失败。kubelet 对提供者不可用的处理策略取决于配置,默认行为可能直接失败。生产环境建议在提供者之外保留一条最低限度的回退路径。

第五个问题,审计发现同一 ServiceAccount 下的 Pod 共享了凭据缓存。这不是 bug,是 cacheType 设为 ServiceAccount 的预期行为。如果你的合规要求不同 ServiceAccount 之间严格隔离,检查是不是有 Pod 错误地使用了同一个 ServiceAccount。

总结与选型建议

KEP-4412 的价值是把"镜像拉取权限"从一种静态的、节点级或集群级的共享密钥,变成一种跟随工作负载身份的动态授权。这对多租户集群、强合规行业、以及镜像仓库凭据轮换频繁的团队,收益很直接。

给出三条落地建议。第一,先在测试集群把凭据提供者的 socket 通信和 CacheKeyType 一致性调通,这两处是纯工程问题,不值得在灰度阶段才发现。第二,expirationSeconds 的取值要基于你实际的最长镜像拉取耗时来定,不要照抄示例的 600 秒。第三,如果你依赖懒加载镜像流式传输,这个特性和它不兼容,需要先评估影响面再决定是否启用。

最后,v1.38 转 GA 之后会锁定两个特性门控,届时回退空间会收窄。如果你打算采用,现在这个 Beta 阶段是试错成本最低的窗口。