Skip to content
Go back

使用 Docker 部署 Vault 并在 K3s 集群中使用 Vault Agent 自动注入 Secret

| 0 Views Edit page

前言

K3s 里的 crawlab-worker 要连数据库,密码一直是写死在 ConfigMap 里的,上线后改密码就要重新部署,太蠢了。
正好借这个机会把 Vault 搭起来,顺便用 Vault Agent 的 Sidecar 模式把密钥自动注入进去,以后轮换密码只要在 Vault 改一次,应用侧什么都不用动。

Vault 官方推荐用 Helm 安装 Agent Injector,但它本质上是一个 MutatingWebhookConfiguration + 一套 Helm Chart,在国内网络环境下拉取镜像和 Chart 都很麻烦,而且引入了额外的 Webhook 组件。
实际上 Vault Agent 就是 hashicorp/vault 镜像里的一个子命令,完全可以直接在 Deployment 里写一个 Sidecar 容器,效果和 Injector 完全一样,还更透明。

本文操作环境:两台虚拟机,<Vault机器IP> 运行 Docker(Vault),<K3s机器IP> 运行 K3s,请自行替换。


方案概述

  1. 使用 Docker 部署 Vault(文件存储持久化)
    1. 准备目录与配置文件
    2. 编写 docker-compose.yml
    3. 初始化与解封
  2. 配置 Vault
    1. 启用 KV v2 密钥引擎
    2. 写入 crawlab-worker 的密钥
    3. 创建访问策略
  3. 在 K3s 中配置 Kubernetes Auth
    1. 创建 ServiceAccount 与 RBAC
    2. 在 Vault 中启用并配置 Kubernetes Auth
    3. 创建 Vault Role 绑定策略
  4. 手动注入 Vault Agent Sidecar(不用 Helm)
    1. 创建 ConfigMap(Vault Agent 配置文件)
    2. 修改 crawlab-worker Deployment
    3. 验证注入效果
    4. 添加新的密钥并验证
    5. 删除密钥并验证

操作步骤

一、使用 Docker 部署 Vault

1、准备目录与配置文件

mkdir -vp /opt/vault/{config,data,logs}
cd /opt/vault

# hashicorp/vault 镜像以 vault 用户(UID 100)运行,需要赋予 data 目录写权限
sudo chown -R 100:1000 /opt/vault/data

nano config/vault.hcl

/opt/vault/config/vault.hcl 中写入以下内容:

storage "file" {
  path = "/vault/data"
}

listener "tcp" {
  address     = "0.0.0.0:8200"
  tls_disable = true
}

api_addr = "http://0.0.0.0:8200"
ui       = true
  • storage "file" 使用本地文件系统持久化,数据写入挂载卷,容器重启后不丢失
  • tls_disable = true 内网使用,生产环境建议配置证书
  • ui = true 开启 Web UI,可通过 http://<Vault机器IP>:8200/ui 访问

2、编写 docker-compose.yml 并启动

/opt/vault/ 目录下创建 docker-compose.yml

services:
  vault:
    image: hashicorp/vault:1.21
    container_name: vault
    restart: unless-stopped
    ports:
      - "8200:8200"
    volumes:
      - ./config:/vault/config
      - ./data:/vault/data
      - ./logs:/vault/logs
    environment:
      VAULT_ADDR: "http://0.0.0.0:8200"
    cap_add:
      - IPC_LOCK
    command: server

IPC_LOCK 是防止 Vault 内存中的密钥被操作系统换出到磁盘。

docker compose up -d
docker logs vault

3、初始化与解封

Vault 首次启动后处于密封(Sealed)状态,必须先初始化再解封:

# 初始化,生成 5 个 Key Share,解封需要其中任意 3 个
docker exec -it vault vault operator init \
  -key-shares=5 \
  -key-threshold=3

输出示例:

Unseal Key 1: xxxx...
Unseal Key 2: xxxx...
Unseal Key 3: xxxx...
Unseal Key 4: xxxx...
Unseal Key 5: xxxx...

Initial Root Token: hvs.xxxx...

Unseal Key 和 Root Token 只显示一次,立即保存好。

# 解封,需要执行 3 次,每次提供不同的 Key
docker exec -it vault vault operator unseal <Unseal Key 1>
docker exec -it vault vault operator unseal <Unseal Key 2>
docker exec -it vault vault operator unseal <Unseal Key 3>

Sealed 变为 false 说明解封成功。

注意:容器重启后 Vault 会重新进入密封状态,需要再次执行上面 3 条 unseal 命令。如果不想每次重启都手动操作,可以后续配置 Auto Unseal(接入云 KMS)。

二、配置 Vault

Vault 跑在 Docker 容器里,所有操作都通过 docker exec 在容器内执行,不需要在宿主机上额外安装 vault CLI。
为了少打字,先进入容器并设好环境变量:

docker exec -it vault sh
export VAULT_ADDR="http://127.0.0.1:8200"
export VAULT_TOKEN="<Initial Root Token>"

后面的 vault 命令都在这个容器 shell 里执行。如果中途退出了容器,重新 docker exec -it vault sh 进来再 export 一次即可。

1、启用 KV v2 密钥引擎

vault secrets enable -path=secret kv-v2

2、写入测试密钥

先写一条测试用的密钥,整个流程跑通后再换成真实业务数据:

vault kv put secret/crawlab/worker \
  test_message="hello-from-vault"

# 验证写入
vault kv get secret/crawlab/worker

3、创建访问策略

策略内容比较短,直接用 heredoc 在容器内写入临时文件再应用:

cat > /tmp/crawlab-worker-policy.hcl <<'EOF'
path "secret/data/crawlab/worker" {
  capabilities = ["read"]
}
EOF

vault policy write crawlab-worker /tmp/crawlab-worker-policy.hcl

KV v2 的实际数据路径是 secret/data/<path>,不是 secret/<path>,这里要注意。

操作完成后可以 exit 退出容器 shell。

三、在 K3s 中配置 Kubernetes Auth

Vault Kubernetes Auth 的工作原理:Pod 用自身的 ServiceAccount Token 向 Vault 登录,Vault 收到请求后调用 K3s 的 TokenReview API 验证 Token 是否合法,验证通过后下发一个有时效的 Vault Token。

因此需要在 K3s 里准备两类 ServiceAccount:

  • vault-auth:专供 Vault 调用 K3s TokenReview API 时使用(相当于 Vault 的”验证通道”)
  • crawlab-worker:Pod 实际使用的身份,用来向 Vault 登录

1、创建 ServiceAccount 与 RBAC

新建 vault-auth-sa.yaml

---
apiVersion: v1
kind: Namespace
metadata:
  name: vault
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: vault-auth
  namespace: vault
---
# K3s 1.24+ 不再自动创建长期 Token,需显式声明
apiVersion: v1
kind: Secret
metadata:
  name: vault-auth-token
  namespace: vault
  annotations:
    kubernetes.io/service-account.name: vault-auth
type: kubernetes.io/service-account-token
---
# 授予 vault-auth 调用 TokenReview API 的权限
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: vault-auth-tokenreview
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: system:auth-delegator
subjects:
  - kind: ServiceAccount
    name: vault-auth
    namespace: vault
---
# crawlab-worker Pod 使用的 ServiceAccount,命名空间和 Deployment 保持一致
apiVersion: v1
kind: ServiceAccount
metadata:
  name: crawlab-worker
  namespace: crawlab
kubectl apply -f vault-auth-sa.yaml

2、在 Vault 中启用并配置 Kubernetes Auth

两台机器都要用到,分开操作:先在 K3s 机器上取出集群信息,再到 Vault 机器上通过 docker exec 写入配置。

在 K3s 机器上执行,取出两个值并打印备用:

# vault-auth 的长期 JWT Token
kubectl get secret vault-auth-token -n vault \
  -o jsonpath='{.data.token}' | base64 -d
echo ""

# K3s 集群的 CA 证书,保存为文件(证书内容有换行,不能直接粘贴进命令)
kubectl config view \
  --raw --minify --flatten \
  -o jsonpath='{.clusters[].cluster.certificate-authority-data}' | base64 -d > /tmp/k8s-ca.crt

把 JWT 输出复制好,CA 证书通过 scp 传到 Vault 机器:

scp /tmp/k8s-ca.crt <Vault机器IP>:/tmp/k8s-ca.crt

切换到 Vault 机器,先把 CA 证书文件复制进容器,再进入容器写入配置:

docker cp /tmp/k8s-ca.crt vault:/tmp/k8s-ca.crt

docker exec -it vault sh
export VAULT_ADDR="http://127.0.0.1:8200"
export VAULT_TOKEN="<Initial Root Token>"

vault auth enable kubernetes

vault write auth/kubernetes/config \
  token_reviewer_jwt="<粘贴JWT Token>" \
  kubernetes_host="https://<K3s机器IP>:6443" \
  kubernetes_ca_cert=@/tmp/k8s-ca.crt

kubernetes_host 必须填写 K3s 安装时使用的 IP 地址(即 K3s Agent 加入集群时 --server 参数里的地址),因为 K3s 的 TLS 证书 SANs 里只包含了这个 IP。如果填写 Tailscale 域名等其他地址,TLS 验证会失败,Vault 会将其统一报告为 403 permission denied。@/tmp/k8s-ca.crt 是 vault CLI 的文件读取语法,会自动读取文件内容传入。

# 验证配置
vault read auth/kubernetes/config

3、创建 Vault Role 绑定策略

继续在 Vault 机器的容器 shell 里执行(沿用上面的 session):

vault write auth/kubernetes/role/crawlab-worker \
  bound_service_account_names=crawlab-worker \
  bound_service_account_namespaces=crawlab \
  policies=crawlab-worker \
  ttl=1h

crawlab-worker 这个 ServiceAccount 的 Pod 登录 Vault 后,就会拿到绑定了 crawlab-worker 策略的 Token,有效期 1 小时,Vault Agent 会在过期前自动续期。

四、手动注入 Vault Agent Sidecar

1、创建 ConfigMap(Vault Agent 配置文件)

我们准备两份配置,分别挂给不同的容器:

  • vault-agent-init.hcl:给 Init 容器用,认证成功并渲染密钥文件后立即退出,确保主容器启动前密钥已就绪
  • vault-agent-sidecar.hcl:给 Sidecar 容器用,持续运行,监听密钥变化并自动续期

新建 vault-agent-configmap.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: vault-agent-config
  namespace: crawlab
data:
  vault-agent-init.hcl: |
    exit_after_auth = true
    pid_file        = "/tmp/.vault-agent.pid"

    vault {
      address = "http://<Vault机器IP>:8200"
    }

    auto_auth {
      method "kubernetes" {
        mount_path = "auth/kubernetes"
        config = {
          role = "crawlab-worker"
        }
      }
      sink "file" {
        config = {
          path = "/home/vault/.vault-token"
        }
      }
    }

    template_config {
      static_secret_render_interval = "1m"
    }

    template {
      destination = "/vault/secrets/config.env"
      contents = <<EOT
    {{- with secret "secret/data/crawlab/worker" -}}
    {{- range $k, $v := .Data.data }}
    {{ $k }}={{ $v }}
    {{- end }}
    {{- end }}
    EOT
    }

  vault-agent-sidecar.hcl: |
    exit_after_auth = false
    pid_file        = "/tmp/.vault-agent.pid"

    vault {
      address = "http://<Vault机器IP>:8200"
    }

    auto_auth {
      method "kubernetes" {
        mount_path = "auth/kubernetes"
        config = {
          role = "crawlab-worker"
        }
      }
      sink "file" {
        config = {
          path = "/home/vault/.vault-token"
        }
      }
    }

    template_config {
      static_secret_render_interval = "1m"
    }

    template {
      destination = "/vault/secrets/config.env"
      contents = <<EOT
    {{- with secret "secret/data/crawlab/worker" -}}
    {{- range $k, $v := .Data.data }}
    {{ $k }}={{ $v }}
    {{- end }}
    {{- end }}
    EOT
    }
kubectl apply -f vault-agent-configmap.yaml

2、修改 crawlab-worker Deployment

在原有 Deployment 基础上新增 serviceAccountNamevolumesinitContainers,以及给 worker 容器加 volumeMounts,再追加 vault-agent sidecar 容器:

---
apiVersion: v1
kind: Namespace
metadata:
  name: crawlab

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: crawlab-worker
  namespace: crawlab
spec:
  replicas: 2
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: crawlab-worker
  template:
    metadata:
      labels:
        app: crawlab-worker
    spec:
      serviceAccountName: crawlab-worker  # 新增
      imagePullSecrets:
        - name: harbor-auth

      hostNetwork: true
      dnsPolicy: ClusterFirstWithHostNet

      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: node-role.kubernetes.io/control-plane
                    operator: DoesNotExist
                  - key: node-role.kubernetes.io/master
                    operator: DoesNotExist
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchLabels:
                  app: crawlab-worker
              topologyKey: "kubernetes.io/hostname"

      volumes:                             # 新增
        - name: vault-secrets
          emptyDir: {}
        - name: vault-agent-config
          configMap:
            name: vault-agent-config
        - name: vault-token
          emptyDir: {}

      initContainers:                      # 新增
        - name: vault-agent-init
          image: hashicorp/vault:1.21
          args:
            - agent
            - -config=/vault/config/vault-agent-init.hcl
          volumeMounts:
            - name: vault-agent-config
              mountPath: /vault/config
            - name: vault-secrets
              mountPath: /vault/secrets
            - name: vault-token
              mountPath: /home/vault

      containers:
        - name: worker
          image: <Harbor地址>/crawlab-custom:latest
          imagePullPolicy: Always
          env:
            - name: CRAWLAB_NODE_NAME
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
            - name: CRAWLAB_NODE_MASTER
              value: 'N'
            - name: CRAWLAB_GRPC_ADDRESS
              value: '<Master节点地址>:9666'
            - name: CRAWLAB_FS_FILER_URL
              value: 'http://<Master节点地址>:8080/api/filer'
            - name: CRAWLAB_TASK_HANDLER_MAXRUNNERS
              value: '12'
            - name: CONSUL_HOST
              value: '<Master节点地址>'
            - name: CONSUL_PORT
              value: '8500'
          volumeMounts:                    # 新增
            - name: vault-secrets
              mountPath: /vault/secrets

        # 新增:Vault Agent Sidecar,持续运行负责密钥续期
        - name: vault-agent
          image: hashicorp/vault:1.21
          args:
            - agent
            - -config=/vault/config/vault-agent-sidecar.hcl
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              cpu: 100m
              memory: 128Mi
          volumeMounts:
            - name: vault-agent-config
              mountPath: /vault/config
            - name: vault-secrets
              mountPath: /vault/secrets
            - name: vault-token
              mountPath: /home/vault

      restartPolicy: Always
kubectl apply -f crawlab-worker-deployment.yaml

3、验证注入效果

# READY 列应为 2/2(worker + vault-agent),Init 容器完成后消失
kubectl get pod -n crawlab -l app=crawlab-worker
# 查看 Pod 列表并获取 Pod 名称
kubectl get pod -n crawlab -l app=crawlab-worker

# 查看 Init 容器日志,确认认证和模板渲染成功
kubectl logs -n crawlab <pod-name> -c vault-agent-init

正常的话可以看到:

[INFO]  auth.handler: authenticating
[INFO]  auth.handler: authentication successful, sending token to sinks
[INFO]  template.server: template rendered successfully
[INFO]  agent: exit_after_auth set, exiting

Pod 正常运行后,在 Crawlab 中新建一个爬虫,写一段读取注入文件并打印的脚本:

with open('/vault/secrets/config.env') as f:
    print(f.read())

运行该爬虫,在日志中看到以下输出说明密钥注入成功:

test_message=hello-from-vault

密钥打印成功

主容器里的应用直接读 /vault/secrets/config.env 这个文件就行,完全不需要知道 Vault 的存在。
后续如果要换成真实业务密钥,只需要 vault kv patch 更新对应字段,Sidecar 会在 1 分钟内自动重新渲染文件。

4、添加新的密钥并验证

ConfigMap 里的模板已使用 range 动态遍历所有字段,后续无论增删多少字段,只需在 Vault 侧操作,不需要改 ConfigMap:

# vault kv patch 只追加/更新指定字段,已有字段不受影响
vault kv patch secret/crawlab/worker \
  another_key="another-value"

# 确认两个字段都存在
vault kv get secret/crawlab/worker

等待约 1 分钟后,再次执行爬虫:
新的密钥打印成功

5、删除密钥并验证

删除某个字段

KV v2 把一个路径下的所有字段当作一个版本化的整体文档来存,没有”只删文档内某个字段”的原子操作。
想删字段,只能重写整个文档(只写保留的字段,不写的自然就没了):

# 只保留 test_message,不写 another_key 即为将其删除
vault kv put secret/crawlab/worker \
  test_message="hello-from-vault"

# 确认只剩一个字段
vault kv get secret/crawlab/worker

只保留 test_message

删除整条密钥路径

KV v2 区分软删除(可恢复)和永久删除:

# 软删除:标记最新版本为已删除,可通过 vault kv undelete 恢复
vault kv delete secret/crawlab/worker

# 永久删除全部版本和元数据(不可恢复)
vault kv metadata delete secret/crawlab/worker

软删除后,Vault Agent 下一次轮询时读到已删除版本,with secret 条件不成立,模板渲染为空文件:

kubectl exec -n crawlab <pod-name> -c worker -- cat /vault/secrets/config.env
# (空输出)

生产环境删除密钥前,务必确认应用侧已做好降级处理,否则主容器读取到空配置文件会引发异常。

结束。


Edit page