UnlockOS Developers
← 記事一覧に戻る
🔐

RLSポリシーによるマルチテナントスマートロック

2026年3月16日2026年3月22日
6
21 commits
深度 8/10
securitydatabasetenant-isolationrls-policies

Row-Level Securityポリシーによるマルチテナントスマートロックシステムの保護

はじめに

スマートロック管理システムにおいて、テナント分離は単なる機能ではなく、重要なセキュリティ要件です。データベースクエリの設定ミス一つで、ある物件のアクセスコードが他のテナントに露出する可能性があり、深刻なセキュリティ脆弱性を引き起こします。本記事では、Row-Level Security(RLS)ポリシーがアクセス制御システムにおけるマルチテナントセキュリティの堅牢な基盤をどのように提供するかを探ります。

マルチテナントセキュリティの課題

スマートロックシステムは、アクセスコード、ゲスト情報、物件詳細などの極めて機密性の高いデータを扱います。マルチテナント環境では、データベースに複数の物件と管理会社のデータが含まれるため、適切な分離が重要になります。

従来のアプリケーション層セキュリティは以下の場合に失敗する可能性があります:

  • 開発者がクエリにテナントチェックを追加し忘れた場合
  • 複雑なJOIN操作でテナントフィルターがバイパスされた場合
  • 管理機能が誤ってテナント横断データを露出した場合
  • セキュリティレビューでデータアクセスパターンのエッジケースを見落とした場合

堅牢なRLSポリシーの実装

Row-Level Securityは、テナント分離をアプリケーション層からデータベース自体に移し、データ漏洩に対する最後の防御線を作成します。

基本的なテナント分離ポリシー

-- 機密テーブルでRLSを有効化
ALTER TABLE access_codes ENABLE ROW LEVEL SECURITY;
ALTER TABLE guest_registrations ENABLE ROW LEVEL SECURITY;
ALTER TABLE property_configurations ENABLE ROW LEVEL SECURITY;

-- テナント分離のためのポリシーを作成
CREATE POLICY tenant_isolation_policy ON access_codes
  FOR ALL
  TO authenticated_users
  USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

高度なマルチロールポリシー

スマートロックシステムでは、物件管理者、スタッフ、システム管理者に対して異なるアクセスレベルが必要になることがよくあります:

-- 物件管理者のポリシー - 自分の物件への完全アクセス
CREATE POLICY property_manager_policy ON access_codes
  FOR ALL
  TO property_managers
  USING (
    tenant_id = current_setting('app.current_tenant_id')::uuid
    AND property_id IN (
      SELECT property_id 
      FROM manager_properties 
      WHERE manager_id = current_setting('app.current_user_id')::uuid
    )
  );

-- スタッフのポリシー - 割り当てられた物件への読み取り専用アクセス
CREATE POLICY staff_readonly_policy ON access_codes
  FOR SELECT
  TO property_staff
  USING (
    tenant_id = current_setting('app.current_tenant_id')::uuid
    AND property_id IN (
      SELECT property_id 
      FROM staff_assignments 
      WHERE staff_id = current_setting('app.current_user_id')::uuid
      AND is_active = true
    )
  );

セキュアなセッションのためのコンテキスト設定

RLSポリシーは、現在のテナントとユーザーコンテキストを決定するためにセッション変数に依存します。これは各データベースセッションの開始時に安全に設定される必要があります:

export class SecureSessionManager {
  async initializeSession(userId: string, tenantId: string): Promise<void> {
    // コンテキスト設定前にテナントメンバーシップを検証
    const membership = await this.validateTenantMembership(userId, tenantId);
    if (!membership.isValid) {
      throw new SecurityError('Invalid tenant access');
    }

    // セキュアなセッション変数を設定
    await this.db.query(`
      SELECT 
        set_config('app.current_user_id', $1, true),
        set_config('app.current_tenant_id', $2, true),
        set_config('app.user_role', $3, true)
    `, [userId, tenantId, membership.role]);
  }

  private async validateTenantMembership(userId: string, tenantId: string) {
    const result = await this.db.query(`
      SELECT role, is_active
      FROM tenant_memberships
      WHERE user_id = $1 AND tenant_id = $2 AND is_active = true
    `, [userId, tenantId]);

    return {
      isValid: result.rows.length > 0 && result.rows[0].is_active,
      role: result.rows[0]?.role
    };
  }
}

RLSポリシーの有効性テスト

堅牢なテストにより、RLSポリシーが異なるシナリオで期待通りに動作することを保証します:

describe('RLS Policy Security Tests', () => {
  it('should prevent cross-tenant data access', async () => {
    // 異なるテナント用のテストデータを設定
    const tenantA = await createTestTenant();
    const tenantB = await createTestTenant();
    
    const accessCodeA = await createAccessCode(tenantA.id);
    const accessCodeB = await createAccessCode(tenantB.id);

    // テナントAの分離をテスト
    await withTenantContext(tenantA.id, async (db) => {
      const codes = await db.query('SELECT * FROM access_codes');
      
      expect(codes.rows).toHaveLength(1);
      expect(codes.rows[0].id).toBe(accessCodeA.id);
      expect(codes.rows.find(r => r.id === accessCodeB.id)).toBeUndefined();
    });
  });

  it('should enforce role-based access restrictions', async () => {
    const tenant = await createTestTenant();
    const property1 = await createProperty(tenant.id);
    const property2 = await createProperty(tenant.id);
    
    // property1のみに割り当てられたスタッフ
    const staffUser = await createStaffUser(tenant.id, [property1.id]);

    await withUserContext(staffUser.id, tenant.id, async (db) => {
      const accessibleCodes = await db.query(
        'SELECT * FROM access_codes WHERE property_id = ANY($1)',
        [[property1.id, property2.id]]
      );
      
      // 割り当てられた物件のコードのみが表示されるはず
      accessibleCodes.rows.forEach(code => {
        expect(code.property_id).toBe(property1.id);
      });
    });
  });
});

パフォーマンスの考慮事項

RLSポリシーは、特に複雑なテナント階層においてクエリパフォーマンスに影響を与える可能性があります。適切なインデックス作成で最適化します:

-- 効率的なポリシー実行のための複合インデックス
CREATE INDEX idx_access_codes_tenant_property 
  ON access_codes (tenant_id, property_id);

CREATE INDEX idx_manager_properties_lookup
  ON manager_properties (manager_id, property_id)
  WHERE is_active = true;

-- ポリシーがインデックスを使用することを確認するためクエリプランを分析
EXPLAIN (ANALYZE, BUFFERS) 
SELECT * FROM access_codes 
WHERE property_id = '123e4567-e89b-12d3-a456-426614174000';

データベースクリーンアップとセキュリティメンテナンス

定期的なデータベースメンテナンスには、セキュリティリスクを引き起こす可能性のある未使用のインデックスとテストテーブルの削除が含まれます:

-- スキーマ情報を漏洩させる可能性のある未使用インデックスを削除
DROP INDEX IF EXISTS old_test_index;
DROP INDEX IF EXISTS unused_performance_index;

-- 本番環境からテストテーブルを削除
DROP TABLE IF EXISTS test_access_codes CASCADE;
DROP TABLE IF EXISTS debug_tenant_data CASCADE;

-- 既存のポリシーを監査
SELECT schemaname, tablename, policyname, permissive, roles, cmd, qual
FROM pg_policies
WHERE schemaname = 'public'
ORDER BY tablename, policyname;

まとめ

Row-Level Securityポリシーは、マルチテナントスマートロックシステムにとって不可欠な多層防御を提供します。データベースレベルでテナント分離を実装することで、システムはアプリケーション層のセキュリティバグに対して回復力を持ち、データアクセスに関する強力な保証を提供します。適切なセッション管理、包括的なテスト、定期的なセキュリティ監査と組み合わせることで、RLSポリシーは信頼できるアクセス制御システムの基盤を形成します。

効果的なRLS実装の鍵は、それをアプリケーションレベルのアクセス制御を置き換えるものではなく、補完するセキュリティ層として扱うことにあります。この多層アプローチにより、アプリケーションロジックが失敗した場合でも、データベース自体が不正なデータアクセスを防ぎます。

主要な発見

1
セキュリティ

データベースレベルのテナント分離

RLSポリシーは、アプリケーションロジックから独立してデータベースレベルで分離を実行し、テナント間データ漏洩に対する最後の防御線を提供します。

2
セキュリティ

セッションコンテキストの検証

セキュアなセッション管理には、RLSポリシーの決定を駆動するデータベースコンテキスト変数を設定する前に、テナントメンバーシップの検証が必要です。

3
テスト

包括的なRLSテスト

セキュリティテストでは、異なるユーザーコンテキストとシナリオ間でテナント分離とロールベースアクセス制限の両方を検証する必要があります。

4
パフォーマンス

RLS用のインデックス戦略

tenant_idと関連フィールドの複合インデックスは、RLSポリシーが実行される際のクエリパフォーマンス維持に不可欠です。