静态加密 Secret 数据

本文展示如何启用和配置静态 Secret 数据的加密。

准备开始

  • 你必须拥有一个 Kubernetes 的集群,同时你的 Kubernetes 集群必须带有 kubectl 命令行工具。 建议在至少有两个节点的集群上运行本教程,且这些节点不作为控制平面主机。 如果你还没有集群,你可以通过 Minikube 构建一个你自己的集群,或者你可以使用下面任意一个 Kubernetes 工具构建:

  • 此任务假设你将 Kubernetes API 服务器组件以静态 Pod 方式运行在每个控制平面节点上。

  • 集群的控制平面必须使用 etcd v3.x(主版本 3,任何次要版本)。

  • 要加密自定义资源,你的集群必须运行 Kubernetes v1.26 或更高版本。

  • 在 Kubernetes v1.27 或更高版本中可以使用通配符匹配资源。

你的 Kubernetes 服务器版本必须不低于版本 1.13. 要获知版本信息,请输入 kubectl version.

配置并确定是否已启用静态数据加密

kube-apiserver 的参数 --encryption-provider-config 控制 API 数据在 etcd 中的加密方式。 该配置作为一个名为 EncryptionConfiguration 的 API 提供。 --encryption-provider-config-automatic-reload 布尔参数决定了磁盘内容发生变化时是否应自动重新加载 --encryption-provider-config 设置的文件。这样可以在不重启 API 服务器的情况下进行密钥轮换。

下面提供了一个示例配置。

理解静态数据加密

apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
      - configmaps
      - pandas.awesome.bears.example
    providers:
      - identity: {}
      - aesgcm:
          keys:
            - name: key1
              secret: c2VjcmV0IGlzIHNlY3VyZQ==
            - name: key2
              secret: dGhpcyBpcyBwYXNzd29yZA==
      - aescbc:
          keys:
            - name: key1
              secret: c2VjcmV0IGlzIHNlY3VyZQ==
            - name: key2
              secret: dGhpcyBpcyBwYXNzd29yZA==
      - secretbox:
          keys:
            - name: key1
              secret: YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY=
  - resources:
      - events
    providers:
      - identity: {} # 即使如下指定 *.* 也不会加密 events
  - resources:
      - '*.apps'
    providers:
      - aescbc:
          keys:
          - name: key2
            secret: c2VjcmV0IGlzIHNlY3VyZSwgb3IgaXMgaXQ/Cg==
  - resources:
      - '*.*'
    providers:
      - aescbc:
          keys:
          - name: key3
            secret: c2VjcmV0IGlzIHNlY3VyZSwgSSB0aGluaw==

每个 resources 数组项目是一个单独的完整的配置。 resources.resources 字段是应加密的 Kubernetes 资源(例如 Secret、ConfigMap 或其他资源)名称 (resourceresource.group)的数组。

如果自定义资源被添加到 EncryptionConfiguration 并且集群版本为 1.26 或更高版本, 则 EncryptionConfiguration 中提到的任何新创建的自定义资源都将被加密。 在该版本之前存在于 etcd 中的任何自定义资源和配置不会被加密,直到它们被下一次写入到存储为止。 这与内置资源的行为相同。请参阅确保所有 Secret 都已加密一节。

providers 数组是可能的加密 provider 的有序列表,用于你所列出的 API。

每个条目只能指定一个 provider 类型(可以是 identityaescbc,但不能在同一个项目中同时指定二者)。 列表中的第一个 provider 用于加密写入存储的资源。 当从存储器读取资源时,与存储的数据匹配的所有 provider 将按顺序尝试解密数据。 如果由于格式或密钥不匹配而导致没有 provider 能够读取存储的数据,则会返回一个错误,以防止客户端访问该资源。

EncryptionConfiguration 支持使用通配符指定应加密的资源。 使用 “*.<group>” 加密 group 内的所有资源(例如以上例子中的 “*.apps”)或使用 “*.*” 加密所有资源。“*.” 可用于加密核心组中的所有资源。“*.*” 将加密所有资源,甚至包括 API 服务器启动之后添加的自定义资源。

如果启用了通配符,但想要针对特定资源退出加密,则可以通过添加带有资源名称的新 resources 数组项, 后跟附带 identity 提供商的 providers 数组项。例如,如果启用了 “*.*”, 但想要排除对 events 资源的加密,则应向 resources 数组添加一个新项(以 events 为资源名称), 后跟包含 identity 的 providers 数组。新项应如下所示:

- resources:
    - events
  providers:
    - identity: {}

确保新项列在资源数组中的通配符 “*.*” 项之前,使新项优先。

有关 EncryptionConfiguration 结构体的更多详细信息, 请参阅加密配置 API

Providers

下表描述了每个可用的 Provider:

Kubernetes 静态数据加密的 Provider
名称 加密类型 强度 速度 密钥长度
identity N/A N/A N/A
不加密写入的资源。当设置为第一个 provider 时,已加密的资源将在新值写入时被解密。
aescbc 带有 PKCS#7 填充的 AES-CBC 32 字节
由于 CBC 容易受到密文填塞攻击(Padding Oracle Attack),不推荐使用。密钥材料可从控制面主机访问。
aesgcm 带有随机数的 AES-GCM 每写入 200k 次后必须轮换 最快 16、24 或者 32 字节
不建议使用,除非实施了自动密钥轮换方案。密钥材料可从控制面主机访问。
kms v1 针对每个资源使用不同的 DEK 来完成信封加密。 最强 慢(kms V2 相比 32 字节
通过数据加密密钥(DEK)使用 AES-GCM 加密数据; DEK 根据 Key Management Service(KMS)中的配置通过密钥加密密钥(Key Encryption Keys,KEK)加密。 密钥轮换方式简单,每次加密都会生成一个新的 DEK,KEK 的轮换由用户控制。
阅读如何配置 KMS V1 Provider
kms v2 (beta) 针对每个 API 服务器使用不同的 DEK 来完成信封加密。 最强 32 字节
通过数据加密密钥(DEK)使用 AES-GCM 加密数据; DEK 根据 Key Management Service(KMS)中的配置通过密钥加密密钥(Key Encryption Keys,KEK)加密。 API 服务器启动时会生成一个新的 DEK,并重复使用它进行加密。 每当轮换 KEK 时,DEK 也会轮换。 如果使用第三方工具进行密钥管理,会是一个不错的选择。 从 `v1.27` 开始,该功能处于 Beta 阶段。
阅读如何配置 KMS V2 Provider
secretbox XSalsa20 和 Poly1305 更快 32 字节
使用相对较新的加密技术,在需要高度评审的环境中可能不被接受。密钥材料可从控制面主机访问。

每个 provider 都支持多个密钥 - 在解密时会按顺序使用密钥,如果是第一个 provider,则第一个密钥用于加密。

默认情况下,identity 驱动被用来对 etcd 中的 Secret 数据提供保护,而这个驱动不提供加密能力。 EncryptionConfiguration 的引入是为了能够使用本地管理的密钥来在本地加密 Secret 数据。

使用本地管理的密钥来加密 Secret 数据能够保护数据免受 etcd 破坏的影响,不过无法针对主机被侵入提供防护。 这是因为加密的密钥保存在主机上的 EncryptionConfig YAML 文件中, 有经验的入侵者仍能访问该文件并从中提取出加密密钥。

封套加密(Envelope Encryption)引入了对独立密钥的依赖,而这个密钥并不保存在 Kubernetes 中。 在这种情况下,入侵者需要攻破 etcd、kube-apiserver 和第三方的 KMS 驱动才能获得明文数据,因而这种方案提供了比本地保存加密密钥更高的安全级别。

加密你的数据

创建一个新的加密配置文件:

apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
      - configmaps
      - pandas.awesome.bears.example
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: <BASE 64 ENCODED SECRET>
      - identity: {}

遵循如下步骤来创建一个新的 Secret:

  1. 生成一个 32 字节的随机密钥并进行 base64 编码。如果你在 Linux 或 macOS 上,请运行以下命令:

    head -c 32 /dev/urandom | base64
    
  1. 将这个值放入到 EncryptionConfiguration 结构体的 secret 字段中。

  2. 设置 kube-apiserver--encryption-provider-config 参数,将其指向配置文件所在位置。

    你将需要把新的加密配置文件挂载到 kube-apiserver 静态 Pod。以下是这个操作的示例:

    1. 将新的加密配置文件保存到控制平面节点上的 /etc/kubernetes/enc/enc.yaml
    2. 编辑 kube-apiserver 静态 Pod 的清单:/etc/kubernetes/manifests/kube-apiserver.yaml, 代码范例如下:
    apiVersion: v1
    kind: Pod
    metadata:
    annotations:
     kubeadm.kubernetes.io/kube-apiserver.advertise-address.endpoint: 10.10.30.4:6443
    creationTimestamp: null
    labels:
     component: kube-apiserver
     tier: control-plane
    name: kube-apiserver
    namespace: kube-system
    spec:
    containers:
    - command:
     - kube-apiserver
     ...
     - --encryption-provider-config=/etc/kubernetes/enc/enc.yaml  # <-- 增加这一行
     volumeMounts:
     ...
     - name: enc                           # <-- 增加这一行
       mountPath: /etc/kubernetes/enc      # <-- 增加这一行
       readonly: true                      # <-- 增加这一行
     ...
    volumes:
    ...
    - name: enc                             # <-- 增加这一行
     hostPath:                             # <-- 增加这一行
       path: /etc/kubernetes/enc           # <-- 增加这一行
       type: DirectoryOrCreate             # <-- 增加这一行
    ...
    
  1. 重启你的 API 服务器。

验证数据已被加密

数据在写入 etcd 时会被加密。重新启动你的 kube-apiserver 后,任何新创建或更新的 Secret 或在 EncryptionConfiguration 中配置的其他资源类型都应在存储时被加密。 如果想要检查,你可以使用 etcdctl 命令行程序来检索你的 Secret 数据内容。

  1. 创建一个新的 secret,名称为 secret1,命名空间为 default

    kubectl create secret generic secret1 -n default --from-literal=mykey=mydata
    
  1. 使用 etcdctl 命令行,从 etcd 中读取 Secret:

    ETCDCTL_API=3 etcdctl get /registry/secrets/default/secret1 [...] | hexdump -C
    

    这里的 [...] 是用来连接 etcd 服务的额外参数。

    例如:

    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/secret1 | hexdump -C
    

    输出类似于(有删减):

    00000000  2f 72 65 67 69 73 74 72  79 2f 73 65 63 72 65 74  |/registry/secret|
    00000010  73 2f 64 65 66 61 75 6c  74 2f 73 65 63 72 65 74  |s/default/secret|
    00000020  31 0a 6b 38 73 3a 65 6e  63 3a 61 65 73 63 62 63  |1.k8s:enc:aescbc|
    00000030  3a 76 31 3a 6b 65 79 31  3a c7 6c e7 d3 09 bc 06  |:v1:key1:.l.....|
    00000040  25 51 91 e4 e0 6c e5 b1  4d 7a 8b 3d b9 c2 7c 6e  |%Q...l..Mz.=..|n|
    00000050  b4 79 df 05 28 ae 0d 8e  5f 35 13 2c c0 18 99 3e  |.y..(..._5.,...>|
    [...]
    00000110  23 3a 0d fc 28 ca 48 2d  6b 2d 46 cc 72 0b 70 4c  |#:..(.H-k-F.r.pL|
    00000120  a5 fc 35 43 12 4e 60 ef  bf 6f fe cf df 0b ad 1f  |..5C.N`..o......|
    00000130  82 c4 88 53 02 da 3e 66  ff 0a                    |...S..>f..|
    0000013a
    
  1. 验证存储的密钥前缀是否为 k8s:enc:aescbc:v1:,这表明 aescbc provider 已加密结果数据。 确认 etcd 中显示的密钥名称和上述 EncryptionConfiguration 中指定的密钥名称一致。 在此例中,你可以看到在 etcdEncryptionConfiguration 中使用了名为 key1 的加密密钥。

  2. 通过 API 检索,验证 Secret 是否被正确解密:

    kubectl get secret secret1 -n default -o yaml
    

    其输出应该包含 mykey: bXlkYXRhmydata 的内容是被加密过的, 请参阅解密 Secret 了解如何完全解码 Secret 内容。

确保所有 Secret 都被加密

由于 Secret 是在写入时被加密,因此对 Secret 执行更新也会加密该内容。

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

上面的命令读取所有 Secret,然后使用服务端加密来更新其内容。

轮换解密密钥

在不发生停机的情况下更改 Secret 需要多步操作,特别是在有多个 kube-apiserver 进程正在运行的高可用环境中。

  1. 生成一个新密钥并将其添加为所有服务器上当前 provider 的第二个密钥条目
  2. 重新启动所有 kube-apiserver 进程以确保每台服务器都可以使用新密钥进行解密
  3. 将新密钥设置为 keys 数组中的第一个条目,以便在配置中使用其进行加密
  4. 重新启动所有 kube-apiserver 进程以确保每个服务器现在都使用新密钥进行加密
  5. 运行 kubectl get secrets --all-namespaces -o json | kubectl replace -f - 以用新密钥加密所有现有的 Secret
  6. 在使用新密钥备份 etcd 后,从配置中删除旧的解密密钥并更新所有密钥

当只运行一个 kube-apiserver 实例时,第 2 步可以忽略。

解密所有数据

要禁用静态加密,请将 identity provider 作为配置中的第一个条目并重新启动所有 kube-apiserver 进程。

apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
    providers:
      - identity: {}
      - aescbc:
          keys:
            - name: key1
              secret: <BASE 64 ENCODED SECRET>

然后运行以下命令以强制解密所有 Secret:

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

接下来

进一步学习 EncryptionConfiguration 配置 API (v1)

最后修改 May 11, 2023 at 9:28 PM PST: [zh] resync page encrypt-data (0770e2bcab)