Kubernetes 的小秘密——从 Secret 到 Bank Vault

Kubernetes 提供了 Secret 对象用于承载少量的机密/敏感数据,在实际使用中,有几种常规或者非常规的方式能够获取到 Secret 的内容:

  1. Pod 加载(自己的或者不是自己的)Secret 为环境变量或者文件
  2. 使用 Kubernetes API(或者 kubectl)获取 Secret 对象内容
  3. 连接 ETCD 读取其中保存的 Secret 明文
  4. 在 CICD 工具中截获含有明文的 Secret 对象 YAML
  5. 在加载了 Secret 的容器中直接读取环境变量或者机密文件

上述泄露途径有几个方式可以进行消减:

  1. 制定细粒度的 RBAC 策略,防止未授权的 Secret 访问以及 Exec 访问
  2. API Server 使用加密参数(EncryptionConfiguration),在 ETCD 中存储密文
  3. 使用 Scratch 等超精简基础镜像,杜绝无用访问
  4. 使用策略引擎,防止不当的加载行为
    • 只有特定的 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 的相对优势:

comparision

部署

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

Avatar
崔秀龙

简单,是大师的责任;我们凡夫俗子,能做到清楚就很不容易了。

comments powered by Disqus
上一页