jimyag's Blog

Kubernetes Secret 实现原理:从 API Server 到 Pod

最后更新:

Secret 已经更新,为什么容器里的环境变量还是旧值?挂载文件已经变了,为什么应用仍然使用旧配置?从 API 对象到应用实际使用的数据,中间还有 kubelet 投射、容器启动配置和应用自身读取逻辑,需要逐层确认。

本文基于 Kubernetes 源码 commit 52ba90138eb40cab0987dac73e05c838149bdd1c 分析这条链路。该 commit 在本地仓库中描述为 v1.37.0-alpha.1-1973-g52ba90138eb。文中的结论以这个快照为准,不把主分支实现当成所有历史版本的共同语义。

下面先用同一份 Secret 的四种消费方式观察更新结果,再追踪 API 存储、kubelet 获取和文件投射。源码快照与实验版本分别标明;开发版源码中的实现细节不作为 v1.35.1 实验已经验证的行为。

文件已经更新,应用为什么没有跟着变

本次通过 SSH 在 zj 上运行 kind v0.33.0、Kubernetes v1.35.1,让同一个 Pod 用四种方式消费实验 Secret settings:环境变量、普通 Secret volume、subPath 挂载,以及应用启动时读取后保存的副本。测试值只有 old 和 new,不使用真实凭据。

修改 Secret 后,脚本持续检查普通 volume 文件,直到它变成 new,再同时读取其余三份值。实测结果如下:

读取位置修改前普通 volume 已更新后
环境变量 VALUEoldold
普通 volume 的 /config/valueoldnew
subPath 挂载的文件oldold
应用启动时保存的配置副本oldold

这组结果区分了三个更新过程。API 值变化后,普通 volume 最终传播到文件路径;已运行进程的环境变量没有被重新注入;应用已经读进自身状态的数据,也不会因为文件路径更新而自动重读。subPath 则没有跟随这次 Secret volume 的更新切换。

因此,确认应用热更新是否有效,需要同时回答:挂载文件是否已经变化、应用是否重新读取这个路径、重新读取后是否替换了实际使用的配置。如果应用只在启动时读一次,即使 cat 已经能看到新值,业务行为也可以仍然使用旧值。

这里的 /cached 用文件保存“应用启动时读取一次”的结果,方便从外部验证;实验没有测试某个具体框架的配置重载接口。真实应用还应验证自己的重新读取与配置替换过程,不能把 Kubernetes 文件投射成功直接当作业务重载成功。

完整链路

一次普通 Secret 的流转可以概括为:

  flowchart LR
    Client["kubectl / API client"]
    Convert["API decode + version conversion\nstringData -> data"]
    Validate["Secret strategy + validation"]
    Registry["generic registry Store"]
    Transform["storage transformer\nidentity / AES / Secretbox / KMS"]
    Etcd[("etcd")]
    Kubelet["kubelet Secret Manager\nWatch / TTL / Get"]
    EnvVolume["env / volume"]
    Container["container process"]
    PullSecret["imagePullSecrets"]
    ImageService["CRI ImageService / PullImage"]

    Client --> Convert --> Validate --> Registry
    Registry --> Transform --> Etcd
    Etcd --> Transform --> Registry
    Registry --> Kubelet
    Kubelet --> EnvVolume --> Container
    Kubelet --> PullSecret --> ImageService

这张图里有两个重要边界:

  • API Server 负责对象语义、授权和静态数据加密;Secret 自身不是加密容器。
  • kubelet 负责把 API 对象转换成节点上的具体消费形式;容器运行时不会自行查询 Secret API。边界在 CRI:镜像拉取凭据由 kubelet 转成 PullImage 的 credentials 参数传入,环境变量和卷则通过容器创建配置传入,运行时拿到的都是转换后的结果。

Secret API 对象保存了什么

Secret 的公开类型定义在 staging/src/k8s.io/api/core/v1/types.go:

正在加载 GitHub 文件…
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
type Secret struct {
    metav1.TypeMeta   `json:""`
    metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`

    Immutable *bool `json:"immutable,omitempty" protobuf:"varint,5,opt,name=immutable"`
    Data map[string][]byte `json:"data,omitempty" protobuf:"bytes,2,rep,name=data"`
    StringData map[string]string `json:"stringData,omitempty" protobuf:"bytes,4,rep,name=stringData"`
    Type SecretType `json:"type,omitempty" protobuf:"bytes,3,opt,name=type,casttype=SecretType"`
}

const MaxSecretSize = 1 * 1024 * 1024

真正持久化的业务数据是 Data map[string][]byte。因为 JSON 不能直接表达任意字节,Go 的 []byte 在 JSON 中被编码成 Base64 字符串。因此下面两个字段表达的是同一份字节内容:

1
2
3
# data 中的字符串是原始字节的 Base64 表示,不是加密结果。
data:
  password: czNjcjN0
1
2
3
# stringData 接受明文输入,API 转换时合并到 data。
stringData:
  password: s3cr3t

stringData 是只写便利字段。版本转换函数 Convert_v1_Secret_To_core_Secret 把字符串转换为字节并写入 data;同名键存在时,stringData 覆盖 data。读取 API 时不会再返回 stringData。

正在加载 GitHub 文件…

所以 Base64 的作用只是序列化。任何能读取 Secret API、etcd 明文或 Secret manifest 的主体都能恢复原始值。Kubernetes 官方的 Secret 良好实践 也明确把 Base64 和加密区分开。

创建和更新时如何校验

Secret 的 REST 存储入口在 pkg/registry/core/secret/storage/storage.go。它没有实现一套 Secret 专用数据库逻辑,而是把 Secret、SecretList、匹配函数和增删改策略交给通用的 genericregistry.Store:

正在加载 GitHub 文件…
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
store := &genericregistry.Store{
    NewFunc:                  func() runtime.Object { return &api.Secret{} },
    NewListFunc:              func() runtime.Object { return &api.SecretList{} },
    PredicateFunc:            secret.Matcher,
    // ... SingularQualifiedResource、TableConvertor 等字段省略
    DefaultQualifiedResource: api.Resource("secrets"),
    CreateStrategy:           secret.Strategy,
    UpdateStrategy:           secret.Strategy,
    DeleteStrategy:           secret.Strategy,
}

secret.Strategy 声明 Secret 是 namespace-scoped 资源,并在创建、更新时调用校验函数。核心校验位于 ValidateSecret:

正在加载 GitHub 文件…
  1. 校验对象名称、namespace 等通用 metadata。
  2. 校验 data 的键名,只允许字母、数字、-、_、.。
  3. 累加所有 value 的原始字节数,超过 1 MiB 时拒绝。
  4. 根据内置 type 做最低限度的结构校验。

不同 Secret 类型的校验强度并不相同。例如:

  • Opaque 不增加数据格式约束。
  • kubernetes.io/service-account-token 要求 metadata.annotations["kubernetes.io/service-account.name"] 非空。
  • kubernetes.io/dockerconfigjson 要求 .dockerconfigjson 存在并且能解析为 JSON,但不会验证它是否真是可用的 Docker 配置。
  • kubernetes.io/basic-auth 要求 username 或 password 至少存在一个。
  • kubernetes.io/tls 只检查 tls.crt 和 tls.key 两个键存在,不解析证书,也不验证公私钥是否匹配。

因此 type 主要提供约定和有限校验,不代表 API Server 已经验证凭据可用。

immutable 的实际语义

ValidateSecretUpdate 里有两层不可变约束:

正在加载 GitHub 文件…
  • 无论 immutable 是否设置,type 字段都不能修改。
  • 当旧对象的 immutable 已经是 true 时,额外禁止两类操作:
    • 把 immutable 改回 false 或删除该字段。
    • 修改 data。

metadata 仍然可以更新。watch 型 Secret manager 也会识别 immutable,不再为这种对象维持无意义的持续 watch。它是 API 不可变性和节点侧资源优化,不是加密开关。

写入 etcd 前发生了什么

通用 etcd3 存储的创建路径位于 store.go:

正在加载 GitHub 文件…
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
data, err := runtime.Encode(s.codec, obj)
// ...
newData, err := s.transformer.TransformToStorage(
    ctx,
    data,
    authenticatedDataString(preparedKey),
)
// ...
txnResp, err := s.client.Kubernetes.OptimisticPut(
    ctx, preparedKey, newData, 0, kubernetes.PutOptions{LeaseID: lease},
)

顺序是:

  1. 把内部对象编码为存储版本。
  2. 调用当前资源对应的 storage transformer。
  3. 把转换结果写入 etcd。

读取时则反过来:先从 etcd 取出字节,执行 TransformFromStorage,再 decode 成 API 对象。etcd 不理解 Secret,也不负责选择加密算法。

默认为什么不是加密存储

kube-apiserver 只有设置 --encryption-provider-config 后,才会加载资源 transformer。源码 maybeApplyResourceTransformers 在配置路径为空时直接返回。

正在加载 GitHub 文件…

未加密路径使用 identity 语义。其 TransformToStorage 原样返回输入字节,不提供机密性。它只会在读取时拒绝带有 k8s:enc: 前缀的加密数据,防止把密文误当成普通序列化对象。

正在加载 GitHub 文件…

这也是「Secret 默认不以明文显示」和「Secret 默认在 etcd 中加密」不能画等号的原因。kubectl 的表格输出不显示 value,并不改变底层存储。

EncryptionConfiguration 如何工作

配置静态数据加密后,每类资源会得到一个 transformer 链。当前源码支持 AESGCM、AESCBC、Secretbox、KMS 和 Identity;其中 KMS 有 v1、v2 两个 API 版本,v1 已弃用,新配置应使用 v2。构造逻辑在 prefixTransformersAndProbes。

正在加载 GitHub 文件…

一个简化配置如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 示例要求已有可用的 KMS v2 插件;写入使用第一项,identity 用于读取旧明文。
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
    providers:
      - kms:
          apiVersion: v2
          name: example-kms
          endpoint: unix:///var/run/kmsplugin/socket.sock
      - identity: {}

provider 顺序有实际语义:

  • 写入只使用第一个 provider。
  • 读取会依次尝试能够识别现有数据前缀的 provider。
  • 如果不是第一个 provider 完成了解密,数据会标记为 stale,后续更新可用首选 provider 重写。

这让密钥轮换和从明文迁移到密文成为可能,但只改配置不会自动重写全部历史对象。既有 Secret 仍需要迁移。另外,本快照的 kube-apiserver 支持 --encryption-provider-config-automatic-reload,配置文件的变更可以在运行中热重载,无需重启 API Server。完整配置和迁移步骤应以官方的 静态数据加密文档 为准。

  sequenceDiagram
    participant API as kube-apiserver REST storage
    participant Codec as storage codec
    participant Transformer as resource transformer
    participant Etcd as etcd

    API->>Codec: encode Secret
    Codec-->>API: serialized bytes
    API->>Transformer: TransformToStorage(bytes, etcd key)
    Transformer-->>API: plaintext or k8s:enc:* ciphertext
    API->>Etcd: OptimisticPut

    Etcd-->>API: stored bytes
    API->>Transformer: TransformFromStorage(bytes, etcd key)
    Transformer-->>API: serialized plaintext
    API->>Codec: decode
    Codec-->>API: Secret object

etcd key 作为 authenticated data 传入 transformer。这意味着支持认证加密的实现不仅保护内容,还把密文和它应当所属的存储路径绑定,降低把一条密文搬到另一条 key 下仍被接受的风险。

kubelet 如何只管理 Pod 引用的 Secret

Pod 调度到节点后,kubelet 通过 Secret manager 取得它需要的对象。接口位于 pkg/kubelet/secret/secret_manager.go:

正在加载 GitHub 文件…
1
2
3
4
5
type Manager interface {
    GetSecret(namespace, name string) (*v1.Secret, error)
    RegisterPod(pod *v1.Pod)
    UnregisterPod(pod *v1.Pod)
}

RegisterPod 通过 VisitPodSecretNames 收集 Pod 里的 Secret 引用。watch 和 TTL manager 只为已经注册的 Pod 维护引用计数与缓存;最后一个 Pod 解除引用后,对象会从本地管理范围移除。

kubelet 支持三种变更检测策略,选择逻辑在 pkg/kubelet/kubelet.go:

正在加载 GitHub 文件…
策略获取方式新鲜度语义
Watch为被引用对象建立 watch,本地缓存返回结果由 watch 传播更新,默认策略
Cache本地 TTL 缓存,过期后重新 GETTTL 来自 Node 的 node.alpha.kubernetes.io/ttl 注解,未设置时兜底 1 分钟
Get每次需要时直接 GET API Server不保留该层对象缓存

默认值在 defaults.go 中设为 Watch。

正在加载 GitHub 文件…

这里的「只管理引用对象」不等于只靠 kubelet 代码就能形成安全边界。API Server 侧还需要 Node authorizer。它把 Secret 到 Pod、Pod 到 Node 建成关系图,并限制节点只读与分配到该节点的 Pod 有关的 Secret。对应图边在 plugin/pkg/auth/authorizer/node/graph.go,授权入口在 node_authorizer.go。集群没有正确启用相应授权机制时,不能只根据 kubelet 的缓存范围推断权限已经最小化。

正在加载 GitHub 文件…
正在加载 GitHub 文件…

kubelet 消费 Secret 的三条主要路径

1. 环境变量:创建容器时求值一次

kubelet 在 makeEnvironmentVariables 中处理 envFrom.secretRef 和 env[].valueFrom.secretKeyRef:

正在加载 GitHub 文件…
  1. 通过 Secret manager 获取对象。
  2. envFrom 遍历全部 secret.Data,把字节转换成字符串。
  3. secretKeyRef 读取指定 key。
  4. 生成最终的容器环境变量列表并交给容器运行时。

环境变量属于进程启动参数的一部分。容器启动后,即使 kubelet 缓存收到新的 Secret,原进程的环境也不会被修改。通过重建 Pod 可以重新解析 Secret 并生成容器配置;若应用仅重新读取自己的环境变量,读到的仍然是旧值。应用热重载需要另有能取得新数据的来源,例如重新打开已更新的挂载文件。

另外,环境变量会把字节按字符串处理,不适合承载含不可打印字节的二进制数据,这类内容应优先走 volume。

这条路径还意味着 Secret 会进入容器运行时配置和进程环境的可见边界。调试输出、崩溃报告、子进程继承等环节都需要避免泄露。

2. Secret volume:内存介质与原子切换

Secret volume plugin 定义在 pkg/volume/secret/secret.go。它不是直接把 API 对象 bind mount 进容器,而是包装一个 medium: Memory 的 emptyDir:

正在加载 GitHub 文件…
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func wrappedVolumeSpec() volume.Spec {
    return volume.Spec{
        Volume: &v1.Volume{
            VolumeSource: v1.VolumeSource{
                EmptyDir: &v1.EmptyDirVolumeSource{
                    Medium: v1.StorageMediumMemory,
                },
            },
        },
    }
}

挂载时的 SetUpAt 依次执行:

正在加载 GitHub 文件…
  1. 从 Secret manager 获取对象;非 optional Secret 不存在时返回错误。
  2. MakePayload 把 data 映射成文件内容、路径和 mode。
  3. 建立内存型 emptyDir。
  4. 用 AtomicWriter 写入文件并处理 fsGroup 权限。

AtomicWriter 不会逐个覆盖容器正在读取的可见文件。它先创建新的时间戳目录,写完全部内容和权限后,把 ..data_tmp 原子 rename 为 ..data。可见文件则是到 ..data/<name> 的符号链接。实现与目录结构说明在 atomic_writer.go。

正在加载 GitHub 文件…
1
2
3
4
5
6
7
/mounted/secret/
├── username -> ..data/username
├── password -> ..data/password
├── ..data -> ..2026_08_13_17_00_00.123456789/
└── ..2026_08_13_17_00_00.123456789/
    ├── username
    └── password

Secret volume 的 RequiresRemount 返回 true,因此 kubelet 的 volume reconcile 会重新执行投射。更新是最终一致的,延迟由 kubelet 同步周期和所选缓存策略共同决定。

应用侧仍需正确读取:已经打开的文件描述符保留对旧文件的引用,..data 切换不会把这个描述符改成新文件。重新按路径打开时,才会经过新的符号链接目标。监听更新的实现也要考虑这种目录切换方式,不能假设 kubelet 直接覆盖了原文件内容。

subPath 则在挂载时把选定文件固定为单文件 bind mount;后续父目录中的 ..data 切换,不会重新建立这个挂载。因此普通 Secret volume 能看到新值,同一 Secret 的 subPath 文件仍可保持旧值。Secret 官方文档明确列出了该限制。

普通 Secret 也可以放进 projected volume(projected.sources[].secret)。它复用同一个 AtomicWriter 机制,RequiresRemount 同样返回 true,更新语义与 secret volume 一致,区别只在声明方式。

内存型 volume 减少了 Secret 写入节点持久化存储的机会,但不等于数据从此不会出现在其他位置。节点 swap、进程内存、core dump、日志和应用复制出来的文件仍属于单独的安全边界。

原子切换保护了什么,没有保护什么

与本次实验相同的 v1.35.1 中,AtomicWriter.Write会先比较现有内容与待写入内容。没有内容差异、也没有路径需要移除时,它跳过新数据目录的写入,同时仍检查可见符号链接是否需要补建。周期性 reconcile 因此不意味着每次都重新生成一套文件。

需要更新时,写入内容和设置权限都发生在切换 ..data 之前。对于这里的 Linux 实现,这让已存在的可见文件路径在切换后指向准备完成的新内容,避免应用正好读到被逐段覆盖的半个文件。失败如果发生在发布新目录之前,旧的 ..data 仍可继续供应用使用。

但一次目录链接切换不等于应用多次读取之间有事务。假设程序先打开 username,发生切换后再打开 password,就可能拿到两个版本;保留旧文件描述符也仍会读旧文件。需要成组一致性的应用,可以把相关配置编码在同一个文件中,一次打开后完整读取、校验,再替换内存中的配置。这个应用层处理是根据目录切换语义给出的建议;开头实验验证了普通卷、环境变量、subPath 和启动缓存的差异,没有测量跨文件读取竞争。

3. imagePullSecrets:交给镜像管理链路

对于 pod.spec.imagePullSecrets,kubelet 的 getPullSecretsForPod 逐个读取 Secret,再把结果交给镜像拉取逻辑。缺失对象会被记录,随后镜像拉取可能失败。

正在加载 GitHub 文件…

这类 Secret 通常是 kubernetes.io/dockerconfigjson。它不会自动作为文件或环境变量出现在业务容器内,但会进入 kubelet、凭据解析和 CRI 镜像拉取的调用链。

ServiceAccount token 为什么需要单独解释

SecretTypeServiceAccountToken 仍然存在,token controller 也能为这种 Secret 补充 token、CA 和 namespace 数据。但它表达的是持久化的长期凭据机制。

当前 ServiceAccount admission plugin 在 Pod 允许自动挂载时,会注入名为 kube-api-access-* 的 projected volume,挂载到 /var/run/secrets/kubernetes.io/serviceaccount。源码在 mountServiceAccountToken。这个投射同时包含:

正在加载 GitHub 文件…
  • serviceAccountToken:短期 token;
  • kube-root-ca.crt ConfigMap 中的 CA;
  • downward API 提供的 namespace。

projected volume plugin 不会读取一个 token Secret,而是在 collectData 的 ServiceAccountToken 分支(L322-L355)中构造绑定当前 Pod UID 的 TokenRequest。kubelet token manager 通过 serviceaccounts/token 子资源取得 token,相关获取与缓存流程在 token_manager.go。刷新时机由 requiresRefresh 决定:token 生命周期消耗 80%,或已存活接近 24 小时时,提前获取新 token 并更新投影。

正在加载 GitHub 文件…
正在加载 GitHub 文件…
正在加载 GitHub 文件…
正在加载 GitHub 文件…

因此,看到容器内的 .../serviceaccount/token 文件,不能据此推断集群里一定存在对应的 Secret 对象。现代默认路径是按需签发、绑定 Pod、可轮换的 projected token。

更新传播为什么因消费方式而不同

更新和删除都由通用 etcd3 store 做乐观并发控制,但具体操作不同:更新经过 GuaranteedUpdate,最终用 OptimisticPut 比较 etcd revision;最终物理删除则由 Delete 中的 OptimisticDelete 比较原对象 revision。变更成功后,etcd 的记录会沿 watch 链路转换成 API watch 事件。

正在加载 GitHub 文件…
正在加载 GitHub 文件…

kubelet 的两种缓存也不能混为一谈:Watch 策略通过 watch 更新本地对象;Cache 策略不消费对象 watch,而是在 TTL 过期后重新 GET API Server。

消费方式首次取值位置Secret 更新后的行为
env / envFromkubelet 生成容器配置已运行进程不更新,需要重建或重启
Secret volumevolume plugin SetUpAtkubelet 最终一致地重新投射,应用需重新打开文件
Secret volume + subPath单文件 bind mount不自动更新
imagePullSecretskubelet 发起镜像拉取前后续拉取可重新读取;已经拉取并运行的镜像不受影响
projected ServiceAccount tokenkubelet TokenRequest + token cache到刷新窗口后获取新 token,并通过 projected volume 更新

这组差异不是 Secret controller 的统一「热更新策略」,而是各消费入口的生命周期不同。

安全边界与常见误解

Secret 不等于安全存储

Secret 提供的是标准 API、权限控制入口和受控投递机制。要形成完整保护,还需要:

  1. 为 secrets 配置静态数据加密,优先考虑由外部 KMS 管理主密钥。
  2. 对 get、list、watch 分别实施最小权限;list 和 watch 同样能取得 Secret 内容。
  3. 限制创建 Pod、Deployment 等工作负载的权限。能在 namespace 中创建使用任意 Secret 的 Pod,通常就能间接读取该 namespace 的 Secret。
  4. 限制每个容器的挂载范围,不要因为同属一个 Pod 就把 Secret 暴露给所有 sidecar。
  5. 避免把 Base64 manifest、解码后的值、环境变量或文件内容写入日志、Git、诊断包和监控标签。

Secret type 不会自动验证凭据

TLS Secret 的键存在不代表证书有效,Docker config 能解析为 JSON 不代表 registry 登录一定成功。业务 controller 或创建工具仍需做语义校验。

加密 etcd 也不是终点

静态数据加密主要防止 etcd 数据文件、快照或备份被直接读取。API Server 解密后仍会把 Secret 返回给获授权主体;kubelet 和使用 Secret 的进程最终也要接触明文。威胁模型必须分别覆盖控制面主机、KMS、节点和应用。

使用限制与注意事项

大小与数量

1 MiB 上限是 API 层硬约束:ValidateSecret 累加 data 所有 value 的字节数,超过 MaxSecretSize 即拒绝创建或更新,不区分 Secret 类型。官方 Secret 文档给出的主要原因是避免大型 Secret 耗尽 API Server 和 kubelet 内存。

这项校验只统计 data value,不统计 metadata,也不能据此推断完整对象一定低于 etcd 的请求大小限制。annotation、label、序列化开销和 etcd 自身的请求上限是另外的边界。

走环境变量消费时还会叠加操作系统限制:启动进程时,参数和环境变量共同占用总大小预算,单个字符串也有长度限制。实际额度受系统配置影响,不能把 Secret 的 1 MiB 上限直接当作可注入环境变量的容量。大量 Secret 注入 env 时,进程可能在启动阶段被操作系统拒绝。

数量没有类似的硬限制,但每个被 Pod 引用的 Secret 都会产生 kubelet 的 watch 或缓存开销,并放大 API Server 与 etcd 的压力。应清理不再被引用的对象,而不是放任堆积。

namespace 边界与轮换

Secret 是 namespace-scoped 资源,env 和 volume 只能引用同 namespace 的对象。跨 namespace 共享没有内建机制:复制到目标 namespace 会多一份拷贝、扩大暴露面;长期共享应交给外部 secret 管理方案。

immutable: true 禁止原地修改数据,metadata 仍可更新。轮换数据时可以创建新名称的 Secret,再更新工作负载引用并重建 Pod;若选择删除后用同名重建,也不能期待已运行进程的环境变量自动变化。

运维与安全依赖

  • KMS provider 是可用性依赖,但故障影响取决于 API 版本和缓存状态。KMS v2 已缓存的 DEK 可以持续用于解密;插件进入错误状态后,当前 DEK 或 seed 还能短暂用于写入,本快照中的 kmsv2PluginWriteDEKSourceMaxTTL 为 3 分钟。故障持续超过窗口后,新写入会失败;缓存未命中的解密也可能需要 KMS 恢复后才能完成。
正在加载 GitHub 文件…
  • 热重载只重载配置:增删或重排 provider 前,先确认旧数据的前缀仍有 provider 可解,再按官方迁移流程操作。
  • etcd 快照与备份和存储层同源:未配置加密时备份即明文,配置后才是密文,备份链路应纳入威胁模型。
  • 官方 Secret 良好实践 建议按需考虑第三方 Secret store,并可通过 Secrets Store CSI Driver 把外部数据挂载给获授权的 Pod。是否使用 Kubernetes Secret 应根据威胁模型、访问控制和轮换要求判断,不宜按“高、中、低敏感度”简单分类。Vault 等系统可以作为外部凭据来源;SOPS 主要保护 Git 中的加密配置,解密后的结果仍可能被写成 Kubernetes Secret,它不等同于运行时外部 Secret store。

实验代码与使用说明见 k8sdev/deep-dive-secret-refresh。

#Kubernetes #Secret #Kube-Apiserver #Kubelet #Etcd #安全