前阵子隔壁组出了个大事故:有个开发小哥写代码时图省事,把阿里云的 AccessKey ID 和 SecretKey 直接硬编码写进了配置文件里。更绝的是,他在做本地调试时,不小心把这个测试文件连同核心代码一起 push 到了 GitHub 公开库。

结果就是悲剧的开始。不到五分钟,黑客安排的爬虫机器人就精准扫到了这个提交,迅速利用这个 AK 在我们阿里云账号下开满了高规格的 GPU 实例疯狂挖矿。直到第二天早上收到服务商的欠费报警短信,账单金额已经突破了五位数,老板的脸绿得像块翡翠。

很多时候我们总觉得“我注意点就行了”,但人肉 Code Review 永远是不靠谱的,百密一疏,只要漏掉一次就是灾难性的后果。想要彻底解决敏感密钥泄露的问题,唯一靠谱的方案就是把“密钥扫描”强制卡死在 CI/CD 流程里,不给任何侥幸心理留空间。

为了做这个拦截,我们团队考察对比了多款开源工具,最后选择了 Trufflehog。它不仅支持扫描已有的 Git 提交历史(防止有人提交了密钥又用新提交盖过去,导致历史提交里仍然留有痕迹),还能通过高熵(High Entropy)算法和上百种云服务厂商的特征正则来扫描最新的变更。

以下是我们目前在 GitHub Actions 中集成的完整流水线配置 .github/workflows/security-scan.yml。这个工作流会在每次提交 PR 时自动运行,并且只增量扫描该 PR 中的变更部分,既保证了安全性,又避免了扫描全量历史导致的 CI 变慢:

name: Security Secrets Scan

on:
  push:
    branches:
      - main
  pull_request:
    types: [opened, synchronize, reopened]

jobs:
  trufflehog_scan:
    name: Run TruffleHog Secrets Scanner
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Source Code
        uses: actions/checkout@v4
        with:
          # 必须拉取完整的历史记录,方便 TruffleHog 分析 commit 历史中的密钥修改
          fetch-depth: 0

      - name: Run TruffleHog Scan for Pull Request
        if: github.event_name == 'pull_request'
        uses: trufflesecurity/trufflehog@main
        with:
          path: ./
          base: ${{ github.event.pull_request.base.sha }}
          head: ${{ github.event.pull_request.head.sha }}
          extra_args: --debug --only-verified

      - name: Run TruffleHog Scan for Direct Push
        if: github.event_name == 'push'
        uses: trufflesecurity/trufflehog@main
        with:
          path: ./
          base: ${{ github.event.before }}
          head: ${{ github.event.after }}
          extra_args: --debug --only-verified

配置里的 --only-verified 参数非常有意思。Trufflehog 在扫到疑似 AWS、阿里云、Slack Webhook 等密钥后,会尝试往对应的服务商发送一个真实的、不产生副作用的验证请求。如果服务商返回鉴权成功,才判定为“有效泄露”并报警。这样极大地减少了因随机字符串引起的误报,提升了研发体验,避免引起“狼来了”的疲劳感。

在我们的规范中,一旦这个 GitHub Action 报错失败,分支的 Merge 按钮就会被强制置灰。开发人员必须使用 git filter-branchbfg 工具从提交历史中彻底抹去那个 commit,才能重新推送通过检查。

对于安全合规来说,这种端到端的闭环卡点是必须经历的阵痛。

你们团队有遇到过类似的密钥泄露惨案吗?现在是用本地 Git Hook(如 pre-commit)在前端拦截,还是在 CI 服务端进行统一把关?