密钥生命周期管理:创建、存储、轮换、吊销、过期处理
密钥生命周期管理,说白了就是管好密钥从生到死的每一步。很多团队只关心「怎么创建」和「放哪儿」,却忽略了轮换、吊销和过期处理。我在项目里见过一个悲剧:某团队把数据库密码直接写死在代码里,发现泄露后只是改了新密码,但旧密码没吊销,结果攻击者用旧凭证继续访问了两个月。
下面我按五个阶段拆开讲,每个阶段都有血泪教训。
1. 创建:从源头保证强度
密钥创建是第一步,也是最容易被糊弄的一步。我个人习惯用至少 256 位随机数生成,绝不手动敲键盘。
- 生成算法:优先用
openssl rand -base64 32或uuidgen配合哈希。别用date +%s | md5sum,那玩意儿可预测。 - 责任分离:创建密钥的人不能同时拥有该密钥的使用权限。我曾在某个金融项目里强制要求「创建者只读初始值,运维人员只负责存储,应用只负责调用」。
- 初始版本记录:创建时立即打上版本标签,方便后续轮换和回滚。
2. 存储:别放仓库,别写环境变量死值
存储环节是 CI/CD 管道出错的重灾区。很多团队把密钥塞进 .env 文件提交到 Git,或者用 GitLab CI/CD 的 variables 明文存储。说白了,这些都是裸奔。
我个人推荐这几种方式,按安全性排序:
| 存储方式 | 安全性 | 审计能力 | 动态轮换 | 适用场景 |
|---|---|---|---|---|
| 环境变量(明文) | 低 | 无 | 需手动 | 开发/测试环境,不推荐生产 |
| GitLab CI/CD 变量(Masked) | 中 | 有操作日志 | 需手动 | 小团队,管道简单 |
| HashiCorp Vault | 高 | 完整审计 | 自动 | 生产环境,多服务 |
| AWS Secrets Manager | 高 | CloudTrail | 自动轮换 | AWS 原生用户 |
| Kubernetes Secrets | 中 | 需配合 RBAC | 需工具 | K8s 集群内部 |
我在项目中用过一段时间的 Vault,它的动态密钥(Dynamic Secret)机制很香:每次请求生成一个临时凭证,用完即废,连轮换都省了。
# 创建静态密钥并存储到 Vault
vault kv put secret/myapp/db \
username="db_user" \
password="$(openssl rand -base64 32)" \
ttl="7d"
3. 轮换:别等到泄露才换
轮换不是应急措施,而是常规操作。我个人习惯设定强制轮换周期,比如:
- :30 天轮换一次
- 数据库密码:90 天轮换一次
- TLS 证书:按证书有效期,提前 30 天触发轮换
你可能会问:「轮换的时候服务会不会断?」嗯,这正是关键。要保证轮换是零停机操作,我常用的套路是「蓝绿版本」:
- 创建新密钥版本(版本 v2)
- 将新密钥写入 Vault,标记为
active - 应用逐步切换到新密钥,同时保留旧密钥一段时间
- 确认所有实例使用新密钥后,吊销旧密钥
我曾经的救火经历:有次轮换脚本没加失败回退,导致所有服务同时拿到旧密钥失效版本,数据库连接全部断开。还好当时手动回滚了 Vault 的版本。现在我的轮换脚本里永远保留「回滚第一步:恢复上一版本」的逻辑。
在 GitLab CI 里实现自动轮换,可以这样写:
rotate-secret:
stage: deploy
script:
- # 检查密钥剩余有效期
- expiry=$(vault read -field=expiry secret/myapp/)
- if [ "$expiry" -lt "$(( $(date +%s) + 7*86400 ))" ]; then
- # 生成新密钥并更新 Vault
- new_key=$(openssl rand -base64 32)
- vault kv put secret/myapp/ value="$new_key"
- # 通知下游服务
- curl -X POST https://hooks.example.com/secret-rotated
- fi
only:
- schedules # 定时触发
4. 吊销:检测到泄露立即行动
吊销不是轮换的附属操作,而是一条独立的安全红线。只要发现密钥有泄露嫌疑,立刻废除,别等什么轮换周期。
吊销步骤我总结为「三步走」:
- 立即禁用:在密钥管理系统中打上
revoked标签,阻止所有新请求使用该密钥。 - 清理所有引用:用脚本扫描 CI/CD 变量、代码仓库、日志中的密钥指纹,确保不会残留旧值。
- 发出告警:通知相关团队该密钥已吊销,并触发备用密钥的自动启用。
5. 过期处理:自动化和告警两手抓
密钥过期本身不是问题,问题是过期后没人管。我建议给每个密钥加上 TTL(Time To Live),到期前自动触发轮换或告警。
| 密钥类型 | 推荐过期策略 | 告警阈值 |
|---|---|---|
| 静态 | 30 天过期 | 到期前 7 天告警 |
| 数据库凭证 | 90 天过期 | 到期前 14 天告警 |
| TLS 证书 | 按 CA 规定(通常 1 年) | 到期前 30 天告警,14 天自动续签 |
在 CI/CD 管道里,我会添加一个独立的审计 job,每天检查所有密钥的剩余有效期,如果某个密钥快要过期又没有轮换计划,就自动阻断发布管道。
# 在 GitLab CI 中检查密钥过期状态
check-secret-expiry:
stage: audit
script:
- |
for secret in $(vault list -format=json secret/myapp/ | jq -r '.[]'); do
expiry=$(vault read -field=expiry "secret/myapp/$secret")
remaining=$(( expiry - $(date +%s) ))
if [ "$remaining" -lt "$(( 7*86400 ))" ]; then
echo "❌ $secret will expire in less than 7 days!"
exit 1
fi
done
嗯,注意这里如果密钥已经过期,管道应该强制失败,而不是悄悄忽略。我见过太多次「告警发到没人看的邮件」的案例。
一句话总结:密钥生命周期不是一次性的配置任务,而是一个持续运行的自动化流程。创建、存储、轮换、吊销、过期处理,每个环节都要在 CI/CD 管道里落地成可审计的 job。