APIキーをGitHubに上げて30分で悪用された。秘密情報を漏らさない手順
APIキー・トークン・DBパスワードを.envと.gitignoreで隔離し、コミット前にgit-secretsで検出、漏れたら即ローテーション。Claude Codeに秘密を渡さない設計まで僕の失敗談つきで。
テスト用のリポジトリを「ちょっとだけ公開」した日のことです。AWSのアクセスキーが書かれた.envを、.gitignoreに入れ忘れたまま一緒にpushしてしまいました。
気づいたのは翌朝。請求アラートのメールでした。誰かが僕のキーを拾って、知らないリージョンで大量にインスタンスを立てていたんです。
公開してから悪用まで、30分かかっていませんでした。あとで知ったのですが、GitHubに上がった鍵は人間が探すまでもなく、bot が常時スキャンして数分で拾っていきます。怖いのは「うっかりpushした」ことより、一度公開された秘密はもう秘密じゃないという事実のほうでした。
この記事は、その失敗を二度とやらないために僕が組んだ手順です。難しい暗号の話はしません。秘密の値をコードから引き離し、コミット前に機械で止め、万が一漏れたら慌てず入れ替える。その地味な配管を、コピペで動く形で渡します。
この記事の要点
- 秘密情報(APIキー・トークン・DBパスワード)は
.envに隔離し、.gitignoreで必ず除外する。リポジトリには値ではなく.env.exampleという「型」だけ置く。 - コミット前に
git-secretsやgitleaksで機械的に検出する。人間の目視は必ず疲れた夜に破綻する。 - 漏れたら「履歴から消す」より先に鍵を無効化して入れ替える(ローテーション)。消しても拾われた後なら手遅れ。
- 本番はSecret Managerに置く。長期キーをローカルやコードに残さない。
- Claude Codeに秘密の値そのものを渡さない。渡すのはキー名・構造・マスク済みエラーだけ。
秘密情報って、結局どれのこと?
最初に詰まるのが「どれを隠せばいいのか」です。判断は単純で、漏れたときに他人が課金・送信・閲覧・書き込み・ログインできてしまう値は全部シークレットです。
逆に、公開URLや機能フラグの名前、OAuthのclient IDのように、単体では何の権限も持たない値は、神経質に隠さなくてかまいません。client ID は表札、client secret は鍵、というイメージです。表札は外から見えてもいいけど、鍵を玄関マットの下に置いてはいけない。
僕がよく扱うものを並べると、こうなります。
| 種類 | 具体例 | ローカルでの置き方 | 本番での置き方 |
|---|---|---|---|
| API連携 | SendGrid APIキー、Stripe secret key、GitHub token | テスト用キーを.envに入れる | Secret Manager か CI secrets から注入 |
| データベース | DATABASE_URL、接続ユーザー、パスワード | 開発DB専用ユーザー | 本番は読み書き権限を分ける |
| クラウド | AWS/GCP/Azure の認証情報 | 長期鍵をローカルに置かない | OIDC・デプロイ専用ロールを使う |
| OAuth | OAUTH_CLIENT_SECRET、Webhook signing secret | ローカル用アプリを別に作る | prod専用secretを保管 |
この表で僕がいちばん伝えたいのは「本番キーをローカルで使い回さない」の一点です。冒頭で悪用されたのも、テスト用だと思い込んで本番権限のキーを開発で使っていたからでした。SendGridもStripeもテストキーを用意できます。GitHub token はリポジトリ単位・期限付き・必要なスコープだけにします。
リポジトリには値を置かず、型だけ置く
.envはローカルの秘密箱です。中身は便利ですが、リポジトリに入れた瞬間にただの公開文書になります。だから.gitignoreで確実に締め出します。
ここで効くのが.env.exampleという発想です。値は書かず、「どの名前が要るか」「どんな形式か」「どこで取るか」だけを共有する。新しく入った人は値の中身を聞かなくても、必要な変数の一覧がわかります。環境変数全般の設計は.envと.env.localの使い分けの記事で詳しく書いたので、合わせて読むとオンボーディングの質問がぐっと減ります。
# ローカルの秘密。これらは絶対にコミットしない
.env
.env.*
!.env.example
!.env.test.example
# トークンが紛れ込みやすい生成物やログも除外
npm-debug.log*
yarn-debug.log*
coverage/
dist/
# .env.example - プレースホルダーのみ。実値は絶対に書かない
NODE_ENV=development
APP_BASE_URL=http://localhost:3000
# 本番ではなくローカル開発用のDBユーザーを使う
DATABASE_URL=postgres://app_user:replace-me@localhost:5432/app_dev
# テスト/サンドボックス用のキーを使う
SENDGRID_API_KEY=SG.xxxxxx
STRIPE_SECRET_KEY=sk_test_xxxxxx
GITHUB_TOKEN=ghp_xxxxxx
# OAuthのsecretは環境ごとに分ける
OAUTH_CLIENT_ID=local-client-id
OAUTH_CLIENT_SECRET=replace-me
# デプロイ時はCIかSecret Managerから読む
AWS_REGION=ap-northeast-1
DEPLOY_ROLE_ARN=arn:aws:iam::123456789012:role/app-deploy-dev
設定の出典を一つ挙げるならThe Twelve-Factor App の Configです。「設定はコードに埋め込まず環境から注入する」という、もう20年近く語られている原則ですが、僕が事故ったのはまさにこれを破った瞬間でした。
コミット前に、機械で止める
.gitignoreに書いても、人はミスをします。git add -Aで別の秘密ファイルを巻き込んだり、コードの中にAPIキーをベタ書きしたまま忘れたり。だからコミットの瞬間に機械で止める門番を置きます。これがこの記事のいちばんの肝です。
手軽なのがgit-secretsです。コミットしようとした差分にキーらしき文字列があれば、その場でコミットを失敗させます。インストールして、リポジトリに仕掛けるだけ。
# macOS は Homebrew、Linux はリポジトリから make install
brew install git-secrets
# このリポジトリの commit フックに登録する
git secrets --install
# よくあるAWSキーのパターンを登録
git secrets --register-aws
# 自分のサービスのキー形式も足す(例: SendGrid と Stripe)
git secrets --add 'SG\.[A-Za-z0-9_\-]{20,}'
git secrets --add 'sk_live_[A-Za-z0-9]{20,}'
# 既存の履歴もまとめて検査しておく
git secrets --scan-history
これで、AWS_SECRET_ACCESS_KEY=...のような値をうっかりコミットしようとすると、コミット自体が拒否されます。冒頭の事故は、この1行があれば起きませんでした。
もう少し広く守りたいならpre-commitというフレームワークと、秘密検出に強いgitleaksを組み合わせます。設定はリポジトリ直下に.pre-commit-config.yamlを1枚置くだけです。
# .pre-commit-config.yaml - コミット前に gitleaks で秘密情報を走査する
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.4
hooks:
- id: gitleaks
# pre-commit を入れて、このリポジトリのフックを有効化
pipx install pre-commit
pre-commit install
# 既存ファイルもまとめて一度走らせる
pre-commit run --all-files
これでチーム全員のコミット前に、同じ門番が立ちます。「気をつけよう」と声をかけるより、コミットを物理的に止めるほうが確実です。
ログに出さない、Claude Codeにも渡さない
秘密情報は、コミット以外にもいろんな隙間から漏れます。いちばん多いのがログとデバッグ出力です。console.log(process.env)をデバッグで入れて、その出力をCIログやチャットにそのまま貼る。これで何度も冷や汗をかきました。
対策は、設定を読み込むときに必ず型として扱い、表示するときはマスク専用の関数を通すこと。下がdotenvとenvalidを使ったコピペで動く設定ローダーです。起動時に「値があるか・URLとして正しいか」を検証し、ログには伏せた値しか出しません。
import { config as loadDotenv } from "dotenv";
import { cleanEnv, str, url } from "envalid";
const envFile = process.env.NODE_ENV === "test" ? ".env.test" : ".env";
loadDotenv({ path: envFile });
// 名前にこのパターンを含む値は「秘密」とみなしてマスク対象にする
const secretKeyPattern = /(KEY|TOKEN|SECRET|PASSWORD|DATABASE_URL|PRIVATE)/i;
const env = cleanEnv(process.env, {
NODE_ENV: str({
choices: ["development", "test", "staging", "production"],
default: "development",
}),
APP_BASE_URL: url({ default: "http://localhost:3000" }),
DATABASE_URL: url(),
SENDGRID_API_KEY: str(),
STRIPE_SECRET_KEY: str(),
GITHUB_TOKEN: str(),
OAUTH_CLIENT_ID: str(),
OAUTH_CLIENT_SECRET: str(),
AWS_REGION: str({ default: "ap-northeast-1" }),
DEPLOY_ROLE_ARN: str(),
});
// アプリ内部では型付きの設定として扱う
export function appConfig() {
return Object.freeze({
nodeEnv: env.NODE_ENV,
appBaseUrl: env.APP_BASE_URL,
databaseUrl: env.DATABASE_URL,
sendgridApiKey: env.SENDGRID_API_KEY,
stripeSecretKey: env.STRIPE_SECRET_KEY,
githubToken: env.GITHUB_TOKEN,
oauthClientId: env.OAUTH_CLIENT_ID,
oauthClientSecret: env.OAUTH_CLIENT_SECRET,
awsRegion: env.AWS_REGION,
deployRoleArn: env.DEPLOY_ROLE_ARN,
});
}
// 秘密値は前後4文字だけ残して伏せる
export function redactValue(key, value) {
if (!secretKeyPattern.test(key)) return value;
if (!value) return "<empty>";
const text = String(value);
if (text.length <= 8) return "<redacted>";
return `${text.slice(0, 4)}...${text.slice(-4)}`;
}
// ログに出す前に必ずこれを通す
export function redactConfig(config) {
return Object.fromEntries(
Object.entries(config).map(([key, value]) => [key, redactValue(key, value)]),
);
}
if (process.argv[1] === new URL(import.meta.url).pathname) {
// 生の process.env ではなく、伏せた設定だけを表示する
console.log(redactConfig(appConfig()));
}
そしてもう一つ、僕が強く言いたいのがClaude Codeに秘密の値を渡さない設計です。Claude Codeは調査も実装も任せられて便利ですが、秘密の値そのものを読ませる必要は、ほぼありません。渡すべきは変数名、設定ファイルの構造、権限の目的、そしてマスク済みのエラーメッセージだけ。
セキュリティ関連の作業を始めるとき、僕は最初にこの境界プロンプトを貼ります。
You may inspect .env.example, package.json, deployment manifests, and secret names.
Do not open, print, summarize, store, or copy .env, CI/CD secret values, cloud credentials, production dumps, or screenshots containing tokens.
When you need a value, ask me to set it outside chat and confirm only the variable name.
If a secret appears in command output, stop, redact it, and report which file or command exposed it.
Before changing permissions, explain the least-privilege scope and ask for approval.
Do not paste real secrets into prompts, logs, documentation, code comments, tests, tickets, or articles.
加えてClaude Codeの実行許可も絞ります。.envを開くコマンド、printenv、クラウド認証情報を表示するCLI、CIログの丸ごと貼り付けは、毎回確認を挟む。秘密の値を知らない状態でも、.env.exampleと型定義とマスク済みエラーがあれば、たいていの修正は進みます。この権限設計の全体像は初心者がまず守る安全対策の記事にまとめてあります。
漏れたら、消す前に「入れ替える」
ここが冒頭の失敗で学んだ最大の教訓です。秘密が漏れたとわかったとき、多くの人がまずgitの履歴から消そうとします。気持ちはわかります。でも順番が逆です。
公開された鍵は、消しても拾われた後なら手遅れです。bot は数分で拾っていきます。だから最優先は「履歴を消す」ではなく「その鍵を無効化して、新しい鍵に入れ替える(ローテーション)」。古い鍵を殺せば、履歴に残っていても悪用できません。
ローテーションを「事故のときの特別作業」にすると、いざというとき手が止まります。平時から手順書にしておきます。
## シークレット ローテーション チェックリスト
- [ ] 所有者・環境・利用箇所・影響範囲を洗い出す
- [ ] 必要最小スコープで新しい鍵を発行する
- [ ] CI secrets か Secret Manager に保存する
- [ ] まず1つのサービス/ジョブだけ新しい値でデプロイする
- [ ] 鍵を表示せずにログとメトリクスで動作確認する
- [ ] 古い鍵を無効化(revoke)する
- [ ] Git履歴・チケット・ドキュメント・スクショ・チャットを走査する
- [ ] ローテーション日と次回見直し日を記録する
このチェックリストをIssueやRunbookに落とし込むのは、Claude Codeが得意な作業です。ただし「古いキーをここに貼るので置換して」と頼んではいけません。値は人間が管理画面で差し替え、Claude Codeには「SENDGRID_API_KEYをv2に切り替えるので、参照箇所と検証手順を洗い出して」と頼みます。
本番はSecret Managerに任せる
ローカルは.env、CIはGitHub ActionsなどのSecrets、本番はクラウドのSecret Manager。この三段重ねが現実的です。本番の鍵をコードにもCIの設定ファイルにも置かず、実行時に取りに行く形にします。
クラウド別の置き場所は次の通りです。
- AWS: AWS Secrets Manager
- Google Cloud: Secret Manager
- Azure: Key Vault
CIでは、秘密の値をワークフローの中でechoしないことだけ守ればだいぶ安全になります。GitHub Actionsは一部のsecretsを自動マスクしますが、加工した値・JSONに埋めた値・URLエンコードした値まで常に守れるとは限りません。下はキーの値を一度も表示せずにデプロイする最小構成です。
name: deploy
on:
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
env:
NODE_ENV: production
AWS_REGION: ap-northeast-1
steps:
- uses: actions/checkout@v4
- name: 必要なsecret名の存在だけ確認する(値は出さない)
run: test -n "${{ secrets.DEPLOY_ROLE_ARN }}" && test -n "${{ secrets.DATABASE_URL }}"
- name: secretをechoせずにデプロイ
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
DEPLOY_ROLE_ARN: ${{ secrets.DEPLOY_ROLE_ARN }}
run: npm run deploy
CI全体の組み方はGitHub ActionsでCI/CD入門の記事で扱っています。Claude CodeにCI修正を頼むときも「secret値をechoしない」「失敗時はキー名だけ出す」「contents: readから権限を増やすなら理由を説明する」と明記しておくと安心です。GitHubのsecret scanningも、公開・非公開を問わず有効にしておきます。
よくある失敗を先に潰す
公開やデプロイの前に、僕はこの一覧を批判的に見ます。1つでも当てはまったら止めます。
.envや.env.productionをコミットした。しかも履歴から消しただけで、鍵の無効化をしていない。- スクリーンショット、CIログ、エラーレポート、記事のコード例にトークンやDB URLが写っている。
- クラウドのキーに
AdministratorAccessのような広すぎる権限を付けている。 - 本番のStripeキー・SendGridキー・DB URLをローカル開発や検証で使い回している。
- OAuth client secret や Webhook signing secret のローテーション手順がなく、担当者の記憶に依存している。
- Claude Codeや別のAIに実値を貼り、その内容をREADME・テスト・チケット・ブログ下書きに再利用してしまった。
事故は技術より運用の穴から起きます。コードだけでなく、ログ・画像・下書きまで見る習慣をチームで揃えるのが効きます。
よくある質問
Q. 間違ってAPIキーをpushしました。まず何をすべき? A. 履歴を消す前に、その鍵をサービス側で無効化して新しい鍵に入れ替えてください。公開された鍵はbotに数分で拾われる前提で動きます。無効化してから、履歴の掃除や原因の特定をします。
Q. .gitignoreに書いたのにコミットされてしまうのはなぜ?
A. すでに一度追跡(track)されたファイルは.gitignoreの対象外になります。git rm --cached .envで追跡から外してからコミットし直してください。新規ファイルなら.gitignoreが効きます。
Q. git-secretsとgitleaks、どちらを使えばいい?
A. まず1人で始めるならgit-secretsが手軽です。チームで揃えたい、検出ルールを充実させたいならpre-commit経由のgitleaksが向きます。両方入れても競合しません。
Q. Claude Codeに.envを読ませないと作業が進まないのでは?
A. ほとんど進みます。Claude Codeに必要なのは変数名・設定の構造・マスク済みのエラーであって、値そのものではありません。値が要る場面は、あなたがチャットの外で設定し、キー名だけ伝えれば十分です。
Q. 個人開発でもSecret Managerは必要?
A. 最初は不要です。.env + .gitignore + コミット前検出 + ローテーション手順があれば個人開発は十分守れます。外部公開する本番サービスを持った段階で、Secret Managerへ移すのが現実的です。
実際に試した結果
この手順を自分の検証用Node.jsアプリに通してみました。.env.exampleから不足していた変数を見つけ、redactConfigでCIログに出ていたDB URLとAPIキーを伏せ、GitHub Actionsのdeployジョブから余計なwrite権限を外せました。git-secretsを入れた直後に、テストコードへベタ書きしていたStripeのテストキーをコミットしようとして、ちゃんと弾かれたのは少し感動しました。
一方で、古いスクリーンショットにテストキーが写っていたのを記事化の直前に見つけ、結局そのキーは再発行しました。コードだけ守っても、画像と下書きが抜け穴になる。冒頭で30分で悪用されたあの日から、僕が変えたのは「気をつける」をやめて「機械に止めさせる」に振り切ったことです。秘密情報は、隠すのではなく、漏れない配管を先に組む。遠回りに見えて、これがいちばん安く済みました。
次の一歩として、チームのリポジトリに合わせたテンプレートが欲しい人は教材一覧も覗いてみてください。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
Claude Codeに1ファイルだけ直させる指示文のつくり方
「もっと良くして」で40行も変えられた失敗から学んだ、触る範囲・検証・戻し方をセットにしたClaude Code用の依頼文テンプレートを紹介します。
Claude Code の権限拒否から復旧する: 止まった理由を次の安全手順に変える
Claude Code のコマンドが拒否されたとき、焦って許可を広げずに、拒否理由、代替手順、証拠コマンド、再試行条件へ分解する方法。
Claude Codeにビルド→スモークテスト→自動修正を回させる足場の作り方
最小スモークテストの選び方、失敗ログを食わせて直させるループ、回数上限と確認ゲートで暴走を止める方法を、コピペで動くコード付きで紹介します。