
Kubernetes Secret 看起来只是一个带有 `data` 字段的 API 对象，但从提交 YAML 到容器读到数据，中间跨过了 API 类型转换、准入与校验、etcd 序列化、kubelet 缓存和 volume 投射等多层逻辑。

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

先给出结论：

1. Secret 的核心是 `map[string][]byte`；Base64 只是字节数组的 JSON 序列化表示，不提供机密性。
2. 是否加密存储不由 Secret 类型决定，而由 kube-apiserver 的 `EncryptionConfiguration` 决定；默认未配置时写入 etcd 不加密。
3. Pod 不直接访问 etcd。kubelet 根据 Pod 引用从 API Server 获取 Secret，再投递给环境变量、volume 或镜像拉取凭据。
4. volume 用内存型 `emptyDir` 和原子符号链接切换，可最终更新；环境变量只在生成容器配置时解析一次，更新不触达已运行进程。
5. 自动挂载的 ServiceAccount token 走 `TokenRequest` + projected volume，与 `kubernetes.io/service-account-token` 旧式 Secret 是两套机制。

<!--more-->

## 完整链路

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

```mermaid
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`](https://github.com/kubernetes/kubernetes/blob/52ba90138eb40cab0987dac73e05c838149bdd1c/staging/src/k8s.io/api/core/v1/types.go#L8287-L8326)：

```go
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 字符串。因此下面两个字段表达的是同一份字节内容：

```yaml
data:
  password: czNjcjN0
```

```yaml
stringData:
  password: s3cr3t
```

`stringData` 是只写便利字段。版本转换函数 [`Convert_v1_Secret_To_core_Secret`](https://github.com/kubernetes/kubernetes/blob/52ba90138eb40cab0987dac73e05c838149bdd1c/pkg/apis/core/v1/conversion.go#L229-L245) 把字符串转换为字节并写入 `data`；同名键存在时，`stringData` 覆盖 `data`。读取 API 时不会再返回 `stringData`。

所以 Base64 的作用只是序列化。任何能读取 Secret API、etcd 明文或 Secret manifest 的主体都能恢复原始值。Kubernetes 官方的 [Secret 良好实践](https://kubernetes.io/docs/concepts/security/secrets-good-practices/) 也明确把 Base64 和加密区分开。

## 创建和更新时如何校验

Secret 的 REST 存储入口在 [`pkg/registry/core/secret/storage/storage.go`](https://github.com/kubernetes/kubernetes/blob/52ba90138eb40cab0987dac73e05c838149bdd1c/pkg/registry/core/secret/storage/storage.go#L30-L57)。它没有实现一套 Secret 专用数据库逻辑，而是把 `Secret`、`SecretList`、匹配函数和增删改策略交给通用的 `genericregistry.Store`：

```go
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`](https://github.com/kubernetes/kubernetes/blob/52ba90138eb40cab0987dac73e05c838149bdd1c/pkg/apis/core/validation/validation.go#L8087-L8161)：

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`](https://github.com/kubernetes/kubernetes/blob/52ba90138eb40cab0987dac73e05c838149bdd1c/pkg/apis/core/validation/validation.go#L8164-L8181) 里有两层不可变约束：

- 无论 `immutable` 是否设置，`type` 字段都不能修改。
- 当旧对象的 `immutable` 已经是 `true` 时，额外禁止两类操作：
  - 把 `immutable` 改回 `false` 或删除该字段。
  - 修改 `data`。

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

## 写入 etcd 前发生了什么

通用 etcd3 存储的创建路径位于 [`store.go`](https://github.com/kubernetes/kubernetes/blob/52ba90138eb40cab0987dac73e05c838149bdd1c/staging/src/k8s.io/apiserver/pkg/storage/etcd3/store.go#L285-L333)：

```go
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`](https://github.com/kubernetes/kubernetes/blob/52ba90138eb40cab0987dac73e05c838149bdd1c/staging/src/k8s.io/apiserver/pkg/server/options/etcd.go#L348-L408) 在配置路径为空时直接返回。

未加密路径使用 identity 语义。其 [`TransformToStorage`](https://github.com/kubernetes/kubernetes/blob/52ba90138eb40cab0987dac73e05c838149bdd1c/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/identity/identity.go#L33-L57) 原样返回输入字节，不提供机密性。它只会在读取时拒绝带有 `k8s:enc:` 前缀的加密数据，防止把密文误当成普通序列化对象。

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

### EncryptionConfiguration 如何工作

配置静态数据加密后，每类资源会得到一个 transformer 链。当前源码支持 `AESGCM`、`AESCBC`、`Secretbox`、`KMS` 和 `Identity`；其中 KMS 有 v1、v2 两个 API 版本，v1 已弃用，新配置应使用 v2。构造逻辑在 [`prefixTransformersAndProbes`](https://github.com/kubernetes/kubernetes/blob/52ba90138eb40cab0987dac73e05c838149bdd1c/staging/src/k8s.io/apiserver/pkg/server/options/encryptionconfig/config.go#L556-L610)。

一个简化配置如下：

```yaml
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。完整配置和迁移步骤应以官方的 [静态数据加密文档](https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/) 为准。

```mermaid
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`](https://github.com/kubernetes/kubernetes/blob/52ba90138eb40cab0987dac73e05c838149bdd1c/pkg/kubelet/secret/secret_manager.go#L38-L53)：

```go
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`](https://github.com/kubernetes/kubernetes/blob/52ba90138eb40cab0987dac73e05c838149bdd1c/pkg/kubelet/kubelet.go#L681-L701)：

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

默认值在 [`defaults.go`](https://github.com/kubernetes/kubernetes/blob/52ba90138eb40cab0987dac73e05c838149bdd1c/pkg/kubelet/apis/config/v1beta1/defaults.go#L268-L270) 中设为 `Watch`。

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

## kubelet 消费 Secret 的三条主要路径

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

kubelet 在 [`makeEnvironmentVariables`](https://github.com/kubernetes/kubernetes/blob/52ba90138eb40cab0987dac73e05c838149bdd1c/pkg/kubelet/kubelet_pods.go#L756-L950) 中处理 `envFrom.secretRef` 和 `env[].valueFrom.secretKeyRef`：

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

环境变量属于进程启动参数的一部分。容器启动后，即使 kubelet 缓存收到新的 Secret，原进程的环境也不会被修改。应用若依赖环境变量，需要通过重建 Pod 或应用自己的重载机制使用新值。

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

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

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

Secret volume plugin 定义在 [`pkg/volume/secret/secret.go`](https://github.com/kubernetes/kubernetes/blob/52ba90138eb40cab0987dac73e05c838149bdd1c/pkg/volume/secret/secret.go#L42-L67)。它不是直接把 API 对象 bind mount 进容器，而是包装一个 `medium: Memory` 的 `emptyDir`：

```go
func wrappedVolumeSpec() volume.Spec {
    return volume.Spec{
        Volume: &v1.Volume{
            VolumeSource: v1.VolumeSource{
                EmptyDir: &v1.EmptyDirVolumeSource{
                    Medium: v1.StorageMediumMemory,
                },
            },
        },
    }
}
```

挂载时的 [`SetUpAt`](https://github.com/kubernetes/kubernetes/blob/52ba90138eb40cab0987dac73e05c838149bdd1c/pkg/volume/secret/secret.go#L178-L257) 依次执行：

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

`AtomicWriter` 不会逐个覆盖容器正在读取的可见文件。它先创建新的时间戳目录，写完全部内容和权限后，把 `..data_tmp` 原子 rename 为 `..data`。可见文件则是到 `..data/<name>` 的符号链接。实现与目录结构说明在 [`atomic_writer.go`](https://github.com/kubernetes/kubernetes/blob/52ba90138eb40cab0987dac73e05c838149bdd1c/pkg/volume/util/atomic_writer.go#L41-L59)。

```text
/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 同步周期和所选缓存策略共同决定。

应用侧仍需正确读取：长期持有已经打开的文件描述符可能继续指向旧 inode，监听更新时应关注 `..data` 符号链接切换并重新打开文件。通过 `subPath` 挂载单个 Secret 文件时也不会自动收到更新，这是 [Secret 官方文档](https://kubernetes.io/docs/concepts/configuration/secret/#using-secrets-as-files-from-a-pod) 明确列出的限制。

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

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

### 3. imagePullSecrets：交给镜像管理链路

对于 `pod.spec.imagePullSecrets`，kubelet 的 [`getPullSecretsForPod`](https://github.com/kubernetes/kubernetes/blob/52ba90138eb40cab0987dac73e05c838149bdd1c/pkg/kubelet/kubelet_pods.go#L1091-L1115) 逐个读取 Secret，再把结果交给镜像拉取逻辑。缺失对象会被记录，随后镜像拉取可能失败。

这类 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`](https://github.com/kubernetes/kubernetes/blob/52ba90138eb40cab0987dac73e05c838149bdd1c/plugin/pkg/admission/serviceaccount/admission.go#L418-L496)。这个投射同时包含：

- `serviceAccountToken`：短期 token；
- `kube-root-ca.crt` ConfigMap 中的 CA；
- downward API 提供的 namespace。

projected volume plugin 不会读取一个 token Secret，而是在 [`collectData`](https://github.com/kubernetes/kubernetes/blob/52ba90138eb40cab0987dac73e05c838149bdd1c/pkg/volume/projected/projected.go#L249-L436) 的 `ServiceAccountToken` 分支（[L322-L355](https://github.com/kubernetes/kubernetes/blob/52ba90138eb40cab0987dac73e05c838149bdd1c/pkg/volume/projected/projected.go#L322-L355)）中构造绑定当前 Pod UID 的 `TokenRequest`。kubelet token manager 通过 `serviceaccounts/token` 子资源取得 token，相关获取与缓存流程在 [`token_manager.go`](https://github.com/kubernetes/kubernetes/blob/52ba90138eb40cab0987dac73e05c838149bdd1c/pkg/kubelet/token/token_manager.go#L96-L130)。刷新时机由 [`requiresRefresh`](https://github.com/kubernetes/kubernetes/blob/52ba90138eb40cab0987dac73e05c838149bdd1c/pkg/kubelet/token/token_manager.go#L168-L194) 决定：token 生命周期消耗 80%，或已存活接近 24 小时时，提前获取新 token 并更新投影。

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

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

更新和删除都由通用 etcd3 store 做乐观并发控制，但具体操作不同：更新经过 [`GuaranteedUpdate`](https://github.com/kubernetes/kubernetes/blob/52ba90138eb40cab0987dac73e05c838149bdd1c/staging/src/k8s.io/apiserver/pkg/storage/etcd3/store.go#L472-L610)，最终用 `OptimisticPut` 比较 etcd revision；最终物理删除则由 [`Delete`](https://github.com/kubernetes/kubernetes/blob/52ba90138eb40cab0987dac73e05c838149bdd1c/staging/src/k8s.io/apiserver/pkg/storage/etcd3/store.go#L352-L468) 中的 `OptimisticDelete` 比较原对象 revision。变更成功后，etcd 的记录会沿 watch 链路转换成 API watch 事件。

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

| 消费方式 | 首次取值位置 | Secret 更新后的行为 |
|---|---|---|
| `env` / `envFrom` | kubelet 生成容器配置 | 已运行进程不更新，需要重建或重启 |
| Secret volume | volume plugin `SetUpAt` | kubelet 最终一致地重新投射，应用需重新打开文件 |
| Secret volume + `subPath` | 单文件 bind mount | 不自动更新 |
| `imagePullSecrets` | kubelet 发起镜像拉取前 | 后续拉取可重新读取；已经拉取并运行的镜像不受影响 |
| projected ServiceAccount token | kubelet 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 文档](https://kubernetes.io/docs/concepts/configuration/secret/#size-limit)给出的主要原因是避免大型 Secret 耗尽 API Server 和 kubelet 内存。

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

走环境变量消费时还会叠加操作系统限制：Linux 上单进程环境总量受 `ARG_MAX` 约束（x86-64 默认约 2 MiB），单个环境变量长度也有独立上限（默认 128 KiB）。大量 Secret 注入 env 时，容器创建可能先被这一层拒绝。

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

### namespace 边界与轮换

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

`immutable: true` 的对象不能原地更新，只能删除重建；删除重建后，通过 env 消费的 Pod 仍需重建才能拿到新值。因此 immutable 应配合「新名字 / 新对象」的轮换流程使用，而不是当作降低更新频率的手段。

### 运维与安全依赖

- KMS provider 是可用性依赖，但故障影响取决于 API 版本和缓存状态。KMS v2 已缓存的 DEK 可以持续用于解密；插件进入错误状态后，当前 DEK 或 seed 还能短暂用于写入，本快照中的 [`kmsv2PluginWriteDEKSourceMaxTTL`](https://github.com/kubernetes/kubernetes/blob/52ba90138eb40cab0987dac73e05c838149bdd1c/staging/src/k8s.io/apiserver/pkg/server/options/encryptionconfig/config.go#L70-L93) 为 3 分钟。故障持续超过窗口后，新写入会失败；缓存未命中的解密也可能需要 KMS 恢复后才能完成。
- 热重载只重载配置：增删或重排 provider 前，先确认旧数据的前缀仍有 provider 可解，再按官方[迁移流程](https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/)操作。
- etcd 快照与备份和存储层同源：未配置加密时备份即明文，配置后才是密文，备份链路应纳入威胁模型。
- 官方 [Secret 良好实践](https://kubernetes.io/docs/concepts/security/secrets-good-practices/) 建议按需考虑第三方 Secret store，并可通过 Secrets Store CSI Driver 把外部数据挂载给获授权的 Pod。是否使用 Kubernetes Secret 应根据威胁模型、访问控制和轮换要求判断，不宜按“高、中、低敏感度”简单分类。Vault 等系统可以作为外部凭据来源；SOPS 主要保护 Git 中的加密配置，解密后的结果仍可能被写成 Kubernetes Secret，它不等同于运行时外部 Secret store。

## 如何验证自己的集群

下面的检查应在隔离测试集群中进行。直接读取 etcd 或调整加密配置需要控制面权限，操作前应确认备份与回滚路径。

### 1. 验证 API 表示

创建一个不含真实凭据的测试 Secret：

```bash
kubectl create secret generic secret-demo \
  --from-literal=username=demo \
  --from-literal=password=not-a-real-password

kubectl get secret secret-demo -o jsonpath='{.data.password}' | base64 -d
```

这只能证明 API 字段是可逆的 Base64，不代表 etcd 是否加密。

### 2. 验证静态数据加密

先检查每个 kube-apiserver 是否设置 `--encryption-provider-config`，再确认 `secrets` 的第一个 provider 不是 `identity`。最后从 etcd 读取测试对象的原始值，检查是否以 `k8s:enc:` 开头（如 `k8s:enc:aesgcm:v1:`、`k8s:enc:kms:v2:`）；未配置加密时原始值没有该前缀。

只检查启动参数仍不够：旧对象可能是在启用加密前写入的。应按官方迁移流程重写历史对象，并抽样验证 etcd 原始数据。

### 3. 验证 volume 与 env 更新差异

让测试 Pod 同时通过环境变量和 volume 使用同一个 Secret，更新 Secret 后分别观察：

```bash
kubectl patch secret secret-demo \
  --type merge \
  -p '{"stringData":{"password":"rotated-test-value"}}'
```

预期结果是：volume 文件最终变化，原进程的环境变量保持旧值。测试时不要用 `subPath`，否则 volume 也不会自动更新。

## 总结

Kubernetes Secret 的实现并不是「把字符串 Base64 后挂载进容器」这么简单。API 类型用 `map[string][]byte` 保存内容，`stringData` 只在写入转换时提供便利；registry 负责 namespace、类型和大小校验；API Server 在写 etcd 前执行可配置的 transformer，未配置时没有静态数据加密；kubelet 只管理 Pod 引用的对象，按 Watch、TTL 或直接 GET 策略保持新鲜度；环境变量、volume、镜像凭据和 ServiceAccount token 各自的解析与更新生命周期不同。

理解这些边界后，许多生产问题会更容易分类：数据是否加密要查 API Server 的 storage transformer；文件是否更新要查 kubelet 缓存和 volume reconcile；环境变量是否变化要查容器是否重建；ServiceAccount token 是否轮换要查 TokenRequest 和 projected volume，而不是只搜索 Secret 对象。

