Multilocataire : isoler les données avec la RLS PostgreSQL
Comment la sécurité au niveau des lignes de PostgreSQL protège les données de chaque client d’un SaaS, et les erreurs à éviter avant la mise en production.
Dans un SaaS où tous les clients partagent les mêmes tables, la sécurité au niveau des lignes (RLS, pour row level security) de PostgreSQL permet de faire respecter l’isolation par la base de données elle-même, et non seulement par le code de l’application. Vous ajoutez un identifiant de client à chaque table, vous écrivez une politique, et la base filtre les lignes à votre place. C’est un bon filet de sécurité, à condition d’éviter quelques pièges qui le rendent inutile sans que rien ne plante.
Pourquoi un WHERE tenant_id = ... ne suffit pas
Dans un modèle où les clients partagent des tables, chaque requête doit filtrer sur le client courant. Il suffit d’un oubli dans une requête ou un rapport pour qu’un client voie les données d’un autre.
Le livre blanc d’AWS sur l’isolation des locataires (présenté comme document de référence historique) rappelle une distinction utile : l’authentification et l’autorisation ne sont pas de l’isolation. Un utilisateur peut être authentifié et autorisé, et quand même accéder aux ressources d’un autre locataire si rien ne le bloque. L’isolation, c’est un mécanisme qui limite l’accès selon le contexte du locataire courant.
La RLS est l’un des endroits où poser ce mécanisme quand vous utilisez PostgreSQL.
Comment fonctionne la RLS?
Selon la documentation de PostgreSQL, une fois la RLS activée sur une table avec ALTER TABLE ... ENABLE ROW LEVEL SECURITY, tout accès normal en lecture ou en écriture doit être permis par une politique. Sans politique, la règle par défaut est de tout refuser : aucune ligne n’est visible ni modifiable.
Une politique comporte deux expressions :
USINGdétermine quelles lignes existantes sont visibles pourSELECT,UPDATEetDELETE.WITH CHECKdétermine quelles lignes peuvent être créées ou modifiées. Si vous l’omettez, la clauseUSINGs’applique aussi aux écritures.
Un exemple minimal, où le client courant vient d’un paramètre de session :
ALTER TABLE factures ENABLE ROW LEVEL SECURITY;
CREATE POLICY isolation_client ON factures
USING (tenant_id = current_setting('app.tenant_id')::uuid);
L’approche du paramètre applicatif est aussi celle de l’article d’AWS sur la RLS multilocataire (mai 2020). Il écarte l’autre option, un rôle PostgreSQL par client, qu’il juge difficile à maintenir et peu extensible.
- Applicationdétermine le client courant
- Connexionfixe app.tenant_id
- Politique RLSfiltre chaque ligne
- Résultatseulement les lignes du client
Quels pièges rendent la RLS inefficace?
Ces pièges viennent de la documentation de PostgreSQL et de l’article d’AWS, chacun pour sa part. Ce sont les premiers points que nous vérifierions.
1. Le rôle de l’application contourne la politique. Les superutilisateurs et les rôles dotés de l’attribut BYPASSRLS ignorent toujours la RLS. Le propriétaire de la table aussi, à moins d’avoir exécuté ALTER TABLE ... FORCE ROW LEVEL SECURITY. L’article d’AWS recommande que l’application se connecte avec un rôle qui n’est pas propriétaire des objets. Si vos migrations et votre application utilisent le même rôle, la politique ne protège rien.
2. Le paramètre de session fuit d’une requête à l’autre. L’article d’AWS avertit que les variables de session peuvent entrer en conflit avec le regroupement de connexions côté serveur, comme pgBouncer, et conseille de tester si l’état de session est partagé. La fonction set_config accepte un troisième argument : à true, la valeur ne vaut que pour la transaction en cours. Fixer le client au début de chaque transaction explicite, avec cette option, évite de laisser une valeur traîner (hors transaction, la valeur disparaît aussitôt).
3. Un paramètre manquant. Par défaut, current_setting renvoie une erreur si le paramètre n’existe pas, ce qui est plutôt sain : la requête échoue au lieu de renvoyer des données. Si vous passez true comme second argument, elle renvoie plutôt NULL. Dans ce cas, la comparaison ne renvoie pas true : la documentation sur la RLS précise que les lignes pour lesquelles l’expression ne renvoie pas true ne sont pas traitées, donc aucune ligne n’est visible.
4. Des politiques lourdes. PostgreSQL précise que le cas le plus simple et le plus rapide est celui où la politique ne regarde que les valeurs de la ligne elle-même. Une politique qui consulte d’autres tables par sous-requête peut créer des conditions de concurrence et des fuites d’information si on n’y prend pas garde.
5. Les exceptions. Les opérations sur toute la table, comme TRUNCATE et REFERENCES, ne sont pas soumises à la RLS. Les vérifications d’intégrité référentielle (clés uniques, clés étrangères) la contournent aussi. Concevez le schéma en conséquence.
Que tester avant la mise en production?
La RLS se vérifie par des tests, pas par lecture. L’article d’AWS le démontre avec un rôle par locataire : le rôle ne voit que ses lignes, une modification ou une suppression chez un autre client touche zéro ligne, et une insertion qui viole la politique produit une erreur. Avec un rôle d’application partagé, adaptez la démarche : connectez-vous avec ce rôle, fixez app.tenant_id au client A, lisez, puis essayez de modifier, de supprimer et d’insérer des lignes du client B.
- 1Deux clients de test
- 2Lire chez l’autre
- 3Écrire chez l’autre
- 4Valider le refus
Chaque test doit échouer si on retire la politique.
Une liste de départ :
- Deux clients de test, avec des données dans chaque table protégée.
- Un test de lecture croisée, de modification croisée et d’insertion croisée, pour chaque table.
- Un test qui vérifie que la connexion de l’application n’est ni propriétaire, ni superutilisateur, ni dotée de
BYPASSRLS. - Un test avec regroupement de connexions, où deux clients se succèdent sur la même connexion.
- Un test où le paramètre du client est absent.
- L’exécution de ces tests dans l’intégration continue, pour qu’une nouvelle table sans politique fasse échouer la livraison.
Pour les sauvegardes, PostgreSQL offre le paramètre row_security. À off, il ne contourne pas la RLS : il produit une erreur si le résultat d’une requête serait filtré, ce qui évite une copie incomplète sans qu’on le sache.
La RLS est-elle suffisante?
Non. C’est une couche de défense, pas toute la défense. Elle ne remplace ni l’authentification, ni les contrôles d’accès de l’application, ni la journalisation. Elle ne règle pas non plus les cas où un client exige des données isolées physiquement : pour ceux-là, une base dédiée peut s’imposer, avec un coût d’exploitation plus élevé. Avant de choisir, nous commençons par les besoins de vos clients les plus exigeants, le nombre de tables à protéger et la capacité de votre équipe à tenir les tests à jour. Si vous réfléchissez à cette décision, notre offre de CTO fractionnel décrit comment nous travaillons.
Cet article est de l’information générale, et non un avis juridique, de sécurité ou de conformité. Validez votre architecture et vos obligations avec des professionnels qualifiés.