.env を消すコミットを積んだので安全だと思っていたら、その値は Git の履歴に残っていました。秘密の値を見つけるシークレットスキャンは、どこで走らせるかで見えるものが変わります。この記事では gitleaks を実際に動かし、コミット前・プッシュ時・CI の3か所それぞれの穴を確かめます。
THREE CHECKPOINTS3つの関所
秘密の値はコミット・プッシュ・CIの3か所で止める
-
コードに書く
エディタ・生成AIの出力
-
コミット前
pre-commit フック(手元・SKIPで外せる)
-
プッシュ時
GitHub のプッシュ保護
-
CI・全履歴
fetch-depth: 0 で全コミットを走査
-
見つかったら
無効化と取り替えが先
30秒でわかる
- ・止める場所は3つ。コミット前(pre-commit フック)、プッシュ時(GitHub のプッシュ保護)、CI と全履歴
- ・実測では、削除済みの
.envの値はgitleaks gitの全履歴走査でしか出ず、作業ツリーと浅いクローンの走査では出なかった - ・gitleaks の公式フックID
gitleaks-systemをそのまま使うと、ファイル1つのコミットは検査せずに通った。pass_filenames: falseを足すと止まる - ・誤検知は、パスの許可リストとベースラインで絞る。値を1件ずつ握りつぶす運用は増えるほど読めなくなる
- ・本物が見つかったら、履歴の書き換えより鍵の無効化と取り替えが先
この連載は、実際の事故と開発者の議論から Web アプリの守り方を1回1テーマで整理しています。セキュリティ分野の記事の全体像はサプライチェーン攻撃とは|手口・防御ツール比較・npm 12の新機構まで実践解説から辿れます。
シークレットスキャンとは:止める場所を3つに分ける
シークレットスキャンは、API キーやトークン、秘密鍵などがコードやコミットに紛れ込んでいないかを、パターンとエントロピー(文字列のでたらめさ)で探す検査です。OWASP の Secrets Management Cheat Sheet は、コミットや PR の前、つまり IDE や pre-commit フックの段階で検出を有効にすることを勧めています。ただし、1か所に置くだけでは足りません。
1か所では足りない理由
| 層 | 動く場所 | 止められるもの | 穴 |
|---|---|---|---|
| コミット前 | 各自の手元(pre-commit フック) | 履歴に入る前の値 | 入れていない人は素通り。SKIP=gitleaks や --no-verify で外せる |
| プッシュ時 | GitHub 側(プッシュ保護) | リモートに届く前の値 | 書き込み権限のある人は理由を書けば迂回できる |
| CI・全履歴 | CI のジョブや定期実行 | 既に履歴にある値 | 見つけた時点で値はもう共有されている |
コミット前の層は速く、気づいた人がその場で直せます。一方で手元の設定に頼るので、強制はできません。実測でも、フックが Failed を返したコミットが git commit --no-verify でそのまま通りました。
GitHub のプッシュ保護が使える条件
GitHub Docs の「Push protection」によると、プッシュ保護はコマンドラインからのプッシュ、Web 画面でのコミット、ファイルのアップロード、REST API への要求で秘密の値を検出すると、その操作を止めます。種類は2つあります。
・ユーザー単位:既定で有効。自分のアカウントから公開リポジトリへのプッシュを止める
・リポジトリ単位:既定で無効。GitHub Secret Protection の有効化が必要。迂回するとリポジトリの「Security and quality」タブにアラートが残る
非公開リポジトリでの利用条件も同じ GitHub Docs にあります。シークレットスキャンは公開リポジトリなら無料で自動的に走り、組織の非公開リポジトリでは Team または Enterprise Cloud で GitHub Secret Protection を有効にしたときに使えます。料金ページ(github.com/security/plans)の表示は、1アクティブコミッターあたり月19ドルのアドオンでした(2026-10-17 に WebFetch で確認)。GitHub 以外のホスティングでは、この層を CI 側で補うことになります。
全履歴のシークレットスキャン:消した .env も検出された
ここから実測です。2026-10-17 に gitleaks v8.30.1 と trufflehog v3.99.2 を Go のモジュールプロキシ経由でビルドしました。取得したモジュールは sum.golang.org のチェックサムと照合されます。github.com 配布のバイナリとそのチェックサムは確かめていません(未検証)。
検体は6コミットの合成リポジトリです。AWS 公式ドキュメントの例示キー、形だけ合わせたランダムな AWS キー、ghp_ で始まる GitHub トークン形式の値、Slack Webhook 形式の URL、秘密鍵のヘッダーを置き、最後のコミットで .env を削除しました。どれも偽の値です。
# 全履歴を走査する(削除済みのファイルも対象。値は伏せて表示)
gitleaks git --redact .
# 今あるファイルだけを走査する
gitleaks dir --redact .
# 既知の検出をベースラインとして保存し、次からは新しい検出だけを見る
gitleaks git --redact --report-path baseline.json .
gitleaks git --redact --baseline-path baseline.json .
SAME REPO, FIVE SCANS走査範囲で変わる件数
消したはずの .env は全履歴の走査でしか出てこない
| 走査 | 件数 | 終了コード | 差 |
|---|---|---|---|
gitleaks git(全履歴) |
5 | 1 | 削除済み .env の github-pat を含む |
gitleaks dir(作業ツリー) |
4 | 1 | .env の値が出ない |
git clone --depth 1 後に gitleaks git |
4 | 1 | 履歴が1コミットしかなく .env が出ない |
| クリーンなリポジトリ(陰性対照) | 0 | 0 | 環境変数から読むだけのコード |
全履歴の走査は 0.07 秒で終わりました(6コミット・約 800 バイト)。gitleaks の表示は「5 commits scanned」で、削除だけのコミットは数に入りません。README も、git の走査が見るのは追加された行だと書いています。
0件は「規則に一致する値が無い」ことの確認で、秘密が無いとは限りません。次の例示キーのように許可リストで除外される値や、既定の規則に無い形式の値は出ず、検出された値が今も有効な本物かどうかも、この結果だけでは判断できません。
AWS の例示キーは0件になる
AKIA…EXAMPLE の例示キーは検出されませんでした。v8.30.1 の既定設定 config/gitleaks.toml を読むと、aws-access-token ルールに .+EXAMPLE$ の許可リストがあり、意図して除外しています。例示キーで「動いた」と確かめることはできないので、陽性対照には形だけ合わせたランダムな値を使います。
CI では fetch-depth: 0 を忘れない
actions/checkout の README によると、既定で取得するのはワークフローを起こした1コミットだけです。表の3行目と同じ状態なので、全履歴を見るジョブでは fetch-depth: 0 を指定します。公式の gitleaks-action を使う場合、README は組織のリポジトリでは無料のライセンスキー(GITLEAKS_LICENSE)が必要で、個人アカウントでは不要だとしています。Actions 側の権限やトークンの扱いはGitHub Actionsセキュリティ|2026年連鎖攻撃と新ロードマップ完全ガイドで整理しました。
コミット前に止める:pre-commit フックの落とし穴
pre-commit フレームワーク(4.7.0)で、gitleaks のフックを試しました。
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.1
hooks:
- id: gitleaks
# 手元の gitleaks を使うなら、次の2行に差し替える
# - id: gitleaks-system
# pass_filenames: false
id: gitleaks は Go でフック用の gitleaks をビルドして使います。トークン形式の値を含むコミットは Failed で止まりました。初回はビルド込みで33秒、2回目は 0.23 秒です。値を外すと Passed になります。
gitleaks-system は設定のままだと素通りした
インストール済みの gitleaks を使う id: gitleaks-system は、結果が違いました。v8.30.1 の .pre-commit-hooks.yaml では、この ID だけ pass_filenames: false がありません。pre-commit はステージしたファイル名をコマンドの末尾に渡すので、gitleaks git [repo] の位置引数がファイル名で埋まります。
実測(2026-10-17)
・ステージしたファイルが1つ:「scanned ~0 bytes」「no leaks found」のまま Passed。秘密の値を含むコミットが通った
・ファイルが2つ:「accepts at most 1 arg(s), received 2」の引数エラーで Failed
・pass_filenames: false を足すと、検出で Failed になり、値を外すと Passed
この事象は上流で issue #2302(2026-10-13 起票、10-17 時点で Open)として報告されています。影響を受けるのは gitleaks-system を使う設定だけで、gitleaks と gitleaks-docker の定義には pass_filenames: false があります。フックを入れたら、偽の値を1つだけステージして本当に止まるかを一度確かめてください。README の pre-commit の例は rev: v8.24.2 のままなので、pre-commit autoupdate で版を上げてから使います。
誤検知の扱い:allowlistとベースラインを使い分ける
テスト用のダミー値やドキュメントの例は、正しく検出されるほど邪魔になります。gitleaks の README が示す抑え方は4つです。
| 方法 | 書く場所 | 向く場面 |
|---|---|---|
gitleaks:allow コメント |
該当行の末尾 | 承知のうえで置くテスト値が1行だけ |
.gitleaksignore |
リポジトリ直下に検出の Fingerprint | 特定の1件。README は実験的機能としている |
[[allowlists]] |
.gitleaks.toml |
テスト用フィクスチャのような、パスや形で決まる誤検知 |
| ベースライン | --baseline-path に過去のレポート |
導入時に既にある検出を一旦脇に置く |
tests/fixtures/ にダミーのトークンを置くと、作業ツリーの走査は5件になりました。次の設定を置くと4件に戻ります。
[extend]
useDefault = true
[[allowlists]]
description = "テスト用のダミー値"
paths = ['''^tests/fixtures/''']
useDefault = true で既定ルールを引き継ぎ、許可リストだけを足します。README によると、v8.25.0 で [allowlist] は [[allowlists]] に置き換わりました。古い記事の書き方のままだと、意図した除外が効いているかを確かめにくくなります。
ベースラインは、導入時に CI を全部赤にしないための道具です。実測では、全履歴の5件をベースラインにしたあと、トークン形式の値を含むコミットを1つ足すと新しい1件だけが出ました。ただし、ベースラインに入れた検出は消えたわけではありません。本物の値なら、次の節の順で片付けます。OWASP は、組織で共通のテスト用の値を決めておくと、誤検知を値の種類ごとに1つ管理すれば済むとしています。
gitleaks・trufflehog・プッシュ保護の比較
| gitleaks v8.30.1 | trufflehog v3.99.2 | GitHub プッシュ保護 | |
|---|---|---|---|
| 動く場所 | 手元・CI | 手元・CI | GitHub 側 |
| ライセンス・料金 | MIT | AGPL-3.0 | 公開リポジトリは無料。非公開は Secret Protection(有料) |
| 値が有効かの確認 | しない | 既定でプロバイダの API に問い合わせる | 有効性チェックは別機能 |
| 実測の検出(合成リポジトリ) | 5件 | 3件(すべて未検証) | 試していない |
| 失敗時の終了コード | 既定で1 | --fail で183。付けないと0 |
プッシュが拒否される |
trufflehog の README によると、検証は見つかった値を、それが属するサービスの API で実際に試す仕組みです。AWS のキーなら GetCallerIdentity を呼び、結果を verified・unverified・unknown に分けます。有効な鍵を先に片付けられる利点がある反面、見つけた値を外部へ送ることになります。今回は偽の値を送らないよう --no-verification で止めました。この状態では Slack・AWS・GitHub 形式の3件が出て、ランダムな本文で作った偽の秘密鍵と例示キーは出ませんでした。
gitleaks の README の冒頭には、原作者の「機能は完成しており、今後はセキュリティパッチのみ」という表明があります。後継の Betterleaks の設定互換性はBetterleaks徹底解説|Gitleaks原作者の新シークレットスキャナと98.6%の数字の正体で確かめています。OWASP は、検出ツールを複数併用して結果を突き合わせる方法も挙げています。
対策:本物が見つかったら鍵の無効化が先
WHEN IT IS REAL本物が出たら
履歴の書き換えより鍵を無効にするのが先
-
無効化と取り替え(最初)
発行元で失効させ、新しい値を保管先から配る
-
使われた形跡
発行元のログで、漏れていた期間の利用を確かめる
-
履歴から消す(二の次)
必要なときだけ。全員のクローンとフォークに影響する
OWASP の Secrets Management Cheat Sheet(§9.2)は、封じ込めの順を無効化、取り替え、削除としています。履歴を書き換える場合は、ほかのコミットへのリンクが壊れることを承知のうえで計画するよう注意しています。GitHub Docs も、アラートを受けたら直ちに取り替え、無効化済みなら履歴からの削除は手間がかかるうえ多くの場合は不要だとしています。消したはずの値でも、誰かのクローンやフォークに残っているかもしれないからです。クラウドサービスの認証情報が侵入の入口になった事例は日経不正アクセスで183万件漏えい|システム開発に利用するクラウドの認証情報が入口、対象者と開発チームの点検で整理しました。
3層を入れる順番は、次のとおりです。
・最初に全履歴を一度走査する。出た検出のうち本物は無効化し、残りはベースラインか許可リストに分ける
・CI に全履歴の走査を置く。fetch-depth: 0 で取得し、新しい検出があればジョブを落とす
・プッシュ保護を有効にする。GitHub ならリポジトリ単位で有効にし、迂回のアラートを誰が見るかを決める
・pre-commit フックを配る。入れたら偽の値で止まるかを一度確かめる
この記事で確かめていないこと
・GitHub のプッシュ保護と有効性チェックの実際の挙動(ドキュメントの記述のみ)
・github.com 配布のリリースバイナリとチェックサム(Go のモジュールプロキシ経由でビルドした)
・trufflehog の検証機能を有効にした走査と、gitleaks-action の CI 上での動作
・ビルドしたバイナリは版の文字列を持たない(gitleaks は「version is set by build process」、trufflehog は「dev」と表示)。版はモジュールのタグで特定した
参照ソース
以下は 2026年10月17日に確認しました。GitHub 上の文書は raw.githubusercontent.com と git の取得で、GitHub Docs は github/docs リポジトリの原稿で、料金は github.com/security/plans を WebFetch で読みました。
・gitleaks/gitleaks — README・.pre-commit-hooks.yaml・config/gitleaks.toml
・gitleaks issue #2302 — gitleaks-system pre-commit hook missing pass_filenames: false
・gitleaks/gitleaks-action — README
・trufflesecurity/trufflehog — README
・GitHub Docs — Push protection
・GitHub Docs — Secret scanning
・GitHub Docs — GitHub Advanced Security license billing
・GitHub Security plans & pricing
・actions/checkout — README
・OWASP Cheat Sheet Series — Secrets Management
・pre-commit





