跨 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
global:
storageClass: local-path

server:
image:
tag: 1.16.2-debian-12-r3
# extraEnvVars:
# - name: VAULT_LOG_LEVEL
# value: trace
ingress:
enabled: true
hostname: vault.example.com
ingressClassName: nginx
tls: false

resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 1
memory: 1Gi

injector:
enabled: false

要注意的有几点:

  • 你需要根据你当前获取到的 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
2
3
4
kubectl create ns vault
helm upgrade --install -n vault \
vault oci://registry-1.docker.io/bitnamicharts/vault \
-f vault.yaml

等待服务启动的过程中,可以通过 kubectl -n vault get pods 查看运行状态,当服务启动完之后它应该是一个 “未就绪” 的状态:

1
2
NAME             READY   STATUS    RESTARTS   AGE
vault-server-0 0/1 Running 0 2m

接下来,我们需要初始化我们的仓库:

1
2
kubectl -n vault exec -it vault-server-0 -- \
vault operator init -key-shares=5 -key-threshold=3

这个命令会输出 5 个 unseal keys,以及一个 initial root token,请妥善保管。当初始化完之后,我们需要用这些 keys 来做 unseal 操作:

1
2
kubectl -n vault exec -it vault-server-0 -- \
vault operator unseal

根据你初始化时传入的 -key-threshold 参数,你可能需要多次执行该命令,每次拿不同的 key 进行 unseal,每次运行成功之后,会输出进度。当 Vault 初始化成功之后,你可以通过 kubectl -n vault get pods 看到它已经正常运行了:

1
2
NAME             READY   STATUS    RESTARTS   AGE
vault-server-0 1/1 Running 0 5m

题外话:保障自己的服务安全

一般来说 Vault 服务是只需要对特定的用户以及服务开放的,所以你可以配置 Ingress Annotations 来让它只对特定的 IP 开放,比如说以 nginx 为例:

1
2
3
4
5
6
7
8
 server:
# ...
ingress:
enabled: true
+ annotations:
+ nginx.ingress.kubernetes.io/whitelist-source-range: 192.168.14.64/32,192.168.14.69/32
hostname: vault.example.com
# ...

在以上的配置例子中,我们限制了 Vault 服务器可访问的客户端 IP 地址的 CIDR[9]

创建我们的密钥以及策略

在 Vault 服务首页的左边侧边栏中,点击 Secrets Engines 我们可以创建一个新的密钥引擎,比如说我们这里创建一个名为 kv 的密钥引擎,它的类型是 KV:

进入这个密钥引擎,我们可以添加自己想要的密钥内容,并自定义内容对应的路径,其中路径是用于策略管控的基本单位。

接下来,打开左边侧边栏的 Policies,创建一个名为 demo-policy 的 ACL 策略,内容如下:

1
2
3
4
5
6
7
path "kv/data/demo/*" {
capabilities = ["read", "list"]
}

path "kv/metadata/demo/*" {
capabilities = ["list"]
}

在该策略中,我们允许绑定该策略的角色访问 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault-token-reviewer
namespace: vault
---
apiVersion: v1
kind: Secret
metadata:
name: vault-token-reviewer-token
namespace: vault
annotations:
kubernetes.io/service-account.name: vault-token-reviewer
type: kubernetes.io/service-account-token
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: vault-token-reviewers
subjects:
- kind: ServiceAccount
name: vault-token-reviewer
namespace: vault
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator

接下来,我们用如下命令让它生效,并抽取出对应的凭证:

1
2
3
4
5
kubectl apply -f service-account.yaml
kubectl -n vault get secrets vault-token-reviewer-token \
-o jsonpath='{.data.token}' | base64 -d >token.txt
kubectl -n vault get secrets vault-token-reviewer-token \
-o jsonpath='{.data.ca\.crt}' | base64 -d >ca.crt

生成出来的 token.txtca.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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
global:
enabled: false
externalVaultAddr: https://vault.example.com

injector:
enabled: true
image:
tag: 1.4.1
agentImage:
tag: 1.16.1
agentDefaults:
cpuLimit: 500m
cpuRequest: 5m
memLimit: 128Mi
memRequest: 64Mi
authPath: auth/kubernetes/woker-cluster

如上的配置中,injector.*.tag 也是为了固定镜像版本而存在的,而 injector.authPath 是我们在第一章节中创建的认证方式的认证路径,并加上 auth/ 前缀。

然后,我们可以用官方的 Helm Chart[12] 来部署 Injector:

1
2
3
4
helm repo add hashicorp https://helm.releases.hashicorp.com
helm upgrade --install -n vault \
vault hashicorp/vault \
-f vault-injector.yaml

通过 kubectl -n vault get pods 确认 Injector 已经正常启动:

1
2
NAME                                   READY   STATUS    RESTARTS   AGE
vault-agent-injector-75fc4687c-qdxdj 1/1 Running 0 3m

关于 Bitnami 的 Injector

Bitnami 的 Vault 应用也有 Injector 的实现,但是它不支持类似 externalVaultAddr 的配置,所以这里我直接以官方的为例子。

尝试获取我们的密钥

接下来我们可以编写一个简单的 Deployment 来难我们的服务可以拿到 Vault 中的密钥:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
apiVersion: apps/v1
kind: Deployment
metadata:
name: vault-demo
namespace: default
labels: &labels
app.kubernetes.io/instance: vault-demo
spec:
selector:
matchLabels: *labels
template:
metadata:
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/agent-pre-populate: "true"
vault.hashicorp.com/agent-pre-populate-only: "true"
vault.hashicorp.com/agent-inject-template-mixed.yaml: |
{{- range secrets "kv/demo" }}

# {{ . }}
{{- with secret (printf "kv/demo/%s" .) }}
---
{{ .Data.data | toYAML }}
{{- end }}
{{- end }}
vault.hashicorp.com/role: demo
labels: *labels
spec:
serviceAccountName: default
containers:
- image: busybox:1.28
name: demo
command:
- sh
- -c
args:
- |
set -euo pipefail
cat /vault/secrets/mixed.yaml
trap : TERM INT
tail -f /dev/null & wait
resources:
limits:
cpu: 10m
memory: 16Mi
requests:
cpu: 10m
memory: 16Mi

在上述配置清单中,做一下额外的简单说明:

  • 需要保证使用的 serviceAccountName 以及 namespace 和角色配置一致。
  • vault.hashicorp.com/role 应该和角色名一致。
  • vault.hashicorp.com/agent-pre-populatevault.hashicorp.com/agent-pre-populate-only 两个注解可以保证使用 init container 而不是 sidecar 的形式进行注入,这样服务运行中,Injector 就不会占用额外的资源了。
  • vault.hashicorp.com/agent-inject-template-mixed.yaml 注解是一个简单的模板,把所有的密钥用 YAMl 文件的形式体现,并合并。具体和模板相关的配置,可以参考官方的文档 [13],以及 Consul Templating Language 的官方文档 [14]

参考资料


  1. https://kubernetes.io/ ↩︎

  2. https://helm.sh/ ↩︎

  3. https://www.vaultproject.io/ ↩︎

  4. https://github.com/bitnami/charts/tree/main/bitnami/vault ↩︎

  5. https://github.com/rancher/local-path-provisioner ↩︎

  6. https://kubernetes.io/zh-cn/docs/concepts/storage/storage-classes/ ↩︎

  7. https://kubernetes.io/docs/tasks/tls/managing-tls-in-a-cluster/ ↩︎

  8. https://kubernetes.io/docs/concepts/configuration/secret/#tls-secrets ↩︎

  9. https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing ↩︎

  10. https://developer.hashicorp.com/vault/docs/platform/k8s/injector ↩︎

  11. https://kubernetes.io/zh-cn/docs/reference/kubernetes-api/authentication-resources/token-review-v1/ ↩︎

  12. https://github.com/hashicorp/vault-helm ↩︎

  13. https://developer.hashicorp.com/vault/docs/agent-and-proxy/agent/template ↩︎

  14. https://github.com/hashicorp/consul-template/blob/v0.28.1/docs/templating-language.md ↩︎