はじめに
.env をうっかりGitHubにpushしてしまった。
「すぐ消したから大丈夫だろう」——その判断が、数時間後に数百万円の請求書として返ってくることがある。
これは仮定の話ではない。AWSのアクセスキーがGitHubのパブリックリポジトリに公開されると、平均4分以内にbotが検出するという実験結果がある。検出されたキーは即座に悪用され、暗号通貨マイニング用のEC2インスタンスが大量に起動される。翌朝AWSからの高額請求アラートで気づいたときには、もう手遅れだ。
本稿では、.envファイルがGitHubに上がった瞬間から何が起きるのかを時系列で追い、なぜ「すぐ消した」では手遅れなのかを解説する。そして、漏洩を防ぐ仕組みと、漏洩してしまった場合の緊急対応を整理する。
1. 漏洩の24時間 ― 何が起きるのか
タイムライン
00:00 エンジニアが .env を含むコミットをpush
00:01 GitHubのイベントストリームにpushイベントが流れる
00:02 世界中のスキャンbotがリアルタイムでイベントを監視している
00:04 botがAWSキーのパターン(AKIA...)を検出
00:05 検出されたキーで有効性チェック(aws sts get-caller-identity)
00:06 有効なキーと確認。攻撃開始
00:10 全リージョンでEC2インスタンスの起動を試みる
00:15 マイニング用のc5.4xlargeインスタンスが20台起動
00:30 エンジニアが気づいてGitHubからファイルを削除
00:31 しかしGitの履歴にキーが残っている。botはすでにキーを保存済み
01:00 起動されたインスタンスがフル稼働でマイニング中
06:00 AWSから異常利用のアラートメール(見てない。寝てる)
08:00 出社してメールを確認。$2,000の請求が発生している
08:05 慌ててキーを無効化。しかしS3バケットへのアクセスログを確認すると...
08:10 顧客データを含むバケットがすでにダウンロードされていた
このタイムラインは誇張ではない。セキュリティ研究者がGitHubにダミーのAWSキーを公開する実験(ハニーポット調査)を行った結果、最短で数分以内に不正利用が開始されることが確認されている。
なぜこんなに速いのか
GitHubにはEvents APIがあり、パブリックリポジトリのすべてのpushイベントをリアルタイムで取得できる。攻撃者はこのAPIを監視するbotを常時稼働させている。
攻撃botの動作:
1. GitHub Events APIをポーリング(数秒間隔)
2. PushEventからコミットの差分を取得
3. 正規表現でシークレットのパターンを検出
4. 検出したキーの有効性を自動検証
5. 有効なキーを即座に悪用
botが探しているパターンの例:
AWSアクセスキー: AKIA[0-9A-Z]{16}
AWSシークレットキー: [0-9a-zA-Z/+]{40}
GitHubトークン: ghp_[0-9a-zA-Z]{36}
Stripeシークレット: sk_live_[0-9a-zA-Z]{24,}
Slackトークン: xoxb-[0-9]{11}-[0-9]{11}-[0-9a-zA-Z]{24}
データベースURL: postgres://[^\s]+:[^\s]+@[^\s]+
パターンが単純で、grep一行で検出できてしまう。だからbotは数秒で見つける。
2. 「すぐ消した」が手遅れな理由
2.1 Gitの履歴は消えない
GitHubのWebインターフェースからファイルを削除しても、Gitの履歴にはコミットが残っている。
# ファイルを削除してコミット
git rm .env
git commit -m "remove .env"
git push
# しかし過去のコミットにはまだ .env が存在する
git log --all --full-history -- .env
# → 追加されたコミットが表示される
git show <commit-hash>:.env
# → ファイルの中身がそのまま見える
GitHubのWeb UIで「このファイルは削除されました」と表示されていても、コミット履歴をたどれば誰でも中身を読める。
2.2 キャッシュとフォーク
GitHubのコンテンツは複数の場所にキャッシュされる。
キーが残り続ける場所:
- Gitのコミット履歴(git log で辿れる)
- GitHubのイベントAPI(一定期間キャッシュされる)
- 検索エンジンのキャッシュ(Google等にインデックスされる場合がある)
- フォークしたリポジトリ(フォークには削除が反映されない)
- 攻撃者のローカル(すでにダウンロード済み)
一度インターネットに公開されたシークレットは、「削除」ではなく「無効化」でしか対処できない。ファイルを消すのではなく、キーそのものをローテーションする必要がある。
2.3 プライベートリポジトリでも安全ではない
「うちはプライベートリポジトリだから大丈夫」——これも危険な思い込みだ。
プライベートリポジトリでもリスクがある場面:
- 将来パブリックに変更する可能性
- チームメンバーの退職(アクセス権の剥奪漏れ)
- CIサービスの連携(ログにシークレットが出力される)
- 依存するサービスの侵害(GitHub自体のセキュリティインシデント)
- フォークの公開設定ミス
プライベートリポジトリは「今この瞬間は外部からアクセスできない」というだけで、「シークレットを置いていい場所」ではない。
3. 何が漏洩するとどうなるのか
漏洩するシークレットの種類によって、被害の内容と深刻度が変わる。
被害マップ
| 漏洩するもの | 攻撃者ができること | 被害の深刻度 |
|---|---|---|
| AWSアクセスキー | EC2起動(マイニング)、S3データ窃取、IAM権限昇格 | 極めて高い |
| データベース接続文字列 | データの閲覧・改ざん・削除、バックドアの設置 | 極めて高い |
| Stripe/決済キー | 不正な返金処理、顧客の決済情報へのアクセス | 極めて高い |
| GitHubトークン | リポジトリの改ざん、プライベートコードの窃取、サプライチェーン攻撃 | 高い |
| Slackトークン | メッセージの閲覧、なりすまし投稿、内部情報の収集 | 中〜高い |
| メールAPIキー | スパム送信、フィッシングメール、ドメインの信用棄損 | 中〜高い |
| Firebase設定 | ユーザーデータへのアクセス(Rulesの設定次第) | 設定依存 |
| JWTシークレット | 任意のユーザーになりすましたトークンの生成 | 極めて高い |
AWS漏洩の場合の具体的な被害額
AWSの料金体系から計算すると:
c5.4xlarge(マイニングに使われやすいインスタンス):
オンデマンド料金: 約 $0.68/時間
攻撃者が全リージョン(20以上)で各10台起動した場合:
$0.68 × 200台 × 24時間 = $3,264/日
週末に気づかなかった場合(48時間):
$3,264 × 2 = $6,528(約100万円)
さらにデータ転送料、EBSボリューム料金が加算される
AWSは不正利用に対する請求を免除してくれることもあるが、保証はされていない。AWSの公式ドキュメントでは、認証情報の管理はユーザーの責任と明記されている。
4. 漏洩を防ぐ仕組み ― 4つのレイヤー
個人の注意力に頼るのは愚策だ。仕組みで防ぐ。
レイヤー1: .gitignoreを正しく設定する(基本中の基本)
# .gitignore(プロジェクトのルートに必ず置く)
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
.env*.local
# その他のシークレットファイル
*.pem
*.key
credentials.json
serviceAccountKey.json
.gitignoreは「最初のコミットの前に」設定する。一度コミットしたファイルは、後から.gitignoreに追加しても履歴から消えない。
ただし、.gitignoreだけでは不十分だ。git add -A や git add . で意図せず追加してしまう、.gitignoreの設定漏れ、一時的に.envを外してテストした後に戻し忘れる、といったヒューマンエラーは必ず起きる。
レイヤー2: コミット前に検出する(pre-commitフック)
コミットの瞬間にシークレットを検出してブロックする。
git-secrets(AWS公式ツール)
# インストール
brew install git-secrets
# リポジトリに設定
cd your-project
git secrets --install
git secrets --register-aws
# AWSキーのパターンが自動登録される
# 以降、AWSキーを含むコミットはブロックされる
# 動作例
$ echo "AKIAIOSFODNN7EXAMPLE" >> config.txt
$ git add config.txt
$ git commit -m "add config"
[ERROR] Matched one or more prohibited patterns
Possible mitigations:
- Mark false positives as allowed using: git config --add secrets.allowed ...
secretlint(より汎用的なツール)
# インストール
npm install -D @secretlint/secretlint-rule-preset-recommend secretlint
# .secretlintrc.json
{
"rules": [
{
"id": "@secretlint/secretlint-rule-preset-recommend"
}
]
}
// package.json の lint-staged に追加
{
"lint-staged": {
"*": ["secretlint"]
}
}
lefthook で pre-commit を設定
# lefthook.yml
pre-commit:
commands:
secretlint:
glob: "*"
run: npx secretlint {staged_files}
gitSecrets:
run: git secrets --pre_commit_hook -- "$@"
レイヤー3: プッシュ後に検出する(GitHub側の防御)
GitHub Secret Scanning
GitHubにはSecret Scanning機能があり、パブリックリポジトリ(および GitHub Advanced Security を有効にしたプライベートリポジトリ)で自動的にシークレットを検出する。
対応しているパターンは200以上。AWS、GCP、Azure、Stripe、Slack、Twilio、npm、PyPIなど主要なサービスのキーを検出する。
さらに、Push Protectionを有効にすると、シークレットを含むpush自体をブロックできる。
Push Protectionの動作:
$ git push origin main
⚠ Secret scanning found the following secrets:
── AWS Access Key ID ──
Location: .env:3
Secret type: Amazon AWS Access Key ID
To push, either remove the secret or use:
git push --no-verify (非推奨)
# リポジトリの設定で有効化
# Settings → Code security and analysis → Secret scanning → Push protection → Enable
GitHub Advanced Securityが使えない場合
パブリックリポジトリではSecret Scanningは無料で使えるが、プライベートリポジトリではGitHub Advanced Security(有料)が必要。代替としてCIパイプラインにスキャンを組み込む。
# .github/workflows/secret-scan.yml
name: Secret Scan
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # 全履歴をチェック
- name: TruffleHog scan
uses: trufflesecurity/trufflehog@main
with:
extra_args: --only-verified
レイヤー4: シークレットをコードに含めない設計
そもそもシークレットが.envファイルに平文で存在する状態を避ける。
段階的なシークレット管理の成熟度:
Level 0: コードにハードコード(最悪)
const API_KEY = "sk_live_xxxxx";
Level 1: .envファイルに分離(最低限)
process.env.API_KEY
Level 2: CIの環境変数 / Vercelの環境変数設定(一般的)
Vercel Dashboard → Settings → Environment Variables
Level 3: シークレットマネージャー(推奨)
AWS Secrets Manager / Google Secret Manager / 1Password CLI
Level 4: 短命な認証情報(理想)
IAM Roles / OIDC Federation / Workload Identity
→ シークレット自体が存在しない
特にLevel 4は重要だ。GitHub ActionsからAWSにアクセスする場合、OIDCフェデレーションを使えばAWSキーをGitHubに保存する必要がなくなる。
# .github/workflows/deploy.yml
# AWSキーを使わない安全な認証
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write # OIDC用
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-role
aws-region: ap-northeast-1
# これ以降、一時認証情報が自動で使われる
# アクセスキーは存在しないので漏洩しようがない
5. 漏洩してしまった場合の緊急対応
防いでいても起きるときは起きる。そのとき最初の30分で何をするかが被害の大きさを決める。
緊急対応フロー
Step 1: キーを無効化する(最優先。1分以内)
→ AWSコンソールでアクセスキーを無効化/削除
→ GitHubトークンを revoke
→ DBのパスワードを変更
→ 該当するシークレットをすべてローテーション
Step 2: 影響範囲を調査する(30分以内)
→ CloudTrail / アクセスログで不正なAPIコールを確認
→ 不審なリソース(EC2、Lambda等)が起動されていないか確認
→ S3バケットへの不正アクセスがないか確認
Step 3: 不正なリソースを削除する(1時間以内)
→ 攻撃者が起動したインスタンスを全リージョンで確認・停止
→ 作成されたIAMユーザー/ロールを確認・削除
→ セキュリティグループの変更がないか確認
Step 4: Git履歴からシークレットを除去する
→ git filter-branch または BFG Repo-Cleaner で履歴を書き換え
→ force push(チームに事前通知が必要)
Step 5: 報告と再発防止
→ チームへの共有
→ pre-commitフックの導入(未導入の場合)
→ push protectionの有効化
AWSキー漏洩の場合の具体的な手順
# 1. 漏洩したキーを無効化
aws iam update-access-key \
--user-name compromised-user \
--access-key-id AKIAXXXXXXXXXXXXXXXX \
--status Inactive
# 2. キーを削除
aws iam delete-access-key \
--user-name compromised-user \
--access-key-id AKIAXXXXXXXXXXXXXXXX
# 3. 全リージョンで不審なEC2を確認
for region in $(aws ec2 describe-regions --query 'Regions[].RegionName' --output text); do
echo "=== $region ==="
aws ec2 describe-instances \
--region $region \
--filters "Name=instance-state-name,Values=running" \
--query 'Reservations[].Instances[].{ID:InstanceId,Type:InstanceType,Launch:LaunchTime}' \
--output table
done
# 4. CloudTrailで不正なAPIコールを確認
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=AccessKeyId,AttributeValue=AKIAXXXXXXXXXXXXXXXX \
--max-results 50
Git履歴からの除去
# BFG Repo-Cleaner を使う方法(git filter-branchより高速で安全)
# https://rtyley.github.io/bfg-repo-cleaner/
# .envファイルを履歴から完全に削除
java -jar bfg.jar --delete-files .env
# 特定の文字列(キー)を履歴から置換
echo "AKIAIOSFODNN7EXAMPLE" >> passwords.txt
java -jar bfg.jar --replace-text passwords.txt
# クリーンアップとforce push
git reflog expire --expire=now --all
git gc --prune=now --aggressive
git push --force
force pushはチームの作業に影響する。必ず事前に通知し、全員がローカルリポジトリを更新するよう共有すること。
6. チェックリスト
プロジェクト開始時
□ .gitignoreに.env系ファイルが含まれている
□ .env.exampleを用意し、実際の値は含めていない
□ git-secrets または secretlint の pre-commitフックを設定した
□ README等にシークレットの管理方法を記載した
CI/CD設定
□ シークレットはCI/CDプラットフォームの環境変数機能で管理している
□ CIのログにシークレットが出力されないことを確認した
□ GitHub Secret Scanning / Push Protection を有効にした
□ TruffleHog等のスキャンをCIパイプラインに組み込んだ
□ 可能であればOIDCフェデレーションを使い、長期キーを排除した
定期チェック
□ 使われていないアクセスキーを定期的に棚卸ししている
□ キーのローテーションスケジュールを設定している
□ CloudTrail / 監査ログを定期的に確認している
□ チームの新メンバーにシークレット管理のルールを共有している
7. おわりに ― シークレットは「漏洩するもの」として設計する
.envをGitHubに上げてしまうのは、エンジニアなら誰にでも起こりうるミスだ。問題は、そのミスが数分で実害に変わるインターネットの速度と、「すぐ消せば大丈夫」という誤った安心感の組み合わせにある。
防御の考え方は多層で組み立てる。
- そもそもシークレットをファイルに置かない(OIDC、シークレットマネージャー)
- 置いてもコミットできない(.gitignore、pre-commitフック)
- コミットしてもpushできない(Push Protection)
- pushしても即座に検出される(Secret Scanning、CIスキャン)
- 漏洩しても被害を最小化する(最小権限、キーのローテーション、短命な認証情報)
そしてこれらすべてを突破されたときのために、緊急対応の手順を事前に用意しておく。
完璧な防御は存在しない。だからこそ、シークレットは「漏洩するもの」として設計する。漏洩したときに何が起きるかを想定し、被害を最小限に抑える仕組みを先に作っておく。それがシークレット管理の本質だ。
次に git add . を実行する前に、一呼吸置いてほしい。その1秒が、数百万円の請求書を防ぐかもしれない。
参考文献
- Comparitech. "How fast do hackers find exposed credentials on GitHub?"
- GitHub Docs. "About secret scanning."
- GitHub Docs. "About push protection."
- GitHub Docs. "Events API."
- AWS. "Best practices for managing AWS access keys."
- AWS. "Configuring OpenID Connect in Amazon Web Services."
- TruffleHog. "TruffleHog - Find leaked credentials."
- git-secrets. "Prevents you from committing secrets."
- BFG Repo-Cleaner. "Removes large or troublesome blobs."