2-4. シークレット管理 - 漏洩を防ぐ実務全パターン
所要時間: 40-60分(がっつりなら2セッション分) ゴール: API キー・DB パスワード・JWT 秘密鍵などの「シークレット」を、開発・CI・本番の各段階で適切に扱える コミット内容:
~/learn/security/day-secrets/に .env, .gitignore, pre-commit 設定、AWS Secrets Manager 取得サンプル
前章とのつながり
このレッスンの位置づけ
SSRF の Capital One 事例で「SSRF で IAM トークンが漏れた」と学んだ。本章はその IAM トークンや API キーを そもそもどう守るか の話。
- 2-1 JWT: JWT 署名鍵もシークレット。漏洩したら全 JWT 偽造可能
- 1-2 パスワード保存 の pepper も本章で扱う DB外シークレット の典型
- 次章 2-5 入力検証: シークレットを守った上で、入力経由の侵入を防ぐ
実害シーン: AWS課金爆発・キー漏洩
- AWS キー public push 事案: GitHub の bot が 秒単位 で発見、攻撃者が EC2 を大量起動 → 24時間で 5万ドル以上の請求。被害者は連日 SNS で報告されている
- Uber GitHub事件 (2014): private リポでも、退職者の私的アカウント経由で漏洩 → 5万人の運転手情報流出 → 和解金 1.48億ドル
- 某スタートアップ:
.envを誤って commit → 翌朝 AWS から「不審な仮想通貨マイニング検出」通知、課金 70万円シークレット漏洩は 「気づくのに数時間〜数日」、「被害は瞬時」。受け身では間に合わない、事前防御がすべて。
大前提: なぜ「シークレット管理」が独立した分野になっているか
シークレット(API キー、DB パスワード、暗号鍵、OAuth Client Secret 等)の漏洩は、バックエンドエンジニアが直面する事故の最頻出原因です。
実際の被害規模:
- 2014年 Uber - GitHub の private リポに AWS キー漏洩: 50,000人の運転手情報流出。FTC との和解金 1億4800万ドル
- AWS キーをうっかり public リポに push: GitHub の bot が秒で発見、攻撃者が EC2 を大量起動して仮想通貨マイニング → 24時間で $50,000以上の請求事例多数。SNS でしばしば話題になる
- 2017年 Equifax: 単一の AWS キー漏洩起点で大量の認証情報が連鎖流出、1億4700万件
- 2019年 First American Financial: 平文の権限情報漏洩、8.85億件の文書が公開状態
- 2023-2024 Hugging Face、CircleCI: シークレット管理サービスやCI/CDから漏洩、影響範囲広
漏洩したら:
- 金銭被害: 不正クラウド利用、データ持ち出し
- 二次攻撃: そのキーで他システムへ侵入
- 訴訟・罰金: GDPR で最大年商4%
- 信用失墜: 取引停止、株価下落
「コードに書かない」だけでは不十分。Git 履歴・ログ・CI・ビルド成果物すべてに漏洩経路がある。このレッスンで全段階の防御を覚える。
セッション①: 開発環境のシークレット管理(30分)
1. シークレット管理の進化段階
段階0: コードにハードコード ← 論外
段階1: 設定ファイル(config.json) ← Git に入る、まだダメ
段階2: .env + .gitignore ← 個人開発・小規模OK
段階3: 環境変数を OS / コンテナで注入 ← 中規模
段階4: Secrets Manager (AWS/GCP/Vault) ← 本番標準
段階5: KMS + envelope encryption + STS 短命 ← 高セキュリティ要件
「自分のチームはどこ?」を意識する
一足飛びに「段階5」を導入するのは過剰。段階に応じた選択が重要。
- 個人開発 / プロトタイプ: 段階2 (.env)
- スタートアップの本番: 段階3-4
- 金融・医療・大企業: 段階4-5
2. 段階0-1 のアンチパターン
ハードコード
// NG: ソースコードに直書き const dbPassword = "supersecret123" var awsKey = "AKIAIOSFODNN7EXAMPLE"なぜNGか:
- Git に永久に残る(履歴から削除しても他人の clone には残る)
- public リポなら世界中に公開
- bot 系スキャナ(truffleHog、gitleaks、GitHub の Secret Scanning)が秒で発見
設定ファイルを Git にコミット
// config.json (NG: Git に入っている) { "db_password": "supersecret123", "stripe_key": "sk_live_..." }なぜNGか: ファイル名が違うだけで本質はハードコードと同じ。
AWS キー公開の事故テンプレ
よくあるパターン:
- 開発中に AWS キーを
config.pyなどにベタ書き- 急いで GitHub に public push(うっかり private にし忘れ)
- 数分後に bot が検出 → 攻撃者の手に渡る
- 数時間で大量の EC2 や Lambda が攻撃者により起動
- 24時間で請求が数万〜数十万ドル
- AWS サポートに泣きついて支払い免除交渉(多くは半額〜全額免除されるが、毎回ではない)
このパターンは個人・企業問わず毎週 SNS で報告される。他人事ではない。
3. .env + .gitignore(段階2)
最小構成:
project/
├── .env ← 実際のシークレット(Git に入れない)
├── .env.example ← サンプル(Git に入れる、値は仮)
└── .gitignore
.env:
DB_PASSWORD=actual-secret-here
STRIPE_KEY=sk_test_XXXXXXXX
JWT_SECRET=Vh3oP9jK...
.env.example:
DB_PASSWORD=your-password-here
STRIPE_KEY=sk_test_XXXXXXXX
JWT_SECRET=at-least-32-bytes-random
.gitignore:
.env
.env.local
.env.*.local
*.pem
*.key
credentials.json
.env.example の役割
- チームの新メンバーが「どんなシークレットを用意すればいいか」分かる
- 「本物が漏れない」「設定例は共有できる」を両立
Next.js、Rails、Django など多くのフレームワークでこの慣習が定着している。
Go での .env 読み込み
import "github.com/joho/godotenv" func init() { // 本番では環境変数が直接渡されているので、.env がなくてもエラーにしない _ = godotenv.Load() } dbPass := os.Getenv("DB_PASSWORD") if dbPass == "" { log.Fatal("DB_PASSWORD is required") }未設定時は即座に Fatal で落とすのが鉄則。空文字で進めて「なぜか接続できない」とデバッグするのは時間の無駄。
4. .gitignore の落とし穴
既に Git に入ったファイルは .gitignore で除外できない
# .gitignore に書く前に commit してしまった場合 git rm --cached .env # Git の追跡から外す(ファイル自体は残る) git commit -m "chore: remove .env from git" # → ただし「過去の履歴」には残る!次のステップへ履歴からも完全削除したい場合:
# BFG Repo-Cleaner や git filter-repo を使う git filter-repo --path .env --invert-paths git push --force origin main # (要相談、共同リポでは慎重に)そもそも一度公開されたシークレットは「履歴から消しても」漏洩済みとして扱う。即ローテーション(後述)。
5. pre-commit hook で漏洩を未然に防ぐ
gitleaks または git-secrets を導入:
# macOS
brew install gitleaks
# プロジェクトで pre-commit フック化
cd ~/learn/security/day-secrets
cat > .git/hooks/pre-commit <<'EOF'
#!/bin/sh
gitleaks protect --staged --redact
EOF
chmod +x .git/hooks/pre-commitこれで .env の中身や AWS キー風文字列が含まれている commit が手元でブロックされる。
gitleaks の仕組み
正規表現ベースのシークレット検出ツール。AWS / GCP / Stripe / Slack / OpenAI など 100+ のサービスのキー形式をパターン化。
例(AWS Access Key の検出ルール):
(AKIA|ABIA|ACCA|ASIA)[A-Z0-9]{16}Git ホスティング側も同様のスキャンを掛けている:
- GitHub Secret Scanning: push 時にスキャン、漏洩検出時に対応プロバイダ(AWS, Stripe など)に自動通知 → 多くの場合キーが自動失効
- GitLab Secret Detection: 同様の機能
6. ローカル開発の OS 環境変数
.env の代わりに OS の環境変数で渡す手もある:
# .zshrc / .bashrc などに(個人マシン)
export STRIPE_KEY="sk_test_..."
# プロジェクト固有なら direnv で
brew install direnv
echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc
# プロジェクトに .envrc を作る(gitignore 必須)
echo 'export STRIPE_KEY="sk_test_..."' > .envrc
direnv allow .direnv の便利さ
ディレクトリに
cdした時に自動で環境変数をロード、cdで出ると解除。
- 「.env を毎回 source するのが面倒」が解決
.envrcを gitignore しておけば漏洩リスク低- プロジェクトごとに別の AWS_PROFILE / 別の Stripe キーを切り替えやすい
セッション②: CI/CD と本番のシークレット管理(30分)
7. CI/CD でのシークレット(GitHub Actions の例)
GitHub Actions の場合:
- リポの Settings → Secrets and variables → Actions で登録
- ワークフロー内で
${{ secrets.STRIPE_KEY }}のように参照
name: deploy
on: [push]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy
env:
STRIPE_KEY: ${{ secrets.STRIPE_KEY }}
run: ./deploy.shGitHub Actions の masked output
シークレットがログに出ても自動的に
***でマスクされる。ただしマスクを破られるケースもある:
echo "$STRIPE_KEY" | base64→ base64 化された値はマスクされない- JSON にエンコードして出力 → エスケープされた値はマスクされない
- エラーメッセージに部分的に混入 → 一部だけマスクで残りは出る
デバッグログに env を直書きする習慣を絶対に持たない。
CI でシークレットを print
- run: echo "Stripe key is $STRIPE_KEY" # NG: ログに残るなぜNGか:
- マスクが効いても、変換や別表現で漏れる可能性
- そもそも CI ログには長期間アクセス権を持つ人が複数いる
- ログがログ収集基盤に流れて、検索可能になることも
8. 本番: AWS Secrets Manager(段階4)
import (
"context"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
)
func loadSecret(name string) (string, error) {
cfg, err := config.LoadDefaultConfig(context.TODO())
if err != nil {
return "", err
}
client := secretsmanager.NewFromConfig(cfg)
out, err := client.GetSecretValue(context.TODO(), &secretsmanager.GetSecretValueInput{
SecretId: &name,
})
if err != nil {
return "", err
}
return *out.SecretString, nil
}
// 起動時に取得
dbPass, _ := loadSecret("prod/db/password")Secrets Manager のメリット
- キーの集中管理: アプリ・人間がアクセスする時に IAM 認可
- 監査ログ (CloudTrail): 誰がいつ取得したか記録
- 自動ローテーション: RDS など対応サービスとの組み合わせで、シークレットを自動更新
- バージョニング: 旧バージョンも一定期間保持、ロールバック可能
- 暗号化 (KMS): 保存も通信も暗号化
同等品:
- HashiCorp Vault: クラウド非依存、OSS の標準
- GCP Secret Manager
- Azure Key Vault
- Doppler / Infisical: SaaS
9. KMS と envelope encryption(段階5)
「シークレット自体を暗号化して保存し、復号鍵だけを KMS が持つ」設計:
1. データ暗号化キー (DEK) を都度生成
2. DEK で実際のデータを暗号化
3. DEK を KMS の Customer Master Key (CMK) で暗号化 → 暗号化済み DEK と暗号データを保存
4. 復号時: KMS に「この暗号化済み DEK を復号して」と依頼 → DEK を取得 → データを復号
なぜ二段階?
- CMK は KMS から出てこない(ハードウェア HSM 内)→ 漏洩リスクが極めて低い
- DEK は使い捨て(メモリ上だけ)→ ローテーションが軽い
- 大容量データに対応(直接 CMK で暗号化しないので KMS のスループット制限を回避)
AWS の S3, EBS, RDS の暗号化はすべてこの方式。「保存時の暗号化を強くしたいけど性能も欲しい」というユースケースで定石。
10. ローテーション戦略
静的キー vs 短命トークン
静的キー(AWS Access Key Id + Secret)は漏洩リスクが時間と共に増える。代替:
- AWS STS (Security Token Service): 一時的な認証情報(15分〜36時間)を発行、自動失効
- AWS IAM Role for EC2/ECS/Lambda: インスタンス自身に権限を付与、長期キー不要
- IAM Roles Anywhere: オンプレ環境にも一時クレデンシャル
- OIDC で短命 token: GitHub Actions ⇄ AWS の信頼関係を OIDC で結び、長期 Access Key を CI に持たせない(推奨)
「長期 Access Key を発行する場面を最小化する」が現代のベストプラクティス。
GitHub Actions × AWS の OIDC 連携
permissions: id-token: write jobs: deploy: steps: - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789:role/github-actions aws-region: ap-northeast-1CI から AWS への認証で長期キーを GitHub Secrets に置く必要なし。トークンは数分で失効する一時クレデンシャル。
11. 漏洩時のインシデント対応
漏洩を発見したら(または GitHub の Secret Scanning から通知が来たら):
1. ローテーション(最優先): 該当キーを今すぐ無効化、新キー発行
- AWS: aws iam create-access-key → 新キーで動作確認 → aws iam delete-access-key で旧キー削除
- Stripe: ダッシュボードから revoke
- DB: パスワード変更(接続中アプリの再起動と整合させる)
2. ログ調査: そのキーがいつ・どこから使われたか
- AWS CloudTrail: アクセスの全履歴
- 普段と違う IP / リージョン / API があれば侵害確定
3. 被害範囲特定: そのキーで何にアクセスできたか
- IAM 権限の棚卸し
- 触れたかもしれないリソースの調査
4. 関係者通知: GDPR / 個人情報保護法の通知義務
- 国・業種により 72時間 / 30日 などの期限あり
5. 事後対策: なぜ漏れたか、再発防止
- .gitignore 不足? CI ログに出していた? Slack に貼った?
「Git の履歴から消したから大丈夫」は通用しない
一度 public に出たキーは「他人の手元」に残っている可能性がある。必ず無効化を最優先。履歴掃除は「次に同じ事故を起こさないため」の対策。
12. Docker / Kubernetes でのシークレット
Dockerfile に ENV で API キー
# NG: イメージ層に永久に焼き込まれる ENV API_KEY=sk_live_abc123なぜNGか:
- イメージを
docker historyすれば誰でも見える- public レジストリにプッシュしたら世界中に公開
- ローカルテストのつもりが本番イメージに混入する事故
実行時に環境変数として注入
# Docker docker run --env-file .env myapp # docker-compose.yml で .env を読む # Kubernetes は Secret リソース + envFrom で注入イメージ自体にはシークレットを含めない。実行時に外から渡す。
Kubernetes Secret は base64 であって暗号化ではない
apiVersion: v1 kind: Secret type: Opaque data: password: c3VwZXJzZWNyZXQ= # base64("supersecret")base64 は誰でも復号できる。Secret リソースは「便利な保管場所」であって暗号化ではない。
本格的な保護には:
- Sealed Secrets (Bitnami): 公開鍵で暗号化、Git に置ける
- External Secrets Operator: AWS Secrets Manager / Vault と連携
- etcd の暗号化を有効化: クラスタレベルで保存時暗号化
13. アンチパターン総まとめ
ログにシークレットを書く
log.Printf("connecting with password=%s", dbPassword) // NGログ収集基盤、検索インデックス、Slack 通知などにそのまま流れる。シークレットを print 系に渡さないルールを徹底。
エラーメッセージに混入
fmt.Errorf("failed to authenticate with key %s", apiKey) // NGエラーログ・APM (Sentry, Datadog) に乗ってチームに広まる。
Slack / Issue にうっかりペースト
「動かないんですが」と Slack にシークレット込みの実行ログ全文を貼る → チーム全員に漏洩 → 退職者がいたら社外漏洩リスク。Slack も漏洩経路。
練習課題
mkdir -p ~/learn/security/day-secrets
cd ~/learn/security/day-secrets
git init.envと.env.exampleを作る。.gitignoreで.envを除外- Go で
godotenvを使って.envを読み込み、未設定なら Fatal で落ちる起動コードを書く gitleaksをインストールし、.envをgit addしてgitleaks protect --stagedでブロックされることを確認- pre-commit フックに
gitleaks protect --stagedを仕込む - (任意) AWS アカウントがあれば、Secrets Manager に 1件作成し、Go から読む
- 32バイトのランダム JWT シークレットを生成して
.envに書く:openssl rand -base64 32
締め: git で証跡を残す
cd ~/learn/security/day-secrets
git add main.go .gitignore .env.example go.mod go.sum
git status # .env が無視されているか必ず確認
git commit -m "feat(security): シークレット管理(.env + gitleaks + Secrets Manager)"チェックリスト
- シークレット管理の段階0〜5を区別できる
-
.gitignoreで.envを除外し、.env.exampleを Git に入れる慣習を実践した - gitleaks を pre-commit に仕込んで動作確認した
- Dockerfile に
ENV API_KEYを書いてはいけない理由を語れる - AWS Secrets Manager の概要と Vault の代替性を理解
- envelope encryption の仕組みを口で説明できる
- STS / OIDC 連携で「長期キーを発行しない」運用を理解
- 漏洩発見時の「ローテーション最優先」フローを言える
詰まった時のチートシート
| やりたいこと | コマンド / コード |
|---|---|
| 32バイトのランダムシークレット生成 | openssl rand -base64 32 |
| Go で .env 読み込み | github.com/joho/godotenv |
| gitleaks スキャン | gitleaks detect --source . |
| 履歴からファイル削除 | git filter-repo --path .env --invert-paths |
| AWS Secrets Manager 取得 (CLI) | aws secretsmanager get-secret-value --secret-id prod/db |
| GitHub Actions のシークレット参照 | ${{ secrets.NAME }} |
| Docker でシークレット注入 | docker run --env-file .env image |
| Kubernetes Secret 作成 | kubectl create secret generic mysec --from-literal=key=val |
「実務OK」基準
- コードレビューで「これシークレット直書きだよ」と指摘できる
- 新規プロジェクトを始める時、最初に
.gitignoreと.env.exampleを整備する癖がある - CI から AWS への認証を OIDC ベースで構築できる
- 漏洩発見の連絡が来たら、ローテーション → ログ調査 → 範囲特定の順で動ける
- Kubernetes Secret が暗号化ではないことを知り、本格保護策(Sealed Secrets / External Secrets)を選べる
さらに深掘りするなら
- AWS の「Cloud Security Best Practices」 - IAM / Secrets Manager / KMS の公式まとめ
- HashiCorp Vault のドキュメント - シークレット管理 OSS の標準
- 「The Twelve-Factor App」III. Config - 設定とコードを分離する原則
gitleaks/truffleHogの GitHub - パターン定義を読むとどんなキー形式があるか勉強になる- AWS Capital One 事件、Uber GitHub 事件の事後報告書 - 事故事例から学ぶ
メタ認知: 自分の理解度を診断する
30秒セルフテスト
.envを 絶対 git に入れない ための3点セット- AWS キーが漏洩したら 最初の30分で やるべきこと(順序)
- Kubernetes Secret が「暗号化ではない」と言われる理由
- CI に AWS 認証を渡す モダンな方法 は?(ヒント: OIDC)
- KMS と Secrets Manager の役割の違い
詰まったら → 各段階(開発・CI・本番)の対策表を再読。
次のレッスン
2-5 入力バリデーション で、ユーザー入力の検証・サニタイズを詳しくやる。「入力を信用しない」がセキュリティの基本姿勢。
間隔反復ポイント
シークレット管理は 「新規プロジェクト立ち上げのたび」 に判断が要る分野。1ヶ月に1度、この章の「最低限の3点セット」を眺める習慣を作ると、リポジトリ作成と同時に安全な設定が反射的に出る。