Supabase RLS無効: 有効にする前に確認すべき点
Security Advisorに赤い警告が出ると、まずRLSを有効にするのが当然に見えます。しかし稼働中のアプリでその設定だけを変えると、読み取りと書き込みが同時に止まることがあります。一方、後回しにすればデータが露出した状態が続きます。テーブルの用途、現在の権限、既存ポリシーを洗い出し、必要な変更を一つのテスト済みリリースとして適用しましょう。
スイッチを操作する前に確認すべき4つの点
- そのテーブルはData API経由で公開されていますか?
anonとauthenticatedの各ロールには、どのGRANT権限がありますか?using (true)のような条件で広いアクセスを許す既存ポリシーはありませんか?- ブラウザがpublishableキーではなく、secretキーやservice-roleキーを使っていませんか?
RLS disabled in publicは、publicスキーマ内のテーブルにおいて、行レベルセキュリティが無効になっていることを意味します。GRANTは、ロールがテーブル上で実行できる操作を制御します。RLSは、これらの操作を許可された行に限定します。どちらか一方で、もう一方の役割を代替することはできません。
RLSはカラムを隠しません。同じ行にメールアドレス、内部メモ、またはブラウザに届いてはいけない他のフィールドが含まれている場合、制限のないテーブル全体のSELECTを許可しないでください。代わりにカラム権限、PostgreSQL 15以降のsecurity_invokerビュー(必要なカラムのみを公開)、または別のサーバーAPIを使用してください。
Supabaseの2026年のデフォルト設定を、プロジェクト作成日だけから推測しないでください。新しい既定値は5月30日から新規プロジェクトへ段階的に適用され、既存プロジェクトには10月30日までに適用される予定です。変更されるのは今後作成するテーブルのデフォルトGRANTであり、既存テーブルの権限が自動的に取り消されるわけではありません。プロジェクト設定と現在の権限を直接確認しましょう。
1. 権限とすべての既存ポリシーを確認
Database → Security Advisorを開き、影響を受けるテーブルを特定し、ログイン前でも読み取れるべきか、サインインしたユーザーのみが書き込めるべきか、サーバー専用のテーブルかを判断してください。SQLエディタで現在のポリシーを一覧表示してください:
select policyname, permissive, roles, cmd, qual, with_check
from pg_policies
where schemaname = 'public' and tablename = 'profiles';
新しいポリシーを追加しても、アクセスが厳格になるとは限りません。複数のpermissiveポリシー(デフォルト種別)はOR条件で合成されます。1つの古いusing (true)またはto publicポリシーは、新しいユーザーごとのポリシーを超えたアクセスを許可する可能性があります。新しいポリシーを追加する前に、不要なポリシーを削除または狭めます。既存の名前のポリシーがある場合は、その定義を確認し、ALTER POLICYまたはレビュー済みのDROP POLICYマイグレーションを使用してください。CREATE POLICYを盲目的に再実行しないでください。
2. RLS、権限、ポリシーを一緒に準備
この例では、profiles.user_idがサインインしたユーザーのIDを保存していると仮定します。これにより、認証されたユーザーが自身の行を選択、挿入、更新、削除できます。製品に合わせてテーブル、カラム、ルールを調整し、ステージング環境で実行してください。
begin;
alter table public.profiles enable row level security;
grant select, insert, update, delete
on table public.profiles to authenticated;
create policy "Users can view own rows"
on public.profiles for select
to authenticated
using ( (select auth.uid()) = user_id );
create policy "Users can insert own rows"
on public.profiles for insert
to authenticated
with check ( (select auth.uid()) = user_id );
create policy "Users can update own rows"
on public.profiles for update
to authenticated
using ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );
create policy "Users can delete own rows"
on public.profiles for delete
to authenticated
using ( (select auth.uid()) = user_id );
commit;
更新の場合、usingが既存の行を変更できるかどうかを決定し、with checkが変更後の行を検証します。with checkを省略するとPostgreSQLはusing条件を再利用しますが、両方を明記した方が意図したルールをレビューしやすくなります。UPDATEにも、行が表示できるようにするSELECTポリシーが必要です。
サーバー専用テーブルには、クライアント向けポリシーを無理に作らないでください。代わりに、Data APIで使われるロールから権限を取り消します。たとえばrevoke all on table public.profiles from anon, authenticated;を使います。アプリケーションが必要なテーブルについては、必要な操作のみを明示的に許可してください。INSERTがシーケンスを直接使用する場合、シーケンスに必要な最小限の権限だけを別途付与してください。ログイン前から利用可能なデータについては、専用のanonポリシーを作成し、パブリックな行とカラムを独立してレビューしてください。
テーブルエディタで作成されたテーブルはRLSがデフォルトで有効になっています。SQLを直接実行して作成したテーブルでは、RLSを手動で有効にする必要があります。UIがRLSを有効にした後に空になる場合、行が削除された可能性は低く、多くの場合、適用可能なポリシーまたはGRANTが存在しないのでリクエストが拒否されています。
3. 旧キーを無効にする前に、すべての利用箇所を移行
ブラウザはpublishableキー(sb_publishable_...)を使用すべきです。既存プロジェクトのanonキーも公開用に意図されていますが、新しいキー体系への移行が推奨されます。secretキー(sb_secret_...)と旧service_roleはRLSをバイパスするため、クライアントに残してはいけません。
新しいキーを作成しても古いキーは取り消されません。次の順番で移行します。
- ブラウザ、モバイル、デスクトップ、既にデプロイされたクライアントで
anonをpublishableキーに置き換えてください。 - サーバー、Edge Functions、ワーカー、CI/CD、cronジョブ、外部統合、データベースWebhooks、および
pg_netでservice_roleをsecretキーに置き換えてください。 - 新しいsecretキーを
Authorization: Bearerではなくapikeyヘッダーで送る必要がある連携先がないか確認してください。 - まだ古い資格情報を使っているリクエストがないことを確認し、
Settings → API Keysの下で古いanonとservice_roleキーを無効にしてください。
もしsb_secret_...の値が漏洩した場合、代替キーを作成し、サーバーを更新し、古いキーを失効してください。旧service_roleが露出した場合も同様です。一つのコードパスだけ直して終わらせないでください。マイグレーションを完了し、旧資格情報を無効にしてください。古いアプリ、定期実行ジョブ、Webhookは切り替え時に動かなくなる可能性があるため、キーを利用している箇所を一覧にしておきましょう。
4. 実際のロールと2つのユーザーでテスト
SQLエディタで、select auth.uid();は通常、ユーザーJWTがないためnullを返します。SET LOCALは現在のトランザクションにのみ有効なので、BEGINとROLLBACKの間に配置してください。次のコードにある山括弧内の文字列は、テスト対象ユーザーのUUIDに置き換えます。
begin;
set local role authenticated;
set local request.jwt.claim.sub = '<UUID of the user under test>';
select * from public.profiles;
rollback;
このSQLはポリシーの簡単なチェックにすぎません。アプリケーションでは、サインアウト状態、ユーザーA、ユーザーBのそれぞれでselect、insert、update、deleteを実行してください。ユーザーAがユーザーBのIDを直接送っても、ユーザーBのデータへのアクセスが拒否されること、制限対象のカラムがレスポンスに含まれないことを確認してください。
ポリシー、必要なGRANT権限、アプリケーションのデプロイを、一度の短時間の本番変更としてまとめて実施してください。危険なほど広いポリシーが見つかったら、利便性を理由に放置しないでください。優先度を上げ、ステージングで修正を検証し、アクセスを閉じる前にロールバック用SQLを用意します。