ARCHITECTURE
Contrôle d'accès dans le RAG : qui voit quoi
Quand les documents d'une entreprise sont confiés à un système RAG, le modèle d'autorisations existant ne fait en général pas le voyage. Une note d'implémentation sur la couche qui applique l'accès, sur la façon dont l'identité atteint la requête, et sur le moment où un changement de droits atteint réellement l'index.
Les documents d'une organisation ne forment jamais un tas plat. Tout le monde n'ouvre pas le dossier RH, la correspondance juridique est réservée à une liste nommée, les tableaux financiers ont un nombre comptable de lecteurs. Cette structure s'est accumulée pendant des années à travers des serveurs de fichiers, des bibliothèques SharePoint, des groupes LDAP et des rôles SSO. Puis ces documents sont confiés à un système RAG. La première question à poser est de savoir où est passé le modèle d'autorisations existant pendant ce transfert.
Dans la plupart des premières mises en œuvre, la réponse est nulle part. Le document est découpé, vectorisé, écrit dans un index unique. Les données d'autorisation du système source ne traversent aucune de ces étapes, et l'index lui-même devient donc un plan sans autorisations. Voici une note d'implémentation sur la façon de remettre les couches en place.
Comment un index unique aplatit le modèle
Dans une installation naïve, la recherche par similarité porte sur tout le corpus. Les k meilleurs fragments reviennent, et au moment de la requête rien ne dit de quelle bibliothèque, de quel dossier ou de quelle liste d'accès ils proviennent. Ces fragments sont donnés au modèle comme contexte, et le modèle traite tout ce qu'on lui donne comme matière à réponse. Le contenu d'un fichier que l'utilisateur n'aurait jamais pu ouvrir ressurgit dans la réponse, reformulé en langage naturel.
L'OWASP Gen AI Security Project range cela sous LLM08:2025, Vector and Embedding Weaknesses : partager une même base vectorielle entre des environnements multi-locataires ou multi-niveaux produit un franchissement de contexte entre utilisateurs ou entre requêtes. En pratique, ce n'est pas une question de comportement du modèle, c'est une question d'autorisation classique, et l'entrée est couramment discutée aux côtés de la classe Broken Access Control de l'OWASP Top 10. Sa première mesure recommandée est tout aussi directe : des contrôles d'accès fins et des magasins de vecteurs et d'embeddings conscients des autorisations, avec un cloisonnement logique et d'accès strict des jeux de données à l'intérieur du magasin.
L'OWASP RAG Security Cheat Sheet va plus loin et exige que les métadonnées vivent au niveau du fragment plutôt qu'au niveau du document : classification, propriétaire, rôles autorisés et locataires autorisés stockés aux côtés de chaque fragment vectoriel. Le raisonnement est pratique. Une autorisation attachée seulement au document peut perdre son association pendant le découpage, et il ne reste alors que des fragments sur lesquels rien ne permet de filtrer.
Où le filtre est appliqué
Le contrôle d'accès peut être appliqué en trois points distincts, et ils ne sont pas équivalents.
- Cloisonnement à l'indexation : les différents niveaux de classification vont dans des index, des collections ou des espaces de noms séparés. C'est l'isolation la plus forte, car le contenu non autorisé n'est tout simplement pas présent dans l'ensemble interrogé. Le coût est la souplesse — une requête qui traverse des droits mixtes doit se répartir, et les changements dans le système source imposent des déplacements.
- Filtrage par métadonnées à la requête : le contenu reste dans un seul index et le prédicat de filtrage s'exécute avec la recherche. Le coût d'exploitation est le plus bas ici, car le contenu reste au même endroit et le filtre tourne dans le moteur.
- Filtrage après récupération : la recherche s'exécute sans filtre et la liste renvoyée est élaguée dans le code applicatif. C'est la position la moins solide du point de vue de la sécurité, et elle produit aussi une baisse de rappel mesurable.
Azure AI Search nomme formellement cette distinction par son paramètre vectorFilterMode (voir Azure AI Search — Filters in vector search). En preFilter, le prédicat est appliqué pendant le parcours du graphe HNSW, et la documentation indique que le préfiltrage garantit le renvoi de k résultats s'ils existent dans l'index. En postFilter, chaque partition est parcourue sans filtre et le prédicat est appliqué ensuite ; pour des filtres très sélectifs, cela réduit le rappel et produit des faux négatifs. strictPostFilter, en préversion, applique le filtre après avoir trouvé le top-k global et peut renvoyer zéro résultat avec des filtres sélectifs. Les index créés après le 15 octobre 2023 environ utilisent preFilter par défaut.
La conclusion qui compte ici : les filtres de sécurité sont en général très sélectifs. Un utilisateur donné voit d'ordinaire un faible pourcentage du corpus. C'est exactement le régime dans lequel le post-filtrage laisse silencieusement tomber des résultats. Le préfiltrage paie la justesse en latence, et Microsoft a publié les chiffres dans le même document : à 1 million de vecteurs et 1536 dimensions, le préfiltrage est environ 30 % plus lent lorsque plus de 30 % du jeu de données est filtré, et environ 7 fois plus lent lorsque c'est moins de 2 %. À 100 000 vecteurs, un filtrage sous 0,1 % rend le préfiltrage environ 50 % plus lent. Budget de latence et modèle de sécurité doivent être planifiés ensemble.
L'élagage de sécurité par métadonnées
Le motif concret le plus courant écrit les identifiants de groupes autorisés sur chaque fragment sous forme de collection filtrable. Dans le motif de filtre de sécurité d'Azure AI Search (Azure AI Search — Security filters for trimming results), le champ est défini ainsi.
{
"name": "group_ids",
"type": "Collection(Edm.String)",
"filterable": true,
"retrievable": false
} Mettre retrievable à false est délibéré : les métadonnées d'autorisation sont là pour filtrer, pas pour être renvoyées dans la charge utile de la réponse. Au moment de la requête, le filtre prend cette forme.
group_ids/any(g: search.in(g, 'grp-finance, grp-legal-readers')) La même documentation avertit que construire à la place une chaîne d'égalités comme Id eq 'id1' or Id eq 'id2' est source d'erreurs et difficile à maintenir, et qu'avec des centaines ou des milliers de valeurs les temps de réponse s'étirent sur plusieurs secondes, tandis que search.in est censé rester sous la seconde. Elle énonce aussi clairement la limite : il n'y a ni authentification ni autorisation par le principal de sécurité — le principal n'est qu'une chaîne. Le filtre suppose que l'identité a été dérivée correctement en amont ; l'authentification appartient toujours à la couche du dessus.
Côté relationnel, la sécurité au niveau des lignes de Postgres fait le même travail. Le guide pgvector de Supabase (Supabase — RAG with Permissions) applique des politiques à la table document_sections, et la recherche sémantique continue de les respecter.
alter table document_sections enable row level security;
create policy "read permitted sections"
on document_sections for select
to authenticated
using (
document_id in (
select id from documents
where owner_id = auth.uid()
)
); L'identité arrive via auth.uid() en REST, ou par une variable de session current_setting() sur une connexion Postgres directe. La documentation ajoute une réserve précise : avec un Foreign Data Wrapper, la sécurité au niveau des lignes est sensible à la latence, et une analyse du plan de requête avant la production est nécessaire.
Choisir le filtrage dans le moteur plutôt que l'élagage applicatif n'est pas seulement une question de vitesse. La justification donnée par Azure est que le filtrage dans le moteur supprime aussi le code sur mesure pour la résolution des groupes imbriqués et le parcours des ACL à plusieurs niveaux. Une fois écrit, ce code devient une seconde copie du modèle d'autorisations de l'organisation, et deux copies divergent.
Porter l'identité dans la requête
L'origine de la valeur du filtre compte autant que le filtre. Faire confiance à une liste de groupes envoyée par le client revient à ce que le filtre n'applique rien : l'élagage dépend d'une valeur choisie par l'appelant. Le flux correct dérive l'identité d'un jeton vérifié.
Sur le chemin ACL natif d'Azure AI Search (Azure AI Search — Document-level access control), le jeton utilisateur voyage dans l'en-tête x-ms-query-source-authorization. Le service extrait du jeton les revendications d'utilisateur, de groupe et de portée, les compare aux métadonnées d'autorisation de l'index, et ne renvoie que les documents autorisés. Séparément, l'application appelante est vérifiée pour le rôle Search Index Data Reader — un contrôle en deux étapes. Ces capacités se trouvent dans une version de l'API REST en préversion ; vérifiez la chaîne de version exacte dans la documentation Microsoft Learn actuelle avant de l'écrire dans un appel. Le service documente quatre approches officielles : les filtres de sécurité (disponibles, indépendants de l'API), les ACL de style POSIX avec des portées RBAC, les étiquettes de confidentialité Microsoft Purview, et les ACL SharePoint M365, les trois dernières en préversion.
Sur Google Vertex AI Search (Google Cloud — Vertex AI Search: data source access control), les ACL sont fournies à l'ingestion via le champ acl_info des métadonnées du document, structuré en readers → principals → group_id ou user_id. L'identité vient de Google Identity ou de Workforce Identity Federation (Microsoft Entra ID, Okta, Ping), et l'attribut google.subject doit correspondre au champ e-mail chez le fournisseur externe. Deux limites façonnent directement la conception : 3000 lecteurs par document, et le fait que la configuration ACL est figée à la création du magasin de données et ne peut pas être modifiée ensuite.
Ce qui se passe quand les droits changent
C'est la partie de l'architecture la plus souvent oubliée. Quand un utilisateur est retiré d'un groupe, l'effet est immédiat dans le système source ; dans l'index, il ne l'est pas. La documentation d'Azure le dit explicitement : un décalage temporel se produit avant que l'API en préversion ne prenne en compte les changements de ces restrictions d'accès ou d'autorisation. Les changements d'autorisation dans le système source — appartenance aux groupes Entra, ACL ADLS Gen2, affectations d'étiquettes Purview, ACL SharePoint — ne se reflètent dans les résultats de recherche qu'après synchronisation de ces métadonnées vers l'index, par une exécution d'indexeur, une mise à jour via l'API de push, ou un rafraîchissement Purview.
Pour SharePoint, la distinction est encore plus fine. Les changements sur des éléments à autorisations uniques sont repris de façon incrémentale à chaque exécution réussie de l'indexeur, tandis que les changements hérités d'une portée parente — site, bibliothèque, liste ou dossier — demandent un rafraîchissement explicite : /resync with options: ["permissions"], ou /resetdocs. Une autorisation resserrée au niveau de la bibliothèque peut donc ne pas se propager d'elle-même, même avec l'indexeur qui tourne selon son planning.
Elasticsearch traite le même sujet avec une autre architecture (Elasticsearch — Document level security and connector access control sync). Il existe deux types de synchronisation distincts : la synchronisation de contenu, vers un index préfixé search-, et la synchronisation du contrôle d'accès, vers un index masqué préfixé .search-acl-filter-<INDEX-NAME>. Le champ sur les documents de contenu est _allow_access_control ; l'accès est accordé lorsqu'au moins une entrée du document de contrôle d'accès de l'utilisateur correspond à une entrée de ce champ, et une valeur vide ferme le document à tout le monde.
{
"title": "Q3 procurement note",
"body": "...",
"_allow_access_control": [
"group:finance-readers",
"group:procurement"
]
} La méthode documentée pour gérer les changements d'autorisation consiste à poser une expiration sur la clé d'API Elasticsearch et à planifier des synchronisations de contrôle d'accès récurrentes ; si les autorisations d'un utilisateur changent, la clé d'API doit être mise à jour ou recréée. Dans le comportement général de la DLS, lorsqu'un utilisateur détient plusieurs rôles sur le même index, les requêtes de rôle sont combinées par OR — un seul rôle correspondant rend le document visible. La DLS ne s'applique pas aux API d'écriture, et comme elle s'exécute à chaque requête elle a un léger coût en performance.
La règle de conception qui en découle est courte : l'intervalle de rafraîchissement est un paramètre de sécurité. Le temps de propagation entre un changement de droits et l'index appartient au contrat écrit du système, choisi par niveau de classification.
Les métadonnées perdues au découpage
La stratégie de cloisonnement dans les magasins multi-locataires appartient au même tableau. Qdrant recommande une seule collection par modèle d'embedding avec un cloisonnement fondé sur la charge utile (Qdrant — Multitenancy) ; depuis la v1.11.0, le paramètre is_tenant: true sur un index de mots-clés regroupe les vecteurs d'un locataire, et à l'échelle, payload_m dans hnsw_config combiné à un m global de 0 donne une indexation indépendante par locataire. La même documentation note que les requêtes globales sans filtre de groupe ralentissent car elles doivent parcourir tous les groupes. Pinecone recommande les espaces de noms pour isoler les locataires et définit les opérateurs de métadonnées $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $exists, $and et $or, seuls $and et $or étant permis au niveau supérieur de l'expression de filtre. Weaviate isole chaque locataire dans sa propre partition avec son propre index vectoriel. Amazon Bedrock Knowledge Bases construit le contrôle d'accès à partir de filtres de métadonnées et autorise jusqu'à 10 Ko de métadonnées personnalisées par document.
Quel que soit le magasin choisi, une étape de configuration est couramment oubliée. La documentation d'Azure note que lorsqu'un skillset découpe le document — compétence Text Split ou vectorisation intégrée — les champs de métadonnées d'autorisation doivent être portés des mappages de champs de l'indexeur vers les projections d'index. Pour les étiquettes Purview, la formulation est explicite : sans cette projection, les références au niveau des fragments ne sont pas filtrées. Le découpage, mal configuré, fait tomber les métadonnées d'autorisation et laisse derrière lui des fragments non filtrés. Cela reste invisible de l'extérieur : l'index se construit, la recherche fonctionne, seul le filtre ne correspond à rien de significatif.
Journal d'audit et étape de génération
L'autre moitié du contrôle d'accès est la trace de quelle requête a touché quel document. L'OWASP RAG Security Cheat Sheet l'énumère : journalisez chaque récupération avec l'identité de l'agent ou de l'utilisateur qui interroge et les métadonnées de contrôle d'accès des fragments récupérés, et couvrez toute la chaîne — requête reçue, fragments récupérés avec identifiants de documents et métadonnées d'accès, entrée du modèle assemblée, sortie du modèle générée, et tout appel d'outil déclenché. La partie couramment oubliée est le cache : les réponses servies depuis le cache doivent être journalisées avec le même niveau de détail que les récupérations fraîches, sinon la trace se rompt. Chaque insertion, mise à jour et suppression sur l'index devrait être enregistrée avec un horodatage et l'identité qui l'a effectuée. La quatrième mesure de LLM08:2025 pointe dans la même direction : conservez des journaux détaillés et immuables des activités de récupération.
La dernière couche est la génération. Même avec une récupération correctement filtrée, un enchaînement en plusieurs étapes peut ramener dans la réponse du contenu hors périmètre, via un résumé, une note intermédiaire ou une sortie d'outil. Les mesures normatives d'OWASP pour cette étape : validez toutes les sorties du modèle avant de les renvoyer, appliquez des filtres de politique et une occultation pour les données personnelles et les secrets, occultez dynamiquement selon le niveau d'accès de l'utilisateur qui interroge, et signez les données d'attribution de source pour qu'elles ne puissent pas être altérées après génération. En pratique, cela signifie que chaque fragment sur lequel repose une réponse porte son identifiant, et que le contrôle final s'exécute dans une couche indépendante du modèle qui a produit la réponse.
Il n'existe pas d'étude publique mesurée connue sur le passage de contenu hors périmètre par le résumé. Traitez cela comme une hypothèse de conception plutôt que comme un résultat empirique : tout ce qui entre dans le contexte du modèle peut ressortir dans la réponse, le contrôle appartient donc à la couche de récupération.
Le contrôle d'accès dans le RAG n'est pas une nouvelle discipline de sécurité. C'est le modèle d'autorisations existant porté sur un nouveau chemin de données. Tout se ramène à quatre questions. Les métadonnées d'autorisation vivent-elles au niveau du fragment ? Le filtre est-il appliqué dans le moteur, pendant le parcours de recherche ? L'identité est-elle dérivée d'un jeton vérifié plutôt que fournie par le client ? Et combien de minutes s'écoulent entre un changement d'autorisation dans le système source et son effet sur l'index ? Un système capable de répondre par écrit à ces quatre questions est auditable. Celui qui ne le peut pas n'a pas non plus de réponse à la question de ce que l'index renvoie.
Sources
- OWASP Gen AI Security Project — LLM08:2025 Vector and Embedding Weaknesses
- OWASP — RAG Security Cheat Sheet
- Azure AI Search — Filters in vector search (vectorFilterMode, prefilter/postfilter latency figures)
- Azure AI Search — Security filters for trimming results
- Azure AI Search — Document-level access control (ACLs, RBAC scopes, Purview labels, SharePoint indexer)
- Supabase — RAG with Permissions (pgvector and row level security)
- Google Cloud — Vertex AI Search: data source access control
- Elasticsearch — Document level security and connector access control sync
- Qdrant — Multitenancy
- Pinecone — Namespaces and metadata filtering
- Weaviate — Multi-tenancy
- Amazon Bedrock Knowledge Bases — Metadata filtering