RLSと認証によるマルチテナントシステムのセキュリティ
はじめに
スマートロック管理プラットフォームのようなセキュリティクリティカルなシステムでは、堅牢なアクセス制御が最重要です。複数の施設、ユーザー、ロールが安全なデータ分離を必要とする場合、PostgreSQLのRow-Level Security(RLS)と適切な認証パターンの組み合わせが強力な防御機構を提供します。本記事では、本番環境のスマートロックシステムから得られた高度なRLS実装パターンと認証強化技術について探求します。
Row-Level Security:基本的なポリシーを超えて
条件付きドロップによる安全なポリシー管理
RLS実装における課題の一つは、本番環境でのポリシー更新の処理です。重複したポリシーの作成を試みると、デプロイメントの失敗を引き起こす可能性があります:
-- 危険:ポリシーが存在する場合は失敗します
CREATE POLICY reservations_tenant_isolation ON reservations
FOR ALL TO authenticated
USING (facility_id = auth.get_current_facility_id());堅牢なアプローチでは、条件付きポリシー管理を使用します:
-- 安全なポリシー再作成パターン
DO $$
BEGIN
-- 既存のポリシーが存在する場合はドロップ
DROP POLICY IF EXISTS reservations_tenant_isolation ON reservations;
-- 新しいポリシーを作成
CREATE POLICY reservations_tenant_isolation ON reservations
FOR ALL TO authenticated
USING (facility_id = auth.get_current_facility_id());
END $$;マルチテーブルポリシーの協調
複雑なアプリケーションでは、複数のテーブル間での協調したポリシーが必要です。プラン、グループ、ユーザーロールを持つ予約システムを考えてみましょう:
-- 関連テーブルの協調RLSポリシー
DO $$
BEGIN
-- プランアクセス制御
DROP POLICY IF EXISTS plans_facility_access ON plans;
CREATE POLICY plans_facility_access ON plans
FOR ALL TO authenticated
USING (facility_id = auth.get_current_facility_id());
-- 階層アクセスを持つプラングループ
DROP POLICY IF EXISTS plan_groups_access ON plan_groups;
CREATE POLICY plan_groups_access ON plan_groups
FOR ALL TO authenticated
USING (
EXISTS (
SELECT 1 FROM plans p
WHERE p.group_id = plan_groups.id
AND p.facility_id = auth.get_current_facility_id()
)
);
END $$;認証セキュリティの強化
エラーレスポンスでの情報漏洩の排除
エラーレスポンスは、意図せず機密情報を漏洩する可能性があります。スタックトレースは特に本番環境では危険です:
// 脆弱:内部構造を露出
interface ErrorResponse {
message: string;
stack?: string; // 本番環境では危険
code: string;
}
// セキュア:サニタイズされたエラーレスポンス
interface SecureErrorResponse {
message: string;
code: string;
timestamp: string;
}
function sanitizeError(error: Error): SecureErrorResponse {
return {
message: error.message,
code: error.name,
timestamp: new Date().toISOString()
// スタックトレースは意図的に省略
};
}ロールベースセキュリティフラグパターン
データ関係から権限を推測するのではなく、明示的なセキュリティフラグを使用します:
// 脆弱:暗黙的な権限チェック
function hasAdminAccess(user: User): boolean {
return user.facilityId === null; // 脆弱な仮定
}
// セキュア:明示的な権限フラグ
interface SecureUser {
id: string;
facilityId: string | null;
isPlatformAdmin: boolean; // 明示的なセキュリティフラグ
roles: UserRole[];
}
function hasAdminAccess(user: SecureUser): boolean {
return user.isPlatformAdmin; // 明確で監査可能
}複雑な階層のための高度なRLSパターン
ネストしたリソースアクセス制御
ネストしたリソース(施設 → 予約 → アクセストークン)を持つシステムでは、階層化されたRLSを実装します:
-- ゲストトークンのマルチレベルアクセス制御
CREATE POLICY guest_tokens_secure_access ON guest_keyvox_tokens
FOR ALL TO authenticated
USING (
-- 直接的な施設所有権
facility_id = auth.get_current_facility_id()
OR
-- 予約を通じた間接的なアクセス
EXISTS (
SELECT 1 FROM reservations r
WHERE r.id = guest_keyvox_tokens.reservation_id
AND r.facility_id = auth.get_current_facility_id()
)
);監査安全なロール管理
ロール割り当てには追加のセキュリティ考慮事項が必要です:
-- 制限的なロール割り当てポリシー
CREATE POLICY role_members_insert_restriction ON role_members
FOR INSERT TO authenticated
WITH CHECK (
-- 施設管理者のみがロールを割り当て可能
auth.has_facility_permission('manage_roles')
AND
-- 現在のユーザーのレベルを超えて昇格不可
auth.get_role_level(role_id) <= auth.get_user_role_level()
AND
-- 同じ施設内でなければならない
EXISTS (
SELECT 1 FROM users u
WHERE u.id = user_id
AND u.facility_id = auth.get_current_facility_id()
)
);データベースマイグレーションセキュリティ
安全なスキーマ進化
本番環境でのデータベースマイグレーションには、慎重な順序とロールバック安全性が必要です:
-- ロールバック安全性を持つマイグレーション
BEGIN;
-- ステップ1:新しい構造の作成
CREATE TABLE IF NOT EXISTS new_permissions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
role_id uuid REFERENCES roles(id),
resource varchar(50) NOT NULL,
action varchar(50) NOT NULL,
UNIQUE(role_id, resource, action)
);
-- ステップ2:検証付きデータマイグレーション
INSERT INTO new_permissions (role_id, resource, action)
SELECT role_id, resource, action
FROM old_permissions
WHERE role_id IS NOT NULL -- データ検証
ON CONFLICT (role_id, resource, action) DO NOTHING;
-- ステップ3:RLSポリシーの適用
ALTER TABLE new_permissions ENABLE ROW LEVEL SECURITY;
CREATE POLICY new_permissions_access ON new_permissions
FOR ALL TO authenticated
USING (auth.has_role_access(role_id));
COMMIT;本番デプロイメント戦略
セキュリティを考慮したフィーチャーフラグ統合
フィーチャーフラグは既存のセキュリティ境界を尊重する必要があります:
interface SecurityAwareFeatureFlag {
key: string;
enabled: boolean;
requiredRole?: string;
facilityRestricted?: boolean;
}
class SecureFeatureManager {
async isEnabled(
flag: string,
user: SecureUser
): Promise<boolean> {
const feature = await this.getFeature(flag);
if (!feature.enabled) return false;
// ロール要件の尊重
if (feature.requiredRole &&
!user.roles.some(r => r.name === feature.requiredRole)) {
return false;
}
// 施設レベルの制限
if (feature.facilityRestricted &&
!this.hasFeatureAccess(user.facilityId, flag)) {
return false;
}
return true;
}
}モニタリングとコンプライアンス
RLSポリシー検証
RLSポリシーの自動テストを実装します:
describe('RLS Policy Security Tests', () => {
it('should isolate facility data', async () => {
const facility1User = await createTestUser({ facilityId: 'facility-1' });
const facility2User = await createTestUser({ facilityId: 'facility-2' });
// テストデータの作成
await createReservation({ facilityId: 'facility-1' });
await createReservation({ facilityId: 'facility-2' });
// 分離のテスト
const facility1Data = await getReservations(facility1User);
const facility2Data = await getReservations(facility2User);
expect(facility1Data).toHaveLength(1);
expect(facility2Data).toHaveLength(1);
expect(facility1Data[0].facilityId).toBe('facility-1');
expect(facility2Data[0].facilityId).toBe('facility-2');
});
});まとめ
マルチテナントシステムのセキュリティには、データベースレベルのRLSポリシー、アプリケーションレベルの認証強化、堅牢なデプロイメント実践を組み合わせた階層化されたアプローチが必要です。主要な原則には以下が含まれます:
- 安全なデプロイメントのための条件付きポリシー管理の使用
- 暗黙的なチェックではなく明示的なセキュリティフラグの実装
- 関連テーブル間での協調RLSポリシーの設計
- 情報漏洩を防ぐためのエラーレスポンスのサニタイズ
- 自動化されたRLS検証によるセキュリティ境界のテスト
- 既存のセキュリティコンテキスト内でのフィーチャーフラグの適用
これらのパターンは、ビジネス要件に応じてスケールしながらセキュリティの整合性を維持する信頼できるアクセス制御システムを構築するための基盤を提供します。