# Secret Rotation Runbook (SSoT)

Parky で投入している全 secret の **rotation trigger / 手順 / 検証 / rollback** を 1 ファイルに集約する。
旧 [secret-rotation.md](./secret-rotation.md) は基本手順の入門で、本ドキュメントは provider 別に網羅した運用基準書。

> **大原則**
> - 値そのものを git に commit しない。1Password が SSoT。
> - 必ず **先に新 key を発行 → wrangler secret put → 検証 → 旧 key revoke** の順。逆順は本番停止リスク。
> - rotation は記録に残す: 1Password 履歴 + Discord `#p2-deploys` に投稿。
> - rollback path は **「旧 key を一時的に revoke 解除する / 1Password 履歴から旧値を取り出して再投入」** の 2 経路。
>   どちらも provider 側で履歴 / shadow key を保持していることが前提。
>
> **共通ツール**
> - `bash api/scripts/set-secrets.sh <env>`（[set-secrets.sh](../../api/scripts/set-secrets.sh)）— `secret-keys.txt` + `secret-keys-1p-map.json` 経由で wrangler に一括投入。
> - `wrangler secret put <NAME> --config <toml> --env <env>` — split worker 個別投入。
> - `bash .claude/scripts/op-cache/op-cache.sh get op://...` — 1Password の値取得（DPAPI 暗号化 cache 経由）。

---

## 0. rotation 共通フロー

1. 影響範囲確認: 当該 secret を読む Worker / Pages / GitHub Actions を [secret-keys.txt](../../api/scripts/secret-keys.txt) と [secret-keys-1p-map.json](../../api/scripts/secret-keys-1p-map.json) で照合。
2. provider の Dashboard で**新 key を発行**（旧 key は即 revoke しない / shadow 状態にする）。
3. **1Password に新値を保存** — 同一 item を上書き（旧値は履歴に残る）。
4. **Cloudflare Workers / Pages / GitHub Actions に投入**:
   - Workers: `bash api/scripts/set-secrets.sh <env>`（`SECRETS_SOURCE=1password-map` 推奨）。split worker は `WRANGLER_CONFIG=wrangler.<channel>.toml`。
   - Pages: Cloudflare Dashboard → Pages → 該当 project → Settings → Environment variables。
   - GitHub Actions: `gh secret set <NAME>` または Settings → Secrets and variables → Actions。
5. **検証** — 後述の provider 別スモーク。
6. **旧 key を revoke**（provider Dashboard）。少なくとも **24 時間** shadow を維持してから revoke するのが望ましい。
7. **記録** — Discord `#p2-deploys` に「<secret> rotated, prev=YYYY-MM-DD, by=<actor>」を投稿。

---

## 1. Cloudflare R2

### 対象 key
- `R2_ACCESS_KEY_ID`
- `R2_SECRET_ACCESS_KEY`
- `R2_ENDPOINT` / `R2_BUCKET` / `R2_REGION` / `R2_PUBLIC_BASE`（通常 rotate 不要）

### trigger
- **定期: 年 1 回（Q3）**。
- PC compromise / `.dev.vars` 漏洩疑い時 → 即時。
- R2 access key を発行した GitHub PAT / 1Password account 退職時。

### 手順
1. Cloudflare Dashboard → R2 → Manage R2 API Tokens → 新 token 発行（Object Read & Write、対象 bucket: `parky` / `parky-dev`）。
2. 旧 token は **revoke せず** に残す（shadow 期間）。
3. 1Password `PJ｜Parky / R2 API Token` (item `rhxd2jnzp5dqbetn7kiky6aala`) を上書き保存。
4. Workers 投入（split worker 全 toml に必要）:
   ```bash
   # 2026-05-11: wrangler.store-sync.toml は削除済 (queue consumer は admin Worker 内)。
   # Phase 2 で再分離する際は git 履歴から復元 + 本ループに復活させる。
   for cfg in wrangler.toml wrangler.public.toml wrangler.admin.toml \
              wrangler.marketing.toml; do
     SECRETS_SOURCE=1password-map WRANGLER_CONFIG=$cfg \
       bash api/scripts/set-secrets.sh prod
   done
   ```
5. ローカル `.dev.vars` を新値に更新（`.dev.vars.example` をコピーして埋める）。
6. **検証** → §1 検証項目。
7. 24h 後に旧 token を revoke（Dashboard）。

### 検証
- `curl -I https://api.parky.co.jp/v1/healthz/ready` → 200。
- 画像アップロード smoke: 管理者 portal → メディアアップロード → R2 PUT 成功 + `cdn.parky.co.jp` 経由で表示。
- presigned URL 発行: `curl -X POST https://api.parky.co.jp/v1/admin/uploads/presign` → 200 + `url` フィールドが新 access key を含む。

### rollback
- 旧 token がまだ revoke されていない場合 → 1Password 履歴から旧値を引いて再 set-secrets。
- 旧 token を revoke 済 → 別の新 token を再発行（既存は完全死亡）。

---

## 2. Stripe

### 対象 key
- `STRIPE_SECRET_KEY` (`sk_live_...` / `sk_test_...`) — 1P item `esc3gnumtqf7o6r5to77tso3re/credential`
- `STRIPE_WEBHOOK_SECRET` (`whsec_...`) — 1P item `esc3gnumtqf7o6r5to77tso3re/webhook-secret`

### trigger
- **定期: 四半期 1 回（Q1）**。
- 漏洩疑い時 → 即時（Stripe Dashboard で alerting あり）。
- Stripe アカウント担当変更時。

### 手順
1. Stripe Dashboard → Developers → API keys → **「Roll」**（rolling key で旧 key を 12h shadow にできる）。
2. Webhook secret は Webhooks → 該当 endpoint → Signing secret → 「Roll」。
3. 1Password 上書き → `set-secrets.sh prod` 投入。
4. 検証後、shadow 期間内（rolling key の expire 前）に確認完了。

### 検証
- Stripe Dashboard → Webhooks → 「Send test webhook」→ 200 受信を確認。
- Owner portal → クレジット購入 sandbox → 完了 → DB `revenue_transactions` に row が立つ。
- Workers log（Sentry）に `StripeAuthenticationError` / `signature_verification_failed` が出ていないこと。

### rollback
- Rolling key は 12h shadow が自動なので、その間は旧 key も使える。投入済の secret を旧値に戻したい場合は 1Password 履歴から復元 → 再投入。
- Rolling 期限切れ後は新 key を再 issue するしかない。

---

## 3. Resend (Newsletter / Mail)

### 対象 key
- `RESEND_API_KEY` (`re_...`) — 1P item `i3o2f6tfukrt3f4i22jj455qbm/credential`

### trigger
- **定期: 年 1 回（Q4）**。
- Marketing portal の broadcast スケジュール繁忙期は避ける。
- Resend Dashboard で「key created at」が 12 か月超えたら即 rotate。

### 手順
1. Resend Dashboard → API Keys → Create API Key（権限: Sending Access / 該当 domain）。
2. 旧 key は revoke せず shadow。
3. 1Password 上書き → `set-secrets.sh prod` 投入。
4. Marketing Portal の `marketing_integrations` 設定 (engine: resend) に保存されたトークンが必要なら別途再認可。

### 検証
- Marketing portal → ニュースレター → 「テスト送信」を `dev@parky.co.jp` 宛に投げる → 受信確認。
- DB `newsletter_broadcasts.status` が `succeeded` になること、`error_message IS NULL`。
- Workers log に `Resend API 401` が出ていないこと。

### rollback
- 旧 key shadow 中なら 1Password 履歴から復元 → 再投入。
- shadow 切れ後は新 key 再発行のみ。

---

## 4. Firebase Cloud Messaging (FCM Service Account)

### 対象 key
- `FIREBASE_PROJECT_ID`
- `FIREBASE_CLIENT_EMAIL`
- `FIREBASE_PRIVATE_KEY`

3 値セットで 1 つの SA JSON。`secret-keys-1p-map.json` は現状 `_TODO`（item ID 未確定）。

### trigger
- **定期: 半年 1 回**。
- SA JSON の漏洩疑い時 → 即時。
- Firebase Console で SA に「不審な activity」検知時。

### 手順
1. Firebase Console → プロジェクト設定 → サービスアカウント → 「新しい秘密鍵の生成」（JSON ダウンロード）。
2. 旧 SA key は **削除せず Disabled に**（即時 revoke は 30 分 push 停止リスク）。
3. 1Password に新 SA JSON を保存（`project_id` / `client_email` / `private_key` の 3 field）。
4. `wrangler secret put FIREBASE_PROJECT_ID --env prod` 等を 3 回実行。`private_key` は改行を `\n` リテラルに置換するか、複数行入力時はそのまま貼り付け（[`lib/integration/fcm/`](../../api/src/lib/integration/fcm/) で両対応）。
5. ローカル `.dev.vars` も同期。

### 検証
- 管理者 portal → 通知メッセージ管理 → テストプッシュ → 自分の端末で受信。
- `user_notifications` 行の `status='delivered'` になること。
- Workers log に `FCM 401 / 403` が出ていないこと。

### rollback
- 旧 SA key を Firebase Console で「Enabled」に戻す → 旧 JSON を 1Password 履歴から復元 → 再投入。

---

## 5. Mapbox

### 対象 key
- `MAPBOX_ACCESS_TOKEN`（admin portal / web home / api 全部で使用） — 1P item `l6gwgm3a3ac3rks3tsne56ilgu/token`

### trigger
- **定期: 年 1 回（Q3）**。
- Mapbox Studio で usage が予想を逸脱 → 漏洩疑いで即時。

### 手順
1. Mapbox Studio → Account → Access tokens → Create a token（scope: styles:read / fonts:read / datasets:read / vision:read 等、Admin secret は不要）。
2. URL restrictions（Referer allowlist）を `parky.co.jp` / `dev.parky.co.jp` / `localhost` に設定。
3. 旧 token は revoke せず shadow。
4. 1Password 上書き。
5. **配布先が複数**:
   - api Workers: `wrangler secret put MAPBOX_ACCESS_TOKEN`
   - web/portal/admin: GitHub Secrets `VITE_MAPBOX_TOKEN` 更新 → Pages 再 deploy
   - web/home (Astro): GitHub Secrets `PUBLIC_MAPBOX_TOKEN` 更新 → 再 deploy
   - mobileapp/prototype: dart-define もしくは Flutter `--dart-define` で渡す（[`mobileapp/prototype/flutter/`](../../mobileapp/prototype/flutter/)）

### 検証
- `dev-admin.parky.co.jp` → 駐車場マップビュー表示成功。
- ブラウザ DevTools Network で `api.mapbox.com` への request が 200 で返ること。
- Mapbox Studio → Statistics で新 token の usage が増えていること。

### rollback
- 旧 token shadow 中なら 1Password 履歴 → 再投入で全 surface 戻す。
- フロントエンド側は再 deploy が必要なので rollback 時間が長くなる点に注意（Mapbox エラーは UX 直撃のため shadow 期間を 7 日確保推奨）。

---

## 6. Discord Webhooks

### 対象 key
- `DISCORD_WEBHOOK_ALERTS` (#p0-alerts) — `yoakhiuw24bctzs65qtovs7j4q/credential`
- `DISCORD_WEBHOOK_OPS` (#p1-ops) — `qzrpntb2drvshjjc6ittvm6wbu/credential`
- `DISCORD_WEBHOOK_DEPLOYS` (#p2-deploys) — `z6mu3s6cwa6ymyweilvdkk6czm/credential`
- `DISCORD_WEBHOOK_INSIGHTS` (#p3-insights) — `2jtvdvtzcqh7cxdlbyj4eudnwi/credential`
- `DISCORD_WEBHOOK_ADMIN_TASKS` (#admin-tasks) — `carbd2wvkzw34wvxln37xhduku/credential`
- (注) **Discord Bot Token** は webhook と別経路。bot を使う場合は別 secret として管理。

### trigger
- **定期: 不要**（webhook URL は server に紐付き revoke 機構が単純）。
- 漏洩疑い / channel 削除時 → 即時。
- 「webhook 経由で spam が出た」など外部報告があった時。

### 手順
1. Discord → 該当 channel → 編集 → Integrations → Webhooks → 該当 webhook → 「Copy URL」が公開済み判明なら **「Delete」**。
2. 同 channel で **新 webhook を作成**（名前を旧と同じ `parky-alerts` 等にする）。
3. 1Password 上書き（5 item を個別に管理）→ `set-secrets.sh prod` 投入。

### 検証
- `curl -X POST -H 'Content-Type: application/json' -d '{"content":"rotation test"}' <new_url>` → 204 No Content。
- Workers から `lib/notify/discord.ts` 経由でテスト送信（dev で `notify-smoke` 関数を叩く）→ 該当 channel に届く。

### rollback
- Discord webhook は revoke が即時で履歴復元不可。失敗したら新 webhook を作り直すしかない。
- そのため **複数 channel を同時 rotate しない**。1 channel ずつ検証 → 次へ。

---

## 7. OAuth Providers (Google / Meta / X / Apple Sign-In)

### 対象 key
- `GOOGLE_OAUTH_CLIENT_ID` / `GOOGLE_OAUTH_CLIENT_SECRET`（GA4 / Search Console / Sign-in）
- `META_CLIENT_ID` / `META_CLIENT_SECRET`（Instagram / Facebook 連携）
- `X_CLIENT_ID` / `X_CLIENT_SECRET`（X 投稿連携）
- Apple Sign-In: 1P item `33d5...`（あかり先生と共通の memory に記載）— Apple `private_key`

### trigger
- **定期: 四半期 1 回（Q2）**。
- OAuth client を作成した GCP / Meta / X account の所有者退職時。
- 漏洩疑い / phishing 報告時。
- Apple は private key の expiry が来る前（180 日）に必ず rotate。

### 手順（Google を例に）
1. Google Cloud Console → APIs & Services → Credentials → 該当 OAuth 2.0 Client ID → 「ADD SECRET」（client_secret v2 を併存できる）。
2. 旧 secret はまだ revoke しない（shadow）。
3. 1Password 上書き → `set-secrets.sh prod` 投入。
4. Marketing Portal の `marketing_integrations.config` に保存された `refresh_token` は **再認可不要**（client_secret 切替のみ）。
5. 1 週間 shadow → 旧 secret revoke。

### Meta / X
- ほぼ同手順。「App Secret」を Meta App Dashboard → 設定 → 基本 → App Secret → Rotate。X は Developer Portal → Project & Apps → Keys and tokens → Regenerate。
- 旧 secret 即時無効化されるので **shadow 期間が無い**。検証を即やる。

### Apple Sign-In private key
1. Apple Developer Portal → Certificates, IDs & Profiles → Keys → 新 key 作成（Sign In with Apple capability）。
2. .p8 ダウンロード → 1Password に保存（item `33d5...`）。
3. `wrangler secret put APPLE_PRIVATE_KEY_P8` で Workers に投入（mobileapp 側は不要）。
4. 旧 key を Apple Portal で `Revoke` する前に検証完了させる。

### 検証
- Marketing portal → 該当 integration の「再連携テスト」ボタン → 200。
- Mobile app → Apple Sign-In flow → 認証完了 → DB `app_users.auth_provider='apple'` row 確認。
- Workers log に `oauth: invalid_client` が出ていないこと。

### rollback
- Google: shadow 中なら 1Password 履歴から旧 secret 復元 → 再投入。
- Meta / X: 即時 revoke のため shadow 不可。新 secret で問題があれば「もう 1 回 regenerate」して全 client を更新するしかない。
- Apple: 旧 key を Apple Portal で `Active` に戻す（revoke 済なら不可）。

---

## 8. Supabase Service Role / Anon

### 対象 key
- `SUPABASE_SERVICE_ROLE_KEY`（dev / prod それぞれ）
- `SUPABASE_ANON_KEY`

### trigger
- **定期: 年 1 回（Q1）**。
- service_role が admin portal / Workers 以外から漏洩した疑い → 即時。
- Supabase 側の Auth secret（JWT signing secret）変更時 → 連動して rotate。

### 手順
1. Supabase Dashboard → Project Settings → API → 「Reset service_role key」（**警告: 旧 key は即無効**）。
2. 1Password の dev / prod item（`kbkdtwii67pzuqclywkt2efeby` / `3pwvrrk7z2nftyvcuynvyafbra`）を上書き。
3. **同時投入が必須**: Workers / Pages / GitHub Actions / 開発者全員の `.dev.vars` を更新。
4. `set-secrets.sh prod` 投入 → Pages も Dashboard で更新 → GitHub Secrets も更新（`gh secret set SUPABASE_SERVICE_ROLE_KEY`）。

### 検証
- `/v1/healthz/ready` 200。
- Admin portal ログイン → ダッシュボード表示。
- DB に直接書き込む BFF endpoint（例: `/v1/admin/parking-lots` POST）で 201。

### rollback
- Supabase は履歴を保持しないため **rollback 不可**。投入失敗時は再 rotate のみ。
- そのため maintenance window 中に実施推奨。

---

## 9. その他の secret

| Secret | 1P item | trigger | 主な手順 |
|---|---|---|---|
| `MARKETING_ENCRYPT_KEY` | `uiyveqol2765hpjqa6vowkrlvy/credential` | **不変**（rotate すると既存暗号化レコードが復号不可） | rotate しない。漏洩時は dual-key 移行が必要 — 別 runbook 起票。 |
| `STATE_HMAC_SECRET` | `un4ydlnvn5xqpjvhqjgo3v75pm/credential` | 半年 1 回 | OAuth flow の state 改ざん検知用。rotate 時 in-flight OAuth は失敗するが redirect で復帰可能。 |
| `CF_PURGE_TOKEN` | `p6ss3qurj5msxcz6igsqvhdcbu/credential` | 年 1 回 | Cloudflare Dashboard → My Profile → API Tokens → Roll。 |
| `OP_SERVICE_ACCOUNT_TOKEN` | (1Password Web の SA 設定画面) | 年 1 回 / 退職時 | [secret-rotation.md §1Password SA](./secret-rotation.md#1password-service-account-token) 参照。 |
| `CLOUDFLARE_API_TOKEN` | (1Password Web で管理) | 年 1 回 | [secret-rotation.md §Cloudflare API Token](./secret-rotation.md#cloudflare-api-token) 参照。 |
| `SENTRY_DSN` | (要 1P item 起票) | 不要（DSN は半 public） | dev/prod 同 DSN、`ENVIRONMENT` で tag 分離。 |
| `POSTHOG_API_KEY` | `mbzaca2ejah2wubk4w2xi2qv6a/rtc7puyic5ggabm3kdfkvwm23i` | 年 1 回 | PostHog → Project settings → API keys → Rotate。client SDK 側 (`VITE_POSTHOG_KEY`) も同期。 |
| Apple App Store Server API（`APPLE_PRIVATE_KEY_P8`） | (要 1P item 起票) | 半年 1 回 | App Store Connect → Users and Access → Keys → 新 key 作成 → 旧 key revoke。 |
| Google Play Developer API（`GOOGLE_PLAY_PRIVATE_KEY`） | `k2jwdkhjscpdqlgi7ncqisdhni/...` | 年 1 回 | GCP Console → IAM → SA → Keys → 新 key 発行 → 旧 key disable。 |

---

## 10. Rotation 頻度カレンダー

四半期ごとにまとめて実施することで本番影響をコントロールする（複数を同日に rotate しない）。

| Quarter | 対象 secret | メモ |
|---|---|---|
| **Q1 (1〜3 月)** | Stripe (`STRIPE_SECRET_KEY` / `STRIPE_WEBHOOK_SECRET`) / Supabase (`SUPABASE_SERVICE_ROLE_KEY`) | 決算期前。Stripe は rolling key 機能で shadow 12h。Supabase は maintenance window 必須。 |
| **Q2 (4〜6 月)** | OAuth (`GOOGLE_OAUTH_*` / `META_CLIENT_*` / `X_CLIENT_*`) / Apple Sign-In private key / FCM Service Account | 連休前後を避ける。OAuth は marketing_integrations の再認可テストを併走。 |
| **Q3 (7〜9 月)** | R2 (`R2_ACCESS_KEY_ID` / `R2_SECRET_ACCESS_KEY`) / Mapbox (`MAPBOX_ACCESS_TOKEN`) / `CF_PURGE_TOKEN` | 夏期休暇前に shadow 期間を確保。Mapbox は全 surface 同時 deploy に注意。 |
| **Q4 (10〜12 月)** | Resend (`RESEND_API_KEY`) / PostHog (`POSTHOG_API_KEY`) / `OP_SERVICE_ACCOUNT_TOKEN` / `CLOUDFLARE_API_TOKEN` / `STATE_HMAC_SECRET` | 年末対応・繁忙期前。 |
| **常時（即時）** | 漏洩疑い / 退職 / PC compromise | カレンダー無視で即 rotate。Discord `#p0-alerts` に announce。 |
| **不変（rotate 禁止）** | `MARKETING_ENCRYPT_KEY` | rotate すると既存暗号化レコード復号不可。漏洩時は dual-key 移行設計を別途起票。 |

---

## 11. 関連ドキュメント

- [secret-rotation.md](./secret-rotation.md) — 旧基本手順（1Password SA / CF API Token / Supabase / FCM の最低限のみ）。本 runbook が拡張版。
- [secrets-injection-runbook.md](./secrets-injection-runbook.md) — 初回投入手順（rotation ではなく setup 用）。
- [api/scripts/secret-keys.txt](../../api/scripts/secret-keys.txt) — 投入対象 secret の一覧（SSoT）。
- [api/scripts/secret-keys-1p-map.json](../../api/scripts/secret-keys-1p-map.json) — secret key → 1Password op-ref のマッピング。
- [.claude/memory/parky/reference_parky_secrets.md](../../.claude/memory/parky/reference_parky_secrets.md) — Parky 専用 1Password Item ID 一覧（memory）。
- [api/CLAUDE.md §13](../../api/CLAUDE.md) — `.env` 廃止 / `.dev.vars` 一本化ルール。
