Un agent qui appelle votre système métier n'a pas de session
Signature HMAC, horodatage, anti-rejeu, idempotence : comment j'ouvre 18 outils métier à une IA sans jamais lui donner les clés de toute la base.

Un agent vocal qui consulte l'historique d'un client n'a pas de cookie. Pas de session, pas de navigateur : un programme qui tourne sur le serveur de quelqu'un d'autre frappe à la porte de votre système métier avec une requête HTTP nue. La question « qui est-ce ? » n'a donc pas la même réponse que devant un formulaire de connexion — et la réponse commode consiste à coller un jeton d'API dans un en-tête, avec les droits de toute la base derrière.
Sur le back-office de ce site, j'expose 18 outils métier à des agents, répartis en 3 périmètres étanches : 6 outils pour un client identifié (lister ses tickets, en ouvrir un, répondre, suivre ses projets), 10 outils d'administration, et 2 outils de consultation d'une base de connaissance en lecture seule. Un seul mécanisme d'entrée les protège : une signature HMAC-SHA256 — un sceau calculé avec un secret partagé, qui prouve que l'expéditeur le détient et que le message n'a pas bougé en route — posée sur une chaîne canonique de cinq lignes. Autour : une fenêtre d'horodatage de ±300 secondes, un jeton anti-rejeu valable 10 minutes, un corps de requête borné à 64 Ko et vérifié avant d'être lu, une comparaison en temps constant, et 12 codes d'erreur contractuels dont trois seulement peuvent être retentés tels quels.
Tout ce qui suit est mesuré sur le code de ce dépôt (app/lib/agents/), pas sur un schéma de principe. Et la thèse tient en une phrase : l'ordre dans lequel on vérifie ces éléments est une décision d'architecture, pas un détail d'implémentation. Inversé, il transforme la protection anti-rejeu en vecteur d'attaque.
Ce qu'on signe, et pourquoi l'empreinte du corps entre dans la signature
Signer une requête, ce n'est pas chiffrer un mot de passe : c'est produire une empreinte que seul le détenteur du secret peut calculer. Reste à décider sur quoi. Signer la seule URL laisse un intermédiaire remplacer la charge utile en gardant la signature. Signer sans horodatage laisse rejouer indéfiniment un appel capté une fois.
D'où une chaîne canonique de cinq lignes, reconstruite à l'identique des deux côtés :
POST
/api/agents/v1/tools/create_ticket
1751452800
7c1b9f2e-3a44-4e51-9b8e-2f0c6d5a1e77
9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
Méthode, chemin seul (sans hôte ni paramètres d'URL), horodatage, jeton unique, puis l'empreinte SHA-256 du corps brut. Changer une virgule dans le corps change la dernière ligne, donc la signature.
Quatre en-têtes, et rien d'autre
| En-tête | Contenu | Secret ? |
|---|---|---|
X-Agent-Key | identifiant de la clé | non, public |
X-Agent-Timestamp | secondes Unix | non |
X-Agent-Nonce | valeur unique par requête | non |
X-Agent-Signature | sceau HMAC de la chaîne canonique | c'est la preuve |
Le secret, lui, ne circule nulle part : ni en-tête, ni URL, ni journal.
L'ordre des vérifications : clé, horodatage, signature, puis seulement le nonce
Le nonce est le jeton à usage unique qui empêche le rejeu : on garde en mémoire ceux qu'on a déjà vus pendant 10 minutes — un peu plus que la fenêtre d'horodatage — et on refuse un doublon.
L'ordre retenu, écrit noir sur blanc en commentaire du module d'authentification, est : clé connue, puis horodatage dans la fenêtre, puis signature valide, et seulement ensuite le nonce.
Inverser cet ordre, c'est fabriquer soi-même le déni de service
Le magasin de nonces est une structure en mémoire qui grossit à chaque requête acceptée. Si on le consulte et l'alimente avant de vérifier la signature, n'importe qui peut le remplir : l'identifiant de clé n'est pas secret, il suffit de le reprendre et d'envoyer des jetons aléatoires en boucle. La protection anti-rejeu devient elle-même la surface d'attaque.
Même raisonnement un étage plus haut : le corps est plafonné à 64 Ko et sa taille annoncée contrôlée avant la lecture, parce que vérifier la signature oblige à lire tout le corps — et qu'un appelant non authentifié ne doit jamais décider de la mémoire qu'on lui consacre.
Troisième règle, la plus utile en pratique : aucune écriture en base sur un refus non authentifié. Journaliser les rejets semble prudent ; en réalité, cela offre à un anonyme une amplification gratuite — une requête pour lui, une insertion pour vous. Ces refus-là partent dans le journal applicatif.
Ce qu'une signature prouve, et ce qu'elle ne prouve pas
C'est la ligne que la plupart des intégrations ratent. Une signature valide prouve que le détenteur du secret a émis cette requête — rien de plus. Quand un agent vocal appelle mon système et déclare « je parle à monsieur X », le sceau certifie l'agent, jamais monsieur X.
Qui a émis la requête n'est pas qui est au bout du fil
L'identité du client est résolue côté serveur à partir d'un numéro de téléphone normalisé, cherché dans les comptes existants : aucune correspondance, l'agent reçoit la consigne de proposer un rendez-vous plutôt que d'insister ; plusieurs correspondances, on refuse. La sécurité d'un périmètre client repose donc entièrement sur la capacité du canal à authentifier cette identité. C'est pourquoi la clé de l'un des canaux est restée non provisionnée tant qu'il n'apportait pas de preuve de possession du numéro : un identifiant d'appelant téléphonique brut ne suffit pas, un canal de messagerie vérifié, oui. Une clé client sans preuve de possession, c'est un annuaire ouvert.
Le périmètre est attaché à la clé, côté serveur
Chaque clé porte son périmètre et son canal, définis dans le code du serveur — jamais déclarés par l'appelant. Un outil hors périmètre est refusé, même si l'outil existe et que la signature est parfaite.
Deux garde-fous complètent le découpage. Des quotas différenciés : 60 requêtes par minute et par clé, 20 mutations, seulement 10 pour la clé d'administration, et 30 requêtes par minute et par client résolu. Et le refus d'énumération : une ressource qui existe mais appartient à un autre client renvoie exactement la même réponse qu'une ressource inexistante. Sans cela, un agent bavard devient un outil de reconnaissance.
Et un fail-closed net : une clé dont le secret est absent de l'environnement, ou fait moins de 32 caractères, n'existe pas. Pas de mode dégradé, pas de valeur par défaut.
L'idempotence, ou comment un agent ne crée pas trois fois le même ticket
Un agent retente, et c'est souhaitable : un délai d'attente dépassé n'est pas une réponse, l'appel a peut-être abouti. Sans protection, trois tentatives produisent trois tickets identiques.
Le motif retenu est une réservation avant effet de bord : la clé d'idempotence est enregistrée en base à l'état « en cours » avant l'action, puis finalisée avec la réponse mémorisée. Une deuxième requête portant la même clé et les mêmes arguments rejoue cette réponse ; la même clé avec des arguments différents lève un conflit explicite. L'empreinte des arguments est calculée sur un JSON aux clés triées récursivement — sinon un simple changement d'ordre des champs la ferait diverger.
La clé ne peut pas venir du modèle
Un grand modèle de langage ne régénère pas le même identifiant au tour suivant. Lui demander de produire la clé d'idempotence, c'est obtenir une clé neuve à chaque tentative — donc exactement les doublons qu'on voulait éviter. Elle doit être générée par le code qui exécute l'appel HTTP. Corollaire assumé : ce champ est requis par le contrat HTTP, mais retiré de la déclaration d'outil montrée au modèle.
Un échec ne consomme pas la clé
Si l'exécution échoue, la réservation est supprimée. C'est contre-intuitif quand on pense « une clé, un usage », mais une clé consommée par un échec rend la reprise impossible : l'agent n'a plus le choix qu'entre mentir à l'utilisateur et créer un doublon. Reste la requête qui meurt entre la réservation et la finalisation : au-delà de 90 secondes, la réservation est considérée comme morte et reprise par la tentative suivante.
Un journal que personne ne lit ne détecte rien
La table d'audit est en ajout seul : une ligne par requête authentifiée, refus compris — clé, outil, client résolu, arguments validés, code de résultat, ressource touchée. Utile, mais passif.
La couche qui détecte réellement est active : la première utilisation de la clé d'administration dans la journée déclenche un email, et une rafale de 5 mutations en 60 secondes en déclenche un autre, avec un délai de refroidissement pour ne pas se noyer. Envoi en tâche de fond : une alerte ne doit jamais faire échouer la requête qu'elle observe.
L'hygiène des messages d'erreur, parce qu'ils sont lus à voix haute
Contrainte propre au vocal : le message d'erreur est prononcé au prospect. Une trace d'exécution, une erreur SQL ou un écho de la valeur reçue ne sont plus une fuite discrète dans un journal.
D'où un contrat d'erreurs stable : 12 codes, chacun avec son statut HTTP et son drapeau « rejouable ». Trois se retentent tels quels — horodatage hors fenêtre (re-signer), nonce déjà vu (nouveau jeton), quota dépassé (attendre le délai) — plus l'erreur serveur, à rejouer avec la même clé d'idempotence. Le reste est définitif : périmètre refusé, outil inconnu, client inconnu, arguments invalides. Les messages restent génériques, écrits pour être reformulés par l'agent.
Corollaire : le message de validation des arguments est reconstruit depuis le type de contrainte violée, jamais recopié depuis la bibliothèque de validation — celle-ci renvoie volontiers la valeur reçue, que l'agent réciterait.
Ce que ça représente comme travail
La partie visible d'une intégration d'agent — le bouton, le composant embarqué — tient en moins de 200 lignes. La couche d'accès machine décrite ici, c'est dix modules : authentification, clés et périmètres, quotas, identité, idempotence, audit, alerte, codes d'erreur, contrat d'outils, exécution des outils. Ce contrat est généré depuis les schémas qui valident déjà les requêtes, pour qu'il n'existe jamais deux définitions divergentes du même outil.
C'est le type de chantier que je traite en données et intégration : ouvrir un système existant à un nouveau consommateur sans le rendre poreux — et c'est ce qui sépare une démonstration d'agent d'un agent réellement branché sur le métier. L'exigence vaut hors IA : dans une application de traçabilité industrielle, c'est le journal d'audit immuable qui rend la conformité démontrable plutôt que racontée.
Sur un projet d'agent, deux questions à poser dès lundi matin : la liste des outils exposés et le périmètre attaché à chaque clé, puis l'ordre dans lequel les vérifications sont faites. Si personne ne sait répondre à la seconde, la première réponse ne vaut rien.
Si vous devez ouvrir un ERP, un CRM ou une base métier à une IA : réservez un créneau pour cadrer les périmètres.
Un enjeu similaire dans votre entreprise ?
30 minutes pour en parler concrètement — gratuit, sans engagement.
Articles similaires

La panne la plus coûteuse d'un agent vocal, c'est un silence
Un écart de contrat d'outils ne produit pas d'erreur : l'action est ignorée sans trace. Retour sur une intégration où le contrat n'a jamais été chargé.

Un agent vocal ne doit jamais citer un prix de mémoire
Comment j'empêche un agent vocal d'inventer un tarif : les chiffres passent par un outil déterministe, les fiches de biens ne portent aucun chiffre.

Le fichier qui coûtait 55 000 tokens avant ma première question
221 679 octets chargés à chaque session, ramenés à 7 875 sans rien perdre. Ce qu'un fichier de règles coûte vraiment à un agent, et comment l'élaguer.