Multitenant — Isolation et Sécurité
Vue d'ensemble
L'architecture multitenant permet de servir plusieurs clients (tenants) avec une seule instance applicative. L'isolation et la sécurité sont critiques.
Principes :
- ✅ Isolation stricte des données entre tenants
- ✅ Choix du tier d'isolation selon le profil client
- ✅ RBAC/ABAC par tenant pour le contrôle d'accès fin
- ✅ Chiffrement field-level pour données sensibles
- ✅ Protection contre les fuites inter-tenants
Table des matières
- Approche Tiered
- Tier 1 : Shared Schema
- Tier 2 : Dedicated Schema
- Tier 3 : Dedicated Database
- RBAC/ABAC par tenant
- Field-Level Encryption
- Sécurité et validation
- Checklist
Approche Tiered
Source : Multi-Tenant Architecture Guide
Une approche tiered permet d'adapter le niveau d'isolation au profil du tenant (startup, SMB, enterprise).
| Tier | Isolation | Coût | Complexité | Cas d'usage |
|---|---|---|---|---|
| Tier 1 | Shared schema avec tenant_id |
Faible | Faible | Startups, petits clients |
| Tier 2 | Dedicated schema par tenant | Moyen | Moyen | SMB, clients sensibles |
| Tier 3 | Dedicated DB par tenant | Élevé | Élevé | Enterprise, compliance stricte |
Migration inter-tier
Les tenants peuvent migrer d'un tier à l'autre selon leur croissance :
Startup (Tier 1)
↓ Croissance
SMB (Tier 2)
↓ Enterprise
Enterprise (Tier 3)
Stratégie : Prévoir des scripts de migration automatisés entre tiers.
Tier 1 : Shared Schema
Principe
Tous les tenants partagent la même base de données et le même schéma. Isolation via colonne tenant_id.
Avantages
- ✅ Faible coût (une seule DB)
- ✅ Maintenance simplifiée
- ✅ Scaling horizontal facile
Inconvénients
- ❌ Risque de fuite de données (SQL injection, bugs)
- ❌ Performance partagée (un tenant peut impacter les autres)
- ❌ Compliance difficile (GDPR, HIPAA)
Implémentation
Symfony (Doctrine) :
// Utiliser un filtre global Doctrine
class TenantFilter extends SQLFilter
{
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
{
if (!$targetEntity->reflClass->implementsInterface(TenantAwareInterface::class)) {
return '';
}
return sprintf('%s.tenant_id = %s', $targetTableAlias, $this->getParameter('tenant_id'));
}
}
Laravel :
// Global scope
protected static function booted()
{
static::addGlobalScope('tenant', function (Builder $builder) {
$builder->where('tenant_id', auth()->user()->tenant_id);
});
}
Validation obligatoire : Chaque requête SQL DOIT inclure WHERE tenant_id = :current_tenant.
Tier 2 : Dedicated Schema
Principe
Chaque tenant possède son propre schéma dans une base de données partagée.
Exemple PostgreSQL :
database: app_prod
├── schema: tenant_acme (tables pour Acme Corp)
├── schema: tenant_beta (tables pour Beta Inc)
└── schema: tenant_gamma (tables pour Gamma LLC)
Avantages
- ✅ Meilleure isolation (pas de risque de fuite par
tenant_id) - ✅ Performance isolée par schéma
- ✅ Backup/restore par tenant possible
Inconvénients
- ❌ Limite de schémas par DB (PostgreSQL : ~10 000 max)
- ❌ Migrations plus complexes (itérer sur tous les schémas)
Implémentation
Symfony (Doctrine) :
// Middleware pour switcher de schéma
class TenantSchemaMiddleware
{
public function process(Request $request, RequestHandler $handler): Response
{
$tenantId = $this->resolveTenant($request);
$schemaName = "tenant_{$tenantId}";
$this->connection->executeStatement("SET search_path TO {$schemaName}");
return $handler->handle($request);
}
}
Laravel :
// Dynamic connection
DB::purge('tenant');
Config::set('database.connections.tenant.schema', "tenant_{$tenantId}");
DB::reconnect('tenant');
Tier 3 : Dedicated Database
Principe
Chaque tenant possède sa propre base de données physique.
Exemple :
database: tenant_acme_prod
database: tenant_beta_prod
database: tenant_gamma_prod
Avantages
- ✅ Isolation maximale (compliance GDPR, HIPAA)
- ✅ Scaling indépendant par tenant
- ✅ Backup/restore/migration par tenant
Inconvénients
- ❌ Coût élevé (serveurs DB multiples)
- ❌ Maintenance complexe (migrations, monitoring)
Implémentation
Symfony (Doctrine) :
// Connection pooling
class TenantConnectionFactory
{
public function getConnection(string $tenantId): Connection
{
$config = [
'dbname' => "tenant_{$tenantId}_prod",
'user' => 'app_user',
'password' => getenv('DB_PASSWORD'),
'host' => "tenant-{$tenantId}.db.example.com",
];
return DriverManager::getConnection($config);
}
}
Laravel :
// Multi-database config
Config::set("database.connections.tenant_{$tenantId}", [
'driver' => 'pgsql',
'host' => "tenant-{$tenantId}.db.example.com",
'database' => "tenant_{$tenantId}_prod",
]);
DB::connection("tenant_{$tenantId}");
RBAC/ABAC par tenant
Principe
Au-delà de l'isolation par tenant_id, implémenter un contrôle d'accès fin (Role-Based / Attribute-Based).
| Modèle | Description | Exemple |
|---|---|---|
| RBAC | Permissions par rôle | admin, editor, viewer |
| ABAC | Permissions par attributs | can_edit_if_owner, can_view_if_same_department |
Implémentation RBAC
Symfony :
// Voter personnalisé
class DocumentVoter extends Voter
{
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
{
$user = $token->getUser();
// Vérifier tenant_id
if ($subject->getTenantId() !== $user->getTenantId()) {
return false;
}
// Vérifier rôle
return match ($attribute) {
'DOCUMENT_EDIT' => in_array('ROLE_EDITOR', $user->getRoles()),
'DOCUMENT_DELETE' => in_array('ROLE_ADMIN', $user->getRoles()),
default => false,
};
}
}
Laravel (Spatie Permission) :
// Permissions scope par tenant
$user->givePermissionTo('edit-documents', 'tenant', $tenantId);
if ($user->hasPermissionTo('edit-documents', 'tenant', $tenantId)) {
// Autoriser
}
Field-Level Encryption
Principe
Chiffrer les champs sensibles (numéro de carte bancaire, données médicales) pour protéger contre les fuites DB.
Règle : Même si un attaquant accède à la DB, les données sensibles restent illisibles.
Implémentation
Symfony (ParagonIE Halite) :
use ParagonIE\Halite\Symmetric\Crypto;
use ParagonIE\Halite\KeyFactory;
class EncryptedField
{
private EncryptionKey $key;
public function __construct()
{
$this->key = KeyFactory::loadEncryptionKey('/secure/path/tenant.key');
}
public function encrypt(string $plaintext): string
{
return Crypto::encrypt(new HiddenString($plaintext), $this->key);
}
public function decrypt(string $ciphertext): string
{
return Crypto::decrypt($ciphertext, $this->key)->getString();
}
}
Laravel (Eloquent Casting) :
// Model
protected $casts = [
'credit_card' => Encrypted::class,
];
// Automatiquement chiffré/déchiffré
$user->credit_card = '1234-5678-9012-3456'; // Chiffré en DB
echo $user->credit_card; // Déchiffré automatiquement
Rotation des clés : Prévoir une stratégie de rotation annuelle des clés de chiffrement.
Sécurité et validation
Checklist sécurité multitenant
- Résolution tenant : Valider le tenant à chaque requête (header, sous-domaine, JWT claim)
- Isolation SQL : Tous les SELECT incluent
tenant_idou schéma dédié - Tests d'isolation : Test unitaire vérifiant qu'un tenant ne peut pas accéder aux données d'un autre
- Audit logs : Logger tous les accès inter-tenants suspects
- Rate limiting : Par tenant (éviter qu'un tenant consomme toutes les ressources)
- RBAC/ABAC : Permissions fines au-delà de
tenant_id - Chiffrement : Field-level pour données sensibles
Tests d'isolation
Exemple :
// Test Symfony
public function testTenantIsolation(): void
{
$tenantA = $this->createTenant('acme');
$tenantB = $this->createTenant('beta');
$documentA = $this->createDocument($tenantA, 'Secret Acme');
$documentB = $this->createDocument($tenantB, 'Secret Beta');
// User de tenantA ne peut pas voir documentB
$this->loginAs($tenantA->getUser());
$response = $this->get("/documents/{$documentB->getId()}");
$this->assertResponseStatusCodeSame(403);
}
Checklist
Nouveau projet multitenant
- Choisir le tier d'isolation par défaut (Tier 1 pour MVP)
- Configurer résolution tenant (header
X-Tenant-ID, sous-domaine, JWT) - Implémenter filtre global SQL (
tenant_id) - Tests d'isolation (un tenant ne voit pas les données d'un autre)
- RBAC/ABAC par tenant
- Field-level encryption pour données sensibles
- Migration script Tier 1 → Tier 2 → Tier 3
Migration vers Tier supérieur
- Script de migration des données
- Tests de non-régression
- Backup avant migration
- Rollback plan
Ressources
- Multi-Tenant Architecture Guide : gainhq.com/blog/multi-tenant-architecture
- OWASP Multitenant : owasp.org/www-project-web-security-testing-guide
- Symfony Multi-Tenancy : symfony.com/doc/current/doctrine.html#multiple-entity-managers
- Laravel Multi-Tenancy : tenancyforlaravel.com
Date de dernière mise à jour : 2026-04 Version : 1.0.0 Auteur : The Bearded CTO