Stripe のサンプルリポジトリを GitHub に push したら、コミットに本物のキーは 1 つも含めていないのに、.env.example の 2 行だけで Push Protection に弾かれました。該当行を消した新しいコミットを足してもう一度 push しても、また同じエラーで止まります。
何度直しても通らない事象の正体は、2 つの構造のズレでした。1 つは検知器が「鍵の形」だけで判定しているのに、書き手は「ダミーだから OK」と思って書いている認知のズレ。もう 1 つは Push Protection が push に含まれるコミット列全体をスキャンするので、悪いコミットがブランチの履歴に残ったままだと何度新しいコミットを足しても止まる、という Git 側の構造です。
この記事では、実際に出たエラーを起点に、なぜこのズレが事故になるのか、ブランチ先端にある「悪いコミット」を中身ごと作り直す手順、そして同じ事故を再発させない .env.example の運用まで整理します。
何が起きたか
git push を実行したところ、リモートから次のような内容が返ってきました(要点のみ抜粋)。
remote: error: GH013: Repository rule violations found for refs/heads/main
remote: —— GITHUB PUSH PROTECTION ——————————————————————————————
remote: Resolve the following violations before pushing again
remote:
remote: —— Highnote SK Test Key ————————————————————————————————
remote: locations:
remote: - commit: 6f8dbee...
remote: path: .env.example:2
注目すべき点が 2 つあります。
1 つ目は、検知名が Highnote SK Test Key であること。Stripe のサンプルとして書いた STRIPE_SECRET_KEY= の行が、Stripe ではない別の決済プロバイダ(Highnote)のテストキー検知パターンに先にマッチしたという意味です。エラーメッセージのプロダクト名で混乱しないように、まずここを押さえます。
2 つ目は、検知箇所が .env.example であること。.env ではなく .env.example、しかも 2 行目。実キーは 1 つも書いていないつもりで、書いていたのは「ダミーのプレースホルダ」だけでした。問題になっていた .env.example は次のような構造でした(鍵文字列そのものは再掲しません)。
STRIPE_SECRET_KEY=の右辺が、sk_test_に続けて十分長い「ランダムっぽいマスク文字」の列NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=も同様にpk_test_+ 長い列
公式の検知パターン一覧は Supported secret scanning patterns にまとまっていますが、Stripe・Highnote を含む多くの決済系プロバイダのテストキーが対象に入っています。
なぜ起きるか
ここからが本題です。なぜ「ダミーのつもり」で書いた行が止まるのか、そして「修正コミットを 1 つ足す」のがなぜ通らないのか、2 つの仕組みを順に見ます。
検知器は「意味」ではなく「形」で判定する
Supported secret scanning patterns に並ぶ各プロバイダのパターンは、基本的に「プレフィックス + 一定の長さのランダム風文字列」をマッチさせる作りになっています。検知器は値の意味を見ません。sk_test_ で始まり、その後に十分な長さの文字列が続いていれば、それが書き手の手元の本物だろうが、ドキュメント用に手で打ったダミーだろうが、同じシグナルとして反応します。
書き手の認知では「.env.example だから当然ダミー」「読みやすい設定例として sk_test_xxxxxxxxxxxx... の形を見せたい」となります。検知器の認知では「形が揃っているのでブロック対象」になります。このギャップが、Stripe のサンプルを GitHub で公開しようとした人が必ずどこかで踏む落とし穴です。
検知名が Highnote SK Test Key だったのも、同じ理由で説明がつきます。Stripe の sk_test_ 形式と類似フォーマットを持つ別プロバイダのパターンが先にマッチしただけで、Stripe 専用のシグナルが反応したわけではありません。
Push Protection はコミット列全体をスキャンする
「ダミー鍵を消した新しいコミットを足せば push が通る」と思いがちですが、ここで Push Protection の 2 つ目の仕組みが効いてきます。Push Protection は push に含まれるコミット列のどこかに 検知パターンへのヒットがあれば、その push を拒否する設計です(Working with push protection from the command line)。
つまり、
- 悪い行を含むコミット
6f8dbeeを一度作る - あとから
.env.exampleを空に直した新しいコミットを積む
の 2 つを順番にやって push しても、まだ 6f8dbee がブランチの履歴に残ったまま push に乗ります。スキャナは古いコミットも読みに行くので、再び同じエラーで弾かれます。何度新しいコミットを足しても通らないのはこのためです。
対処の方向は次のどちらかになります。
- 悪いコミットがまだリモートに乗っていない場合は、そのコミットの中身ごとブランチから作り直す
- すでにリモートに乗っている場合は、履歴の書き換えと
git push --force-with-lease、組織ポリシーに応じた bypass フローなど、運用側の手順が必要
本記事は前者、つまり「まだ push が拒否され続けていて、悪いコミットがリモートに乗っていない」状態を扱います。後者は本番のキー漏洩を伴う可能性があるため、ここでは扱いません。
どう直すか
前提を整理します。
- 問題のコミット(仮に
6f8dbeeとします)がブランチ先端にある - そのコミットはまだリモートに乗っていない(push が拒否され続けている)
この前提なら、やることは「先端コミットを別ハッシュで作り直す」だけです。アプローチは 2 通りあります。両者は結果が等価です。
公式推奨の git commit --amend --all で書き換える
GitHub の公式ドキュメントが推奨しているのは、先端コミットを直接書き換える方法です。
# 1. .env.example を編集(右辺を空に、後述の運用に変更)
# 2. 編集内容を先端コミットに畳み込む
git commit --amend --all
# 3. 再度 push
git push
--amend --all は「先端コミットの内容を、ステージング済みかどうかに関わらず現在の作業ツリーの状態に置き換える」動きをします。元の 6f8dbee のハッシュは消え、新しいハッシュのコミットがブランチ先端に乗ります。
git reset --soft HEAD~1 で作り直す(別解)
--amend の挙動が手元の修正範囲と一致するか不安なときは、一度コミットだけ取り消してステージングを目で確認する流れも使えます。
git reset --soft HEAD~1
これで先端コミットだけが取り消され、そのコミットで入れた変更一式はインデックスと作業ツリーに残ります。続けて .env.example と README を編集し、
git add .env.example README.md
git commit -m "(元と同じ趣旨のメッセージ)"
git push
として 1 本のコミットに組み直します。結果として 6f8dbee はブランチから消え、push に乗るコミット列のどこにも検知対象の文字列が含まれなくなり、git push が通ります。
--amend --all と git reset --soft HEAD~1 → git commit は、ブランチ先端のコミットを別ハッシュで作り直すという結果が同じです。コミットメッセージの組み直しや、複数ファイルにまたがる変更を 1 つに畳み込むタイミングを目視で確認したいなら後者、最短の 2 コマンドで済ませたいなら前者を選びます。
.env.example を直す
修正するときは、「鍵の形」を値として書かない方針に切り替えます。
STRIPE_SECRET_KEY=/NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=/STRIPE_PRICE_ID=などは右辺を空にするsk_test_やpk_test_の形はコメントだけで説明する(「Stripe ダッシュボードからコピーした値を.env.localに貼り付ける」など)
これで .env.example 内には検知対象になり得る文字列が一切残らなくなります。
bypass URL は使わなかった
GitHub はエラーログに「この検知を bypass する URL」を併記してきます。これを踏めば push 自体は通せます。それでも今回は使いませんでした。
公開サンプルでは、クローンする全員が同じリポジトリの履歴を手元に持ちます。そこに「allow を踏まないと初回 push できないコミット」が残っているのは、配布物として不利です。bypass フローは「organizational secret として登録済みのキーを意図的に push したい」「内部利用に限られたリポジトリで一度だけ通したい」など、運用側で明確な合意がある場合に使う選択肢で、誰がクローンしても再現性を持たせたい公開サンプルでは、リポジトリの履歴側を直すほうが扱いやすくなります。
ただし運用判断としての限界もあります。書き手のリポジトリが組織所有で、組織ポリシーで履歴の書き換えが禁止されている場合は、bypass フローを使うか、新しいブランチを切って作り直すかを管理者と相談する必要があります。組織所有の Public リポジトリでは、安易な force push が壊しうるものが個人リポジトリより多いです。
再発防止 — .env.example を「設計図」ではなく「空欄リスト」として運用する
事故が起きた根本は、.env.example を「設定例として読みやすくしたい」という動機で値の形まで書いてしまったことでした。書き手の意図は「クローンした人が .env.local を書きやすいように、形を見せておく」だったのですが、検知器の側から見れば「sk_test_ + 長い文字列」はそのまま検知対象です。
そこで今後の .env.example の運用ルールは次のようにします。
- 値は空(
KEY=まで書いて改行) - 鍵の形(
sk_test_/pk_test_の長さや構造)はコメントだけで説明する - 実キーは
.env.localまたは CI のシークレットだけに置く
合わせて、アプリ側に「必須 env が足りない」というセットアップ案内をきちんと出す実装を入れておきます。空の .env.example をクローンしても、npm run dev した瞬間に「STRIPE_SECRET_KEY が未設定です。docs/setup.md を確認してください」のようなエラーが出れば、初回セットアップは破綻しません。.env.example が読みやすい設定例として機能しなくても、アプリ側の案内で代替できます。
公開サンプルとして配布する場合は、Stripe 公式の Best practices for managing secret API keys に従い、実キーをリポジトリに含めない運用は前提として揃えておきます。pre-commit hook を使った検知(gitleaks / trufflehog など)の導入は、本記事のスコープを超えるので別途検討してください。
まとめ
今回起きたのは、Stripe のサンプル特有のバグではありません。プレフィックスと長さでマッチする汎用の秘密検知に、Stripe のテストキー形式が偶然マッチしていただけです。同じ落とし穴は、sk_test_ に類似したフォーマットを持つ別プロバイダのサンプル(Highnote / Square / その他)を作る場合にも同じ構造で起きます。
解決の要点は 2 つです。1 つは .env.example の値を空にして「鍵の形はコメントだけ」に切り替えること。もう 1 つは Push Protection がコミット列全体をスキャンするので、ブランチ先端の悪いコミットを別ハッシュで作り直す必要があること。修正コミットを足し算で増やしても通らないのは、検知器の挙動として正しい動作で、ここを理解しないまま push リトライを繰り返すと時間だけが溶けます。
公開サンプルを Stripe で書く立場として一番事故りにくいのは、「.env.example は鍵の形をコメントで説明する空欄リスト、形を見せるのは README やセットアップ手順の本文側」という分業です。サンプルの読みやすさと検知器の認知を分けて設計することで、git push が止まる事故をそもそも作らない運用にできます。
参考
- Working with push protection from the command line — 公式の解消手順、
git commit --amend --allの根拠 - Supported secret scanning patterns — 検知対象のプロバイダ一覧
- Stripe: Best practices for managing secret API keys — Stripe 公式のキー管理ベストプラクティス