B2B SaaS開発において、共有データベース(Shared Schema)運用はコスト効率に優れますが、常に「他テナントのデータ漏洩」という致命的なリスクが伴います。アプリケーションの実装ミスで1つのWHERE句を忘れただけで、全顧客の機密情報が流出する恐れがあります。
PostgreSQLのRow-Level Security(RLS)を導入すれば、アプリケーション層のロジックに依存せず、データベース層で物理に近い論理分離を強制できます。本記事では、実務に即したRLSの実装方法と運用上の注意点を解説します。
TL;DR — PostgreSQL RLSは、テーブル内の各行に対してアクセス権限を定義する機能。アプリケーション側でテナントIDをセッション変数にセットするだけで、意図しないデータ取得をDBエンジンが自動的に遮断します。
1. PostgreSQL RLSとは
💡 イメージで理解する: ホテルの「マスターキー」と「各部屋のカードキー」の違いです。アプリの管理者権限がマスターキーだとすれば、RLSは各部屋の住人が自分の部屋の鍵しか使えないように「扉そのものに制限をかける」仕組みです。
PostgreSQL Row-Level Security(RLS)は、PostgreSQL 9.5から導入されたセキュリティ機能です。通常、SELECTやUPDATEの権限は「テーブル単位」で設定しますが、RLSは「行単位」で評価を行います。PostgreSQL 16/17環境でも、この機能はマルチテナント設計の標準的な選択肢となっています。
従来、データ分離はアプリケーション側のクエリ(WHERE tenant_id = ?)に依存していました。しかし、開発者のミスやORMの挙動によってフィルタが漏れるリスクが常にありました。RLSは、DBユーザーやセッション変数に基づき、DB内部で自動的にフィルタリングを適用します。
2. 実務でRLSが必要な理由
B2B SaaSにおいて、他顧客のデータが見えてしまう「情報漏洩」はサービスの即終了を意味します。特に金融、医療、製造業向けのSaaSでは、論理的な分離が契約上の必須要件となるケースが増えています。RLSは、これらの厳しいコンプライアンス要件(GDPRやISMSなど)を満たす強力な根拠となります。
また、コードベースが巨大化すると、全てのクエリにテナントIDの条件を付与するのは現実的ではありません。ライブラリのアップデートやリファクタリングの際にミスが発生しやすくなります。RLSを導入することで、エンジニアは「データ分離」を意識せずにビジネスロジックの開発に集中できるメリットがあります。
3. ステップ別実装ガイド
PostgreSQLでランタイム時にテナントを切り替える、標準的な実装フローを説明します。
ステップ 1. テナント管理用テーブルの作成
まず、データを保持するテーブルに tenant_id カラムを追加し、RLSを有効化します。
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
tenant_id UUID NOT NULL,
title TEXT,
content TEXT
);
-- RLSを有効にする
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
ステップ 2. セパレーションポリシーの定義
セッション変数 app.current_tenant を参照してアクセスを制限するポリシーを作成します。USING句が真を返す行のみがアクセス対象となります。
CREATE POLICY tenant_isolation_policy ON documents
USING (tenant_id = current_setting('app.current_tenant')::UUID);
-- テーブル作成者以外(アプリケーションユーザー)に強制適用
ALTER TABLE documents FORCE ROW LEVEL SECURITY;
ステップ 3. アプリケーション層での接続制御
アプリケーションからDBに接続する際、トランザクション開始直後に現在の tenant_id をセットします。これにより、以降のクエリは自動的にフィルタリングされます。
BEGIN;
-- セッションにテナントIDをセット
SET LOCAL app.current_tenant = '550e8400-e29b-41d4-a716-446655440000';
-- WHERE句にtenant_idを指定しなくても、自動的にフィルタリングされる
SELECT * FROM documents;
COMMIT;
4. RLS vs スキーマ分離 vs 別DB
マルチテナントの分離手法にはいくつか選択肢があります。それぞれの特性を比較します。
| 基準 | 共有スキーマ + RLS | スキーマ分離 | 物理DB分離 |
|---|---|---|---|
| コスト | 低い(安価) | 中 | 高い(管理増) |
| 分離レベル | 論理的(強) | 論理的(中) | 物理的(最強) |
| 運用の容易さ | 非常に高い | 低い(Migration困難) | 非常に低い |
| 適合規模 | 全規模 | 100テナント以下 | 超大規模エンタープライズ |
数千、数万のテナントを管理するSaaSであれば、インフラ管理コストの観点から「共有スキーマ + RLS」が最も現実的な解です。
5. 導入時の注意点とハマりどころ
⚠️ よくあるミス: スーパユーザーやテーブル所有者にはRLSがデフォルトで適用されません。テスト時に管理者アカウントを使うと、全データが見えてしまうため「設定が効いていない」と誤解することがあります。
RLSを確実に動作させるには、アプリケーションが使用するロールから BYPASSRLS 権限を剥奪し、前述の FORCE ROW LEVEL SECURITY を設定する必要があります。
エラー別の対処法
-- エラー例: unrecognized configuration parameter "app.current_tenant"
-- 原因: セッション変数が初期化されていない
-- 対策: 接続プールの開始時またはデフォルト値として空のUUIDをセットする
SET app.current_tenant = '00000000-0000-0000-0000-000000000000';
6. 実践的なパフォーマンス最適化
RLSを導入すると、PostgreSQLは各クエリに対してポリシー条件を暗黙的に追加します。これにより、インデックスが適切に貼られていないとパフォーマンスが大幅に低下します。
インデックスの設計: tenant_id を含む複合インデックスを必ず作成してください。単一インデックスよりも、(tenant_id, created_at) のような組み合わせが、実際の検索シナリオでは約30%高速化する傾向にあります。
📌 まとめ
- RLSはDB層でテナント分離を強制する、SaaS開発の守護神。
SET LOCALでセッション変数を使い回すのがクリーンな実装。- パフォーマンス維持のため、
tenant_idを含む複合インデックスを徹底する。
よくある質問
Q. RLSを使うとパフォーマンスは低下しますか?
A. 適切にインデックスが貼られていれば、オーバーヘッドは無視できるレベルです。
Q. 外部キー制約はRLSを越えてチェックされますか?
A. はい、RLSは表示制限を行いますが、制約の整合性チェックには影響しません。
Q. 複数のポリシーを設定した場合、どう評価されますか?
A. デフォルトでは「OR」条件として結合されます。全てを満たす必要がある場合は注意が必要です。
Post a Comment