Gitleaks、TruffleHog 与 detect-secrets:从提交拦截到凭据泄漏响应
开发者在 PR 中删除了一行云访问密钥,扫描器也恢复了绿灯,但事故并没有结束。那串字符可能仍在 Git 历史、CI 日志、构建缓存、镜像层和开发者本机里;只要它曾经离开可信边界,就必须先撤销或轮换,再谈清理历史。把“文件里找不到了”当成“凭据已经安全”,是 Secret 治理最危险的误判。
Gitleaks 擅长按规则扫描 Git 历史、目录和标准输入;TruffleHog 能从多种 Source 发现、分类并尝试验证凭据;detect-secrets 通过插件、过滤器和经过审计的 baseline 控制新增风险。三者分别偏向快速门禁、深度发现和渐进治理,组合后也仍不能替代凭据平台与事件响应。
先区分检测、验证和响应
检测回答“字符串是否符合规则或熵特征”,会有误报和漏报。验证回答“服务端是否接受这份凭据”,需要向对应 API 发请求,可能留下审计记录、触发限流,甚至暴露数据。响应则包括撤销、轮换、调查访问日志、清理副本与修复分发方式。
开发机和 PR 默认只做离线检测。真实凭据的在线验证只能在获批的隔离环境执行,并且应优先由凭据所属平台查询状态,而不是把秘密再次发送给扫描器。下面所有实验都使用明确标注的合成字符串,绝不对它们做在线验证。
安装并锁定工具来源
示例固定 Gitleaks 8.30.1、TruffleHog 3.95.9 和 detect-secrets 1.5.0。版本升级会改变规则、Fingerprint、Detector 和输出字段,必须用同一组 fixture 比较新增、消失和重分类结果。
Gitleaks Release 提供 checksums。Linux 安装时先核验资产:
set -euo pipefail
GITLEAKS_VERSION=8.30.1
ASSET="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
curl -fLO "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/${ASSET}"
curl -fLO "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/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 versionTruffleHog 不执行 main 分支上的动态安装脚本。固定 Release 资产,先用 Cosign 验证 checksums 的证书身份,再核对资产摘要,最后安装二进制:
set -euo pipefail
TRUFFLEHOG_VERSION=3.95.9
TH_ASSET="trufflehog_${TRUFFLEHOG_VERSION}_linux_amd64.tar.gz"
TH_SUMS="trufflehog_${TRUFFLEHOG_VERSION}_checksums.txt"
TH_BASE="https://github.com/trufflesecurity/trufflehog/releases/download/v${TRUFFLEHOG_VERSION}"
for file in "$TH_ASSET" "$TH_SUMS" "${TH_SUMS}.pem" "${TH_SUMS}.sig"; do
curl -fLO "${TH_BASE}/${file}"
done
cosign verify-blob "$TH_SUMS" \
--certificate "${TH_SUMS}.pem" \
--signature "${TH_SUMS}.sig" \
--certificate-identity-regexp 'https://github\.com/trufflesecurity/trufflehog/\.github/workflows/.+' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com'
grep " ${TH_ASSET}$" "$TH_SUMS" | sha256sum -c -
tar -xzf "$TH_ASSET" trufflehog
install -m 0755 trufflehog "$HOME/.local/bin/trufflehog"
trufflehog --versiondetect-secrets 是 Python 工具,使用独立虚拟环境和哈希锁定依赖,避免全局 Python 污染项目:
python3 -m venv .tools/detect-secrets
. .tools/detect-secrets/bin/activate
python -m pip install --require-hashes -r tools/detect-secrets-requirements.txt
detect-secrets --versiontools/detect-secrets-requirements.txt 由受控依赖升级任务生成,至少固定 detect-secrets==1.5.0 及其传递依赖摘要。容器运行时同样先解析 tag 的平台 digest,再在 CI 引用 image@sha256:...,不能把 latest 当版本策略。
用临时 Git 仓库验证退出码
建立一个完全独立的仓库,合成值使用 EXAMPLE-NOT-A-CREDENTIAL 前缀。规则只识别这个前缀,因此不会被任何第三方服务接受,也不需要联网验证。
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 = "local harmless fixture"
[[rules]]
id = "example-noncredential"
description = "synthetic token used only for scanner tests"
regex = '''EXAMPLE-NOT-A-CREDENTIAL-[A-Z0-9]{16}'''
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。随后加入合成值并要求 Gitleaks 以 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-leak
set +e
gitleaks git "$LAB_DIR" \
--config "$LAB_DIR/.gitleaks.toml" \
--exit-code 23 \
--report-format json \
--report-path "$LAB_DIR/gitleaks.json" \
--redact=100 --no-banner
rc=$?
set -e
test "$rc" -eq 23
test -s "$LAB_DIR/gitleaks.json"
printf 'expected finding, rc=%s\n' "$rc"这组实验同时验证规则、Git 历史入口、报告落盘和退出码。报告即使开启 redaction,也可能包含文件名、提交、作者和内部路径,只能作为受限安全制品保存。
TruffleHog 可对同一目录执行离线文件系统检测,但不要使用会向服务端验证的结果模式:
trufflehog filesystem "$LAB_DIR/app.conf" \
--results=unverified,unknown \
--no-verification --no-update --json \
> "$LAB_DIR/trufflehog.json"--no-verification 是关键护栏。TruffleHog 的结果语义中,verified 表示工具已向对应服务确认有效,unknown 表示验证尝试因网络或 API 问题失败;不能把 unknown 当作安全,也不能用 --only-verified 过滤掉所有未验证风险后声称扫描通过。
Gitleaks 的规则、基线与历史
Gitleaks 的配置优先级依次包括 --config、环境变量和目标目录下的 .gitleaks.toml。团队配置应继承默认规则,再添加组织内部格式;全量替换默认配置前必须做规则覆盖回归。
规则中的 regex 决定候选,secretGroup 决定报告和 Fingerprint 使用哪一段,path 可收窄文件位置,entropy 用于过滤低随机度候选。Allowlist 应限定 commit、path、regex 或具体 fingerprint,并写明 owner、原因、审批与到期时间。宽泛排除 test/、docs/ 或整个锁文件会形成长期盲区。
大型旧仓库第一次扫描会发现历史债务。Gitleaks baseline 是一份旧报告:
gitleaks git --redact=100 \
--report-format json \
--report-path security/gitleaks-baseline.json
gitleaks git \
--baseline-path security/gitleaks-baseline.json \
--report-format json \
--report-path artifacts/security/gitleaks-new.jsonBaseline 不是“已确认安全”的证明,只是暂时不阻断的旧发现集合。每条旧发现仍需调查、撤销和到期清零;规则或版本升级后 Fingerprint 可能变化,要用双版本报告迁移,不能直接重新生成 baseline 把新增风险吞掉。
目录扫描只看当前文件,Git 扫描还能发现已经删除但仍在历史中的值。PR 为了速度可以只扫合并基点后的提交,默认分支和周期任务必须完整扫描历史与制品。浅克隆会导致历史扫描漏报,CI 要记录 fetch depth、起止 commit 和扫描模式。
detect-secrets 的渐进治理
detect-secrets 的 baseline 同时保存插件、过滤器设置和发现结果。先扫描,再人工审计:
detect-secrets scan --no-verify > .secrets.baseline
detect-secrets audit .secrets.baseline
detect-secrets-hook --no-verify --baseline .secrets.baseline staged-file.txt插件负责发现,过滤器负责排除。正则插件适合固定格式,熵检测覆盖未知高随机字符串但误报更多,KeywordDetector 能发现看起来并不随机的硬编码密码。audit 将候选标记为真实或误报,团队应由两人复核敏感结论;baseline 里的哈希虽不是明文,文件名、规则和审计状态仍可能泄漏系统结构。
在临时仓库中还要证明 baseline 只放行旧候选,不会吞掉新候选。先对已有合成值建立 baseline,再新增另一个无效字符串并运行 Hook:
detect-secrets scan --no-verify "$LAB_DIR/app.conf" \
> "$LAB_DIR/.secrets.baseline"
printf 'password = "EXAMPLE-NOT-A-CREDENTIAL-NEWVALUE987654"\n' \
> "$LAB_DIR/new-config.ini"
set +e
detect-secrets-hook \
--no-verify \
--baseline "$LAB_DIR/.secrets.baseline" \
"$LAB_DIR/new-config.ini" \
> "$LAB_DIR/detect-secrets-hook.log" 2>&1
ds_rc=$?
set -e
test "$ds_rc" -ne 0
test -s "$LAB_DIR/detect-secrets-hook.log"
printf 'new candidate blocked, rc=%s\n' "$ds_rc"该字符串只是 KeywordDetector 的本地输入,不对应任何服务。若工具升级后实验不再命中,应检查启用插件和过滤器变化,调整 fixture 让它继续验证门禁机制,而不是换成形似真实厂商令牌的字符串。
更新 baseline 时使用同一配置重新扫描并审阅 diff。pragma: allowlist secret 只适合局部、显然无害且可审查的值;全局 --exclude-lines 和 --exclude-files 必须通过 fixture 证明不会遮住邻近真实候选。
分层接入 Git 与 CI
提交前 Hook 追求亚秒级反馈,只扫描 staged 文件;服务端 PR 门禁不可被开发者跳过,扫描完整 diff 并保存机器报告;默认分支和周期任务扫描完整历史、镜像层、对象存储或制品。紧急事件响应则使用隔离 Runner 和只读凭证扩大数据源。
一个 Gitleaks CI 步骤应显式保留退出状态:
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) echo "no new finding" ;;
23) echo "secret candidate found" >&2; exit 23 ;;
*) echo "scanner execution failed: $scanner_rc" >&2; exit "$scanner_rc" ;;
esacTruffleHog 在 CI 使用 --fail 时,发现符合结果过滤条件的凭据会返回 183。命令必须限制提交区间,并默认禁用在线验证:
trufflehog git file://. \
--since-commit "$MERGE_BASE" \
--branch HEAD \
--no-verification --no-update \
--results=unverified,unknown \
--json --fail \
> artifacts/security/trufflehog.json不要用 || true 消除 183,否则扫描器崩溃、仓库读取失败和真正命中都会变成同一个绿灯。报告上传也要在失败分支执行,但日志中不得打印 Raw 字段。
发现真实凭据时先止血
响应顺序不是“删文件、重写历史、关闭告警”。第一步在凭据平台撤销或禁用;第二步签发替代凭据并更新消费者;第三步使旧会话、缓存和派生令牌失效;第四步检查服务端审计日志,确定首次暴露、使用范围和数据影响;最后才决定是否清理 Git 历史及各类副本。
历史重写会改变 commit ID,破坏开放 PR、签名、Tag 和下游 Fork。执行前冻结写入、列出所有镜像和 Fork、通知消费者、备份审计证据;执行后强制重新克隆并扫描远端对象。即使历史被彻底删除,旧凭据仍必须保持撤销状态,因为无法证明所有副本已经消失。
TruffleHog 的在线验证尤其敏感。它可能向第三方 API 发请求并产生访问日志;某些凭据具备写权限,错误验证方式甚至可能改变状态。只允许专用隔离环境、批准的数据源、最小权限和完整审计,输出先脱敏再进入工单。合成凭据不得拿去在线试探,真实凭据也不得复制到公共验证网站。
常见故障如何取证
PR 扫描没有发现而全历史扫描命中,先检查浅克隆和 --log-opts/提交区间。升级后突然出现大量告警,比较工具版本、规则提交、解码深度和归档扫描设置。命中只在本机出现,核对配置优先级、Hook 版本和 CI 工作目录。TruffleHog 长时间无输出,则检查数据源规模、网络限流和是否启用了验证;不应直接扩大令牌权限来提速。
报告为空但命令非零,需要把工具错误与命中退出码分开。报告存在但无法上传,先保护本地制品并修复权限,不能把完整 Raw 内容打印到日志。Baseline diff 暴增时,检查插件和过滤器配置是否变化;直接接受新 baseline 只会把规则回归永久化。
权限、敏感数据与成本
扫描公开 PR 不得注入生产密钥。扫描私有 Git 只授予只读仓库权限,组织级扫描账号与开发者账号分离;对象存储、CI 系统和聊天平台也采用独立只读身份。扫描容器镜像不需要 Registry 推送或删除权限。
Secret 报告可能比源码更危险,因为它把候选值、位置和历史集中到一处。开启最大脱敏,限制下载者和保留期,对访问留审计,处置完成后按策略销毁。缓存、临时 clone、SARIF、Hook 输出和失败日志都属于敏感数据边界。
成本来自完整历史遍历、二进制与归档解码、远端 API 限流、人工审计和凭据轮换。PR 使用增量扫描降低反馈时间,周期任务承担全量发现;规则精度用固定 fixture 度量,不能靠关闭高误报 Detector 节省人力。长期指标应包含新增泄漏数、撤销耗时、重复泄漏率、baseline 年龄、过期 allowlist、扫描覆盖率和误报复核时长。
清理与回滚
实验结束前确认目录确实由当前流程创建,再删除:
test -d "$LAB_DIR/.git"
test -f "$LAB_DIR/.gitleaks.toml"
case "$LAB_DIR" in
/tmp/*|/var/tmp/*) rm -rf -- "$LAB_DIR" ;;
*) printf 'refuse to remove unexpected path: %s\n' "$LAB_DIR" >&2; exit 64 ;;
esac工具升级回滚应恢复上一份二进制、配置与 baseline,并保留新旧差异报告。不能通过扩大 allowlist 或重新生成 baseline 回滚“告警数量”。平台团队维护工具镜像、Hook 分发和受限制品库;安全团队维护规则、审计和响应流程;应用团队负责撤销、轮换与消费方迁移。责任链清晰后,扫描器才是预防泄漏的工程能力,而不是又一个容易被跳过的红灯。
