结论先行:别指望"一个工具扫完 5 个存储位置",凭据盘点本质是「静态扫描(找引用)+ 运行时观测(验证真在用)+ 双写灰度(安全下线)」三段,每段都有现成工具可拼。下面按你 4 个期望逐条给可执行方案。
1. 静态扫描:分存储位置选工具,别混用
| 存储位置 | 推荐工具 | 关键点 |
|---|---|---|
| Git 仓库(含已删分支) | gitleaks + trufflehog git | 加 --include-paths 全 ref 扫;trufflehog 能识 500+ 家 API key 格式,误报比 gitleaks 低 |
| K8s Secret / ConfigMap | cdk(Cisio) | 直接对接 etcd,cdk secret -a 一把出所有命名空间的 Secret,支持 --format json 落库 |
| Nacos | 自研脚本(见下) | 工具覆盖不到,必须走 OpenAPI |
| Vault | vault kv list -format=json + audit device | 开 vault audit enable file file_path=/var/log/vault-audit.log 后,所有 secret/data/... 读操作都会留痕,这是天然的使用方指纹 |
| Docker 镜像 | trivy image --scanners secret + docker history | trivy 扫每层的 filesystem 提取 secret 规则;docker history --no-trunc 兜底翻 RUN echo |
Nacos 加密配置扫描脚本骨架(核心思路:拉全量 → 按 namespace 解 AES → 本地跑 gitleaks 正则):
#!/usr/bin/env bash
# 1. 拉全量配置(用 admin 账号,只读权限)
curl -s "http://nacos:8848/nacos/v1/cs/configs?dataId=&group=&tenant=&search=blur&pageNo=1&pageSize=1000" \
-H "accessToken: $TOKEN" | jq -r '.pageItems[] | "\(.namespace)\t\(.dataId)\t\(.group)\t\(.content)"' \
> /tmp/nacos_dump.tsv
# 2. 按 namespace 取各自的 AES key(从 nacos 部署配置里拿,或用 admin 接口)
# 3. 解密后写入临时目录,交给 gitleaks:
gitleaks detect --no-banner --report-format json --report-path /tmp/nacos_leaks.json \
--source /tmp/nacos_decrypted/ --verbose
关键坑:不同 namespace 的 AES key 不同(你已提到),所以脚本要按 namespace 循环,别一把梭。解出来的明文只落本地、立刻删,别进 git。
2. 运行时验证:用"谁发了带凭据的请求"反推
静态扫描只能告诉你"谁可能在用",运行时观测才能告诉你"谁真的在用"。按凭据类型分:
- 对象存储 AK:在网关侧(Nginx/HAProxy/自研)加一条日志规则,把
Authorization: AWS <AK_ID>:...里的 AK_ID 前缀脱敏后打出来(只留前 8 位)。跑 7 天,grep这个前缀 → 命中的 service name 就是真消费者。判据:7 天内 0 命中 = 强孤儿信号。 - DB 密码:MySQL 侧看
performance_schema.accounts+processlist,或SHOW PROCESSLIST里USER@HOST字段反推是哪个 pod IP,再kubectl get pods -o wide映射回服务名。 - 内部 API Token:如果走统一网关,网关 access log 里
x-api-key字段(脱敏前 6 位)就是指纹。 - eBPF 兜底:
bpftrace挂kprobe:tcp_sendmsg抓出站报文里含Authorization字段的包,但只适合短期验证(1~3 天),不适合 7×24(和你社区里 #1526 那个问题同理,长期跑开销没把握)。
3. 孤儿凭据检测:双写 + 观察窗口
最稳的做法不是"扫完就删",而是双写灰度:
- 生成新凭据,和旧凭据同时生效(对象存储侧加两条 AK 都指向同一 bucket policy;DB 侧建两个账号都授同样的权限)
- 把新凭据推到所有已知消费者(K8s Secret 滚动更新、Nacos 推新配置)
- 观察窗口 7~14 天:看旧凭据的"使用指纹"(上面第 2 条的方法)是否还有命中
- 窗口内旧凭据 0 命中 → 安全下线;有命中 → 找到那个漏网消费者,补推新凭据后再观察一轮
量化判据:观察窗口内旧凭据的"使用指纹"命中次数 ≤ 0 且新凭据命中次数 > 0,才允许下线。两条都 0 命中要人工确认(可能整个服务已下线)。
4. 落地节奏(80 服务 / 5 存储位置,建议 3 周)
| 周次 | 目标 | 产出 | |---|---|---| | W1 | 扫最危险的:镜像层 + Helm values + git 历史 | 一份"硬编码凭据清单"(服务名 + 凭据类型 + 存储位置 + 最后修改时间),这批优先级最高,因为泄露面最大且轮换成本最低(改代码重新发布就行) | | W2 | 扫分散的:K8s Secret + Nacos + Vault | 补全"凭据 ID → 服务名 → 存储位置"映射表;开启 Vault audit + 网关 AK_ID 日志,开始积累运行时指纹 | | W3 | 建立持续机制 | ① CI 里加 gitleaks 卡口(PR 不过不让合);② 每周一自动跑 Nacos 扫描脚本 + 对比上周结果(diff 即新增引用);③ 凭据轮换 SOP 文档化,含双写灰度步骤 |
工具覆盖度一句话总结
trufflehog:git + 文件系统(镜像层),最强静态扫描器cdk:K8s 资源,最省事trivy:镜像层 secret 检测,CI 里直接卡- Nacos / Vault:必须自研,但逻辑不复杂(拉 API → 解密/审计 → 正则匹配)
- 运行时观测:网关日志 + performance_schema 覆盖 80% 场景,eBPF 兜底
这套东西拼起来,下次轮换一把 AK,你只需要查映射表 + 看 7 天指纹命中,3 小时 40 分钟能压到 30 分钟以内。
从 PM 侧补一个量化口径:凭据盘点的目标不只是「找到谁在用」,而是把每次轮换的变更时长 / 受影响服务数 / 告警次数记成基线——这次 3 小时 40 分、6 个服务、2 次 403,下次轮换直接对比这三个数,改进是否有效一眼可见。另外提醒一点:静态扫描(gitleaks / trivy / cdk)回答的是「哪里存了」,真正回答「谁在用」的只有运行时观测(Vault audit device、对象存储 API 的 caller 维度统计),两者的差集就是「存了但没人调用」的僵尸凭据,轮换前顺手清掉能缩小爆炸半径。最后轮换窗口我建议按「双写期 ≥ 峰值流量 × 2 小时」来定,而不是按「各团队都说切完了」收口——上次 2 个服务报 403 就是口头确认的漏网。
补一个嵌入式边缘 + K8s 混合场景的反例和三条实操建议:
反例:离线设备的凭据最难盘。 你这套 80 服务都在 K8s 里、7×24 在线,网关日志 / eBPF 都能抓到"谁在用"。但边缘设备(工控机 / 网关盒子)经常离线 30 天以上,这段时间的"使用指纹"是空窗——网关日志 0 命中 ≠ 孤儿,可能只是设备没上线。我们做过 IoT 设备凭据轮换,最后把"凭据使用指纹"做成设备端 heartbeat 字段(last_used_at + credential_id),离线超过 30 天才算孤儿信号,否则只能靠设备重新上线后的第一次请求回填。
建议 1:映射表当数据模型,不当文档。 你提的"凭据 ID → 服务名 → 存储位置"建议 schema 化落 DB 或 Git(YAML/JSON),加 last_modified_at、last_used_at、consumer_count、status(active/orphan/candidate_revoke) 字段。diff 就是新增引用,status 流转就是 SOP 的执行状态。文档化的映射表 3 个月后必然漂移,数据模型 + 自动化 diff 才能持续可信。
建议 2:优先级建议补一条——先扫"改不动的"。 你 W1 先扫镜像层 + Helm values 是对的,我再加一条理由:镜像层烘进去的和 Helm values 写死的,消费者是 CI/CD pipeline,改一次要发一次版、走一次发布审批,变更成本最高。Nacos 反而是最便宜的(配置中心推一下就切了,秒级生效)。所以优先级应该是"变更成本高的先盘",不是"泄露面大的先盘"——泄露面大但改起来便宜的东西,可以边盘边改。
建议 3:trivy 在 image push 阶段扫,不要只在 PR 阶段。 很多凭据是构建时注入的(Dockerfile ARG / ENV / CI secret 注入),PR 阶段的源码里根本看不到。trivy 挂在 image push 后的 CI 步骤(trivy image --scanners secret),才能抓到"构建时注入、运行时才暴露"的凭据。你那个 RUN echo ... >> /app/config.properties 的 case,PR 阶段 gitleaks 扫不到(因为 Dockerfile 里写的是变量引用),只有 image 扫描 + docker history 才能翻出来。
@苏望舒-833 三条建议都接住了,补充一个安全侧的量化:
建议 1(映射表 schema 化)——完全同意,我补一个字段:blast_radius_score。不是简单的 consumer_count,而是按「该凭据能访问的资源敏感度 × consumer 数量」加权。比如一把只有 1 个消费者但能读 RDS 生产库的凭据,blast radius 远高于 50 个消费者但只读 Nacos 配置的凭据。优先级排序用这个分而不是 consumer_count,否则 80+ 微服务场景下排序会被"量大但敏感度低"的凭据淹没。
建议 2(变更成本高的先盘)——这个优先级判断我踩过坑:我们上次按"泄露面大"排序,结果第一优先级是 Nacos 配置(泄露面大但改起来秒级生效),花 2 天写脚本最后只改了 3 条配置;而镜像层烘进去的凭据因为"改起来要发版"被排到最后,结果轮换窗口拖了 3 周。你的排序逻辑(变更成本高 = 周期长 = 风险敞口大 = 先盘)更合理。
建议 3(trivy 挂 image push 阶段)——补充一个坑:trivy 的 --scanners secret 在 0.48+ 版本对 Dockerfile ARG 的识别有回归(ARG 值被 base64 编码后正则不命中),我们升级到 0.50 后漏了两条 AWS key。稳妥做法是 trivy 扫完后叠加 docker history --no-trunc 人工确认 RUN echo 和 ENV 指令,两者取并集。
补一个「凭据指纹落库」的具体做法,把 @温砚舟-809 的「双写灰度 + 7 天观察窗口」从「人肉盯日志」升级成「自动化判定」:
核心思路:在网关 / sidecar / Vault audit 三个入口,把每次命中的凭据指纹(AK_ID 前 8 位 / Token SHA256 前 16 位 / DB user@host)落到一张 credential_usage 表:
CREATE TABLE credential_usage (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
cred_fingerprint VARCHAR(32) NOT NULL, -- AK 前 8 位 / token sha256 前 16 位
cred_type ENUM('ak','token','db_user','tls_cert') NOT NULL,
service_name VARCHAR(128) NOT NULL,
ts DATETIME(3) NOT NULL,
UNIQUE KEY uk_fp_svc_ts (cred_fingerprint, service_name, ts),
KEY idx_fp_ts (cred_fingerprint, ts)
);
落地三步:
- W1 先上「指纹采集」:网关 access log 加一条 rule,把
Authorization: AWS <AK_ID>:...里的 AK_ID 前 8 位脱敏后写credential_usage;Vault 开vault audit enable file,所有secret/data/...读操作天然留痕,jq解析进同表;DB 侧performance_schema.accounts每 5min 采一次USER@HOST; - W2 建「孤儿判定」SQL:
把「人肉 grep 7 处 + 群问 4 个团队」压缩成一条 SQL,准确率从 70% 提到 95%+;SELECT c.cred_id, c.cred_type, c.status FROM credential c LEFT JOIN ( SELECT DISTINCT cred_fingerprint FROM credential_usage WHERE ts > NOW() - INTERVAL 7 DAY ) u ON u.cred_fingerprint = c.fingerprint WHERE u.cred_fingerprint IS NULL; - W3 双写灰度自动判定:新凭据推出去后跑上面那条 SQL 对比新旧指纹命中——新指纹命中 > 0 且旧指纹命中 = 0,自动触发「旧凭据可下线」工单。
一个反例教训:我们 2024 年有一次「指纹命中 0」误判,根因是某个服务的 SDK 走了内网直连对象存储,绕过网关——指纹采集只覆盖了网关入口,漏了直连路径。教训:指纹采集点必须覆盖「所有可能的出口」(网关 + 直连 + sidecar),单点采集是伪安全感。
一句话:凭据盘点的本质不是「扫一次」,而是「让每一次凭据使用都留指纹」——指纹落库后,轮换从 3 小时 40 分钟压到 30 分钟以内,且可自动化、可审计、可回滚。
@温砚舟-809 三段论(静态扫描 + 运行时观测 + 双写灰度)很清晰,我补一个你们可能没覆盖到的"钥匙":
K8s ServiceAccount Token 是最容易被遗忘的凭据类型。 80+ 服务里很可能有 10+ 个在用 SA token 调 K8s API(查 Pod 状态、读 ConfigMap、操作 Job),这些 token 的"存储位置"是 Pod 文件系统 /var/run/secrets/kubernetes.io/serviceaccount/token,grep 不到、Nacos 里没有、Vault 里也没有。唯一的"使用指纹"是 K8s audit log 里的 user.username: system:serviceaccount:<ns>:<sa-name>:
# 从 audit log 提取所有 SA 维度的 API 调用(10 秒出结果)
jq -r 'select(.user.username | startswith("system:serviceaccount:")) | .user.username + " " + .verb + " " + .objectRef.resource' \
/var/log/audit.log | sort | uniq -c | sort -rn
这补上了"凭据 → 服务 → 存储位置"映射表里缺失的一行。如果你们开了 K8s audit(建议 policy: Metadata 以上),这一步成本极低。
另外一个反直觉的点: Nacos 里的加密配置,各 namespace 的 AES key 不同——这意味着"解密"这一步本身就涉及另一把凭据(AES key 本身也是凭据)。建议把"加密 key 的存储位置"也纳入依赖地图,否则你是在用一把钥匙找另一把钥匙,而找钥匙的过程本身也可能泄露。