ADR-002: PostgreSQL RLS によるマルチテナント分離
- ステータス: Accepted
- 日付: 2025-12-16
- 決定者: アーキテクトチーム
コンテキスト
TASHIKA は SaaS プラットフォームとして複数のテナントのデータを管理する。エンティティ階層は Agency(代行事業者)→ Tenant(テナント)→ Employee(従業員)の3層構造であり、テナント間のデータ分離は最重要のセキュリティ要件である(→ SR-004 認可要件)。
特に以下の点がテナント分離の設計を左右する:
- マイナンバー(個人番号)の保護: テナント間でのマイナンバー漏洩は法的に重大な問題となる(→ CR-011 マイナンバー法対応)
- 7年間のデータ保持: 税務データの法定保持期間中、テナント分離が維持されなければならない
- 代理ログイン(Impersonation): システム管理者がテナントのデータにアクセスする際にも分離が維持される必要がある
- 代行事業者の横断アクセス: Agency は複数の Tenant を管理するが、各 Tenant のデータは分離されたまま横断的に操作できる必要がある
決定
PostgreSQL の Row Level Security (RLS) と EF Core の Global Query Filter を併用する二重防御方式でテナント分離を実現する。
- RLS: データベースレベルでテナント分離を強制(
SET app.current_tenant_idをコネクションごとに設定) - Global Query Filter: アプリケーションレベルでテナントフィルタを自動適用(
Where(e => e.TenantId == currentTenantId))
理由
- 構造的安全性(Defense in Depth): アプリケーションにバグがあっても、RLS により他テナントのデータは DB レベルで不可視となる。Global Query Filter はアプリケーション側の追加防御層として機能する
- マイナンバー保護の要件: マイナンバーはアプリケーションレベル暗号化(AES-256)で保護されるが、暗号化前のクエリ段階でテナント分離が破れていた場合、暗号化されたデータ自体が漏洩する。RLS はクエリ段階での分離を保証する
- 監査ログのテナント分離: 監査ログ(→ DM-901 AuditLog)もテナントスコープであり、RLS でフィルタされる。企業管理者が自テナントの監査ログのみを参照できることを構造的に保証する
- 共有スキーマの運用効率: テナントごとに DB を分離する方式と比較し、スキーマ管理・マイグレーション・バックアップの運用コストが低い
- 代行事業者の横断アクセスとの両立: Agency ユーザーは管理下の複数 Tenant に順次コンテキストスイッチして操作する。RLS のセッション変数
app.current_tenant_idを切り替えることで、横断操作と分離を両立できる
却下した代替案
| 代替案 | 却下理由 |
|---|---|
| テナント別データベース | スキーマ管理・マイグレーション・コネクション管理のコストが高い。MVP 規模(100テナント)では過剰。代行事業者の横断アクセス実装が複雑化する |
| テナント別スキーマ | データベース別と同様の管理コスト。PostgreSQL のスキーマ数上限は実質的に問題ないが、マイグレーションの複雑性が増す |
| アプリケーション層のみでのフィルタリング(RLS なし) | WHERE 句の追加漏れによるデータ漏洩リスクがある。セキュリティを開発者の注意力に依存する設計は受け入れられない |
トレードオフ・リスク
- コネクションプーリングの制約: RLS セッション変数
app.current_tenant_idをコネクションごとに設定・リセットする必要がある。コネクション返却時にRESET ALLを発行してセッション変数をクリアし、テナントコンテキストの漏洩を防止する(→ operational-architecture.md) - RLS のパフォーマンスオーバーヘッド: 全クエリに暗黙のフィルタが追加されるため、わずかなオーバーヘッドがある。ただし
tenant_idカラムへのインデックスにより実用上問題ない水準に抑制できる .IgnoreQueryFilters()の危険性: EF Core の Global Query Filter をバイパスする.IgnoreQueryFilters()の使用は、明示的な正当化理由のコメントを必須とする。コードレビューで検出する- バッチジョブでのテナントコンテキスト: 複数テナントを処理するバッチジョブ(PDF一括生成等)では、テナントごとにコンテキストを切り替える必要がある。専用の
DbContext(別コネクションプール)を使用し、API用プールを圧迫しない設計とする
結果
- 全テナントスコープエンティティに
TenantIdプロパティを必須化 - EF Core
DbConnectionInterceptor.ConnectionOpenedAsync()でSET app.current_tenant_idを自動設定 - 新規テナントスコープテーブル追加時は、マイグレーション内で RLS ポリシーを同時設定
- RLS ポリシーの欠落を CI で自動検出する仕組みを整備
docs/design/security-design.mdに RLS + Global Query Filter の設計詳細を記載