HashiCorp Vault 密钥管理实战:从单机部署到应用动态取密

配置写进代码仓库、密码贴在 Wiki、数据库账号全公司共用一个——这些是很多团队的现状。一旦有人离职或者仓库泄露,改密码要牵动所有服务。Vault 解决的就是这个问题:密钥集中保管、按身份授权、动态生成、到期自动回收。本文以单机部署为例,把从安装到应用取密的完整链路走一遍,适用于需要把静态密码收敛到统一密钥平台的中小团队。环境口径:Rocky Linux 9,Vault 1.17.x,存储后端用内置 Raft 单节点模式。

核心概念先讲清

Vault 的安全模型有三个关键词。第一是 seal(封印):Vault 启动后数据是加密状态,必须 unseal(解封)才能对外服务。解封依赖主密钥,而主密钥被拆成若干份分片(默认 5 份、任意 3 份可解封),这是 Shamir 秘密共享。第二是 Token 与策略(policy):所有访问都要带 Token,Token 绑定策略,策略用路径规则描述"能对哪些路径做哪些操作",没有匹配规则的路径默认拒绝。第三是租约(lease):动态生成的密钥都带租约,到期自动吊销,这是 Vault 区别于普通密码本的核心能力。理解这三点,后面的命令就都是细节了。

前置条件

  • 一台 Linux 服务器,2 核 2G 即可起步,准备好域名 vault.example.com 并解析到本机(生产环境必须启用 HTTPS)。
  • 已准备 TLS 证书(可用内部 CA 签发),放在 /etc/vault.d/tls/ 下。
  • 防火墙放行 8200 端口(API)与 8201 端口(Raft 集群通信,单机可暂不开放)。
  • 一个测试用的 MySQL 实例,用于演示数据库动态密钥,可用 Docker 临时起一个。

第一步:安装与单机部署

通过官方仓库安装,配置文件指定 Raft 存储与 TLS 监听:

dnf config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
dnf install -y vault
mkdir -p /opt/vault/data /etc/vault.d/tls
storage "raft" {
  path    = "/opt/vault/data"
  node_id = "vault-01"
}

listener "tcp" {
  address            = "0.0.0.0:8200"
  tls_cert_file      = "/etc/vault.d/tls/vault.crt"
  tls_key_file       = "/etc/vault.d/tls/vault.key"
}

api_addr     = "https://vault.example.com:8200"
cluster_addr = "https://vault.example.com:8201"
ui           = true
systemctl enable --now vault
export VAULT_ADDR="https://vault.example.com:8200"
vault status

此时 status 应显示 Sealed 为 true、Initialized 为 false,属于未初始化状态,符合预期。

第二步:初始化与解封

vault operator init -key-shares=5 -key-threshold=3 > /root/vault-init.txt
# 用其中任意 3 份分片依次解封
vault operator unseal
vault operator unseal
vault operator unseal
export VAULT_TOKEN="上一步输出的 Initial Root Token"
vault status

解封密钥的保管口径要提前定死:/root/vault-init.txt 里是 5 份分片和 root token,是整个平台的命根子。正确做法是抄录后分由不同人员保管(或存入两家不同的密码管理工具),服务器上的明文文件确认抄录无误后立即删除。root token 只用于初始化配置,日常操作应改用自己身份的 Token,用完 root token 后建议执行 vault token revoke 将其吊销,后续如需再用可凭分片重新生成。

第三步:KV v2 静态密钥的读写

先启用 KV v2 引擎并写入一条静态密钥。KV v2 带版本管理,每次写入生成新版本,可回滚、可查看历史。

vault secrets enable -path=secret kv-v2
vault kv put secret/myapp/db username=app_ro password='请替换为真实密码'
vault kv get secret/myapp/db
vault kv get -version=1 secret/myapp/db

注意 CLI 读写路径会被自动补上 data/ 段,API 直调时路径是 secret/data/myapp/db,写策略时按 API 路径写,不要漏掉。

第四步:用策略做最小授权

给应用单独建策略:只能读自己那一条密钥,其他一律拒绝。

path "secret/data/myapp/*" {
  capabilities = ["read"]
}
vault policy write myapp-ro - <<'EOF'
path "secret/data/myapp/*" {
  capabilities = ["read"]
}
EOF
vault token create -policy=myapp-ro -ttl=24h

策略编写三条经验:一是路径用通配收尾但不跨级放大,secret/data/myapp/* 不要图省事写成 secret/data/*;二是能力只给实际用到的,读密钥给 read 就够,不要顺手带上 create/update;三是写完用 vault token capabilities <token> <path> 反查一遍,确认生效结果与预期一致。

第五步:数据库动态密钥

静态密码再集中,泄露了也要人工改。动态密钥的思路是:应用每次来取,Vault 现场在数据库里建一个临时账号,租约到期自动删。先启用 database 引擎并配置到 MySQL 的连接(这里 Vault 用一个有建号权限的管理账号连接 MySQL):

vault secrets enable database
vault write database/config/my-mysql \
  plugin_name=mysql-database-plugin \
  connection_url="{{username}}:{{password}}@tcp(192.0.2.20:3306)/" \
  allowed_roles="app-role" \
  username="vault_admin" \
  password="请替换为管理账号密码"

vault write database/roles/app-role \
  db_name=my-mysql \
  creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; GRANT SELECT, INSERT, UPDATE ON appdb.* TO '{{name}}'@'%';" \
  default_ttl="1h" \
  max_ttl="24h"

应用取密只需要一行,返回的账号密码只在租约内有效:

vault read database/creds/app-role
# 输出形如 username: v-token-app-role-xxxx  password: xxxxxxxx  lease_id: database/creds/app-role/xxxx

验证方法是拿这个临时账号去登录 MySQL,能连上且权限与 creation_statements 一致;等租约到期(或手动 vault lease revoke <lease_id>)后,该账号在 MySQL 里会被自动删除,再登录应报错。这样单个账号泄露的影响面被压缩到最长 1 小时。

第六步:应用侧怎么接入

两种主流方式。一种是应用直接调 API:应用用自己的 Token(生产环境应换成 AppRole 或 Kubernetes Auth 等机器身份方式,而不是人工发的 Token)定期拉取并在租约到期前续租或换新账号,实现简单,改造点在应用内。另一种是 Vault Agent:在应用同机或同 Pod 部署 Agent,Agent 负责认证、取密、续租,并把密钥渲染成文件或通过 Agent Injector 注入为环境变量,应用无感知。取舍口径:存量应用多、语言杂,优先 Agent 文件模式,应用只读本地文件;新建服务且能配合改造的,可以直接调 API 并处理好续租逻辑。无论哪种,都要给取密失败留降级路径:本地缓存上一份可用密钥并告警,避免 Vault 短暂不可用拖垮全部应用。

第七步:开启审计日志

密钥平台必须回答"谁在什么时候读了什么"。开启 file 审计设备,所有请求与响应(敏感字段以哈希形式)落盘:

vault audit enable file file_path=/var/log/vault/audit.log
tail -f /var/log/vault/audit.log

审计日志要接入现有日志平台集中留存,并对"短时间内大量读取""非工作时段读取生产密钥"这类模式做告警规则。注意审计文件本身包含请求路径与哈希后的密钥值,权限要收紧。

验证

  • vault status 显示 Sealed=false;重启 vault 服务后重新解封,数据仍在。
  • 用 myapp-ro 策略的 Token 读取 secret/myapp/db 成功,读取 secret/other 应返回 403。
  • 动态账号在 MySQL 中可见(SELECT user, host FROM mysql.user;),手动吊销租约后该账号消失。
  • 审计日志中能检索到上述每一次读取与吊销操作。

常见问题

每次重启都要人工解封太麻烦,能不能自动解封?可以用云 KMS 或 HSM 做 Auto Unseal,把主密钥交给外部服务保管,重启后自动解封。取舍在于:自动解封降低了运维成本,但也等于把"分片在多人手里"这层人为闸门换成了对云账号权限的信任。单机自建场景如果做不到规范的分片保管,至少应把解封动作脚本化到只有运维能触发的跳板机流程,而不是把分片写进启动脚本与 Vault 同机存放——那等于没封印。

应用续租失败导致密钥过期。动态密钥到期是被删除而不是"改密码",续租(renew)只能在租约存续期内做。客户端要监控 lease 的剩余 TTL,提前续租或提前换取新账号并平滑切换连接池;同时在 Vault 侧告警续租失败的请求。把 default_ttl 设得过短(如 5 分钟)会放大这个问题,一般业务取 1 小时起步。

KV 读不到数据,报 raw 相关的错。多半是 KV v1/v2 路径混淆:v2 的 API 路径必须带 data/ 段,元数据操作走 metadata/ 段。用 vault secrets list -detailed 确认引擎版本再对照路径。

root token 丢了怎么办。只要分片还在,可用 vault operator generate-root 流程重新生成;分片也丢了则数据不可恢复——这正是分片要分散保管、且保管人要定期确认可用性的原因。

小结

Vault 落地的顺序建议是:先把静态密码从代码和文档里收进 KV 并配好最小授权策略,再把数据库这类高价值系统改成动态密钥,最后补齐审计与告警。单机部署适合起步与中小规模,数据量上来后再演进为 Raft 三节点集群,配置模型不用推倒重来。

发表回复

后才能评论