0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

なぜPostgreSQL の Row Level Security (RLS) を使わないのか

Last updated at Posted at 2025-12-02

〜 Next.js + Supabase アーキテクチャにおける現実解 〜


はじめに

PostgreSQL の Row Level Security (RLS) は、データベースレベルでアクセス制御を実現する強力な機能です。Supabase を使った開発では「RLS を有効化すべし」という声をよく耳にします。

しかし、私たちの SaaS プロダクト「Founders Direct Cockpit(FDC)」では、あえて RLS を使用していません

本記事では、その理由と、RLS に代わるセキュリティアーキテクチャについて解説します。


TL;DR

観点 RLS あり RLS なし(我々の選択)
防御レイヤー DB 層 + アプリ層 アプリ層のみ
複雑さ 二重管理が必要 シンプル
デバッグ 切り分け困難 明確
開発速度 やや遅い 速い
適用範囲 全アクセス API 経由のみ

結論: クライアントから直接 Supabase にアクセスしないアーキテクチャでは、RLS は必須ではない。


アーキテクチャ

データアクセスフロー

┌──────────────────┐
│   Browser        │
│  (Next.js 15)    │
│   React 19       │
└────────┬─────────┘
         │
         │ API Call
         │ Cookie: fdc_session
         │ (HttpOnly, Secure, SameSite=Lax)
         │
         ▼
┌───────────────────────┐
│  Next.js API Routes   │  ← ここで認証・認可
│  (app/api/**/route.ts)│
└──────────┬────────────┘
           │
           │ SERVICE_ROLE_KEY
           │ (RLS バイパス)
           │
           ▼
┌──────────────────────┐
│  Supabase PostgreSQL │
│  (RLS は無効)        │
└──────────────────────┘

ポイント

  1. ブラウザから Supabase に直接アクセスしない

    • すべての DB 操作は Next.js API Routes 経由
    • ANON_KEY はクライアント認証のみに使用
  2. SERVICE_ROLE_KEY でアクセス

    • RLS を自動的にバイパス
    • サーバーサイドでのみ使用(クライアントに露出しない)
  3. 認証・認可は Next.js で実装

    • HttpOnly Cookie によるセッション管理
    • API Route 内でワークスペースメンバーシップ検証

RLS を使わない理由

1. アーキテクチャとの不一致

RLS は「クライアントから直接 DB にアクセスする」構成で真価を発揮します。

// Supabase の典型的な使い方(RLS が有効な場合)
const { data } = await supabase
  .from('todos')
  .select('*')
  .eq('user_id', userId);
// → RLS ポリシーが自動適用され、他ユーザーのデータは見えない

しかし、今回の構成では:

//FDCのアーキテクチャ
// 1. Browser → API Route(Cookie 認証)
// 2. API Route → Supabase(SERVICE_ROLE_KEY)

// api/workspaces/[workspaceId]/data/route.ts
export async function GET(request: NextRequest) {
  // 1. 認証チェック
  const user = await getSession(request);
  if (!user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // 2. 認可チェック(ワークスペースメンバーシップ)
  const membership = await getWorkspaceMembership(workspaceId, user.id);
  if (!membership) {
    return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
  }

  // 3. データ取得(SERVICE_ROLE_KEY でアクセス、RLS バイパス)
  const { data } = await supabase
    .from('workspace_data')
    .select('*')
    .eq('workspace_id', workspaceId);

  return NextResponse.json(data);
}

SERVICE_ROLE_KEY を使う限り、RLS は無意味です。
中途半端に RLS を ON にしても、バイパスされるだけ。

2. 二重管理の複雑さ

RLS を有効にすると、認可ロジックが二箇所に分散します:

レイヤー 責務 実装場所
アプリ層 ビジネスロジック認可 lib/server/auth.ts
DB 層 行レベル認可 migrations/rls-policies.sql

問題点

  1. 同期の維持が困難

    • アプリ層で「ADMIN 以上が編集可能」と実装
    • RLS で「OWNER のみ編集可能」と設定
    • → 不整合が発生しても気づきにくい
  2. デバッグの困難さ

    • 「データが取得できない」問題が発生した時
    • 原因が「RLS ポリシー」なのか「アプリの認可ロジック」なのか切り分けが難しい
  3. テストの複雑化

    • RLS ポリシーのテストは SQL レベルで行う必要がある
    • アプリ層のテストとは別に管理

3. 開発速度への影響

新しいテーブルを追加するたびに:

RLS あり:

  1. テーブル作成
  2. RLS ポリシー設計
  3. RLS ポリシー実装(SQL)
  4. RLS ポリシーテスト
  5. アプリ層の認可ロジック実装
  6. アプリ層のテスト

RLS なし:

  1. テーブル作成
  2. アプリ層の認可ロジック実装
  3. アプリ層のテスト

ステップが半減します。スタートアップにとって、この差は大きい。

4. Service Role Key の存在

Supabase を使う限り、SERVICE_ROLE_KEY は必要です:

  • 管理者機能(全ユーザー一覧の取得など)
  • バッチ処理
  • マイグレーション
  • データ移行

これらの処理では RLS をバイパスする必要があります。
結局、「RLS をバイパスできるキー」が存在する以上、
RLS は「唯一の防御線」にはなりえません。


RLS の代わりに何をしているか

多層防御アーキテクチャ

┌─────────────────────────────────────────────────────────────────┐
│                        セキュリティレイヤー                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. 認証(Authentication)                                      │
│     - HttpOnly Cookie によるセッション管理                       │
│     - Google OAuth 2.0 認証                                     │
│     - セッションキャッシュ(Vercel KV、TTL 5分)                 │
│                                                                 │
│  2. 認可(Authorization)                                       │
│     - ワークスペースメンバーシップ検証                           │
│     - ロールベースアクセス制御(OWNER/ADMIN/MEMBER)             │
│     - lib/utils/permissions.ts で権限チェック関数を集約         │
│                                                                 │
│  3. レート制限                                                  │
│     - Sliding Window Counter 方式                               │
│     - エンドポイント別制限(AI: 5req/min、通常: 60req/min)      │
│                                                                 │
│  4. 入力バリデーション                                          │
│     - Zod によるスキーマバリデーション                          │
│     - XSS / SQL インジェクション対策                            │
│                                                                 │
│  5. 暗号化                                                      │
│     - AES-256-GCM による機密データ暗号化                        │
│     - TLS 1.2+ による通信暗号化                                 │
│                                                                 │
│  6. 監査ログ                                                    │
│     - すべての重要操作を audit_logs テーブルに記録               │
│     - Pino 構造化ログ(機密情報自動マスキング)                  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

実装例

認可チェック関数の集約

// lib/utils/permissions.ts
export function canEdit(role: string): boolean {
  return ['OWNER', 'ADMIN', 'MEMBER'].includes(role);
}

export function canManageMembers(role: string): boolean {
  return ['OWNER', 'ADMIN'].includes(role);
}

export function canDeleteWorkspace(role: string): boolean {
  return role === 'OWNER';
}

API Route での認可実装

// app/api/workspaces/[workspaceId]/members/route.ts
export async function POST(request: NextRequest) {
  // 1. 認証
  const user = await getSession(request);
  if (!user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // 2. メンバーシップ検証
  const membership = await getWorkspaceMembership(workspaceId, user.id);
  if (!membership) {
    return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
  }

  // 3. ロール権限チェック
  if (!canManageMembers(membership.role)) {
    return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 });
  }

  // 4. 処理実行
  // ...
}

マルチテナント分離

// すべての業務リポジトリで tenant_id + workspace_id を必須パラメータとする
type TenantAwareParams = {
  tenantId: string;
  workspaceId: string;
  userId: string;
};

export async function listTodos({ tenantId, workspaceId, userId }: TenantAwareParams) {
  // 1. メンバーシップ検証
  await verifyMembership(workspaceId, userId);

  // 2. tenant_id + workspace_id でフィルタリング
  return db
    .selectFrom('todos')
    .where('tenant_id', '=', tenantId)
    .where('workspace_id', '=', workspaceId)
    .execute();
}

RLS を導入すべきタイミング

我々は「永久に RLS を使わない」とは言っていません。
以下の条件で RLS 導入を検討します:

1. Realtime Subscriptions の導入時

// クライアントから直接 Supabase Realtime を使う場合
const channel = supabase
  .channel('todos')
  .on('postgres_changes', { event: '*', schema: 'public', table: 'todos' }, (payload) => {
    // → RLS がないと、全ユーザーの変更が見えてしまう!
  })
  .subscribe();

2. anon key を使った直接クエリの導入時

// ブラウザから直接 Supabase にクエリする場合
const { data } = await supabase
  .from('todos')
  .select('*');
// → RLS がないと、全ユーザーのデータが取得可能!

3. 多層防御が監査要件で求められた時

外部監査で「DB レベルのアクセス制御」を要求された場合。


まとめ

観点 我々の判断
RLS の価値 認める(ただし、特定のアーキテクチャで)
現状の必要性 なし(SERVICE_ROLE_KEY 経由のため)
代替策 アプリ層での多層防御
将来の導入 トリガー条件を明確化して待機

RLS は「銀の弾丸」ではありません。

アーキテクチャに応じた適切なセキュリティ設計が重要です。
我々の場合、Next.js API Routes による BFF パターンが主軸であり、
RLS よりも「アプリ層での厳格な認可」の方が合理的でした。


補足:RLS を使うべきケース

以下のような構成では、RLS を積極的に使うべきです:

  1. クライアントから直接 Supabase にクエリする構成

    • モバイルアプリ(React Native + Supabase)
    • Supabase Realtime を活用するリアルタイムアプリ
  2. マルチテナント SaaS で Defense in Depth を重視する場合

    • 外部開発者がコードに触れる環境
    • セキュリティ監査で DB レベル制御を要求される場合
  3. サーバーレスで API 層がない場合

    • Supabase Edge Functions のみで構成
    • ブラウザから直接 DB にアクセスする構成

参考資料


執筆: Founders Direct 開発者
更新日: 2025-12-02
適用プロジェクト: Founders Direct Cockpit (FDC) Phase 14.6

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?