コンテンツにスキップ

0018. Supabase への同一オリジン リバースプロキシ (企業DLPのドメイン遮断回避)

コンテキストと課題

Web (apps/web) は Cloudflare Pages 配信で、ブラウザから Supabase (auth = GoTrue / REST = PostgREST / Storage) を直接呼ぶ構成でした。本番の接続先はカスタムドメイン sb.waste-link.appyybolwcdojbjwyymzskk.supabase.co への CNAME)。

2026-07 に一部テナント(西宮)でログイン不能が発生。原因は企業ネットワークの DLP / プロキシが Supabase ドメインを遮断・傍受していたこと:

  • sb.waste-link.app/auth/v1/token への preflight 応答から CORS ヘッダが剥がされ、ブラウザが No 'Access-Control-Allow-Origin' header で失敗(Failed to fetch)。
  • 生ドメイン *.supabase.co/auth/v1/health会社のブロック画面が返る。

つまり「ブラウザが Supabase ドメインへ直接 HTTP を出す」という前提そのものが、DLP 環境では成立しない。キー形式や CORS 設定の調整では解けない(ネットワーク層のドメイン遮断)。

決定

アプリと同一オリジンの Cloudflare Pages Function /supabase/* を経由して Supabase を中継する。 ブラウザからは「既に許可済みのアプリドメインへのリクエスト」にしか見えず、DLP のドメイン遮断を回避する。同一オリジンなので CORS preflight 自体が発生しない。Supabase への実ホップはエッジでサーバ側実行され、企業プロキシから不可視。

構成

  1. functions/supabase/[[path]].ts(リポジトリ直下・CF Pages root_dir="" 準拠): /supabase プレフィックスを除いた残りを SUPABASE_PROXY_TARGET(Pages 環境変数・既定=本番 Supabase 生ドメイン)へ透過中継。メソッド・ヘッダ(apikey/Authorization 含む)・ボディ・ステータスをそのまま転送。redirect: 'manual'、body はストリーム透過(content-encoding 二重化なし)。
  2. apps/web/src/lib/supabase.ts: VITE_SUPABASE_URL === '/supabase'(または 'same-origin')のとき実行時オリジン配下 ${window.location.origin}/supabase に解決。それ以外は絶対 URL を直用(既存挙動不変・ローカル開発は従来どおり)。有効化は本番 Pages env を VITE_SUPABASE_URL=/supabase にして再ビルドするだけ。

セキュリティ上の論拠

  • 転送先ホストは env で固定SUPABASE_PROXY_TARGET)。攻撃者が制御できるのは path/query/body のみで宛先ホストは固定なので、任意ホストへの SSRF にはならない(Supabase のパスにしか到達しない)。
  • プロキシは Supabase 自身の認可(apikey + RLS + JWT)を一切迂回しない。公開されるのは「Supabase は元々インターネット公開」という既存事実と等価で、新たな権限は増えない。
  • Realtime (WebSocket) は本アプリ未使用のため対象外。

結果

  • 全 Supabase トラフィック(auth/REST/Storage)が同一オリジン → エッジ Function → 生 Supabase の経路になる。ブラウザは sb.waste-link.app も生 *.supabase.co一切叩かないsb.waste-link.app は設定は残るが休眠)。
  • DLP のドメイン遮断・CORS 剥奪の影響を受けない。エッジ1ホップ増えるがレイテンシ影響は軽微(両者 CF エッジ)。
  • ロールバックは本番 Pages env VITE_SUPABASE_URL を絶対 URL に戻して再ビルドするだけ(可逆)。
  • 本番全体のルーティング変更のため、preview での実ログイン経路(POST /auth/v1/token)検証を投入前ゲートとする。