SaaS や複数顧客向けの業務システムで、最も起こしてはいけない事故があります。「A 社のログイン画面に、B 社のデータが表示される」——いわゆるテナント越境です。情報漏えいとして即・重大インシデントになり、原因の多くは派手なものではありません。クエリの WHERE 句に tenant_id を付け忘れた、たった一行。それだけで、本来見えてはいけないデータが画面に出ます。
この問題を正面から扱った記事として、アプリ層と DB 層の二重防御でテナント分離を担保する(Zenn・counterworks) が参考になります。結論はシンプルで、「アプリ層だけ」でも「DB 層だけ」でもなく、両方で守ること。受託でマルチテナントなシステムを引き受ける立場では、これは「ベストプラクティス」ではなく「事故を一回でも起こしたら終わる」前提条件だと考えています。
なぜ「アプリ層のフィルタ」はいつか破れるのか
マルチテナント分離の基本は、すべてのクエリにテナントの境界条件を付けることです。多くのシステムは、これをアプリケーション層(ORM のスコープや共通の WHERE 条件)で実現しています。普段はこれで動きます。問題は、「付け忘れる経路」が無数にあることです。
- 新しく追加した管理画面の集計クエリで、共通スコープを通さず生 SQL を書いた
- バッチ処理や CSV エクスポートが、通常の API とは別経路でデータを引いた
- ORM の
JOINの途中で、片方のテーブルにだけ条件が効いていなかった - 「全社横断レポート」機能を足したとき、境界条件を一時的に外して戻し忘れた
人間が毎回・全経路で漏れなく tenant_id を付け続けるのは、規模が大きくなるほど非現実的です。アプリ層の防御は「正しく書けば守れる」けれど、「一度でも書き漏らせば破れる」。この非対称性こそが、テナント越境が後を絶たない理由です。論理削除の付け忘れで「消したはずのデータが見える」事故と構造はよく似ており、状態モデルの設計思想は 「論理削除」をやめる DB 設計(GH Media) でも扱いました。
二重防御 — DB 層に「最後の砦」を置く
そこで効くのが、DB 層にもう一枚の防御を置く考え方です。PostgreSQL であれば Row Level Security(RLS、行レベルセキュリティ)を使い、「現在のテナント以外の行は、そもそも DB が返さない」状態を作れます。アプリが万一 WHERE を付け忘れても、DB が境界の外の行を遮断する。これが「最後の砦」になります。
| 層 | 役割 | 強み | 弱み |
|---|---|---|---|
| アプリ層 | 通常のクエリにテナント条件を付与 | 柔軟・高速、業務ロジックと一体 | 付け忘れに弱い(一行で破れる) |
| DB 層(RLS) | テナント外の行を DB が遮断 | 付け忘れても漏れない最後の砦 | 設定ミス・接続単位の文脈管理が必要 |
考え方は「どちらか」ではなく「両方」です。アプリ層は普段の正しさと性能を担い、DB 層は人間のミスを吸収する保険として効かせる。セキュリティの基本である多層防御(defense in depth)を、テナント分離に適用したものだと捉えてください。Web 全般のセキュリティの土台は Web サイトのセキュリティ対策入門(GH Media) も併せて参考になります。
受託でどう組むか — 「事故が起きても漏れない」を作る
弊社の受託では、マルチテナントなシステムを「正しく書けば安全」ではなく、「ミスが起きても漏れない」水準で設計します。具体的には次の順で進めます。
接続単位でテナントの文脈を確実に渡す
RLS が効くかどうかは、「いまどのテナントとして DB に接続しているか」をリクエストごとに正しくセットできるかにかかっています。受託では、認証から DB セッションまでの間でテナント文脈が途切れない経路を作り、ここを共通基盤として固めます。あるクラウド管理システムの案件では、リクエストの入口で一度だけテナントを確定し、以降のすべての DB アクセスがその文脈の下で走るように統一したことで、「付け忘れても境界の外は返らない」状態を担保できました。
越境を「テストで検知できる」状態にする
二重防御を入れても、設定が正しいかは検証しなければ分かりません。受託では、「テナント A の認証で、テナント B のデータを取りに行ったら必ず空(または拒否)になる」ことを自動テストとして書き、CI の合格条件に組み込みます。これにより、新しい機能や生 SQL を足したときに越境が再発しないかを機械的に守れます。テストを「実行しただけ」で終わらせない品質の考え方は Mutation Testing で納品物のテスト品質を保証する受託(GH Media) と地続きです。
「全社横断」など例外経路を設計に組み込む
実務では、運用管理者向けに「全テナント横断のレポート」が必要になる場面が必ず出ます。ここを場当たりで境界条件を外すと事故の温床になるため、例外経路を最初から設計に含め、誰がどの条件で越境参照できるかを明示的に分ける。例外を「裏口」ではなく「正規の管理機能」として作るのが、受託で引き渡す品質です。
ハマりやすい落とし穴
第一に、RLS を入れただけで安心してしまうこと。接続単位のテナント文脈の管理を誤ると、RLS が意図どおり効かなかったり、逆に正規のアクセスまで止めたりします。設定の検証テストまでがワンセットです。
第二に、性能を理由に DB 層防御を外すこと。RLS は適切なインデックス設計と併用すれば実用的な性能で運用できます。性能を理由に最後の砦を外すのは、事故の確率と引き換えにする割に合わない選択です。引き渡し後に「正常」を定義して監視する考え方は 監視は「正常」を先に定義する(GH Media) を参照してください。
第三に、アプリ層をサボってよいと誤解すること。DB 層はあくまで保険で、普段の正しさと性能はアプリ層が担います。二重防御は「両方ちゃんとやる」が前提です。
まとめ — 一行の付け忘れで終わらせない設計へ
テナント越境は、派手なハッキングではなく「WHERE 句の一行の漏れ」から起きます。アプリ層のフィルタは正しく書けば守れますが、一度でも書き漏らせば破れる。だからこそ、アプリ層に普段の正しさを、DB 層(RLS)に「ミスが起きても漏れない最後の砦」を置く二重防御が要ります。受託で引き受けるなら、接続単位のテナント文脈を固め、越境を自動テストで検知し、例外経路を正規の機能として設計する——この 3 つから始めるのが、事故を一回も起こさないための現実的な一手です。
「マルチテナント SaaS のテナント分離が今の設計で大丈夫か診断したい」「越境事故が起きない作りに作り直したい」というご相談は お問い合わせフォーム からどうぞ。既存システムの分離方式の棚卸しから着手できます。