Skip to content

機密情報 (secret) の管理 ​

UZU の機密値 (DB password, API token, GitHub App PEM, OAuth secret 等) は GCP Secret Manager を単一の Source of Truth として管理する。ローカル開発・K8s 本番・Cloudflare Workers 本番の 3 経路から同じ Secret Manager を参照する形で統一する。

設計判断の根拠は ADR 0001 を参照。

SoT モデル ​

K8s 経路は External Secrets Operator (ESO) が Secret Manager → K8s Secret の同期を自動化する (検証フェーズで dev クラスタに先行導入済み、stg/prd は UZU-5262 本実装フェーズで展開)。CF Workers 経路は現状手動同期、将来的に WIF + GHA で自動化する余地あり。

2 階層レイアウト ​

env 境界を GCP プロジェクト単位で物理的に切る ため、Secret Manager を 2 階層で使う。

種別配置先プロジェクト命名規則ClusterSecretStore例
env 固有uzu-{env}{service}-{key-kebab} (env suffix なし、project が env を表現)gsm-uzu-{env}uzu-dev/uzutokyo-database-url
env 共通uzu-shared{prefix}-{key-kebab}gsm-uzu-shareduzu-shared/cloudflare-api-token, uzu-shared/translator-github-app-private-key

env 共通に置いて良いもの ​

  • 1 アカウントに 1 個しか発行されない token (Cloudflare API token 等)
  • dev/stg/prd で意図的に同じ値を共有したいもの (社外 SaaS の共通鍵)
  • ローカル開発専用で本番には流れないもの (translator GitHub App credential 等)

「env で値が違うもの」は必ず env 固有側 (uzu-{env}) に置く。共通側に {key}-dev / {key}-prd を並べて IAM Condition で振り分ける案は不採用 (Condition の書き間違いで漏洩する事故リスクがあるため、ADR 0001 案 S 参照)。

key 名の付け方 ​

  • {service}-{key-kebab} — kebab-case 固定 (env suffix は付けない)
  • 値は 1 secret = 1 用途に分け、JSON で複数値を束ねない (rotation 単位を細かく保つ)
  • 複数行 / バイナリ (PEM 等) はそのまま投入し、利用側で改行処理を行う

ローカル開発経路 ​

各開発者は gcloud auth application-default login 済み + sally-inc.jp domain の roles/secretmanager.secretAccessor を保持していれば追加設定不要 (= 新メンバーは ADC ログインだけで動く)。

構成 ​

.dev.vars (もしくは .env.local) は task <service> 起動の度に再生成する。リポジトリにも開発者の Home にも長期保管しない。

参考実装 ​

  • scripts/translator/gen-dev-vars.sh — gen script の参考
  • taskfiles/translator.yaml の _api task — cmds 前置の参考

新規 secret の追加手順 (ローカル開発) ​

  1. gcloud secrets create {service}-{key-kebab} --project=uzu-shared --replication-policy=automatic (env 共通の場合)
  2. gcloud secrets add-iam-policy-binding ... --member=domain:sally-inc.jp --role=roles/secretmanager.secretAccessor
  3. echo -n "<value>" | gcloud secrets versions add {service}-{key-kebab} --project=... --data-file=-
  4. scripts/<service>/gen-dev-vars.sh の出力に 1 行追加

複数行値 (PEM 等) を dotenv (.dev.vars) で扱う場合は 1 行化が必須。改行を \n リテラルにエスケープし、コード側で replace(/\\n/g, '\n') で戻す:

bash
printf 'PRIVATE_KEY="'
fetch <service>-private-key | awk 'BEGIN{ORS="\\n"} {print}'
printf '"\n'

.gitignore への .dev.vars 追加を忘れない (commit されると Secret Manager に置いた意味が消える)。

K8s 本番経路 (ESO) ​

ESO 基盤は dev クラスタに先行導入済み、stg/prd は本実装フェーズで展開予定。

構成 ​

新規 secret の追加手順 (K8s) ​

  1. Secret Manager に投入 (上記「ローカル開発」と同じ手順)

  2. <service>/k8s/<env>/external-secret.yaml を新規 or 追記。例:

    yaml
    apiVersion: external-secrets.io/v1
    kind: ExternalSecret
    metadata:
      name: <service>-secret
    spec:
      refreshInterval: 1h
      secretStoreRef:
        kind: ClusterSecretStore
        name: gsm-uzu-{env}        # env 固有なら gsm-uzu-{env}、共通なら gsm-uzu-shared
      target:
        name: <service>-secret      # 生成される K8s Secret 名
        creationPolicy: Owner
      data:
        - secretKey: DATABASE_URL   # Pod envFrom が参照する key
          remoteRef:
            key: <service>-database-url  # Secret Manager 上の secret 名
  3. <service>/k8s/<env>/kustomization.template.yaml の resources に external-secret.yaml を追加

  4. kustomization.template.yaml の secretGenerator.literals から該当行を削除

  5. Pod の envFrom.secretRef.name が <service>-secret を指していれば変更不要

参考実装 ​

  • backend/k8s/deploy/dev/external-secret.yaml — env 共通 (uzu-shared) パターン
  • javascript/uzutokyo/k8s/dev/external-secret.yaml — env 固有 (uzu-dev) パターン
  • infra/k8s/external-secrets/dev/cluster-secret-store.yaml — ClusterSecretStore 定義
  • infra/terraform/dev/eso.tf — GSA + WI binding + 自プロジェクト IAM
  • infra/terraform/shared/eso.tf — 越境 IAM

ESO 基盤自体のデプロイ ​

infra/k8s/external-secrets/** の差分検知で infra_k8s_external_secrets.yml workflow が dev/stg/prd へ自動 apply する (terraform_apply.yml と同じパターン: dev/stg は main push、prd は tag push)。手元から kubectl apply -k を直接打つのは検証時の例外のみ。

Cloudflare Workers 本番経路 ​

ESO は K8s 内でしか動かないので、CF Workers の secret は別経路で投入する。

bash
cd javascript/apps/<service>/worker-api
wrangler secret put SOME_API_KEY     # 値は Secret Manager から取って貼る

Secret Manager 上の値が SoT である点は K8s 経路と同じ。CF Workers への同期は現状手動。将来的に GitHub Actions の WIF 経由で Secret Manager → wrangler secret を自動同期する形にすると一元化できる (v1 では手動で OK)。

Rotation 運用 ​

  1. GCP Secret Manager 上で versions add (新 version を作るだけ。旧 version は disable → 数日後 destroy)
  2. ローカル開発: 次回 task <service> 起動で .dev.vars 再生成 → 自動反映
  3. K8s 本番: ExternalSecret の refreshInterval (1h) で自動反映。即時反映したい場合は ExternalSecret に annotation を打って sync を強制
  4. CF Workers 本番: 手動で wrangler secret put を再実行

env 共通 secret (uzu-shared) は 1 箇所更新するだけで全 env (dev/stg/prd) に反映される。

トラブルシュート ​

症状確認
ローカルで gen-dev-vars.sh が permission deniedgcloud auth application-default login 済みか / sally-inc.jp domain かを確認
kubectl get clustersecretstore が Ready にならないexternal-secrets@uzu-{env} GSA に自プロジェクト + uzu-shared の secretAccessor が付いているか / WI annotation が KSA に付いているか
kubectl get externalsecret <name> が SecretSyncedErrorSecret Manager 上の key 名が remoteRef.key と一致しているか / 値が空でないか
ESO controller が動いていないkubectl get pods -n external-secrets で external-secrets Pod の状態 / infra_k8s_external_secrets.yml workflow の最新 run

参照 ​