Donner une identité à une machine
Par Alban Mouton, directeur technique de Koumoul, avec Claude.
Cet article a été écrit à quatre mains, dont deux artificielles. Le sujet s'y prête : il porte précisément sur l'identité que nous donnons aux agents qui travaillent sur nos plateformes, et celui-ci en a une.
Trois clients qui ne sont pas des personnes
Depuis deux ans, la même demande revient sous trois formes. Un traitement planifié doit mettre à jour un jeu de données chaque nuit. Une chaîne d'intégration continue doit publier le résultat d'une construction logicielle. Et, plus récemment, un agent conversationnel doit rédiger des pages, lire des jeux de données, piloter un navigateur.
La réponse habituelle tient en une ligne : on crée un compte, on génère une clé d'API, on la colle dans la configuration du système. Elle a fonctionné tant que ces clients étaient rares, stables, et administrés par la même personne que la plateforme.
Elle tient mal aujourd'hui, pour une raison qui n'est pas théorique : un agent lit sa propre configuration. Il cherche dans le dépôt, il affiche son environnement, il ouvre un fichier pour comprendre pourquoi un appel échoue. Ce qu'il lit entre dans son contexte, et de là dans des transcriptions, des journaux, et chez le fournisseur du modèle. Une clé d'API de longue durée placée sur le chemin d'un agent est une clé qui finira lue.
Trois chemins de fuite, un seul remède
Un secret de longue durée fuit par trois chemins, qui n'ont pas les mêmes parades.
- Le journal : une requête tracée, une erreur verbeuse, une variable d'environnement recopiée dans un rapport de plantage.
- Le dépôt : un fichier de configuration versionné par distraction, une capture d'écran dans un ticket.
- Le contexte du modèle : le chemin nouveau, et le seul qui ne se referme pas - ce que l'agent lit a déjà quitté la machine au moment où il le lit.
Contre le premier, on filtre. Contre le deuxième, on analyse les dépôts. Contre le troisième, il n'existe pas de filtre : le contexte est le canal de sortie. La seule parade qui vaille pour les trois est donc la même - faire en sorte qu'il n'y ait pas de secret durable à fuiter. Non pas mieux cacher la clé, mais la remplacer par quelque chose qui expire tout seul.
Le secteur a déjà tranché
Ce problème n'est pas le nôtre, et il est largement résolu ailleurs. La ligne de partage passe entre ce que le client détient en permanence et ce qu'il prouve à chaque fois.
D'un côté, les approches à secret permanent : la clé d'API, et le mode client_credentials d'OAuth 2.0 - qui échange certes un secret contre un jeton court, mais laisse le secret d'origine dans la configuration du client. Le jeton est éphémère, sa racine ne l'est pas.
De l'autre, les approches où le client ne détient aucun secret partagé. SPIFFE et SPIRE délivrent à chaque charge de travail une identité vérifiable, portée par l'infrastructure. Kubernetes projette dans chaque conteneur un jeton de compte de service à durée courte, lié à une audience précise, signé par le cluster et vérifiable contre sa clé publique. Les chaînes d'intégration continue font désormais la même chose : un jeton OIDC émis par la forge, présenté au fournisseur d'informatique en nuage, échangé contre des droits temporaires.
Le point commun de cette famille : le client prouve son identité avec quelque chose que son hôte lui a donné pour quelques minutes, et que personne n'a eu à copier-coller.
Reste une seconde question, dont on parle beaucoup moins : ce que la plateforme reconnaît une fois la preuve acceptée. Un justificatif vérifié peut n'ouvrir qu'un service, ou désigner une identité de plein exercice.
Un sous-ensemble délibérément étroit
Nous avons retenu le mécanisme standard de cette famille : la RFC 7523, le profil JWT d'OAuth 2.0. Le principe tient en quatre temps - faire confiance à un émetteur externe, vérifier son jeton contre sa clé publique publiée, associer le sujet porté par ce jeton à une identité locale, échanger le tout contre une session.
Nous en avons implémenté un sous-ensemble, et les deux choses que nous avons écartées disent mieux le choix que celles que nous avons retenues.
Pas OAuth complet. Pas de grant_type, pas de codes d'erreur normalisés, pas de serveur d'autorisation à part entière. Un seul point d'entrée qui fait une seule chose.
Pas SPIFFE. C'est probablement la meilleure réponse du marché quand on maîtrise l'infrastructure des deux côtés. Nous ne la maîtrisons pas : nos clients arrivent avec le cluster qu'ils ont, la forge qu'ils ont, et parfois un simple ordinateur portable. Le principe directeur est là : nous ne devenons jamais la source de l'identité, seulement son vérificateur.
Ce qu'une clé d'API ne sait pas être
Le protocole n'est que la moitié du sujet. L'autre moitié est une décision de modèle de données : un compte de service est stocké comme un utilisateur. Il en découle trois choses qu'une clé d'API ne sait pas offrir.
Une seule identité pour toute la plateforme. Une clé d'API est presque toujours attachée à un service ; un compte de service obtient une session qui vaut partout.
Les mêmes rôles que tout le monde. Un compte de service porte l'un des rôles de la plateforme, peut être rattaché à un département, et recevoir des permissions nominatives - exactement comme une personne.
Un nom et un visage. Dans un journal d'audit, un agent identifiable d'un coup d'œil ne se confond pas avec une collègue, là où une clé d'API ne laisse qu'un identifiant opaque.
Le prix de ce choix. Un utilisateur a une adresse e-mail ; un compte de service en reçoit une, synthétique et jamais distribuée. Une contrainte acceptée pour éviter d'entretenir un second type d'utilisateur.
Quatre vérifications, une session courte
Trois détails de cette chaîne méritent d'être explicités, parce qu'ils sont le fruit d'arbitrages.
L'audience est l'adresse du site appelé, pas celle du service d'authentification : un client Kubernetes peut réutiliser tel quel le jeton que son cluster lui projette déjà.
La vérification est atomique. Signature, émetteur, sujet, audience et expiration sont contrôlés en une seule opération, pas en cascade.
Le refus ne renseigne jamais. Qu'un identifiant n'existe pas, qu'une adresse soit interdite ou qu'une signature soit mauvaise, la réponse est la même, avec le même délai.
Deux scénarios en service
Ce ne sont pas des projections : les deux fonctionnent.
Sur un poste de travail. Un agent - nous utilisons opencode et Claude Code - pilote des outils locaux ordinaires : un navigateur via Playwright, curl, n'importe quel client HTTP. Aucun de ces outils ne connaît d'identifiant. Un petit service local détient la clé de signature, obtient les sessions et les injecte dans le trafic.
Dans un cluster. Pour un agent autonome, la même identité est obtenue par un composant compagnon qui ouvre la session et la renouvelle. S'y ajoutent une passerelle qui autorise les appels d'outils un par un, et un relais de sortie qui applique une liste de domaines autorisés. La ligne que tient cette architecture se résume ainsi : l'agent peut se servir de sa session, il ne peut pas l'emporter.
Ce qui reste quand la première ligne cède
Supposons le pire utile : une session valide se retrouve dans un journal ou une sortie d'intégration continue.
Elle dure environ deux minutes. Le plafond côté plateforme est de trente minutes, mais une session ne dépasse jamais l'expiration de l'assertion qui l'a obtenue - et notre service local signe des assertions de cent vingt secondes.
Elle ne se renouvelle pas. Aucun chemin de code ne prolonge la session d'un compte de service.
Elle peut être liée à son adresse d'origine. Sur option, la session n'est utilisable que depuis l'adresse qui l'a demandée. Nous la laissons désactivée par défaut, car elle suppose une adresse de sortie stable.
Et la session reste bridée. Cantonnée à une seule organisation, jamais administratrice de l'instance, refusée sur tous les points d'entrée qui modifient des comptes.
Les limites que nous assumons
Le contexte du modèle reste un canal de sortie. Tout ce qu'un agent lit part chez le fournisseur du modèle. Les identités non humaines bornent ce qu'un agent détourné peut faire ; elles ne réduisent pas ce qu'il peut voir.
L'assertion n'est pas protégée contre le rejeu. Interceptée pendant sa très courte validité, elle peut être rejouée ; la restriction par adresse est aujourd'hui la seule parade.
Plusieurs choses ne sont pas faites, volontairement : une identité appartient à une seule organisation ; le point d'entrée n'implémente pas OAuth complet ; seul le stockage principal gère ces comptes.
Pour aller plus loin
- Présentation fonctionnelle des comptes de service sur le site produit ;
- le code de
simple-directoryet denhi-proxy, publiés sous licence libre.