Kubernetes etcd 敏感数据加密教程 — 保护 Secret 免受明文存储

在 Kubernetes 中创建Secret时,其值会以 Base64 形式显示。因此,初次接触的人很容易误以为 Secret 已经加密。

然而,Base64 是一种编码,而非加密。任何人都可以通过以下命令查看原始值:

echo 'bXlkYXRh' | base64 --decode

更重要的问题是,默认配置的kube-apiserver在将 Secret 存储到 etcd 时,不会应用单独的静态加密(encryption at rest)。如果 etcd 备份文件、数据目录或磁盘快照泄露,敏感数据可能会在未经 RBAC 授权的情况下暴露。

本文将以通过 kubeadm 配置的 Kubernetes 集群为例,亲自验证以下过程:

  • 在应用加密前检查 etcd 中存储的 Secret
  • 生成 32 字节加密密钥
  • 编写EncryptionConfiguration
  • 将配置连接到kube-apiserver静态 Pod
  • 在 etcd 中验证新 Secret 是否已加密
  • 重写现有 Secret 以全部加密
  • 密钥轮换和生产环境中的注意事项

本教程是基于 Kubernetes 官方文档的Encrypting Confidential Data at Rest章节编写的。

API 服务器加密并将 Secret 存储到 etcd 的流程


首先需要理解:加密什么?

Kubernetes 静态加密并非用于加密挂载到 Pod 的文件或应用程序数据库的功能。

加密对象是通过 Kubernetes API 创建并持久存储在 etcd 中的 API 资源。典型地,可以指定以下资源:

  • secrets
  • configmaps
  • 用户自定义资源 CRD 对象
  • 其他存储在 etcd 中的 Kubernetes API 资源

数据流如下:

kubectl 또는 애플리케이션
        ↓
kube-apiserver
        ↓  EncryptionConfiguration에 따라 암호화
      etcd

当客户端通过 Kubernetes API 查询 Secret 时,kube-apiserver会解密 etcd 中的密文并返回正常的 API 响应。因此,用户可以像以前一样使用kubectl get secret。

静态加密是防范 etcd 磁盘或备份泄露的防御层。它不能替代以下安全控制:

安全控制 保护区域
Kubernetes RBAC 控制谁可以通过 API 读取 Secret
TLS 保护 API 服务器和 etcd 之间的传输路径
etcd 静态加密 保护存储在 etcd 数据和备份中的值
磁盘加密 保护整个节点磁盘或卷
KMS/HSM 在集群外部保护加密密钥并管理其生命周期

仅应用其中一项并不能消除对其他层的需求。

实践环境和注意事项

本文假设以下环境:

  • 通过 kubeadm 配置的 Kubernetes 集群
  • etcd 3.x
  • 每个控制平面上kube-apiserver作为静态 Pod 运行
  • 可使用sudo访问控制平面节点
  • 可使用kubectl和etcdctl

通过以下命令检查集群版本:

kubectl version

此操作会更改控制平面的核心设置。在生产环境中,务必备份 etcd 快照和当前静态 Pod 清单,并确认恢复过程后再进行。

sudo cp -a /etc/kubernetes/manifests/kube-apiserver.yaml 
  /root/kube-apiserver.yaml.before-encryption

如果是高可用集群,所有控制平面节点必须具有相同的加密设置和密钥。如果节点之间的设置不同,可能会出现某些 API 服务器写入的数据无法被其他 API 服务器解密的情况。

第 1 步:检查是否已应用加密

首先,检查kube-apiserver是否包含--encryption-provider-config参数。

sudo grep -- '--encryption-provider-config' 
  /etc/kubernetes/manifests/kube-apiserver.yaml

如果没有输出结果,则表示 API 服务器级别的静态加密未启用。

即使存在参数,也不要掉以轻心。如果EncryptionConfiguration的第一个 provider 是identity,则新数据将以明文形式存储。这是因为 provider 的顺序决定了读写操作。

  • 写入时:使用列表中的第一个 provider
  • 读取时:从上到下尝试,使用第一个可解密的 provider

在迁移过程中,应将加密 provider 放在首位,并将用于读取旧明文数据的identity放在其后。

第 2 步:在应用前创建 Secret 并检查 etcd 值

为了进行比较,创建一个测试 Secret。

kubectl create secret generic secret-before 
  --namespace default 
  --from-literal=mykey=mydata

通过 Kubernetes API 查询时,值显示为 Base64。

kubectl get secret secret-before -n default -o yaml

预期数据如下:

data:
  mykey: bXlkYXRh

现在,在控制平面节点上检查 etcd 的原始值。

sudo 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/secret-before | hexdump -C

如果未应用加密,您应该能够在输出中识别 Secret 的结构和对应于mydata的数据。这就是为什么仅凭 Base64 显示无法保护 etcd 存储数据的原因。

etcdctl连接证书和端点可能因集群配置而异。如果使用外部 etcd,则必须指定实际的端点和客户端证书。

第 3 步:生成 32 字节加密密钥

官方文档建议在使用本地加密 provider 时,生成一个 32 字节的随机密钥并进行 Base64 编码。

KEY=$(head -c 32 /dev/urandom | base64)

不建议将密钥打印到屏幕、复制到 shell 历史记录、即时通讯工具或普通文档中。拥有此密钥的人可以解密 etcd 中的加密数据。

创建用于存储加密配置的目录。

sudo install -d -m 700 /etc/kubernetes/enc

第 4 步:编写 EncryptionConfiguration

本次实践中,为了理解 Kubernetes 官方教程的存储格式和验证流程,我们将使用aescbc provider 加密 Secret 和 ConfigMap。

注意:当前的 Kubernetes 官方文档将aescbc归类为弱加密方式,因为它可能受到 CBC 填充预言攻击,不建议在生产环境中使用。以下配置仅用于实践,以学习静态加密的工作原理和迁移过程。对于新的生产环境,应优先考虑 KMS v2。

sudo tee /etc/kubernetes/enc/enc.yaml >/dev/null <<EOF
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
      - configmaps
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: ${KEY}
      - identity: {}
EOF

unset KEY
sudo chmod 600 /etc/kubernetes/enc/enc.yaml
sudo chown root:root /etc/kubernetes/enc/enc.yaml

provider 的顺序很重要。

providers:
  - aescbc: ...
  - identity: {}

在此配置中,新创建或更新的数据将通过aescbc加密。由于末尾的identity provider,现有明文存储的数据仍可读取。

反之,如果将identity放在首位,API 服务器将继续以明文形式存储新数据。

aescbc、aesgcm、secretbox、KMS 中如何选择?

Kubernetes 支持多种加密 provider,但其操作特性各不相同。

Provider 特点 操作注意事项
aescbc 配置简单的 AES-CBC 本地对称密钥方式 官方文档将其归类为弱方式,不建议在生产环境中使用
aesgcm 提供认证加密 需要密钥轮换,以避免使用同一密钥写入超过约 20 万条记录
secretbox 基于 XSalsa20 和 Poly1305 密钥存在于本地配置文件中
kms v2 外部 KMS 管理密钥加密密钥的信封加密 提供强大的密钥隔离,需要单独的插件和可用性设计

将本地密钥直接存储在EncryptionConfiguration中的方式优于不使用加密,但如果控制平面文件和 etcd 数据同时被窃取,防御能力将受到限制。对于重要的生产环境,应考虑与云 KMS 或 HSM 集成。

第 5 步:将加密设置连接到 kube-apiserver

编辑静态 Pod 清单。

sudo vi /etc/kubernetes/manifests/kube-apiserver.yaml

在kube-apiserver的command中添加以下参数:

- --encryption-provider-config=/etc/kubernetes/enc/enc.yaml

在同一容器的volumeMounts中添加只读挂载。

volumeMounts:
  - name: enc
    mountPath: /etc/kubernetes/enc
    readOnly: true

在 Pod spec.volumes中添加 hostPath 卷。

volumes:
  - name: enc
    hostPath:
      path: /etc/kubernetes/enc
      type: DirectoryOrCreate

保存文件后,kubelet 将检测到静态 Pod 更改并重新创建kube-apiserver。

kubectl -n kube-system get pod 
  -l component=kube-apiserver -w

检查 API 服务器是否恢复正常状态。

kubectl get --raw='/readyz?verbose'

如果发生错误,请检查 kubelet 日志和容器运行时日志,并使用预先备份的清单进行恢复。

sudo journalctl -u kubelet -n 200 --no-pager

第 6 步:验证新 Secret 是否已加密存储

应用加密设置后,创建一个新的 Secret。

kubectl create secret generic secret-after 
  --namespace default 
  --from-literal=mykey=mydata

直接从 etcd 读取值。

sudo 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/secret-after | hexdump -C

输出中应显示以下前缀:

k8s:enc:aescbc:v1:key1:

各项目的含义如下:

值 含义
k8s:enc Kubernetes 静态加密数据
aescbc 使用的 provider
v1 存储格式版本
key1 用于加密的密钥名称

现在,验证通过 API 进行的解密是否也正常。

kubectl get secret secret-after 
  --namespace default 
  -o jsonpath='{.data.mykey}' | base64 --decode
echo

如果输出以下值,则表示正常:

mydata

这意味着 etcd 中存储的是密文,但拥有权限的用户仍然可以通过 Kubernetes API 正常使用 Secret。

第 7 步:重写现有 Secret 以全部加密

这里有一个最容易被忽视的部分。

即使启用了加密设置,etcd 中已存储的数据也不会自动重新加密。只有在设置应用后新创建或更新的资源才会加密。

如果将所有现有 Secret 以相同内容重新保存,API 服务器将应用加密 provider 并将其记录到 etcd 中。

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

如果发生写入冲突,再次执行也是安全的。在大型集群中,最好按命名空间分批执行 Secret 更新,而不是一次性更新所有 Secret。

如果 ConfigMap 也被指定为加密对象,则需要单独重新保存。

kubectl get configmaps --all-namespaces -o json 
  | kubectl replace -f -

迁移后,检查最初创建的secret-before是否也具有加密前缀。

sudo 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/secret-before | hexdump -C

第 8 步:是否移除明文回退?

如果已充分验证所有目标数据均已加密,则可以移除identity provider,以防止 API 服务器读取明文存储的数据。

providers:
  - aescbc:
      keys:
        - name: key1
          secret: <BASE64_ENCODED_32_BYTE_KEY>

然而,这一步需要谨慎。如果仍有任何未加密的资源,API 服务器将无法读取该对象。

因此,必须在满足以下所有条件后才能进行:

  • 所有目标资源是否已重新保存?
  • 所有控制平面是否使用相同的配置?
  • 是否已在 etcd 中对加密前缀进行抽样或全面检查?
  • 加密密钥是否已安全备份?
  • 恢复过程是否已实际验证?

在运营初期,更安全的做法是保留identity作为读取回退,并在迁移验证完成后,通过单独的更改操作将其移除。

第 9 步:不中断地轮换加密密钥的原理

密钥轮换并非立即删除现有密钥的操作。

Kubernetes 官方文档指导的顺序如下:

  1. 将新密钥作为现有 provider 的第二个密钥添加到所有控制平面。
  2. 应用设置,使所有 API 服务器都能读取新密钥。
  3. 安全备份新密钥。
  4. 将新密钥提升到密钥列表的首位。
  5. 应用设置,使所有 API 服务器在新写入时使用新密钥。
  6. 使用kubectl replace重新保存现有 Secret。
  7. 验证所有数据都已使用新密钥加密后,移除旧密钥。

例如,中间阶段的配置将如下所示:

providers:
  - aescbc:
      keys:
        - name: key2
          secret: <NEW_BASE64_KEY>
        - name: key1
          secret: <OLD_BASE64_KEY>
  - identity: {}

第一个密钥用于加密新数据,后面的密钥用于解密旧数据。如果在所有数据都用新密钥重新保存之前删除旧密钥,则无法解密现有 Secret。

Kubernetes 还支持通过--encryption-provider-config-automatic-reload=true选项自动重新加载配置文件。启用后,API 服务器会定期检查文件更改,因此无需在每次密钥轮换时重启 API 服务器。但是,实际应用时间需要通过相关指标进行确认。

高可用控制平面中必须遵循的顺序

如果存在多个控制平面,请不要同时更改所有节点,而应逐个顺序应用。

步骤 检查事项
1 将相同的enc.yaml安全地部署到所有节点
2 更改第一个 API 服务器设置并确认正常恢复
3 逐个更改其余 API 服务器
4 通过负载均衡器检查 API 请求是否正常
5 检查新数据的加密前缀
6 重新保存现有数据

在滚动更改期间,所有 API 服务器都必须能够读取当前的 etcd 数据。在将新密钥部署到所有节点之前,不应将其提升为第一个写入密钥。

生产环境中推荐 KMS 的原因

到目前为止使用的aescbc方式对于按照官方流程实践静态加密操作很有用。然而,目前的官方文档将此方式归类为弱加密,并且加密密钥也位于/etc/kubernetes/enc/enc.yaml中。

如果攻击者同时获取 etcd 数据和控制平面配置文件,他们就可以同时获取密文和解密密钥。

使用 KMS provider 可以将实际数据加密密钥保护在集群外部的 KMS 或 HSM 中。此外,还可以更容易地利用以下操作功能:

  • 密钥访问权限分离
  • 集中式密钥轮换
  • 密钥使用审计日志
  • 利用 HSM 或云管理型 KMS
  • Kubernetes 管理员和密钥管理员的角色分离

因此,本文的本地aescbc配置应被视为理解静态加密工作原理的起点,对于需要高安全级别的生产集群,建议考虑使用 KMS provider 进行数据加密。

常见错误

1. 认为 Secret 的 Base64 是加密

Base64 是一种任何人都可以恢复的表示方式。它与 etcd 静态加密无关。

2. 将 identity 作为第一个 provider

可以读取,但新数据将以明文形式存储。写入 provider 是列表中的第一个项目。

3. 仅应用设置而不重新保存现有 Secret

现有数据不会自动转换。需要像kubectl replace这样的重写过程。

4. 为每个节点部署不同的密钥

在 HA 环境中,API 服务器的解密能力可能因节点而异,导致间歇性故障。

5. 未备份密钥就删除旧密钥

用该密钥加密的数据将无法恢复。如果只有 etcd 备份而没有密钥备份,则无法恢复加密的 Secret。

6. 对加密配置文件设置宽松权限

本地 provider 的密钥实际上就是解密权限。应限制为只有 root 和 API 服务器的执行主体才能读取。

实践总结

本次实践中确认的核心要点如下:

  1. Kubernetes Secret 的 Base64 不是加密。
  2. 默认设置下,API 资源可能在 etcd 中以未加密方式存储。
  3. EncryptionConfiguration的第一个 provider 会加密新数据。
  4. identity是迁移过程中用于读取现有明文数据的回退机制。
  5. 仅应用设置并不能加密现有数据。
  6. 必须在 etcd 中检查k8s:enc:aescbc:v1:key1:前缀。
  7. 密钥和 etcd 备份应一起管理,但访问权限应分离。
  8. 在生产环境中,外部 KMS 集成提供更强的密钥保护。

静态加密并不能阻止攻击者获取 Kubernetes API 权限。相反,它降低了 etcd 磁盘、快照和备份文件泄露时敏感数据立即暴露的风险。

Kubernetes 安全性在 RBAC、网络保护、审计日志、备份安全和静态加密协同工作时才能完善。


参考官方文档


Comments

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注