Kubernetes v1.35:向 CSI 驱动程序传递服务账号令牌的最佳方式

如果你维护使用服务账号令牌的 CSI 驱动程序, Kubernetes v1.35 带来了一项你希望了解的改进。 自从引入 TokenRequests 特性以来, CSI 驱动程序请求的服务账号令牌一直通过 volume_context 字段传递给他们。 虽然这可以工作,但它不是存储敏感信息的理想位置, 我们已经看到过在 CSI 驱动程序中意外记录令牌的情况。

Kubernetes v1.35 引入了一个 Beta 解决方案来解决这个问题: 通过 Secret 字段实现服务账号令牌的 CSI 驱动程序选择加入。 这允许 CSI 驱动程序通过 NodePublishVolumeRequest 中的 secrets 字段接收服务账号令牌, 这是 CSI 规范中存储敏感数据的适当位置。

理解现有方法

当 CSI 驱动程序使用 TokenRequests 特性时, 它们可以通过在 CSIDriver spec 中配置 TokenRequests 字段来请求工作负载身份的服务账号令牌。 这些令牌作为卷属性映射的一部分传递给驱动程序, 使用密钥 csi.storage.k8s.io/serviceAccount.tokens

volume_context 字段可以工作,但它不是为敏感数据设计的。 正因为如此,存在一些挑战:

首先,CSI 驱动程序使用的 protosanitizer 工具不会将卷上下文视为敏感数据, 因此服务账号令牌可能会在记录 gRPC 请求时出现在日志中。 这在 Secret Store CSI 驱动程序的 CVE-2023-2878 和 Azure File CSI 驱动程序的 CCVE-2024-3744 中都发生过。

其次,每个想避免此问题的 CSI 驱动程序都需要实现自己的清理逻辑, 这导致了驱动程序之间的不一致。

CSI 规范在 NodePublishVolumeRequest 中已经有一个 secrets 字段, 这正是为这类敏感信息设计的。 挑战在于,我们不能仅仅改变令牌放置的位置, 而不破坏期望在卷上下文中获取令牌的现有 CSI 驱动程序。

选择加入机制如何工作

Kubernetes v1.35 引入了一个选择加入机制,让 CSI 驱动程序选择如何接收服务账号令牌。 这样,现有的驱动程序继续按当前方式工作,而驱动程序可以在准备好时迁移到更合适的 Secret 字段。

CSI 驱动程序可以在其 CSIDriver spec 中设置一个新字段:

#
# 注意:这只是一个配置示例。
#      请勿将此配置用于你自己的集群!
#
apiVersion: storage.k8s.io/v1
kind: CSIDriver
metadata:
  name: example-csi-driver
spec:
  # ... 现有字段...
  tokenRequests:
  - audience: "example.com"
    expirationSeconds: 3600
  # 新增 Secret 传递选项
  serviceAccountTokenInSecrets: true  # 默认为 false

行为取决于 serviceAccountTokenInSecrets 字段:

当设置为 false(默认)时,令牌会被放置在 VolumeContext 中, 密钥为 csi.storage.k8s.io/serviceAccount.tokens,与今天相同。 当设置为 true 时,令牌仅被放置在 Secrets 字段中,使用相同的密钥。

关于 Beta 发布

CSIServiceAccountTokenSecrets 特性门控在 kubelet 和 kube-apiserver 上默认启用。 由于 serviceAccountTokenInSecrets 字段默认为 false, 启用特性门控不会改变任何现有行为。 所有驱动程序继续通过卷上下文接收令牌,除非它们明确选择加入。 这就是为什么我们觉得从 Beta 开始而不是 Alpha 会更安心。

CSI 驱动程序作者指南

如果你维护使用服务账号令牌的 CSI 驱动程序,以下是采用此特性的方法。

添加回退逻辑

首先,更新驱动程序代码以检查两个位置的令牌。 这使你的驱动程序与新旧方法都兼容:

const serviceAccountTokenKey = "csi.storage.k8s.io/serviceAccount.tokens"

func getServiceAccountTokens(req *csi.NodePublishVolumeRequest) (string, error) {
    // Check secrets field first (new behavior when driver opts in)
    if tokens, ok := req.Secrets[serviceAccountTokenKey]; ok {
        return tokens, nil
    }

    // Fall back to volume context (existing behavior)
    if tokens, ok := req.VolumeContext[serviceAccountTokenKey]; ok {
        return tokens, nil
    }

    return "", fmt.Errorf("service account tokens not found")
}

此回退逻辑向后兼容,可以安全地包含在任何驱动程序版本中, 即使在集群升级到 v1.35 之前也是如此。

推出顺序

CSI 驱动程序作者在采用此特性时需要遵循特定顺序,以避免破坏现有卷。

驱动程序准备(可以随时进行)

你可以立即通过添加检查 Secret 字段和卷上下文中令牌的回退逻辑来准备驱动程序。 此代码更改向后兼容,可以安全地包含在任何驱动程序版本中,即使在集群升级到 v1.35 之前也是如此。 我们鼓励你尽早添加此回退逻辑、发布版本,并在可行的情况下甚至 backport 到维护分支。

集群升级和特性启用

一旦你的驱动程序部署了回退逻辑,以下是在集群中启用该特性的安全推出顺序:

  1. 完成 kube-apiserver 升级到 1.35 或更高版本
  2. 完成所有节点上 kubelet 升级到 1.35 或更高版本
  3. 确保部署了具有回退逻辑的 CSI 驱动程序版本(如果尚未在准备阶段完成)
  4. 在所有节点上完全完成 CSI 驱动程序 DaemonSet 推出
  5. 更新你的 CSIDriver 清单以设置 serviceAccountTokenInSecrets: true

重要约束

最重要需要记住的是时机。 如果你的 CSI 驱动程序 DaemonSet 和 CSIDriver 对象在同一个清单或 Helm Chart 中, 你需要两次单独的更新。 首先部署带有回退逻辑的新驱动程序版本,等待 DaemonSet 推出完成, 然后更新 CSIDriver 以设置 serviceAccountTokenInSecrets: true

此外,在所有驱动程序 Pod 推出完成之前不要更新 CSIDriver。 如果你这样做了,在仍在运行旧驱动程序版本的节点上,卷挂载将失败, 因为那些 Pod 只检查卷上下文。

为什么这很重要

采用此特性有助于以下几方面:

  • 消除了在 gRPC 请求中作为卷上下文的一部分意外记录服务账号令牌的风险
  • 使用 CSI 规范指定用于敏感数据的字段,这是正确的
  • protosanitizer 工具自动正确处理 Secret 字段,因此你不需要特定于驱动程序的变通方法
  • 这是选择加入的,因此你可以按自己的节奏迁移,而不会破坏现有部署

号召行动

我们(Kubernetes SIG Storage)鼓励 CSI 驱动程序作者采用此特性并提供关于迁移体验的反馈。 如果你对 API 设计有想法或在采用过程中遇到任何问题, 请在 Kubernetes Slack 的 #csi 频道联系我们 (获取邀请,请访问 https://slack.k8s.io/)。

你可以在 KEP-5538 上跟踪后续 Kubernetes 版本的进度。

最后修改 May 24, 2026 at 12:44 PM PST: [zh-cn]Add blog: csi-sa-tokens-secrets-field-beta (b7afb73a43)