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から漏洩、影響範囲広

漏洩したら:

  1. 金銭被害: 不正クラウド利用、データ持ち出し
  2. 二次攻撃: そのキーで他システムへ侵入
  3. 訴訟・罰金: GDPR で最大年商4%
  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 キー公開の事故テンプレ

よくあるパターン:

  1. 開発中に AWS キーを config.py などにベタ書き
  2. 急いで GitHub に public push(うっかり private にし忘れ)
  3. 数分後に bot が検出 → 攻撃者の手に渡る
  4. 数時間で大量の EC2 や Lambda が攻撃者により起動
  5. 24時間で請求が数万〜数十万ドル
  6. 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.sh

GitHub 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-1

CI から 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
  1. .env.env.example を作る。.gitignore.env を除外
  2. Go で godotenv を使って .env を読み込み、未設定なら Fatal で落ちる起動コードを書く
  3. gitleaks をインストールし、.envgit add して gitleaks protect --staged でブロックされることを確認
  4. pre-commit フックに gitleaks protect --staged を仕込む
  5. (任意) AWS アカウントがあれば、Secrets Manager に 1件作成し、Go から読む
  6. 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秒セルフテスト

  1. .env絶対 git に入れない ための3点セット
  2. AWS キーが漏洩したら 最初の30分で やるべきこと(順序)
  3. Kubernetes Secret が「暗号化ではない」と言われる理由
  4. CI に AWS 認証を渡す モダンな方法 は?(ヒント: OIDC)
  5. KMS と Secrets Manager の役割の違い

詰まったら → 各段階(開発・CI・本番)の対策表を再読。

次のレッスン

2-5 入力バリデーション で、ユーザー入力の検証・サニタイズを詳しくやる。「入力を信用しない」がセキュリティの基本姿勢。

間隔反復ポイント

シークレット管理は 「新規プロジェクト立ち上げのたび」 に判断が要る分野。1ヶ月に1度、この章の「最低限の3点セット」を眺める習慣を作ると、リポジトリ作成と同時に安全な設定が反射的に出る。