ソフトウェア開発パートナー SaaS Go-Live セキュリティ

PostgreSQL行レベルセキュリティ(RLS)— マルチテナントSaaSにおけるテナントデータ分離

2026年09月02日 2 分で読めます 72 ビュー

PostgreSQL行レベルセキュリティ(RLS)— マルチテナントSaaSにおけるテナントデータ分離

要約: 行レベルセキュリティ(RLS)は、各クエリがどの行を読み書きできるかを、実行者の識別情報に基づいてデータベース自身が判断するPostgreSQLの機能です。マルチテナントSaaSにおいては、テナント分離をアプリケーションコードからデータベースエンジンへ移すことで、WHERE句の書き忘れが情報漏洩に直結しない構造をつくります。

目次

なぜWHERE句の書き忘れがマルチテナントSaaS最大のリスクなのか

マルチテナントSaaSアプリケーションには、例外なく同じ潜在的欠陥があります。コードベースのどこかに、AND tenant_id = $1 を書き忘れたクエリが存在するという欠陥です。

金曜の夕方に追加したエンドポイントかもしれません。リクエストコンテキストの外側で動くバックグラウンドジョブ、CSVエクスポート、検索インデックスの再構築、あるいは誤ったリポジトリメソッドを呼んだORMの一行かもしれません。

アプリケーションには数百のクエリ経路があります。テナント分離は現在、そのすべてが未来永劫にわたって正しいことによってのみ担保されています。まだ書かれていないコードも含めて、です。それはセキュリティモデルではなく、単なる願望にすぎません。

このリスクは均等に分布しません。システムが負荷を受けている時間帯と、レビューが最も手薄なコード経路に集中します。日本のお客様にとって、テナント間の情報漏洩は報告義務を伴うインシデントであり、B2B SaaSプロダクトの商業的な前提そのものを失わせます。

本記事では、分離の保証を開発者の規律からデータベース層へ移す方法と、その仕組みを静かに無効化してしまう落とし穴をご説明します。これは、当社の日本企業向けシステム開発サービスにおいて実際に採用している設計判断です。

行レベルセキュリティ(RLS)とは

行レベルセキュリティ(Row-Level Security、RLS)とは、クエリを実行するユーザーの識別情報や権限に基づいて、データの読み取り・書き込みを行単位で制限するPostgreSQLの機能です。

既定では、テーブルに SELECT 権限を持つロールはそのテーブルの全行を読み取れます。RLSを有効化すると、PostgreSQLはそのテーブルに触れるすべてのクエリにポリシー式を付加し、行ごとに評価します。

公式ドキュメントは評価順序を明確に規定しています。ポリシー式は、ユーザーのクエリに由来するあらゆる条件や関数よりも前に、各行に対して評価されます(PostgreSQL Documentation §5.9、2026年)。

この順序が本質です。ポリシーはアプリケーションが省略できる推奨事項ではなく、アプリケーション自身の WHERE 句より前に適用されます。したがって、テナントフィルタを書き忘れた開発者であっても、取得できるのは自テナントの行だけです。

ポリシーには2つの側面があり、いずれも重要です。

構成要素 制御する対象 適用されるコマンド
USING 既存のどの行が見えるか SELECTUPDATEDELETE
WITH CHECK 新規・変更後のどの行が許可されるか INSERTUPDATE

WITH CHECK を欠くと、テナントは自分の行しか読めない一方で、他社の tenant_id を持つ行を書き込めてしまいます。これは逆方向の分離破綻であり、検知は読み取り側の漏洩よりはるかに困難です。

RLSは、次の3つの統制としばしば混同されます。

統制 適用箇所 防げるもの 防げないもの
RLS データベースエンジン テナント条件が欠けたクエリ キャッシュ・ジョブ・ストレージ側の不備
アプリ側フィルタ アプリケーションコード 全クエリが正しい限り有効 見落とされたコード経路
GRANT / テーブル権限 データベースエンジン テーブル単位のアクセス 行の所有者による区別ができない
保存時暗号化 ストレージ層 ファイル・バックアップの直接読取 妥当だが対象テナントを誤ったクエリ

テナント分離を検証する5層フレームワーク

RLSが塞ぐのは特定の障害クラスであり、すべてではありません。VAONがマルチテナント構成のアーキテクチャ監査を行う際は、以下の5層を順に確認します。この順序は障害の重大度ではなく、実際に不備が見つかる頻度を反映しています。

確認事項 必要な根拠
1. スキーマ テナントデータを持つ全テーブルに ENABLE および FORCE ROW LEVEL SECURITY が設定されているか pg_class への問い合わせ結果が0件であること
2. 接続 アプリケーションが所有者でもスーパーユーザーでもないロールで接続しているか \du の出力と実際の接続文字列
3. セッション テナントコンテキストはトランザクション単位か、セッション単位か コンテキスト設定コードとプーラー設定
4. クエリ インデックスの先頭列は tenant_id か。ポリシーフィルタは早い段階で適用されているか EXPLAIN (ANALYZE, BUFFERS)
5. SQL の外側 キャッシュ・ジョブ・検索インデックス・ストレージのキーにテナントが含まれているか 推測ではなく構成要素ごとの実地確認

最初の3層は、RLSがそもそも機能するかどうかを決めます。第4層は負荷に耐えられるかを決めます。第5層はRLSの管轄外であり、個別の統制が必要です。

特筆すべき点があります。当社が確認してきた範囲では、不備は第1層ではなく第2層・第3層に集中します。ポリシー自体は正しく書かれているにもかかわらず、それが一度も強制されていないという状態です。

RLS導入の6ステップ

現在のテナントを識別する方法は2つあります。テナントごとにPostgreSQLロールを割り当てる方法と、テナント識別子を実行時のセッション変数として渡す方法です。AWSは、前者がテナントごとに新しいPostgreSQLユーザーの作成を必要とするため、後者を推奨すると明記しています(AWS Prescriptive Guidance、2026年)。

ステップ1 — 識別子列の追加。 テナントデータを保持するすべてのテーブルに tenant_id NOT NULL を持たせます。成果物は、スキーマ全体で一貫した列構成です。

ステップ2 — 接続ロールの分離。 SELECT/INSERT/UPDATE/DELETE のみを持つアプリケーション専用ロールを作成し、マイグレーションは別の特権ロールで実行します。最も省略されやすく、最も重要な手順です。

ステップ3 — FORCE 付きでRLSを有効化。

ALTER TABLE invoice ENABLE ROW LEVEL SECURITY;
ALTER TABLE invoice FORCE  ROW LEVEL SECURITY;

ステップ4 — 両方の側面を持つポリシーを定義。

CREATE POLICY tenant_isolation ON invoice
    FOR ALL
    TO app_user
    USING      (tenant_id = current_setting('app.current_tenant', true)::UUID)
    WITH CHECK (tenant_id = current_setting('app.current_tenant', true)::UUID);

current_setting の第2引数 true は、コンテキスト未設定時にエラーを送出せずNULLを返すことを意味します。結果として、そのクエリはどの行にも一致しません。原則は fail open ではなく fail closed です。

ステップ5 — コンテキストをトランザクション単位で設定。

BEGIN;
SELECT set_config('app.current_tenant', $1, true);  -- true = トランザクションスコープ
SELECT * FROM invoice ORDER BY issued_at DESC LIMIT 50;
COMMIT;

テナント識別子は、サーバー側で検証済みのセッションまたはJWTから取得します。クライアントが操作可能なリクエストパラメータから取得してはいけません。

ステップ6 — ポリシーに合わせたインデックス設計。 テナントテーブルのインデックスの先頭列は必ず tenant_id とします。RLSがすべてのクエリに追加する述語がそれだからです。

CREATE INDEX invoice_tenant_issued_idx ON invoice (tenant_id, issued_at DESC);

本番環境で得られた知見

VAONは、20万人を超える利用者を擁する建設現場管理システムを、pool モデル(tenant_id で区別する共有テーブル方式)で構築し、スキーマ設計の段階からRLSを組み込みました。実際に直面した3つの問題を、検知の難しさの順にご紹介します。

アプリケーションがテーブル所有者ロールで接続していた。 ポリシー自体は正しく記述されており、権限を制限したユーザーによる手動テストも通過していました。しかし実際の実行環境ではアプリケーションが所有者ロールを使用していたため、ポリシーは一切強制されていませんでした。エラーは何も発生せず、クエリが本来より多くの行を返すだけです。対処は FORCE ROW LEVEL SECURITY とロール分離、そして検知手段はコードレビューではなく自動テストです。

コネクションプーラー経由でテナントコンテキストが漏洩した。 素の SET でコンテキストを設定したため、値がトランザクションを越えて接続上に残りました。トランザクションモードで動作するプーラーは、その接続を直ちに別のリクエストへ引き渡します。症状は同時実行時にのみ現れ、ステージング環境での再現はほぼ不可能でした。対処は SET LOCAL または set_config(..., true) です。

インデックスの先頭列が誤っていたためクエリが低速化した。 RLS有効化後、インデックスが tenant_id で始まっていなかった一部のクエリが劣化しました。プランナが行を取得してからテナントで絞り込むためです。診断は EXPLAIN (ANALYZE, BUFFERS) に現れます。ポリシーフィルタが遅い位置で適用されている状態です。

いずれもRLS自体の欠陥ではありません。RLSを取り巻く環境側の問題であり、前掲の5層フレームワークが存在する理由がまさにここにあります。こうした判断を上流工程へ移す当社の進め方は、VAONの働き方にてご紹介しています。

RLSを静かに無効化する5つの誤り

RLSは静かに失敗します。設定を誤ったとき、エラーは返りません。返るのは結果です。多すぎる行を返すクエリは、正常に動作したクエリと見分けがつきません。

1. FORCE ROW LEVEL SECURITY の付け忘れ。 PostgreSQLのドキュメントは、スーパーユーザーおよび BYPASSRLS 属性を持つロールが常に行セキュリティを回避すること、テーブル所有者も ALTER TABLE ... FORCE ROW LEVEL SECURITY を用いない限り通常は回避することを明記しています。結果として、ポリシーは存在するのに実行されません。対策は、全テナントテーブルへの FORCE 付与と、アプリケーションロールの所有者ロールからの分離です。

2. コネクションプーラー配下で SET LOCAL ではなく SET を使う。 結果として、あるテナントが別のテナントのデータを、非決定的に、負荷時にのみ読み取ります。対策はトランザクションスコープの徹底です。

3. ビューが所有者権限で実行される。 従来の仕様では、ビューはビュー所有者の権限で実行されるため、特権ロールが所有するビューはポリシーを貫通する経路になります。対策はPostgreSQL 15以降で、テナントテーブル上の全ビューに WITH (security_invoker = true) を指定することです。

4. 一意制約をグローバルに定義する。 ドキュメントは、参照整合性チェックが常に行セキュリティを回避することを明記し、「covert channel(隠れチャネル)」による漏洩に注意を促しています。具体的には、email にグローバルな UNIQUE があると、重複エラーを受け取ったテナントは他社がそのアドレスを使用している事実を知ることになります。対策は UNIQUE (tenant_id, email) です。

5. クエリプランナとの相互作用を軽視する。 セキュリティチェックより前に並べ替えられるのは LEAKPROOF とマークされた関数のみです。leakproofでない関数はプッシュダウンできず、プランは遅延フィルタ付きのシーケンシャルスキャンへ劣化します。Supabaseは、ポリシーが参照する列へのインデックス付与により大規模テーブルで100倍を超える改善が得られたこと、およびポリシー内の関数呼び出しをスカラーサブクエリで包むことである試験ケースが178,000msから12msへ短縮されたことを記録しています(Supabase Docs、2026年)。

パッチ運用も統制の一部です

RLSはデータベースエンジンのコードであり、エンジンのコードにはCVEが発生します。CVE-2024-10976(CVSS 4.2)は、ユーザーIDが変化した際に再利用されたクエリプランが誤ったポリシーを適用し得る問題で、17.1・16.5・15.9・14.14・13.17・12.21で修正されました(PostgreSQL Security、2024年)。CVE-2025-8713(CVSS 3.1)は、ポリシーが隠すはずの行のサンプルデータがオプティマイザ統計を通じて露出し得る問題で、17.6・16.10・15.14・14.19・13.22で修正され、2025年8月14日にリリースされました(PostgreSQL Security、2025年)。

いずれも容易に悪用できるものではありません。重要なのは深刻度ではなく、「RLSを有効化しています」という説明にはバージョン番号が伴うという点です。

RLSを採用すべきでない場合

RLSがあらゆる場面で最適とは限りません。他の方式をご検討いただくべき状況が4つあります。

物理的な分離が要件として求められる場合。 金融・医療分野を中心に、データを独立したデータベースまたはリージョンに配置することを契約条件とされるお客様がいらっしゃいます。RLSはあくまで論理的な分離です。この場合は、運用コストが高くなるとしても silo モデルが正しい答えです。

テナント数が少なく、各テナントの規模が大きい場合。 5〜10社程度で各社のデータ量が大きい構成では、テナントごとにデータベースを分ける方が単純です。バックアップと個別リストアが容易で、監査時の説明もしやすくなります。

PostgreSQL以外を利用している場合。 RLSはPostgreSQLの機能です。同等の仕組みを持たないエンジンをご利用の場合、単一機能を理由にデータベースを選定するのではなく、別の層でより厳格な統制を設けて分離を実現すべきです。

パッチ適用のプロセスが確立していない場合。 RLSはセキュリティの保証をエンジン側へ移す仕組みです。マイナーバージョンを定期的に適用する運用がなければ、リスクを低減しているのではなく移転しているにすぎません。導入前に運用体制の評価が必要な場合は、VAONのアーキテクチャ監査からご検討ください。

明確に申し添えます。現時点でVAONはISO 27001認証を取得しておりません。当社の情報セキュリティ基準はISO 27001のフレームワークに沿って整備しており、認証取得はロードマップ上にあります。ベンダー評価の過程でお客様が知ることになる前に、こちらから率直にお伝えしています。

よくあるご質問

RLSはクエリを遅くしますか。 従来のアクセスパターンのままインデックスを設計している場合に限ります。ポリシーはすべてのクエリに tenant_id を述語として追加するため、複合インデックスの先頭列は tenant_id である必要があります。正しく設計すればオーバーヘッドは無視できる水準です。誤れば遅延フィルタ付きのシーケンシャルスキャンとなり、EXPLAIN (ANALYZE, BUFFERS) で即座に判明します。

アプリケーション側で既にテナントを絞り込んでいます。RLSは冗長ではありませんか。 冗長ではありません。RLSは、まだ書かれていないクエリにも効く統制です。アプリケーション側の絞り込みは、いずれか一つのコード経路が誤るまでは正しく機能しますが、拡大し続けるコードベース全体でその不在を証明することはできません。RLSであればシステムカタログから証明できます。

稼働中の本番データベースに後からRLSを導入できますか。 テーブル単位で可能です。まずシャドー環境で有効化し、分離テストスイートを実行したうえで段階的に展開します。最もリスクが高いのはポリシーそのものではなく、アプリケーションがテーブル所有者ロールで接続しており、これまで一切強制されていなかったと判明することです。

RLSは監査やペネトレーションテストの代替になりますか。 なりません。RLSが塞ぐのはテナント述語の欠落という最大の障害クラスです。キャッシュ、バックグラウンドジョブ、検索インデックス、オブジェクトストレージのパスはその範囲外であり、個別の統制が必要です。

どのPostgreSQLバージョンが必要ですか。 実務上は15以降です。ビュー向けの security_invoker オプションが15からの機能であるためです。どのメジャーバージョンであっても、パッチ適用済みのマイナーリリースを維持してください。

現行システムに漏れているテーブルがないか、どう確認しますか。 pg_classpg_policy を結合し、relrowsecurity および relforcerowsecurity で絞り込みます。テナントデータを保持していながら rls_enabled = false のテーブルは、トラフィックを待っているだけのインシデントです。

まとめ

  • RLSはテナント分離の保証を開発者の規律からデータベース層へ移し、アプリケーションのあらゆるクエリより前に適用します。
  • 最も頻出する3つの不備はポリシーそのものではなく、その周辺にあります。接続ロール、セッションのスコープ、インデックスの先頭列です。
  • 分離はすべての層で成立すべき性質です。RLSは床であって、天井ではありません。

マルチテナントのスキーマを設計中の方、あるいは現行のポリシーが本番環境で実際に強制されているか確信を持てない方は、VAONの無料アーキテクチャ相談をご利用ください。本記事の5層フレームワークに沿って確認し、検証用クエリとあわせて結果をご報告いたします。


ビジネス変革の準備はできていますか?

AIとデジタル変革の活用について、ぜひご相談ください。

記事をシェア