目前,我们的 ConfigMap
中存储了大量的 token 数据。每个服务挂载一个 ConfigMap
,而 ConfigMap
中包含多个配置文件 (conf
),每个配置文件中又包含多个 token,有的甚至包含几十个 token。
在此之前,我测试了从 Deployment
资源获取 Vault 的 secret 的方式,发现这种方法存在明显的弊端:
- 服务重启问题:增删操作会导致服务重启。
- 冗余的 Vault 容器:每个服务Sidecar 一个 Vault 容器,看起来非常冗余。
- 读取失败影响服务:在测试过程中,如果存在 secret 读取失败(例如权限错误或 key 不存在),init 容器会挂起,从而影响整个服务。
为了将 Vault 投入线上环境,需要使用动态配置,即从代码中使用 JWT 去请求 secret。这个步骤我已基本实现,如果要上线的话还需要很多地方需要完善,secret的增删改查、维护、vault policy的修改、vault role的修改、平滑从configmap迁移到vault,这里面需要大量的人为工作,手工或自动化。这不包括vault与开发代码的兼容性工作,我们需要和开发去沟通vault的secret存储设计(策略 权限)和名称等细节。
一、单点vault部署
1.1、创建vault所需pvc(可选)
部署vault
cat >vault-pvc.yaml<<EOF
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: vault-storage
namespace: vault
spec:
accessModes:
- ReadWriteMany
storageClassName: dev-eks-efs
resources:
requests:
storage: 5Gi
EOF
1.2、创建vault helm vaules文件
cat >custom-vaules.yaml<<EOF
server:
dataStorage:
enabled: true
size: 5Gi
storageClass: dev-eks-efs
accessMode: ReadWriteMany
existingClaim: vault-storage
standalone:
enabled: true # 根据你的需求启用单实例模式或禁用以使用 HA
ui:
enabled: true # 启用 Vault UI
EOF
1.3、安装vault
helm repo add hashicorp https://helm.releases.hashicorp.com
helm upgrade --install vault hashicorp/vault --namespace vault --values custom-values.yaml
1.4、查看容器日志
kubectl logs -f -n vault vault-0
这里会看到容器是等待初始化的一个状态
1.5、初始化vault
vault命令都是进入容器以后执行的
kubectl exec -it vault-0 -n vault sh
vault operator init
我们需要将5个key中的3个key解禁,保存好这里的Root Token!
Unseal Key 1: xx
Unseal Key 2:xx
Unseal Key 3: xx
Unseal Key 4: xx
Unseal Key 5: xx
Initial Root Token: xx
1.6、解禁key
vault operator unseal <Unseal_Key_1>
vault operator unseal <Unseal_Key_2>
vault operator unseal <Unseal_Key_3>
1.7、使用root token登录vault
这里更换成自己的token,上面的初始化提示中会输出
vault login $root_token
1.8、测试kv2功能
登录成功后这一步肯定成功
vault secrets enable -path=internal kv-v2
准备工作已经完成了
二、配置vault secret
2.1、启用一个服务用的path
我使用k8s的namesapce来命名主path
vault secrets enable -path=chat-test kv-v2
创建完成后查看一下
vault secrets list
2.2、批量创建一些vault secret
容器的解释器是sh,需要替换你自己的root_token,另外容器要编辑文件需要去/tmp目录
最后的路径表示服务名称,需要注意的kv2会自动在主路径后面添加一个data路径,API查询的路径是chat-test/data/backend/chat-ai
,授权和代码也同样。
# 创建key
#!/bin/sh
# 定义 Vault 存储路径
vault_path="chat-test/backend/chat-ai"
# 登录 Vault(确保已解封并可以访问)
vault login $root_token # 将这里替换为你的实际 Vault Token
if [ $? -ne 0 ]; then
echo "登录失败,请检查 Token 或 Vault 服务器状态。"
exit 1
fi
echo "登录成功"
# 定义要存储的 token 列表
tokens="gemini-pro_1:AIzaSyAX
gemini-pro_2:AzayAXx43h6wk
gemini-pro_3:AzaSyAtfb1giUt
gemini-pro_4:AIaSy9oro3Z5K
gemini-pro_5:AzaSyDm_dPznU
han10:89443b5EbF4F5773dC
han4:5E6A7aC79336F183"
# 将 token 存储到 Vault
echo "$tokens" | while IFS=: read -r key token; do
vault kv put "$vault_path/$key" token="$token"
if [ $? -ne 0 ]; then
echo "Failed to store token for $key in Vault."
else
echo "Stored token for $key in Vault."
fi
done
创建完成后可以list一下
vault kv list chat-test/backend/chat-ai
2.3、创建vault服务策略
kv2会自动将数据路径主path后面添加一个data,将元数据路径的主path后面添加一个metadata,代码也一样。只是我们vault客户端命令在查询时不需要手工添加data和metadata,还是按照我们创建的chat-test/backend/chat-ai/
操作即可
cat >chat-ai.hcl<<EOF
# 对数据路径的权限
path "chat-test/data/backend/chat-ai/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
# 对元数据路径的权限
path "chat-test/metadata/backend/chat-ai/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
EOF
# 创建chat-api-policy策略
vault policy write chat-ai-policy chat-ai.hcl
2.4、vault启用k8s auth
vault auth enable kubernetes
2.5、vault启用对k8s的token验证
# 变量都是容器固定位置
vault write auth/kubernetes/config \
token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443" \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
2.6、检查当前配置
# 确认有chat-test path
vault secrets list
# 确认有chat-ai的policy
vault policy list
# 确认有k8s auth
vault auth list
# list key,之前提到过,命令查询不需要增加data path
vault kv list chat-test/backend/chat-ai
2.7、创建k8s认证角色(Role)
names是k8s服务用的serviceaccount,一定要填写服务所在的namespace,最后关联之前创建的策略
vault write auth/kubernetes/role/chat-ai-role \
bound_service_account_names=vault-chat-ai \
bound_service_account_namespaces=chat-test \
policies=chat-ai-policy
查看role和读取role,确认符合预期
vault list auth/kubernetes/role
vault read auth/kubernetes/role/chat-ai-role
三、k8s应用使用接入vault
3.1、创建ServiceAccount
cat <<EOF|kubectl create -f -
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault-chat-ai # ServiceAccount应用中要配置
namespace: chat-test
EOF
3.2、静态配置
annotions中添加如下配置,agent-inject: "true"
允许vault agent注入(vault-agent容器替我们去和vault-server交互),vault.hashicorp.com/role: chat-ai-role
使用我们2.7
中创建好的Role,通过Role去vault中获取相关权限,另外三个配置是secret的静态配置
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/agent-inject-secret-gemini-pro-1: chat-test/data/backend/chat-ai/gemini-pro_1
vault.hashicorp.com/agent-inject-secret-gemini-pro-2: chat-test/data/backend/chat-ai/gemini-pro_2
vault.hashicorp.com/agent-inject-secret-han10: chat-test/data/backend/chat-ai/han10
vault.hashicorp.com/role: chat-ai-role
# 和containers同级
serviceAccountName: vault-chat-ai # 使用指定的 ServiceAccount
我遇到过的报错基本都是403,ServiceAccount没有创建,valult policy path错误,Role绑定policy错误。如果这些没有问题,静态配置读取应该是正常的,可以从容器中获取到secret
/app # ls /vault/secrets/
gemini-pro-1 gemini-pro-2 han10 han4
静态配置一笔带过,静态配置的弊端较多,不适合业务广泛使用
3.3、动态配置
回顾一下vault secret配置的流程,vault中创建策略后使用一个Role去绑定,配置在指定ns下,Deployment通过annotions启用了vault agent的注入,指定role的名称和ServiceAccount,这两个操作是让agent可以通过容器ServiceAccount和role获取到正确的权限。权限的获取是通过读取k8s的ServiceAccount Token(JWT),代码中的Client初始化操作同样是指定JWT位置和Vault Role
前提是应用先配置好正确的ServiceAccount,这个环节是程序获取JWT的关键步骤
~# kubectl logs -f -n chat-test sijia-test-6776985f9b-47s64
2024/09/04 01:14:31 密钥:chat-test/data/backend/chat-ai/gemini-pro_1, 值:map[token:yAXbBljmJjvR0]
package main
import (
"fmt"
"log"
"net/http"
"os"
vaultApi "github.com/hashicorp/vault/api"
)
var (
vaultHost = "vault.vault.svc:8200" // Vault 服务器地址
vaultRole = "chat-ai-role" // 程序需要 vault role + jwt 登录
vaultSecretsPath = "chat-test/data/backend/chat-ai/gemini-pro_1" // v2 kv会自动添加data路径,命令查询和curl不需要加data,但是api需要
vaultJWTPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" // JWT 的路径
)
func main() {
config := vaultApi.DefaultConfig()
config.Address = fmt.Sprintf("http://%s", vaultHost)
client, err := vaultApi.NewClient(config)
if err != nil {
log.Fatalf("创建 Vault 客户端错误:%s", err)
}
jwt, err := os.ReadFile(vaultJWTPath)
if err != nil {
log.Fatalf("读取 JWT 错误:%s", err)
}
options := map[string]interface{}{
"jwt": string(jwt),
"role": vaultRole,
}
loginSecret, err := client.Logical().Write("auth/kubernetes/login", options)
if err != nil {
log.Fatalf("登录 Vault 错误:%s", err)
}
client.SetToken(loginSecret.Auth.ClientToken)
// 打印特定密钥的值
printKey(client)
log.Fatal(http.ListenAndServe(":8080", nil))
}
// 修改此函数以打印特定密钥
func printKey(client *vaultApi.Client) {
secret, err := client.Logical().Read(vaultSecretsPath)
if err != nil {
log.Printf("读取密钥错误:%s", err)
return
}
log.Printf("密钥:%s, 值:%v\n", vaultSecretsPath, secret.Data["data"])
}
我尝试获取chat-test/data/backend/chat-ai/
下的所有key时出现了错误,暂时无法解决