希曼日记

GitOps 篇 01:用 Gitleaks 阻止凭据进入 Git 历史

约 8 分钟2,940 字

本文是全栈电商微服务从 0 到 1 再到 10的 GitOps 系列第 1 篇。上一篇:基础设施篇:Reloader 让 Secret 变更自动滚动 Pod;下一篇(TODO):GitOps 篇 02——用 Trivy fs/config 检查依赖与 IaC 配置。

GitOps 把 Git 中的声明作为部署依据。这个机制提高了可追溯性,也放大了凭据误提交的后果:密码、Token 或私钥一旦进入提交历史,即使后来删除当前文件,旧提交、开发者克隆、CI 缓存和镜像仍可能保留副本。

Gitleaks 是这条 GitOps 流程的第一道门。它在代码构建之前检查敏感信息,不允许「先提交、再删除」成为日常处理方式。

这个系列如何组织

本系列不按工具品牌罗列功能,而是按一次变更从代码到集群的顺序推进。每篇只回答一个阶段的问题,并把输出交给下一阶段:

Mermaid
flowchart LR
A[开发者提交变更] --> B[Gitleaks<br/>阻止凭据进入 Git]
B --> C[Trivy fs/config<br/>检查依赖与 IaC]
C --> D[zizmor<br/>检查 GitHub Actions]
D --> E[Buildx<br/>构建不可变镜像]
E --> F[Syft<br/>生成 SBOM]
F --> G[Trivy image<br/>扫描镜像漏洞]
G --> H[Cosign<br/>签名与证明]
H --> I[Kyverno<br/>准入验证镜像]
I --> J[Argo CD<br/>持续协调目标状态]
顺序文章解决的问题阶段输出
01Gitleaks变更是否泄露凭据不含新增凭据的提交
02Trivy fs/config依赖和 IaC 是否存在高风险问题通过配置门禁的源码
03zizmorGitHub Actions 是否存在危险写法可信的流水线定义
04Syft镜像包含什么SPDX SBOM
05Trivy image最终镜像是否包含可修复的高危漏洞通过漏洞门禁的 digest
06Cosign谁对镜像和 SBOM 负责签名与 attestation
07Kyverno集群是否只接受满足策略的镜像准入结果和 PolicyReport
08GitOps 自动化收口Git 如何成为部署真相源GitHub CI → 清单 → Argo CD 的闭环
TODOGitLab 单独篇GitLab 门禁如何与 GitHub 发布职责分离不重复发布权的第二远端门禁

这里需要区分两个容易混用的词:

  • CI(持续集成):验证代码、工作流和制品,失败时阻止变更继续前进。
  • GitOps:以 Git 中的声明为目标状态,由控制器持续协调实际状态。CI 可以更新声明,但不应绕过 Git 直接把临时状态当作最终状态。

Gitleaks 属于 CI 的源码门禁,但它保护的是 GitOps 的真相源,因此放在系列第一篇。

Gitleaks 检查什么

Gitleaks 扫描文件内容和 Git 提交,识别疑似敏感信息,例如:

  • 云厂商 Access Key;
  • GitHub、GitLab、npm 等平台的 Token;
  • 数据库连接串中的用户名和密码;
  • OAuth client_secret;
  • JWT 和长期 Bearer Token;
  • PEM、RSA 等私钥;
  • Webhook Secret;
  • 项目自定义格式的短弱口令。

它主要依赖规则、关键词、正则表达式和熵值判断。默认规则适合通用 Token,但项目仍要补充自己的泄露形态。

例如,通用规则不一定能识别下面这种连接串里的短口令:

Text
postgresql://postgres:weak-password@postgres.example.internal/ecommerce

因此当前电商项目在 .gitleaks.toml 中增加了 url-embedded-credential 和 config-password-assignment 两类规则,覆盖 URL 内嵌凭据,以及 YAML、TOML、Markdown、HTTP、SQL、Shell 等文件中的明文密码赋值。

为什么删除文件还不够

假设第一次提交包含真实 Token:

Text
A -- B(secret) -- C(remove secret)

分支最新状态 C 已经看不到 Token,但 B 仍然是 Git 历史的一部分。任何能读取 B 的人都可能取得原始值。

因此处置泄露必须分成两件事:

  1. 处置凭据:立即撤销或轮换凭据,并检查使用日志。
  2. 处置仓库:删除当前文件中的凭据;必要时重写历史并协调所有克隆重新同步。

Gitleaks 只能发现风险,不能撤销 Token,也不能证明泄露后的凭据没有被使用。只删除文件、只添加 .gitignore 或只修改最新提交,都不能代替密钥轮换。

当前电商项目的三层门禁

当前项目把同一份 .gitleaks.toml 接到三个位置。规则只有一份,扫描范围根据阶段变化:

Mermaid
flowchart TB
Config[.gitleaks.toml<br/>规则与 allowlist 真相源]
Config --> Local[pre-commit<br/>扫描暂存区]
Config --> Verify[verify-quick<br/>扫描完整历史]
Config --> GitHub[GitHub context-gate<br/>扫描完整历史]
Local --> Commit[允许创建提交]
Verify --> Push[允许继续本地验证]
GitHub --> Protected[持续证明远端历史仍然干净]

第一层:提交前扫描暂存区

frontend/.vite-hooks/pre-commit 在创建提交前执行:

Shell
gitleaks git "$root" \
-c "$root/.gitleaks.toml" \
--pre-commit \
--staged \
--no-banner \
--redact=80

这一层只检查即将进入提交的内容,反馈最快。项目采用失败即关闭(fail closed):如果没有安装 Gitleaks,钩子直接拒绝提交,而不是静默跳过。

本地安装:

Shell
brew install gitleaks

手动检查暂存区:

Shell
gitleaks git . -c .gitleaks.toml --pre-commit --staged --no-banner --redact=80

预期结果:命令退出码为 0,然后才允许提交。如果命令报告疑似凭据,先判断是真实凭据还是误报,不要直接绕过。

第二层:本地验证完整历史

scripts/verify-quick.sh 执行:

Shell
gitleaks git . -c .gitleaks.toml --no-banner --redact=80

这一层检查整个可见 Git 历史。它用于发现暂存区扫描无法覆盖的问题,例如旧提交中的凭据,或者开发者绕过本地钩子后留下的记录。

扫描完整历史会比暂存区扫描慢,但它回答了更强的问题:当前仓库的可见历史是否仍然不包含规则命中的凭据?

第三层:GitHub Actions 持续证明完整历史

GitHub 的 context-gate.yml 使用完整克隆:

YAML
- name: Checkout repository (full history)
uses: actions/checkout@v5
with:
fetch-depth: 0

然后安装固定版本、校验发布物 SHA-256,再执行:

Shell
gitleaks git . -c .gitleaks.toml --no-banner --redact=80

fetch-depth: 0 不能省略。浅克隆只包含少量提交,即使命令显示扫描通过,也不能证明完整历史干净。

当前 workflow 还从输出中读取 N commits scanned,并断言扫描数量大于项目设置的最低值。这个断言防止流水线被误改成只扫描最新提交后继续显示绿色。

发布 tag 的供应链 workflow 采用另一种范围:从上一个语义化版本 tag 扫到当前提交。它检查本次发布区间是否引入泄露;context-gate.yml 则负责持续证明完整历史。两种扫描不是重复,而是回答两个不同的问题。

为什么不用“没安装就跳过”

安全门禁常见的错误写法是:

Shell
command -v gitleaks >/dev/null || exit 0

这个脚本在工具缺失时仍然返回成功,CI 看起来是绿色,但实际上没有发生扫描。

当前项目采用相反规则:

Shell
command -v gitleaks >/dev/null 2>&1 || {
echo "缺少 gitleaks,凭据门禁无法执行" >&2
exit 1
}

门禁成功必须表示「扫描已经执行并且没有发现阻断项」,不能只表示「脚本没有崩溃」。GitHub Actions 还固定 Gitleaks 版本并校验下载文件,避免流水线每次运行时取得不同的工具行为。

电商项目为什么需要自定义规则

电商系统连接的外部系统多,凭据类型也多:数据库、支付渠道、对象存储、身份系统、消息系统和镜像仓库都可能有单独的密钥。

当前项目的 .gitleaks.toml 在默认规则上增加项目规则,并显式扫描这些容易被忽略的文件类型:

TOML
path = '''(?i)\.(ya?ml|toml|env|ini|json|properties|conf|md|http|sql|sh|txt)$'''

这一步很重要,因为凭据不只会出现在 .env:

  • Markdown 排障记录可能粘贴真实密码;
  • .http 请求样例可能保存 Bearer Token;
  • sqlc.yaml 可能内嵌数据库连接串;
  • Shell 脚本可能写死访问密钥;
  • YAML 可能包含未替换的 Secret 值;
  • base64 文本可能包装整份配置,但 base64 不是加密。

规则应来自项目实际出现过的泄露形态,不能只依赖通用的“高熵字符串”判断。

allowlist 应该怎么写

Gitleaks 会产生误报,例如测试占位符、公开默认值、类型声明和 Secret 引用路径。正确做法是只放行已经确认不是凭据的具体内容。

当前项目使用带说明的 allowlist,例如:

TOML
[[allowlists]]
description = "占位符与已脱敏标记"
regexes = [
'''\*\*\*REMOVED-[A-Z-]+\*\*\*''',
'''YOUR_JWT_TOKEN''',
]

推荐顺序:

  1. 确认命中的值是否真实有效。
  2. 如果是真实凭据,先轮换,再从代码中移除。
  3. 如果是误报,收窄到具体路径、规则或正则。
  4. 在 description 中说明为什么可以放行。
  5. 重新扫描完整历史,确认没有放宽其他风险。

不要使用下面这些方式“修绿”:

  • 放行整个配置目录;
  • 为真实凭据添加 allowlist;
  • 全局关闭默认规则;
  • 永久设置 SKIP_GITLEAKS=1;
  • 把明文改成 base64 后继续提交。

allowlist 是有证据的例外,不是忽略告警的垃圾桶。

凭据应该放在哪里

Gitleaks 只负责防止凭据进入 Git,不负责保存凭据。电商项目需要按使用位置选择存储方式:

使用位置推荐位置Git 中保存什么
GitHub ActionsGitHub Actions Secrets 或 OIDCSecret 名称、最小权限说明、非敏感配置
Kubernetes 工作负载OpenBao/Vault + External Secrets Operator,或受控的 Kubernetes Secret 流程ExternalSecret 引用,不保存真实值
本地开发被 .gitignore 排除的本地配置或密码管理器示例文件和占位符
应用运行时Config Center 或专用 Secret 管理系统配置键与读取规则,不保存明文凭据

/Users/lens/lens077/kubernetes/components 提供面向通用集群的组件部署,其中 OpenBao 和 External Secrets Operator 可作为集群侧凭据来源。Gitleaks 本身不需要部署到 Kubernetes:它应在凭据进入 Git 之前运行。

后续 GitOps 文章会为电商项目单独给出 ExternalSecret、命名空间、镜像仓库和准入策略配置,不直接复制通用组件目录中的示例值。

发现泄露后的处理顺序

如果扫描命中真实凭据,先停止发布。不要为了继续流水线而添加 allowlist。

  1. 撤销或轮换已暴露的凭据。
  2. 检查提供方日志,确认凭据是否被使用。
  3. 从当前代码和配置中移除真实值。
  4. 将凭据迁移到正确的 Secret 管理位置。
  5. 判断是否需要重写 Git 历史。
  6. 如果重写历史,通知所有协作者重新同步仓库。
  7. 运行完整历史扫描。
  8. 补充规则或负例,防止同类泄露再次发生。

git filter-repo 可以重写历史,但它不能让已经被第三方读取的密钥失效。密钥轮换必须先于“仓库看起来干净”。

GitLab 的位置(TODO)

本系列以 GitHub Actions 为主要 CI 和发布链。GitLab 作为第二远端,只承担代码门禁,不构建、签名、推送镜像或回写部署清单,避免同一个发布 tag 被两个平台写入两次。

GitLab 的 Gitleaks 配置将在单独文章中处理。该篇需要解释:

  • 如何复用 .gitleaks.toml;
  • 如何完整拉取 Git 历史;
  • 如何固定工具版本;
  • 如何与 GitHub 保持同一道读取门禁;
  • 为什么 GitLab 不取得发布权。

本篇不展开 GitLab YAML,以免把 GitHub 主流程和第二远端职责混在一起。

本篇验收清单

完成这一篇对应的工程动作后,应能回答:

  • 暂存区中的真实 Token 会让提交失败。
  • 未安装 Gitleaks 时,本地门禁失败,而不是跳过。
  • .gitleaks.toml 是本地和 GitHub 共用的规则真相源。
  • GitHub 使用完整历史检出,而不是浅克隆。
  • CI 能证明扫描范围没有退化成单个提交。
  • 每个 allowlist 都有具体对象和放行理由。
  • 真实凭据存放在 Secret 管理系统,不在 Git 中。
  • 泄露处置先轮换凭据,再处理仓库历史。

通过这些检查后,GitOps 的真相源才具备进入下一道门的资格。

下一篇(TODO)将使用 Trivy fs/config 检查源码依赖和 Kubernetes、Dockerfile 等 IaC 配置。Gitleaks 回答「有没有把秘密交给 Git」,Trivy fs/config 接着回答「准备交给构建和集群的配置是否安全」。

相关文章