50_セキュリティ
本章では、filix-shadowwork-api におけるセキュリティ前提と、認証・署名・Secrets/PII の取り扱いを定義する。
ここで扱うのは「どのように安全性を確保するか」という設計上の前提であり、個別の実装詳細(ライブラリ呼び出し等)は対象外とする。
セキュリティ方針(前提)
-
最小権限(Least Privilege)
API の利用者・運用者・外部サービスに付与する権限は最小化する。 -
入力はすべて不正である前提
すべてのリクエスト入力を検証し、想定外のデータを拒否する。 -
外部サービスとの境界は署名/トークンで保護する
Webhook、管理 API、LLM 連携は必ず認証情報で境界を守る。 -
監査可能性(Auditability)
不具合や不正の切り分けのため、重要操作はログに残る設計にする(PIIを含めない)。
認証(Authentication)
目的
- 「このリクエストが誰のものか」を確実に特定する(なりすまし防止)
user_idの真正性を担保する
前提
- フロントエンドは Supabase Auth によりログイン状態を得る。
- バックエンドは、フロントエンドから提示される認証情報を検証して user_id を確定する。
認証方式(JWT + Cookie)
採用方式
- JWT(JSON Web Token)
- Supabase Auth の access token をバックエンドで検証し、内部JWT を発行して返す。
- フロントエンドは以後、すべての API リクエストで JWT をCookieで送信する。
- バックエンドは JWT の署名を検証し、
subクレーム(= Supabase user_id)からユーザーを特定する。
詳細仕様
JWT発行エンドポイント:POST /api/auth/exchange
- 入力:Supabase access token(リクエストボディに
tokenフィールド) - 処理:
- Supabase JWT を JWKS / issuer / audience で検証し、
subを取得 subを内部JWTのsubとして再発行- HttpOnly / Secure / SameSite Cookie で返却
- 出力:
{ "ok": true, "member_id": "supabase-user-uuid", "token_type": "Bearer", "expires_in": 900 } member_idは後方互換のためのレスポンス名であり、値は Supabase のsubを返す。
JWT仕様
- 署名方式:HS256(HMAC-SHA256)
- 署名鍵:
JWT_SIGNING_SECRET(環境変数/Secret) - 有効期限:900秒(15分)
- クレーム:
sub:Supabase user_id(ユーザー識別子)iss:filix-shadowwork-api(発行者)aud:shadowwork-navigator-web(対象者)exp:発行時刻 + 900秒iat:発行時刻- 送信方法:Cookie(以下参照)
Cookie属性
Set-Cookie: access_token=<JWT>; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=900
HttpOnly:JavaScript からのアクセス禁止(XSS対策)
- Secure:HTTPS のみで送信
- SameSite=Strict:CSRF対策(クロスサイトリクエストで送信しない)
- Path=/:全パスで有効
- Max-Age=900:900秒後に自動削除
Supabase access token 検証
- 検証方法:Supabase の JWKS を用いた署名検証 + issuer / audience 検証
- 使用する環境変数:
SUPABASE_JWKS_URL、SUPABASE_ISSUER、SUPABASE_AUDIENCE - 検証失敗時:401 エラーを返す
ユーザー特定の流れ
- フロント:Shadowwork Navigator で Supabase Auth にログインし、access token を取得
- フロント:
POST /api/auth/exchangeに Supabase access token を送信 - API:Supabase JWT を署名検証し、
subを取得 - API:内部JWT を生成し、Cookie で返却
- フロント:以後の API リクエストで自動的に Cookie が送信される(credentials: include)
- API:受け取った JWT を検証 →
subから Supabase user_id を確定 - API:
user_idはすべて JWT のsubから取得(クライアント入力は受け付けない)
必須要件(実装前の前提確認)
- Supabase JWT の検証条件(JWKS / issuer / audience)が確定していること
SUPABASE_JWKS_URL、SUPABASE_ISSUER、SUPABASE_AUDIENCE、JWT_SIGNING_SECRETなどの環境変数が設定可能であること- ブラウザの Cookie 送受信が確実に機能すること(CORS credentials)
CORS(Cross-Origin Resource Sharing)
方針
- フロント(Shadowwork Navigator)は別ドメイン(
https://shadowwork-navigator.com)で運用される。 - JWT は Cookie で送信するため、CORS で
Access-Control-Allow-Credentials: trueが必須。 - 認証情報を含むため、許可 Origin は
*(ワイルドカード)禁止、allowlist で明示的に制限する。
許可Origin(Allowlist)
本番
https://shadowwork-navigator.com(完全一致のみ)Originヘッダがあるリクエストで、allowlist 不一致の場合は403 FORBIDDEN(origin not allowed)を返す。ALLOWED_ORIGINSが空の場合、Originヘッダ付きリクエストは許可しない(fail close)。
開発・Preview
- 原則として preview(
*.pages.dev) は許可しない。 - セキュリティと Cookie 運用との相性が悪いため。
- どうしても必要な期間だけ、例外として 1件だけ allowlist に追加(例:
https://shadowwork-navigator-9lt.pages.dev)。 - 追加時も、ワイルドカード(
*.pages.dev)は禁止。
独自ドメイン導入時
- 本番 allowlist に追加し、環境変数で管理する。
CORS レスポンスヘッダ
Access-Control-Allow-Origin: https://shadowwork-navigator.com(許可Originのみ)
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Vary: Origin
Vary: Origin:キャッシュの重複を防止(重要)
- Origin ヘッダが無いリクエスト(サーバー間通信やCLI等)は CORS 判定対象外として扱う。
クライアント実装
fetch('https://api.shadowwork-navigator.com/api/thread/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include', // Cookie の送受信を有効化
body: JSON.stringify({ /* ... */ })
});
利用権限の前提(paid 等)
- 有料状態(paid)は
user_flags.paidを参照する。 - paid の更新経路は
40_課金と利用権限に従う。 - 認証により確定した user_id に対してのみ paid を参照する(JWT の
subから確定される)。
Stripe Webhook の署名検証
目的
- Webhook の送信元が Stripe であることを保証し、偽装を防ぐ。
識別子の取り扱い(Checkout ↔ Webhook)
- Checkout Session 作成時に、Supabase の user_id(JWT の
sub)をclient_reference_idに設定する。 - Webhook 側は
client_reference_idのみを信頼し、user_idとして扱う(フォールバックは行わない)。
必須要件
- Stripe が付与する署名ヘッダ(例:
Stripe-Signature)を検証する。 - 検証に使用する秘密鍵(webhook secret)は 環境変数(Secrets) として安全に管理する。
- 検証失敗時は 4xx を返し、DB更新等の副作用を起こさない。
追加要件
- タイムスタンプ検証により、リプレイ攻撃の難易度を上げる。
- Webhook の冪等性(重複排除)は
40_課金と利用権限に従う。
管理用API(/api/admin/*)の保護
/api/admin/set_paid のような管理操作は、通常のユーザー操作と同等には扱えない。
誤用・悪用時の影響が大きいため、強い制限を必須とする。
必須要件
- 管理者のみ実行可能
- 管理者トークン、または環境変数で持つ共有秘密を用いて認証する。
- 本番環境では最小限
- 可能なら本番で露出させず、運用ツールや一時的な手段に閉じる。
/api/admin/set_paid の認証方式(確定仕様)
- JWT(Cookie)で管理者(Supabase user_id)を特定し、
ADMIN_MEMBER_IDS(カンマ区切りallowlist、変数名は据え置き)に含まれること - 追加の管理トークン(共有秘密) が一致すること
- 環境変数:
PAID_ADMIN_TOKEN - リクエストヘッダ:
X-PAID-ADMIN-TOKEN: <token> - いずれかが満たされない場合は 403 を返す
追加要件
- IP 制限(Cloudflare Access / Zero Trust など)
- 監査ログ(誰がいつ何を変更したか)
- レート制限(ブルートフォース対策)
Secrets の管理
対象
- Stripe webhook secret
- LLM API key
- 管理者トークン(admin secret)
- その他、外部サービスの API key / secret
方針
- Secrets は リポジトリにコミットしない
- Cloudflare Workers の Secrets / Vars に格納する
- ログに出さない(例外メッセージに含めない)
PII(個人情報)の取り扱い
取り扱い対象
user_id(会員基盤の識別子)- ユーザー入力(シャドウワークの内容は機微性が高い)
方針
- 最小限の保存: 必要な範囲に限定して保存する
- D1は平文保存しない: メッセージ本文は D1 に暗号文としてのみ保存する(本文平文は保存しない)
- 封筒暗号(DEKのラップ): 本文暗号化に用いた共通鍵(DEK)は、別系統の鍵(KEK)で暗号化(ラップ)し、平文のDEKをDBに保存しない
- 鍵の保管場所を分離: KEK(マスター鍵)は AWS KMS 等の鍵管理基盤で保管・ローテーション・監査し、DBにはラップ済みDEKのみを保存する
- ログには出さない: 平文(
/api/thread/chat入力、復号後本文)をログに出さない - アクセス制御: 認証された本人のデータのみ取得可能とする
- データ削除: 将来、ユーザー要求に応じた削除・退会対応を検討する(運用ポリシー)
補足:
- メッセージ永続化は暗号文保存(/api/thread/message)を前提とし、DBに平文を保存しない。
ベクトルDB(Qdrant)に保存するチャンクの扱い
ベクトルDBを活かすため、本文(暗号文)とは別に「検索用チャンク」を保存する。チャンクはやむを得ず平文を含みうるため、本文と同等に機微情報として扱う。
方針
- 派生データとして扱う: ベクトルDBは検索インデックスであり、本文の正(canonical)は D1 の暗号文とする
- 最小化: 保存する平文は検索に必要な範囲に限定する(過剰な前後文脈や個人情報の付与を避ける)
- アクセス制御: Qdrant はパブリックに露出させず、API 経由のみでアクセスする(APIキー/ネットワーク制限/TLS)
- ログ抑制: チャンク平文や検索結果の平文をログに出さない
- 削除伝播: 退会・削除要求時は、D1 とベクトルDBの双方から削除する(運用ポリシーに明記)
注記: - 「ベクトルDBを使うために平文が必須」というより、embedding と検索用payloadが必要である。payloadは平文が最も単純だが、リスクに応じて「最小スニペットのみ」「ID参照のみ」等の方式を選択できる。
入力検証(Validation)
- すべての API は入力を検証し、想定外の型・範囲・サイズは拒否する。
- LLM利用料金の抑制を目的とした文字数制限は、
message(/api/thread/chatの平文入力)に適用する。 /api/thread/messageの保存入力は、平文ではなく暗号文を受けるため、コスト制御ではなく保存API保護(型妥当性・異常入力対策)として検証する。client_message_idなどの識別子は型・サイズ制約を設ける。id(run/thread/message 等)は形式を統一し、受け入れ可能な文字種を限定する。
CORS とブラウザ公開
- ブラウザから API を呼ぶ場合、CORS を適切に設定する。
- Origin を必要以上に許可しない(固定のフロントURLを許可する)。
- 認証情報(Authorizationヘッダ等)を使う場合は、CORS で明示的に許可する。
参照
20_API仕様: 認証前提、エラー形式40_課金と利用権限: Webhook と paid 更新の冪等性60_利用制限と運用ポリシー: 悪用対応、運用上の制限