Introduction
Brancher un agent IA sur une base de données, c’est l’affaire d’une après-midi. Un serveur MCP, une chaîne de connexion, et le voilà qui répond à « combien de commandes cette semaine? ». Reste une question moins amusante: quel badge remet-on à ce nouvel employé?
La première question d’un RSSI, d’un DPO ou d’un auditeur portera sur l’accès aux données: qui a accédé à quelle donnée, et avec quels droits? Dans la plupart des intégrations que je croise, la réponse honnête est « le compte de service de l’agent », c’est-à-dire tout ce que ce compte peut lire.
Dans le cas typique, une ou plusieurs applications exploitent les sources de données de l’entreprise, et le contrôle d’accès est défini au niveau applicatif: c’est l’application qui sait qui peut voir quoi, et qui filtre en conséquence. Il n’est pas prévu qu’un agent, ou plus généralement une application tierce, vienne s’y intégrer. Le brancher directement sur la base de données revient à contourner cette couche, et avec elle toutes les règles qu’elle applique.
Cet article en deux parties documente un POC qui tente de répondre autrement à la problématique de l’accès par un agent aux données de l’entreprise. Un serveur MCP est placé entre l’agent et une base de données PostgreSQL. Il applique des politiques d’accès (rôles, masquage de colonnes, filtrage de lignes) décidées par Open Policy Agent (OPA), au nom de l’utilisateur qui délègue l’interrogation de la base de données, et plafonnées par ce que l’agent a lui-même le droit de faire.
Autrement dit, notre IA reçoit un badge pour ouvrir la boutique, mais pas la clef du coffre-fort.
Dans cette première partie, les limites des réponses sont exposées. Je présente alors les deux principes sur lesquels reposent l’approche alternative que je propose. Afin d’illustrer ma proposition, j’ai réalisé un POC dont les choix architecturaux sont également détaillés.
Dans la seconde partie, les choix d’outils sont précisés et les résultats obtenus sont discutés.
Toutes les données du POC sont fictives.
Les approches naïves
Lorsqu’on s’interroge sur la manière d’intégrer un LLM dans un environnement informatique d’entreprise, trois approches viennent naturellement à l’esprit, et aucune ne donne entièrement satisfaction.
- Créer, dans la base de données, un compte de service unique pour l’agent. Celui-ci dispose alors de l’union des droits de tous ses utilisateurs: un employé qui n’a pas accès à une table peut simplement demander à l’agent de la lire à sa place. Les anglophones parlent de confused deputy.
- Laisser l’agent écrire du SQL libre, que l’on réécrit ensuite pour y injecter masques et filtres. Réécrire du SQL arbitraire (CTE, sous-requêtes, jointures,
SELECT *) est fragile. Cela ouvre également la porte à de possibles fuites par inférence: unWHERE email = '...'sur une colonne masquée suffit à tester des valeurs jusqu’à trouver la bonne. - Créer, dans la base de données, un rôle par utilisateur, avec une sécurité définie au niveau de la ligne. C’est un excellent filet de sécurité, qui garde son utilité en seconde couche. Mais PostgreSQL ne connaît pas l’agent, ne sait pas le restreindre en dessous de l’humain qui le pilote, et son journal ne garde aucune trace de la délégation. De plus, cette solution ne peut être adaptée aux sources de données qui ne sont pas des bases relationnelles, comme les API REST.
L’approche présentée ici tente d’éviter ces écueils. La décision d’accès est prise en dehors de l’agent et de la base, à chaque appel, en tenant compte à la fois de l’utilisateur et de l’agent. Et comme l’agent ne dispose que d’outils structurés, il n’a aucun moyen d’exprimer une requête qui échapperait à ce contrôle.
Deux principes
Ma proposition de gestion de cette problématique repose sur deux principes.
Délégation de l’identité
Le premier principe est que l’identité de l’utilisateur est déléguée, jamais déclarée. L’agent n’agit pas en son nom propre: il agit pour quelqu’un, et le système doit pouvoir dire pour qui, à chaque appel. Cette information ne peut pas venir de l’agent lui-même, puisqu’un LLM peut se tromper, être trompé ou inventer.
Le système retient donc deux identités à chaque appel, celle de l’utilisateur qui a posé la question et celle de l’agent qui l’exécute, et ne s’en remet qu’à une preuve qu’il peut vérifier. Cette preuve établit uniquement qui est qui: il s’agit d’une authentification double. Les droits eux-mêmes (l’autorisation) relèvent de la politique de permission.
Concrètement, chaque prompt s’accompagne d’un JWT dans lequel sub désigne l’utilisateur et act.sub l’agent, d’après le mécanisme de délégation décrit dans la RFC 8693. Le serveur vérifie la signature, l’émetteur, l’audience et l’expiration, et rejette tout identifiant d’utilisateur passé en argument d’un outil, puisque cet argument est rédigé par le LLM. Le jeton ne contient aucune permission, selon l’approche minimaliste déjà décrite sur ce blog: il garde une taille constante, et modifier une politique n’oblige ni à le renouveler, ni à le révoquer.
Intersection des permissions
Il pourrait sembler naturel de laisser l’agent voir tout ce que l’utilisateur peut voir seul. Trois raisons justifient pourtant de lui accorder moins. La première tient au LLM, qui constitue un destinataire supplémentaire des données: ce qui entre dans son contexte peut être envoyé à un fournisseur externe ou se retrouver dans des journaux, et même un modèle hébergé en interne n’échappe pas à ces derniers. Pour des données personnelles, c’est un enjeu de conformité (RGPD): le principe de minimisation veut qu’un traitement n’accède qu’à ce qui est nécessaire à sa finalité, et la tâche d’un agent est bien plus étroite que le métier de l’utilisateur.
La deuxième raison est que l’agent se laisse manipuler. Il lit des contenus qu’il ne maîtrise pas, comme une note saisie par un client, et une injection de prompt peut lui faire exécuter des actions que l’utilisateur n’a jamais demandées, avec les droits de ce dernier et sans son jugement. Plafonner l’agent limite les dégâts d’un tel détournement.
La troisième raison est le changement d’échelle permis par la machine. Un humain consulte une fiche à la fois. Il est limité tant par l’interface de l’application que par sa propre capacité de traitement de l’information. Un agent peut parcourir des milliers de lignes en quelques secondes et les recouper, ce qui rend possible des extractions en masse et des inférences que l’usage manuel décourage. Il devient alors nécessaire de mettre en place des garanties d’anonymisation ou d’impossibilité de recoupement sur les données fournies à l’agent. Cela peut passer par l’établissement de limites minimales des groupes d’agrégation, ou encore en replaçant des identifiants unique par des valeurs empêchant leur utilisation dans une jointure de données.
Dans les trois cas, plafonner l’agent revient à appliquer le principe du moindre privilège à ce nouvel acteur.
Il est donc nécessaire de s’assurer que les droits effectifs d’un agent soient l’intersection de ceux de l’utilisateur et de ceux propres à l’agent.
Cette intersection se calcule dimension par dimension:
- Pour les outils et les tables, seul ce qui est permis aux deux est retenu.
- Pour les colonnes, le traitement le plus restrictif l’emporte. Il en existe quatre, du plus permissif au plus strict:
allow(la valeur est retournée telle quelle),mask:partial(elle est partiellement masquée, par exemplea***@exemple.com),mask:hash(elle est remplacée par un hachage, qui permet de repérer des valeurs identiques sans les lire) etdeny(la colonne est inaccessible). - Pour les lignes, les filtres de l’utilisateur et de l’agent se cumulent.
- Pour le nombre de lignes retournées, la plus petite des deux limites s’applique.
- Pour les agrégats, la taille minimale d’un groupe est la plus grande des deux: un groupe de quelques lignes restitue ces lignes, et un agrégat ne vaut donc pas anonymisation.
- Une colonne masquée ne peut être utilisée ni dans un filtre, ni dans un tri, ni dans un regroupement, ni comme métrique d’un agrégat. Cela ferme la porte aux fuites par inférence évoquées plus haut.
Par exemple, Alice, du support, peut lire l’adresse e-mail complète d’un client dans l’application. Au travers du support-agent, cette adresse reste partiellement masquée: pour résumer une commande, l’agent n’a pas besoin de la connaître, et le LLM n’a donc aucune raison de la recevoir. Le row-level security natif de PostgreSQL ne sait pas exprimer ce type de contraintes.
Périmètre du POC
Avant d’entrer dans les détails, précisons les menaces visées par le POC, et celles qu’il laisse de côté.
Le POC évite les situations suivantes:
- Un utilisateur obtient, par l’intermédiaire de l’agent, des données auxquelles il n’a pas droit (confused deputy).
- Un agent reçoit davantage de données que sa finalité n’en exige.
- Une colonne masquée est reconstituée par inférence, au moyen de filtres, de tris ou de regroupements.
- Une instruction cachée dans une donnée (injection de prompt) pousse l’agent à tenter d’accéder à ce qu’il ne devrait pas voir.
- Un appel prétend agir au nom d’un autre utilisateur, en passant son identifiant en argument d’un outil ou au moyen d’un jeton invalide.
Il ne traite pas les situations suivantes:
- Un serveur MCP compromis ou mal configuré: il est le point d’application des décisions, et sa fiabilité est supposée acquise.
- Un jeton valide volé: son détenteur dispose des droits correspondants.
- Un administrateur de la base de données ou du système de gestion des permissions peut contourner le dispositif.
- L’usage que le LLM fait des données autorisées: il peut les répéter, les résumer ou les transmettre à un autre outil.
Autrement dit, le POC borne ce que l’agent peut obtenir, sans garantir ce qu’il en fait.
Le cas d’usage: Lumen & Co
Pour la démonstration, imaginons Lumen & Co, une boutique en ligne fictive de luminaires, dont la base de données compte quatre tables: customers, orders, payments et products. La table orders contient une colonne customer_note, en texte libre. Elle nous servira plus loin à tester l’injection de prompt: un client malicieux peut y écrire ce qu’il veut.
Voici la structure de la base (PK désigne une clef primaire, FK une clef étrangère):
customers id PK full_name email phone birth_date country loyalty_tierorders id PK customer_id FK -> customers.id status total currency created_at shipping_country customer_notepayments id PK order_id FK -> orders.id method card_last4 payment_tokenproducts id PK name category price cost_price supplier
Chaque colonne de la base de données peut être associée à des tags de classification. Ils sont pour l’instant simulés par un fichier JSON, mais devraient à terme être fournis par un catalogue de métadonnées, tel qu’OpenMetadata. Les politiques d’accès raisonnent sur ces tags plutôt que sur les noms de colonnes:
PII.Direct: full_name, email, phone.PII.Sensitive: birth_date.PII.Indirect: customer_note (texte libre, qui peut contenir des données personnelles ou des instructions hostiles).Financial.Sensitive: card_last4, payment_token.Business.Confidential: cost_price, supplier.
Quatre utilisateurs interagissent avec le système:
- Alice, du support, accède à
customersetorderspour les lignes de Belgique et de France. Elle voit les données personnelles directes, mais pas les données financières. - Bruno, de la finance, accède à
ordersetpayments, pour tous les pays, avec les données personnelles masquées. - Chloé, du merchandising, accède à
products, y compriscost_price, et aux commandes, sans aucune donnée personnelle. - Dan, le DPO (Data Protection Officer), accède à tout, sans masque.
Trois agents leur servent d’intermédiaires:
support-agentaccède àcustomersetorders. Il voit l’e-mail partiellement masqué, aucune donnée personnelle sensible, uniquement les commandes des 90 derniers jours, et reçoit au plus 20 lignes.analytics-agentne peut lire aucune ligne: seulaggregatelui est ouvert. Les données personnelles directes lui parviennent sous forme de hachages, de quoi compter des clients distincts sans jamais les identifier, et les groupes de moins de cinq lignes sont écartés des résultats.merch-agentn’accède qu’àproducts, sanscost_pricenisupplier.
Architecture
Avant de détailler les outils, voici comment les pièces s’assemblent, et ce qui circule entre elles.
Utilisateur
| prompt + JWT (sub: utilisateur, act.sub: agent)
v
Agent (LLM)
| JSON-RPC (MCP), jeton dans l'en-tête Authorization
v
+-----------------------------+ demande +-----------------+
| Serveur MCP |------------->| OPA |
| Scala + Pekko HTTP |<-------------| politique Rego |
| | décision | rôles, agents |
| vérifie le jeton | +-----------------+
| valide les noms demandés | ^
| applique la décision | | tags
| journalise chaque appel |<----------+ |
+-----------------------------+ schéma | |
| ^ +-----+-----+
| SQL | lignes masquées |
v | +-----------------+
+-----------------------------+ | Catalogue |
| PostgreSQL | | fichier JSON |
| rôle en lecture seule | +-----------------+
| fonctions de masquage |
+-----------------------------+
Le serveur MCP est le seul composant qui parle à tous les autres. Il reçoit l’appel de l’agent, demande une décision à OPA, construit la requête et l’exécute. Chacun des autres ignore une partie du tableau: OPA ne voit jamais de données, PostgreSQL ne voit jamais le jeton, et l’agent ne reçoit rien d’autre que le résultat d’un outil. Le catalogue alimente les deux décideurs: les tags pour la politique, les noms de tables et de colonnes pour la validation des arguments.
Fin de la première partie
Cette première partie a introduit une problématique liée à l’exposition d’un agent IA aux bases de données d’une entreprise. Les deux principes qui guident ma proposition de gestion de cette problématique (identité déléguée, et intersection des droits de l’utilisateur et de l’agent) sont illustrées par un POC qui exploite le cas d’usage de Lumen & Co. L’architecture qui en découle a été discutée.
Reste à voir de quoi chaque pièce de l’architecture est constituée, et la manière dont le système traite réellement des demandes d’un agent. Dans la seconde partie, à venir, nous présenteront les outils choisis pour réaliser le POC (OPA, serveur MCP, émetteur de jetons, PostgreSQL, Docker), les observations issues des tests réalisés, ainsi que les pistes potentielles d’amélioration.