Un proxy central basé sur Node peut épuiser en une nuit le budget mensuel alloué à un fournisseur si une clé de service compromise accède à une route de génération permissive et si le proxy fait confiance aux paramètres de routage fournis par l’appelant (tels que provider=openai et model=$OPENAI_CHAT_MODEL) dans le corps de la requête.
Dans notre système en production, AcmeSupportApp appelle la passerelle pour le compte du locataire t_acme_prod ; la passerelle relaie ensuite la requête vers $OPENAI_CHAT_MODEL, $ANTHROPIC_CHAT_MODEL ou $LLAMA_CHAT_MODEL, tandis qu’un cache sémantique partagé, situé à proximité de la passerelle, permet d’économiser des jetons lors de requêtes répétées.
La défaillance ne résulte pas d’une manipulation sophistiquée du modèle. Il suffit à l’attaquant de disposer d’une clé passant les vérifications superficielles actuelles, puis d’envoyer un grand nombre de requêtes comportant des invites (prompts) longues et des limites de sortie élevées ; la passerelle les transmet alors, car aucun budget contraignant par locataire n’a été réservé avant le lancement de l’appel au fournisseur.
Comme le proxy transmet également la réponse brute (non filtrée) du fournisseur à l’appelant, cette même clé compromise peut entraîner l’inclusion de contenu inapproprié dans les réponses aux tickets en aval, faute de politique appliquée au texte généré au niveau de la passerelle.
Compte tenu de ce scénario, quelle étape aurait pu rejeter la toute première requête avant toute dépense et avant la génération de tout contenu inapproprié ?
La passerelle est le seul composant qui connaît simultanément l’identité de l’appelant et le choix du fournisseur ; c’est donc à ce niveau que nous pouvons associer AcmeSupportApp au locataire t_acme_prod et refuser tout modèle ou toute route sortant de son périmètre avant même l’utilisation de la clé du fournisseur.
Chaque autre étape du parcours manque d’une information cruciale. Le fournisseur ne voit que les identifiants de la passerelle et ne peut donc pas distinguer les locataires de notre plateforme ; quant au cache, il ne voit qu’une clé dérivée du contenu de la requête et ne peut donc pas contrôler les droits de dépense ou l’accès aux modèles.
Des données non fiables pénètrent le système à plusieurs points, même lorsque la connexion TLS est sécurisée. Les en-têtes de requête peuvent falsifier l’identité si nous acceptons les identifiants de locataire fournis par l’appelant ; le corps de la requête peut induire en erreur sur le routage et le choix du modèle si nous en tenons compte ; enfin, la réponse du fournisseur constitue une sortie non fiable pouvant contenir du contenu enfreignant les politiques ou des champs structurés non conformes au schéma. Voici le parcours de la requête, avec l’indication des frontières de confiance et des points d’application des règles.
Ce schéma illustre la contrainte pratique autour de laquelle nous structurons la suite du parcours. Seule la passerelle peut authentifier l’appelant, évaluer une politique spécifique à la requête, réserver un quota sur un compteur partagé et appliquer des contrôles tant sur les données d’entrée que sur le flux de sortie, avant que la réponse ne rejoigne le système de l’appelant.
Une passerelle conserve un comportement prévisible en cas d’attaque lorsque chaque point de contrôle utilise un code de refus unique, généré avant l’étape suivante, plus coûteuse en ressources. Nous utilisons une matrice de refus unique pour l’ensemble du parcours afin d’assurer la cohérence des journaux, des alertes et du comportement des clients, quel que soit le fournisseur.
Des identifiants d’appelant manquants, invalides, révoqués ou compromis entraînent une réponse 401 Unauthorized.
Un appelant authentifié demandant une route ou un modèle hors de sa liste d’autorisation reçoit une réponse 403 Forbidden ; la passerelle rejette la requête avant même de contacter l’hôte du fournisseur.
Le dépassement d’une limite de débit ou d’un quota (en jetons ou en coût) entraîne une réponse 429 Too Many Requests accompagnée d’un en-tête Retry-After, car le client a besoin de règles claires concernant le délai d’attente avant nouvelle tentative, et ces tentatives ne doivent pas entraîner une augmentation des coûts.
Si la passerelle ne peut pas effectuer les contrôles en toute sécurité (par exemple, si le système de stockage des compteurs partagés est indisponible ou si un disjoncteur ouvert bloque les vérifications), elle renvoie une erreur 503 Service Unavailable plutôt que de laisser passer la requête et d’engager des frais sans attribution possible.
Si l’appel au fournisseur dépasse le délai d’attente (timeout) de la passerelle, celle-ci renvoie une erreur 504 Gateway Timeout et ne transmet pas de sortie partielle comme s’il s’agissait d’une réponse réussie.
Si une sortie structurée est requise et que la réponse du fournisseur échoue à la validation du schéma, la passerelle renvoie une erreur 422 Unprocessable Content afin d’éviter qu’un analyseur (parser) en aval ne traite des champs non fiables.
Dans le scénario initial, le premier point de refus doit se situer au niveau de l’authentification et de la vérification de la politique par la passerelle, avant toute réservation de quota ou appel au fournisseur ; en effet, une fois que la requête a commencé à être transmise vers l’extérieur, les étapes ultérieures ne disposent plus des informations nécessaires pour associer les coûts et les autorisations à t_acme_prod.