用 Vault AppRole,Postgres 密码只活一小时
每个服务拥有自动过期的独立账号,解锁它的 SecretID 只能登录一次。
要点速览
- 应用不应持有高权限凭据。它用自己的 Vault token 申请一个权限收窄、带有效期的 Postgres 账号。
- RoleID 和 SecretID 必须走两条不同的通道,只在运行应用的机器上会合。
- 用响应封装包裹 SecretID,并设置 secret_id_num_uses=1,这样即使 SecretID 泄露也几乎没用。
你的应用今天用的 Postgres 账号,三年后可能依然有效,而且很可能和另外五个服务一起躺在同一个 .env 文件里。借助 HashiCorp Vault 的动态密钥,这个账号在应用启动时才创建,一小时后自动过期。
本指南在笔记本上重建这套流程:应用用 AppRole 登录 Vault,拿到一个短期 token,再用这个 token 申请属于自己的 Postgres 账号——权限收窄,自带有效期。
要搭建什么,需要准备什么
最终效果:一个名为 orders-app 的服务(名字为虚构)可以读取 orders 数据库中的表,而它的代码和配置里没有任何数据库密码。
你需要一个以 dev 模式运行的 Vault 服务器、一个 Postgres 实例(容器即可),以及 vault 和 psql 命令行工具。下面的命令针对实验环境做了精简。参数名和路径可能因版本而异,请对照客户所用版本的 Vault AppRole 和 PostgreSQL 文档核对。
第 1 步:数据库钥匙由 Vault 保管,应用不碰
Aiven 的指南讲明了背后的原则:应用不应持有高权限凭据,这类凭据由 Vault 保管。应用只用自己的 Vault token 申请权限更低、有时间限制的账号。
vault secrets enable database
vault write database/config/orders-db \
plugin_name=postgresql-database-plugin \
connection_url="postgresql://{{username}}:{{password}}@localhost:5432/orders" \
allowed_roles="orders-readonly" \
username="vault_admin" password="temp-password"
接着立即轮换 root 密码,让刚才敲进终端的那个密码作废。Vault 文档为此提供了专门的 rotate root credentials 端点。
vault write -f database/rotate-root/orders-db
**检查:**用 temp-password 再次通过 psql 登录,这次必须失败。从此以后,只有 Vault 知道 vault_admin 的密码。
第 2 步:一个角色对应一条 SQL 语句
Vault 的 PostgreSQL 文档把角色描述为一种映射:从 Vault 中的名字映射到用于创建凭据的 SQL 语句。文档示例把 default_ttl 设为 1h、max_ttl 设为 24h,本指南沿用这两个数字。
vault write database/roles/orders-readonly \
db_name=orders-db \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
default_ttl=1h max_ttl=24h
算一算这两个数字意味着什么。假设某个凭据被写进了日志。如果是静态密码,捡到它的人可以一直用下去,直到有人想起来改密码。
换成上面的角色,凭据不续期就会在 1 小时后过期,即使续期也活不过 24 小时。反过来,连续运行三天的服务必须重新申请凭据,所以它的连接代码要能应对密码变化。
然后写一条只允许读取这个路径的策略:
# orders-app.hcl
path "database/creds/orders-readonly" {
capabilities = ["read"]
}
vault policy write orders-app orders-app.hcl
第 3 步:AppRole 配合 batch token 和一次性 SecretID
AppRole 通过 RoleID 选择角色,默认情况下登录还需要 SecretID。和 token 一样,SecretID 也有使用次数限制、TTL 和过期时间。HashiCorp 的最佳实践页面说得很明确:要获得最高安全性,把 secret_id_num_uses 设为 1。AppRole 文档还推荐使用 batch token。
vault auth enable approle
vault write auth/approle/role/orders-app \
token_type=batch \
token_policies=orders-app \
token_ttl=20m \
secret_id_num_uses=1 \
secret_id_ttl=10m
token_ttl=20m 和 secret_id_ttl=10m 是为实验选的值,并非文档推荐。在客户现场,应根据服务的实际启动时间来设置。
第 4 步:钥匙的两半走两条通道
这是最常做错的一步:只要有一个地方能同时拿到两部分,整套机制就失效了。HashiCorp 建议 RoleID 和 SecretID 通过不同通道下发,只在运行应用的机器上会合。第一条通道传递 RoleID:
vault read -field=role_id auth/approle/role/orders-app/role-id
SecretID 走第二条通道,而且不能明文传输。使用响应封装时,Vault 把 SecretID 存进一个使用次数为 1 的新 token 的 cubbyhole 中,然后返回这个 token。
vault write -wrap-ttl=60s -f auth/approle/role/orders-app/secret-id
命令返回一个封装 token。在运行应用的机器上解封并登录:
vault unwrap -field=secret_id <wrapping_token>
vault write auth/approle/login \
role_id=<role_id> secret_id=<secret_id>
HashiCorp 关于 AppRole 的博客文章解释了封装为什么值得:只能使用一次的 token 即使泄露,危害也很小;如果有人抢先解封,应用会立刻察觉,因为它自己已经无法解封了。文章还强调,SecretID 应只在需要时才送到应用手里,通常是在启动时。
**检查:**用刚用过的 SecretID 再执行一次登录命令,这次必须被拒绝。如果仍能登录成功,说明 secret_id_num_uses 没有生效。
第 5 步:申请 Postgres 账号,看着它过期
VAULT_TOKEN=<client_token> vault read database/creds/orders-readonly
Vault 返回 username、password 和租约时长。用这组凭据通过 psql 登录,执行 \du,就能看到一个由 Vault 生成名字、带 VALID UNTIL 时间戳的角色。
数据库密钥引擎文档点出了一个很少有人注意的好处:每个服务都用自己的凭据访问数据库,审计可疑访问会容易得多。设想 12 个服务共用一个 app 用户。
Postgres 日志显示 app 在凌晨 3 点读取了客户表,却无从判断是哪个服务。当每次申请凭据都产生唯一的名字,这个问题就有了答案。
让系统只在纸面上好看的错误
第一个要避免的错误:让 CI worker 同时有权获取 RoleID 和 SecretID。HashiCorp 的最佳实践页面直接指出这违背最佳实践,因为谁拿下这个 worker,谁就同时握有钥匙的两半。正确做法是:负责下发 SecretID 的组件只能生成封装后的 SecretID,RoleID 则通过另一条通道送达。
第二个错误是跳过封装,把明文 SecretID 放进任务的环境变量。第三个是不限制 secret_id_num_uses,这样 SecretID 实际上就成了换了名字的静态密码。
第四个在老系统里很常见:动态密钥配好了,应用却仍保留着 root 凭据“以防万一”。如果某些遗留用户暂时还不能下线,Vault 提供静态角色(static role),可以自动轮换现有用户的密码。比起在配置文件里放一个 root 密码,这是合理得多的过渡方案。
这项技能在客户现场是什么样子
很少有人会直接让你“实现 AppRole”。你更常听到的是:“安全团队不会批准你的 Agent 直连数据库。”
HashiCorp 的博客文章把问题的根源称为 secret zero 问题:应用需要第一个密钥,才能获取其他所有密钥。AppRole 用可信代理(trusted broker)来解决它,即由一个可信系统负责下发 SecretID。
所以在客户现场,第一件事是画清楚谁持有哪一部分:哪条通道传 RoleID,哪个系统下发 SecretID,有没有哪个任务能同时拿到两者。这样一页图,往往比任何演示都更快赢得安全团队的认可。
写进简历时,描述结果,例如“用 1h TTL 动态密钥、一次性 SecretID 和每服务独立账号(便于审计)取代静态 Postgres 凭据”。
没人需要保管的密码,就没人能泄露。在客户现场,这往往决定了你能不能接触真实数据。
这篇文章对你有帮助吗?
感谢你的反馈!
6 个信源
- AppRole auth method | Vault | HashiCorp Developer
- Best practices for AppRole authentication | Vault | HashiCorp Developer
- How (and Why) to Use AppRole Correctly in HashiCorp Vault · 2021-08-30
- Database secrets engines | Vault | HashiCorp Developer
- PostgreSQL database secrets engine | Vault | HashiCorp Developer
- Secure your database access with HashiCorp Vault