FDE Times 数据在招 FDE 职位 824▼37 近 7 天在招职位数的变化(同口径比较):不计入本周新开始追踪的公司的职位近 7 天新增 76在招公司 153支持远程 25%美国薪资中位数 $202k招聘最多 Databricks 104
ENVI

前线部署工程师(FDE)的专业媒体

技能指南 · 指南

用 Vault AppRole,Postgres 密码只活一小时

每个服务拥有自动过期的独立账号,解锁它的 SecretID 只能登录一次。

分享微博

要点速览

  • 应用不应持有高权限凭据。它用自己的 Vault token 申请一个权限收窄、带有效期的 Postgres 账号。
  • RoleID 和 SecretID 必须走两条不同的通道,只在运行应用的机器上会合。
  • 用响应封装包裹 SecretID,并设置 secret_id_num_uses=1,这样即使 SecretID 泄露也几乎没用。
示意图:RoleID 和一次性包装的 SecretID 分别经由两个独立渠道送到 orders-app,旁注说明没有任何一个任务同时持有两者。orders-app 登录 Vault,Vault 随即创建一个 Postgres 账号,该账号的密码在 1 小时后自动过期(图中高亮)。
应用通过两个不同渠道分别拿到 RoleID 和 SecretID,再登录 Vault。Vault 为它创建专用的 Postgres 账号,默认 1 小时后过期,最长不超过 24 小时。来源:PostgreSQL 文档,HashiCorp Vault 的 AppRole 及最佳实践页面。

你的应用今天用的 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 个信源
路线图上的下一篇 · 阶段 5: 部署上线改客户表结构,别直接动正在用的列把破坏性变更拆成多次小部署,收缩阶段之前每步都能回滚,最后的清理也不能省。