# 用 Vault AppRole，Postgres 密码只活一小时

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

原文: https://fdetimes.net/zh/guides/vault-approle-dynamic-postgres-credentials/

你的应用今天用的 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 申请权限更低、有时间限制的账号。

```bash
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 端点。

```bash
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，本指南沿用这两个数字。

```bash
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 小时。反过来，连续运行三天的服务必须重新申请凭据，所以它的连接代码要能应对密码变化。

然后写一条只允许读取这个路径的策略：

```hcl
# orders-app.hcl
path "database/creds/orders-readonly" {
  capabilities = ["read"]
}
```

```bash
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。

```bash
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：

```bash
vault read -field=role_id auth/approle/role/orders-app/role-id
```

SecretID 走第二条通道，而且不能明文传输。使用响应封装时，Vault 把 SecretID 存进一个使用次数为 1 的新 token 的 cubbyhole 中，然后返回这个 token。

```bash
vault write -wrap-ttl=60s -f auth/approle/role/orders-app/secret-id
```

命令返回一个封装 token。在运行应用的机器上解封并登录：

```bash
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 账号，看着它过期

```bash
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 凭据”。

没人需要保管的密码，就没人能泄露。在客户现场，这往往决定了你能不能接触真实数据。

**本周动手试试:**

- 在笔记本上搭好 dev 模式的 Vault 和 Postgres 容器，走完全部五步，再用 psql 确认 Vault 生成的账号带有 VALID UNTIL
- 尝试复用一个已登录过的 SecretID，观察第二次尝试被拒绝，记下错误信息，以便向客户解释
- 检查一条你有权限访问的 CI 流水线，找出任何一个任务能独自同时获取 RoleID 和 SecretID 的地方

## 信源

- [AppRole auth method | Vault | HashiCorp Developer](https://developer.hashicorp.com/vault/docs/auth/approle)

- [Best practices for AppRole authentication | Vault | HashiCorp Developer](https://developer.hashicorp.com/vault/docs/auth/approle/approle-pattern)

- [How (and Why) to Use AppRole Correctly in HashiCorp Vault](https://hashicorp.com/blog/how-and-why-to-use-approle-correctly-in-hashicorp-vault)

- [Database secrets engines | Vault | HashiCorp Developer](https://developer.hashicorp.com/vault/docs/secrets/databases)

- [PostgreSQL database secrets engine | Vault | HashiCorp Developer](https://developer.hashicorp.com/vault/docs/secrets/databases/postgresql)

- [Secure your database access with HashiCorp Vault](https://aiven.io/developer/secure-your-db-with-vault)
