Aetherio

Sécuriser une API exposée à un agent IA : permissions, sandboxing et tool use

12 minutes min de lecture

Partager l'article

Introduction

L'émergence des agents IA capables d'interagir directement avec des systèmes externes via des API (Application Programming Interfaces) est une révolution technologique majeure. Ces 'tool use' ou 'fonctionnalités d'outils' promettent une automatisation sans précédent, transformant la manière dont les entreprises opèrent et interagissent avec leurs données. Un agent IA peut désormais traiter une commande client, générer un rapport financier ou modifier des informations dans une base de données, le tout de manière autonome. Cependant, cette puissance s'accompagne d'un défi de taille : la sécurité. Car si une API est un pont vers vos systèmes, un agent IA sans garde-fou peut devenir une porte ouverte à des risques inattendus.

Chez Aetherio, notre expertise en développement d'applications sur-mesure à Lyon nous confronte quotidiennement à ces enjeux. En tant que CTO externalisé et partenaire technique de startups et PME, nous concevons des architectures où la sécurité est pensée dès la conception. Cet article se propose de vous guider à travers les stratégies et bonnes pratiques essentielles pour sécuriser une API exposée à un agent IA. Nous explorerons comment prévenir le prompt injection, mettre en place des permissions granulaires, sandboxing les actions potentiellement destructrices et garantir la fiabilité de vos systèmes métier. Préparez-vous à transformer le potentiel de l'IA en une force sécurisée et maîtrisée pour votre entreprise.

Sécurité API Agent IA

Le nouveau vecteur de risque : quand l'IA interagit avec vos systèmes

Traditionnellement, la sécurité des API se concentre sur les attaques directes (injection SQL, XSS, DDoS, etc.) et l'authentification/autorisation des utilisateurs humains ou d'autres services. L'arrivée des agents IA, dotés de capacités d'interprétation du langage et de prise de décision, introduit une complexité nouvelle. Ils ne sont pas des utilisateurs classiques et peuvent présenter des vulnérabilités inédites.

Les risques inhérents à l'interaction API-Agent IA

Hallucinations et actions imprévues : Les Modèles de Langage de Grande Taille (LLM) sont connus pour leur capacité à 'halluciner', c'est-à-dire générer des informations incorrectes ou inventées. Lorsqu'un agent IA doit choisir quelle fonction API appeler et avec quels arguments, une hallucination peut se traduire par l'exécution d'une action inappropriée, voire dangereuse. Un agent censé récupérer des données, pourrait, par hallucination, tenter de les modifier ou de les supprimer si l'API le lui permet.

Prompt Injection : C'est la menace la plus insidieuse pour les agents IA. Il s'agit d'une technique où un utilisateur malveillant manipule le comportement de l'agent en injectant des instructions cachées ou contradictoires dans les prompts qui lui sont soumis. L'agent, conçu pour suivre ces instructions, pourrait alors outrepasser ses objectifs initiaux et exécuter des actions non autorisées via les APIs.

Exploitation des permissions excessives : Si votre agent IA dispose de permissions larges sur une API, une attaque par prompt injection réussie ou une hallucination malheureuse peut avoir des conséquences désastreuses. Un accès admin ou write sans restriction est par exemple un risque majeur.

Fuite d'informations sensibles : Un agent malveillant ou compromis par une injection pourrait être délibérément incité à interroger des endpoints API contenant des données confidentielles, puis à les exfiltrer via des canaux détournés (par exemple, en les incluant dans ses réponses à l'utilisateur).

L'importance d'une approche proactive

Face à ces défis, une stratégie de sécurité réactive ne suffit pas. Il est impératif d'adopter une approche proactive dès la conception de votre architecture. Cela implique de repenser la manière dont vous exposez vos API à ces nouvelles entités autonomes et de mettre en place des garde-fous robustes. Comme pour la sécurité des applications web traditionnelles, la clé réside dans une défense en profondeur et le principe du moindre privilège.

Principes fondamentaux pour un tool use sécurisé

Pour construire une architecture robuste face aux risques des agents IA, plusieurs principes de conception doivent être scrupuleusement appliqués. Ces fondations garantissent que même en cas de comportement inattendu ou de tentative de manipulation, l'impact sur vos systèmes reste maîtrisé.

1. Permissions minimales par outil (Least Privilege)

C'est le principe de sécurité le plus important : chaque outil (tool ou function) mis à disposition de l'agent IA ne doit avoir accès qu'aux ressources et aux actions strictement nécessaires à son fonctionnement. Ne donnez jamais à un agent IA des permissions d'administrateur ou des accès write si une permission read est suffisante.

Implémentation :

  • Découpage fin des API : Au lieu d'exposer une API générique, créez des micro-services ou des endpoints API spécifiques pour chaque fonction que l'agent peut appeler. Par exemple, au lieu d'un POST /users générique, ayez un POST /users/create_guest avec des permissions très limitées et un PATCH /users/update_profile avec des permissions différentes.
  • Rôles et Scopes dédiés : Créez des rôles d'autorisation spécifiques pour vos agents IA. Ces rôles doivent avoir des scopes d'accès très restreints, définissant précisément les endpoints, méthodes HTTP et champs de données auxquels l'agent est autorisé à interagir. Un agent de support client n'a pas besoin de supprimer des comptes utilisateurs.
  • Validation des arguments : Au niveau de l'API, validez strictement les arguments reçus de l'agent. Si un argument est censé être un ID numérique, rejetez toute valeur non numérique. C'est une barrière essentielle contre les données malformées dues à des hallucinations de l'IA.

2. Validation humaine sur les actions à impact élevé

Pour toute action ayant des conséquences irréversibles ou critiques (suppression de données, transactions financières, modifications de configurations système), une intervention humaine doit être requise avant exécution. L'agent IA peut préparer l'action, mais un utilisateur doit la valider explicitement.

Exemples d'implémentation :

  • Workflow d'approbation : L'agent génère une proposition d'action (ex: "supprimer le compte de l'utilisateur X"). Cette proposition est envoyée à un administrateur via une interface dédiée (interface d'administration, email, notification mobile). L'administrateur valide ou rejette l'action.
  • Mode "Dry Run" : Avant d'exécuter une action destructrice ou coûteuse, l'agent peut avoir accès à un mode "dry run" de l'API qui simule l'action et retourne le résultat potentiel sans l'appliquer. L'agent peut alors présenter ce résultat à l'utilisateur pour validation.
  • Historique et audit trail : Chaque tentative d'action à impact doit être loggée avec l'identité de l'agent, le prompt initial, la fonction appelée et les arguments. Cela facilite l'audit et la traçabilité en cas de problème.

3. Sandboxing des actions potentiellement destructrices

Le sandboxing consiste à isoler l'exécution des actions de l'agent dans un environnement contrôlé, limitant ainsi les dommages potentiels en cas de comportement malveillant ou inattendu.

Approches de sandboxing :

  • Environnements non-production : Pour le développement et le test, faites interagir les agents avec des API pointant vers des environnements de staging ou de développement. Cela permet de tester les interactions sans risque pour les données de production.
  • Virtualisation/Conteneurisation (Docker) : Les API appelées par les agents peuvent être exécutées dans des conteneurs isolés qui ont un accès très limité au reste de l'infrastructure. Si un conteneur est compromis, l'impact est circonscrit.
  • Tokenisation des données : Pour les données sensibles (informations de paiement, PII), utilisez la tokenisation. L'agent manipule des tokens plutôt que les données réelles, réduisant ainsi le risque de fuite directe. Ce principe est particulièrement pertinent pour assurer la conformité RGPD des agents IA.

4. Journalisation (logging) complète de chaque appel d'outil

Une journalisation détaillée est indispensable pour la détection d'anomalies, le débogage et l'audit de sécurité. Chaque interaction de l'agent avec une API doit être enregistrée.

Informations à logger :

  • Identifiant de l'agent : Quel agent a initié l'appel.
  • Timestamp : Quand l'appel a eu lieu.
  • Prompt initial : Le prompt utilisateur ayant déclenché l'action (si pertinent).
  • Fonction appelée : Le nom de l'outil ou de la fonction API utilisée.
  • Arguments : Les paramètres passés à la fonction.
  • Résultat de l'API : La réponse de l'API (succès, échec, données).
  • Durée d'exécution : Pour l'analyse de performance et la détection d'activités anormales.

Ces logs devraient être centralisés, sécurisés et surveillés pour détecter des patterns d'accès inhabituels ou des tentatives d'exploitation.

Architecture sécurisée pour l'intégration de l'IA

Pour une bonne intégration de l'IA dans vos applications web, la mise en œuvre de ces principes requiert une architecture bien pensée. Voici les composants clés et leur rôle pour construire un système robuste et sûr.

Schéma d'architecture recommandée

 Utilisateur authentifié
        |
        v
 Application (session : utilisateur, tenant, permissions)
        |
        v
 Orchestrateur de l'agent  <------>  LLM (fournisseur)
        |
        |  demande d'appel d'outil : { nom, arguments }
        v
 Passerelle d'outils (tool gateway)
   1. l'outil existe et est exposé à cet agent ?
   2. les arguments respectent le schéma ?
   3. l'utilisateur a-t-il le droit d'effectuer cette action ?
   4. impact élevé : mise en attente d'une validation humaine
   5. journal d'audit de la tentative, acceptée ou refusée
        |
        v
 API métier (compte de service aux scopes minimaux)

Trois règles structurent ce schéma :

  • Le LLM ne détient rien. Aucune clé, aucun jeton, aucun accès direct à l'API. Il ne fait que proposer des appels d'outils. La passerelle est le seul composant autorisé à appeler l'API métier, et c'est elle qui décide.
  • L'identité vient de la session, jamais du modèle. L'identifiant de l'utilisateur et celui de son organisation sont injectés par la passerelle à partir de la session authentifiée. Ils ne figurent jamais dans les arguments d'un outil : sinon, une injection du type « utilise le client 42 » suffit à lire les données d'une autre entreprise.
  • Les droits effectifs sont une intersection. L'agent agit au nom de l'utilisateur, avec au maximum les droits de cet utilisateur, et au maximum le périmètre prévu pour l'agent. Un agent de support utilisé par un administrateur n'hérite pas pour autant des droits d'administration.

Implémenter une passerelle d'outils en TypeScript

Voici une implémentation compacte de cette passerelle, avec la bibliothèque zod pour valider les arguments. Chaque outil déclare son schéma, la permission requise et son niveau de risque.

// agent/tools.ts
import { z } from 'zod';
import { db } from '../db';
import { refunds } from '../billing/refunds';

// Contexte construit par l'application à partir de la session : jamais fourni par le LLM.
export interface ActorContext {
  userId: string;
  tenantId: string;
  permissions: ReadonlySet<string>;
}

export interface AgentContext extends ActorContext {
  agentId: string;
  conversationId: string;
}

type Risk = 'standard' | 'critical';

export interface Tool {
  description: string;
  permission: string;
  risk: Risk;
  check: (rawArgs: unknown) => string | null; // message d'erreur, ou null si valide
  invoke: (rawArgs: unknown, ctx: ActorContext) => Promise<unknown>;
}

function defineTool<S extends z.ZodType>(def: {
  description: string;
  input: S;
  permission: string;
  risk: Risk;
  run: (args: z.infer<S>, ctx: ActorContext) => Promise<unknown>;
}): Tool {
  return {
    description: def.description,
    permission: def.permission,
    risk: def.risk,
    check: (rawArgs) => {
      const parsed = def.input.safeParse(rawArgs);
      return parsed.success ? null : parsed.error.issues.map((issue) => issue.message).join(', ');
    },
    // Les arguments sont revalidés à chaque exécution, même après une validation humaine.
    invoke: (rawArgs, ctx) => def.run(def.input.parse(rawArgs), ctx),
  };
}

export const tools = {
  getOrder: defineTool({
    description: "Récupère le statut d'une commande de l'entreprise de l'utilisateur",
    input: z.object({ orderId: z.string().uuid() }).strict(),
    permission: 'orders:read',
    risk: 'standard',
    run: ({ orderId }, ctx) =>
      db.order.findFirst({
        // Le filtre tenantId vient du contexte : impossible d'atteindre la commande d'un autre client.
        where: { id: orderId, tenantId: ctx.tenantId },
        // Seuls les champs utiles au modèle sortent de la base.
        select: { id: true, status: true, total: true, createdAt: true },
      }),
  }),
  refundOrder: defineTool({
    description: 'Rembourse une commande',
    input: z.object({ orderId: z.string().uuid(), reason: z.string().min(5).max(500) }).strict(),
    permission: 'orders:refund',
    risk: 'critical',
    run: ({ orderId, reason }, ctx) => refunds.create({ tenantId: ctx.tenantId, orderId, reason, actorId: ctx.userId }),
  }),
};

export type ToolName = keyof typeof tools;

export function isToolName(name: string): name is ToolName {
  return Object.hasOwn(tools, name);
}

La passerelle applique ensuite les contrôles dans l'ordre, et renvoie toujours un résultat structuré au modèle, y compris en cas de refus. L'agent peut ainsi expliquer à l'utilisateur pourquoi l'action n'a pas eu lieu, au lieu de réessayer à l'aveugle.

// agent/tool-gateway.ts
import { tools, isToolName, type AgentContext } from './tools';
import { approvals } from './approvals';
import { auditLog } from '../audit';
import { logger } from '../logger';

export type ToolResult =
  | { status: 'ok'; data: unknown }
  | { status: 'pending_approval'; approvalId: string }
  | { status: 'refused'; reason: string };

export async function executeToolCall(ctx: AgentContext, name: string, rawArgs: unknown): Promise<ToolResult> {
  const startedAt = Date.now();
  const result = await dispatch(ctx, name, rawArgs);
  // Chaque tentative est tracée, refus compris : ce sont souvent les refus qui trahissent une injection.
  await auditLog.write({
    agentId: ctx.agentId,
    conversationId: ctx.conversationId,
    userId: ctx.userId,
    tenantId: ctx.tenantId,
    tool: name,
    args: rawArgs,
    status: result.status,
    durationMs: Date.now() - startedAt,
  });
  return result;
}

async function dispatch(ctx: AgentContext, name: string, rawArgs: unknown): Promise<ToolResult> {
  if (!isToolName(name)) return { status: 'refused', reason: `Outil inconnu : ${name}` };
  const tool = tools[name];

  const invalid = tool.check(rawArgs);
  if (invalid) return { status: 'refused', reason: `Arguments invalides : ${invalid}` };

  if (!ctx.permissions.has(tool.permission)) {
    return { status: 'refused', reason: "Vous n'avez pas les droits pour effectuer cette action." };
  }

  if (tool.risk === 'critical') {
    const approval = await approvals.create({ tenantId: ctx.tenantId, requestedBy: ctx.userId, tool: name, args: rawArgs });
    return { status: 'pending_approval', approvalId: approval.id };
  }

  try {
    return { status: 'ok', data: await tool.invoke(rawArgs, ctx) };
  } catch (error) {
    // Le détail technique reste dans les logs : le modèle n'a pas à connaître la structure interne de l'API.
    logger.error({ error, tool: name }, 'Échec d\'un appel d\'outil');
    return { status: 'refused', reason: "L'action a échoué." };
  }
}

Côté orchestrateur, que vous utilisiez l'API d'Anthropic, celle d'OpenAI ou le Vercel AI SDK, chaque outil exposé au modèle a la même implémentation : un appel à executeToolCall. Le modèle ne voit que la description et le schéma de l'outil, et ce qu'il reçoit en retour, y compris un refus, lui arrive comme un résultat d'outil ordinaire.

La validation humaine, sans double exécution

Une action critique en attente est validée par un humain, depuis l'interface, avec sa propre session. Deux détails comptent : la recherche de la demande est scopée au tenant du valideur, et le passage de l'état « en attente » à « validé » est atomique, pour qu'un double clic ne rembourse pas deux fois.

// agent/approve.ts
import { tools, isToolName, type ActorContext } from './tools';
import { approvals } from './approvals';
import type { ToolResult } from './tool-gateway';

export async function approveToolCall(approvalId: string, reviewer: ActorContext): Promise<ToolResult> {
  const approval = await approvals.findPending(approvalId, reviewer.tenantId);
  if (!approval || !isToolName(approval.tool)) {
    return { status: 'refused', reason: 'Demande introuvable ou déjà traitée.' };
  }

  const tool = tools[approval.tool];
  if (!reviewer.permissions.has(tool.permission)) {
    return { status: 'refused', reason: 'Droits insuffisants pour valider cette action.' };
  }

  // UPDATE ... SET status = 'approved' WHERE id = ? AND status = 'pending' : une seule validation gagne.
  const claimed = await approvals.claim(approval.id, reviewer.userId);
  if (!claimed) return { status: 'refused', reason: 'Cette demande a déjà été traitée.' };

  return { status: 'ok', data: await tool.invoke(approval.args, reviewer) };
}

L'interface de validation doit montrer exactement ce qui va être exécuté (l'outil, les arguments validés, la conversation qui a mené à la demande), pas un résumé rédigé par le modèle. Un résumé généré peut lui-même avoir été manipulé.

Se défendre contre le prompt injection

L'injection directe (un utilisateur qui écrit « ignore tes instructions ») est la plus connue, mais la plus dangereuse est indirecte : des instructions cachées dans un contenu que l'agent lit pour faire son travail. Un email, un PDF, une page web, un ticket de support ou un champ de votre propre base de données peuvent contenir un texte adressé au modèle. Tout contenu externe doit être traité comme une entrée non fiable, au même titre qu'un formulaire public.

Aucune technique connue ne bloque l'injection de prompt de façon fiable, et le classement OWASP Top 10 pour les applications LLM la place en tête des risques. La défense repose donc d'abord sur ce que l'agent peut faire, pas sur ce qu'on lui demande de ne pas faire. Le développeur Simon Willison résume le danger par une combinaison de trois capacités : accès à des données privées, exposition à du contenu non fiable, et capacité à communiquer vers l'extérieur. Un agent qui réunit les trois peut être amené à exfiltrer vos données ; retirer l'une des trois casse la chaîne.

Concrètement :

  • Couper les canaux d'exfiltration. Pas d'outil générique « appeler une URL » ou « envoyer un email à n'importe quelle adresse ». Si un outil doit contacter l'extérieur, limitez-le à une liste de domaines ou de destinataires connus.
  • Ne pas afficher d'images ou de liens externes produits par le modèle. Une image Markdown dont l'URL contient des données dans ses paramètres suffit à les envoyer vers un serveur tiers au moment de l'affichage. Filtrez ou neutralisez les URL externes dans le rendu du chat.
  • Séparer instructions et données. Encadrez clairement le contenu externe dans le prompt et rappelez au modèle qu'il s'agit de données à analyser, pas d'instructions à suivre. Cela réduit le risque sans l'éliminer, d'où l'importance des points précédents.
  • Borner le rayon d'action. Un nombre maximal d'étapes par conversation, un rate limiting sur les appels d'outils et un budget par session limitent les dégâts d'un agent détourné ou parti en boucle.
  • Tester l'attaque. Intégrez des tentatives d'injection à vos jeux de tests, comme nous le décrivons dans notre article sur les tests d'applications IA non déterministes. Un outil comme promptfoo propose aussi des scénarios de red teaming automatisés.

Sandboxing concret : exécuter le code produit par un agent

Certains agents écrivent et exécutent du code : analyse de données, transformation de fichiers, génération de rapports. Ce code est par définition non fiable. Il doit tourner dans un environnement jetable, sans réseau, avec des ressources plafonnées et sans aucun accès à vos secrets.

# Conteneur éphémère : pas de réseau, système de fichiers en lecture seule,
# ressources limitées, aucun privilège, et un timeout dur sur l'exécution.
docker run --rm \
  --network none \
  --read-only --tmpfs /tmp:rw,size=64m \
  --memory 256m --cpus 0.5 --pids-limit 64 \
  --cap-drop ALL --security-opt no-new-privileges \
  --user 1000:1000 \
  -v "$JOB_DIR/input:/input:ro" \
  -v "$JOB_DIR/output:/output" \
  python:3.12-slim timeout 30 python /input/script.py

Un conteneur partage le noyau de la machine hôte : c'est une bonne première barrière, pas une isolation absolue. Pour exécuter du code non fiable à grande échelle, tournez-vous vers une isolation plus forte (gVisor, microVM de type Firecracker) ou vers un service de sandbox managé comme E2B. Dans tous les cas, la machine qui exécute le code ne doit avoir ni variables d'environnement sensibles, ni accès au réseau interne. Pour aller plus loin sur les conteneurs, voir notre guide Docker pour les développeurs web.

Checklist avant de brancher un agent sur vos API

  • Chaque outil a un schéma d'entrée strict et une permission explicite.
  • L'utilisateur et le tenant proviennent de la session, jamais des arguments de l'outil.
  • Les requêtes sont filtrées par tenant côté serveur, et les réponses d'outils ne contiennent que les champs nécessaires.
  • Les actions irréversibles passent par une validation humaine atomique.
  • Aucun outil ne peut contacter une destination arbitraire.
  • Le compte de service de l'agent a les scopes minimaux, distincts de ceux de l'application.
  • Chaque tentative d'appel est journalisée, refus compris, et les anomalies déclenchent une alerte.
  • Le code généré s'exécute dans une sandbox sans réseau ni secrets.
  • Des tentatives d'injection font partie de la suite de tests.

Conclusion

Sécuriser une API exposée à un agent IA revient à accepter une idée simple : le modèle peut être trompé, et il le sera. La question n'est donc pas de le rendre infaillible, mais de faire en sorte qu'un agent trompé ne puisse rien faire de grave. Des outils étroits, des permissions héritées de l'utilisateur, une identité qui vient de la session, une validation humaine sur l'irréversible et un journal complet couvrent l'essentiel du risque.

Ces garde-fous se conçoivent beaucoup plus facilement au départ qu'une fois l'agent en production. Si vous préparez le passage d'un chatbot à un agent IA capable d'agir sur vos systèmes, notre équipe de développement d'agents IA peut vous aider à poser cette architecture.

FAQ - Questions fréquentes