APIキーやパスワードをGitHubにpushしてしまったときの対応

公開 更新 書いた人 大林 諒

APIキーやパスワードをGitHubに送ってしまったら、最初にやるのは履歴の削除ではありません。鍵を止めて作り直すことです。APIキーは、外部のサービスを動かすための合言葉のような文字列で、手に入れた人はそのサービスを使えます。履歴を消す作業には時間がかかり、消しても他の人の手元に残ります。止めてしまえば、残っていても使えません。

読み終わると、何をどの順番でやるか、どこまで消せてどこから消せないかが分かり、次に同じことが起きない形まで作れます。

この記事の内容
  1. 先に答えると
  2. 順番の全体
  3. ①鍵を止める
  4. ②使われた形跡を見る
  5. 消すコミットでは消えない
  6. pushする前に気づいたとき
  7. pushしたあとに履歴から消す
  8. 書き換えても戻ってくる
  9. ④他の人に知らせる
  10. GitHubの検出機能の範囲
  11. 手元で止める道具 gitleaks
  12. 再発を防ぐ
  13. 個人情報を漏らしたとき
  14. できないことと、その代わり
  15. よく詰まるところ
  16. 次に読む

先に答えると

順番は、鍵を止める、使われた形跡を見る、履歴から消す、関係者に知らせる、再発を防ぐ、です。GitHub公式の手順も、履歴を書き換える前に「revoke and/or rotate」(無効化か作り直し)を求めています。今すぐやるのは1番目だけです。

  • 鍵を消すコミットをしても、過去のコミットに鍵は残ります
  • 公開リポジトリなら、すでに誰かに取られた前提で動きます
  • 履歴から消すのは git filter-repo です。上書きのpushと、GitHubサポートへの連絡まで要ることがあります
  • 他の人のクローン(手元の複製)とフォーク(GitHub上の複製)に残った分は、こちらからは消せません
  • Freeの非公開リポジトリではGitHubの検出機能が使えないので、手元で止める道具を入れます

順番の全体

順番やること具体的には急ぎ
①鍵を止める発行元の管理画面で無効化し、新しい鍵を作って差し替える今すぐ
②使われた形跡を見る発行元の利用履歴・請求額、GitHubのSecurity log今日中
③履歴から消すgit filter-repo、force push、必要ならサポートへ連絡落ち着いてから
④他の人に知らせるクローンを消して取り直してもらう③と同時
⑤再発を防ぐ鍵の置き場所、送る前の検査今週中
漏らしたときに進める順番。最初は鍵を止める
漏らしたときに進める順番。最初は鍵を止める

①鍵を止める

サンプル料理教室の代表が、予約メールのスクリプト send_mail.py にメール配信サービスの鍵を書いたまま、リポジトリ reservation-mail に送ってしまったとします。リポジトリは、ファイルと履歴をまとめて入れておく保管場所です。

  1. メール配信サービスの管理画面を開きます。
  2. 新しい鍵を作ります。
  3. サーバーの .env(鍵を書いておく設定ファイル)を新しい鍵に差し替えます。
  4. 予約メールが届くかを確かめます。
  5. 古い鍵を無効化します。

これは止まる時間を短くしたい順番です。公開リポジトリに出てしまったなら、予約メールが少し止まっても先に古い鍵を止めます。何分で拾われるかの数字はGitHubの公式資料にはありません。ただ、公開リポジトリの中身は誰でも見られるので、自動で集めるプログラムが巡回している前提で動きます。

GitHubのトークン(パスワードの代わりに使う文字列)については、有効なものを公開リポジトリに送ると、GitHubが自動で無効にします。外部サービスの鍵まで止めてくれるとは考えず、自分で止めます。非公開リポジトリでも止めます。招待した人全員とそのクローンに鍵が渡っているからです。

②使われた形跡を見る

見る場所は、まず鍵の発行元です。メール配信サービスなら送信履歴と請求額、クラウドなら利用履歴です。身に覚えのない送信や請求があれば、発行元のサポートに連絡します。

GitHub自身のアカウントが心配なときは、Security logを見ます。

  1. 右上のプロフィール写真から「Settings」を開きます。
  2. サイドバーの「Archives」にある「Security log」を開きます。
  3. 身に覚えのない操作がないかを見ます。直近90日分の操作、実行した人、国、日時が並びます。
Security logの画面
Security logの画面

消すコミットでは消えない

ここからは練習用のフォルダでGit(ファイルの変更履歴を残す仕組み)を動かします。リモート(送り先)もGitHubではなく同じフォルダの中に作りました。鍵は xxxxxxxx-fake-key という偽物です。鍵を直接書いた send_mail.py をコミットして送ったあと、気づいて鍵を消しました。コミットは、変更を履歴に1回分記録する操作です。

$ git commit -m "予約メールのスクリプトを追加"
[main (root-commit) 3cf1bfe] 予約メールのスクリプトを追加
 1 file changed, 2 insertions(+)
 create mode 100644 send_mail.py
$ git push -u origin main
To ../remote.git
 * [new branch]      main -> main
branch 'main' set up to track 'origin/main'.
$ cat send_mail.py
import os
MAIL_API_KEY = os.environ["MAIL_API_KEY"]
print("send")
$ git commit -m "鍵をコードから消す"
[main 3e5c9e0] 鍵をコードから消す
 1 file changed, 2 insertions(+), 1 deletion(-)
$ git push
To ../remote.git
   3cf1bfe..3e5c9e0  main -> main

いまのファイルを検索すると、鍵は見つかりません。

$ git grep fake-key

何も出ないので消えたように見えます。履歴を見ます。

$ git log -p -- send_mail.py
commit 3e5c9e0e7b873983c06fcb7e7eda415d669f4e77
Author: 代表 <daihyo@sample-cooking.example>
Date:   Sat Nov 7 09:25:16 2026 +0900

    鍵をコードから消す

diff --git a/send_mail.py b/send_mail.py
index e541460..37c5f8e 100644
--- a/send_mail.py
+++ b/send_mail.py
@@ -1,2 +1,3 @@
-MAIL_API_KEY = "xxxxxxxx-fake-key"
+import os
+MAIL_API_KEY = os.environ["MAIL_API_KEY"]
 print("send")

commit 3cf1bfe691057f771535f8f7592f55155d65ed68
Author: 代表 <daihyo@sample-cooking.example>
Date:   Sat Nov 7 09:25:16 2026 +0900

    予約メールのスクリプトを追加

diff --git a/send_mail.py b/send_mail.py
new file mode 100644
index 0000000..e541460
--- /dev/null
+++ b/send_mail.py
@@ -0,0 +1,2 @@
+MAIL_API_KEY = "xxxxxxxx-fake-key"
+print("send")

鍵はそのまま読めます。消すコミットも「この行を消した」という記録なので、消した行を載せています。git revert(打ち消すコミットを足す操作)で戻しても同じです。

どのコミットに鍵が入っているかは git log -S で探せます。あとからクローンした人の手元でも、同じものが見えます。

$ git log -S fake-key --oneline
3e5c9e0 鍵をコードから消す
3cf1bfe 予約メールのスクリプトを追加
$ git clone -q ../remote.git ../reservation-mail-b
$ git -C ../reservation-mail-b log -S fake-key --oneline
3e5c9e0 鍵をコードから消す
3cf1bfe 予約メールのスクリプトを追加
$ git -C ../reservation-mail-b show HEAD~1:send_mail.py
MAIL_API_KEY = "xxxxxxxx-fake-key"
print("send")
消すコミットを足しても、古いコミットに鍵が残る
消すコミットを足しても、古いコミットに鍵が残る

pushする前に気づいたとき

まだ送っていないなら話は簡単です。鍵は手元から出ていないので、止める必要もありません。直前のコミットを作り直します。次の例は、料金表の修正に git add . で .env まで混ざった場合です。

$ git status -sb
## main...origin/main [ahead 1]
$ git log origin/main..HEAD --oneline
b386b6f 料金表を修正
$ git show --stat --format=%h HEAD
b386b6f

 .env      | 1 +
 menu.html | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

ahead 1 は、送っていないコミットが1つあるという印です。.env を外し、.gitignore(Gitに入れないファイルの一覧)を足してコミットをやり直します。

$ git rm --cached .env
rm '.env'
$ echo '.env' > .gitignore
$ git add .gitignore
$ git commit --amend --no-edit
[main fe140d7] 料金表を修正
 Date: Sat Nov 7 09:25:16 2026 +0900
 2 files changed, 2 insertions(+), 1 deletion(-)
 create mode 100644 .gitignore
$ git log -S fake-key --oneline
$ git push
To ../remote.git
   edcb6a5..fe140d7  main -> main

--amend は、直前のコミットを作り直す操作です。GitHubの大きなファイルの案内にも、同じ手順があります。

pushしたあとに履歴から消す

ここは止めたあとの作業です。GitHub公式の手順を、ファイル名だけ例に合わせて写します。この節のコマンドは手元で動かしていないので、実行結果は載せません。先に、公式が挙げている副作用を確かめます。

副作用内容
全部のハッシュが変わる該当のコミット以降、コミットの番号がすべて変わる
古いクローンから戻る古いクローンの人がpull(更新の受け取り)のあとpushすると鍵が戻る
他の人の作業が消える作業中のブランチ(変更を分けて作る枝)を上書きすると変更が失われる
プルリクエストの差分が消える閉じたプルリクエスト(Pull Request、PR。変更の合流の依頼)の差分が見られなくなる。開いているものは先にマージか閉じる
署名が消えるコミットやタグの署名がなくなる
鍵の場所を教えることになる古いクローンの人は、履歴の食い違いから鍵を見つけられる

手順です。

  1. git filter-repo を入れます。Macなら brew install git-filter-repo です。2.47以上が要ります。
  2. 普段の作業フォルダではなく、新しくクローンします(下のコマンド①)。
  3. .env のようにファイルごと消すなら、コマンド②を実行します。ファイル名だけでなくGit上のパス全体を書きます。
  4. send_mail.py のようにファイルの中の文字列だけ消すなら、鍵を書いたテキストをリポジトリの外に作り、コマンド③で指定します。
  5. 消えたかを確かめます。git log -S で探すのが手早い方法です。
  6. 影響するプルリクエストの数をコマンド④で数えます。多すぎるなら、ここで止められます。次の手順からは取り消せません。
  7. コマンド⑤で上書きのpush(force push)をします。
  8. refs/pull/ で始まるものは必ず失敗します。GitHubが読み取り専用にしているためです。それ以外が失敗したら、ブランチ保護(直接の変更を禁じる設定)を一時的に解除してやり直します。
  9. プルリクエストが影響するときや、キャッシュも消したいときは、GitHub Support portalから連絡します。オーナー名とリポジトリ名、影響するプルリクエストの数、出力にある「NOTE: First Changed Commit(s)」の内容を伝えます。
  10. 他の人とフォークの持ち主に連絡します。
①
git clone https://github.com/sample-cooking/reservation-mail
cd reservation-mail
②
git-filter-repo --sensitive-data-removal --invert-paths --path .env
③
git-filter-repo --sensitive-data-removal --replace-text ../passwords.txt
④
grep -c '^refs/pull/.*/head$' .git/filter-repo/changed-refs
⑤
git push --force --mirror origin

連絡が要る理由は、force pushの先にサーバー側で何が残るかを見ると分かります。

履歴を書き換えるとコミットの番号が変わり、他の人の手元には古いものが残る
履歴を書き換えるとコミットの番号が変わり、他の人の手元には古いものが残る

書き換えても戻ってくる

git filter-repo が手元にないので、直前のコミットを --amend で作り直してforce pushする例で同じことを確かめます。.env が入ったコミットを送ったあと、外注エンジニアBがクローンを持っている状態です。

普通のpushは断られます。履歴が食い違うからです。--force-with-lease は、自分の知らない更新がリモートに入っていないときだけ上書きする、--force より安全な指定です。

$ git log --oneline
d0c788f 料金表を修正
c56d07c 最初のコミット
$ git push
To ../remote.git
 ! [rejected]        main -> main (non-fast-forward)
error: failed to push some refs to '../remote.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. If you want to integrate the remote changes,
hint: use 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
$ git push --force-with-lease
To ../remote.git
 + 714078a...d0c788f main -> main (forced update)
$ git log --all -S fake-key --oneline

手元では消えました。ところがサーバー側で古いコミットの番号を指定すると、まだ読めます。

$ git -C ../remote.git show 714078a:.env
MAIL_API_KEY=xxxxxxxx-fake-key

GitHubでも、古いコミットの掃除はサポートの作業です。これがサポートへ連絡する理由です。次に、Bの手元です(git pull の案内とパスの行は省略しました)。

$ git log -S fake-key --oneline
714078a 料金表を修正
$ git pull
fatal: Need to specify how to reconcile divergent branches.
$ git pull --no-rebase --no-edit
Merge made by the 'ort' strategy.
 .gitignore | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 .gitignore
$ git ls-files
.env
.gitignore
menu.html
$ git push
   d0c788f..c1f7042  main -> main

Bは悪気なくマージ(枝の合流)してpushしました。代表の側で確かめます。

$ git fetch
From ../remote
   d0c788f..c1f7042  main       -> origin/main
$ git log origin/main -S fake-key --oneline
714078a 料金表を修正
$ git show origin/main:.env
MAIL_API_KEY=xxxxxxxx-fake-key

消したはずの鍵が戻りました。公式が古いクローンの扱いを細かく書いているのは、このためです。

④他の人に知らせる

書き換えの前に作業を止めてもらい、終わったら次を伝えます。外注エンジニアBへの連絡はこの程度で足ります。

reservation-mailの履歴を書き換えました。
今のクローンは使わず、削除してクローンし直してください。
古いクローンからpushしないでください。
作業中の変更があれば先に連絡をお願いします。

公式は古い履歴のブランチについて「rebase, not merge」(合流させずに付け替える)と書いていますが、古いクローンを捨てる方が確実です。フォークは別です。持ち主に削除を頼むしかなく、GitHubは持ち主の連絡先を教えてくれません。

GitHubの検出機能の範囲

GitHubには、鍵らしいものを見つけるSecret scanningと、送る時点で止めるpush protectionがあります。使えるかどうかはリポジトリの種類で変わります(2026年11月時点)。

種類Secret scanningpush protection
公開リポジトリ無料で自動個人アカウントの設定で既定で有効
Freeの非公開リポジトリ使えない使えない
Team・Enterprise Cloudの組織の非公開GitHub Secret Protectionを有効にすれば使える同じくSecret Protectionが必要

サンプル料理教室のように非公開で使っていると、この検出は働きません。手元で止める道具が要ります。

手元で止める道具 gitleaks

gitleaksは、リポジトリやファイルの中からパスワードやAPIキーを探す道具です。ライセンスはMITで、GitHubの公式手順でもコミット前の検査の例に挙がっています。コマンドは公式のREADMEから写しました。

brew install gitleaks
gitleaks git -v path_to_repo
gitleaks dir -v path_to_directory_or_file

git は履歴の検査、dir はフォルダやファイルの検査です。--redact を付けると、見つけた鍵を画面に出しません。コミットのたびに自動で走らせるなら、pre-commit という別の道具を使い、リポジトリの一番上に .pre-commit-config.yaml を置いて pre-commit install を実行します。

repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.24.2
    hooks:
      - id: gitleaks

なお、READMEには機能開発を終えて今後はセキュリティの修正だけになるとあり、作者は開発の中心を Betterleaks という別の道具に移すと書いています(2026年11月時点)。検査に使う分には困りませんが、入れるときにREADMEの最新の案内を読みます。

再発を防ぐ

決めること中身
鍵はコードに書かないsend_mail.py は os.environ で読み、値は .env に置く
.env はGitに入れない.gitignore に書き、見本の .env.example だけ入れる
鍵の原本は1か所パスワード管理ツールに集める。チャットやメールで渡さない
送る前に検査ファイルを名指しして git add、git diff --cached で中身を見る、gitleaks
鍵の範囲を絞る止めるときの影響が小さくなるよう、用途ごとに分けて期限を付ける
AIにも鍵を渡さない鍵をチャットに貼らない。読めないファイルに指定する

最後の行は、AIと一緒にコードを直すようになって増えた話です。実際に運用していると、AIは動かすために .env を読もうとしますし、読んだ値をコードに直接書いてしまうこともあります。渡さない形にしておくのが確かです。Claude Codeなら、設定ファイル ~/.claude/settings.json の permissions に次を書くと、標準の読み取りと cat などのコマンドでは .env を読めなくなります。

"deny": [
  "Read(./.env)",
  "Read(./.env.*)"
]

完全には防げません。ファイル名を指定しない grep -r や、Pythonなどのスクリプトからの読み取りには効かないので、すべて止めるにはサンドボックス(OSの側で読み取りを止める仕組み)を使います。

個人情報を漏らしたとき

顧客の名簿や受講者の連絡先が入っていたら、鍵とは別の対応になります。鍵は作り直せますが、出た情報は取り戻せません。履歴から消す作業は同じようにやったうえで、誰の何が何件、いつからいつまで、誰に見える状態だったかを整理します。

法律上の報告と本人への通知が必要になる場合があります。法的な判断は自分で決めず、個人情報保護委員会のサイトの、漏えい等の対応のページを確認し、必要なら弁護士に相談します。公開か非公開かで見える範囲は変わるので、招待していた人の一覧も控えておきます。

できないことと、その代わり

できないことその代わり
他の人のクローンから消す消して取り直してもらう。鍵は止めてあるので実害は防げる
フォークから消す持ち主に頼む
取られたかどうかを確かめる発行元の利用履歴を見る。分からなければ取られた前提で作り直す
Freeの非公開でGitHubに検出させるgitleaksを手元で動かす。必要ならTeamとSecret Protectionを検討する
出た個人情報を取り戻す影響範囲を整理し、報告や通知の要否を確認する

よく詰まるところ

症状原因直し方
鍵を消したのに git log -p で見える消すコミットは履歴を消さない鍵を止めてから git filter-repo を使う
--amend のあと git push が rejected になる履歴が食い違っている送ったあとなら他の人に伝えてから git push --force-with-lease
force pushが拒否されるブランチ保護で禁止されている一時的に解除し、終わったら戻す
refs/pull/ で失敗と出るGitHubの読み取り専用の参照正常。他に失敗がなければサポートへ連絡する
消したはずの鍵が戻った古いクローンからマージしてpushされた全員にクローンを取り直してもらってから、もう一度消す
git filter-repo が見つからないGitとは別の道具で、入っていないbrew install git-filter-repo で入れる
鍵を止めたら予約メールが止まったサーバーの .env が古い鍵のまま新しい鍵に差し替えて、送信を確かめる

次に読む

置いてはいけないファイルの書き方は「.gitignoreの書き方と、リポジトリに入れてはいけないファイル」にあります。トークンの範囲や招待した人の見直しは「GitHubの権限と2段階認証:共同作業者・Organization・非公開リポジトリ」です。git revert や git reset の違いは「Gitで元に戻す:revert・reset・restoreの違いと使い分け」に、AIと一緒に使うときの注意は「GitHub CopilotとClaude Code:AIでコードを書くときのGitの使い方」にまとめています。

あわせて読む