HashiCorp Vault 动态凭据踩坑:一次数据库密码泄露后,我把轮换周期从 30 天缩到 1 小时

📅 2026/7/17 10:19:00
HashiCorp Vault 动态凭据踩坑:一次数据库密码泄露后,我把轮换周期从 30 天缩到 1 小时
HashiCorp Vault 动态凭据踩坑一次数据库密码泄露后我把轮换周期从 30 天缩到 1 小时上个月组里一个实习生在 Git 提交里不小心把测试环境的 RDS 密码带上去了。虽然仓库是私有的但代码扫描工具还是告警了。这件事让我意识到静态密码这东西再长再复杂只要被人看到过就永远是颗定时炸弹。说实话我们之前的密码管理已经不算太差了。每 30 天轮换一次存在公司的密码管理工具里服务启动时通过环境变量注入。听起来很标准对吧但问题就在于只要密码被写进配置文件、代码或者 CI 变量它就有无数机会泄露。而且 30 天的轮换周期意味着泄露后你有 30 天都在裸奔。这次之后我决定把数据库凭据切到 HashiCorp Vault 的动态凭据。轮换周期从 30 天直接缩到 1 小时。下面是我踩过的坑和最终落地的方案。背景为什么静态密码靠不住先说说我们原来的流程。每个服务启动时都会从环境变量读DB_USER和DB_PASSWORD。密码由 DBA 团队统一生成每季度换一次通过内部 Wiki 下发。听着很规范但实际操作里漏洞很多本地开发时很多人把密码直接写.env文件里提交时一个不留神就带上去了。某些老旧的 CI 脚本把密码打印在日志里虽然现在禁了但历史日志还在。离职员工的本地机器、个人笔记里可能还存着旧密码。最尴尬的是有些微服务运行了半年用的还是老密码轮换时根本没人敢动生怕重启就起不来。说白了静态密码的最大的问题是你根本不知道它在哪里被复制过多少份。一旦泄露除了全员换密码几乎没有更好的办法。方案选型Vault 的动态凭据是什么HashiCorp Vault 的 Database Secrets Engine 可以为每个客户端动态生成一套数据库账号密码。每次应用启动、或者每隔一段时间Vault 会创建一个新账号授予必要的权限Lease 到期后自动删除。核心就三点每个客户端拿到的密码都不一样。即使某个 Pod 的密码泄露了攻击者也只能用这个账号访问影响范围被圈死。密码有 TTL。可以设置成 1 小时、24 小时或者每次应用启动时重新获取。Lease 到期不续约账号直接失效。应用无需知道密码怎么生成的。它只需要向 Vault 请求 “给我一套 db-readwrite 角色的凭据”Vault 返回用户名密码应用拿来用就行。这个方案比静态密码麻烦的地方在于你需要在 Vault 和应用之间搭一座桥。但搭好之后密码管理这件事基本就自动化了。落地步骤从 MySQL 到应用我们用的是 AWS RDS MySQL 8.0下面是我实际配置的流程。1. 在 Vault 里启用 Database Secrets Enginevault secretsenabledatabase2. 配置数据库连接Vault 需要能连上数据库并且拥有创建/删除用户的权限。我在 RDS 里建了一个专门给 Vault 用的管理账号vault-admin。vaultwritedatabase/config/my-mysql\plugin_namemysql-rotate-root-credentials-database-plugin\connection_url{{username}}:{{password}}tcp(rds.internal:3306)/\allowed_rolesapp-readwrite\usernamevault-admin\passwordyour-vault-admin-password注意这里我用了mysql-rotate-root-credentials插件它会自动轮转 Vault 管理账号自身的密码。这样就连 Vault 管理账号也不会长期暴露。3. 创建角色定义权限和 TTL这是最关键的一步。我们设置了默认 TTL 1 小时最大 TTL 4 小时。创建用户时只授予应用需要的最小权限。vaultwritedatabase/roles/app-readwrite\db_namemy-mysql\creation_statementsCREATE USER {{name}}% IDENTIFIED BY {{password}}; GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO {{name}}%;\default_ttl1h\max_ttl4h1 小时 TTL 听起来很激进但对我们的微服务来说完全没问题。服务启动时拉一次凭据之后每隔 30-40 分钟主动刷新一次Lease 续不上就自动退出Kubernetes 会重新调度。4. 应用接入Spring Boot 示例我们大部分服务是 Java/Spring Boot。用 Spring Cloud Vault 可以比较无缝地接入。bootstrap.ymlspring:cloud:vault:enabled:trueuri:https://vault.internal:8200authentication:KUBERNETESkubernetes:role:app-readwriteservice-account-token-file:/var/run/secrets/kubernetes.io/serviceaccount/tokendatabase:enabled:truerole:app-readwritebackend:databaseSpring Cloud Vault 会自动把数据库凭据注入到spring.datasource里。应用启动时它先去向 Vault 要账号密码然后连数据库。整个过程应用代码几乎不用改。不过这里有个坑Spring Cloud Vault 默认只在启动时获取一次凭据。如果 TTL 只有 1 小时密码过期后应用就挂了。需要配置spring.cloud.vault.config.lifecycle.enabledtrue让它在后台自动刷新。spring:cloud:vault:config:lifecycle:enabled:truemin-renewal:10mexpiry-threshold:30m5. 非 Java 服务直接走 Vault Agent 模板有些 Python 和 Go 服务不想改代码。我们用 Vault Agent 的 template 功能把凭据渲染成文件。agent-config.hcltemplate { destination /app/db-creds.env contents EOT {{ with secret database/creds/app-readwrite }} DB_USER{{ .Data.username }} DB_PASSWORD{{ .Data.password }} DB_HOSTrds.internal DB_PORT3306 {{ end }} EOT }Vault Agent 会每 30 秒检查一次 Lease如果快过期了就重新渲染文件。应用里只要读取这个文件就行完全不用关心 Vault 的协议细节。踩坑记录这次迁移我踩了几个实打实的坑记录下来给后面的人避坑。坑 1数据库连接数爆了一开始我把 TTL 设成 10 分钟结果 RDS 的连接数直接翻倍。因为每个 Pod 每隔 10 分钟就要创建一个新账号旧账号 10 分钟之后才回收。高峰期几百个 Pod 同时刷新数据库里残留了大量待清理的用户。解决方案把 TTL 调到 1 小时同时让应用在密码快过期时主动REVOKE旧 Lease而不是等 Vault 自动回收。Vault 的sys/leases/revoke接口可以立即清理。坑 2权限配置写错了导致创建出来的用户权限不足测试环境没问题一上生产发现应用报错INSERT denied。排查半天发现creation_statements里的ON app_db.*在测试库名是对的但生产库叫prod_app_db。 Vault 不会帮你校验库名SQL 执行失败它只会在日志里轻描淡写地提一句。血泪教训上线前一定要在目标数据库上手动跑一遍creation_statements里的 SQL确认权限正确。坑 3K8s Service Account 权限没对齐Vault 的 Kubernetes 认证需要给每个 Service Account 绑定 Vault Role。我们有个 namespace 的 SA 名字写错了结果 Pod 启动时一直报 403。Vault 的日志里没有明确的 “SA not found”只有 “permission denied”。建议把 Vault Kubernetes 认证配置写成 Terraform和 K8s 的 RBAC 一起管。不要手动在 UI 上点。坑 4旧服务还没迁移完混合期间密码管理混乱不可能所有服务一次性切过去。迁移期间有些服务用 Vault有些服务还是用静态密码。我们在 Vault 里单独建了一个legacy角色给静态密码的过渡服务用同时设定 7 天 TTL 并强制过期倒逼团队尽快迁移。坑 5Lease 续租失败时应用行为不优雅Spring Boot 默认在 Lease 过期后不会主动退出而是继续用错误密码重试数据库连接日志里疯狂刷Access denied。后来我们在应用里加了一个健康检查如果 Vault 凭据获取失败或 Lease 过期就让健康检查失败K8s 会自动重启 Pod。ComponentpublicclassVaultLeaseHealthIndicatorimplementsHealthIndicator{AutowiredprivateLeaseEventPublisherleaseEventPublisher;OverridepublicHealthhealth(){// 简化的逻辑检查 Lease 是否有效returnleaseIsValid()?Health.up().build():Health.down().build();}}最终效果迁移完成后数据库凭据管理的变化非常明显密码轮换周期从 30 天缩短到 1 小时。每个 Pod 的密码都不一样一个 Pod 被入侵不会影响其他 Pod。研发本地不再有真正的生产密码。他们要访问数据库必须通过 Vault 临时申请最长 4 小时。审计粒度细了很多。Vault 的 audit log 能精确到哪个 K8s Service Account 在什么时候申请了什么角色的凭据。当然代价也是有的。服务启动速度变慢了 2-3 秒因为要多一次 Vault 认证。极少数极端情况下如果 Vault 本身挂了服务无法启动。但我们通过 Vault 集群高可用和 K8s 就绪探针已经把这个风险控制在可接受范围内。写在最后这次密码泄露事件其实是个警钟。静态密码的管理方式在微服务时代已经有点力不从心了。Vault 动态凭据不是唯一方案但它确实把我们最担心的密码泄露后影响范围有多大这个问题从全看运气变成了可控可审计。如果你也在做类似的事情我的建议是不要追求一次性把所有服务都切过去。先挑一个非核心服务试点把creation_statements、K8s 认证、Lease 续租这些流程跑通再逐步推广。中间态肯定会有些混乱但比等到下一次泄露事件发生后再亡羊补牢要好得多。有问题欢迎在评论区交流Vault 的坑远不止我写的这几个。