4. Secret的静态加密

Secret 默认以 Base64 编码形式存放在 etcd 中,Base64 只是编码而非加密,任何能读取 etcd 数据文件或备份的人都能直接还原出明文凭据。静态加密(Encryption at Rest)解决的就是这一层风险:在 API Server 写入 etcd 之前对 Secret 等资源加密,读取时再解密。它保护的是"落盘数据",不改变传输层 TLS 和 RBAC 的职责边界。

一、EncryptionConfiguration 的结构与语义

静态加密由 API Server 的启动参数 --encryption-provider-config 指向一个 YAML 配置文件来启用。该文件的核心是 resources 列表,每一项声明"哪些资源"用"哪些 provider"加密,而 provider 列表的顺序决定了行为:第一个 provider 用于写入加密,所有 provider 依次尝试用于读取解密。这意味着轮换密钥时,把新密钥放在列表首位、旧密钥保留在后面,即可实现"新写用新钥、旧数据仍可读"的平滑过渡。

常见的 provider 有三类:identity(不加密,明文写入,仅用于读取历史明文数据)、aescbc/aesgcm(本地密钥,密钥直接写在配置里)、kms(调用外部 KMS 服务,本地只保存密文形式的 DEK)。下面是一个同时覆盖三类 provider 的示例配置:

apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
      - configmaps
    providers:
      - kms:
          name: myKMS
          endpoint: unix:///var/run/kmsplugin/socket.sock
          cachesize: 1000
          timeout: 3s
      - aescbc:
          keys:
            - name: key2
              secret: c2VjcmV0IGtleTIgYmFzZTY0IGVuY29kZWQ=
            - name: key1
              secret: c2VjcmV0IGtleTEgYmFzZTY0IGVuY29kZWQ=
      - identity: {}

注意 aescbc 的 keys 也是有序的:第一个 key 用于加密新数据,其余 key 用于解密旧数据。密钥必须是 32 字节的随机值再做 Base64 编码,可用 head -c 32 /dev/urandom | base64 生成。示例中的 secret 值仅为占位,实际部署必须替换为真实随机密钥,且不得提交到代码仓库。

二、KMS Provider 集成:信封加密

本地 aescbc 的弱点是密钥与密文同处一台机器,一旦节点被攻破,加密形同虚设。KMS Provider 采用信封加密(Envelope Encryption):API Server 为每个资源生成一个数据加密密钥 DEK,用 DEK 加密资源本身,再把 DEK 发送给外部 KMS 加密成 KEK 密文,最终 etcd 中保存的是"资源密文 + 被 KMS 加密的 DEK"。主密钥始终留在 KMS 内部,API Server 本地不落盘任何明文密钥。

集成方式有两种:v1 通过 Unix Domain Socket 与 kubelet 上的 KMS 插件通信(即上面的 endpoint: unix://...),v2 则直接以 gRPC 连接 KMS,支持密钥版本与健康状态上报。生产环境建议使用 v2,因为它能感知 KMS 不可用并触发降级告警,而 v1 在 KMS 故障时可能静默失败。

Secret 静态加密:写入与读取路径 kubectl / 控制器 提交 Secret 明文 kube-apiserver EncryptionConfiguration providers 顺序 = 优先级 ① kms ② aescbc ③ identity 外部 KMS 主密钥 KEK 不出域 加密 / 解密 DEK gRPC (v2) / UDS (v1) etcd 资源密文 + 加密的 DEK k8s:enc:kms:v2 写入 DEK 加密请求 落盘 读取 DEK 解密 返回明文 密钥轮换与验证要点 轮换:新密钥置于 providers 首位 → 重启 apiserver → 旧数据仍可解密 重写:kubectl get secrets -A -o json | kubectl replace -f - 触发全量重新加密 验证:etcdctl get /registry/secrets/<ns>/<name> 应看到 k8s:enc: 前缀而非明文 清理:确认无旧密钥依赖后,从配置中移除旧 key 并再次重启

三、etcd 加密实践与验证方法

配置生效后,最直接的验证是绕过 API Server 直接读 etcd。使用 etcdctl 读取原始键值:

ETCDCTL_API=3 etcdctl \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key \
  get /registry/secrets/default/my-secret | hexdump -C | head

若加密生效,输出开头会出现 k8s:enc:kms:v2:myKMS: 或 k8s:enc:aescbc:v1:key2: 这样的前缀,其后是二进制密文;若仍是可读的 YAML 文本,说明该资源未被加密,需检查 resources 是否包含 secrets、API Server 是否真正重启加载了新配置。注意:启用加密不会自动加密已有数据,历史 Secret 仍是明文,必须通过重写操作触发重新加密。

重写所有 Secret 的常用做法是读取后原样替换:

kubectl get secrets --all-namespaces -o json \
  | kubectl replace -f -

该命令会逐个更新 Secret,每次更新都走一次写入路径,从而用当前首选 provider 重新加密。执行后应再次用 etcdctl 抽查若干命名空间,确认前缀已变为新 provider 标识。对于大规模集群,建议分批按命名空间执行,避免 API Server 瞬时压力过高。

四、密钥轮换策略

轮换的目标是限制单个密钥的生命周期与影响面。推荐策略如下:

  • 轮换周期:KMS 主密钥建议 90 天轮换一次,本地 aescbc 密钥建议不超过 30 天,具体以合规要求为准。
  • 两阶段执行:先"加新留旧"(新密钥写入首位,旧密钥保留用于解密),完成全量重写后,再"删旧"(移除旧密钥并重启)。任何一步跳过都可能导致旧数据无法解密。
  • 变更窗口:重启 API Server 会短暂中断写入,应在低峰期滚动重启各控制平面节点,并确认所有节点加载了同一份配置。
  • 备份与回滚:轮换前备份 etcd 快照,并保留旧密钥的安全副本;一旦新配置异常,可快速回退到旧配置恢复服务。
切勿在未完成全量重写前删除旧密钥。一旦旧密钥丢失,使用该密钥加密的历史 Secret 将永久无法解密,表现为 API Server 读取时报 "unable to decrypt" 错误,且无法通过重启恢复。

五、常见排障与判断依据

实践中几类高频问题值得留意:其一,配置写入了但未重启 API Server,加密未生效,etcd 中仍是明文;其二,resources 只写了 secrets 却遗漏了 configmaps 等同样含敏感信息的资源;其三,KMS 服务不可达时,若配置中 identity 排在前面,会静默降级为明文写入,因此务必把 identity 放在最后一位,仅用于兼容读取。判断加密是否真正生效,唯一可靠的依据是直接观察 etcd 中的原始字节,而不是依赖配置文件的"看起来正确"。

一个实用的自检清单:① etcdctl 抽查是否出现 k8s:enc: 前缀;② 确认所有控制平面节点使用同一份 EncryptionConfiguration;③ 确认轮换后旧密钥仍保留且可解密;④ 确认 KMS 健康检查与告警已接入监控。四项全部通过,静态加密才算真正落地。