--- title: 使用 KMS 驱动进行数据加密 content_type: task weight: 370 --- 本页展示了如何配置密钥管理服务(Key Management Service,KMS)驱动和插件以启用 Secret 数据加密。 在 Kubernetes {{< skew currentVersion >}} 中,存在两个版本的 KMS 静态加密方式。 如果可行的话,建议使用 KMS v2,因为(自 Kubernetes v1.28 起)KMS v1 已经被弃用并 (自 Kubernetes v1.29 起)默认被禁用。 KMS v2 提供了比 KMS v1 明显更好的性能特征。 {{< caution >}} 本文适用于正式发布的 KMS v2 实现(以及已废弃的 v1 实现)。 如果你使用的控制平面组件早于 Kubernetes v1.29,请查看集群所运行的 Kubernetes 版本文档中的相应页面。 早期版本的 Kubernetes 在信息安全方面具有不同的行为。 {{< /caution >}} ## {{% heading "prerequisites" %}} {{< include "task-tutorial-prereqs.md" >}} 你所需要的 Kubernetes 版本取决于你已选择的 KMS API 版本。Kubernetes 推荐使用 KMS v2。 - 如果你选择了 KMS API v1 来支持早于 v1.27 版本的集群,或者你有一个仅支持 KMS v1 的旧版 KMS 插件, 那么任何受支持的 Kubernetes 版本都可以良好工作。此 API 自 Kubernetes v1.28 起被弃用。 Kubernetes 不推荐使用此 API。 {{< version-check >}} ### KMS v1 {{< feature-state for_k8s_version="v1.28" state="deprecated" >}} * 需要 Kubernetes 1.10.0 或更高版本 * 对于 1.29 及更高版本,KMS 的 v1 实现默认处于禁用状态。 要启用此特性,设置 `--feature-gates=KMSv1=true` 以配置 KMS v1 驱动。 * 你的集群必须使用 etcd v3 或更高版本 ### KMS v2 {{< feature-state for_k8s_version="v1.29" state="stable" >}} * 你的集群必须使用 etcd v3 或更高版本 ### KMS 加密和为每个对象加密的密钥 {#kms-encryption-and-perobject-encryption-keys} KMS 加密驱动使用封套加密模型来加密 etcd 中的数据。数据使用数据加密密钥(DEK)加密。 这些 DEK 经一个密钥加密密钥(KEK)加密后在一个远端的 KMS 中存储和管理。 如果你使用(已弃用的)KMS v1 实现,每次加密将生成新的 DEK。 对于 KMS v2,**每次加密**将生成新的 DEK: API 服务器使用**密钥派生函数**根据秘密的种子数结合一些随机数据生成一次性的数据加密密钥。 种子会在 KEK 轮换时进行轮换 (有关更多详细信息,请参阅下面的“了解 key_id 和密钥轮换”章节)。 KMS 驱动使用 gRPC 通过 UNIX 域套接字与一个特定的 KMS 插件通信。 这个 KMS 插件作为一个 gRPC 服务器被部署在 Kubernetes 控制平面的相同主机上,负责与远端 KMS 的通信。 ## 配置 KMS 驱动 {#configuring-the-kms-provider} 为了在 API 服务器上配置 KMS 驱动,在加密配置文件中的 `providers` 数组中加入一个类型为 `kms` 的驱动,并设置下列属性: ### KMS v1 {#configuring-the-kms-provider-kms-v1} * `apiVersion`:针对 KMS 驱动的 API 版本。此项留空或设为 `v1`。 * `name`:KMS 插件的显示名称。一旦设置,就无法更改。 * `endpoint`:gRPC 服务器(KMS 插件)的监听地址。该端点是一个 UNIX 域套接字。 * `cachesize`:以明文缓存的数据加密密钥(DEK)的数量。一旦被缓存, 就可以直接使用 DEK 而无需另外调用 KMS;而未被缓存的 DEK 需要调用一次 KMS 才能解包。 * `timeout`:在返回一个错误之前,`kube-apiserver` 等待 kms-plugin 响应的时间(默认是 3 秒)。 ### KMS v2 {#configuring-the-kms-provider-kms-v2} * `apiVersion`:针对 KMS 驱动的 API 版本。此项设为 `v2`。 * `name`:KMS 插件的显示名称。一旦设置,就无法更改。 * `endpoint`:gRPC 服务器(KMS 插件)的监听地址。该端点是一个 UNIX 域套接字。 * `timeout`:在返回一个错误之前,`kube-apiserver` 等待 kms-plugin 响应的时间(默认是 3 秒)。 KMS v2 不支持 `cachesize` 属性。一旦服务器通过调用 KMS 解密了数据加密密钥(DEK), 所有的 DEK 将会以明文形式被缓存。一旦被缓存,DEK 可以无限期地用于解密操作,而无需再次调用 KMS。 参见[理解静态配置加密](/zh-cn/docs/tasks/administer-cluster/encrypt-data)。 ## 实现 KMS 插件 {#implementing-a-kms-plugin} 为实现一个 KMS 插件,你可以开发一个新的插件 gRPC 服务器或启用一个由你的云服务驱动提供的 KMS 插件。 你可以将这个插件与远程 KMS 集成,并把它部署到 Kubernetes 控制平面上。 ### 启用由云服务驱动支持的 KMS {#enabling-the-kms-supported-by-your-cloud-provider} 有关启用云服务驱动特定的 KMS 插件的说明,请咨询你的云服务驱动商。 ### 开发 KMS 插件 gRPC 服务器 {#developing-a-kms-plugin-grpc-server} 你可以使用 Go 语言的存根文件开发 KMS 插件 gRPC 服务器。 对于其他语言,你可以用 proto 文件创建可以用于开发 gRPC 服务器代码的存根文件。 #### KMS v1 {#developing-a-kms-plugin-gRPC-server-kms-v1} * 使用 Go:使用存根文件 [api.pb.go](https://github.com/kubernetes/kubernetes/blob/release-1.25/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/envelope/v1beta1/api.pb.go) 中的函数和数据结构开发 gRPC 服务器代码。 * 使用 Go 以外的其他语言:用 protoc 编译器编译 proto 文件: [api.proto](https://github.com/kubernetes/kubernetes/blob/release-1.25/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/envelope/v1beta1/api.proto) 为指定语言生成存根文件。 #### KMS v2 {#developing-a-kms-plugin-gRPC-server-kms-v2} * 使用 Go:提供了一个高级[库](https://github.com/kubernetes/kms/blob/release-{{< skew currentVersion >}}/pkg/service/interface.go)简化这个过程。 底层实现可以使用存根文件 [api.pb.go](https://github.com/kubernetes/kms/blob/release-{{< skew currentVersion >}}/apis/v2/api.pb.go) 中的函数和数据结构开发 gRPC 服务器代码。 * 使用 Go 以外的其他语言:用 protoc 编译器编译 proto 文件: [api.proto](https://github.com/kubernetes/kms/blob/release-{{< skew currentVersion >}}/apis/v2/api.proto) 为指定语言生成存根文件。 然后使用存根文件中的函数和数据结构开发服务器代码。 #### 注意 ##### KMS v1 {#developing-a-kms-plugin-gRPC-server-notes-kms-v1} * kms 插件版本:`v1beta1` 作为对过程调用 Version 的响应,兼容的 KMS 插件应把 `v1beta1` 作为 `VersionResponse.version` 版本返回。 * 消息版本:`v1beta1` 所有来自 KMS 驱动的消息都把 version 字段设置为 `v1beta1`。 * 协议:UNIX 域套接字 (`unix`) 该插件被实现为一个在 UNIX 域套接字上侦听的 gRPC 服务器。 该插件部署时应在文件系统上创建一个文件来运行 gRPC UNIX 域套接字连接。 API 服务器(gRPC 客户端)配置了 KMS 驱动(gRPC 服务器)UNIX 域套接字端点,以便与其通信。 通过以 `/@` 开头的端点,可以使用一个抽象的 Linux 套接字,即 `unix:///@foo`。 使用这种类型的套接字时必须小心,因为它们没有 ACL 的概念(与传统的基于文件的套接字不同)。 然而,这些套接字遵从 Linux 网络命名空间约束,因此只能由同一 Pod 中的容器进行访问,除非使用了主机网络。 ##### KMS v2 {#developing-a-kms-plugin-gRPC-server-notes-kms-v2} * KMS 插件版本:`v2` 作为对过程调用 `Status` 的响应,兼容的 KMS 插件应把 `StatusResponse.version` 作为其 KMS 兼容版本返回。 该状态响应还应包括 "ok" 作为 `StatusResponse.healthz` 以及 `key_id`(远程 KMS KEK ID)作为 `StatusResponse.key_id`。Kubernetes 项目建议你的插件与稳定的 `v2` KMS API 兼容。 Kubernetes {{< skew currentVersion >}} 针对 KMS 还支持 `v2beta1` API; Kubernetes 后续版本可能会继续支持该 Beta 版本。 当一切健康时,API 服务器大约每分钟轮询一次 `Status` 过程调用, 而插件不健康时每 10 秒钟轮询一次。使用这些插件时要注意优化此调用,因为此调用将经受持续的负载。 * 加密 `EncryptRequest` 过程调用提供明文和一个 UID 以用于日志记录。 响应必须包括密文、使用的 KEK 的 `key_id`,以及可选的任意元数据,这些元数据可以 帮助 KMS 插件在未来的 `DecryptRequest` 调用中(通过 `annotations` 字段)进行解密。 插件必须保证所有不同的明文都会产生不同的响应 `(ciphertext, key_id, annotations)`。 如果插件返回一个非空的 `annotations` 映射,则所有映射键必须是完全限定域名, 例如 `example.com`。`annotation` 的一个示例用例是 `{"kms.example.io/remote-kms-auditid":"<远程 KMS 使用的审计 ID>"}`。 当 API 服务器运行正常时,并不会高频执行 `EncryptRequest` 过程调用。 插件实现仍应力求使每个请求的延迟保持在 100 毫秒以下。 * 解密 `DecryptRequest` 过程调用提供 `EncryptRequest` 中的 `(ciphertext, key_id, annotations)` 和一个 UID 以用于日志记录。正如预期的那样,它是 `EncryptRequest` 调用的反向操作。插件必须验证 `key_id` 是否为其理解的密钥ID - 除非这些插件确定数据是之前自己加密的,否则不应尝试解密。 在启动时,API 服务器可能会执行数千个 `DecryptRequest` 过程调用以填充其监视缓存。 因此,插件实现必须尽快执行这些调用,并应力求使每个请求的延迟保持在 10 毫秒以下。 * 理解 `key_id` 和密钥轮换 `key_id` 是目前使用的远程 KMS KEK 的公共、非机密名称。 它可能会在 API 服务器的常规操作期间记录,因此不得包含任何私有数据。 建议插件实现使用哈希来避免泄漏任何数据。 KMS v2 指标负责在通过 `/metrics` 端点公开之前对此值进行哈希。 API 服务器认为从 `Status` 过程调用返回的 `key_id` 是权威性的。因此,此值的更改表示远程 KEK 已更改, 并且使用旧 KEK 加密的数据应在执行无操作写入时标记为过期(如下所述)。如果 `EncryptRequest` 过程调用返回与 `Status` 不同的 `key_id`,则响应将被丢弃,并且插件将被认为是不健康的。 因此,插件实现必须保证从 `Status` 返回的 `key_id` 与 `EncryptRequest` 返回的 `key_id` 相同。 此外,插件必须确保 `key_id` 是稳定的,并且不会在不同值之间翻转(即在远程 KEK 轮换期间)。 插件不能重新使用 `key_id`,即使在先前使用的远程 KEK 被恢复的情况下也是如此。 例如,如果插件使用了 `key_id=A`,切换到 `key_id=B`,然后又回到 `key_id=A`, 那么插件应报告 `key_id=A_001` 或使用一个新值,如 `key_id=C`。 由于 API 服务器大约每分钟轮询一次 `Status`,因此 `key_id` 轮换并不立即发生。 此外,API 服务器在三分钟内以最近一个有效状态为准。因此, 如果用户想采取被动方法进行存储迁移(即等待),则必须安排迁移在远程 KEK 轮换后的 `3 + N + M` 分钟内发生 (其中 `N` 表示插件观察 `key_id` 更改所需的时间,`M` 是允许处理配置更改的缓冲区时间 - 建议至少使用 5 分钟)。 请注意,执行 KEK 轮换不需要进行 API 服务器重启。 {{< caution >}} 因为你未控制使用 DEK 执行的写入次数,所以 Kubernetes 项目建议至少每 90 天轮换一次 KEK。 {{< /caution >}} * 协议:UNIX 域套接字 (`unix`) 该插件被实现为一个在 UNIX 域套接字上侦听的 gRPC 服务器。 该插件部署时应在文件系统上创建一个文件来运行 gRPC UNIX 域套接字连接。 API 服务器(gRPC 客户端)配置了 KMS 驱动(gRPC 服务器)UNIX 域套接字端点,以便与其通信。 通过以 `/@` 开头的端点,可以使用一个抽象的 Linux 套接字,即 `unix:///@foo`。 使用这种类型的套接字时必须小心,因为它们没有 ACL 的概念(与传统的基于文件的套接字不同)。 然而,这些套接字遵从 Linux 网络命名空间,因此只能由同一 Pod 中的容器进行访问,除非使用了主机网络。 ### 将 KMS 插件与远程 KMS 整合 {#integrating-a-kms-plugin-with-the-remote-kms} KMS 插件可以用任何受 KMS 支持的协议与远程 KMS 通信。 所有的配置数据,包括 KMS 插件用于与远程 KMS 通信的认证凭据,都由 KMS 插件独立地存储和管理。 KMS 插件可以用额外的元数据对密文进行编码,这些元数据是在把它发往 KMS 进行解密之前可能要用到的 (KMS v2 提供了专用的 `annotations` 字段简化了这个过程)。 ### 部署 KMS 插件 {#deploying-the-kms-plugin} 确保 KMS 插件与 Kubernetes API 服务器运行在同一主机上。 ## 使用 KMS 驱动加密数据 {#encrypting-your-data-with-the-kms-provider} 为了加密数据: 1. 使用适合于 `kms` 驱动的属性创建一个新的 `EncryptionConfiguration` 文件,以加密 Secret 和 ConfigMap 等资源。 如果要加密使用 CustomResourceDefinition 定义的扩展 API,你的集群必须运行 Kubernetes v1.26 或更高版本。 2. 设置 kube-apiserver 的 `--encryption-provider-config` 参数指向配置文件的位置。 3. `--encryption-provider-config-automatic-reload` 布尔参数决定了磁盘内容发生变化时是否应自动 [重新加载](/zh-cn/docs/tasks/administer-cluster/encrypt-data/#configure-automatic-reloading) 通过 `--encryption-provider-config` 设置的文件。这样可以在不重启 API 服务器的情况下进行密钥轮换。 4. 重启你的 API 服务器。 ### KMS v1 {#encrypting-your-data-with-the-kms-provider-kms-v1} ```yaml apiVersion: apiserver.config.k8s.io/v1 kind: EncryptionConfiguration resources: - resources: - secrets - configmaps - pandas.awesome.bears.example providers: - kms: name: myKmsPluginFoo endpoint: unix:///tmp/socketfile-foo.sock cachesize: 100 timeout: 3s - kms: name: myKmsPluginBar endpoint: unix:///tmp/socketfile-bar.sock cachesize: 100 timeout: 3s ``` ### KMS v2 {#encrypting-your-data-with-the-kms-provider-kms-v2} ```yaml apiVersion: apiserver.config.k8s.io/v1 kind: EncryptionConfiguration resources: - resources: - secrets - configmaps - pandas.awesome.bears.example providers: - kms: apiVersion: v2 name: myKmsPluginFoo endpoint: unix:///tmp/socketfile-foo.sock timeout: 3s - kms: apiVersion: v2 name: myKmsPluginBar endpoint: unix:///tmp/socketfile-bar.sock timeout: 3s ``` `--encryption-provider-config-automatic-reload` 设置为 `true` 会将所有健康检查集中到同一个健康检查端点。 只有 KMS v1 驱动正使用且加密配置未被自动重新加载时,才能进行独立的健康检查。 下表总结了每个 KMS 版本的健康检查端点: | KMS 配置 | 没有自动重新加载 | 有自动重新加载 | | ------------ | ----------------------- | ------------------ | | 仅 KMS v1 | Individual Healthchecks | Single Healthcheck | | 仅 KMS v2 | Single Healthcheck | Single Healthcheck | | KMS v1 和 v2 | Individual Healthchecks | Single Healthcheck | | 没有 KMS | 无 | Single Healthcheck | `Single Healthcheck` 意味着唯一的健康检查端点是 `/healthz/kms-providers`。 `Individual Healthchecks` 意味着每个 KMS 插件都有一个对应的健康检查端点, 并且这一端点基于插件在加密配置中的位置确定,例如 `/healthz/kms-provider-0`、`/healthz/kms-provider-1` 等。 这些健康检查端点路径是由服务器硬编码、生成并控制的。 `Individual Healthchecks` 的索引序号对应于 KMS 加密配置被处理的顺序。 在执行[确保所有 Secret 都加密](#ensuring-all-secrets-are-encrypted)中所给步骤之前, `providers` 列表应以 `identity: {}` 提供程序作为结尾,以允许读取未加密的数据。 加密所有资源后,应移除 `identity` 提供程序,以防止 API 服务器接受未加密的数据。 有关 `EncryptionConfiguration` 格式的更多详细信息,请参阅 [kube-apiserver 加密 API 参考(v1)](/zh-cn/docs/reference/config-api/apiserver-config.v1/)。 ## 验证数据已经加密 {#verifying-that-the-data-is-encrypted} 当静态加密被正确配置时,资源将在写入时被加密。 重启 `kube-apiserver` 后,所有新建或更新的 Secret 或在 `EncryptionConfiguration` 中配置的其他资源类型在存储时应该已被加密。 要验证这点,你可以用 `etcdctl` 命令行程序获取私密数据的内容。 1. 在默认的命名空间里创建一个名为 `secret1` 的 Secret: ```shell kubectl create secret generic secret1 -n default --from-literal=mykey=mydata ``` 2. 用 `etcdctl` 命令行,从 etcd 读取出 Secret: ```shell ETCDCTL_API=3 etcdctl get /kubernetes.io/secrets/default/secret1 [...] | hexdump -C ``` 其中 `[...]` 包含连接 etcd 服务器的额外参数。 3. 验证对于 KMS v1,保存的 Secret 以 `k8s:enc:kms:v1:` 开头, 对于 KMS v2,保存的 Secret 以 `k8s:enc:kms:v2:` 开头,这表明 `kms` 驱动已经对结果数据加密。 4. 验证通过 API 获取的 Secret 已被正确解密: ```shell kubectl describe secret secret1 -n default ``` Secret 应包含 `mykey: mydata`。 ## 确保所有 Secret 都已被加密 {#ensuring-all-secrets-are-encrypted} 当静态加密被正确配置时,资源将在写入时被加密。 这样我们可以执行就地零干预更新来确保数据被加密。 下列命令读取所有 Secret 并更新它们以便应用服务器端加密。如果因为写入冲突导致错误发生, 请重试此命令。对较大的集群,你可能希望根据命名空间或脚本更新去细分 Secret 内容。 ```shell kubectl get secrets --all-namespaces -o json | kubectl replace -f - ``` ## 从本地加密驱动切换到 KMS 驱动 {#switching-from-a-local-encryption-provider-to-the-kms-provider} 为了从本地加密驱动切换到 `kms` 驱动并重新加密所有 Secret 内容: 1. 在配置文件中加入 `kms` 驱动作为第一个条目,如下列样例所示 ```yaml apiVersion: apiserver.config.k8s.io/v1 kind: EncryptionConfiguration resources: - resources: - secrets providers: - kms: apiVersion: v2 name : myKmsPlugin endpoint: unix:///tmp/socketfile.sock - aescbc: keys: - name: key1 secret: ``` 2. 重启所有 `kube-apiserver` 进程。 3. 运行下列命令使用 `kms` 驱动强制重新加密所有 Secret。 ```shell kubectl get secrets --all-namespaces -o json | kubectl replace -f - ``` ## {{% heading "whatsnext" %}} 如果你不想再对 Kubernetes API 中保存的数据加密, 请阅读[解密已静态存储的数据](/zh-cn/docs/tasks/administer-cluster/decrypt-data/)。