密钥不该躺在配置文件里:敏感配置治理实战
密钥不该躺在配置文件里:敏感配置治理实战
很多安全事故的起点并不高明:它就是一行躺在 application.yml 里的 password: Prod@2023,被顺手提交进了 Git。到了 2026 年,OIDC 联邦、服务网格、短期凭证早已是成熟方案,但“把密钥写进配置文件”依然是最常见的凭证泄露来源之一。原因很朴素——它最省事。
这篇文章不讨论理念,只给一套能在一到两周内落地的八步流程:盘点、剥离、选型、接入、动态凭证、CI/CD 改造、扫描告警、应急响应。每一步都有可执行的命令或代码。
一、先认清:明文配置到底危险在哪
泄露面失控。 一份配置文件会同时出现在 Git 历史、构建镜像层、CI 缓存、日志、备份、开发者本地磁盘和临时排障的聊天窗口里。你只需要一次疏忽,它就会扩散到七个你无法回收的地方。
轮换成本高到没人愿意轮换。 当同一个数据库口令被硬编码在 12 个仓库里,改密码就变成一场跨团队协调会。于是大家选择不改——这才是真正的风险。
没有审计。 配置文件里的密钥被谁读过、什么时候读的,你永远查不到。密钥管理系统能做的不只是加密,更重要的是“每一次读取都留痕”。
下面进入实操。
二、第 1 步:建立敏感配置清单并分级
不要一上来就选工具。先花两天做一份清单:哪些服务、哪些配置项、敏感等级、当前存放位置、负责人、轮换周期。
分级建议:
| 等级 | 内容举例 | 允许存放位置 | | --- | --- | --- | | L0 公开 | 日志级别、feature flag、超时时间 | 可进 Git | | L1 内部 | 端口、内部域名、并发数 | 可进 Git(私有仓库) | | L2 敏感凭证 | 数据库口令、第三方 API Key、SMTP 密码 | 密钥管理系统 | | L3 根凭证 | 云厂商 AK/SK、KMS 主密钥、CA 私钥 | 仅 KMS/HSM,且只用于换取短期凭证 |
清单用 YAML 维护,随服务一起走代码评审:
yaml
secrets-inventory.yaml
- service: order-api
owner: team-trade
items:
- key: DB_PASSWORD level: L2 source: vault:secret/data/order-api/db rotate: 30d
- key: STRIPE_API_KEY level: L2 source: aws-secretsmanager:prod/stripe rotate: 90d
- key: AWS_ACCESS_KEY_ID level: L3 source: none # 待改造:改用 IRSA / OIDC rotate: manual action: "替换为工作负载身份,禁止长期 AK"
action 字段是这套清单的价值所在:它把“已知问题”变成了可追踪的待办,而不是一句“以后再说”。
三、第 2 步:从仓库里彻底剥离密钥
这里有一个必须记住的顺序:先轮换,再清洗历史。只清理 Git 历史而不轮换,等于把钥匙从门口地毯下挪到花盆下。
1)冻结与新建。 在密钥后端创建新凭证,与旧凭证并存(双活),避免割接瞬间中断业务。
2)替换读取方式。 应用改为从环境变量或密钥后端读取,代码里不再出现字面量。
3)清理历史。 用 git filter-repo 而非 filter-branch:
bash pip install git-filter-repo
从全部历史中删除某个文件
git filter-repo --path config/prod.yml --invert-paths
或按内容替换(慎用,先在全量镜像上演练)
cat > replacements.txt <<'EOF' Prod@2023==>REMOVED EOF git filter-repo --replace-text replacements.txt
git push --force --all && git push --force --tags
4)吊销旧凭证。 在云控制台或密钥后端把旧凭证标记为失效,并检查访问日志里是否有异常来源 IP。
5)通知与同步。 强制所有开发者重新克隆仓库——旧 clone 里仍然残留着明文。
四、第 3 步:选一个合适的密钥后端
| 方案 | 适合场景 | 注意点 |
| --- | --- | --- |
| 环境变量 | 本地开发、临时验证 | 会被 ps、异常堆栈、子进程泄露,不构成安全边界 |
| K8s Secret | 已上 K8s 的小团队 | 本质是 base64,必须开启 etcd 静态加密与 RBAC |
| SOPS + age/KMS | GitOps 流程、配置即代码 | 加密文件可进 Git,但要管控解密密钥 |
| 云 Secrets Manager | 单云为主、想少运维 | 计费按调用量,注意 SDK 缓存 |
| HashiCorp Vault | 多云、需要动态凭证与细粒度审计 | 自身是高可用组件,需要认真运维 |
选型原则:能用托管就不自建,能用短期就不用长期。如果团队只有三五个人,云厂商的 Secrets Manager 加 OIDC 已经足够;只有在需要动态数据库凭证、PKI 签发这类能力时,再上 Vault。
五、第 4 步:应用侧接入——统一配置加载层
最容易失败的地方不是后端选型,而是每个服务各写一套读取逻辑。正确做法是提供一个统一的加载层,并遵守四条规则:
- 单一入口:业务代码只调用
settings.xxx,不直接 import SDK; - 启动即失败:必需密钥缺失时立刻退出,不要留默认值;
- 不落盘、不打印:日志和异常信息里对敏感字段脱敏;
- 支持刷新:凭证轮换后能热加载,不用重启。
python
settings.py
import os from functools import lru_cache from pydantic_settings import BaseSettings
class Settings(BaseSettings): db_password: str stripe_api_key: str log_level: str = "INFO"
class Config:
env_prefix = "APP_"
@lru_cache def get_settings() -> Settings: settings = Settings() # 缺失即抛异常,进程启动失败 assert settings.db_password, "DB_PASSWORD 未注入" return settings
def masked(value: str) -> str: return value[:3] + "***" if value else ""
启动脚本里从密钥后端拉取并注入进程环境,绝不写回磁盘:
bash #!/usr/bin/env bash set -euo pipefail
export APP_DB_PASSWORD="$(vault kv get -field=password secret/order-api/db)" export APP_STRIPE_API_KEY="$(vault kv get -field=key secret/order-api/stripe)"
exec "$@" # 用 exec 替换进程,环境变量随进程生命周期结束而消失
注意:环境变量只是“传输通道”,不是“存储方案”。它的安全边界来自进程隔离,而不是保密性。
六、第 5 步:用动态凭证替代静态密码
静态密码最大的问题是它永远有效。动态凭证的思路是:每次启动申请一个带 TTL 的账号,到期自动失效。
以 Vault 的数据库引擎为例:
bash vault secrets enable database
vault write database/config/order-pg
plugin_name=postgresql-database-plugin
connection_url="postgresql://{{username}}:{{password}}@pg.internal:5432/orders"
allowed_roles="order-api"
username="vault_admin" password="$BOOTSTRAP_PW"
vault write database/roles/order-api
db_name=order-pg
creation_statements="CREATE ROLE "{{name}}" LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';
GRANT SELECT,INSERT,UPDATE ON ALL TABLES IN SCHEMA public TO "{{name}}";"
default_ttl="1h" max_ttl="24h"
应用侧拿到的用户名形如 v-token-order-api-a1b2c3,一小时后再也不存在。好处是:即使凭证泄露,攻击窗口也只有一小时,而且你能从数据库侧精确知道是哪个实例、哪个租约泄露的。
对于无法改造的老系统,退一步用“定期轮换 + 双凭证滚动”也能拿到 80% 的收益:新密码写入密钥后端,应用每 30 天重新读取一次。
七、第 6 步:把它接进 CI/CD,消灭长期凭证
CI 是密钥泄露的重灾区——因为流水线需要权限,而历史做法是把长期 AK/SK 塞进 Repository Secret。一旦仓库或 runner 被攻破,攻击者拿到的是永久凭证。
正确做法是用 OIDC 联邦换取短期令牌:
yaml
.github/workflows/deploy.yml
permissions: id-token: write contents: read
jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789012:role/gh-deploy-order-api aws-region: ap-northeast-1 - run: ./scripts/deploy.sh
关键点在信任策略里限制 sub 声明,只允许特定仓库的特定分支:
{ "Condition": { "StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com", "token.actions.githubusercontent.com:sub": "repo:acme/order-api:ref:refs/heads/main" } } }
同时给流水线做权限收敛:部署任务只拿到“推送镜像 + 更新服务”的最小权限,不要给管理员策略。
八、第 7 步:防泄漏——扫描、审计、告警
提交前拦截。 用 gitleaks 做 pre-commit 钩子,成本最低、收益最高:
bash
.pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.0
hooks:
- id: gitleaks
CI 中兜底。 在流水线里跑一次全量扫描,命中即失败:
bash
扫描最近一次提交,命中返回码非 0
gitleaks detect --source . --redact --exit-code 1
运行时审计。 打开密钥后端的审计日志,只对“非常规时间、非常规来源、批量读取”三类行为告警。全量告警等于没有告警。
定期复盘。 每月抽一次清单,看 rotate: manual 的条目还剩几条。这个数字比任何安全评分都真实。
九、第 8 步:泄露之后的三十分钟
预案比工具重要。发现密钥外泄后,按这个顺序动手,不要边查边等:
- 0–5 分钟:在密钥后端吊销凭证,同时确认业务是否有降级方案;
- 5–10 分钟:拉取该凭证最近 30 天的访问日志,定位使用来源 IP 与 User-Agent;
- 10–20 分钟:签发新凭证并滚动发布,确认所有实例已切换;
- 20–30 分钟:排查同一仓库、同一开发机上的其他凭证,横向扩散通常比想象中严重;
- 事后:把这次事件写进清单,补上对应的自动化检测项。
有两件事容易被漏掉:一是聊天记录和工单系统里的密钥同样属于泄露面;二是备份和快照里的旧配置不会随仓库清理而消失。
十、验收清单
改造完成后,逐条核对:
- [ ] 所有 L2 及以上配置项都已登记在
secrets-inventory.yaml中 - [ ] 主分支全量扫描
gitleaks零命中 - [ ] 代码中不存在任何硬编码口令(含测试文件与注释)
- [ ] 应用启动时缺少密钥会立即失败,而不是用默认值继续跑
- [ ] 日志、异常堆栈、健康检查接口中不出现敏感字段
- [ ] CI 使用 OIDC 或等价机制,仓库中无长期 AK/SK
- [ ] 至少 50% 的 L2 凭证已改为动态或 90 天内自动轮换
- [ ] 密钥后端审计日志已接入告警,且有明确的响应人
- [ ] 泄露应急预案演练过一次,知道谁在什么时间做什么
写在最后
敏感配置治理的难点从来不是技术。密钥管理系统、OIDC 联邦、动态凭证,这些方案都已经非常成熟,文档齐全、开箱即用。真正的难点是把“临时先这样”变成“这次就改掉”。
一个务实的推进方式是:不要发起“密钥治理专项”,而是从下一次需求开始,凡是新增一个凭证,就必须走密钥后端;凡是碰到的老代码,就顺手把那一处改掉。半年之后回头看,清单里 rotate: manual 的条目会悄无声息地归零。
密钥不该躺在配置文件里。它应该躺在一个你随时能吊销、随时能审计、随时能证明“谁在什么时候用过它”的地方。