Vault:从一次凭据泄漏到可撤销的动态 Secret 基础设施
一次数据库密码泄漏后,团队很快在 Vault 中改掉了对应值,流水线也显示新版本已经发布。事故却没有结束:两个没有重启的实例继续使用旧连接,某台构建机上的长期 token 仍能读取整个业务目录,日志平台保存了一段模板渲染后的明文,离职账号创建的子 token 也没有随账号禁用而失效。大家完成了“改密码”,却没有完成旧身份撤销、消费者收敛、泄漏出口清理和证据核对。
Vault 处理的不是“找个更安全的地方放字符串”这么简单。客户端先通过 auth method 证明身份,Vault 把外部身份映射为 token,policy 再决定这个 token 能对哪些 API path 执行哪些 capability。静态值由 KV 等 secrets engine 保存;数据库、云和 PKI 等 engine 还能按需生成带租约的动态凭据。租约到期、显式 revoke 或父 token 被撤销后,Vault Core 再要求对应 engine 回收外部凭据。真正的工程闭环因此是:
这条链上任何一段缺失,Vault 都可能退化成昂贵的共享密码本:身份仍是长期凭据,policy 仍然过宽,应用只在启动时读取一次,轮换后无人确认,审计设备还可能因为磁盘写满反过来阻断整个服务。
先分清 server、client、storage 与 seal
Vault server 是持有加密状态、执行认证授权、运行 secrets engine 并管理 token 与 lease 的服务进程。CLI、HTTP API、语言 SDK、Vault Agent、Kubernetes 集成和 Terraform provider 都是 client;client 不会因为安装了 vault 可执行文件就自动拥有权限,它最终都要向 server 的 API 发请求。
server 的持久数据交给 storage backend。HashiCorp 当前建议大多数新部署优先使用 Integrated Storage:每个 Raft 节点保存一份复制数据,日志只有获得多数派确认后才提交。storage 中的内容虽然经过 Vault 加密,但存储访问控制仍然重要,因为攻击者可以删除、回滚、复制密文,或者破坏可用性。把底层对象存储或磁盘权限放开,不能用“反正是密文”解释。
seal 解决的是另一个问题。Vault 用 keyring 中的加密密钥保护数据,再用 root key 保护 keyring;seal 决定 server 启动后如何取得解开 root key 所需的材料。默认 Shamir seal 把 unseal key 拆成若干份,达到阈值后才能重构,每个节点重启后都需要解封。auto-unseal 则把解封依赖委托给 KMS、HSM 或受支持的外部 seal 服务,减少人工值守,但也把 Vault 的存活绑定到外部密钥的可用性。
Vault 的 seal 文档明确区分了 unseal key 与 recovery key:启用 auto-unseal 后,recovery key 用于生成 root token、rekey 等需要法定人数授权的操作,不能在 KMS 密钥永久丢失时解开 root key。如果 auto-unseal 的主密钥被删除,即使保留了 Raft 快照也可能无法恢复。KMS 删除保护、跨账号职责分离、密钥恢复窗口和 seal 迁移演练必须和 Vault 备份一起设计。
请求的运行链是:listener 接收 TLS 连接,Vault Core 验证 token 或把登录请求交给 auth plugin,policy 对规范化 path 做授权,secrets engine 处理业务动作,barrier 在写入 storage 前加密,审计设备记录请求与响应证据。由此可以反推故障位置:
connection refused、TLS 握手失败首先属于 listener、地址、证书链或代理问题。Vault is sealed 说明进程可达但 keyring 尚不可用。permission denied 通常表示身份已经到达 Vault,失败发生在 token、policy、namespace 或 path。
no handler for route 多半是 mount path、KV v1/v2 API 路径或 engine 未启用的问题。已拿到 Vault 响应却无法登录数据库,问题可能在动态角色的 creation statement、目标系统权限或租约已经回收。
安装只解决可执行文件,版本与来源才决定可重复性
开发机可以从 Vault 安装页选择发行方软件仓库、Homebrew tap 或经过校验的二进制。Linux 发行仓库安装后通常同时得到 server 与 CLI;Windows 解压后需要把 vault.exe 放进受控工具目录;macOS 可使用:
brew tap hashicorp/tap
brew install hashicorp/tap/vault
vault version
vault -helpLinux 使用发行方软件源时,先验证仓库签名和组织批准的代理,再安装包。下面以 Debian/Ubuntu 形态说明完整动作,CI 不应每次无条件追随最新包:
wget -O - https://apt.releases.hashicorp.com/gpg \
| sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" \
| sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt-get update
sudo apt-get install vault
vault version生产基线记录精确 CLI/server 版本、软件包摘要、镜像 digest、插件版本和支持矩阵。CLI 可以比 server 新或旧,但不能假定所有命令参数和 API 都向前兼容;升级前以目标版本的 upgrade guide 和 release notes 为准。容器镜像必须固定 digest,不能让 latest 在某次重启时静默跨版本。
企业代理环境还要分别验证三条网络:开发机到 Vault API、server 到外部 auth discovery/JWKS、server 到数据库或云 API。CLI 的 HTTPS_PROXY 生效不代表 server 插件也继承;给 CLI 设置 VAULT_CACERT 只解决客户端信任,不会替 server 的 listener 安装完整证书链。长期修复是把企业 CA 放入对应进程的信任库并保持轮换,而不是设置 VAULT_SKIP_VERIFY=true。
用隔离 dev server 建立第一条可撤销链路
vault server -dev 会自动初始化并解封,使用内存 storage,默认没有生产 TLS,停止进程后数据全部丢失。dev server 文档明确要求它只能用于开发和实验。这里把容器端口映射到本机回环地址,并在运行时生成实验 root token;VAULT_DEV_ROOT_TOKEN_ID 会让这个 token 在该次 server 生命周期内保持固定,方便后续命令使用,但每次实验都重新生成,绝不把它固化到仓库。不要把该端口暴露到局域网,也不要把实验 token 贴进聊天。
$LabName = "vault-lab-$([guid]::NewGuid().ToString('N').Substring(0,8))"
$LabRoot = "lab-root-$([guid]::NewGuid().ToString('N'))"
$LabPort = 18200
docker run --detach --rm `
--name $LabName `
--cap-add IPC_LOCK `
--publish "127.0.0.1:${LabPort}:8200" `
--env "VAULT_ADDR=http://127.0.0.1:8200" `
--env "VAULT_DEV_ROOT_TOKEN_ID=$LabRoot" `
hashicorp/vault:<APPROVED_VERSION> server -dev -dev-listen-address="0.0.0.0:8200"
docker exec $LabName vault status
docker exec $LabName vault version<APPROVED_VERSION> 必须替换为组织已批准且固定的镜像版本;镜像拉取后再记录 digest。vault status 应显示 Initialized true、Sealed false 和内存 storage。这个结果只证明实验 server 已启动,不证明 TLS、持久化、备份、HA、审计或 seal 可用。
先创建 KV v2 mount。mount path 是 API 命名空间,不是操作系统目录;同一种 engine 可以在不同 path 启用多次,以隔离团队、生命周期和策略。
docker exec -e VAULT_TOKEN=$LabRoot $LabName vault secrets enable -path=team-kv -version=2 kv
docker exec -e VAULT_TOKEN=$LabRoot $LabName vault secrets list -detailed然后生成一次性合成值,在容器内写入;终端只打印字段名和版本,不打印值:
$SyntheticValue = "generated-$([guid]::NewGuid().ToString('N'))"
docker exec -e VAULT_TOKEN=$LabRoot -e LAB_VALUE=$SyntheticValue $LabName `
sh -lc 'vault kv put -mount=team-kv catalog/local username=demo-user password="$LAB_VALUE"'
docker exec -e VAULT_TOKEN=$LabRoot $LabName `
vault kv metadata get -mount=team-kv catalog/local预期元数据中的 current_version 为 1,但这仍是 root 身份写入。接下来必须建立最小策略和短期工作负载 token,否则实验只证明了超级管理员什么都能做。
policy 对 API path 授权,不对界面菜单授权
KV v2 的 CLI 会帮忙改写 path,但 policy 直接作用于 HTTP API。读取 team-kv/catalog/local 实际访问 team-kv/data/catalog/local;列举目录访问 team-kv/metadata/catalog/*。把 policy 写成 team-kv/catalog/* 会得到 permission denied,因为那个 API path 根本不存在。
$Policy = @'
path "team-kv/data/catalog/local" {
capabilities = ["read"]
}
path "team-kv/metadata/catalog/*" {
capabilities = ["list", "read"]
}
'@
$PolicyPath = Join-Path ([IO.Path]::GetTempPath()) "catalog-reader-$([guid]::NewGuid().ToString('N')).hcl"
[IO.File]::WriteAllText($PolicyPath, $Policy, [Text.Encoding]::ASCII)
try {
docker cp $PolicyPath "${LabName}:/tmp/catalog-reader.hcl"
docker exec $LabName vault policy fmt /tmp/catalog-reader.hcl
docker exec -e VAULT_TOKEN=$LabRoot $LabName `
vault policy write catalog-reader /tmp/catalog-reader.hcl
} finally {
Remove-Item -LiteralPath $PolicyPath -Force -ErrorAction SilentlyContinue
}
$TokenJson = docker exec -e VAULT_TOKEN=$LabRoot $LabName `
vault token create -policy=catalog-reader -ttl=15m -explicit-max-ttl=30m -format=json
$TokenResult = $TokenJson | ConvertFrom-Json
$ReaderToken = $TokenResult.auth.client_token
$ReaderAccessor = $TokenResult.auth.accessor这里不用 Windows PowerShell 5.1 的 Set-Content -Encoding utf8,因为它会写入 BOM,Vault policy 解析器会把 BOM 识别为非法首字符。纯 ASCII HCL 可以像上面一样明确写成 ASCII;包含非 ASCII 注释时应使用无 BOM UTF-8。vault policy fmt 只验证并格式化语法,最终仍要以 vault policy write 的成功响应确认 server 已接受。
正向读取只提取元数据,不把 secret 打到日志:
docker exec -e VAULT_TOKEN=$ReaderToken $LabName `
vault kv get -field=username -mount=team-kv catalog/local预期输出 demo-user。反向实验尝试读取另一个业务 path:
docker exec -e VAULT_TOKEN=$ReaderToken $LabName `
vault kv get -mount=team-kv payments/local预期失败并出现 403 或 permission denied。这份失败证据比“策略看起来很小”更有价值:它证明当前 token 在当前 mount 上确实不能跨业务读取。还要注意 capability 不是简单文件权限:create、update、patch、delete、list、read、sudo 对不同 endpoint 含义不同;deny 优先于其他匹配,通配符匹配规则也不能靠直觉推断。上线前用 vault token capabilities <path> 配合真实负例验证。
token 创建时固化 token policy 的名称集合,后续不能把新 policy 直接附加到既有 token;修改同名 policy 的内容则会在下一次请求时生效。工作负载换权限应重新登录获取新 token。service token 有存储记录、父子关系和可续租 lease;batch token 更轻量,没有相同的持久层能力和续租语义,适合某些高吞吐、短任务场景。选择 token type 前先确认目标 auth method、续租、审计、revoke 和企业复制行为。
root token 拥有不受普通 policy 限制的最高权限。初始化后应先建立管理员 auth method 与受限 policy,再撤销初始 root token。紧急情况下可由 unseal/recovery key 持有人达到阈值后执行 operator generate-root,完成审批动作后立即撤销;root token 不是放在密码库里“以防万一”的日常管理员账号。
KV v2 的版本、CAS、软删与销毁必须分开治理
静态 secret 也有并发覆盖问题。两个流水线都读取版本 1,各自写入新值;没有 CAS 时最后到达者会静默覆盖先到者。KV v2 支持 check-and-set,把“我基于哪个版本更新”写进请求条件。
先由 root 写入版本 2,再故意用旧版本号更新:
$NextValue = "generated-$([guid]::NewGuid().ToString('N'))"
docker exec -e VAULT_TOKEN=$LabRoot -e LAB_VALUE=$NextValue $LabName `
sh -lc 'vault kv put -mount=team-kv -cas=1 catalog/local username=demo-user password="$LAB_VALUE"'
$ConflictValue = "generated-$([guid]::NewGuid().ToString('N'))"
docker exec -e VAULT_TOKEN=$LabRoot -e LAB_VALUE=$ConflictValue $LabName `
sh -lc 'vault kv put -mount=team-kv -cas=1 catalog/local username=demo-user password="$LAB_VALUE"'第一次命令在当前版本为 1 时成功并生成版本 2;第二次应报告 check-and-set 不匹配。若两个命令都成功,说明 mount 配置、参数或实验前状态不符合预期。生产写入者应先读取 metadata,提交时携带已观察版本,并把冲突交给重试或人工合并,不能无条件覆盖。
CAS 默认不是强制门禁:写入者省略 cas 时,KV v2 仍可能接受一次无条件覆盖。cas=0 只允许 key 尚不存在时创建,cas=N 则要求当前版本严格等于 N;两者都不是“自动重试”。需要把并发保护变成不变量时,可以在 mount 配置上启用 cas_required=true,也可以用 secret metadata 只约束高风险 key。启用后,put 与 patch 等写入口都必须携带 CAS,旧版脚本会立即失败,因此应先盘点所有写入者、灰度更新,再打开门禁,并持续保留一个陈旧版本写入的反向实验。CAS 只防止基于旧版本的覆盖,不会合并两个配置对象,也不能替代业务层审批和外部凭据轮换。
KV v2 的删除动作有三层语义:
vault kv delete 软删最新版本,数据不可读但可以 undelete。vault kv destroy -versions=N 永久销毁指定版本数据,只留下 destroyed 元数据。vault kv metadata delete 删除整条 key 的所有版本与 metadata,是最强破坏动作。
因此“删除权限”不能只写一个 delete。data/、delete/、undelete/、destroy/ 和 metadata/ 是不同 endpoint,应分别授予应用、轮换控制器、恢复人员和数据销毁角色。软删适合误操作恢复窗口,destroy 适合经过审批的不可恢复清除;版本上限和 delete_version_after 影响存储增长与恢复窗口,不能无限保留。
轮换不是向 KV 再写一版就结束。消费者要能观察版本变化,加载新值,建立新连接,关闭旧连接,再由控制器撤销旧凭据。验收信号至少包括:新实例只使用新版本、旧版本读取被拒绝或旧外部账号已禁用、失败率没有因连接切换持续升高、审计日志能定位轮换运行。只看到 current_version 增加,不能证明旧 secret 已退出系统。
dynamic secret 把生成与撤销延伸到外部系统
database、AWS、Azure、Google Cloud、PKI 等 secrets engine 不只是保存管理员交给 Vault 的值。它们用受控的高权限连接配置和 role 模板,在请求到来时创建临时账号、云凭据或证书,并返回 lease_id、lease_duration 和 renewable。调用者不再共享一个永不过期账号,Vault 也有机会在 TTL 到期或显式 revoke 时调用外部系统删除凭据。
数据库 engine 的对象链是:plugin 配置连接与管理身份,role 定义允许创建什么账号、执行哪些 SQL、默认 TTL 和最大 TTL,database/creds/<role> 生成一对动态凭据。合成结构可以写成:
: "${VAULT_DB_ADMIN_USER:?set a synthetic admin user for the isolated database}"
: "${VAULT_DB_ADMIN_PASSWORD:?generate a temporary admin password at runtime}"
vault write database/config/catalog-db \
plugin_name="postgresql-database-plugin" \
allowed_roles="catalog-read" \
connection_url="postgresql://{{username}}:{{password}}@db.example.invalid:5432/catalog" \
username="$VAULT_DB_ADMIN_USER" \
password="$VAULT_DB_ADMIN_PASSWORD"
vault write database/roles/catalog-read \
db_name="catalog-db" \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA app TO \"{{name}}\";" \
revocation_statements='DROP ROLE IF EXISTS "{{name}}";' \
default_ttl="15m" \
max_ttl="1h"保留域名和环境变量只是结构模板,不能直接执行。真正上线前要在隔离数据库验证创建、续租、到期、显式撤销和异常清理。allowed_roles 限制该 connection 可被哪些 role 使用;default_ttl 决定正常更换节奏;max_ttl 是不可再续的硬上限。TTL 太短会在 Vault 或数据库短暂不可用时造成连接风暴,太长又扩大泄漏窗口。连接池还可能在 lease 已撤销后保留旧物理连接,因此应用必须在 credential 变化时重建池,并用旧账号登录失败作为撤销证据。
云 secrets engine 的共同模型相同,但外部对象和撤销能力并不等价。AWS engine 可以按 role 签发 iam_user、assumed_role、federation_token 或 session_token;优先选择 STS 短期会话,减少创建与删除长期 access key 的压力,并用 permissions boundary 和限定资源 ARN 约束 Vault 的管理身份。Google Cloud engine 的 roleset 可以产生 OAuth access token 或 service account key:access token 短期且没有同等提前撤销语义,service account key 可随 lease 删除但受每个账号的 key 配额影响。Azure engine 可以创建动态 service principal 或轮换既有 principal 的凭据,角色 assignment 的传播延迟、应用对象软删和租户配额都会影响回收证据。
因此“云凭据已 revoke”必须落到提供商侧验证:AWS STS 会话是否只能等过期、IAM access key 是否已删除,GCP service account key 是否从 key 列表消失,Azure service principal 或 password credential 是否已删除且角色 assignment 不再生效。Vault 的管理身份本身也应优先使用实例身份或 workload identity,而不是在 aws/config/root、GCP credentials 或 Azure config 中再放一个多年不变的根 secret;只有插件、版本和许可明确支持的能力才能作为基线。
外部回收并不保证永远成功。Vault 收到 revoke 后,数据库不可达、SQL 权限不足或云 API 限流都可能让 lease 进入撤销失败状态。监控不能只看“生成成功数”,还要看 lease 数量趋势、renew/revoke 错误、外部孤儿账号、凭据年龄和插件延迟。定期从目标系统反查由 Vault 命名规则生成的账号,并和有效 lease 对账,才能发现控制面认为已删除、数据面仍然存在的孤儿。
lease、renew 与 revoke 决定凭据真正活多久
lease 文档指出,每个动态 secret 和 service token 都会关联租约元数据。lease_duration 是当前有效期,不等于永远可续;renew 受 engine、role、token 和系统级最大 TTL 共同限制。应用必须在到期前续租或获取替代凭据,并为 Vault 暂时不可用保留有限的失败预算。
常见错误是把续租线程当成“保活开关”。如果进程暂停、网络隔离或父 token 到期,续租会失败;无限重试只会让应用在凭据已经失效后继续报错。更稳健的状态机是:
ISSUED -> RENEWING -> ROTATING -> RELOADED
| | |
| +-> DEGRADED +-> ROLLBACK_OR_STOP
+-> REVOKED / EXPIREDDEGRADED 必须有截止点。到达“剩余 TTL 小于完成一次替代凭据获取、连接验证和流量切换所需预算”时,应用应停止接收新流量或切换到受控降级,不能等到数据库突然拒绝所有请求。轮换完成后还要显式 revoke 旧 lease;否则旧凭据会一直有效到 TTL,泄漏窗口没有缩短。
token 的父子关系也影响撤销。普通子 token 会在父 token 撤销时连同其 lease 级联回收;通过外部 auth method 登录通常得到 orphan token,不依赖某个操作员 token。revoke-orphan 会撤销当前 token 却把直接子 token 变成孤儿,属于特殊事故操作,不应作为常规离职回收。团队要按 entity、token accessor、auth mount 和 lease prefix 建立撤销剧本,而不是只删除登录账号。
回到 dev 实验,先让工作负载 token 查询并续租自己的 lease。renew-self 只能把当前 TTL 延长到创建时的 explicit_max_ttl 等上限以内,不能把短期 token 续成永久 token:
docker exec -e VAULT_TOKEN=$ReaderToken $LabName `
vault token lookup -format=json
docker exec -e VAULT_TOKEN=$ReaderToken $LabName `
vault token renew -increment=5m预期 lookup 的 renewable 为 true,renew 返回新的当前 TTL;它可能小于请求的 5m 增量,因为 token 创建时间、当前 TTL、显式最大 TTL 和系统上限会共同裁剪结果。随后用 accessor 撤销 token,避免把 token ID 作为待撤销参数再暴露到命令行:
docker exec -e VAULT_TOKEN=$LabRoot $LabName `
vault token revoke -accessor $ReaderAccessor
docker exec -e VAULT_TOKEN=$ReaderToken $LabName `
vault kv get -field=username -mount=team-kv catalog/local第二条命令应返回 permission denied。这证明 Vault token 已失效,但不证明先前复制到文件、环境变量或应用内存中的静态 secret 消失。对 KV 值还要轮换实际外部凭据;对 dynamic secret 则确认对应 lease 的外部账号无法再使用。
把这些命令放进自动化时,应在每个反例后立即保存 $LASTEXITCODE 并断言非零,不能依赖 PowerShell 对原生命令失败的默认处理。错误文本会随 CLI 版本变化;稳定不变量是跨 path 访问、陈旧 CAS 和撤销后读取均失败,而且目标 secret 值没有进入日志。
response wrapping 让交付凭据变成一次性可检查动作
部署系统有时需要把 AppRole SecretID 或一次性引导材料交给新实例。直接把值放进 CI 输出、用户数据或消息队列,会让所有中继都看到明文。response wrapping让 Vault 把原响应放进 wrapping token 的 cubbyhole,调用者只传递短 TTL、单次使用的引用。
vault write -wrap-ttl=5m -f auth/approle/role/catalog/secret-id
vault unwrap <WRAPPING_TOKEN_FROM_SECURE_CHANNEL>接收端先用 vault unwrap -lookup 检查 creation_path 与 TTL,再执行一次 unwrap。若 token 已被使用、来源 path 不符或超时,应立即视为截获或交付故障,而不是重新发送同一份凭据。wrapping token 不是签名离线包,接收端仍必须连接预期 Vault server 验证;TLS、DNS 和服务发现被劫持时,单靠 wrapping 不能建立信任。
policy 可以设置 min_wrapping_ttl 和 max_wrapping_ttl,防止调用者绕过组织要求。包装只缩短交付暴露,不会改变被包装 secret 自身的 TTL,也不会自动清除接收端写入磁盘的副本。
人、机器与 Kubernetes 应使用不同认证路径
auth method 的任务是验证外部身份并生成 Vault token。OIDC 常用于人类交互登录,JWT 常用于 CI 或工作负载携带的签名断言,AppRole 面向没有原生平台身份的机器,Kubernetes auth 验证 Pod 的 service account token。token auth 本身始终存在,但把长期 token 直接发给应用会绕过身份生命周期。
AppRole 由 RoleID 和默认需要保密的 SecretID 组成。RoleID 用于选择 role,不应单独被当作密码;SecretID 可以限制 TTL、使用次数和来源 CIDR。更安全的 pull 模式让受信编排器生成短期、单次 SecretID,并用 response wrapping 交付。AppRole role 的 token_policies、token_ttl、token_max_ttl、secret_id_ttl、secret_id_num_uses 和 CIDR 约束共同决定 blast radius。Vault Agent auto-auth 不适合有限 token_num_uses 的 token,因为它无法可靠追踪剩余次数。
JWT auth 先验证签名,再校验 iss、aud、sub 和 bound claims。只配置 JWKS URL 而不绑定 audience、repository、branch、environment 或 workload identity,可能让同一 issuer 下的其他项目换到同样权限。OIDC role 还需要严格限制 allowed_redirect_uris;本机 callback 和生产 UI callback 不应混用。CI 应优先用平台 OIDC 换短期 Vault token,避免仓库级长期 SecretID。
Kubernetes auth 把 service account 身份映射到 Vault role。至少绑定 bound_service_account_names 和 bound_service_account_namespaces,不要给默认 service account 广泛授权。Kubernetes RBAC 只决定 Pod 能否使用 service account 和访问 Kubernetes 对象,Vault policy 才决定能读哪个 Vault path;二者不能互相替代。多集群应明确每个 auth mount 的 reviewer/JWT 信任和 issuer,防止不同集群中同名 namespace/service account 被错误视为同一身份。
auth mount 本身也是隔离对象。开发、CI、不同集群或不同 issuer 可以挂载到不同 path,分别配置 discovery、CA、role 和审计标签。把所有 JWT 来源塞进一个 mount,会受制于单一验证配置,并让 role 误绑定更难发现。
Agent、Template、CSI 与 Operator 改变 secret 的落点
应用直接使用 SDK 能看见 token、lease 和更新事件,但要自行实现认证、续租、退避、重载与撤销。Vault Agent 把这些职责移到伴随进程:auto-auth 获取并续租 token,template 把值渲染到文件,cache 管理部分 token 与 leased secret,process supervisor 还能把环境模板交给子进程。下面的结构使用 AppRole 文件输入和内存目录输出:
vault {
address = "https://vault.example.invalid:8200"
ca_cert = "/etc/vault/ca.pem"
}
auto_auth {
method "approle" {
mount_path = "auth/approle"
config = {
role_id_file_path = "/run/bootstrap/role-id"
secret_id_file_path = "/run/bootstrap/secret-id"
remove_secret_id_file_after_reading = true
}
}
}
template_config {
exit_on_retry_failure = true
static_secret_render_interval = "5m"
}
template {
source = "/etc/vault/templates/catalog.ctmpl"
destination = "/run/catalog/credentials.json"
perms = "0640"
error_on_missing_key = true
command = "/usr/local/bin/reload-catalog-credentials"
}destination 应在 tmpfs 或受限运行时目录,owner 和 mode 与应用身份匹配;command 要幂等、超时并留下不含 secret 的结果证据。模板成功改写文件不表示应用已经重载,重载脚本需要执行一次新连接验证,再原子切换。KV v2 没有 lease,Agent template 按 static_secret_render_interval 重新读取;可续租动态凭据通常在租约经过约三分之二时续租。缩短刷新间隔会增加 Vault 读流量,放大抖动时的惊群。
Kubernetes 中常见四条链路各有代价:
Agent Injector 通过 mutating webhook 给 Pod 注入 init/sidecar,并把模板结果写到共享内存卷。它支持 auto-auth、模板和持续刷新,但每个 Pod 增加 sidecar 资源,webhook 与 Vault 不可用会阻断启动。Vault Secrets Store CSI provider 在容器创建阶段把值写入临时 CSI volume,应用按文件读取;它不具备 Agent 的同等模板、缓存和自动续租能力,动态刷新需求要单独验证。
Vault Secrets Operator 用 CRD 对账,并默认同步为 Kubernetes Secret。应用接入自然,但值会进入 Kubernetes API 与 etcd,RBAC、encryption at rest、备份和审计都扩大为新的 secret 边界。Vault Secrets Operator CSI driver 可以把部分场景改为临时卷;受保护 secret 能力带有产品版本与许可边界,不能把普通 Operator 的 Kubernetes Secret 同步和 CSI 形态混写。
容器环境变量最省改造,却最难热更新,还可能通过进程检查、崩溃报告、诊断页面和子进程继承泄漏。优先采用只读文件和原子重载;若旧应用只能读环境变量,就把进程重启纳入轮换事务,并证明所有旧 Pod 已退出。无论哪种集成,关闭 sidecar 或删除 Pod 都不等于外部 dynamic secret 已撤销,要核对 lease 和目标系统账号。
listener、TLS 与服务发现决定谁能安全到达 Vault
生产 listener 不应复用 dev 默认。一个最小的 Raft 节点配置骨架如下:
ui = true
api_addr = "https://vault-1.example.invalid:8200"
cluster_addr = "https://vault-1.example.invalid:8201"
storage "raft" {
path = "/var/lib/vault/raft"
node_id = "vault-1"
}
listener "tcp" {
address = "0.0.0.0:8200"
cluster_address = "0.0.0.0:8201"
tls_cert_file = "/etc/vault/tls/server-fullchain.pem"
tls_key_file = "/etc/vault/tls/server-key.pem"
tls_min_version = "tls12"
}address 是客户端 API 监听,cluster_address 用于节点间通信;api_addr 是节点向客户端/standby 宣告的可达地址,cluster_addr 是节点向集群成员宣告的地址。写成 Pod IP、NAT 后不可达地址或证书中没有的主机名,会造成 307 重定向失败、standby 无法转发或节点加入后反复断连。
TLS 私钥文件只能由 Vault 服务身份读取,证书要包含负载均衡域名和节点所需 SAN。listener 的 TLS 配置变更需要优雅重启,不能假定 SIGHUP 会热加载。负载均衡器使用 /sys/health 时,要理解 sealed、standby、performance standby 和 DR secondary 的不同状态码,不能只把 HTTP 200 当健康。公开未认证 health 响应还可能泄漏版本、集群名和节点地址,可按 listener 配置脱敏。
Vault 进程使用独立非 root 账号,只允许写 Raft 数据与审计目标,不能覆盖自身二进制和配置。mlock、容器 capability、swap 和内核限制要按平台支持状态评估;简单设置 disable_mlock=true 不是性能优化,而是接受敏感内存页可能被换出的风险。
从单节点到 HA、托管与跨地域复制的选型
dev server 是一次性实验;带 file storage 的 standalone 可以做隔离集成测试或短期非关键环境,但没有节点冗余。生产自建通常采用多个 Vault server 与 Integrated Storage,节点分布在独立故障域并保持低延迟、稳定磁盘。Raft 提交需要多数派,所以节点数和故障域要围绕 quorum 设计;两节点不能在失去一个节点后保持多数派,盲目增加跨地域 voter 又会把每次写入延迟绑定到广域网。
普通 HA 只有一个 active 节点处理请求,standby 负责转发/重定向和故障接管;增加普通 standby 提升可用性,不等于线性增加读吞吐。performance standby 才能在本地处理符合条件的只读请求,它需要相应 Vault Enterprise 许可或 HCP Vault Dedicated 能力。动态凭据生成、token 更新和其他写请求仍要到 active。
Integrated Storage 的 Autopilot 会观察新节点稳定性,再将其提升为 voter;dead server cleanup 不是默认自动清理所有故障节点,阈值太短还可能把正在加载大快照的新节点误移除。运维指标要观察 leader、peer 数、voter 状态、last contact、committed/applied index 差距、磁盘延迟、seal 状态和请求队列,而不是只看进程存活。
跨地域有两类容易混淆的企业能力:performance replication 让 secondary 在本地服务工作负载并降低读取延迟,数据以异步方式从 primary 复制;DR replication 的 secondary 平时不服务普通请求,灾难时提升为新 primary。两者都不是同步双活数据库,切换时仍要考虑复制滞后、DNS/流量、auth 依赖、外部动态凭据和演练证据。
namespace 提供层级化管理和多租户委派,但属于相应 Enterprise/HCP 能力。它能隔离 mount、policy 与身份管理边界,不会自动隔离底层 cluster、seal、升级和容量故障。HCP Vault Dedicated 由平台管理控制面与底层集群,减少节点、升级、备份等日常负担;不同 tier 的 HA、审计流、恢复、复制、client 计费和 root namespace 能力不同,应在采购时从 HCP Vault Dedicated 能力页动态核对,不能把开发 tier 当生产 SLA,也不能把自建 root 操作照搬到托管服务。
选型先看恢复目标、跨区延迟、合规隔离、工作负载数量、峰值请求、团队值守能力和退出要求。小团队没有密钥基础设施与全天候值守时,托管费用可能低于自建 HA 的真实人力;已有成熟 KMS、平台团队和数据驻留要求时,自建 Integrated Storage 更可控。许可证、client 计量、HSM/KMS、跨区网络、审计存储和灾备环境都要进入总成本,而不是只比较 server CPU。
审计设备既是证据链,也是可用性依赖
新集群默认没有启用审计设备。Vault 审计文档说明,审计设备记录绝大多数 API 请求与响应,字符串值通常以 HMAC-SHA256 形式写入;非字符串值不会获得同样默认保护,所以敏感数字也应作为字符串发送。server operational log 记录进程运行问题,不能替代谁读取了哪个 path 的审计证据。
vault audit enable -path=file-a file file_path=/var/log/vault/audit-a.json
vault audit enable -path=file-b file file_path=/var/log/vault/audit-b.json
vault audit list -detailedVault 会尝试向所有启用设备写入,并保证每条审计记录至少成功写入一个设备;若没有任何设备成功,对应 API 请求会失败。还要区分“快速返回写入失败”和“设备写入阻塞”:阻塞的 socket、syslog 或文件系统可能让请求一直等到设备恢复,即使另一个设备可写也不能把这种延迟风险忽略。双设备要落到独立失败域,而不是同一个已满磁盘的两个文件。file audit 使用独立卷并配置容量、权限、轮转和采集确认;socket/syslog 要限制阻塞影响。log_raw=true 会关闭敏感字段 HMAC,几乎不应作为日常排障手段。
审计验收不能只看文件存在。发出带唯一 correlation ID 的合成读请求,确认每个目标设备都收到 request/response 对,能按 token accessor、entity、namespace、mount path、operation 和结果关联;再分别模拟一个设备快速写入失败和一个设备阻塞,验证请求结果、延迟、告警与恢复动作。禁用再启用同一路径会生成新的 HMAC key,旧日志不能再用新设备计算同样哈希,这会影响跨周期调查。
审计日志同样是敏感数据:它包含 path、身份、来源地址、错误、元数据和访问模式。集中平台需要最小读取权限、不可变保留、地域与期限策略,以及对导出完整性的校验。把所有 request/response 全量长期索引可能让日志成本超过 Vault 本身,先按法规和调查需求设计热、冷、归档层。
快照、解封材料和外部系统必须一起恢复
Integrated Storage 支持 vault operator raft snapshot save、inspect 和 restore。快照包含 Vault 加密状态,应保存到独立账号或故障域,限制下载者并校验摘要:
vault operator raft snapshot save vault-raft.snap
vault operator raft snapshot inspect vault-raft.snap
sha256sum vault-raft.snap > vault-raft.snap.sha256有备份不等于能恢复。演练需要在隔离环境启动兼容版本 Vault,准备仍可用的 Shamir shares 或 auto-unseal 根依赖,恢复快照,验证关键 mount、policy、auth 配置和合成 secret,再确认审计、DNS 与客户端没有连到生产。同一集群且 seal key 一致时使用普通 restore;把快照恢复到新初始化的替代集群时,临时集群的 seal key 与快照不一致,官方灾难恢复流程要求使用 snapshot restore -force。-force 只是绕过 seal key 一致性检查,不会把快照重新加密给新 seal,也不会免除原集群的 Shamir unseal key 或原 auto-unseal 根依赖;恢复后仍要用快照所属集群的解封材料。这个参数属于受审批的灾难恢复动作,不能作为日常迁移或覆盖现有集群的快捷手段。
Raft 快照只覆盖 Vault 内部状态,不会自动恢复外部数据库里已创建的账号、云 IAM 对象、Kubernetes 中同步的 Secret、审计平台日志或 KMS 主密钥。恢复到旧时间点还可能让已经撤销的 token/lease 状态回到过去,或者与外部系统现状分叉。隔离恢复环境必须阻断到生产数据库和云 API 的撤销路径,否则恢复出来的过期 lease 可能触发清理并伤及仍在使用的外部凭据。正式恢复后的第一批动作应冻结普通流量、核对 seal 和集群身份、按 token accessor 与 lease prefix 对账、轮换外部管理凭据,再逐步放行工作负载;不能仅凭 Vault 内部 lease 状态判断外部账号仍有效或已经删除。
备份指标关注最近成功快照、大小突变、校验失败、异地复制和最近一次恢复演练,而不是“备份任务退出码 0”。恢复时间由快照下载、节点初始化、解封依赖、外部对账和客户端切换共同决定,RTO 不能只测 snapshot restore 命令耗时。
升级要同时约束 server、plugin、Agent 与集成组件
Vault 升级不是替换一个二进制。server 数据格式、Integrated Storage、seal、auth/secrets plugin、Vault Agent、Helm chart、Injector、CSI、Secrets Operator 和 SDK 都可能有兼容窗口。先阅读当前到目标版本的 upgrade guide 与 release notes,保存并验证快照,在隔离环境恢复后跑关键认证、KV、dynamic lease、审计和撤销回归。
自建 HA 通常逐个替换 standby,等待 Raft index 追平和节点稳定,再切换 active;跨多个大版本跳升要遵循官方中间版本要求。升级期间不能让旧版与新版节点长期混跑,也不能在 quorum 尚未恢复时继续替换。每一步记录 server version、leader、peer/voter、committed/applied index、seal、请求错误和回退条件。
外部 plugin 以 catalog 中的名称、类型、版本和 SHA-256 注册。覆盖同一版本二进制会让多处 mount 同时改变,官方建议把插件版本当不可变制品。升级先新增版本、重载指定 mount、观察错误与 lease,再清理旧制品。provider 或数据库兼容变化还要在目标系统跑创建/撤销实验。
Enterprise Automated Upgrades、HCP 版本管理能减少手工步骤,但不会替团队验证 auth 断言、应用重载、审计导出和业务 SLA。自动化的价值是让步骤可重复,不是把决策责任交给平台。
排障要从证据所在层开始
看到 permission denied 时先保存请求 method、规范化 API path、namespace、auth mount、token accessor 和 request ID,再检查 vault token capabilities、policy 匹配和 identity group。不要立刻给 token 加 sudo 或 path "*";短暂恢复服务会掩盖真正的 path 错误并留下永久越权。
看到 missing client token 或 token lookup 失败,要区分客户端根本没取得 token、sink 文件权限错误、token 已到期、父 token 被撤销和 Agent 没有重新认证。Agent operational log 可以显示 auto-auth/renew 状态,但不能打开 trace 把 secret 打出来。检查 token 文件 owner、mtime 与进程打开文件,再用 accessor 查审计,不打印 token 本身。
看到 Vault is sealed,先判断单节点、全 cluster 还是 load balancer 把流量发到 sealed 节点。Shamir 模式按法定人数流程解封每个目标节点;auto-unseal 检查 KMS/HSM 网络、IAM、key 状态和审计事件。不要因为一次 KMS 超时就重新初始化,operator init 对已有 storage 也不应成为修复动作。
看到 Raft no quorum、peer 落后或频繁 leadership change,检查故障域、网络 RTT/丢包、磁盘 fsync、节点时间、voter 列表与 applied index。不要同时重启多数节点,也不要随意 remove-peer;被移除节点重新加入前要按官方流程清理旧 Raft 数据,防止带着旧身份回归。
看到 dynamic credential 生成成功但应用认证失败,依次核对 lease 是否仍有效、creation statement 是否正确、目标系统账号状态、连接字符串、网络和连接池缓存。生成接口返回 200 只证明 Vault plugin 完成了当时动作,不证明业务进程已加载,也不证明 revoke 能成功。
看到审计相关请求被拒绝,先确认是否所有 audit device 都不可写、磁盘是否满、文件 owner/mode 是否变化、采集 socket 是否断开。恢复至少一个设备后验证新请求,再补齐故障期间的事件边界;不要直接永久关闭全部审计换取可用性。
成本、供应链与长期治理决定 Vault 会不会再次退化
Vault 的固定成本包括 server/HCP tier、KMS/HSM、跨区网络、负载均衡、快照与审计存储;可变成本来自 client 计量、请求量、动态凭据创建频率、日志索引和平台人力。高频 sidecar 对 KV 每几分钟刷新,可能让读取、审计和网络同步放大;短 TTL dynamic secret 会增加外部账号 churn。容量测试要使用合成数据测峰值认证、secret 读取、lease renew/revoke、Raft 写延迟和审计吞吐,并把失败模式带入测试。
供应链不仅是 Vault server 二进制。Helm chart、容器基础镜像、外部 plugin、Terraform provider、Agent/Injector、CSI 与 Operator 都能获得高价值权限。所有制品固定版本与 digest,验证签名/摘要,维护 SBOM 和升级责任;plugin 目录不得由 Vault 服务账号随意覆盖,Kubernetes webhook 与 CRD 升级先 dry-run 并审查权限变化。
团队治理围绕四类 owner 展开:平台 owner 负责可用性、升级与容量;安全 owner 负责 seal、root、policy 基线与审计;业务 owner 负责 path、消费者与轮换验证;目标系统 owner 负责动态账号权限和孤儿对账。任何 secret 至少记录业务 owner、数据分类、来源、消费者、轮换方式、最大有效期、撤销入口和退出动作。
最小治理不变量可以写成可查询证据:
生产工作负载没有常驻 root token,也不依赖个人 token。每个业务身份只能读取明确 path,跨业务负例持续失败。动态 lease 到期或显式 revoke 后,目标系统旧身份无法继续认证。
KV 轮换后,所有活跃消费者都报告新版本,旧外部凭据已失效。至少一个完整审计链可按 request ID、entity 与 accessor 关联,审计设备故障会告警。Raft 快照在隔离环境能够恢复,seal 根依赖、KMS 和 key holder 流程也经过演练。
卸载应用集成后,没有残留 token sink、模板明文、Kubernetes Secret、lease 或目标系统孤儿账号。
清理实验时撤销身份,再删除容器
实验收尾不能只执行 docker stop。先用 root token 删除实验数据与策略,撤销可能仍存在的 token,再停止容器;这样能验证管理路径,也避免把“进程消失”误认为撤销成功。
docker exec -e VAULT_TOKEN=$LabRoot $LabName `
vault kv metadata delete -mount=team-kv catalog/local
docker exec -e VAULT_TOKEN=$LabRoot $LabName `
vault policy delete catalog-reader
docker exec -e VAULT_TOKEN=$LabRoot $LabName `
vault secrets disable team-kv
docker stop $LabName
Remove-Variable ReaderToken,ReaderAccessor,TokenResult,TokenJson,SyntheticValue,NextValue,ConflictValue,LabRoot -ErrorAction SilentlyContinue
docker ps --all --filter "name=$LabName"最后一条命令不应再列出容器;--rm 会删除容器层,内存 storage 随之消失。PowerShell 历史中保存的是变量引用而不是运行时生成值,但终端录屏、进程环境和 Docker inspect 仍可能在实验期间看到它们,所以实验只能在可信单机进行。真实 Vault 的卸载顺序完全不同:先迁移消费者和身份,停止新签发,等待或撤销 lease,导出审计与清单,验证备份恢复和目标系统孤儿,再关闭流量;直接删除 cluster 会把仍在外部系统有效的动态凭据变成无人管理资产。
当团队能从一个业务请求反查 auth identity、policy path、secret version 或 lease、消费者加载状态、撤销结果和审计证据,并能在丢失节点或误操作后恢复这条链,Vault 才真正成为 secret 基础设施。否则,它只是把原来散落在 .env、CI 和聊天里的长期密码,集中到了一个更关键、也更需要工程纪律的系统里。
