ADR 0001: secret 管理を External Secrets Operator + GCP Secret Manager に統一
ステータス
提案 (UZU-5262)
コンテキスト
UZU の K8s manifest では、すべてのサービス (backend / mdms_jp / uzutokyo / studio_editor_backend / auth / analytics_server / 以降の追加サービス) が、各 overlay ({service}/k8s/{env}/kustomization.template.yaml) の secretGenerator.literals に 機密値を平文で直書き している。
具体的には DB password, R2 HMAC key, Cloudflare API token, LiveKit secret, OAuth client secret などが git 履歴に永久に残っている。
問題
- private repo であっても repo 読み取り権限を持つ全員が値を見られる
- rotation のたびに dev/stg/prd の複数 PR / 複数箇所を更新する必要がある (SoT が分散)
- 退職者・権限変更時の invalidation には先に rotation が必須
決定
External Secrets Operator (ESO) を 3 クラスタ全てに導入し、GCP Secret Manager を Source of Truth とする。K8s Secret は ESO が ExternalSecret リソース経由で同期する。
機密の配置 (ハイブリッド)
env 境界を GCP プロジェクト単位 で物理的に作るため、Secret Manager を 2 階層で使う:
| 種別 | 配置先 | 命名規則 | ClusterSecretStore |
|---|---|---|---|
| env 固有 | uzu-{env} の Secret Manager | {service}-{key-kebab} (env suffix なし、project が env を表現) | gsm-uzu-{env} |
| env 共通 | uzu-shared の Secret Manager | {prefix}-{key-kebab} (env なし) | gsm-uzu-shared |
例:
uzu-dev/Secret Manager/uzutokyo-database-url← env 固有uzu-shared/Secret Manager/cloudflare-api-token← env 共通 (dev/stg/prd で同一値)
env 共通は rotation を 1 箇所更新するだけで全 env に反映される。
ESO のインストール
Helm chart は使わず、公式 release の external-secrets.yaml (install.yaml) を infra/k8s/external-secrets/base/install.yaml に pin する。
namespace は default のまま運用する。install.yaml は Helm chart の template 展開出力で、cert-controller の --service-namespace / --secret-namespace と webhook の --dns-name に default が hardcode されているため、別 namespace で動かすには毎リリースで args patch が必要になる。UZU の他サービス (backend / mdms_jp / uzutokyo / auth / studio_editor_backend 等) も default namespace で動いているので素直に揃える。
apply は infra_k8s_external_secrets.yml workflow が infra/k8s/external-secrets/** の差分検知で自動実行する (terraform_apply.yml と同じパターン: dev/stg は main push、prd は tag push)。手元から kubectl apply -k を直接打つのは検証時の例外のみ。
kubectl apply は server-side で行う (CRD schema が ~250KB と大きく client-side apply の last-applied-configuration annotation 上限 262144 byte を超えるため)。さらに base (CRD + controller) と overlay (ClusterSecretStore = CR) は 2 段に分けて間で CRD の establish を待つ (CRD register 完了前に CR apply が走ると no matches for kind で失敗する)。
Helm を採用しない理由:
- UZU は他に Helm を使っていない。ESO 単独のために helm provider を terraform に追加する利得が小さい
- CRD のバージョン差分は manifest 比較で十分追える (
install.yamlを release ごとに置き換える) - 既存の kustomize 流儀と一貫する
Workload Identity 設計
各環境に external-secrets@uzu-{env}.iam.gserviceaccount.com を作成し、クラスタ内の KSA external-secrets/external-secrets から WI 経由で impersonate させる (GSA 名は GCP の account_id 制約 6 文字以上を満たすため eso ではなく external-secrets を採用)。GSA には以下 2 つの IAM:
- 自プロジェクト (
uzu-{env}) のroles/secretmanager.secretAccessor— env 固有 secret 用 (infra/terraform/{env}/eso.tf) uzu-sharedのroles/secretmanager.secretAccessor— env 共通 secret 用 (infra/terraform/shared/eso.tf)
段階的移行 (UZU-5262 のスコープ)
- 検証フェーズ (本 ADR 提案時点) — dev クラスタに ESO 基盤を導入し、env 固有 (
uzutokyoの DATABASE_URL) と env 共通 (backendの CLOUDFLARE_API_TOKEN) の 2 系統で sync 動作を確認する。中間段階の ExternalSecret は別名 ({service}-secret-external) で K8s Secret を生成し、Pod は既存のsecretGeneratorと並行する形でenvFromを 2 段重ねる (後勝ちで ESO 経由の値が採用される) - 本実装フェーズ — secret 単位で順次 ExternalSecret に置き換え、
kustomization.template.yamlからsecretGenerator.literalsを削除する。stg / prd へ展開 - clean up — git 履歴に残った漏洩 secret は別 issue で rotation 対応
代替案 (採用しなかったもの)
案 S: 全 secret を uzu-shared に集約
env 固有も {service}-{env}-{key} 形式で uzu-shared に置き、ClusterSecretStore を 1 つにする案。
不採用理由: env 境界が project IAM で物理的に切れない。external-secrets@uzu-dev GSA が uzu-shared 全体の secretAccessor を持つと、dev クラスタから prd 用 secret が読める状態になる。これを防ぐには IAM Condition で secret 名の正規表現マッチが必要だが、Condition の書き間違いで漏洩する事故リスクがある。
案 V: Sealed Secrets / SOPS で git にコミット
公開鍵で暗号化された値を git に置き、クラスタ内の controller が復号する案。
不採用理由: rotation は依然として「git に commit + push」が必要 (SoT が git のままになる)。privacy 観点の改善は限定的で、退職時の鍵 invalidation 運用も追加で必要になる。GCP の Secret Manager をすでに使っている前提では ESO の方が薄い差分で導入できる。
案 H': env プロジェクトのみで env 共通も複製
env 共通 secret も dev/stg/prd の各 Secret Manager に同じ値を 3 個投入する案。
不採用理由: rotation が 3 箇所更新になる。値が drift するリスクが復活する。
影響
- インフラ: 各 env スタックに
eso.tf、shared スタックに越境 IAM を追加。infra/k8s/external-secrets/を新設 - 開発フロー: 新規 secret 追加は Secret Manager への投入 + ExternalSecret 追記の 2 ステップ。詳細は別途 runbook を整備する
- security: 平文 secret が git から消える (履歴に残った分は別 issue で rotation)
参照
- External Secrets Operator docs
- UZU-5262 (本 ADR の親 issue)
- 既存パターン:
uzu-shared/Secret Managerのtranslator-github-app-*(env 共通 secret の前例)