# 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 的相对优势：

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1745944444098/03c829ce-48e8-4ad7-9850-af05a4f887d4.png align="center")

### 部署

Bank Vault 提供了一个 Operator，能够非常方便的部署 Vault 服务极其相关的 Webhook。所以首先从 Helm 安装 Operator 开始。

```bash
$ 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 对象：

```bash
$ 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 实例：

```bash
$ 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 服务：

```bash
$ kubectl port-forward vault-0 8200 &
...
Forwarding from 127.0.0.1:8200 -> 8200
Forwarding from [::1]:8200 -> 8200
```

然后是给 Vault 客户端准备接入端点和 CA：

```bash
# 端点就是 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 的连接：

```bash
$ vault status
Key                     Value
---                     -----
Seal Type               shamir
Initialized             true
Sealed                  false
...
```

用环境变量保存凭据：

```bash
export VAULT_TOKEN=$(kubectl get secrets vault-unseal-keys -o jsonpath={.data.vault-root} | base64 --decode)
```

### 部署 Webhook

Vault 服务启动并连接之后，就可以开始着手部署功能部分了，前面提到过，Bank Vault 是用 Webhook 实现功能的，所以接下来部署的就是 Webhook 了：

```bash
$ 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 写入一个密钥：

```bash
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 会对他做什么。

```yaml
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 的日志：

```bash
$ 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，会发现环境变量没有变化：

```bash
$ 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`，启动命令就变成了：
    
    ```yaml
      - 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`：

```yaml
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 的指定文件中看到渲染结果，例如：

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