Kubernetes 的小秘密——从 Secret 到 Bank Vault
Kubernetes 提供了 Secret 对象用于承载少量的机密/敏感数据,在实际使用中,有几种常规或者非常规的方式能够获取到 Secret 的内容:
Pod 加载(自己的或者不是自己的)Secret 为环境变量或者文件
使用 Kubernetes API(或者
kubectl)获取 Secret 对象内容连接 ETCD 读取其中保存的 Secret 明文
在 CICD 工具中截获含有明文的 Secret 对象 YAML
在加载了 Secret 的容器中直接读取环境变量或者机密文件
上述泄露途径有几个方式可以进行消减:
制定细粒度的 RBAC 策略,防止未授权的 Secret 访问以及 Exec 访问
API Server 使用加密参数(
EncryptionConfiguration),在 ETCD 中存储密文使用 Scratch 等超精简基础镜像,杜绝无用访问
使用策略引擎,防止不当的加载行为
只有特定的 Pod/容器可以加载特定的 Secret
禁止随意加载主机卷,防止 Kubernetes 组件的身份证书被冒用
除了上述的原生方案之外,还有一些补充手段也是有帮助的,例如:
Bitnami 的 Sealed Secret 工具,使用密钥对机密信息进行加密,只有在进入集群之后才会还原为目标 Secret,防止在供应链中泄露信息。
Vault 提供了一个 Sidecar,能把 Vault 中存储的机密信息,直接在 Pod 中生成相应的敏感信息文件
Secrets Store CSI Driver 项目,能从 Vault、Azure 等设施获取信息,注入 Pod 或者生成 Secret。
Bank Vault
Bank Vault 是个 Vault 周边项目,它大大的降低了 Vault 的落地难度,通过 Webhook 注入,Sidecar 等方式,为 Kubernetes 集群中的工作负载提供了方便的 Vault 接入手段。下图表示了它和原生 Vault 的相对优势:

部署
Bank Vault 提供了一个 Operator,能够非常方便的部署 Vault 服务极其相关的 Webhook。所以首先从 Helm 安装 Operator 开始。
$ helm upgrade --install --wait vault-operator \
oci://ghcr.io/bank-vaults/helm-charts/vault-operator
Release "vault-operator" does not exist. Installing it now.
Pulled: ghcr.io/bank-vaults/helm-charts/vault-operator:1.22.1
Digest: sha256:f9d976c39f96942ae52b26a3ab923f173109de64a87c3161fed2470f7bcfa86f
NAME: vault-operator
LAST DEPLOYED: Sat Apr 6 13:54:32 2024
...
接下来使用 Kustomize 生成 Vault 所需的 RBAC 对象:
$ kubectl kustomize https://github.com/bank-vaults/vault-operator/deploy/rbac | kubectl apply -f -
serviceaccount/vault created
role.rbac.authorization.k8s.io/vault created
role.rbac.authorization.k8s.io/leader-election-role created
rolebinding.rbac.authorization.k8s.io/leader-election-rolebinding created
rolebinding.rbac.authorization.k8s.io/vault created
clusterrolebinding.rbac.authorization.k8s.io/vault-auth-delegator created
最后创建 Vault 实例:
$ kubectl apply -f https://raw.githubusercontent.com/bank-vaults/vault-operator/v1.21.0/deploy/examples/cr-raft.yaml
vault.vault.banzaicloud.com/vault created
创建结束后,会出现几个 Pod,分别是 vault-operator、vault-configurer 以及三个有状态 vault 实例。
连接到 Vault
首先是新开一个终端窗口,使用端口转发方式暴露 Vault 服务:
$ kubectl port-forward vault-0 8200 &
...
Forwarding from 127.0.0.1:8200 -> 8200
Forwarding from [::1]:8200 -> 8200
然后是给 Vault 客户端准备接入端点和 CA:
# 端点就是 kubectl 转发的端口
$ export VAULT_ADDR=https://127.0.0.1:8200
# 导出证书,并记录到环境变量里
$ kubectl get secret vault-tls -o jsonpath="{.data.ca\.crt}" | base64 --decode > $PWD/vault-ca.crt
export VAULT_CACERT=$PWD/vault-ca.crt
检查一下 vault 的连接:
$ vault status
Key Value
--- -----
Seal Type shamir
Initialized true
Sealed false
...
用环境变量保存凭据:
export VAULT_TOKEN=$(kubectl get secrets vault-unseal-keys -o jsonpath={.data.vault-root} | base64 --decode)
部署 Webhook
Vault 服务启动并连接之后,就可以开始着手部署功能部分了,前面提到过,Bank Vault 是用 Webhook 实现功能的,所以接下来部署的就是 Webhook 了:
$ kubectl create namespace vault-infra
$ kubectl label namespace vault-infra name=vault-infra
namespace/vault-infra created
namespace/vault-infra labeled
$ helm upgrade --install --wait vault-secrets-webhook \
oci://ghcr.io/bank-vaults/helm-charts/vault-secrets-webhook \
--namespace vault-infra
...
LAST DEPLOYED: Sat Apr 6 14:45:05 2024
NAMESPACE: vault-infra
STATUS: deployed
部署完成之后发现生成了两个 Webhook。查看代码,可以看到:
pods.vault-secrets-webhook会被 Pod 的创建事件触发
跳过
kube-system和刚创建的vault-infra两个命名空间跳过
security.banzaicloud.io/mutate标签为skip的 Pod
secrets.vault-secrets-webhook会被 Secret 的创建和更新事件触发
跳过
kube-system和刚创建的vault-infra两个命名空间跳过
security.banzaicloud.io/mutate标签为skip的 Secret
写入测试数据
向 Vault 写入一个密钥:
vault kv put secret/demosecret/aws AWS_SECRET_ACCESS_KEY=s3cr3t
======= Secret Path =======
secret/data/demosecret/aws
======= Metadata =======
Key Value
--- -----
created_time 2024-04-06T07:12:27.042649134Z
...
用环境变量读取 Vault 内容
创建一个 Pod,看看 Webhook 会对他做什么。
apiVersion: v1
kind: Pod
metadata:
name: vault-test-pod
labels:
app.kubernetes.io/name: vault
annotations:
vault.security.banzaicloud.io/vault-addr: "https://vault:8200"
vault.security.banzaicloud.io/vault-role: "default"
vault.security.banzaicloud.io/vault-skip-verify: "false"
vault.security.banzaicloud.io/vault-tls-secret: "vault-tls"
vault.security.banzaicloud.io/vault-agent: "false"
vault.security.banzaicloud.io/vault-path: "kubernetes"
spec:
serviceAccountName: default
containers:
- name: alpine
image: alpine
command: ["sh", "-c", "echo $AWS_SECRET_ACCESS_KEY && echo going to sleep... && sleep 10000"]
env:
- name: AWS_SECRET_ACCESS_KEY
value: vault:secret/data/demosecret/aws#AWS_SECRET_ACCESS_KEY
创建成功之后,看看 Pod 的日志:
$ kubectl logs -f vault-test-pod
Defaulted container "alpine" out of: alpine, copy-vault-env (init)
...
s3cr3t
going to sleep...
这里输出了我们之前写入 Vault 的密钥值,然而回头看看,我们的 Pod 定义里,并没有引用 Secret,只是定义了一个值为 vault:secret/data/demosecret/aws#AWS_SECRET_ACCESS_KEY 的环境变量,command 节中的命令行直接输出这个环境变量,就能够输出保存在 Vault 中的内容了。但是进入 Pod 的 Shell,会发现环境变量没有变化:
$ kubectl exec -it vault-test-pod -- env | grep -i aws
Defaulted container "alpine" out of: alpine, copy-vault-env (init)
AWS_SECRET_ACCESS_KEY=vault:secret/data/demosecret/aws#AWS_SECRET_ACCESS_KEY
所以 Pod 中被注入了什么呢?
首先是注入了一个初始化容器,在临时卷里面复制了一个 vault-env 命令
用卷加载了 Configmap,其中包含了访问 Vault 所需的 CA
加载了
根据我们前面的注解,生成了一系列的
VAULT*环境变量最重要的,它劫持了原有的启动命令,在前面加入了一个
/vault/vault-env,启动命令就变成了:- args: - sh - -c - echo $AWS_SECRET_ACCESS_KEY && echo going to sleep... && sleep 10000 command: - /vault/vault-env
所以可以推测——/vault/vault-env 充当了 sh 的父进程,在其中根据环境变量 AWS_SECRET_ACCESS_KEY 的值获取了保存在 Vault 中的机密内容。
用机密数据渲染配置文件
看看下面的 Configmap:
apiVersion: v1
kind: ConfigMap
metadata:
labels:
app.kubernetes.io/name: my-app
my-app.kubernetes.io/name: my-app-vault-agent
branches: "true"
name: my-app-vault-agent
data:
config.hcl: |
vault {
// This is needed until https://github.com/hashicorp/vault/issues/7889
// gets fixed, otherwise it is automated by the webhook.
ca_cert = "/vault/tls/ca.crt"
}
auto_auth {
method "kubernetes" {
mount_path = "auth/kubernetes"
config = {
role = "default"
}
}
sink "file" {
config = {
path = "/vault/.vault-token"
}
}
}
template {
contents = <<EOH
{{- with secret "secret/data/demosecret/aws" }}
token: {{ .Data.data.AWS_SECRET_ACCESS_KEY }}
{{ end }}
EOH
destination = "/tmp/config"
// command = "/bin/sh -c \"kill -HUP $(pidof sleep) || true\""
}
上面的配置文件指示了如何对接 Vault,从 secret/data/demosecret/aws 拉取 AWS_SECRET_ACCESS_KEY 中的值,渲染到 template 一节中的模板里面。只要在 Pod 的注解中加入 vault.security.banzaicloud.io/vault-agent-configmap: "my-app-vault-agent"。就可以在这个容器中加入 Sidecar,使用 Sidecar 在 destination 字段指定的配置文件里保存渲染结果。如果 command 有赋值,还可以发出命令,通知业务应用刷新配置。
加入该注解的 Pod 运行后,可以在这个 Pod 的指定文件中看到渲染结果,例如:
$ kubectl get pods | grep vault-agent-pod
vault-agent-pod 2/2 Running 0 9m8s
$ kubectl exec -it vault-agent-pod -- cat /tmp/config
Defaulted container "vault-agent" out of: vault-agent, alpine
token: s3cr3t
后记
Bank Valut 这个项目虽然已经有 2000 Star 了,不过文档还弱的很,甚至 Blog 全挂了也没人理。但是这个思路还是有点意思。虽说有点像屠龙技,不过被安全同学卡脖子的时候,这种使用父进程遮盖环境变量,或者用轮转方式刷新配置文件的玩法,都算是个可行的解法。
