PostgreSQLで行レベルセキュリティを実現!RLSの基礎

はい、承知いたしました。PostgreSQLの行レベルセキュリティ(RLS)に関する詳細な記事を作成します。約5000語を目指し、基礎から応用、注意点まで網羅的に説明します。


PostgreSQLで行レベルセキュリティを実現!RLSの基礎から応用まで徹底解説

はじめに:データセキュリティの重要性と従来の限界

現代のアプリケーション開発において、データのセキュリティは最も重要な要素の一つです。特に、複数のユーザーや組織が同じデータベースを共有するようなシステム(マルチテナントアーキテクチャなど)や、機密性の高い個人情報を取り扱うシステムでは、誰がどのデータにアクセスできるかを厳密に制御する必要があります。

従来のデータベースにおけるアクセス制御は、主にロール(役割)やユーザーに対して、テーブルやビューといったオブジェクトレベルでの操作権限(SELECT, INSERT, UPDATE, DELETEなど)を付与したり剥奪したりすることで実現されてきました。これは GRANTREVOKE コマンドを使用して行われます。

“`sql
— 例: ‘app_user’ ロールに ‘users’ テーブルへのSELECT権限を与える
GRANT SELECT ON users TO app_user;

— 例: ‘admin_user’ ロールから ‘sensitive_data’ テーブルへのDELETE権限を剥奪する
REVOKE DELETE ON sensitive_data FROM admin_user;
“`

このオブジェクトレベルの権限管理は、テーブル全体やカラム全体へのアクセスを制御する上では効果的です。しかし、これだけでは不十分なシナリオが数多く存在します。例えば:

  • マルチテナントシステム: 各テナントは自分のデータのみにアクセスでき、他のテナントのデータには一切アクセスできないようにしたい。
  • 組織内のアクセス制御: 営業担当者は自分の担当顧客のデータのみを参照・更新でき、他の担当者の顧客データにはアクセスできないようにしたい。
  • 個人情報の保護: ユーザーは自分のプロフィール情報のみを更新でき、他のユーザーの情報を勝手に変更できないようにしたい。特定の管理者ロールのみが全ユーザーの情報を見られるようにしたい。
  • 承認ワークフロー: 特定のステータスにあるレコードは、承認権限を持つユーザーのみが更新できるようにしたい。

これらのシナリオに共通するのは、「テーブルやカラム全体ではなく、特定の行に対してアクセスを制御したい」という要件です。従来の GRANT/REVOKE だけでは、テーブル全体に SELECT 権限を与えると、そのユーザーはそのテーブル内のすべての行を参照できてしまいます。行ごとにアクセスを制限するには、アプリケーション側でクエリに WHERE 句を付けて絞り込むロジックを実装するか、データを行ごとに分割して複数のテーブルやスキーマに格納するといった回避策が必要でした。

アプリケーション側での WHERE 句による制御は、実装が煩雑になりがちで、SQLインジェクションなどのセキュリティリスクを高める可能性もあります。また、すべてのアプリケーションコードで厳密なフィルタリングが保証される必要があり、少しの漏れが情報漏洩につながる危険性があります。データを行ごとに物理的に分離する方法は、データベース設計が複雑になり、管理コストが増大します。

ここで登場するのが、データベースそのものが行レベルでのアクセス制御を行う機能です。PostgreSQLでは、この機能は行レベルセキュリティ(Row Level Security, RLS)と呼ばれています。

この記事では、PostgreSQLのRLSについて、その基本的な考え方から、有効化・無効化、ポリシーの作成方法、具体的な適用例、運用上の注意点、そして他のセキュリティ機能との連携まで、詳しく解説します。この記事を読めば、PostgreSQL RLSを使って、よりきめ細かく、より安全なデータアクセス制御を実現できるようになるでしょう。

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

PostgreSQLにおける行レベルセキュリティ(RLS)とは、データベースオブジェクト(テーブル)に対するアクセスを、接続しているユーザーやセッション変数、または行自体の内容に基づいて、個々のデータ行に対して制御する機能です。

RLSを有効にしたテーブルに対してクエリが実行される際、PostgreSQLは自動的に、定義されたセキュリティポリシーに従ってクエリを書き換えます。これにより、ユーザーは自分がアクセスを許可された行のみを参照、挿入、更新、削除できるようになります。ユーザーが意図的に、あるいは誤って許可されていない行にアクセスしようとしても、データベースがそれを阻止します。

なぜ行レベルの制御が必要か

前述の通り、オブジェクトレベルの権限だけでは不十分な多くのシナリオで、行レベルの制御は不可欠です。

  • マルチテナント: 1つのテーブルに複数のテナントのデータを混在させる場合、各テナントからのアクセスは自分のテナントIDを持つ行のみに制限する必要があります。RLSは、この「テナントIDが現在のセッションのテナントIDと一致する行のみにアクセスを許可する」というポリシーをデータベース側で強制できます。
  • 機密情報/個人情報: 特定のユーザー(例えば、その情報の所有者自身や特定の管理者)のみが、特定の行(例えば、自分の連絡先情報や給与情報)を閲覧・変更できるようなシステム。
  • 監査/コンプライアンス: 特定のロール(例: 監査担当者)のみが、全てのデータを参照できる一方で、他のロールは一部のデータしか見られないようにする、といった要件。

RLSは、これらの要件を満たすために、アプリケーションコードからデータアクセス制御ロジックをデータベース層に移管することを可能にします。これにより、アプリケーションコードはシンプルになり、セキュリティロジックが一元管理されるため、セキュリティホールが生まれにくくなります。

RLSと他のアクセス制御機能との違い

  • GRANT/REVOKE (オブジェクトレベル権限): テーブル、ビュー、シーケンスなどのオブジェクト全体に対する操作(SELECT, INSERT, UPDATE, DELETEなど)を制御します。RLSは、これらの権限が付与されていることを前提として、さらにどの行にその操作を許可するかを決定します。RLSはオブジェクトレベル権限を置き換えるものではなく、補完するものです。あるテーブルに対するSELECT権限がないユーザーは、RLSポリシーがあってもそのテーブルの行を参照することはできません。
  • 列レベル権限: PostgreSQL 14以降で導入された機能で、テーブル内の特定の列へのアクセスを制御します。例えば、給与カラムへのSELECT権限は特定のロールのみに与える、といったことが可能です。RLSは行を制御し、列レベル権限は列を制御します。これらは同時に使用できます。
  • ビュー: ビューは、特定のクエリの結果を仮想的なテーブルとして公開する機能です。データの一部をフィルタリングしたり、特定の列を非表示にしたりするのに利用できます。ビューを使って行レベルのフィルタリングを行うことも可能ですが、これは参照(SELECT)にしか適用できません。INSERT, UPDATE, DELETE操作に対して同様の行レベル制御を行うには、複雑な INSTEAD OF トリガーなどが必要になる場合があります。RLSは、すべてのDML操作(SELECT, INSERT, UPDATE, DELETE)に対して統一的な方法で行レベル制御を適用できます。また、ビューによるフィルタリングはビューを経由したアクセスに限定されますが、RLSはテーブルへの直接アクセスを含む全てのアクセスに適用されます(RLSが有効なテーブルに対して)。

RLSがどのように機能するか

RLSが有効なテーブルに対してクエリが実行されると、PostgreSQLのクエリプランナーは、そのクエリを実行するユーザー、対象となるテーブル、実行されるコマンド(SELECT, INSERT, UPDATE, DELETE)、そして定義されているRLSポリシーを考慮して、元のクエリを自動的に書き換えます。

具体的には、ポリシーで定義された条件式が、クエリの WHERE 句として、あるいは WITH CHECK 制約として内部的に追加されるイメージです。この書き換えはユーザーからは見えません。

例えば、あるユーザーが SELECT * FROM documents; というクエリを実行したとします。この documents テーブルに「owner_id が現在のユーザーIDと一致する行のみ参照可能」というRLSポリシーが設定されている場合、PostgreSQLは内部的にこのクエリを SELECT * FROM documents WHERE owner_id = current_user_id; のように書き換えて実行します。その結果、ユーザーには自分が所有するドキュメントだけが表示されます。

同様に、UPDATE documents SET content = '...' WHERE id = 123; というクエリが実行された場合、RLSポリシーによっては「owner_id が現在のユーザーIDと一致する行」に対する更新のみを許可し、かつ「更新後の行の owner_id が現在のユーザーIDと一致するか」をチェックする、といった処理が行われます。

この内部的なクエリ書き換えの仕組みにより、アプリケーションはRLSが有効なテーブルに対して透過的にアクセスできます。アプリケーションコードは通常のクエリを発行するだけで、行レベルのフィルタリングやチェックはデータベースが自動で行ってくれます。

PostgreSQL RLSの有効化と無効化

RLSはテーブル単位で有効化/無効化できます。デフォルトでは、どのテーブルでもRLSは無効になっています。

RLSの有効化

テーブルに対してRLSを有効にするには、ALTER TABLE コマンドを使用します。

sql
ALTER TABLE table_name ENABLE ROW LEVEL SECURITY;

このコマンドを実行しても、ポリシーが一つも定義されていなければ、すべての行にアクセス可能です。RLSが有効になったテーブルでアクセスを制限するためには、この後にセキュリティポリシーを定義する必要があります。

RLSが有効化されたテーブルは、ポリシーがない場合でも、pg_policy システムカタログにその情報が記録され、そのテーブルへのアクセスがRLSの評価プロセスを経由するようになります(ただし、ポリシーがないため結果的に全行アクセス可となります)。

RLSの無効化

RLSを無効にするには、同様に ALTER TABLE を使用します。

sql
ALTER TABLE table_name DISABLE ROW LEVEL SECURITY;

RLSを無効にすると、そのテーブルに定義されていたすべてのポリシーは無視され、従来のオブジェクトレベル権限のみに基づいてアクセスが制御されるようになります。ポリシー自体は削除されず、テーブルに紐づいたまま保持されますが、アクティブにはなりません。再度 ENABLE すると、定義済みのポリシーが再び有効になります。

デフォルトの動作 (NO FORCE, FORCE)

RLSが有効なテーブルに対して、PostgreSQLには2つのデフォルトの動作モードがあります。これは ALTER TABLE コマンドで指定できます。

  • NO FORCE ROW LEVEL SECURITY (デフォルト):
    このモードが設定されている(または何も指定されていない)場合、スーパーユーザーおよび BYPASSRLS 属性を持つロールに対しては、定義されたRLSポリシーが適用されません。これらの特権ユーザーは、ポリシーに関係なくテーブルの全ての行にアクセスできます。通常の非特権ユーザーにはポリシーが適用されます。

    sql
    ALTER TABLE table_name NO FORCE ROW LEVEL SECURITY; -- 明示的に指定する場合 (デフォルト)

  • FORCE ROW LEVEL SECURITY:
    このモードが設定されている場合、スーパーユーザーや BYPASSRLS 属性を持つロールを含む、全てのユーザーに対して、定義されたRLSポリシーが強制的に適用されます。これは、たとえ管理者であっても特定のポリシーに従ってデータにアクセスする必要がある場合に有用です。ただし、これはポリシーが定義されている場合に限られます。ポリシーが一つも定義されていない場合は、FORCE であっても全行にアクセスできます。

    sql
    ALTER TABLE table_name FORCE ROW LEVEL SECURITY;

どちらを選択すべきか?

ほとんどの場合、デフォルトの NO FORCE で十分です。スーパーユーザーは通常、管理目的で全てのデータにアクセスできる必要があります。しかし、監査要件が厳しく、特定の管理者であってもポリシーを通したアクセスのみを許可したい場合は、FORCE を使用することを検討してください。FORCE モードは、意図しないデータ漏洩を防ぐための追加の防御層となり得ますが、管理者がデータ復旧やトラブルシューティングのために全データにアクセスする必要がある場合に、かえって障害となる可能性もあります。注意深く検討して選択してください。

なお、FORCE 設定は、SELECT クエリに対してのみ影響します。INSERT, UPDATE, DELETE のような書き込み操作に対しては、スーパーユーザーや BYPASSRLS 属性を持つロールにはポリシーが適用されません(これらの操作では BYPASSRLS は常に有効になるため)。ただし、WITH CHECK 句が指定されているポリシーは、スーパーユーザーや BYPASSRLS 属性を持つロールがデータを挿入または更新する際にも、ポリシーの条件を満たさないデータの書き込みを防ぐために評価されることがあります。この挙動は少し複雑なので、公式ドキュメントを確認してください。一般的には、非特権ユーザーに対する SELECT, INSERT, UPDATE, DELETE の全ての操作に対してポリシーを有効にしたい場合は、テーブルに ENABLE ROW LEVEL SECURITY を設定し、適切なポリシーを定義するだけで十分です。FORCE は主にスーパーユーザーや BYPASSRLS 属性を持つユーザーの SELECT 操作に対するポリシー適用を制御するためのものです。

ポリシーの作成 (CREATE POLICY)

RLSの中核機能は、セキュリティポリシーの定義です。ポリシーは、どのロールが、どのようなコマンドで、テーブルのどの行にアクセスできるかを規定します。ポリシーは CREATE POLICY コマンドを使用して作成します。

CREATE POLICY 構文

sql
CREATE POLICY name ON table_name
[ FOR { ALL | SELECT | INSERT | UPDATE | DELETE } ]
[ TO role_name [, ...] ]
[ USING ( expression ) ]
[ WITH CHECK ( expression ) ];

各句について詳しく見ていきましょう。

  • name: ポリシーに付ける任意の名前。テーブル内で一意である必要があります。
  • ON table_name: ポリシーを適用するテーブルの名前。
  • FOR { ALL | SELECT | INSERT | UPDATE | DELETE }:
    このポリシーがどのコマンドに適用されるかを指定します。

    • ALL: SELECT, INSERT, UPDATE, DELETE のすべてのコマンドに適用されます。デフォルトです。
    • SELECT: SELECT コマンドにのみ適用されます。どの行を参照できるかを制限します。
    • INSERT: INSERT コマンドにのみ適用されます。どの行を挿入できるかを制限します。具体的には、挿入しようとしている行がポリシーの条件を満たすかをチェックします(WITH CHECK 句が使用されます)。
    • UPDATE: UPDATE コマンドにのみ適用されます。どの行を更新できるかを制限し、また更新後の行がポリシーの条件を満たすかをチェックします(USINGWITH CHECK 句が使用されます)。
    • DELETE: DELETE コマンドにのみ適用されます。どの行を削除できるかを制限します(USING 句が使用されます)。
  • TO role_name [, ...]:
    このポリシーが適用されるロールを指定します。複数のロールを指定できます。PUBLIC を指定すると、すべてのロールに適用されます。この句を省略した場合、ポリシーは PUBLIC に適用されます(つまり、すべてのロールに適用されます)。

  • USING ( expression ):
    これはアクセスを許可する行を決定するための条件式です。SELECT および DELETE コマンドに対して、この式が TRUE となる行のみが処理の対象となります。UPDATE コマンドに対しては、更新対象となる元の行がこの式を満たす必要があります。この式は、クエリの WHERE 句として内部的に追加されるようなイメージで機能します。USING 式は、対象の行のカラム、現在のユーザー (current_user, session_user)、セッション変数 (current_setting('variable')) など、クエリの評価時に利用可能な情報に基づいている必要があります。副問い合わせも使用可能ですが、パフォーマンスに注意が必要です。INSERT コマンドでは USING 句は効果を持ちません(INSERT は既存の行を選択するわけではないため)。

  • WITH CHECK ( expression ):
    これは書き込み操作(INSERT, UPDATE)に対して、新しい行や更新後の行がポリシーの条件を満たすかをチェックするための条件式です。INSERT コマンドでは、挿入しようとしている新しい行全体に対してこの式が評価され、TRUE となる行のみ挿入が許可されます。UPDATE コマンドでは、更新後の行全体に対してこの式が評価され、TRUE となる更新のみが実行されます。DELETE コマンドでは WITH CHECK 句は効果を持ちません(行が削除されるため、削除後の行に対するチェックは無意味です)。WITH CHECK 式も、対象の行のカラム、現在のユーザー、セッション変数などに基づいて記述できます。

USINGWITH CHECK の違いと使い分け

この2つの句はRLSにおいて非常に重要であり、混同しやすい部分です。

  • USING: 「ユーザーが見ることができる(あるいは削除・更新対象として選択できる)行」を定義します。既存の行に対するフィルタリングです。
  • WITH CHECK: 「ユーザーが作成できる(あるいは更新結果として残せる)行」を定義します。書き込み操作(INSERT/UPDATE)の結果としてデータベースに格納される行に対する検証です。

例で理解する:

documents テーブルがあり、各行に owner_id カラム(ドキュメントの所有者を示す)と status カラム(’draft’, ‘published’, ‘archived’など)があるとします。

シナリオ1: 自分のドキュメントのみ参照・更新・削除できる

sql
-- ポリシー名: owner_access
-- テーブル: documents
-- コマンド: 全て (ALL)
-- 対象ロール: PUBLIC (全てのユーザー)
-- 条件: owner_id が現在のユーザーと一致する行
CREATE POLICY owner_access ON documents
FOR ALL
TO PUBLIC
USING (owner_id = current_user)
WITH CHECK (owner_id = current_user);

  • USING (owner_id = current_user):
    • SELECT: owner_id が自分のユーザー名と一致する行のみが表示されます。
    • UPDATE: owner_id が自分のユーザー名と一致する行のみが更新対象として選択できます。
    • DELETE: owner_id が自分のユーザー名と一致する行のみが削除対象として選択できます。
  • WITH CHECK (owner_id = current_user):
    • INSERT: 挿入しようとしている行の owner_id が自分のユーザー名と一致する場合のみ挿入できます。これにより、他のユーザーが所有者となるドキュメントを勝手に作成するのを防ぎます。
    • UPDATE: 更新後の行の owner_id が自分のユーザー名と一致する場合のみ更新が許可されます。これにより、自分のドキュメントを更新する際に、誤って owner_id を他のユーザー名に変更してしまうことを防ぎます(あるいは、意図的に所有権を譲渡するような機能がない限り、このチェックは重要です)。

この例のように、多くの場合、USINGWITH CHECK は同じ条件になります。

シナリオ2: 全ユーザーが公開されているドキュメントを参照できる。ただし、所有者のみが更新・削除できる。

この場合、参照(SELECT)と書き込み(UPDATE/DELETE)でポリシーを分ける必要があります。

“`sql
— ポリシー名: view_published
— テーブル: documents
— コマンド: SELECT
— 対象ロール: PUBLIC
— 条件: status が ‘published’ または owner_id が現在のユーザーと一致する行
CREATE POLICY view_published ON documents
FOR SELECT
TO PUBLIC
USING (status = ‘published’ OR owner_id = current_user);

— ポリシー名: modify_own
— テーブル: documents
— コマンド: UPDATE, DELETE
— 対象ロール: PUBLIC
— 条件: owner_id が現在のユーザーと一致する行
CREATE POLICY modify_own ON documents
FOR UPDATE, DELETE
TO PUBLIC
USING (owner_id = current_user);

— INSERT に関するポリシー (ここでは所有者のみが挿入できると仮定)
CREATE POLICY insert_own ON documents
FOR INSERT
TO PUBLIC
WITH CHECK (owner_id = current_user);
“`

  • view_published ポリシー (FOR SELECT):
    • USING (status = 'published' OR owner_id = current_user): SELECT 時、status が ‘published’ の行 または owner_id が自分のユーザー名の行が表示されます。WITH CHECKSELECT では効果がないため不要です(指定しても無視されます)。
  • modify_own ポリシー (FOR UPDATE, DELETE):
    • USING (owner_id = current_user): UPDATE および DELETE 時、owner_id が自分のユーザー名の行のみが対象として選択できます。
    • WITH CHECK: UPDATE に対して WITH CHECK が必要な場合(例: 更新後も所有者である必要がある)、ここに追加します。ここでは簡単のため省略していますが、例えば WITH CHECK (owner_id = current_user) を追加することも可能です。DELETE に対して WITH CHECK は効果がないため不要です。
  • insert_own ポリシー (FOR INSERT):
    • USING: INSERT では効果がないため不要です。
    • WITH CHECK (owner_id = current_user): 挿入しようとしている行の owner_id が自分のユーザー名と一致する場合のみ挿入が許可されます。

この例のように、コマンド(FOR 句)によって異なるポリシーを定義したり、USINGWITH CHECK を異なる条件にしたりすることで、柔軟なアクセス制御が可能です。

複数のポリシーがある場合の評価

1つのテーブルに対して、同じコマンド(例: SELECT)や同じロールに対して、複数のポリシーを定義することができます。この場合、同じコマンドに対して定義されたポリシーの USING 条件は、論理和(OR)で結合されて評価されます

例えば、documents テーブルに対して、以下の2つの SELECT ポリシーがあるとします。

  1. ポリシー p1: USING (owner_id = current_user)
  2. ポリシー p2: USING (status = 'published')

このテーブルに対して SELECT * FROM documents; というクエリを実行すると、PostgreSQLは内部的にこれらのポリシーを結合して、SELECT * FROM documents WHERE (owner_id = current_user) OR (status = 'published'); のように書き換えます。結果として、ユーザーは自分が所有するドキュメントまたは公開されているドキュメントを参照できるようになります。

WITH CHECK 条件の評価は少し異なります。
INSERTUPDATE の場合、対象となる行は、そのコマンドに対して定義された全てのポリシーの WITH CHECK 条件を論理積(AND)で満たす必要があります。つまり、全てのチェックに合格しないと書き込みは許可されません。

ポリシーの適用順序:

ポリシーは名前順や作成順で評価されるわけではありません。PostgreSQLは、対象となるユーザー、コマンド、テーブルに一致する全てのポリシーを特定し、それらをまとめて評価します。

  • SELECT, DELETE: 一致する全てのポリシーの USING 条件がOR結合されます。
  • UPDATE: 更新対象となる元の行は、一致する全てのポリシーの USING 条件のOR結合を満たす必要があります。さらに、更新後の行は、一致する全てのポリシーの WITH CHECK 条件のAND結合を満たす必要があります。
  • INSERT: 挿入しようとしている新しい行は、一致する全てのポリシーの WITH CHECK 条件のAND結合を満たす必要があります。

この「OR結合」の仕組みは非常に重要です。デフォルトでは、ポリシーが一つもない場合、そのコマンドに対しては全ての行へのアクセスが許可されます。 RLSを有効にしたテーブルでアクセスを制限したい場合は、最低一つはポリシーを定義する必要があります。

USING / WITH CHECK 式での利用例

ポリシー式の中では、現在のセッションに関する情報や、対象行のカラムの値などを利用できます。

  • current_user: 現在認証されているユーザー名(ロール名)を返します。
  • session_user: セッションの開始時に認証されたユーザー名(ロール名)を返します。SET ROLE などで現在のロールが変更されても変わりません。通常は current_user を使用することが多いです。
  • current_setting('variable_name'): SET コマンドで設定されたカスタムセッション変数の値を取得します。これはマルチテナントシステムで現在のテナントIDを渡す際などに非常に便利です。例: current_setting('app.tenant_id').
  • 対象行のカラム名: 例 owner_id, status. UPDATEWITH CHECK 句では、更新後の行のカラム値を参照できます。
  • サブクエリ: 例 owner_id IN (SELECT user_id FROM user_roles WHERE role = 'manager'). ただし、パフォーマンスに注意が必要です。

例: カスタムセッション変数を使ったマルチテナントポリシー

orders テーブルに tenant_id カラムがある場合。

“`sql
— RLS有効化
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

— テナントポリシー定義
CREATE POLICY tenant_policy ON orders
FOR ALL
TO PUBLIC
USING (tenant_id = current_setting(‘app.current_tenant_id’, true)) — true は設定されていない場合にエラーにしない
WITH CHECK (tenant_id = current_setting(‘app.current_tenant_id’, true));
“`

アプリケーションでは、データベース接続確立後、クエリ実行前に現在のユーザーのテナントIDをセッション変数に設定します。

“`sql
SET app.current_tenant_id = ‘tenant_a’;
— この後のクエリは tenant_id = ‘tenant_a’ の行にのみアクセス可能

SELECT * FROM orders; — WHERE tenant_id = ‘tenant_a’ が自動で付加されるイメージ
“`

ポリシーの管理

定義したポリシーは、必要に応じて変更したり削除したりできます。

  • ALTER POLICY name ON table_name ...:
    既存のポリシーの定義を変更します。FOR, TO, USING, WITH CHECK 句を変更できます。ポリシー名や対象テーブルは変更できません。

    “`sql
    — ポリシー ‘owner_access’ に admin ロールを追加
    ALTER POLICY owner_access ON documents
    TO PUBLIC, admin;

    — ポリシー ‘view_published’ の USING 条件を変更
    ALTER POLICY view_published ON documents
    USING (status IN (‘published’, ‘internal’) OR owner_id = current_user);
    “`

  • DROP POLICY name ON table_name:
    指定したポリシーを削除します。

    sql
    -- ポリシー 'insert_own' を削除
    DROP POLICY insert_own ON documents;

  • ポリシーの確認:
    定義されているポリシーは、pg_policy システムカタログビューから確認できます。

    sql
    SELECT * FROM pg_policy WHERE polrelid = 'documents'::regclass;

    または、psql コマンドラインツールで \d+ table_name を実行すると、テーブル定義の一部としてRLSの状態とポリシーが表示されます。

    sql
    \d+ documents

RLSの適用例

RLSは様々なシナリオで強力なツールとなります。いくつかの代表的な適用例を見てみましょう。

例1: マルチテナントアーキテクチャ

前述の例ですが、最も一般的なRLSのユースケースです。

  • テーブル設計: 各テーブルに tenant_id カラム(通常は integer または uuid 型)を追加します。
  • RLS設定:
    “`sql
    — 各テナントテーブルでRLSを有効化
    ALTER TABLE tenants_table ENABLE ROW LEVEL SECURITY;
    ALTER TABLE users_table ENABLE ROW LEVEL SECURITY;
    ALTER TABLE orders_table ENABLE ROW LEVEL SECURITY;
    — … 他のテーブルも同様

    — テナント分離ポリシーを定義
    — (tenant_id が現在のセッションの app.current_tenant_id と一致する行のみ許可)
    CREATE POLICY tenant_isolation ON tenants_table
    FOR ALL TO PUBLIC
    USING (tenant_id = current_setting(‘app.current_tenant_id’, true))
    WITH CHECK (tenant_id = current_setting(‘app.current_tenant_id’, true));

    CREATE POLICY tenant_isolation ON users_table
    FOR ALL TO PUBLIC
    USING (tenant_id = current_setting(‘app.current_tenant_id’, true))
    WITH CHECK (tenant_id = current_setting(‘app.current_tenant_id’, true));

    CREATE POLICY tenant_isolation ON orders_table
    FOR ALL TO PUBLIC
    USING (tenant_id = current_setting(‘app.current_tenant_id’, true))
    WITH CHECK (tenant_id = current_setting(‘app.current_tenant_id’, true));
    — … 他のテーブルも同様
    “`
    * アプリケーション側:
    ユーザーがログインした際に、そのユーザーが所属するテナントのIDを取得し、データベースセッション変数に設定します。

    sql
    -- Java (JDBC) の例
    try (Statement stmt = connection.createStatement()) {
    stmt.execute("SET app.current_tenant_id = '" + userTenantId + "'");
    }
    // この後のクエリは tenant_id によるフィルタリングが自動適用される

    または、各クエリの前に動的に設定することも可能です。接続プールを利用する場合は、接続を使い回す際にセッション変数が前のユーザーの値のままにならないよう注意が必要です(接続を返す際に変数をリセットするか、接続取得直後に設定するなど)。

この設定により、各アプリケーションユーザーは、自分のテナントに紐づいたデータのみにアクセスできるようになります。アプリケーションコードは WHERE tenant_id = ... のようなフィルタリングロジックを明示的に記述する必要がなくなり、シンプルかつ安全になります。

ただし、クロステナントアクセスが必要な管理者機能などを実装する場合は、別途、BYPASSRLS 属性を持つロールを使用するか、特定のロールに対して別のポリシー(例: TO admin_role USING (true))を定義するなどの対応が必要です。

例2: 組織内のアクセス制御(部署・担当者別アクセス)

ユーザーが所属する部署や、自分が担当しているデータのみにアクセスできるように制御する場合。

  • テーブル設計: sales_leads テーブルに assigned_user_id または department カラムを追加。
  • ユーザー/ロール管理: ユーザーと部署の関連付けを行うテーブルや、ユーザーに部署を示すカスタム属性を付与するなど。ユーザーのロールに部署名を含めることも考えられます。
  • RLS設定:
    ユーザーのロール名が部署名と一致すると仮定する場合:
    “`sql
    — RLS有効化
    ALTER TABLE sales_leads ENABLE ROW LEVEL SECURITY;

    — 部署ポリシー
    CREATE POLICY department_access ON sales_leads
    FOR ALL
    TO PUBLIC — または特定のアプリケーションロール
    USING (department = current_user)
    WITH CHECK (department = current_user);

    — 必要に応じて、管理ロールに全アクセス権限を与えるポリシー
    CREATE POLICY admin_access ON sales_leads
    FOR ALL
    TO admin_role
    USING (true) — 条件なし、全て許可
    WITH CHECK (true); — 条件なし、全て許可
    ユーザーIDと担当者IDを紐付ける場合:sql
    — RLS有効化
    ALTER TABLE sales_leads ENABLE ROW LEVEL SECURITY;

    — 担当者ポリシー
    CREATE POLICY assigned_user_access ON sales_leads
    FOR ALL
    TO PUBLIC — または特定のアプリケーションロール
    USING (assigned_user_id = current_user)
    WITH CHECK (assigned_user_id = current_user);

    — マネージャーポリシー (自分の部署のメンバーが担当するリードを見られる)
    — user_department は user_id と department_id の関連を持つテーブルと仮定
    CREATE POLICY manager_access ON sales_leads
    FOR SELECT
    TO manager_role
    USING (assigned_user_id IN (SELECT user_id FROM user_department WHERE department_id = (SELECT department_id FROM user_department WHERE user_id = current_user)));

    — admin_role のポリシーも必要に応じて追加
    ``
    この例では、
    manager_access` ポリシーでサブクエリを使用しています。サブクエリが多く実行されるとパフォーマンスが低下する可能性があるため、注意が必要です。ユーザーの部署情報をセッション変数に設定して利用する方が効率的な場合があります。

例3: 個人情報保護

各ユーザーが自分のアカウント情報のみを参照・更新できるが、他のユーザーの情報は見られないようにする場合。管理者ロールは全ユーザー情報にアクセスできる。

  • テーブル設計: users テーブルに user_id カラム。
  • RLS設定:
    “`sql
    — RLS有効化
    ALTER TABLE users ENABLE ROW LEVEL SECURITY;

    — ユーザー自身のアクセス許可ポリシー
    CREATE POLICY own_data_access ON users
    FOR ALL
    TO PUBLIC
    USING (user_id = current_user) — user_id がデータベースユーザー名と一致すると仮定
    WITH CHECK (user_id = current_user);

    — 管理者ロールへの全アクセス許可ポリシー
    CREATE POLICY admin_full_access ON users
    FOR ALL
    TO admin_role
    USING (true)
    WITH CHECK (true);
    ``
    この設定により、一般ユーザーは
    SELECT * FROM users;とクエリしても、自分のuser_idと一致する行(おそらく1行)しか表示されません。UPDATE users SET email = ‘…’ WHERE user_id = …;のようなクエリも、WHERE句のuser_idが自分のものと一致し、かつ更新後の行のuser_idも自分のものである場合にのみ許可されます。管理者はadmin_role` で接続すれば全ての行にアクセスできます。

RLS利用時の考慮事項と注意点

RLSは強力な機能ですが、導入・運用にあたってはいくつかの考慮事項と注意点があります。

パフォーマンス

RLSポリシーは、クエリが実行されるたびに評価され、クエリプランに影響を与えます。不適切に定義されたポリシーは、クエリのパフォーマンスを著しく低下させる可能性があります。

  • ポリシー式の複雑さ: ポリシー式が複雑であったり、効率の悪いサブクエリを含んでいたりする場合、その評価に時間がかかり、クエリ全体の実行速度が低下します。
  • インデックスの利用: ポリシー式で参照されるカラムには、適切にインデックスを貼りましょう。例えば、USING (tenant_id = current_setting(...)) というポリシーがある場合、tenant_id カラムにインデックスがあると、PostgreSQLはRLSによって追加された条件を効率的に利用して行を絞り込むことができます。複合インデックスも考慮します(例: (tenant_id, status) など)。
  • クエリプランの確認: RLSを導入した後は、重要なクエリに対して EXPLAIN コマンドを実行し、RLSポリシーがクエリプランにどのように影響しているかを確認することが非常に重要です。ポリシーが原因で全件スキャンが発生していないか、インデックスが正しく使われているかなどを確認しましょう。
  • ポリシーキャッシュ: PostgreSQLはポリシー定義をキャッシュするため、頻繁にポリシー定義を変更しない限り、ポリシーのロード自体が大きなオーバーヘッドになることは少ないです。問題になりやすいのは、ポリシー式自体の評価コストです。

セキュリティ上の注意

RLSを誤って設定すると、意図しないデータ漏洩や改ざんのリスクが発生します。

  • USINGWITH CHECK の設定ミス:
    • SELECT ポリシーで USING 条件が緩すぎる場合、見せるべきでないデータが見えてしまいます。
    • INSERT ポリシーで WITH CHECK 条件がない、または緩すぎる場合、ユーザーは不正なデータ(例: 他のテナントのデータ)を挿入できてしまいます。
    • UPDATE ポリシーで USING 条件が緩すぎる場合、ユーザーは更新権限のない行を更新しようとできてしまいます。WITH CHECK 条件がない、または緩すぎる場合、ユーザーは行を更新して不正な状態(例: 他のユーザーの所有物にする)にしてしまう可能性があります。
      特に WITH CHECK は忘れがちですが、書き込みセキュリティにおいて非常に重要です。
  • ロール権限との組み合わせ: RLSは GRANT/REVOKE を置き換えるものではありません。テーブルへの SELECT 権限がないユーザーは、たとえRLSポリシーで行へのアクセスが許可されていても、その行を参照することはできません。RLSは、オブジェクトレベル権限が付与されている前提で、さらに細かい行レベルの制限を加えるものです。権限設計は両方を考慮して行う必要があります。
  • FORCE ROW LEVEL SECURITY の理解: FORCE オプションの挙動を正しく理解しておく必要があります。特にスーパーユーザーや BYPASSRLS 属性を持つユーザーがRLSポリシーから除外されないことを意図しない場合に FORCE を使うことがありますが、前述の通り INSERT/UPDATE/DELETE 操作では BYPASSRLS が常に有効になることに注意が必要です。ほとんどのケースでは、一般ユーザーに対するポリシー適用のみを考えればよく、NO FORCE (デフォルト) で問題ありません。管理者は BYPASSRLS 属性を使い分けるか、管理者専用のポリシーを定義することが一般的です。
  • BYPASSRLS 属性: ロールに BYPASSRLS 属性を付与すると、そのロールはRLSが有効なテーブルであっても、定義されたポリシーを無視して全ての行にアクセスできます。スーパーユーザーはこの属性を常に持っています。この属性は非常に強力な権限であるため、付与するロールは慎重に選ぶ必要があります。アプリケーションがデータベースに接続する際に使用するロールに安易に BYPASSRLS を付与すると、RLSによるセキュリティが完全に無効化されてしまいます。

アプリケーションとの連携

RLSポリシーで current_setting() を利用する場合、アプリケーション側でセッション変数を正しく設定することが不可欠です。

  • セッション変数の設定: データベース接続確立後、ユーザー認証が完了した直後に、そのユーザーに関連する情報(テナントID、ユーザーID、所属部署など)をセッション変数に設定します。
    sql
    SET app.current_tenant_id = '...';
    SET app.current_user_id = '...';
  • 接続プール: アプリケーションがデータベース接続プールを使用している場合、注意が必要です。接続を使い回す際に、セッション変数が前のユーザーの値のままになっていると、セキュリティ上の問題が発生します。
    • 推奨: 接続をプールに戻す直前に、設定したセッション変数を全てリセットする (SET app.current_tenant_id = NULL; など)。
    • 代替: 接続プールから接続を取得した直後に、必ずセッション変数を再設定する。
      どちらの方法でも、セッション変数の設定漏れがないようにアプリケーションコードを設計する必要があります。
  • search_path の利用: セッション変数以外に、RLSポリシーで search_path の値を活用することも考えられます。例えば、スキーマ名をテナントIDに対応させ、search_path に現在のテナントのスキーマを設定する、といった方法です。ただし、これはテーブルがスキーマごとに分かれている場合に限られます。

管理の複雑さ

多数のテーブル、多数のロール、複雑なアクセス要件を持つシステムでは、RLSポリシーの管理が複雑になる可能性があります。

  • ポリシー数の増加: テーブルごと、コマンドごと、ロールごと、条件ごとにポリシーが増えていくと、全体像の把握や変更時の影響範囲の分析が難しくなります。
  • 共通ポリシー: 多くのテーブルで共通のポリシー(例: テナント分離)を定義する場合、定義が重複しがちです。これはスクリプト化や、ポリシーを生成するツールなどである程度自動化することを検討できます。
  • スキーマ変更との連携: テーブル構造を変更する場合、そのテーブルに関連するRLSポリシーもレビューし、必要に応じて修正する必要があります。

RLSを導入する際は、少数の重要なテーブルや特定の機密データから段階的に適用していくことを検討し、管理負担を評価しながら進めるのが良いでしょう。

RLSの高度なトピック(簡潔に)

  • JOIN時のポリシー評価: RLSは、クエリに含まれる各テーブルについて、それぞれのテーブルに定義されたポリシーを個別に評価します。JOINの結果に対してまとめてポリシーが適用されるわけではありません。あるテーブルのポリシーによって一部の行がフィルタリングされた後、そのフィルタリングされた結果を使ってJOINが行われます。この挙動を理解しておくことは、JOINを含むクエリでのRLSの振る舞いを予測する上で重要です。
  • ビューやマテリアライズドビューとRLS: ビューやマテリアライズドビュー自体にはRLSポリシーを設定できません。しかし、ビューやマテリアライズドビューが参照している基底テーブルにRLSポリシーが設定されていれば、ビューやマテリアライズドビューを経由したアクセスにもRLSポリシーが適用されます。ビューは基底テーブルへのクエリをラップしているだけなので、基底テーブルのRLSが有効になります。マテリアライズドビューはデータを物理的に保持しますが、そのデータは基底テーブルからロードされる際にRLSが適用された結果となります。
  • トリガーとRLS: トリガー関数内では、current_user やセッション変数の値はトリガーを実行したユーザーのものが利用できます。トリガー関数内でRLSが有効なテーブルにアクセスする場合、トリガーを実行したユーザーの権限とポリシーに基づいてアクセスが制御されます。ただし、トリガー関数は通常 SECURITY DEFINER で作成され、定義者の権限で実行されることが多いです。この場合、定義者のロールが BYPASSRLS 属性を持っていれば、トリガー関数内のアクセスではRLSが無視されます。トリガーとRLSを組み合わせる際は、権限のフローを注意深く設計する必要があります。

RLS以外のセキュリティ機能との比較

RLSはPostgreSQLのセキュリティ機能の一部であり、他の機能と組み合わせて使用することで、より堅牢なセキュリティを実現できます。

  • GRANT/REVOKE: オブジェクトレベルの権限管理。RLSと組み合わせて使用し、まずテーブルへの基本的なアクセス権限を与え、RLSでさらに詳細な行レベルの制限を加えます。
  • ビュー: データの一部を公開したり、複雑なクエリを隠蔽したりするために使用できます。RLSと組み合わせて、RLSによってフィルタリングされたデータをビューとして提供することも可能です。ただし、ビューだけでは書き込み操作の行レベル制御は難しいです。
  • 暗号化: データをストレージ上で暗号化することで、物理的なデータファイルへの不正アクセスからの保護を提供します(透過的データ暗号化 TDE など)。RLSはデータベースに接続した正当なユーザーからの、論理的なデータアクセスを制御します。これらは異なる層のセキュリティを提供します。カラムレベル暗号化は、特定のカラムの内容を読み取れるユーザーを制御できます。RLSと組み合わせることで、特定の行の、特定のカラムの内容へのアクセスを制御するといった非常に細かい設定が可能になります。

まとめ

PostgreSQLの行レベルセキュリティ(RLS)は、テーブル内の個々のデータ行に対するアクセスを、柔軟かつ強力に制御するための機能です。アプリケーションコードやビューによるフィルタリングと異なり、データベース自身がアクセスを強制するため、より高いセキュリティレベルを実現できます。

RLSの主なメリット:

  • セキュリティ強化: アプリケーションコードに依存せず、データベース自身がアクセス制御を行うため、セキュリティホールが生まれにくい。
  • アプリケーションの簡素化: アプリケーション側で複雑なフィルタリングロジックを実装する必要がなくなり、コードがシンプルになる。
  • 一元管理: セキュリティポリシーをデータベースで一元管理できる。
  • 透過性: アプリケーションからはRLSの存在を意識せず、通常のクエリを発行できる。
  • 柔軟性: ユーザー、ロール、セッション変数、行の内容に基づいて、SELECT, INSERT, UPDATE, DELETE の各操作に対して異なるポリシーを定義できる。

RLSの注意点:

  • パフォーマンス: 複雑なポリシー式はクエリパフォーマンスに影響を与える可能性があるため、EXPLAIN による確認とインデックスの最適化が重要。
  • 設定ミス: USINGWITH CHECK の設定を誤ると、意図しないデータ漏洩や改ざんのリスクがある。特に WITH CHECK の適切な設定が重要。
  • 管理コスト: 多数のテーブルと複雑な要件がある場合、ポリシーの管理が煩雑になる可能性がある。
  • アプリケーション連携: current_setting() を使用する場合、アプリケーション側でのセッション変数の設定・管理が不可欠。

どのような場合にRLSが有効か:

  • マルチテナントシステムで、テナント間のデータ分離を確実に実現したい場合。
  • 組織内で、ユーザーの部署、役職、担当者などに紐づいてデータのアクセス権限を厳密に制御したい場合。
  • 機密性の高い個人情報などを含むテーブルで、データ所有者や特定の権限を持つユーザーのみがアクセスできるようにしたい場合。
  • 従来の GRANT/REVOKE では実現できない、行ごとの複雑なアクセス制御が必要な場合。

RLSは、適切に設計・実装されれば、PostgreSQLデータベースのセキュリティレベルを大幅に向上させることができます。導入にあたっては、対象となるテーブル、必要なアクセス制御の要件、関与するロール、そしてアプリケーションとの連携方法を慎重に検討し、段階的に適用していくことをお勧めします。

参考資料

RLSを使いこなして、あなたのPostgreSQLデータベースのデータセキュリティを新たなレベルへ引き上げましょう。


コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール