PostgreSQLの行レベルセキュリティ(RLS)による堅牢なマルチテナントデータ分離の実現
Daniel Hayes
Full-Stack Engineer · Leapcell

はじめに
現代のソフトウェア開発、特にSaaSやクラウドベースのシステムの世界では、マルチテナンシーがアーキテクチャの基盤パターンとなっています。これにより、単一のアプリケーションインスタンスで複数の顧客(テナント)にサービスを提供でき、大幅なコスト削減と管理の簡素化につながります。しかし、この効率性には重大な課題が伴います。それは、テナント間の絶対的なデータ分離を保証することです。この分離が破られると、プライバシー侵害、セキュリティインシデント、深刻な風評被害につながる可能性があります。従来、開発者は認証されたテナントに基づいてデータをフィルタリングするために、アプリケーションレベルのロジックに大きく依存してきました。これはある程度有効ですが、このアプローチはアプリケーションにすべての負担をかけ、複雑さ、エラーの可能性を高め、潜在的な単一障害点となります。この記事では、PostgreSQLの行レベルセキュリティ(RLS)が、マルチテナントデータ分離の根本的な問題に対処するための強力でデータベースネイティブなメカニズムをどのように提供し、より堅牢で安全なソリューションを提供するかに迫ります。
基本の理解
RLSを詳しく掘り下げる前に、いくつかのコアコンセプトを明確に理解しておきましょう。
- マルチテナンシー: 単一のソフトウェアアプリケーションインスタンスが複数のテナント(顧客またはグループ)にサービスを提供するアーキテクチャ。各テナントのデータは他のテナントから分離されていますが、すべてのテナントが同じアプリケーションインスタンスとデータベーススキーマを共有します。
 - データ分離: あるテナントに属するデータが、他のテナントからアクセス不可能であり、見えないことを保証する原則。これはセキュリティとプライバシーにとって最重要です。
 - 行レベルセキュリティ(RLS): クエリを実行するユーザーの特性に基づいて、テーブル全体ではなく個々のデータ行へのアクセスを制限するデータベース機能。このきめ細かな制御は、データベースシステムによって直接強制されます。
 - ポリシー: RLSの文脈では、ポリシーとは、ユーザーがアクセスまたは変更できる行を決定する、テーブルに定義されたルールのセットです。ポリシーは、
SELECT、INSERT、UPDATE、およびDELETE操作に適用できます。 
PostgreSQL行レベルセキュリティの力
PostgreSQLのRLSは、ポリシーをテーブルに直接アタッチすることによって機能します。これらのポリシーは、アクセスまたは変更が試みられた各行に対して条件を評価します。条件がtrueと評価された場合、操作は許可されます。それ以外の場合は拒否されます。この強制は、アプリケーションにクエリ結果が返される前に行われるため、バイパス不可能なセキュリティレイヤーを提供します。
マルチテナント分離の中心的アイデアは、関連テーブルのtenant_id列に基づいて行をフィルタリングすることです。現在のテナントのIDをRLSポリシーに統合することで、データベース自体が、その特定のテナントに属する行のみが表示または変更可能であることを保証します。
仕組み:ステップバイステップの実装
実際的な例で示しましょう。各製品が特定のテナントに属する、マルチテナントのproductsテーブルを想像してみてください。
まず、データベースが現在アクティブなテナントを知る方法が必要です。PostgreSQLのSET SESSION AUTHORIZATION、またはマルチテナンシーではより一般的に、SET LOCAL変数がこれに最適です。app.current_tenant_idというカスタムセッション変数を使用します。
-- 1. tenant_idを持つ`products`テーブルを作成します CREATE TABLE products ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, price DECIMAL(10, 2) NOT NULL, tenant_id INT NOT NULL ); -- 異なるテナントのサンプルデータを挿入します INSERT INTO products (name, price, tenant_id) VALUES ('Laptop A', 1200.00, 1), ('Mouse B', 25.00, 1), ('Keyboard C', 75.00, 2), ('Monitor D', 300.00, 1), ('Webcam E', 50.00, 2); -- 2. テーブルで行レベルセキュリティを有効にします ALTER TABLE products ENABLE ROW LEVEL SECURITY; -- 3. tenant_idに基づいてアクセスを制限するポリシーを作成します -- このポリシーは、ユーザーが現在のtenant_idに属する製品のみを表示/変更できるようにします。 CREATE POLICY tenant_isolation_policy ON products FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::int); -- 新しい製品が正しくタグ付けされていることを確認するために、挿入用のポリシーも必要になる場合があります -- または、上記の'USING'句は、'FOR ALL'が使用されている場合、挿入にも適用されます。 -- INSERT、UPDATE、DELETEに対してより厳密な制御または異なるロジックが必要な場合は、個別のポリシーを作成できます。 -- CREATE POLICY insert_tenant_policy ON products FOR INSERT WITH CHECK (tenant_id = current_setting('app.current_tenant_id')::int); -- CREATE POLICY update_tenant_policy ON products FOR UPDATE USING (tenant_id = current_setting('app.current_tenant_id')::int) WITH CHECK (tenant_id = current_setting('app.current_tenant_id')::int);
ここで、これがどのように機能するかを見てみましょう。アプリケーションがデータベースに接続し、ユーザーをテナント1の一部として認証すると、 any データクエリの前に次のコマンドを発行します。
SET app.current_tenant_id = '1';
その後、アプリケーションによるproductsテーブルへの後続のクエリは、RLSによって自動的にフィルタリングされます。
-- テナント1のアプリケーションクエリ SELECT * FROM products;
app.current_tenant_id = '1'に対する期待される出力:
| id | name | price | tenant_id | 
|---|---|---|---|
| 1 | Laptop A | 1200.00 | 1 | 
| 2 | Mouse B | 25.00 | 1 | 
| 4 | Monitor D | 300.00 | 1 | 
アプリケーションが、テナント2のユーザーを認証した後、コンテキストをテナント2に切り替えると:
SET app.current_tenant_id = '2'; -- テナント2のアプリケーションクエリ SELECT * FROM products;
app.current_tenant_id = '2'に対する期待される出力:
| id | name | price | tenant_id | 
|---|---|---|---|
| 3 | Keyboard C | 75.00 | 2 | 
| 5 | Webcam E | 50.00 | 2 | 
SELECTクエリ自体は汎用的なものであることに注意してください。フィルタリングは完全にRLSに委任され、データベースによって強制されます。これにより、すべてのアプリケーションクエリでWHERE tenant_id = <current_tenant_id>句を記述する必要がなくなります。
堅牢性と利点
- 絶対的なデータ分離: RLSは最終的なゲートキーパーとして機能します。アプリケーションコードに
WHERE tenant_id = Xを忘れるバグがあったとしても、データベースはポリシーを強制し、データ漏洩を防ぎます。 - アプリケーションの複雑性の軽減: 開発者はテナントフィルタリングのためのボイラープレートコードを少なく記述でき、ビジネスロジックに集中できるようになります。
 - セキュリティの向上: セキュリティの懸念をデータベースレイヤーにプッシュすることで、SQLインジェクションやその他の脆弱性を通じて攻撃者がテナント分離をバイパスすることがより困難になります。
 - 集中管理: セキュリティポリシーはデータベースレベルで定義および管理され、データにアクセスするすべてのアプリケーション部分とマイクロサービス間で一貫性を保証します。
 - パフォーマンス: PostgreSQLのクエリプランナーはRLSポリシーを認識しており、クエリ実行を最適化できます。多くの場合、
tenant_idの効率的なインデックス利用につながります。 
高度な考慮事項
- RLSのバイパス(スーパーユーザー/管理者向け): PostgreSQLのスーパーユーザーまたは
BYPASSRLS権限を持つロールは、ポリシーをバイパスできます。これはメンテナンス、バックアップ、管理タスクに不可欠ですが、細心の注意を払って(と厳重な注意を払って)使用する必要があります。 - コマンドごとのポリシー: RLSでは、
SELECT、INSERT、UPDATE、DELETE操作に対して個別のポリシーを定義でき、データ操作に対するきめ細かな制御を提供します。WITH CHECK句は、新しい行または変更された行がポリシーに準拠していることを保証するために、INSERTおよびUPDATEポリシーで特に役立ちます(例:ユーザーは別のテナントの行を挿入できません)。 - 複雑なテナント階層: RLSは、テナントに親子の関係がある、またはデータが共有される、より複雑なシナリオを処理できます。ポリシーは、複雑なアクセスルールを実装するために、関数やサブクエリを組み込むことができます。
 
結論
PostgreSQLの行レベルセキュリティは、マルチテナントデータ分離のためのエレガントで強力なソリューションを提供します。テナントレベルのデータフィルタリングの負担をアプリケーションレイヤーからデータベースコアに移行することで、RLSはセキュリティを劇的に向上させ、アプリケーションの複雑さを軽減し、揺るぎない手でデータ境界を強制します。PostgreSQL上に構築されたマルチテナントアプリケーションにとって、RLSを採用することは単なるベストプラクティスではありません。真に安全で保守可能なアーキテクチャへの基本的な一歩です。データベースがテナントデータの分離の最終的な守護者となることを可能にします。