深夜2時、リポジトリにシークレットが漏洩した
あの夜のことは今でも忘れられない。チームに加わったばかりの開発者が .env ファイルを main ブランチに直接コミットしてしまった。本番データベースのパスワードとAWSシークレットキーが丸ごと含まれたまま。次のコミットでファイルを削除したものの、誰もが知っているように——Gitは決して本当には忘れない。リポジトリをクローンした人なら誰でも git log -p を実行すれば、履歴の中にそのシークレットが丸見えになってしまう。
その後GitHubから警告メールが届いた。「We found a secret in your repository history.」AWSはこちらが何かする前に、すでにキーを自動で無効化していた。そのとき初めて git-filter-repo の使い方を学んだ——そして、このツールは必要になる前に知っておくべきだと痛感した。
なぜgit-filter-repoを選ぶのか
Gitの履歴を書き換える方法はいくつかあるが、それぞれに問題がある:
- git filter-branch:Gitに標準搭載されているが、大きなリポジトリでは非常に遅く、エラーも起きやすい。Git自身もずっと前から非推奨としている。
- BFG Repo Cleaner:速くて使いやすいが、Javaが必要で、複雑なケースに対応しにくい。
- git-filter-repo:Pythonで書かれており、Gitプロジェクトが
filter-branchの公式代替として推奨。実際に10〜50倍高速。
具体的には:2GBの履歴を持つリポジトリで、filter-branch は40分かかった。git-filter-repo は3分以内に完了した。それ以上の理由は必要ない。
git-filter-repoのインストール
まずGitのバージョンを確認する——2.22.0以上が必要:
git --version
# git version 2.43.0
pipでインストール(Python 3.6+):
# システム全体にインストール
pip3 install git-filter-repo
# またはvirtualenv内で
pip install git-filter-repo
# インストール確認
git filter-repo --version
# 2.45.0
macOSでHomebrewを使う場合:
brew install git-filter-repo
Debian/Ubuntuの場合:
sudo apt install git-filter-repo
作業前の鉄則:元のリポジトリに直接手を加えてはいけない。別のコピーをクローンし、バックアップを取ってから作業すること:
# バックアップ用にミラークローン
git clone --mirror https://github.com/yourorg/your-repo.git repo-backup.git
# 作業用にフレッシュクローン
git clone https://github.com/yourorg/your-repo.git repo-clean
cd repo-clean
機密ファイルと特定データの削除
ファイルを全コミットから完全に削除する
ケースの90%はこれだ——.env、config/secrets.yml、その他リポジトリに存在すべきでないものを削除する:
# .envファイルを全履歴から削除
git filter-repo --path .env --invert-paths
# 複数ファイルを一度に削除
git filter-repo --path .env --path config/secrets.yml --path credentials.json --invert-paths
# パターンで削除(全.pemファイル)
git filter-repo --path-glob '*.pem' --invert-paths
# ディレクトリごと削除
git filter-repo --path secrets/ --invert-paths
--invert-paths フラグは「これらのパスを削除し、それ以外を保持する」という意味で、デフォルトの動作とは逆になる。
ファイル内の機密データを置換する(ファイルは残す)
ファイル自体は残しつつ、中のシークレット値だけを消したい場合は、置換する文字列のマップファイルを作成する:
# 置換対象の文字列を含むファイルを作成
cat > expressions.txt << 'EOF'
literal:sk-ant-api03-AbCdEf123456789===>***REMOVED***
literal:AKIAIOSFODNN7EXAMPLE===>***AWS_KEY_REMOVED***
EOF
# 適用する
git filter-repo --replace-text expressions.txt
フォーマットは 旧文字列===>新文字列 で、正規表現にも対応している:
cat > expressions.txt << 'EOF'
regex:password=\S+===>password=***REMOVED***
regex:api_key:\s*['"]\S+['"]===>api_key: "***REMOVED***"
EOF
git filter-repo --replace-text expressions.txt
大容量ファイルの削除
チームでかつて、メンバーが誤って node_modules フォルダごと、さらに500MBのデモ動画ファイルをコミットしてしまったことがあった。リポジトリは50MBから600MBに膨れ上がり、クローンに午前中まるまるかかるようになった。対処法はこうだ:
# リポジトリの履歴を分析して大容量ファイルを探す
git filter-repo --analyze
# 結果は .git/filter-repo/analysis/ ディレクトリに出力される
# 最も大きなファイルを確認
cat .git/filter-repo/analysis/path-all-sizes.txt | sort -rn | head -20
出力はこのようになる:
=== All paths by reverse size ===
Format: size, packed size, date deleted, path name
524288000 498234112 2024-03-15 assets/demo-video.mp4
145234567 138234089 2023-11-20 node_modules.tar.gz
45678901 43211234 2024-01-08 dist/bundle.min.js
# パスを指定して特定ファイルを削除
git filter-repo --path assets/demo-video.mp4 --invert-paths
# 10MB以上の全ファイルを削除
git filter-repo --strip-blobs-bigger-than 10M
結果の確認とリモートへのプッシュ
データが削除されたことを確認する
# ファイルが履歴に残っていないことを確認
git log --all --full-history -- .env
# 出力なし = 完全に削除済み
# 全履歴から特定の文字列を検索
git log --all -p | grep -i "sk-ant-api"
# 出力なし = シークレットは削除済み
# 削除後のリポジトリサイズを確認
git count-objects -vH
クリーンアップとガベージコレクション
git-filter-repo は自動的にクリーンアップを実行するが、念のため:
# 全reflogを強制的に期限切れにする
git reflog expire --expire=now --all
# アグレッシブなガベージコレクション
git gc --prune=now --aggressive
# 処理前後のサイズを確認
git count-objects -vH
なお、誤って削除したコミットを復元する必要が生じた場合は、Git Reflogで削除したコミットやブランチを「復活」させる究極のテクニックが参考になる。
リモートへのforce push
このステップは避けられない——履歴を書き換えた以上、force pushは必須だ:
# リモートを再追加(git-filter-repoは誤プッシュ防止のためリモートを自動削除する)
git remote add origin https://github.com/yourorg/your-repo.git
# 全ブランチをforce push
git push origin --force --all
# 全タグをforce push
git push origin --force --tags
force push後、チームの全員がリセットする必要がある——各自のマシン上にある古いリポジトリはもう使えない:
# 各メンバーが実行する必要があるコマンド
git fetch --all
git reset --hard origin/main
# またはシンプルに:古いリポジトリを削除して再クローン
rm -rf old-repo/
git clone https://github.com/yourorg/your-repo.git
漏洩した全シークレットを無効化・ローテーションする
Gitの履歴から削除しただけでは終わりではない。リポジトリがかつてpublicだった場合、あるいは誰かが対処前にクローンしていた場合——そのシークレットはすでに侵害されたものとみなすべきだ。例外はない:
- APIキーとパスワードを直ちに無効化して再発行する
- AWSクレデンシャルとデータベースパスワードをローテーションする
- アクセスログを確認し、そのキーが使われていないかチェックする
- GitHubのシークレットスキャニングを有効にして、次回から早期警告を受け取る
再発防止策
あの夜を経て、すぐにpre-commitフックを設定してコミット前にシークレットをチェックするようにした。gitleaks を使えば軽量で追加ランタイムも不要だ。Gitleaks導入ガイドでは、設定方法から運用まで詳しく解説している:
# gitleaksをインストール
brew install gitleaks # macOS
# または
wget https://github.com/gitleaks/gitleaks/releases/latest/download/gitleaks_linux_x64.tar.gz
# 現在のリポジトリをスキャン
gitleaks detect --source . --verbose
# pre-commitフックに追加
cat > .git/hooks/pre-commit << 'EOF'
#!/bin/bash
gitleaks protect --staged --verbose
if [ $? -ne 0 ]; then
echo "Gitleaksがシークレットを検出しました!コミットをブロックします。"
exit 1
fi
EOF
chmod +x .git/hooks/pre-commit
このようなpre-commitフックをチーム全体に展開するには、Git Hooksによるワークフロー自動化の仕組みを理解しておくと、全メンバーへの配布と管理がずっとスムーズになる。
最後に、.env を .gitignore に追加しよう。当たり前のことだが、最もよく忘れられる:
echo '.env' >> .gitignore
echo '*.pem' >> .gitignore
echo 'credentials.json' >> .gitignore
git add .gitignore
git commit -m "chore: ignore sensitive files"
8人のチーム全体にpre-commitフックを導入して以来、シークレット漏洩インシデントはゼロになった——そして夜もずっとよく眠れるようになった。

