UnlockOS Developers
← 記事一覧に戻る
🔐

セキュアなマルチテナントシステムの構築: RLS & 認証強化

2026年5月11日2026年5月17日
6
18 commits
深度 8/10
securityauthenticationrow-level-securitymulti-tenant

セキュアなマルチテナントシステムの構築: Row-Level Securityと認証強化

はじめに

スマートロック管理プラットフォームのようなセキュリティクリティカルなシステムでは、堅牢なマルチテナントセキュリティの実装が最重要です。認証とデータアクセスパターンの最近の改善により、データ漏洩の防止と適切なテナント分離を確保するための主要戦略が実証されました。この記事では、Row-Level Security(RLS)、ステートレス認証クライアント、リクエスト間汚染の防止などの重要なセキュリティパターンについて探求します。

Row-Level Security: マルチテナントデータ保護の基盤

Row-Level Securityは、ユーザーコンテキストに基づいてデータを自動的にフィルタリングすることで、データベースレベルのテナント分離を提供します。包括的なRLSポリシーの実装方法は以下の通りです:

-- Create user context functions for policy enforcement
CREATE OR REPLACE FUNCTION current_user_is_admin()
RETURNS boolean AS $$
BEGIN
  RETURN EXISTS (
    SELECT 1 FROM user_roles 
    WHERE user_id = auth.uid() 
    AND role = 'admin'
  );
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- Apply RLS policies before enabling protection
CREATE POLICY "Admin access to checkin_configurations"
  ON checkin_configurations
  FOR ALL
  TO authenticated
  USING (current_user_is_admin());

-- Enable RLS only after policies are in place
ALTER TABLE checkin_configurations ENABLE ROW LEVEL SECURITY;

ここでの重要なセキュリティ原則はポリシー優先の有効化です:row-level securityを有効にする前に、必ずRLSポリシーを作成してください。これにより、RLSが有効だがポリシーが存在しない危険な状況を防ぎ、すべてのアクセスがブロックされる可能性を回避します。

認証状態汚染の防止

マルチテナントシステムでは、リクエスト間での認証状態漏洩が深刻なセキュリティ脆弱性につながる可能性があります。ステートレス認証クライアントの実装方法は以下の通りです:

// Secure stateless client configuration
const createStatelessSupabaseClient = () => {
  return createClient(supabaseUrl, supabaseKey, {
    auth: {
      // Critical: disable persistent sessions in stateless contexts
      persistSession: false,
      autoRefreshToken: false,
      // Prevent auth state from persisting across requests
      storage: {
        getItem: () => null,
        setItem: () => {},
        removeItem: () => {}
      }
    }
  });
};

// Proper error handling to prevent request contamination
async function handleAuthRequest(request: Request): Promise<Response> {
  let requestBody: any;
  
  try {
    requestBody = await request.json();
  } catch (error) {
    // Critical: drain the request stream to prevent test cross-contamination
    await request.text().catch(() => {});
    return new Response('Invalid JSON', { status: 400 });
  }
  
  // Process with clean auth state
  return processAuthenticatedRequest(requestBody);
}

重要な洞察はリクエスト分離です:各リクエストはクリーンな認証状態から開始する必要があります。特に、テストケース間でリクエスト状態が漏洩する可能性があるテスト環境では重要です。

セキュリティクエリのデータベース最適化

セキュリティポリシーは、パフォーマンスに影響を与える可能性がある複雑なクエリを含むことがよくあります。戦略的なインデックス作成により、セキュリティがシステムの応答性を損なうことがないようになります:

-- Index organization relationships for efficient RLS queries
CREATE INDEX CONCURRENTLY idx_facilities_organization_id 
  ON facilities(organization_id);

-- Index user permissions for fast policy evaluation
CREATE INDEX CONCURRENTLY idx_user_permissions_lookup 
  ON user_permissions(user_id, resource_type, organization_id);

-- Compound index for multi-tenant filtering
CREATE INDEX CONCURRENTLY idx_checkin_configs_tenant_filter 
  ON checkin_configurations(organization_id, facility_id) 
  WHERE deleted_at IS NULL;

セキュアなデータフィルタリングパターン

適切なソフトデリート処理の実装により、アーカイブされた機密データの露出を防止します:

// Secure data filtering with proper type safety
interface SecureQueryOptions {
  organizationId: string;
  includeDeleted?: boolean;
  userPermissions: UserPermission[];
}

class SecureDataAccess {
  async getUnits(options: SecureQueryOptions): Promise<Unit[]> {
    // Validate user has access to organization
    this.validateOrganizationAccess(options.organizationId, options.userPermissions);
    
    const query = this.db
      .from('units')
      .select('*')
      .eq('organization_id', options.organizationId);
    
    // Security-first: exclude soft-deleted by default
    if (!options.includeDeleted) {
      query.is('deleted_at', null);
    }
    
    const { data, error } = await query;
    
    if (error) {
      // Audit security-related query failures
      await this.auditLog.logSecurityEvent({
        type: 'DATA_ACCESS_FAILURE',
        organizationId: options.organizationId,
        error: error.message,
        timestamp: new Date()
      });
      throw new SecurityError('Data access denied');
    }
    
    return data || [];
  }
  
  private validateOrganizationAccess(
    orgId: string, 
    permissions: UserPermission[]
  ): void {
    const hasAccess = permissions.some(
      p => p.organizationId === orgId && p.resource === 'units'
    );
    
    if (!hasAccess) {
      throw new UnauthorizedError(`Access denied to organization ${orgId}`);
    }
  }
}

環境ベースのセキュリティ設定

セキュリティ設定は、一貫した保護を維持しながら、異なるデプロイメント環境に適応する必要があります:

// Environment-aware security configuration
interface SecurityConfig {
  emailSender: string;
  authDomain: string;
  sessionTimeout: number;
  auditLevel: 'minimal' | 'standard' | 'verbose';
}

const getSecurityConfig = (): SecurityConfig => {
  const env = process.env.NODE_ENV;
  
  const configs: Record<string, SecurityConfig> = {
    production: {
      emailSender: 'noreply@secure.unlockos.com',
      authDomain: 'auth.unlockos.com',
      sessionTimeout: 3600000, // 1 hour
      auditLevel: 'verbose'
    },
    staging: {
      emailSender: 'noreply@staging.unlockos.com',
      authDomain: 'auth.staging.unlockos.com', 
      sessionTimeout: 7200000, // 2 hours
      auditLevel: 'standard'
    },
    development: {
      emailSender: 'noreply@dev.unlockos.com',
      authDomain: 'localhost:3000',
      sessionTimeout: 86400000, // 24 hours
      auditLevel: 'minimal'
    }
  };
  
  return configs[env] || configs.development;
};

セキュリティ境界のテスト

End-to-Endテストは、テナント分離が実際に機能することを確実にするために、セキュリティ境界を検証する必要があります:

// Security-focused E2E test patterns
describe('Multi-tenant Security', () => {
  test('should prevent cross-tenant data access', async () => {
    // Setup: Create two separate tenant contexts
    const tenant1Client = await createTenantClient('org-1');
    const tenant2Client = await createTenantClient('org-2');
    
    // Create data in tenant 1
    const facility1 = await tenant1Client.createFacility({
      name: 'Secure Building A',
      organizationId: 'org-1'
    });
    
    // Attempt cross-tenant access with tenant 2 credentials
    await expect(
      tenant2Client.getFacility(facility1.id)
    ).rejects.toThrow('Access denied');
    
    // Verify audit log captures the attempt
    const auditLogs = await getAuditLogs({
      type: 'UNAUTHORIZED_ACCESS_ATTEMPT',
      resourceId: facility1.id
    });
    
    expect(auditLogs).toHaveLength(1);
    expect(auditLogs[0].organizationId).toBe('org-2');
  });
  
  test('should handle authentication state isolation', async () => {
    // Verify no session bleeding between test runs
    const client1 = createStatelessSupabaseClient();
    const client2 = createStatelessSupabaseClient();
    
    await client1.auth.signInWithPassword({
      email: 'user1@example.com',
      password: 'secure123'
    });
    
    // Client 2 should not inherit client 1's auth state
    const { data: user } = await client2.auth.getUser();
    expect(user.user).toBeNull();
  });
});

まとめ

セキュアなマルチテナントシステムには、複数のレイヤーで動作する多層防御戦略が必要です。ここで実証された重要な原則は以下の通りです:

  1. データベースレベルのセキュリティ - 適切に設定されたRow-Level Securityポリシーを通じて
  2. ステートレス認証 - リクエスト間のセッション汚染を防止
  3. 戦略的インデックス作成 - セキュリティポリシーを強制しながらパフォーマンスを維持
  4. 環境対応設定 - デプロイメントコンテキスト間でセキュリティを維持
  5. 包括的テスト - 現実的なシナリオでセキュリティ境界を検証

これらのパターンを実装することで、セキュリティクリティカルなシステムは、プロフェッショナルなアクセス管理プラットフォームにユーザーが期待するパフォーマンスと信頼性を提供しながら、堅牢なテナント分離を維持できます。

主要な発見

1
セキュリティ

ポリシー優先のRLS実装

危険なアクセス期間を防ぐため、RLSを有効にする前に必ずRow-Level Securityポリシーを作成する

2
認証

ステートレスクライアントアーキテクチャ

ステートレスコンテキストでセッション永続化を無効にして認証状態汚染を防止する

3
パフォーマンス

セキュリティ最適化インデックス

戦略的なデータベースインデックスにより、セキュリティクエリがシステム応答性を損なわないようにする

4
テスト

セキュリティ境界検証

E2Eテストでテナント分離を検証し、不正アクセス試行を監査する必要がある