ADR-0155: 静的シークレットのクラウド集約と CI の Google キーレス化 (GitHub OIDC + Workload Identity Federation)
- Status: Accepted
- Mode: Critical
- Kruchten Type: Existence/Executive
- Scope: platform
- Implementation Status: Not Started
- 起案者: [email protected]
- 起案日時 (JST): 2026-06-16 16:43
- 承認日時 (JST): 2026-06-16
- Approver Role: platform
- Approver Who: [email protected]
- Driver: [email protected]
- Consulted: Decision Pipeline AI 審査 (Gate 0-4)
1. コンテキスト
1.1 背景
ADR-0104 / ADR-0110 / RQ-085 の系譜で、1 人法人 monorepo における長寿命シークレット管理の構造的脆弱が露見した。本決定はその系譜の残テーマ (theme③ = RQ-085 推奨 #1 / #2) に対応する。
1.2 現状 (As-Is)
長寿命シークレット (失効しない API キー・Basic 認証情報・Google OAuth refresh token・Cloudflare API token 等) が、ローカル macOS Keychain・GitHub Actions Secret・各 Worker の wrangler secret・GCP Secret Manager と複数箇所に散在し、単一の正本 (SoT) が存在しない。同一の秘密がローカルとクラウドに二重保持されている。Keychain canonical 6 項目 (setup_keychain.sh: OPENAI/ANTHROPIC/GEMINI/LITELLM_MASTER の API キー + DECISION_PIPELINE_AUTH ユーザー/パスワード) に加え、docs-search・OCR・CF Workflows read token・ops alert webhook 等が Keychain と GitHub Secret に二重保持されている。
1.3 課題
3 つの構造的問題がある: (1) 長寿命シークレットは勝手に期限切れにならず、漏洩しても気づかなければ無期限に悪用可能 (blast radius が時間方向に無限)。(2) 失効を事前検知できず、破綻して初めて気づく。(3) ローカル Keychain 依存のため、新 PC・headless・sub サンドボックス・CI など実行環境によって疎通可否がぶれる。定量根拠: ADR-0104 ベースラインで clasp OAuth client の失効で deploy.yml が直近 60 run 連続失敗 (100%・2026-05-29〜06-01)。
1.4 制約・要件
- CI から長寿命 Google 鍵 (SA キー JSON 等) を 0 にする。
- 追加金銭コスト $0/月 (Secret Manager / WIF / Cloudflare Secrets Store 無料枠内) を維持。
- Cloudflare 側は完全な鍵レス化が不可で、least-privilege の長寿命 API token は許容。
- ローカル開発・launchd (GCS 同期) は当面 user ADC を維持 (ディスクに長寿命 SA 鍵ファイルを置かない)。
- clasp/Apps Script の鍵レス化は ADR-0104 決定② (別 PoC・可否未確定) に委ね、本決定スコープ外。
- 対象リポジトリは現時点 private 前提。public 化時は本決定の信頼条件を再評価する (§5.3 / §7 で扱う)。
1.5 目標 (To-Be)
CI から長寿命 Google 鍵を 0 にし、失効を破綻前に検知し、新 PC/headless/sub での疎通を安定させ、秘密の正本を単一化する。Non-Goals: ローカル開発の ADC 廃止、clasp 経路の鍵レス化、Cloudflare の完全鍵レス化。
2. 決定
静的シークレットの正本をクラウド管理 vault (GCP 側 = Secret Manager、Cloudflare 側 = Cloudflare Secrets Store) に一元化し、CI → Google Cloud のアクセスを GitHub OIDC + Workload Identity Federation (WIF) で鍵レス化する。ローカル Keychain は読み取り専用の派生コピーに格下げする。残る長寿命シークレット (Cloudflare token 等) は vault を正本とし、失効事前検知を CI に組み込む。
3. 判断基準 (Decision Drivers)
3.1 評価軸
| # | 軸 | 重要度 (係数) | 案件特有の解釈 |
|---|---|---|---|
| 1 | #secure | [Must] (×2.0) | 長寿命シークレットの露出面・滞留を最小化。K.O. = CI に長寿命 Google 鍵が残るなら不採用。 |
| 2 | #operable | [Must] (×2.0) | 新 PC・headless・sub・CI から疎通が安定し、失効を破綻前に検知できる。 |
| 3 | #maintainable | [High] (×1.0) | 正本が単一で所在が明確になる。 |
| 4 | #efficient | [Medium] (×0.5) | 追加金銭コスト $0/月を維持 (無料枠内)。 |
K.O. criterion: Must 軸 (#secure / #operable) の score < 3 は不採用。
3.2 評価軸 × 案スコア表
各案 0-5 で評価。加重和 = (Σ score × 係数) / (5 × Σ 係数) で正規化 (0.0-1.0)。Σ 係数 = 2.0+2.0+1.0+0.5 = 5.5。満点 = 5 × 5.5 = 27.5。
| 軸 | 係数 | 採択案 (Z: vault 集約 + WIF) | 案 A (現状維持 + rotation 強化) | 案 B (外部 vault SaaS) | 案 C (Cloudflare 含め一括鍵レス) |
|---|---|---|---|---|---|
#secure | ×2.0 | 5 | 1 | 4 | — (実現不可) |
#operable | ×2.0 | 4 | 2 | 4 | — |
#maintainable | ×1.0 | 4 | 2 | 4 | — |
#efficient | ×0.5 | 5 | 5 | 2 | — |
| 加重和 (正規化) | 0.864 | 0.418 | 0.764 | N/A | |
| K.O. 通過 (Must ≥ 3) | ✓ | ❌ (#secure=1) | ✓ | ❌ (実現不可) |
4. 検討した代替案 (Alternatives Considered)
- 案 A (現状維持 + 個別 rotation 強化): スクリプトで定期 rotation を足すのみ — 長寿命シークレットと Keychain 依存が残り
#secure/#operableで K.O.。失効事前検知も得られない (§3.2:#secure=1)。 - 案 B (外部 vault SaaS = Doppler / Infisical に全集約): RQ-085 で評価。ネイティブ機構 (GCP Secret Manager + Cloudflare Secrets Store + GitHub OIDC) で要件を満たせ、外部 SaaS への依存・課金・新たな信頼境界を避けられるため不採用 (規模拡大時に再評価)。
- 案 C (Cloudflare 含め全面キーレス化を一括前提): Cloudflare はキーレス化不可 (least-privilege 長寿命 token が必須) で一括前提が成立しないため不採用。Google 側のみ WIF で鍵レス化する段階解 (Z) を採る。
5. 影響 (Consequences)
5.1 正の影響 (Good)
- CI の長寿命 Google 鍵が 0 になる。
- 失効を事前検知できる (Cloudflare token 残日数の 30 日 / 14 日 2 段階アラート)。
- Keychain 依存の疎通不安定が解消、新 PC 復元が容易になる。
- 秘密の正本が単一化 (vault = SoT、Keychain = 派生)。
5.2 負の影響 (Bad)
- WIF プール・vault という管理対象が増える。
- 初期設定の一度きりコスト (約 1.25 人日)。
- Cloudflare 側は長寿命 token が残り、年次 rotation が必要。
- KPI が Google 側に偏ると Cloudflare 側の rotation 放置リスクがある (§5.3 / §7 で対応)。
5.3 中立・トレードオフ (Neutral / Trade-offs) — リスクと緩和
- WIF 信頼条件の過緩 (fork PR / Poisoned Pipeline Execution):
attribute.repositoryだけでは fork PR (特にpull_request_target) からの権限借用を弾けない (GitHub Security Lab 2023 / GitHub 公式 2024)。→ 緩和: 信頼条件はattribute.repositoryに加えassertion.subのenvironment:セグメントを必須フィルタとし、GitHub Environment Protection Rules でレビュワー承認を要求。CI 側でgithub.event_name == 'pull_request_target'かつ fork からの呼び出しを明示ブロック。本リポジトリは現時点 private、public 化時は本 ADR を再評価する (§7 で manifest 化)。 - GCP 組織ポリシー (
constraints/iam.workloadIdentityPoolProviders) の後付け適用: Workspace 管理者が制約を有効化すると既存 WIF プールが即時無効化される可能性。→ 緩和: 実施前にgcloud org-policies list --project=PROJECT_IDで WIF 関連制約を確認し結果を Confirmation チェックリストに記録。検出された場合は別プロジェクトに Pool を分離する経路を撤退条件 (§7) に併記。 - KPI 計測スコープの穴 (GitHub Secret 以外の鍵退避):
gh secret listのみでは GCP Secret Manager・wrangler secret・ローカル SA 鍵ファイルに退避した SA 鍵 JSON を検出できない。→ 緩和: 検査対象を 4 レイヤー (GitHub Secret / GCP Secret Manager / wrangler secret / ローカル SA 鍵ファイル) に拡張 (§7 Confirmation)。clasp 経路は「暫定除外・ADR-0104 決定② 完了時に統合」と期限付き明記。 - 1 SA で広範な
secretAccessorを持つことによる種類方向 blast radius: → 緩和: CI ジョブ種別ごとに SA を分割 (deploy / data-sync / ops-monitor 等)、各 SA は最小限の secret にのみroles/secretmanager.secretAccessorを bind する設計 (§7 実装方針)。 - Secret Manager rotation 後の旧バージョン未
DISABLED: 新版追加後に旧版をDISABLEDし忘れると、新 token revoke 時に CI 全停止のパターン。→ 緩和: rotation 手順書に「旧バージョン即時DISABLED」を必須化、ENABLEDバージョン ≥ 2 を CI pre-flight で警告、保持ポリシー (最大 3 件) を Terraform で宣言。 - 低頻度ジョブで失敗率閾値が機能しない: 月次ジョブは 4 週間で母数が小さく「5% 超」に達しない。→ 緩和: 撤退条件を「実行頻度別: 月次以下のジョブは 2 回連続失敗で即時ロールバック」に細分化 (§6)。
- Cloudflare token 年次 rotation の放置 (認知クロージャー): → 緩和: KPI ④ として「Cloudflare token 残有効日数 ≥ 60 日を常時維持」を追加 (§7)、月次スクリプトで残日数を Issue に自動投稿。
- Cloudflare Secrets Store の無料枠未確定: pricing 揺れ (Cloudflare Changelog 2024-09)。→ 緩和: 現時点の無料枠条件 (件数・呼び出し上限) を ADR に記録し、有料化時の移送先 (GitHub Secrets または GCP Secret Manager) と移送手順概略を撤退条件 (§6) に併記。
- 移行期の値ズレ: vault と Keychain の値不整合。→ 緩和: vault を正本と宣言し Keychain は派生として運用、移行直後に値一致を 1 回検証。
- 無音失敗 (Google 到達 / 権限不足): → 緩和: CI に pre-flight (トークン取得 + 最小 API 200 応答) を入れ fail-loud にする。
6. 撤退条件 (Rollback Plan)
- WIF 移行後 4 週間以内に、CI の Google 系ジョブの失敗率が 5% を超え、かつ 24 時間以内に原因特定できない 場合、当該ジョブを旧 secret 方式に一時ロールバックして切り分ける (WIF プールは温存)。
- 実行頻度別の追加閾値: 月次以下の低頻度ジョブ (GCS 同期・定期 rotation 等) は 2 回連続失敗で即時ロールバック (失敗率閾値が母数不足で機能しないため)。
- vault 集約後に、同一シークレットの値ズレ (正本不整合) が 月 1 件以上 発生したら、集約範囲を縮小し正本運用を見直す。
- Cloudflare の無料枠条件改定で Secrets Store / WIF 周辺が月額 $0 を超過したら、即座に代替案を再評価する。移送先候補: (a) GitHub Actions Secret + 環境別分離、(b) GCP Secret Manager に Cloudflare 用 secret も寄せる。移送手順概略: ① 新 vault に書き込み → ② Worker / CI の参照先を環境変数経由で切替 → ③ 1 週間並走し read 監査ログで旧側参照ゼロを確認 → ④ 旧側を
DISABLED→ ⑤ 30 日後に削除。 - GCP 組織ポリシー (
constraints/iam.workloadIdentityPoolProviders等) が後付け適用された場合、WIF Pool を別プロジェクトに分離して継続するか、旧 SA 鍵方式に一時退避するかを 24 時間以内に決定する。
コスト試算
- 実装工数:
- WIF プール + SA + CI ワークフロー切替: 約 0.5 人日
- vault への集約・移送 + Keychain の派生化: 約 0.5 人日
- pre-flight 失効検知 + KPI スクリプト: 約 0.25 人日
- 合計 約 1.25 人日 (AI 支援あり)
- 運用コスト: $0/月 (GCP Secret Manager・WIF は無料、Cloudflare Secrets Store は現時点無料枠内 — ただし上限が公式に未確定のため §6 撤退条件で監視)。
- rotation 工数: Cloudflare 側の長寿命 token のみ年 1 回 約 10 分。
Confirmation
- 検証手段:
- 長寿命 Google 鍵残存検査 (4 レイヤー): (a)
gh secret list+ grep で GitHub Actions Secret、(b)gcloud secrets listの値検査で GCP Secret Manager、(c)wrangler secret listで各 Worker、(d) リポ内*.json+ Keychain item の grep でローカル SA 鍵ファイル、を PR 毎にスキャン。検出時 CI を fail させ issue 自動起票。clasp 経路は ADR-0104 決定② 完了時まで暫定除外と明記。 - WIF pre-flight: デプロイ前にトークン取得 + 最小 API (例:
storage.buckets.get) の 200 応答を確認。 - Secret Manager バージョン健全性:
ENABLEDバージョンが 2 以上の secret を検出したら CI で警告。保持ポリシー (最大 3 件) を Terraform で宣言。 - 失効残日数アラート: 残長寿命 token (Cloudflare 等) の失効までの残日数をログ出力し、30 日で Issue 起票 / 14 日でエスカレーション の 2 段階。
- WIF 信頼条件監査: Terraform 上で
attribute.repository+assertion.sub(environment:セグメント) の AND 条件が維持されていることを月次で確認。 - GCP 組織ポリシー事前確認: ADR 実施前に
gcloud org-policies list --project=PROJECT_IDを実行し、WIF 関連制約の有無を本 ADR に記録 (Implementation Status を In Progress に進める前提条件)。
- 長寿命 Google 鍵残存検査 (4 レイヤー): (a)
- 実行頻度: PR 毎 (鍵残存検査 / pre-flight) / 月次 (KPI 集計 / 残日数 / 請求 / 信頼条件監査)。
- 違反時対応: 長寿命 Google 鍵を検出したら CI を意図的に fail させ issue 自動起票。Cloudflare token 残日数 < 14 日でエスカレーション (Slack または GitHub Issue mention)。
- 観測可能 KPI:
- CI 内の長寿命 Google 鍵数 (4 レイヤー合計) = 0。
- Keychain 専用依存の自動経路 (headless/sub/CI から Keychain が無いと動かない経路) = 0。
- 失効起因の無告知断 = 0 件/四半期。
- Cloudflare token 残有効日数 ≥ 60 日を常時維持 (月次自動投稿スクリプトで Issue 化)。
- 実装スコープ追記: CI ジョブ種別ごとの SA 分割設計 (deploy / data-sync / ops-monitor 等) と各 SA の secret 単位 IAM binding を Terraform で明示。Cloudflare token 残日数を月次で Issue / Slack に自動投稿するスクリプトを実装対象に含める。
- Review After (JST): 2026-09-16 (移行 3 ヶ月後。WIF 移行効果・KPI 達成・Cloudflare Secrets Store 無料枠条件を振り返り、必要なら本 ADR を改訂する)。
参照 (References)
- 関連 ADR:
- ADR-0104: 補完 (theme① = clasp/GAS デプロイ認証の Internal OAuth 化。決定② の ADC キーレス化 PoC は本決定の Google キーレス化と方向一致、Apps Script は別経路のため別実験として扱う)。
- ADR-0110: 補完 (theme② = DRP の Basic 認証 → Cloudflare Access。DRP 認証情報の Keychain 依存解消と「Keychain を正本にしない」方針が整合)。
- 関連 PR/Issue: -
- 外部資料:
- RQ-085: 出典 (3 モデル Deep Research 推奨 #1 = 秘密を単一 vault に集約・ネイティブ優先 / #2 = GitHub OIDC + Google WIF で CI をキーレス化)。
- GitHub Security Lab "Poisoned Pipeline Execution" (2023)。
- GitHub 公式: Workload Identity Federation と
pull_request_targetの組み合わせ警告 (2024 年版)。 - Google Cloud IAM ベストプラクティス: 1 workload = 1 SA。
- Cloudflare Changelog (2024-09): Secrets Store pricing 記載。