ストーリー
高橋アーキテクトが複数企業のロゴを並べました。
テナント分離のモデル
1. データベース分離
// モデル1: テナントごとに別データベース
// メリット: 完全な分離、カスタマイズ容易
// デメリット: コスト高、管理が煩雑
const getDbConnection = (tenantId: string) => {
return new Pool({
host: `${tenantId}.db.example.com`,
database: `tenant_${tenantId}`,
// テナントごとに異なる接続情報
});
};
2. スキーマ分離
// モデル2: 同一DBでテナントごとにスキーマを分離
// メリット: 適度な分離、コスト中程度
// デメリット: スキーマ管理の手間
const getSchemaName = (tenantId: string): string => {
return `tenant_${tenantId}`;
};
const query = async (tenantId: string, sql: string, params: unknown[]) => {
const schema = getSchemaName(tenantId);
await db.query(`SET search_path TO ${schema}`);
return db.query(sql, params);
};
3. 行レベル分離(共有テーブル)
// モデル3: 同一テーブルで tenant_id カラムで分離
// メリット: コスト低、シンプル
// デメリット: 実装ミスでデータ漏洩のリスク
// Prismaの例: 全クエリにtenantIdフィルターを自動適用
const prismaWithTenant = (tenantId: string) => {
return prisma.$extends({
query: {
$allOperations({ args, query }) {
// 全クエリにtenantIdを自動付与
if (args.where) {
args.where.tenantId = tenantId;
} else {
args.where = { tenantId };
}
return query(args);
},
},
});
};
// PostgreSQLのRow Level Security (RLS)
// CREATE POLICY tenant_isolation ON users
// USING (tenant_id = current_setting('app.current_tenant')::uuid);
テナント分離モデルの比較
| 項目 | DB分離 | スキーマ分離 | 行レベル分離 |
|---|---|---|---|
| 分離レベル | 最高 | 高 | 中 |
| コスト | 高 | 中 | 低 |
| スケーラビリティ | テナント数に制限 | 中程度 | 高 |
| カスタマイズ性 | 高 | 中 | 低 |
| 運用複雑度 | 高 | 中 | 低 |
| データ漏洩リスク | 最低 | 低 | 実装次第 |
テナントコンテキストの伝播
全てのレイヤーでテナントIDを正しく伝播させることが重要です。
// テナントコンテキストミドルウェア
const tenantContext = async (
req: Request,
res: Response,
next: NextFunction,
) => {
// 1. テナントIDの特定(サブドメイン、ヘッダー、JWTから)
const tenantId =
extractFromSubdomain(req) ||
req.headers["x-tenant-id"] as string ||
req.user?.tenantId;
if (!tenantId) {
return res.status(400).json({ error: "Tenant not identified" });
}
// 2. テナントの存在確認
const tenant = await tenantRepo.findById(tenantId);
if (!tenant || !tenant.isActive) {
return res.status(403).json({ error: "Invalid tenant" });
}
// 3. コンテキストに設定
req.tenantId = tenantId;
next();
};
// クロステナントアクセスの防止
const preventCrossTenantAccess = async (
req: Request,
res: Response,
next: NextFunction,
) => {
if (req.user.tenantId !== req.tenantId) {
// ユーザーのテナントとリクエストのテナントが不一致
await auditLog.alert({
type: "CROSS_TENANT_ACCESS_ATTEMPT",
userId: req.user.id,
userTenant: req.user.tenantId,
requestedTenant: req.tenantId,
});
return res.status(403).json({ error: "Forbidden" });
}
next();
};
テナントセキュリティのチェックポイント
| チェック項目 | 説明 |
|---|---|
| テナントID検証 | 全リクエストでテナントIDを検証 |
| クエリフィルター | 全DBクエリにテナントIDフィルターを適用 |
| APIレスポンス | レスポンスに他テナントのデータが含まれないことを確認 |
| ファイルストレージ | テナント別のパスでファイルを分離 |
| キャッシュ | キャッシュキーにテナントIDを含める |
| ログ | ログにテナントIDを含めて分析可能にする |
| バックアップ | テナント別のバックアップ・復元が可能 |
まとめ
| ポイント | 内容 |
|---|---|
| DB分離 | 最高のセキュリティ、高コスト |
| 行レベル分離 | 低コスト、RLS等で安全に実装可能 |
| コンテキスト伝播 | 全レイヤーでテナントIDを正しく伝播 |
| 防御策 | クロステナントアクセスの検知・防止 |
チェックリスト
- 3つのテナント分離モデルを理解した
- 行レベルセキュリティ(RLS)の概念を把握した
- テナントコンテキストの伝播方法を理解した
- クロステナントアクセス防止の重要性を認識した
次のステップへ
次は演習です。ここまで学んだ認証・認可の知識を使って、実際にシステムの認証・認可アーキテクチャを設計しましょう。
推定読了時間: 25分