LESSON 25分

ストーリー

高橋アーキテクト
SaaSを設計するなら、マルチテナントのセキュリティは避けて通れない

高橋アーキテクトが複数企業のロゴを並べました。

高橋アーキテクト
A社のデータがB社に見えてはいけない。当然のことだが、設計を誤ると簡単にデータ漏洩が起きる。テナント分離は、マルチテナントシステムの最重要セキュリティ要件だ

テナント分離のモデル

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分