数据库密码硬编码在配置文件里,这几乎是所有运维人员都踩过的坑。应用代码、配置文件、脚本中散落着各种明文凭据,一旦泄露,整个数据库集群的安全防线就形同虚设。更麻烦的是密码轮换——你需要同时修改多处配置,重启一堆服务,稍有不慎就会导致业务中断。HashiCorp Vault 正是为解决这类“秘密蔓延”问题而生的一套动态秘密管理引擎。把它部署在 CentOS 上,可以让数据库密码变成按需生成、自动过期、一次一密的动态凭据,从根本上消除静态密码带来的安全隐患。

理解 Vault 数据库动态凭据的工作机制

在动手安装之前,有必要先搞清楚 Vault 到底是怎么“动态管理”数据库密码的。Vault 并不会去修改你已有的数据库用户密码,而是通过预先配置好的数据库连接,在你每次请求时临时创建一个新的数据库用户,并赋予该用户一组随机生成的用户名和密码。这个凭据自带租约期限,到期后 Vault 会自动回收——也就是在数据库层面删除这个临时用户。

整个过程大致分为四步:Vault 管理员先配置好数据库引擎,告诉 Vault 如何连接目标数据库、用什么角色创建用户;应用通过 API 或命令行向 Vault 请求数据库凭据;Vault 在数据库中执行 CREATE USER 语句,生成临时账号并返回凭据;租约到期后 Vault 执行 DROP USER 完成回收。这样一来,应用拿到的永远是一个临时身份,即使凭据被日志误记录或被攻击者截获,它的有效时间也非常有限。

环境准备与 Vault 安装

本文以 CentOS 7.9 和 Vault 1.15 版本为例,后端数据库使用 MySQL 8.0。首先确保系统时间准确,因为 Vault 的租约机制强依赖时间同步。执行 ntpdate 或配置 chronyd 完成时间校准。

Vault 官方提供了预编译的二进制包,这是最干净的安装方式。下载解压后直接放到系统路径即可:

# 安装必要工具
yum install -y yum-utils unzip curl

# 下载 Vault 二进制文件
curl -fsSL https://releases.hashicorp.com/vault/1.15.0/vault_1.15.0_linux_amd64.zip -o /tmp/vault.zip
unzip /tmp/vault.zip -d /usr/local/bin/
chmod +x /usr/local/bin/vault

# 验证安装
vault version

安装完成后,建议创建一个专用的 vault 系统用户来运行 Vault 服务,避免使用 root 权限:

useradd --system --home /etc/vault.d --shell /bin/false vault
mkdir -p /etc/vault.d /var/lib/vault
chown -R vault:vault /etc/vault.d /var/lib/vault
配置 Vault 开发模式用于测试

如果是初次接触 Vault,建议先在开发模式下跑通整个流程。开发模式会自动初始化并解封,省去生产环境中繁琐的解封步骤。但请注意,开发模式的数据完全存储在内存中,重启后全部丢失,绝对不能用于生产。

# 启动开发模式,API 监听在所有网卡上
vault server -dev -dev-listen-address="0.0.0.0:8200"

启动后终端会输出 Root Token 和解封密钥,务必记录下来。另开一个终端设置环境变量:

export VAULT_ADDR='http://127.0.0.1:8200'
export VAULT_TOKEN='刚才输出的Root Token'

验证 Vault 状态:

vault status

如果看到 Sealed 为 false,说明 Vault 已经解封并可用。

生产环境部署:初始化与解封

生产环境绝不能使用开发模式。需要创建一个正式的配置文件 /etc/vault.d/vault.hcl:

storage "raft" {
  path    = "/var/lib/vault"
  node_id = "node1"
}

listener "tcp" {
  address     = "0.0.0.0:8200"
  tls_disable = true  # 生产环境务必配置 TLS 证书并设为 false
}

api_addr = "http://127.0.0.1:8200"
cluster_addr = "http://127.0.0.1:8201"
ui = true
disable_mlock = true

这里使用了 Raft 集成存储,数据持久化在磁盘上。listener 中 tls_disable 设为 true 仅用于内网测试环境,生产环境必须配置 TLS 证书并关闭此选项。配置完成后创建 systemd 服务文件 /etc/systemd/system/vault.service:

[Unit]
Description=Vault Server
After=network.target

[Service]
User=vault
Group=vault
ExecStart=/usr/local/bin/vault server -config=/etc/vault.d/vault.hcl
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
LimitMEMLOCK=infinity

[Install]
WantedBy=multi-user.target

启动服务并初始化:

systemctl daemon-reload
systemctl enable vault
systemctl start vault

# 初始化 Vault,生成 5 个解封密钥,解封阈值设为 3
vault operator init -key-shares=5 -key-threshold=3

初始化命令会输出 5 个解封密钥和一个 Root Token。这些信息必须安全地离线保存,它们是恢复 Vault 的唯一凭证。Vault 启动后处于封印状态,需要用至少 3 个解封密钥来解封:

vault operator unseal <解封密钥1>
vault operator unseal <解封密钥2>
vault operator unseal <解封密钥3>

每次 Vault 服务重启都需要执行解封操作,这是 Vault 的安全设计——即使服务器被攻破,没有足够数量的解封密钥也无法读取存储的秘密。

启用数据库秘密引擎

Vault 解封并登录后,第一步是启用数据库秘密引擎。Vault 支持多种数据库,包括 MySQL、PostgreSQL、MongoDB、Oracle 等,每种数据库的配置参数略有不同。

# 启用数据库引擎,路径可以自定义
vault secrets enable -path=database database

这条命令在 database/ 路径下挂载了数据库秘密引擎。你可以用 vault secrets list 查看所有已启用的引擎。

配置 MySQL 数据库连接

接下来需要让 Vault 知道如何连接你的 MySQL 数据库。首先在 MySQL 中创建一个具有用户管理权限的账号,这个账号是 Vault 用来创建和删除临时用户的“管理员账号”:

-- 在 MySQL 中执行
CREATE USER 'vault_admin'@'%' IDENTIFIED BY 'StrongAdminPassword123!';
GRANT CREATE USER, DROP, SELECT, INSERT, UPDATE, DELETE ON *.* TO 'vault_admin'@'%' WITH GRANT OPTION;
FLUSH PRIVILEGES;

然后在 Vault 中配置数据库连接:

vault write database/config/mysql-prod \
  plugin_name=mysql-database-plugin \
  connection_url="{{username}}:{{password}}@tcp(192.168.1.100:3306)/" \
  allowed_roles="app-readonly,app-readwrite" \
  username="vault_admin" \
  password="StrongAdminPassword123!"

这里的 mysql-prod 是自定义的连接名称,plugin_name 指定使用 MySQL 插件,connection_url 中的占位符 {{username}} 和 {{password}} 会被 Vault 自动替换为配置的用户名密码。allowed_roles 限制了哪些角色可以使用这个连接。

配置完成后用 rotate-root-credentials 命令轮换 Vault 管理员的密码,这是安全最佳实践——即使初始密码被泄露,Vault 也能自动更新:

vault write -force database/rotate-root/mysql-prod

执行后,Vault 会修改 vault_admin 用户在 MySQL 中的密码,并更新自己存储的凭据。从此以后,只有 Vault 自己知道这个管理员密码是什么。

创建数据库角色定义动态凭据模板

角色是 Vault 动态凭据的核心概念。一个角色定义了一组 SQL 语句模板,Vault 在创建临时用户时会执行这些语句。下面创建一个只读角色:

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

这里 {{name}} 和 {{password}} 是 Vault 自动生成的随机值。default_ttl 设为 1 小时,意味着每个生成的凭据默认 1 小时后过期;max_ttl 是最大续租期限,即使应用不断续租,凭据最长也只能存活 24 小时。再创建一个读写角色:

vault write database/roles/app-readwrite \
  db_name=mysql-prod \
  creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO '{{name}}'@'%';" \
  default_ttl="30m" \
  max_ttl="4h"

角色创建完成后,可以用 vault read 查看角色配置:

vault read database/roles/app-readonly
获取动态数据库凭据

一切就绪,现在可以实际获取动态凭据了:

vault read database/creds/app-readonly

输出类似这样:

Key                Value
---                -----
lease_id           database/creds/app-readonly/abc123xyz
lease_duration     1h
lease_renewable    true
password           A1b2C3d4E5-随机密码
username           v-token-app-readonly-随机后缀

此时登录 MySQL 查看,会发现多了一个临时用户。这个用户名和密码都是 Vault 随机生成的,有效期 1 小时。应用拿到这个凭据后就可以正常连接数据库,到期后 Vault 会自动清理。你可以手动测试回收:

vault lease revoke database/creds/app-readonly/abc123xyz

执行后,MySQL 中对应的临时用户就会被删除。

配置 ACL 策略控制访问权限

在生产环境中,你不可能把 Root Token 分发给每个应用。Vault 通过 ACL 策略实现细粒度的访问控制。创建一个只允许读取数据库凭据的策略文件 app-policy.hcl:

path "database/creds/app-readonly" {
  capabilities = ["read"]
}

path "database/creds/app-readwrite" {
  capabilities = ["read"]
}

将策略写入 Vault:

vault policy write app-policy app-policy.hcl

然后为应用创建一个受限的 Token,该 Token 只能执行策略中定义的读取操作:

vault token create -policy=app-policy -ttl=720h -display-name="app-server-01"

应用使用这个 Token 调用 Vault API 获取数据库凭据,即使 Token 泄露,攻击者也只能获取临时凭据,无法修改 Vault 配置或读取其他秘密。

应用集成:通过 API 获取凭据

应用代码中集成 Vault 通常有两种方式:直接调用 HTTP API,或者使用 Vault 官方提供的客户端 SDK。HTTP API 方式最为通用,任何语言都能实现。以 curl 为例:

curl -H "X-Vault-Token: s.yourAppTokenHere" \
     -X GET http://127.0.0.1:8200/v1/database/creds/app-readonly

返回的 JSON 中包含 username、password 和 lease_duration 字段。应用需要解析这些字段,用它们连接数据库,并在凭据到期前重新获取新凭据。一个健壮的实现应该包含以下逻辑:缓存当前凭据和过期时间;在凭据到期前 10% 的时间窗口内主动刷新;如果数据库连接失败且原因为认证错误,立即尝试获取新凭据;进程退出时主动撤销租约,释放数据库资源。

对于 Java 应用,可以使用 Spring Cloud Vault 或直接引入 Vault 的 Java Driver;Python 应用可以使用 hvac 库。这些 SDK 封装了租约管理和自动续租逻辑,能大幅降低集成复杂度。

监控与告警

Vault 自身的健康状态和凭据租约情况需要纳入监控体系。关键监控指标包括:Vault 是否处于封印状态,如果封印,所有秘密读取都会失败;数据库连接配置是否正常,如果 rotate-root 失败说明 Vault 与数据库之间的管理员凭据出了问题;租约到期速率是否异常,大量凭据同时到期可能意味着应用没有正确续租;审计日志中是否有大量拒绝请求,这可能是攻击者在使用无效 Token 进行探测。

Vault 提供了 Prometheus 格式的指标端点,可以直接对接 Prometheus 和 Grafana:

# 在 vault.hcl 中添加
telemetry {
  prometheus_retention_time = "30s"
  disable_hostname = true
}

然后通过 http://vault-server:8200/v1/sys/metrics?format=prometheus 获取指标数据。

备份与灾难恢复

使用 Raft 存储时,Vault 的数据保存在 /var/lib/vault 目录下。定期备份这个目录或者使用 Vault 的 snapshot 功能:

vault operator raft snapshot save /backup/vault-raft-$(date +%Y%m%d).snap

恢复时使用:

vault operator raft snapshot restore /backup/vault-raft-20250101.snap

需要特别注意的是,解封密钥和 Root Token 的离线保存比数据备份更重要。没有解封密钥,即使有完整的 Raft 快照也无法读取数据。建议将解封密钥分发给多个受信任的管理员,使用 Shamir 秘密共享机制保证安全。

常见问题排查

配置过程中最容易踩的坑有几个。一是数据库连接权限不足,Vault 管理员账号必须有 CREATE USER 和 DROP 权限,MySQL 8.0 还需要注意认证插件兼容性,建议使用 mysql_native_password。二是网络连通性问题,Vault 服务器必须能直接访问数据库的 3306 端口,如果中间有防火墙需要放行。三是租约时间设置不合理,default_ttl 太短会导致应用频繁刷新凭据增加数据库负载,太长则失去动态凭据的安全优势,建议根据业务场景在 30 分钟到 2 小时之间调整。四是忘记配置 allowed_roles,导致角色无法使用数据库连接,排查时可以用 vault read database/config/mysql-prod 检查连接配置的完整信息。

将数据库密码管理从静态配置迁移到 Vault 动态凭据体系,本质上是在安全性和运维复杂度之间做了一次价值交换。短期来看,你需要搭建 Vault 集群、修改应用代码、培训团队;长期来看,你获得的是一个可审计、可追溯、自动轮换的凭据生命周期管理体系。对于需要满足等保合规或处理敏感数据的企业环境,这笔投入是完全值得的。