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 全挂了也没人理。但是这个思路还是有点意思。虽说有点像屠龙技,不过被安全同学卡脖子的时候,这种使用父进程遮盖环境变量,或者用轮转方式刷新配置文件的玩法,都算是个可行的解法。