Isolation multi-tenant : comment les fuites entre clients arrivent
Pourquoi les fuites entre clients viennent rarement de la requête SQL et presque toujours des copies de la donnée (cache, files, index de recherche, stockage objet), et où poser le tenant pour qu'un oubli ne soit plus possible.
Par Elias VarenMis à jour le 9 min de lectureCompte gratuit
Prenons un scénario. Un client écrit qu'en ouvrant son tableau de bord il a vu, pendant une seconde, le nom d'une société qu'il ne connaît pas. Dans le repo, chaque requête du module porte son WHERE tenant_id = ?. La revue de code les a toutes vues passer, le test d'isolation est vert. Et pourtant.
La fuite ne vient pas de la base. Elle vient d'une entrée de cache dont la clé est dashboard:summary, écrite par le premier tenant qui a chargé la page après le déploiement, et servie à tous les autres pendant cinq minutes.
Dans un SaaS à base partagée, la requête SQL est l'endroit le mieux gardé, parce que tout le monde y pense. Les fuites arrivent dans les copies de la donnée, tout ce qui la met en cache, la met en file, l'indexe, l'exporte ou l'écrit dans les logs. Chacune de ces copies a quitté la table où vivait la colonne tenant_id, et ne sait plus à qui elle appartient si personne ne le lui a redit.
Un contexte ambiant qui ne voyage pas
Dans une requête HTTP, le tenant est un contexte ambiant. Il est résolu une fois, à l'entrée, à partir du nom d'hôte ou du token, puis il flotte ; un service le porte, les repositories le lisent. Ce confort est justement la source du problème, car le contexte ambiant ne traverse aucune frontière tout seul.
requête HTTP ──► [tenant résolu] ──► SQL ✔ filtré
│
├──► clé de cache ? si on y a pensé
├──► message en file ? s'il est dans le corps
├──► document d'index ? s'il est dans le filtre
├──► chemin de fichier ? s'il est dans le préfixe
└──► ligne de journal, export ? …À chaque flèche, quelqu'un doit recopier le tenant à la main. La probabilité qu'un développeur l'oublie sur une flèche donnée est faible, mais le nombre de flèches augmente à chaque fonctionnalité. L'isolation par convention se dégrade donc mécaniquement avec la taille du code, et la seule réponse durable consiste à rendre l'oubli impossible à écrire, surface par surface.
Sources et historique
- PostgreSQL 17 — Row Security Policies
- PgBouncer — Features (SQL feature map for pooling modes)
- RFC 9111 — HTTP Caching
- Next.js 16 — unstable_cache
Vérification technique : 6 octobre 2026. Une erreur ? Voici comment elle est corrigée.
Le dossier complet : SaaS multi-tenant de bout en bout
À lire ensuite
Toute la rubrique ArchitectureArchitecture
Migrations par tenant : mille schémas ou une colonne
Ce que coûte, en exploitation, chacun des trois modèles de base multi-tenant (colonne partagée, schéma par tenant, base par tenant) le jour d'une migration, d'une restauration et d'un départ de client, et le critère pour choisir.
9 minCompte gratuit
Architecture
Le voisin bruyant : quotas, équité et facturation de l'usage
Pourquoi une limite globale ne protège aucun client, comment répartir une ressource partagée entre tenants, et pourquoi le compteur qui limite n'est jamais celui qui facture.
10 minLecture libre
Sécurité
Row-level security PostgreSQL : garde-fou ou piège
Ce que la row-level security filtre vraiment, les rôles et les objets qui passent au travers, ce qu'elle coûte au planificateur, et le critère pour en faire un second filet plutôt que votre seule isolation.
8 minLecture libre