• 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.0514— (実現不可)
#operable×2.0424
#maintainable×1.0424
#efficient×0.5552
加重和 (正規化)0.8640.4180.764N/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.subenvironment: セグメントを必須フィルタとし、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

  • 検証手段:
    1. 長寿命 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 決定② 完了時まで暫定除外と明記。
    2. WIF pre-flight: デプロイ前にトークン取得 + 最小 API (例: storage.buckets.get) の 200 応答を確認。
    3. Secret Manager バージョン健全性: ENABLED バージョンが 2 以上の secret を検出したら CI で警告。保持ポリシー (最大 3 件) を Terraform で宣言。
    4. 失効残日数アラート: 残長寿命 token (Cloudflare 等) の失効までの残日数をログ出力し、30 日で Issue 起票 / 14 日でエスカレーション の 2 段階。
    5. WIF 信頼条件監査: Terraform 上で attribute.repository + assertion.sub (environment: セグメント) の AND 条件が維持されていることを月次で確認。
    6. GCP 組織ポリシー事前確認: ADR 実施前に gcloud org-policies list --project=PROJECT_ID を実行し、WIF 関連制約の有無を本 ADR に記録 (Implementation Status を In Progress に進める前提条件)。
  • 実行頻度: PR 毎 (鍵残存検査 / pre-flight) / 月次 (KPI 集計 / 残日数 / 請求 / 信頼条件監査)。
  • 違反時対応: 長寿命 Google 鍵を検出したら CI を意図的に fail させ issue 自動起票。Cloudflare token 残日数 < 14 日でエスカレーション (Slack または GitHub Issue mention)。
  • 観測可能 KPI:
    1. CI 内の長寿命 Google 鍵数 (4 レイヤー合計) = 0
    2. Keychain 専用依存の自動経路 (headless/sub/CI から Keychain が無いと動かない経路) = 0
    3. 失効起因の無告知断 = 0 件/四半期
    4. 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 記載。