コードの中の秘密:APIキーはどこから漏れ、15分でどう入れ替えるか
コードからAPIキーを消しても、gitの履歴には残ります。偽のキーを使った実際の実験、キーが漏れたときの正しい順番(先に入れ替え、あとで掃除)、pre-commitフックと.gitignoreで漏れを防ぐ方法、AIエージェントと働くときの習慣を解説します。
コードを書く人、AIにコードを書かせる人なら誰でも、初心者の方でも読めます。セキュリティの知識は不要です。用語にはどれも身近なたとえを添え、コマンドを実行する部分は印をつけてあるので、今は不要なら読み飛ばせます。
この記事に出てくるキーはすべて偽のキーで、AWSの文書に載っている例のキーの組も含みます。数字は公開情報から取っており、末尾に一覧があります。「例」と書いたものは作った例で、実在の人物や会社には触れていません。これは実践のための解説で、あなたのシステムに対するセキュリティの助言ではありません。
まず要点#
- APIキー、パスワード、アクセストークンは秘密情報です。持っている人なら誰でも使えます。家の鍵と同じです。
- ファイルからキーを消しても終わりではありません。 gitは古い版をすべて覚えています。この記事の実験では、2つ目のコミットでキーを消したあとも、
git log -Sが両方のコミットを見つけ、git showがキーをもう一度表示しました。 - キーが漏れたときの正しい順番は、先にキーを無効化または入れ替え、次に履歴を掃除し、そして利用ログを読むことです。 GitHubの文書も同じ趣旨です。
- 履歴を書き換えても、すでにあるコピーは取り戻せません。 実験では、書き換える前に作ったクローンにキーが残っていました。
- 環境変数、
.gitignore、30行ほどのpre-commitフック、プラットフォームのスキャンで早めに止められます。どの層にも抜けがあるので、重ねて使います。 - AIエージェントと働くときは、3つの習慣を足します。本物のキーを会話に貼らない、
.envファイルをエージェントの手の届かない所に置く、コミットの前に差分を読む、です。
むずかしい言葉をやさしく#
| 用語 | やさしい意味 |
|---|---|
| 秘密情報(secret) | 持っている人なら誰でも使えるもの。APIキー、パスワード、アクセストークン、データベース接続文字列、秘密鍵など。家の鍵のようなものです。 |
| コミット(commit) | gitでの1回の保存。名前とメッセージがつき、その時点のすべてのファイルを保存します。 |
| 履歴(history) | これまでのコミットすべて。新しいコミットで1行消しても、古いコミットからは消えません。 |
| ローテーション(入れ替え) | 新しいキーを作り、使う場所をすべて新しいキーに移し、古いキーを無効にすること。錠前を替えて新しい鍵を配るのと同じです。 |
| 無効化(revoke) | キーをすぐに使えなくして、何も開けられなくすること。 |
| 環境変数(environment variable) | コードの外に置いた値で、プログラムが実行時に読むもの。鍵をドアに刻まず、別の箱にしまっておくようなものです。 |
| pre-commitフック | gitがコミットを保存する直前に自動で動くスクリプト。その保存を断ることもできます。 |
| シークレットスキャン/プッシュ保護 | GitHubなどのプラットフォームの機能。プッシュされたコードを調べたり、秘密らしきものがあるとプッシュを止めたりします。 |
この記事のあと、説明できるようになること#
- ファイルからキーを消してもキーが消えない理由と、3つのgitコマンドでの確かめ方。
- キーが漏れたときの正しい順番(先に入れ替え、あとで掃除)と、履歴の書き換えだけでは足りない理由。
- 3つの守りの層(コードの外に置く、コミットで止める、プッシュ後にスキャンする)と、それぞれの抜け。
1. まずイメージ:玄関マットの下の鍵#
家の鍵を玄関マットの下に隠していたとします。翌日、それはまずいと気づいて鍵を引き出しに移しました。ところがその1日のうちに、通りがかった隣人が合鍵を作っていたかもしれません。鍵をしまっても、合鍵は消えません。確実な方法は錠前を替えることだけです。
コードの中のAPIキーも同じです。コミットした時点で、gitはコピーを保存しています。誰かがリポジトリをクローンしたかもしれません。自動のスキャナが公開リポジトリから読み取ったかもしれません。次のコミットで1行消すのは、鍵を引き出しにしまうようなもので、よいことですが、合鍵の問題は解決しません。
教訓: 秘密が隠し場所を出てしまったら、漏れたものとして扱い、しまい直すのではなく錠前を替えます。
2. 何が秘密情報で、どこから漏れるか#
| 種類 | 例 | よく漏れる場所 |
|---|---|---|
| APIキー | クラウドのキー、決済のキー、AIサービスのキー | コードへの直接記述、コミット済みの設定ファイル |
| パスワード | データベースのパスワード、サービスアカウント | コードやREADMEの例の中の接続文字列 |
| アクセストークン | 短期または長期のログイントークン | プログラムのログ、スクリーンショット |
| 秘密鍵 | SSH鍵、署名鍵 | 間違ってリポジトリに入れたファイル |
| 接続文字列 | データベースのアドレスとパスワード | 設定ファイル、出力された環境変数 |
漏れが起きやすい場所:
- gitに直接コミットして、次のコミットで消す(3章で実際に試します)。
.envファイルをコミットしてしまう。.gitignoreに入れ忘れた、または入れる前にコミットした場合です。- キーを含むログやスクリーンショットを、同僚に送ったり掲示板に載せたりする。
- AIとの会話に貼る。「エラーを見せるため」に貼った内容は、会話の履歴に保存され、あとで再表示されることがあります。
- ブラウザ側で動くコード(クライアントバンドル):ユーザーのブラウザに送られるものはすべて読めるので、秘密のキーを置いてはいけません。
教訓: 秘密は1つの扉からだけ漏れるのではありません。だから予防は何層にも重ねます。
3. 実際の実験:消したのに、残っている#
この章のコマンドと出力はすべて、使い捨てのリポジトリで実際に実行したもので、Git 2.56.0とPython 3.14.8、偽のキーだけを使っています。コミットのハッシュは実行のたびに変わります。時間は私のマシンで2コミットのリポジトリを使った値なので、桁を知る目安にしてください。
3.1 偽のキーをコミットし、次のコミットで消す#
git init -q -b main repo
cd repo
# config.pyにはAWSの文書にある例のキーの組(偽のキー)が入っている
# AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"
# AWS_SECRET_ACCESS_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
git add .
git commit -m "add config" # 約0.03秒
# 次のコミットで環境変数から読むように変える
git add .
git commit -m "remove secret from config"
git log --oneline
2bfea15 remove secret from config
a0d01a2 add config
3.2 見た目はきれい#
grep -n AKIA config.py # 一致なし
git grep AKIA # 一致なし。今のファイルだけを探す
どちらも何も表示しません。今のファイルは確かにきれいです。
3.3 でも履歴には残っている#
git log --oneline -S AKIAIOSFODNN7EXAMPLE
2bfea15 remove secret from config
a0d01a2 add config
-Sは、その文字列の出現回数が変わったコミットを探すので、追加したコミットと削除したコミットの両方が出ます。次に、すべてのコミットを探し、古いファイルを表示します。
git grep -n AKIA $(git rev-list --all) # 約0.02秒
git show a0d01a2:config.py
a0d01a2473ddcb923dbfd69164c1df8a11de379b:config.py:3:AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"
import os
AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"
AWS_SECRET_ACCESS_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
REGION = "ap-southeast-1"
3つのコマンドと1秒未満で、このリポジトリを持っている人なら誰でも、2つのキーをもう一度読めます。
教訓: ファイルがきれいでも、リポジトリがきれいとは限りません。履歴を探す必要があります。
4. キーが漏れたときの正しい順番#
「あなたの家の合鍵が作られたらしい」と誰かに教えられたとします。まず錠前を替え、そのあとで誰が入ったかを調べるはずです。
- すぐにキーを無効化または入れ替える。 GitHubのシークレットスキャンの文書は「すぐに」入れ替えることを勧め、キーが無効になっていればgit履歴からの削除は時間がかかり、たいてい不要だと述べています。
- キーの利用ログを読む。 サービス側で、公開されていた間に、誰が、どこから、いつ使ったかを見ます。
- 必要なら履歴を掃除する(6章)。次のプッシュにキーが含まれないようにするためです。
- 記録を残し、 再発を防ぐ仕組みを入れます(5章)。
4.1 AWSのキーの入れ替え:文書にある手順#
AWSの「アクセスキーの更新」の文書は、アプリケーションを止めずに入れ替える手順を説明しています。
- 最初のキーが有効なうちに、2つ目のキーを作る(このとき2つのキーが同時に有効)。
- すべてのアプリケーションとツールを新しいキーに更新する。
- 古いキーの「最終使用」の情報を見て、まだ使われていないか確かめる。文書は、数日待つ方法もあると述べています。
- 古いキーをすぐには削除しない。先に非アクティブにする。
- 新しいキーだけでアプリケーションが動くか確認する。古いキーをまだ使っているものは動かなくなり、一時的に古いキーを有効に戻すこともできます。
- しばらく待ったあと、古いキーを削除する。
この手順は、待つ時間のある通常の入れ替えのために書かれています。すでに漏れたキーについて、古いキーをすぐに無効にするのは、GitHubの「先に無効化」という考え方からの私の推論で、AWSの文書に書いてあることではありません。代償として、古いキーをまだ使っているものはすぐ動かなくなるので、新しいキーを用意してから行います。
4.2 15分でできること、できないこと#
これは私が提案する目標で、実測値ではありません。実際の時間は、すでに権限があるか、キーがどこで使われているかを把握しているかによります。
| 分 | 作業 | 15分でできるか |
|---|---|---|
| 0〜2 | 漏れたキーをサービス側で無効にする | できる(権限があれば) |
| 2〜5 | 新しいキーを作り、秘密の保管場所に入れる | できる |
| 5〜10 | 使っている場所(環境変数、サーバー、CI)を更新する | 少なければできる。全部思い出せないと難しい |
| 10〜13 | 新しいキーでアプリケーションが動くか確認する | できる |
| 13〜15 | 関係者に知らせ、時刻を記録する | できる |
| そのあと | 利用ログを読み、被害を評価する | できない。たいてい数時間かかる |
| そのあと | 履歴を掃除し、全員にpullし直してもらう | できない。キーの入れ替えの代わりにもならない |
| そのあと | このキーが届く他のキーも入れ替える | できない。先に洗い出しが必要 |
教訓: 15分あれば扉を閉めることはできます。被害を調べるのは、そのあとの数時間の仕事です。
5. 早めに止める:何層も重ね、どの層にも抜けがある#
3つの層がある家を想像してください。鍵を持ち出さない、玄関で誰かが声をかける、通りにカメラがある、の3つです。
5.1 第1層:秘密をコードの外に置く#
秘密は環境変数や秘密の保管サービスに置き、設定ファイルには「この変数から読む」とだけ書きます。実験の2つ目のコミットがまさにそうでした。
AWS_ACCESS_KEY_ID = os.environ["AWS_ACCESS_KEY_ID"]
手元で.envファイルを使うなら、ファイルを作る前に.gitignoreに入れます。実際に試しました(私の実行環境では.envという名前のファイルを作れないため、dev.envという名前にしました。仕組みは同じです)。
git status --short
?? dev.env
?? tools/
echo "dev.env" > .gitignore
git status --short
git check-ignore -v dev.env
?? .gitignore
?? tools/
.gitignore:1:dev.env dev.env
dev.envは一覧から消え、check-ignoreは.gitignoreのどの行が止めているかを教えてくれます。ただし、.gitignoreが効くのはgitがまだ追跡していないファイルだけです。以前にコミットしたファイルで試しました。
git add -f old.env
git commit -m "oops: commit old.env"
echo "old.env" >> .gitignore
git ls-files old.env
old.env
.gitignoreに入れても、gitはこのファイルを追跡し続けています。追跡をやめるにはgit rm --cached old.envが必要で、履歴の中のキーは3章のとおり残ります。
5.2 第2層:pre-commitフックでコミット時に止める#
次は小さなスキャナ(Pythonで30行ほど)です。これから追加される行を読み、3つの形を探します。AWS形式のキー、秘密鍵のヘッダー、api_key、secret、token、passwordのような名前に文字列を代入する行です。
PATTERNS = {
"aws access key id": re.compile(r"AKIA[0-9A-Z]{16}"),
"private key header": re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"),
"key or password assigned to a string": re.compile(
r"""(?i)(api[_-]?key|secret|token|passwd|password)\w*\s*[=:]\s*['"][^'"\s]{8,}['"]"""
),
}
# 読むもの:git diff --cached -U0。「+」で始まる行を調べ、一致したら終了コード1で終わる
.git/hooks/pre-commitに設定します。
#!/bin/sh
exec python3 tools/scan_secrets.py
偽のキーを含むファイルをコミットしようとすると:
Blocked: this commit adds something that looks like a secret.
deploy.py: key or password assigned to a string
Use an environment variable, then rotate the key if it was ever real.
exit code: 1
コミットは断られます(試行全体で約0.14秒)。ただし、このスキャナには弱点が2つあり、両方を試しました。
- 見逃し: キーを2つの文字列に分けて(
part1 = "AKIA"、part2 = "IOSFODNN7EXAMPLE"、あとでつなぐ)書くと、コミットは終了コード0で通ります。 - 誤検知: サンプル文書の
password = "change-me-please"という行は、ただの仮の文字なのに止められます。
教訓: 文字のパターンで探すスキャナは、よくあるミスは見つけますが、意図的な、または巧妙なものは見つけられず、ときには誤って警報を鳴らします。保証と考えないでください。
フックは.git/hooksにあり、リポジトリと一緒には配られません。チームで共有するには、フックを配る仕組み(たとえばプロジェクト内のフック管理ツール)が必要で、各自が--no-verifyで飛ばすこともできます。
5.3 第3層:プッシュのあとでスキャンし、プッシュで止める#
プラットフォームにはこのための機能があります。私が読んだGitHubの文書のページ(ページ要約ツール経由)によると:
- シークレットスキャンは、APIキーやパスワードのような漏れた認証情報を自動で検出し、「すべてのブランチのgit履歴全体」と、issue、プルリクエスト、ディスカッション、wikiの文章を調べます。
- プッシュ保護は、秘密を含むプッシュを、リポジトリに届く前に止めます。提供元のパターン、一般的なパターン(秘密鍵やデータベース接続文字列など)、独自のパターンを認識します。リポジトリ単位では初期設定はオフで、書き込み権限のある人は理由を示せば回避できます。ユーザー単位では、GitHub.comの公開リポジトリへ秘密をプッシュするのを止める機能が、初期設定でオンと文書に書かれています。
これらのページは要約ツールを通して読んだだけで、一字一句は確認していません。元のページ(末尾の参考資料)を開き、ご自身のアカウントの設定を見てください。gitleaks、trufflehog、git-filter-repoのような独立したツールもありますが、ここでは実行していません。私のテスト環境にはインストールしていないからです。
6. 履歴の掃除:できることと、できないこと#
キーを入れ替えたあとは、リポジトリからもキーをなくしたくなるかもしれません。現在お勧めされているツールはgit filter-repoですが、私のテストマシンにはなく、実行できませんでした。そこで、Gitに付属するgit filter-branchで実演します(Gitの文書ページは、このコマンドの使用は「推奨されない」と書き、git filter-repoを勧めていますが、動くので説明には十分です)。
git clone -q repo clone-before # 書き換える「前」にコピーを取る
cd repo
git filter-branch -f --tree-filter "python3 redact.py" -- --all # 約0.8秒、5コミット
ここでredact.pyは、すべてのコミットの2つの偽キーの文字列をREDACTEDに置き換えます。確認します。
git log --oneline -S AKIAIOSFODNN7EXAMPLE main
何も表示されません。mainブランチはきれいになりました。ところが、ほかの場所にはまだキーが残っています。
git for-each-ref --format='%(refname)'
refs/heads/main
refs/original/refs/heads/main
ツールはバックアップ用の参照refs/original/refs/heads/mainを残し、git log -S AKIAIOSFODNN7EXAMPLE --allには古い2つのコミットがまだ見えます。掃除するには、バックアップの参照を削除し、reflogを期限切れにし、どこからも指されていないオブジェクトを整理します。
git for-each-ref --format='%(refname)' refs/original | xargs -n1 git update-ref -d
git reflog expire --expire=now --all
git gc -q --prune=now # 約0.1秒
git log --oneline -S AKIAIOSFODNN7EXAMPLE --all --reflog # 何も表示されない
git cat-file -t a0d01a2 # fatal: Not a valid object name
これでこのリポジトリは完全にきれいです。では、書き換える前に取ったコピーは?
cd ../clone-before
git log --oneline -S AKIAIOSFODNN7EXAMPLE
git cat-file -t a0d01a2
2bfea15 remove secret from config
a0d01a2 add config
commit
そのコピーにはまだキーが残っています。だから履歴の掃除は、キーの入れ替えの代わりになりません。以前にpullしたりforkしたりした人は古い版を持ち続けますし、公開リポジトリなら誰かがコピーや保存をしているかもしれません。
もう1つ注意があります。履歴を書き換えるとコミットのハッシュが変わるので、チームメンバーはpullし直す必要があり、競合が起きることもあります。先に知らせてください。記事エージェントを使う人のためのGit入門で、強制プッシュに注意が必要な理由を説明しています。
7. AIエージェントと働くとき:さらに6つの習慣#
業者を家に入れるときを想像してください。マスターキーではなく、有効期限があり、1部屋だけ開く鍵を渡します。
- 本物のキーを会話に貼らない。 AIにエラーを見せる必要があるときは、キーを
<API_KEY>のような仮の文字に置き換えます。 .envファイルを、エージェントが読める場所の外に置く。 またはツールの読み取りを止める設定を使います。プロジェクトのフォルダ全体を読めるエージェントは、その中の秘密も読めます。- 権限が狭く、有効期限の短いキーを使う。 エージェントの作業には、そのタスクに足りるだけで、すぐに期限が切れるキーにします。
- コミットの前に差分を読む。 エージェントは「すぐ動くように」と接続文字列をコードに直接書くことがあります。
git addを1回するだけで、履歴に入ってしまいます。AIが書いたコードのレビューを参照してください。 - エージェントにメインブランチへ直接プッシュさせない。 誰も見ていない状態で、インストールや高権限のコマンドを実行させません。AIアプリのOWASP Top 10も参照してください。
- ルールをチームの方針に入れる。 記事少人数チームのためのAI利用ポリシー1ページには、この種のデータのための「禁止」の列があります。
7.1 例:エージェントにデータベース接続を頼んだ朝#
これは想定した例で、実在の人物や製品には触れていません。名前や値は作ったものです。
この習慣がない場合:
- 10:00。 開発者がエージェントに「データベースにつないで。接続文字列はこれ」と頼み、本物の文字列を会話に貼る。
- 10:10。 エージェントは
config.pyを書き、「すぐ動くように」接続文字列をコードに直接書いてコミットする。 - 10:20。 開発者はざっと見て、動くのを確認してプッシュする。
- 10:45。 プラットフォームから、リポジトリに秘密が検出されたという通知が来る。開発者はその行を消して再プッシュし、終わったと思う。
この習慣がある場合:
- 10:00。 開発者は接続文字列を自分のマシンの環境変数に入れ、エージェントに「
DATABASE_URLから読んで。値をコードに書かないで」と頼む。 - 10:10。 エージェントは環境変数を読む
config.pyを書く。pre-commitフックは何も異常を見つけない。 - 10:20。 開発者は差分を読み、変数名だけなのを確認してプッシュする。
最初の展開になってしまったら、10:45にすべきことは、データベースのパスワードをすぐに入れ替えること、次にアクセスログを読むこと、履歴の掃除を考えるのはそのあとです。次のコミットでその行を消しても何も解決しません。履歴に古い文字列が残り、AIとの会話にも含まれているからです。
8. よくある間違い#
| 間違い | なぜダメか | どうするか |
|---|---|---|
| 新しいコミットでキーを消して終わりにする | 履歴とコピーが残る | まずキーを無効化または入れ替える |
| キーをコミットし、強制プッシュで上書きしてきれいになったと考える | pullやforkをした人は古い版を持ち、プラットフォームもプッシュされた内容を保持しているかもしれない | キーは漏れたものとして入れ替える |
| 漏れたファイルに2つキーがあるのに1つだけ入れ替える | 攻撃者が残りのキーを使う | 漏れたファイルと履歴の秘密をすべて洗い出し、全部入れ替える |
コミットしたあとでファイルを.gitignoreに入れる |
gitはコミット済みのファイルを追跡し続ける | git rm --cached。一度でもプッシュしたなら入れ替えも |
| スキャナを完全に信じる | パターンは分割や符号化されたキーを見逃し、仮の文字を誤検知する | 秘密をコードの外に置き、層を重ねる |
| キーを入れ替えたのに、使う場所を更新しない | アプリケーションが突然動かなくなる | 先に新しいキーを作り、更新してから古いキーを無効にする |
| 漏れたあとに利用ログを読まない | 悪用されたかどうか分からない | 公開されていた間に誰がキーを使ったかを見る |
| 本物のキーをAIとの会話に貼る | 内容が会話の履歴に残るかもしれない | 仮の文字を使う |
9. 使った数字と文書#
| 出典 | 内容 | 読み方 | 注意 |
|---|---|---|---|
| GitHub Docs「About secret scanning」 | すべてのブランチのgit履歴全体をスキャン。すぐに入れ替えることを勧め、無効化済みなら履歴からの削除は時間がかかりたいてい不要 | 「先に入れ替え、あとで掃除」の根拠 | ページ要約ツール経由で読んだ |
| GitHub Docs「About push protection」 | 秘密を含むプッシュを止める。リポジトリ単位では初期設定オフ。理由を示せば回避できる | 守りの一層であり、保証ではない | ページ要約ツール経由で読んだ |
| AWS Documentation「Update access keys」 | 2つ目のキーを作り、アプリを更新し、古いキーを無効化してから削除 | 停止なしの入れ替え | 通常の入れ替え向けで、漏れたキー専用ではない |
| Escape「The State of Security of Vibe Coded Apps」 | 「バイブコーディング」系プラットフォームで作られたアプリで、2,000を超える脆弱性、400を超える秘密の露出、175件の個人情報 | 急いで作ったアプリは秘密を漏らしやすい | セキュリティ会社が自社のサンプルを調べたもの。見たページによってアプリ数が異なる(約1.4千と5.6千)ため件数は引用せず、ページに日付もない |
| この記事の実験 | 削除後も履歴からキーを読める。30行のスキャナには見逃しと誤検知がある。書き換え前のコピーにキーが残る | 仕組みを示すもので、現実の頻度は示さない | 1台のマシン、2〜5コミットのリポジトリ、偽のキー |
使わなかった数字: 「100個のアプリのうち41%で秘密やAPIキーが露出していた」という、VibeWrenchの調査(2026年3月)とされる数字を確かめようとしました。裏づける出典は見つかりませんでした。VibeWrenchの製品ページには、100個のアプリを調べて318件の脆弱性を見つけたとだけあり、キー露出の割合は書かれていません。そのためこの記事では、その数字を使いません。
正しく読むために: 仕組み(gitは履歴を覚えている、コピーは残る)には、自分で再実行できるので良い根拠があります。キーが一般にどのくらいの頻度で漏れるかについて信頼できる数字はなく、pre-commitフック単独の効果を測った研究も見つけられませんでした。
10. 答えつきの練習(30分)#
課題。 使い捨てのリポジトリを作り、偽のキー(上のAWSの例の組のようなもの。本物は絶対に使わない)で次を行います。
- 偽のキーを含むファイルをコミットし、次のコミットでキーを消す。
git log -Sとgit showで、キーがまだあることを示す。- 本物のキーだったと仮定して、5段階の対応の順番を書く。
- スキャン用のpre-commitフックを入れ、直接代入したキーのコミットと、2つの文字列に分けたキーのコミットを試す。
- リポジトリのコピーを取り、
git filter-branchで履歴を書き換え、両方のリポジトリを確認する。
答え。
- 2回のコミットのあと、
git log --onelineは2行を表示する。 git log --oneline -S <キー>が両方のコミットを表示する。git show <最初のコミット>:<ファイル>がキーを表示する。- 妥当な順番:(1) サービス側で古いキーを無効にする、(2) 新しいキーを作って使う場所を更新する、(3) アプリケーションを確認する、(4) 利用ログを読む、(5) 必要なら履歴を掃除し、チームに知らせる。採点のポイントは、入れ替えが履歴の掃除より前にあること。
- 直接代入したキーのコミットは止められる(終了コード1)。分けたキーは、スキャナが完全なパターンしか探さないため止められない。
- バックアップの参照を消し、reflogを期限切れにして
gcを実行すると、書き換えたリポジトリはきれいになる。以前に取ったコピーにはキーが残る。スキャナの改善案:追加行だけでなくファイル全体をスキャンする、文字列のランダムさで判定する、専用ツールを使う。
11. プッシュする前のチェックリスト#
- この変更に、本物の秘密(キー、パスワード、接続文字列、秘密鍵)は入っていないか。
- 秘密は環境変数や秘密の保管場所にあり、コードの中にないか。
.envなどのファイルを、ファイルを作る前に.gitignoreに入れたか。- コミットの前に差分を読み、変に長い文字列を探したか。
- 手元でスキャンのフックが動き、プラットフォーム側のスキャンもオンか。
- 使っているキーは権限が狭く、有効期限が短いか。
- 本物のキーをAIとの会話に貼ったことはないか。あるなら入れ替える。
- 今キーが漏れたら、どこで入れ替えるか、どのくらいかかるか、誰に知らせるかを分かっているか。
12. バランスよく:分かっていること、まだ分からないこと#
かなり確か: gitは履歴を保持し、消したキーを再表示できる。履歴の書き換えは、すでにあるコピーを取り戻せない。文字のパターンで探すスキャナは、見逃しも誤検知もある。キーの入れ替えが根本の一手である。4つとも、この記事で実際に実行しました。
不明、または限られていること:
- キーが現実にどのくらいの頻度で漏れ、どのくらい被害が出るか。今ある数字は主にセキュリティ会社が自社のサンプルを調べたもので、確認できなかった数字は外しました。
- 各防御層の実際の効果。pre-commitフックやプッシュ保護の単独の効果を測った研究は見つかりませんでした。
- 専用ツール(git-filter-repo、gitleaks、trufflehog)は実行していません。使う前に文書を読んでください。
- プラットフォームごとに機能や初期設定が違います。GitHubのページは要約ツール経由で読んだもので、変わっているかもしれません。
大げさにしない: どの層も1つだけでは足りません。軽く見ない: 予防のコストは設定の数分ですが、漏れたキーは他人があなたのデータや資源に触れる原因になりえます。
13. まとめ#
APIキーが一度gitに入ったら、手元を離れたものとして扱います。次のコミットで1行消すのは、鍵を引き出しにしまうだけで、合鍵は外にあるままです。まずキーを入れ替え、ログを読み、そのあとで履歴を掃除します。繰り返さないために、秘密をコードの外に置き、コミットで止め、プッシュのあとでスキャンし、AIエージェントには権限が狭く期限の短いキーを渡し、コミットの前には必ず差分を読みましょう。
参考資料#
数字は2026年10月時点の公開情報です。GitHubとAWSのページはページ要約ツール経由で読んだので、引用する前に元のページで確認してください。
- GitHub Docs、About secret scanning
- GitHub Docs、About push protection
- AWS Documentation、Update access keys(IAMユーザーガイド)
- Escape、The State of Security of Vibe Coded Apps(日付なし、セキュリティ会社による独自の調査)
- Git、git-filter-branchとgit-log(-Sオプション)
よくある質問
- APIキーをコミットしてしまい、次のコミットで消しました。これで安全ですか?
- いいえ。gitは履歴をすべて保存しているので、古いコミットに対してgit log、git show、git grepを使えばキーを読めます。この記事の実験でも、ファイルからキーを消したあとにgit log -Sを実行すると両方のコミットが見つかり、git showがキーをそのまま表示しました。最初にすべきことはキーの無効化または入れ替えです。キーが生きている間は、どのコピーも危険だからです。
- キーが漏れたとき、履歴の掃除と入れ替えのどちらが先ですか?
- 先にキーを入れ替えるか無効化します。GitHubのシークレットスキャンの文書も、すぐに入れ替えることを勧め、キーが無効になっていればgit履歴からの削除は時間がかかり、たいてい不要だと述べています。そのあとで履歴を掃除し、公開されていた間にキーが使われていないか利用ログで確かめます。
- git履歴を書き換えれば(filter-branchやgit-filter-repo)、キーは完全に消えますか?
- 書き換えたリポジトリからは消えますが、すでにあるコピーからは消えません。実験では、書き換える前に作ったクローンにキーが残っていました。ツールはバックアップ用の参照とreflogも残すので、それらも掃除が必要です。だから履歴の書き換えは、キーを入れ替えたあとの補助的な作業です。
- pre-commitフックや秘密情報スキャナは、すべて見つけてくれますか?
- いいえ。スキャナは文字のパターンで探すので、見逃しも誤検知もあります。実験では、30行ほどのスキャナは直接代入されたキーを止めましたが、2つの文字列に分けたキーは通し、サンプルのパスワードを誤って止めました。追加の一層として使い、キーの入れ替えや、秘密をコードの外に置くことの代わりにはしないでください。
- AIエージェントと働くとき、APIキーについて何に気をつけますか?
- 本物のキーを会話に貼らない、.envファイルをエージェントが読める場所の外に置く、権限が狭く有効期限の短いキーを使う、コミットの前に差分を読んでエージェントがキーをコードに直接書いていないか確かめる、の4点です。git addを1回するだけで、履歴に入ってしまいます。