Skip to main content

Postgres 行レベルセキュリティ (RLS) の限界と代替

Tianzhou · 2025年5月28日

PostgreSQL の組み込み行レベルセキュリティ (RLS) を使うと、DBA はユーザー単位で、どの行がクエリで返るか、どの行がデータ操作コマンドで変更できるかを制御するポリシーを定義できます。有効化すると、テーブルへのアクセスはすべてセキュリティポリシーに許可される必要があり、テーブルオーナーは通常これらを迂回します (明示的に設定しない限り)。

主な特徴:

  • ALTER TABLE ... ENABLE ROW LEVEL SECURITY でテーブル単位に有効化
  • ポリシーはコマンド (SELECT、INSERT、UPDATE、DELETE) やロール単位に固有化できる
  • どの行が見え・変更できるかを Boolean 式で判定
  • 複数のポリシーを OR (permissive) または AND (restrictive) で組み合わせられる
  • スーパーユーザーや BYPASSRLS 属性のロールは行セキュリティを迂回

強力ですが、RLS には性能影響、柔軟性の限界、運用オーバーヘッドがあり、広い採用を阻みます。詳しく見て、代替を議論しましょう。

性能影響

PostgreSQL RLS とビューの性能差を、内部メカニズムを交えて整理します。

RLS ポリシーはクエリ実行中にすべての行について評価されます。RLS が有効なテーブルをクエリすると、PostgreSQL は:

  1. テーブルをスキャン (または索引を使用) する
  2. 取り出した各行について、適用対象のすべての RLS ポリシーを評価する
  3. ポリシーチェックを通過した行だけを返す
  4. クエリ自身に WHERE 句があってもこれが起きる
-- RLS ポリシー
CREATE POLICY tenant_isolation ON orders
FOR ALL TO app_user
USING (tenant_id = current_setting('app.tenant_id')::int);

-- このクエリは:
-- 1. orders テーブルをスキャン
-- 2. 各行について tenant_id が現在の設定と一致するか確認
-- 3. ユーザーの WHERE 句を適用
-- 4. 一致した行を返す
SELECT * FROM orders WHERE status = 'pending';

ポリシー合成の複雑さ 同じテーブルに複数の RLS ポリシーがあると AND で合成され、すべての行に対する各ポリシーの評価が必要になります。

-- 複数ポリシーは評価コストを積み上げる
CREATE POLICY tenant_policy ON orders
USING (tenant_id = current_setting('app.tenant_id')::int);

CREATE POLICY department_policy ON orders
USING (department_id IN (
  SELECT dept_id FROM user_departments
  WHERE user_id = current_setting('app.user_id')::int
));

-- すべての行が両ポリシーチェックを通過しなければならない

代替: ビュー

事前フィルタされた結果集合 セキュリティ述語付きのビューはクエリプラン作成時に解決されるため、オプティマイザは述語をプッシュダウンして索引を有効活用できます。

-- セキュリティビュー
CREATE VIEW tenant_orders AS
SELECT * FROM orders
WHERE tenant_id = current_setting('app.tenant_id')::int;

-- ビューに対するクエリ
SELECT * FROM tenant_orders WHERE status = 'pending';

-- PostgreSQL は次のように最適化できる:
-- SELECT * FROM orders
-- WHERE tenant_id = current_setting('app.tenant_id')::int
--   AND status = 'pending'
-- そして (tenant_id, status) の複合索引を使う

索引の活用 セキュリティ述語が行単位フィルタではなく基底クエリの一部に来ると、クエリプランナは索引を有効に使えます。

-- RLS の場合: 行取得後にセキュリティチェックが走るため、
-- tenant_id の索引が最適に使われないことがある

-- ビューの場合: (tenant_id, status) の複合索引を有効に使える
CREATE INDEX idx_orders_tenant_status ON orders(tenant_id, status);

根本的な問題は、RLS がクエリ最適化の不可分な部分ではなく、後処理のセキュリティフィルタとして動く点です。一方ビューは、セキュリティ制約をコアなクエリ実行計画の一部として最適化できます。

RLS 版は:

  1. フル結合を実施
  2. 結合された各行で RLS ポリシーを評価
  3. 結果をフィルタ

ビュー版は:

  1. 両テーブルを索引で事前フィルタ
  2. 関連行だけを結合
  3. 最終述語を効率的に適用

もちろん、ビューにも限界があります。

  • ビューの乱立: アクセスパターン別に多数のビューが必要になり得る
  • 保守オーバーヘッド: 基底テーブルの変更でビュー更新が必要になり得る
  • 動性の限界: 高度に動的な権限モデルには不向き
  • 書き込み操作: INSERT/UPDATE の扱いがより複雑

柔軟性の限界

SQL のみの式に縛られる

RLS ポリシーは SQL 式に制約され、表現できることに限りがあります。

-- RLS でできるのはこの程度:
CREATE POLICY simple_tenant_policy ON documents
USING (tenant_id = current_setting('app.tenant_id')::int);

-- これは無理:
-- 「ユーザーは、同じ部署で
--   かつ業務時間内で
--   かつ必要な研修を完了していて
--   かつドキュメントがコンテンツモデレーションでフラグされておらず
--   かつ外部コンプライアンス API がアクセスを承認した場合に、
--   ドキュメントへアクセスできる」

静的なコンテキストの限界

RLS ポリシーは限定的なコンテキスト — 主に DB セッション変数と現在ユーザー情報 — で評価されます。

-- セッションコンテキストに限定
CREATE POLICY time_based_policy ON sensitive_data
USING (
  EXTRACT(hour FROM NOW()) BETWEEN 9 AND 17 -- 業務時間の粗い近似
  AND EXTRACT(dow FROM NOW()) BETWEEN 1 AND 5 -- 平日のみ
);

-- 考慮できない:
-- - ユーザーの実タイムゾーンや所在
-- - 企業固有の休日カレンダー
-- - ユーザーのデバイス/IP レピュテーション
-- - 直近のセキュリティイベントやリスクスコア
-- - 動的なコンプライアンス要件

外部システム連携が不可

RLS は外部 API 呼び出しや他システム連携ができません。

-- これは RLS で不可能:
-- CREATE POLICY compliance_policy ON financial_records
-- USING (external_compliance_service_check(user_id, document_id, current_timestamp));

-- 制約は PostgreSQL インスタンス内のデータに限られる

代替: アプリケーション層のロジック

RLS が SQL と DB セッションコンテキストの制約内に縛られるのに対し、現代の認可システムは任意のデータソースを取り込み、任意のビジネスロジックを当て、各リクエストのフルコンテキストで判断できます。

異なる属性タイプを認可判断に取り込む ABAC (Attribute-Based Access Control) システムの例:

// Casbin など ABAC エンジンを利用
const authorizationEngine = {
  async checkAccess(subject, resource, action, context) {
    const policy = await this.evaluatePolicy({
      // ユーザー属性
      user: {
        id: subject.id,
        roles: subject.roles,
        department: subject.department,
        clearanceLevel: subject.clearanceLevel,
        lastTrainingDate: subject.lastTrainingDate,
        currentLocation: await geoService.getLocation(context.ip),
        riskScore: await riskEngine.getUserRiskScore(subject.id),
      },

      // リソース属性
      resource: {
        id: resource.id,
        classification: resource.dataClassification,
        owner: resource.owner,
        createdDate: resource.createdDate,
        lastModified: resource.lastModified,
        complianceFlags: await complianceService.getFlags(resource.id),
      },

      // 環境属性
      environment: {
        currentTime: new Date(),
        userAgent: context.userAgent,
        networkLocation: context.networkLocation,
        isBusinessHours: await businessCalendar.isBusinessHours(),
        threatLevel: await securityService.getCurrentThreatLevel(),
      },

      action: action,
    });

    return policy.decision;
  },
};

// 利用
const canAccess = await authorizationEngine.checkAccess(
  currentUser,
  requestedDocument,
  'read',
  requestContext,
);

運用オーバーヘッド

管理オーバーヘッド

RLS ポリシーを管理する DBA は、ポリシーライフサイクルのあらゆる段階で手作業が必要になり、組織のボトルネックになります。ポリシー作成には、安全な SQL ロジックを組み立て、性能影響とポリシー間依存を分析する PostgreSQL の深い知識が要ります。既存ポリシー管理は、適切なバージョニング、依存追跡、影響分析が無いまま、相互に絡む数百のポリシーが積み上がると、ますます複雑になります。ポリシーの廃止には、依存システムを壊さずに安全に削除するための関係を特定する、手作業の考古学が必要です。

開発者セルフサービスの使い勝手の限界

RLS まわりで開発者は摩擦に直面します — アクセス管理やテストのセルフサービスが無いからです。RLS は Just-in-Time のアクセス申請、緊急時のデバッグアクセス、一時的な権限のメカニズムを提供せず、重大インシデント時にも DBA の手作業に頼ることになります。開発体験の面でも、適切な認可ロジックを備えたローカルテスト環境の構築不可、認可失敗のデバッグの困難さ、アクセスシナリオ別のユニットテストの欠如、といった影響があります。

代替: ミドルウェア + ツール

DB レベルの RLS を使う代わりに、クエリを受け取りアプリ層でセキュリティポリシーを強制する認可ミドルウェア層を実装できます。

Open Policy Agent (OPA): ポリシーは Rego 言語で定義し、Git のようなバージョン管理で管理する。コードレビュー、自動テスト、CI/CD 統合といったベストプラクティスを取り入れられる。

Bytebase: Bytebase をデータベースアクセス管理プラットフォームとして使い、開発者にセルフサービスのアクセスポータルを提供する。承認ワークフローとポリシー生成を自動化し、DBA が手作業で一時権限を作成・取り消しする必要を取り除く。Bytebase は監査証跡、ブレークグラスの手順も提供し、既存の ID システムとも統合する。

まとめ

RLS は、tenant_id の隔離のような素直なデータフィルタには良く効きます — DB レベルの強制が、速く信頼できるセキュリティのベースラインを提供します。外部統合や複雑なビジネスロジックを必要としない、DB クエリに直接マップする単純な述語を当てるのに優れます。

ただし、本番グレードのマルチテナントサービスでは、層をなしたアプローチがよりスケールしやすいことが多いです。

  1. 基本的なテナント隔離は RLS tenant_id のようなフィールドに基づく、シンプルな行レベルアクセス制御を RLS で強制する。DB 内フィルタリングを最小の運用オーバーヘッドで効率良く実現する。

  2. 複雑なポリシーはアプリケーションロジック + OPA 高度な認可ニーズには、アプリ層に Open Policy Agent (OPA) のようなポリシーエンジンを統合する。OPA は SQL では表現できない動的ロジック — 時間ベースアクセス、ワークフロー状態、外部 API チェック、ユーザーロール — を扱いつつ、Git ベースのバージョン管理、自動テスト、ポリシーシミュレーションをサポートする。

  3. 人の Just-in-Time アクセスは Bytebase 本番トラブルシュートや緊急アクセスの運用シナリオには、Bytebase が自動承認ワークフロー、監査証跡、ブレークグラス手順を備えたセルフサービスポータルを提供する。DBA の手作業を取り除きながら、ガバナンスと追跡性を保てる。

ブログに戻る

データベース開発のスタンダードを体験する