gitleaksを使ってsecretの誤コミット対策してみた

こんにちは、いわむらです。

AI エージェントといっしょに作業することが増えてきました。速くて助かる一方で、ローカルの設定値や検証用トークンが、気づかないうちにコミットに紛れ込むリスクは前より意識するようになりました。

間違ってコミットに含まれてしまうのを避けるため、機密情報はコミット前に機械的に止めるやり方にチャレンジしてみた内容を共有できたらと思っています。今回は、gitleaks を lefthook から pre-commit で実行する構成にしています。

本記事で紹介するのは次の3点です。

  • なぜ git-secrets ではなく gitleaks に寄せたのか
  • ローカルで止める最小構成
  • ローカルで漏れた場合に GitHub Actions でどう拾うか

人の注意に頼る場所を減らして、フローの中で止める形にしてみてます。

gitleaks に寄せた理由

もともとは git-secrets を使っていました。AWS 系の検知が優秀なのですが、それ以外のサービスには標準では対応していません。今回は AWS 以外も最初から広めに見たかったので、gitleaks のデフォルトのルールセットで Stripe のシークレットキーや Slack の Webhook URL、Vercel のアクセストークンなど、100 種類以上のサービス固有のパターンに対応していることから選択してみました。

もうひとつは Windows への導入のしやすさです。チームメンバーで Windows を利用している方も多く、git-secrets だと導入に手こずっている方もいました。gitleaks は Scoop を利用することで導入が簡単にできるので、より良い運用に乗せやすいと判断しました。

ローカルで止める最小構成

以下は gitleaks v8 系を前提にした例です。

まずは gitleaks を入れます。

# macOS
brew install gitleaks

# Windows
scoop install gitleaks

gitleaks version

lefthook はバイナリで入れます。

# macOS
brew install lefthook

# Windows
scoop install lefthook

clone してきたら、一度だけ次のコマンドを実行します。

lefthook install

これはリポジトリごとに一度だけ必要です。.git/hooks/ に hook が書き込まれ、以降は自動で動きます。

全リポジトリにグローバルで適用したい場合は、lefthook を使わず git のグローバル hooks を直接使う方法もあります。

mkdir -p ~/.git-hooks
cat > ~/.git-hooks/pre-commit << 'EOF'
#!/bin/sh
gitleaks git --staged --redact --verbose
EOF
chmod +x ~/.git-hooks/pre-commit
git config --global core.hooksPath ~/.git-hooks

ただし、グローバル設定は自分のマシンにしか効かないため、チームへの展開には向きません。チームで同じルールを共有するなら、リポジトリに lefthook の設定を置く方式のほうが確実です。

lefthook の設定は次のようにしています。

pre-commit:
  commands:
    gitleaks:
      run: gitleaks git --staged --redact --verbose

ここでやりたいのは、コミットの直前に一度ブレーキをかけることです。--redact を付けているのは、検知時の出力に secret をそのまま出さないためです。--verbose は、導入直後に何が起きているか追いやすくするために入れています。

初回導入時にやっておいたこと

pre-commit だけだと、これから入る差分しか止められません。既存の履歴を一度見ておきたいので、初回導入時はリポジトリ全体もスキャンしました。

gitleaks git --redact --verbose

ここで検知が出たら、まずは本物の secret かどうかを確認します。誤検知だけを allowlist に寄せるほうが安全です。

allowlist は狭く書く

gitleaks を使い始めると、サンプル文字列やドキュメントで誤検知が出ることがあります。そこで allowlist を入れますが、ここを雑に広げると後で本物を見逃します。

例えば、ドキュメント配下の EXAMPLE だけを許可するなら、次のように条件を絞って書きます。

title = "team gitleaks config"

[extend]
useDefault = true

[[allowlists]]
description = "allow documented placeholders only in docs"
condition = "AND"
paths = ['''^docs/''']
regexTarget = "match"
regexes = ['''\bEXAMPLE\b''']

「とりあえず docs を全部通す」のような書き方は避けたほうが安心だと考えてます。

CI でもう一度見る

ここでひとつ大事なのは、CI は GitHub に入る前に止める仕組みではない、という点です。pushpull_request で動かす以上、差分はすでに GitHub にはあります。

それでも CI を置く意味はあります。ローカルで拾えなかったものを PR で止めることと、デフォルトブランチに流し込まないことです。

GitHub Actions は次のようにしています。

name: gitleaks

on:
  pull_request:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITLEAKS_CONFIG: .gitleaks.toml
          GITLEAKS_ENABLE_COMMENTS: false
          # Organization リポジトリで使う場合だけ追加
          # GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}

fetch-depth: 0 を入れているのは、履歴を見られる状態にするためです。pushmain に絞っているのは、不要な実行を増やしすぎないためです。

検知後にやること

検知して終わりにはしません。もし本物の secret が push 済みなら、回収とローテーションまでやれるようになると良いかなと思っていますが今回は割愛します。

まとめ

AI エージェントで差分が増えるなら、機密情報対策は「気を付ける」だけでは足りないと考えてます。コミット前に止める。漏れたら CI でもう一度見る。その二段にしておくほうが、実際の運用はかなり楽になります。

git-secrets は今でも良いツールです。ただ、今回は AWS 以外も広く見たかったことと、Windows を含めて配りやすくしたかったことから、gitleaks に寄せました。

まずは pre-commit で止めるところから始めて、必要になったら CI と allowlist を育てていく。この順番がいちばん無理なく入れられるんじゃないかなと思います。