Gitleaks:把 Git 凭据扫描做成可解释的门禁
Gitleaks 最适合解决一个边界清楚的问题:在目录、标准输入或 Git 提交历史里,按可审计规则寻找疑似凭据,并用稳定的报告与退出码把结果交给本机或 CI。它不会替团队判断凭据是否仍然有效,也不会替凭据平台完成撤销。命中后的第一动作始终是止血,而不是先把那一行从仓库里删掉。
本文使用 Gitleaks 8.30.1 Release 作为可复现实验基线。版本号只是本文验证过的坐标,不是永久推荐;升级时要拿同一组正反 fixture 比较规则覆盖、Fingerprint 和报告字段,不能只看新版本能否启动。
安装时先验证发布物
官方 Release 同时发布压缩包和 checksums。Linux x64 可以在临时目录中核对摘要,再把单个二进制放入受控工具目录:
set -euo pipefail
GITLEAKS_VERSION=8.30.1
ASSET="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
BASE="https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}"
work_dir="$(mktemp -d)"
cd "$work_dir"
curl -fLO "${BASE}/${ASSET}"
curl -fLO "${BASE}/gitleaks_${GITLEAKS_VERSION}_checksums.txt"
grep " ${ASSET}$" "gitleaks_${GITLEAKS_VERSION}_checksums.txt" | sha256sum -c -
tar -xzf "$ASSET" gitleaks
install -m 0755 gitleaks "$HOME/.local/bin/gitleaks"
gitleaks versionCI 镜像同样应固定 digest,Runner 不应每次从 latest 下载未知二进制。工具二进制、规则文件和 CI 包装脚本是一组发布单元,其中任何一项变化都可能改变门禁结论。
用无害规则证明扫描链路
不要拿形似真实厂商密钥的字符串测试扫描器。更稳妥的做法是定义一个只匹配项目 fixture 前缀的本地规则,让命中值从设计上不可能被第三方服务接受:
set -euo pipefail
LAB_DIR="$(mktemp -d)"
git -C "$LAB_DIR" init -q
git -C "$LAB_DIR" config user.name "Security Fixture"
git -C "$LAB_DIR" config user.email "fixture@example.invalid"
cat > "$LAB_DIR/.gitleaks.toml" <<'TOML'
title = "harmless local fixture"
[[rules]]
id = "example-noncredential"
description = "synthetic scanner fixture"
regex = '''EXAMPLE-NOT-A-CREDENTIAL-[A-Z0-9]{16}'''
secretGroup = 0
tags = ["fixture"]
TOML
printf 'mode = "clean"\n' > "$LAB_DIR/app.conf"
git -C "$LAB_DIR" add .
git -C "$LAB_DIR" commit -qm clean
gitleaks git "$LAB_DIR" --config "$LAB_DIR/.gitleaks.toml" --no-banner干净提交应返回 0。随后加入合成值,要求命中使用专用退出码 23,并确认脱敏报告确实落盘:
printf 'token = "EXAMPLE-NOT-A-CREDENTIAL-ABCDEF0123456789"\n' >> "$LAB_DIR/app.conf"
git -C "$LAB_DIR" add app.conf
git -C "$LAB_DIR" commit -qm synthetic-fixture
set +e
gitleaks git "$LAB_DIR" \
--config "$LAB_DIR/.gitleaks.toml" \
--exit-code 23 \
--redact=100 \
--report-format json \
--report-path "$LAB_DIR/gitleaks.json" \
--no-banner
scanner_rc=$?
set -e
test "$scanner_rc" -eq 23
test -s "$LAB_DIR/gitleaks.json"这组实验同时验证了 Git 读取、规则加载、命中、报告和退出状态。只跑 gitleaks version 只能证明二进制存在,不能证明默认规则没有失效、目标历史没有被浅克隆截断,也不能证明包装脚本没有吞掉非零状态。
git 与 dir 不是同一种覆盖
gitleaks dir 检查当前文件内容,适合工作树、解包制品和生成目录;gitleaks git 通过 Git 历史寻找已经从当前分支删除、但仍留在提交对象中的值。PR 门禁为了速度可以限制提交区间,默认分支和周期任务仍需承担完整历史扫描。
浅克隆是最常见的假绿来源。CI 必须记录 fetch depth、当前 revision、merge base 和传给 Git 的范围;如果 MERGE_BASE 不存在,应明确失败或先补齐历史,不能悄悄退化成只扫工作树。大型仓库还要把 LFS、子模块、压缩包和构建制品是否进入覆盖写进扫描合同。
规则配置要保留默认能力
组织规则通常是在默认规则上增加内部令牌格式,而不是把默认配置整份替换掉。配置的选择来源必须明确,CI 应打印配置文件路径或摘要,避免本机读取 .gitleaks.toml、Runner 却读取环境变量指定的另一份配置。
规则中的 regex 负责生成候选,secretGroup 决定报告中哪一段被视为秘密,path 缩小文件位置,entropy 过滤低随机度字符串。规则变更至少需要一个应命中的 fixture 和一个相似但不应命中的反例。规则 ID 一旦进入报告、忽略文件和工单,就不应在没有迁移方案时随意改名。
Allowlist 必须尽量落到具体 Fingerprint、commit、path 或窄正则,并附 owner、理由和到期时间。把 test/、docs/ 或整个锁文件排除,会让扫描器在恰好最容易复制示例密钥的位置失明。无害测试值可以通过专用规则和明确注释处理,不要为了让测试绿灯扩大生产盲区。
Fingerprint 与 baseline 的真实含义
Gitleaks 报告中的 Fingerprint 用于识别某个规则在某个位置发现的实例。它能帮助增量治理,却不是凭据安全状态。文件移动、提交重写、规则 ID 或 secret group 变化都可能让 Fingerprint 改变,所以升级前后要比较报告,而不是重新生成一份 baseline 后宣布存量消失。
旧仓库可以先形成受控 baseline:
gitleaks git . \
--redact=100 \
--report-format json \
--report-path security/gitleaks-baseline.json
gitleaks git . \
--baseline-path security/gitleaks-baseline.json \
--redact=100 \
--report-format json \
--report-path artifacts/security/gitleaks-new.jsonBaseline 表示“这些旧发现暂时不阻断新增变更”,不表示“这些值已经确认无效”。每条记录仍应有调查状态、凭据 owner、撤销证据和清零期限。若 baseline 文件进入仓库,还要评估其中的路径、作者、提交和规则元数据是否泄露系统结构。
CI 要区分命中和执行错误
一个可靠的包装脚本不会用 || true 把所有失败抹平:
set +e
gitleaks git . \
--log-opts="${MERGE_BASE}..HEAD" \
--redact=100 \
--report-format sarif \
--report-path artifacts/security/gitleaks.sarif \
--exit-code 23 --no-banner
scanner_rc=$?
set -e
case "$scanner_rc" in
0) printf 'no new finding\n' ;;
23) printf 'secret candidate found\n' >&2; exit 23 ;;
*) printf 'scanner execution failed: %s\n' "$scanner_rc" >&2; exit "$scanner_rc" ;;
esac报告上传应在成功和失败路径都能运行,但不得把 JSON 或 SARIF 原文直接回显到公共日志。即使开启 --redact=100,文件名、commit、作者和内部路径仍可能敏感;制品库需要最小读取权限、访问审计与短保留期。
命中后先处理凭据生命周期
发现真实凭据时,先在凭据所属平台撤销或禁用,再签发替代凭据并迁移消费者,然后检查访问日志、派生会话和缓存,最后才决定是否重写 Git 历史。历史清理会改变 commit ID,也无法证明 Fork、镜像、CI 缓存和开发者本机中的副本全部消失。
扫描平台只需要读取源代码和历史,不需要云资源写权限。公开 Fork PR 不应获得生产 Secret,扫描报告也不应被所有构建参与者下载。安全团队维护规则与事件分级,平台团队维护二进制、Runner 和报告边界,应用 owner 负责撤销、轮换和消费方迁移;没有这条责任链,扫描器只会留下没人处理的红灯。
故障、升级与回滚
PR 绿而周期扫描命中,先看 fetch depth 与提交区间。升级后告警暴增,比较规则 ID、解码深度、归档支持和配置摘要。报告为空但进程非零,应按执行错误处理,不要归类为“无泄漏”。本机命中而 CI 不命中,优先核对版本、配置优先级、工作目录和被扫 revision。
回滚要同时恢复上一版二进制、规则与 baseline,并保留新旧报告差异。扩大 allowlist、降低熵阈值或重新生成 baseline 只是让数字变小,不是回滚。实验目录清理前先验证它确实是当前流程创建的临时 Git 仓库,再限制在临时路径内删除;生产仓库与共享缓存绝不能成为清理命令的模糊目标。
