密钥不该躺在配置文件里:敏感配置治理实战

密钥不该躺在配置文件里:敏感配置治理实战

很多安全事故的起点并不高明:它就是一行躺在 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

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 步:应用侧接入——统一配置加载层

最容易失败的地方不是后端选型,而是每个服务各写一套读取逻辑。正确做法是提供一个统一的加载层,并遵守四条规则:

  1. 单一入口:业务代码只调用 settings.xxx,不直接 import SDK;
  2. 启动即失败:必需密钥缺失时立刻退出,不要留默认值;
  3. 不落盘、不打印:日志和异常信息里对敏感字段脱敏;
  4. 支持刷新:凭证轮换后能热加载,不用重启。

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:

CI 中兜底。 在流水线里跑一次全量扫描,命中即失败:

bash

扫描最近一次提交,命中返回码非 0

gitleaks detect --source . --redact --exit-code 1

运行时审计。 打开密钥后端的审计日志,只对“非常规时间、非常规来源、批量读取”三类行为告警。全量告警等于没有告警。

定期复盘。 每月抽一次清单,看 rotate: manual 的条目还剩几条。这个数字比任何安全评分都真实。

九、第 8 步:泄露之后的三十分钟

预案比工具重要。发现密钥外泄后,按这个顺序动手,不要边查边等:

  1. 0–5 分钟:在密钥后端吊销凭证,同时确认业务是否有降级方案;
  2. 5–10 分钟:拉取该凭证最近 30 天的访问日志,定位使用来源 IP 与 User-Agent;
  3. 10–20 分钟:签发新凭证并滚动发布,确认所有实例已切换;
  4. 20–30 分钟:排查同一仓库、同一开发机上的其他凭证,横向扩散通常比想象中严重;
  5. 事后:把这次事件写进清单,补上对应的自动化检测项。

有两件事容易被漏掉:一是聊天记录和工单系统里的密钥同样属于泄露面;二是备份和快照里的旧配置不会随仓库清理而消失。

十、验收清单

改造完成后,逐条核对:

写在最后

敏感配置治理的难点从来不是技术。密钥管理系统、OIDC 联邦、动态凭证,这些方案都已经非常成熟,文档齐全、开箱即用。真正的难点是把“临时先这样”变成“这次就改掉”。

一个务实的推进方式是:不要发起“密钥治理专项”,而是从下一次需求开始,凡是新增一个凭证,就必须走密钥后端;凡是碰到的老代码,就顺手把那一处改掉。半年之后回头看,清单里 rotate: manual 的条目会悄无声息地归零。

密钥不该躺在配置文件里。它应该躺在一个你随时能吊销、随时能审计、随时能证明“谁在什么时候用过它”的地方。