
一个临时测试用的 API Key一份带密码的配置文件或者一段排障时复制进 README 的连接字符串都可能随着一次git push进入公开仓库。你可能觉得仓库没什么人看发现后马上删掉就好。但公开代码可以被程序持续检索凭据又能直接换取服务的访问权限。一次误提交可能让别人读取数据、调用付费 API甚至在权限允许的情况下创建云资源让你承担账单。我给自己的项目加了一条 Gitleaks 凭据扫描工作流。第一次运行GitHub Actions 扫描了 161 个提交结果是no leaks found。这次没有发现问题但这道检查值得在出事之前装好。本文把实际配置拆开解释也讲清楚一个容易误解的地方push 之后才启动的 CI无法阻止凭据首次上传。想减少泄漏就要把同一套检查向前移动。仓库没人关注也可能有人在扫描GitHub 提供公开 Events API其中包含PushEvent。公开推送的仓库、分支和提交信息可以被程序轮询获取再用于定位公开代码。所谓“订阅推送”更准确地说是持续获取公开活动流它不需要有人先在网页上关注你的仓库。GitHub Events API 文档这里有两个边界公开事件不会自动公开私有仓库的文件事件里也不等于包含了完整代码。GitHub 还明确说明这个 API 并非实时服务事件延迟可能在 30 秒到 6 小时之间。不过公开代码本身仍然可以通过其他途径访问这个延迟不能作为安全窗口。GH Archive 就是记录和归档 GitHub 公开活动的项目。这说明持续采集公开开发活动有成熟路径。当然公共数据项目不等于攻击者问题在于同样的可发现性也能被寻找有效凭据的人利用。安全研究者、网络“寻宝者”和以获利为目的的攻击者寻找的东西可能相似使用方式却完全不同。不能指望捡到密钥的人会先通知你。只要凭据有效、目标服务可达、权限足够拿到凭据的人就可能绕过代码里的业务限制直接访问它所代表的服务。这个问题也远不止偶发失误。GitGuardian 在 2026 年发布的报告中称其检测到的 2025 年 GitHub 新泄漏凭据约为 2,900 万个。这是该厂商扫描口径下的检测数量不等于 2,900 万次成功攻击但足以说明凭据误入代码是一种规模化风险。GitGuardian 报告发布说明删除文件不能让别人忘掉密钥Git 保存历史。在后续提交里删除.env之前那个提交里的内容仍然可能被读到。即使重写了历史已经被克隆、缓存或复制的内容也无法靠一次删除全部收回。因此一旦确认真实凭据进入公开仓库第一步应是撤销或轮换让旧凭据失效。然后检查它是否被使用再处理代码与历史。GitHub 的敏感数据清理文档也明确把撤销、轮换放在前面。GitHub 敏感数据清理指南风险大小取决于权限。只能读取测试数据的密钥与能管理生产环境的密钥后果当然不同。但“测试用”并不保证安全它可能访问真实数据可能绑定付费账户也可能一直没有过期。AWS 对暴露访问密钥的处理建议包括检查未授权资源与使用情况并尽量采用角色和临时凭据。AWS 暴露访问密钥处理建议等到账单异常或数据外流再补救成本已经发生。我的选择是让检查成为提交流程的一部分减少对记忆和人工浏览的依赖。Gitleaks 检查什么Gitleaks 是一个凭据检测工具可以扫描 Git 历史、目录以及标准输入中的文本。它通过检测规则识别密码、API Key、Token 等可疑内容规则可以结合格式、关键词和熵阈值等条件。Gitleaks v8.24.3 文档这很适合 CI扫描命令返回非零退出码时让任务失败开发者就在熟悉的检查页面里看到问题。它不需要为了扫描源代码而拿到你的生产密钥也不需要把代码交给一个在线检测接口。但规则匹配有边界。它可能把示例误判成密钥也可能漏掉格式特殊的内部密码。扫描通过表示在本次范围和规则下没有发现匹配不能证明仓库不存在任何敏感信息也不代表历史上的凭据从未被滥用。这次 GitHub Actions 的实际配置我使用的是 Gitleaks CLI版本固定为8.24.3运行环境是 GitHub 托管的ubuntu-24.04。下面是本次运行对应的完整配置保存为.github/workflows/secret-scan.yml即可。这个版本是案例版本不是对“当前最新版”的声明。name:Credential securityon:push:pull_request:workflow_dispatch:schedule:-cron:17 2 * * 1permissions:contents:readjobs:secret-scan:name:Secret scanruns-on:ubuntu-24.04timeout-minutes:10steps:-name:Check out full historyuses:actions/checkout11bd71901bbe5b1630ceea73d27597364c9af683# v4.2.2with:fetch-depth:0persist-credentials:false-name:Install verified Gitleaksenv:GITLEAKS_VERSION:8.24.3GITLEAKS_SHA256:9991e0b2903da4c8f6122b5c3186448b927a5da4deef1fe45271c3793f4ee29cshell:bashrun:|set -euo pipefail install_dir${RUNNER_TEMP}/gitleaks-bin mkdir -p $install_dir archive${install_dir}/gitleaks.tar.gz curl --fail --silent --show-error --location --retry 3 \ https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz \ --output $archive printf %s %s\n $GITLEAKS_SHA256 $archive | sha256sum --check --strict tar -xzf $archive -C $install_dir gitleaks echo $install_dir $GITHUB_PATH-name:Scan all Git historyshell:bashrun:|set -euo pipefail gitleaks git . --log-opts--all --redact100 --verbose --exit-code1配置来自本次提交的工作流文件。下载包的 SHA-256 对应这个版本的 Linux x64 包升级版本或换架构时必须一并更换不能照搬到其他包上。官方发布页这份配置里有几处比“能跑起来”更值得注意。push和pull_request负责日常变更workflow_dispatch支持手动触发。定时任务的 cron 使用 UTC本例是每周一 02:17 UTC也就是北京时间 10:17。GitHub 的定时工作流从默认分支运行文件需要先进入默认分支才会按这个时间表触发。GitHub 定时工作流说明完整拉取历史。fetch-depth: 0避免只检出最近的一小段提交。扫描命令的--log-opts--all遍历本地已有引用所能到达的历史因此在早先提交中加入、后来删除的凭据也可能被发现。它不能扫描根本没被获取的引用、其他仓库或已脱离所有引用的对象。让失败真正向上传递。发现匹配时--exit-code1让 Gitleaks 返回失败set -euo pipefail也让下载或校验失败不能被后续步骤掩盖。不要加continue-on-error也不要用|| true把失败吞掉。避免扫描日志造成二次泄漏。--verbose提供定位信息--redact100将检测到的凭据内容脱敏。本例不上传报告也没有把凭据原文写进 PR 评论。需要报告时还应检查脱敏情况与访问权限。扫描器只拿必要权限。工作流只有contents: readcheckout 不持续保留 Git 登录凭据。Action 固定到提交 SHA二进制固定版本并校验哈希。这样可以减少依赖漂移也能避免损坏或不匹配的下载包直接执行。这里直接运行 CLI没有调用gitleaks/gitleaks-action。后者是另一层 Action 包装组织仓库使用时有许可证要求不要把包装 Action 的要求与 CLI 混为一谈。Gitleaks Action 官方说明实测结果161 个提交没有发现匹配先看截图中的Secret scan绿色成功状态再看后面的原始日志。这次下载校验通过任务整体约用 5 秒完成扫描器记录的是约 2.36 MB 内容、161 个提交扫描用时 357 ms。这个速度只代表本仓库、这个版本和本次运行不能直接推算大型仓库的耗时。本次 Actions 运行图本项目的实际运行截图。绿色成功状态表示检查完成具体扫描结论以日志为准。手机上重点看下面放大的原始日志161 commits scanned和no leaks found是这次运行的结果。图同一次运行截图中的两条日志分别裁切后拼接未改写日志内容。我还在临时本地仓库里用合成的 GitHub Token 形状字符串做了验证干净历史返回成功加入该字符串并提交、再删除并提交后扫描仍返回失败输出没有出现完整测试字符串。这个测试验证了历史检测和脱敏行为没有使用真实可用密钥也没有把测试凭据推送到 GitHub。第一次扫描如果发现问题不要为了让 CI 变绿就忽略全部历史。先判定是不是有效凭据。确认无效的示例可以经过评审后精确豁免真实凭据要先轮换再处理历史记录和例外。历史扫描会持续提示旧记录需要明确的处置流程不能让告警长期变成背景噪声。CI 能阻止合并提交前检查才能更早拦住下图从上往下是代码进入仓库的过程。重点看红色“公开上传”这一层它位于本地检查、推送保护之后却在 CI 扫描之前。图右侧的红色箭头说明上传成功后公开读取与 CI 检查可以同时发生。图防护时序示意。push 触发的 CI 扫描发生在上传之后不能撤回已经公开的内容。要让 CI 失败阻止合并需要在仓库 Rulesets 或分支保护中把Secret scan配成必需状态检查。只有一个红色 Job并不会自动禁止所有合并。本例已验证任务执行成功并未据此宣称分支规则已经配置。更早的一步是在本地检查准备提交的内容。安装相同版本的 Gitleaks 后可以运行# 检查暂存区差异文本set-opipefailgitdiff--cached-U0|gitleaks stdin--redact100--exit-code1# 检查已经提交的本地历史gitleaksgit.--log-opts--all--redact100--exit-code1第一条可以放进团队维护的 pre-commit 检查第二条适合推送前检查历史。第一条扫描的是完整差异文本包含删除行因此删除旧凭据时也可能告警遇到这种情况应确认旧凭据已经撤销再按团队规则处理。git模式不扫描尚未提交的修改所以只运行第二条无法覆盖暂存区。钩子需要安装也可能被跳过因此 CI 仍然有必要保留。此外检查 GitHub Push protection 的启用情况。它针对支持的凭据类型在接受推送之前进行检测并拦截它也有检测范围和绕过机制不能当成所有敏感信息的万能过滤器。GitHub Push protection 文档.gitignore同样值得设置忽略.env、私钥、个人配置与浏览器凭据文件。但它不保护已跟踪文件不清理旧历史也挡不住把密钥复制到 README。它减少误提交机会扫描负责检查实际内容两者配合使用。在 GitLab CI、Jenkins 等系统中怎么用Gitleaks 的核心是 CLI其他 CI 也可以执行同一条扫描命令。迁移时保留三个条件获取所需历史安装固定且校验过的扫描器让非零退出码阻止后续合并或发布。例如下面是 GitLab CI 的结构示例不是本次已运行验证的流水线secret_scan:stage:testvariables:GIT_DEPTH:0script:# 前提Runner 已安装并校验固定版本的 Gitleaks-gitleaks git .--log-opts--all--redact100--exit-code1allow_failure:false实际接入时还需按项目分支和 MR 检出方式确认需要扫描的引用确实在本地。Jenkins 或其他 CI 也是一样扫描器所在的 stage 失败后后续发布应停止不能一边输出“发现凭据”一边继续部署。小仓库可以先采用完整历史扫描简单、容易理解。大型仓库可考虑日常扫描提交范围配合周期性全量扫描但必须正确处理比较基线、合并提交和新分支。优化漏掉了提交检查再快也没有意义。发现真实凭据时先让它失效处理顺序可以很明确在凭据所属平台撤销或轮换旧凭据并更新依赖它的服务。查看审计日志、异常调用、数据访问和账单确认是否已有滥用。从代码移除凭据改用环境变量、Secret 管理服务或短期身份新凭据限制权限与有效期。按需协调清理 Git 历史、分支和相关副本并补上自动检查。扫描是发现问题的手段撤销凭据才会让旧密钥失去作用。不能把“删除提交”当成“已经安全”。对已有仓库来说便捷的凭据扫描是最值得优先落地的防线之一接入成本低能持续执行也能覆盖人眼容易漏掉的历史。它应与提交前检查、推送保护和最小权限一起使用。先给仓库加一道检查再去优化它的范围和速度。一次绿色结果的价值是确认这道防线已经在工作真正希望避免的是某天从异常账单上才知道密钥早已公开。