跨 Kubernetes 集群对接 Vault by Hashicorp
这篇文中,我将简单介绍如何在 Kubernetes 集群中通过 Bitnami 社区提供的 Helm Chart 搭建一个 Vault by Hashicorp 服务,并在另外一个 Kubernetes 集群中连接、读取它上面的密钥。
在理解该篇文章之前,你可能需要有对 Kubernetes 以及 Helm 最基本的了解,如果你对这两者不怎么熟悉,可以到 Kubernetes 官方网站 [1],以及 Helm 的官方网站 [2],或者其它 Kubernetes 社区去了解更多的内容。在这篇文章中,我基于 Kubernetes >= 1.24
以及 Helm >= 3.8
进行展开说明。
Vault by Hashicorp 简介
Vault by HashiCorp[3] (下文我们简称之为 Vault)是一个专注于安全性的工具,用于存储和管理敏感信息,包括但不限于令牌、密码、证书和加密密钥。它通过多种方式(UI、CLI 和 HTTP API)提供对这些敏感数据的访问控制,确保数据的安全性和保密性。用户可以通过这些接口来安全地存储、检索和管理他们的秘密数据,同时确保对这些数据的访问是经过严格控制的。
对于 Kubernetes 来说,我们可以配置让它和 Vault 对接,来让特定的服务账号可以访问特定的 KV 密钥,方便不同团队有各自密钥的修改、查看权限,可以有效避免运维人员 “一家独大” 或者说只有他一个人可以修改配置而什么事都让他来做的情况。
快速搭建 Vault
我们可以用 Bitnami 社区提供的 vault 应用 [4] 直接部署一个可以让我们快速上手的 Vault 服务,为了方便配置,我以 UI 配置的方式来介绍 Vault 服务内部所有的配置流程。
准备配置清单
首先,我们可以查看 Chart 的 values 模板,这里提供一个相对比较精简的配置清单,为了方便这里我用了 local path provisioner[5] 作为存储类 [6],你可以根据自己集群提供的不同的存储类修改 global.storageClass
配置。把如下文件存在 vault.yaml
中:
1 | global: |
要注意的有几点:
- 你需要根据你当前获取到的
bitnami/vault
应用配置中的镜像标签,来修改server.image.tag
配置,一般来说我们是建议锁定版本的,以防出现服务不兼容的情况。 - 我在配置中保留了日志等级的配置,如果有必要,可以取消注释。
- 在正式使用的时候,
server.ingress.tls
是需要配置成true
的,而目前 Bitnami 的 Chart 配置不支持自定义 TLS 密钥的名称,需要用域名加-tls
后缀来让生成的 ingress 支持 HTTPS,比如说你的域名是vault.example.com
,那么需要创建一个 TLS 证书 [7][8],名称为vault.example.com-tls
。 - 在这篇文章中,我们以跨集群为例子,所以我在这边把
injector
禁用了,另外目前 Bitnami 提供的 Injector 的服务配置有问题,只支持在同一个命名空间下执行相关的容器,用官方的 Injector 可以解决。
部署及初始化
接下来,我们通过如下命令部署 Vault:
1 | kubectl create ns vault |
等待服务启动的过程中,可以通过 kubectl -n vault get pods
查看运行状态,当服务启动完之后它应该是一个 “未就绪” 的状态:
1 | NAME READY STATUS RESTARTS AGE |
接下来,我们需要初始化我们的仓库:
1 | kubectl -n vault exec -it vault-server-0 -- \ |
这个命令会输出 5 个 unseal keys,以及一个 initial root token,请妥善保管。当初始化完之后,我们需要用这些 keys 来做 unseal 操作:
1 | kubectl -n vault exec -it vault-server-0 -- \ |
根据你初始化时传入的 -key-threshold
参数,你可能需要多次执行该命令,每次拿不同的 key 进行 unseal,每次运行成功之后,会输出进度。当 Vault 初始化成功之后,你可以通过 kubectl -n vault get pods
看到它已经正常运行了:
1 | NAME READY STATUS RESTARTS AGE |
题外话:保障自己的服务安全
一般来说 Vault 服务是只需要对特定的用户以及服务开放的,所以你可以配置 Ingress Annotations 来让它只对特定的 IP 开放,比如说以 nginx 为例:
1 | server: |
在以上的配置例子中,我们限制了 Vault 服务器可访问的客户端 IP 地址的 CIDR[9]。
创建我们的密钥以及策略
在 Vault 服务首页的左边侧边栏中,点击 Secrets Engines 我们可以创建一个新的密钥引擎,比如说我们这里创建一个名为 kv
的密钥引擎,它的类型是 KV:
进入这个密钥引擎,我们可以添加自己想要的密钥内容,并自定义内容对应的路径,其中路径是用于策略管控的基本单位。
接下来,打开左边侧边栏的 Policies,创建一个名为 demo-policy
的 ACL 策略,内容如下:
1 | path "kv/data/demo/*" { |
在该策略中,我们允许绑定该策略的角色访问 kv
密钥引擎下的 demo
子目录。现在你可以尝试在 kv
中添加一些密钥了,密钥的路径可以填写比如 demo/secret
这样的值,使得该策略生效。
将我们的工作集群接入 Vault
基于 Kubernetes 的认证流程原理
假设如上图所示,我们的工作 Kubernetes 集群名称为 worker-cluster
,整条链路的简单说明如下:
- 首先,当工作集群创建了一个新的 Pod 的时候,运行在该工作集群上的 Vault Agent Injector[10] 会被创建出来,目前支持 sidecar 模式或者 init container 一次性注入的模式。
- Vault Agent Injector 拿着自己服务账号的临时凭证,请求 Vault 服务,进行鉴权。
- Vault 服务收到认证请求的时候,反向向工作集群的 API 服务请求 Token Review API[11],来确认服务账号凭证的合法性。
准备服务账号
正如我们在上个章节中所说, Vault 服务是需要一个拥有 Token Review API 权限的服务账号来进行 API 调用,所以我们需要准备如下的服务账号以及对应的 RBAC 配置,我们将它存储在 service-account.yaml
中:
1 | apiVersion: v1 |
接下来,我们用如下命令让它生效,并抽取出对应的凭证:
1 | kubectl apply -f service-account.yaml |
生成出来的 token.txt
和 ca.crt
就是这个服务账号的凭证以及 CA 证书了。
配置工作 Kubernetes 集群的认证方式
切换回 Vault 服务视角,我们已经把服务起起来了,你可以通过 https://vault.example.com 来访问你的 Vault UI,接下来我们用 initial root token 登录,在左侧边栏中选择 Access、Authentication Methods,在右边点击 Enable new method 创建一个新的认证方式。
在创建的时候,选择 Kubernetes,path 可以选择一个非默认的名称,比如说 kubernetes/worker-cluster
:
接下来进入配置页,填入工作集群 API 服务的地址,以及我们刚才在工作集群中生成的服务账号的 CA 证书、token:
注意
由于我们的 Vault 服务并不是搭在工作集群中,也就是说他们用的不是同一个集群的 API 服务,我们需要勾上 “Disable use of local CA and service account JWT” 选项,Vault 服务用自己的服务账号来访问我们的工作集群。
接着我们可以为它配置我们要用的服务的角色,切换到 Roles 标签,点击 Create role 创建一个新的角色:
在上述的配置中,有如下几个配置项需要考虑:
- Name:角色的名称,用于 Pod 访问密钥数据时的主体指明。
- Bound service account name:绑定的服务账号,这里指的是工作集群目标 Pod 用到的服务账号。
- Bound service account namespaces:绑定的命名空间,同时这里指的也是目标 Pod 所在的命名空间。
- Do Not Attach ‘default’ Policy To Generated Tokens:一般来说,不是特别建议自动加载默认的策略给服务。
- Generated Token’s Policies:该角色绑定的策略,在这里我们用之前创建的
demo-policy
策略。 - Generated Token’s Type:如果是服务在用,填
service
,如果是类似 Job 这样的自动化在用,可以用batch
。
部署 Vault Agent Injector
接下来,我们需要在工作集群中部署 Vault Agent Injector,把如下的配置存放在 vault-injector.yaml
中:
1 | global: |
如上的配置中,injector.*.tag
也是为了固定镜像版本而存在的,而 injector.authPath
是我们在第一章节中创建的认证方式的认证路径,并加上 auth/
前缀。
然后,我们可以用官方的 Helm Chart[12] 来部署 Injector:
1 | helm repo add hashicorp https://helm.releases.hashicorp.com |
通过 kubectl -n vault get pods
确认 Injector 已经正常启动:
1 | NAME READY STATUS RESTARTS AGE |
关于 Bitnami 的 Injector
Bitnami 的 Vault 应用也有 Injector 的实现,但是它不支持类似 externalVaultAddr
的配置,所以这里我直接以官方的为例子。
尝试获取我们的密钥
接下来我们可以编写一个简单的 Deployment 来难我们的服务可以拿到 Vault 中的密钥:
1 | apiVersion: apps/v1 |
在上述配置清单中,做一下额外的简单说明:
- 需要保证使用的 serviceAccountName 以及 namespace 和角色配置一致。
vault.hashicorp.com/role
应该和角色名一致。vault.hashicorp.com/agent-pre-populate
和vault.hashicorp.com/agent-pre-populate-only
两个注解可以保证使用 init container 而不是 sidecar 的形式进行注入,这样服务运行中,Injector 就不会占用额外的资源了。vault.hashicorp.com/agent-inject-template-mixed.yaml
注解是一个简单的模板,把所有的密钥用 YAMl 文件的形式体现,并合并。具体和模板相关的配置,可以参考官方的文档 [13],以及 Consul Templating Language 的官方文档 [14]。
参考资料
- Vault configuration
- Essential Vault 中文手册
- The Vault Secrets Operator on Kubernetes
- Apply a template to the injected secrets
- Vault Injector Annotations
- Integrate a Kubernetes cluster with an external Vault
- Manage Kubernetes Secrets using Vault
https://github.com/bitnami/charts/tree/main/bitnami/vault ↩︎
https://kubernetes.io/zh-cn/docs/concepts/storage/storage-classes/ ↩︎
https://kubernetes.io/docs/tasks/tls/managing-tls-in-a-cluster/ ↩︎
https://kubernetes.io/docs/concepts/configuration/secret/#tls-secrets ↩︎
https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing ↩︎
https://developer.hashicorp.com/vault/docs/platform/k8s/injector ↩︎
https://kubernetes.io/zh-cn/docs/reference/kubernetes-api/authentication-resources/token-review-v1/ ↩︎
https://developer.hashicorp.com/vault/docs/agent-and-proxy/agent/template ↩︎
https://github.com/hashicorp/consul-template/blob/v0.28.1/docs/templating-language.md ↩︎