# Isolation des identifiants dans les pools de connexions HTTP pour les agents IA

Une connexion HTTP réutilisée n’entraîne pas automatiquement une fuite d’identifiants. La considérer comme inoffensive pousse les équipes à manquer la véritable défaillance : un objet client accumule des cookies, des réponses à des défis, des en-têtes de redirection, une identité de proxy ou une identité TLS, puis une requête ultérieure réutilise cet état avec un autre identifiant.

L’isolation des identifiants dans les pools de connexions HTTP consiste à décider précisément quel état peut passer d’une requête à l’autre et à rendre le reste impossible à partager. Le seul hôte de destination constitue généralement une frontière trop large pour un exécuteur d’actions ou un agent qui peut agir pour plusieurs comptes. Un client rapide qui envoie parfois le cookie du mauvais compte est pire qu’un client plus lent, car l’erreur ressemble à une requête normale réussie.

## Une socket transporte des données, elle n’appartient pas à un compte

Une connexion TCP transporte des octets pour une origine. Elle ne fournit pas à l’application une frontière de compte fiable. HTTP/1.1 réutilise la connexion de manière séquentielle, et HTTP/2 peut exécuter plusieurs flux en même temps, mais aucun de ces protocoles n’indique que toutes les requêtes d’une connexion appartiennent au même utilisateur ou au même compte de service.

Cette distinction compte, car les bibliothèques clientes placent souvent des sujets sans rapport derrière une interface pratique appelée session, agent, client ou transport. Les développeurs créent alors un de ces objets par hôte et ajoutent un jeton bearer avant chaque appel. L’en-tête bearer peut être correct à chaque fois, tandis qu’un stockage de cookies, un cache de défis Digest ou un contexte de proxy appartient discrètement au dernier compte qui a utilisé l’objet.

La RFC 9110 traite l’authentification comme un comportement de requête et d’espace de protection. Un client répond à un défi pour une origine, un schéma et un domaine d’authentification précis. Elle ne dit pas qu’une connexion ouverte possède un humain ou un compte de service. Si votre client fait cette hypothèse de propriété, elle vient de votre code ou de sa bibliothèque, pas de HTTP.

La réutilisation des connexions reste souhaitable. Elle évite des négociations répétées, réduit l’épuisement des ports et aide un service distant sous charge. La bonne règle est plus précise : réutilisez une connexion uniquement entre des requêtes dont l’état lié à la connexion et géré par le client est volontairement compatible.

Les états à séparer se répartissent en deux groupes :

- L’état de requête comprend Authorization, Cookie, les en-têtes de compte, les corps de requête et les valeurs d’idempotence. L’appelant doit les reconstruire pour chaque action.
- L’état de connexion et du client comprend une entrée de pool, une session de proxy, la sélection du certificat client, le comportement de redirection, les cookies, les caches de défis et les paramètres du protocole. Le client doit délimiter ou désactiver chacun d’eux volontairement.

Un identifiant bearer utilisé uniquement dans un en-tête Authorization peut partager une connexion TCP avec un autre identifiant bearer si la bibliothèque envoie les en-têtes pour chaque requête et ne conserve aucun état de compte. C’est une affirmation limitée, pas une autorisation générale. Dès que le service définit aussi un cookie de session, redirige vers un autre hôte ou demande un certificat client, la conception du pool doit être réexaminée.

## Les cookies sont un état de compte, même quand leur nom paraît anodin

Un stockage de cookies est la source la plus fréquente de passage involontaire entre comptes. Les équipes voient une API à jeton bearer et supposent que les cookies ne comptent pas, jusqu’à ce qu’un répartiteur de charge, un point de terminaison de connexion interactif ou un service historique ajoute `Set-Cookie` à une réponse. Un client HTTP généraliste le stocke habituellement, sauf si vous lui demandez de ne pas le faire.

La RFC 6265 définit comment un agent utilisateur sélectionne les cookies selon le domaine, le chemin, les attributs de sécurité et des règles associées. Son modèle de stockage ne comporte aucun champ pour « l’enregistrement d’identifiant qui a reçu ce cookie ». Deux identifiants bearer appelant le même hôte et le même chemin peuvent donc correspondre au même cookie stocké. Le protocole suit parfaitement ses règles tandis que votre application mélange les comptes.

Voici une trace issue d’un service de test. Le compte alpha utilise un jeton bearer et reçoit un cookie d’affinité :

```http
GET /v1/whoami HTTP/1.1
Host: api.example.test
Authorization: Bearer alpha-token

HTTP/1.1 200 OK
Set-Cookie: route=alpha-node; Path=/; Secure; HttpOnly
Content-Type: application/json

{"account":"alpha"}
```

L’action suivante sélectionne beta. Si un stockage partagé envoie le cookie conservé, la requête porte deux signaux de propriété :

```http
GET /v1/whoami HTTP/1.1
Host: api.example.test
Authorization: Bearer beta-token
Cookie: route=alpha-node
```

Le meilleur scénario est un service qui rejette cette incohérence. Des services moins rigoureux font passer la requête de beta par la session persistante d’alpha, l’attachent au mauvais contexte côté serveur ou acceptent l’en-tête qui l’emporte. Le client ne peut pas compter sur le service distant pour sauver une requête mélangée.

Pour les API entre machines, ma règle par défaut est directe : désactivez le stockage ambiant des cookies. Si une API exige réellement des cookies, créez un stockage de cookies pour un enregistrement d’identifiant et un contexte de service prévu. Ne le partagez pas simplement parce que la chaîne de l’hôte correspond.

Vérifiez aussi les entrées de la requête avant d’injecter l’identifiant. Un agent, un plugin ou un appelant ne doit pas pouvoir fournir son propre en-tête `Cookie` et le voir survivre dans un appel avec identifiant. Supprimez les en-têtes d’authentification et les cookies ambiants, puis ajoutez uniquement les en-têtes permis par la définition de l’action. Sinon, vous avez construit une isolation autour d’une porte que les appelants peuvent contourner.

## Les défis d’authentification nécessitent un appelant actuel

Une réponse 401 n’est pas qu’un code d’erreur. Elle peut inviter le client à réessayer après avoir choisi des identifiants, et cette logique de sélection vit souvent hors du code qui a créé la requête initiale. Les gestionnaires d’authentification Basic et Digest sont particulièrement sujets à ce schéma, mais un middleware personnalisé peut faire la même erreur avec des jetons bearer.

Une mauvaise implémentation conserve un `currentCredential` mutable sur un client partagé. La requête A reçoit un défi, le gestionnaire charge le secret d’alpha et le client réessaie. La requête B démarre avant la fin de cette nouvelle tentative et remplace `currentCredential` par beta. Le gestionnaire de défis se trouve alors confronté à une condition de concurrence que les suites de tests manquent, car elles exécutent les requêtes une par une.

Ne corrigez pas cela en plaçant un verrou autour d’un champ global d’identifiant. Le verrou sérialise le travail et laisse tout de même le mauvais objet responsable de l’identité. Attachez l’enregistrement d’identifiant au contexte de la requête, faites-le suivre à chaque nouvelle tentative et rejetez toute nouvelle tentative dont le contexte est absent.

L’authentification Digest mérite une vigilance particulière. Elle implique des nonces, des domaines d’authentification, des compteurs et une réponse calculée. Ces valeurs sont liées à l’espace de protection qui a émis le défi, pas à un client partagé générique. L’authentification Basic est plus simple sur le réseau, mais un cache qui l’ajoute automatiquement aux requêtes ultérieures doit respecter la même portée d’identifiant.

Utilisez un point de terminaison de test qui renvoie un domaine d’authentification ou un défi distinct pour chaque compte. Exécutez ensuite des requêtes simultanées et vérifiez que chaque nouvelle tentative utilise l’enregistrement d’identifiant associé à sa propre action. Un test qui vérifie seulement le statut final 200 est trop faible. Capturez l’identité de la requête côté serveur et échouez si le défi d’alpha produit une tentative d’autorisation de beta.

Ne confondez pas une 403 avec un défi. Les serveurs utilisent 403 pour de nombreuses décisions d’autorisation, et les clients ne doivent pas la traiter comme une permission de chercher un autre identifiant. Le basculement automatique entre identifiants transforme un échec d’accès en exploration de comptes, ce qui est dangereux et difficile à auditer.

## HTTP/2 rend les erreurs de propriété plus faciles à dissimuler

HTTP/2 permet à plusieurs flux de partager une connexion TLS. C’est efficace, mais cela fait disparaître l’ancien repère visuel d’une requête qui attend derrière une autre. Un client peut envoyer des actions pour alpha et beta au même instant, et leurs en-têtes ne restent distincts que si la bibliothèque les modélise comme des requêtes séparées jusqu’au bout.

La RFC 9113 interdit les champs d’en-tête propres aux connexions HTTP/1.1, comme `Connection`, dans les requêtes HTTP/2. Cette règle ne donne pas une portée de compte aux cookies ou à l’authentification. Elle empêche seulement qu’une catégorie de comportement entre sauts soit exprimée sous forme d’en-têtes de requête ordinaires. Votre client reste responsable de la sélection des cookies, des nouvelles tentatives, des redirections et de tout middleware partagé.

La coalescence des connexions HTTP/2 ajoute une autre subtilité. Certains clients peuvent utiliser une connexion sécurisée pour plusieurs origines lorsque le certificat et les contrôles de nom l’autorisent. La coalescence ne rejoue pas d’elle-même un en-tête Authorization. Elle signifie qu’une identité de pool basée sur une comparaison d’hôte approximative peut ne pas décrire la connexion réellement choisie par le client. Séparez les décisions d’autorisation d’origine de l’optimisation du transport, et testez le comportement de la bibliothèque déployée.

La compression des en-têtes inquiète aussi les gens pour de mauvaises raisons. HPACK compresse les en-têtes au sein d’une connexion HTTP/2, et QPACK fait un travail similaire pour HTTP/3. Un client conforme ne décode pas la valeur Authorization d’une requête précédente dans une requête ultérieure. Le danger vient de la réutilisation directe d’état dans votre code client, ainsi que d’éventuels enjeux de métadonnées dans des modèles de menace inhabituels. Marquez les en-têtes sensibles comme jamais indexés lorsque votre bibliothèque offre ce contrôle, mais n’y voyez pas un remplacement de l’isolation des requêtes.

HTTP/3 remplace TCP par QUIC pour le transport. Il ne change pas la règle de propriété. Un pool qui laisse un stockage de cookies ou un cache d’identifiants circuler librement entre les actions reste erroné avec QUIC.

Les certificats clients sont différents. Un certificat client TLS est choisi lors de l’établissement de la connexion, il est donc réellement lié à la connexion. Ne multiplexez jamais différentes identités de certificat client sur la même connexion ou la même entrée de pool. Intégrez l’enregistrement du certificat à l’identité du pool, ou donnez à chaque certificat une instance cliente distincte. La même prudence vaut pour un proxy qui authentifie une connexion avant de transférer les requêtes.

## Rendez l’identité du pool explicite dans le code

L’identité du pool doit inclure toute valeur qui change ce qu’une nouvelle connexion peut signifier sans danger. Pour une API utilisant uniquement des jetons bearer, avec cookies désactivés et sans identité de proxy, il peut s’agir de l’origine et d’un identifiant d’enregistrement d’identifiant. Pour un certificat client, un chemin authentifié par proxy ou un profil TLS particulier, incluez aussi ces identifiants.

N’utilisez pas le texte du secret comme identifiant de carte. Cela crée des copies inutiles du secret en mémoire et dans les journaux, complique le renouvellement et incite quelqu’un à afficher la carte pendant le débogage. Utilisez un identifiant opaque d’enregistrement d’identifiant, dont le cycle de vie est contrôlé par votre coffre ou votre registre d’actions.

Cette esquisse TypeScript montre la forme à adopter. Elle utilise le `Pool` d’Undici, mais la frontière s’applique à toute bibliothèque cliente. Le `credentialId` est un identifiant, pas la valeur bearer.

```ts
import { Pool } from "undici";

type Boundary = {
  origin: string;
  credentialId: string;
  proxyId?: string;
  clientCertificateId?: string;
};

const pools = new Map<string, Pool>();

function poolId(boundary: Boundary): string {
  return [
    boundary.origin,
    boundary.credentialId,
    boundary.proxyId ?? "direct",
    boundary.clientCertificateId ?? "none"
  ].join("\u001f");
}

function poolFor(boundary: Boundary): Pool {
  const id = poolId(boundary);
  let pool = pools.get(id);
  if (!pool) {
    pool = new Pool(boundary.origin);
    pools.set(id, pool);
  }
  return pool;
}

function headersFor(token: string, input: HeadersInit = {}): Headers {
  const headers = new Headers(input);
  headers.delete("authorization");
  headers.delete("cookie");
  headers.delete("proxy-authorization");
  headers.set("authorization", `Bearer ${token}`);
  return headers;
}
```

L’esquisse empêche un en-tête Authorization ou Cookie fourni par l’appelant d’accompagner l’identifiant injecté. Elle n’implémente pas de stockage de cookies, de redirections, de répartition par proxy ni de configuration de certificat. Cette omission est volontaire : chacun de ces éléments exige une décision consciente plutôt qu’un réglage par défaut accidentel.

Si tous les identifiants parlent au même point de terminaison public anonyme, des pools séparés peuvent être inutiles. Si le point de terminaison voit différents comptes, utilisez des entrées de pool séparées jusqu’à disposer d’une raison précise et de preuves pour les partager. Un pool coûte peu face à une confusion de comptes.

Le renouvellement exige sa propre règle. Quand un enregistrement d’identifiant change, cessez d’attribuer de nouvelles actions à son ancien pool. Laissez les requêtes déjà envoyées se terminer sous leur enregistrement initial si la sémantique de vos actions le permet, puis fermez ce pool. Un renouvellement change l’autorité, il ne corrige pas le sens d’un travail déjà commencé.

Une recommandation populaire conseille de désactiver keep-alive partout. Cela réduit le nombre d’objets de transport partagés, ce qui paraît sûr après un incident. Cela masque aussi la question de savoir si les cookies, le code de redirection et les caches de défis sont correctement délimités, tout en créant une nouvelle charge de négociation pour chaque requête. Conservez les pools, rendez leur propriété explicite et testez les parcours difficiles.

## Les redirections peuvent envoyer une requête propre au mauvais endroit

La gestion des redirections est un second client caché dans le premier. Une bibliothèque reçoit une réponse 301, 302, 303, 307 ou 308, construit une autre requête et décide quels en-têtes survivent. Si cette décision intervient après votre code de frontière d’identifiant, elle peut transporter un en-tête de compte ou un cookie vers une destination imprévue.

Pour les actions avec identifiants injectés, commencez avec des redirections désactivées ou manuelles. Examinez l’origine cible avant d’en suivre une. N’autorisez les redirections que lorsque la définition de l’action les prévoit, que la destination appartient à son ensemble d’origines approuvées et que le client reconstruit les en-têtes depuis le contexte d’identifiant initial au lieu de copier un ancien lot d’en-têtes.

Le code de statut compte. Une 303 peut changer une requête en GET dans le comportement habituel des navigateurs, tandis que 307 et 308 conservent la méthode et le corps. Un client qui réessaie et les traite tous de la même façon peut répéter une écriture avec identifiant vers un autre point de terminaison. C’est un problème d’intégrité de l’action, même si aucun secret ne traverse une origine.

Supprimez les en-têtes Authorization et Cookie à chaque redirection inter-origine. Dans de nombreux clients, c’est déjà le comportement par défaut, mais une valeur par défaut n’est pas un résultat de test. Vérifiez la version et la configuration utilisées. Les redirections de même origine nécessitent aussi une liste de chemins autorisés lorsqu’un identifiant donne davantage d’autorité que le point de terminaison initial ne devrait en recevoir.

L’authentification du proxy demande le même traitement. `Proxy-Authorization` appartient au chemin de proxy sélectionné, pas au service d’origine. Ne le placez jamais dans une carte générique d’en-têtes par défaut. Si deux actions atteignent la même API via des proxys authentifiés différents, séparez leurs entrées de pool et rendez la sélection du proxy visible dans l’enregistrement de l’action.

## Un test d’isolation échoué a une forme reconnaissable

La plupart des équipes testent qu’alpha peut appeler l’API et que beta peut appeler l’API. Cela prouve que les identifiants fonctionnent. Cela ne prouve pas que le même client de longue durée peut passer d’alpha à beta sans emporter d’état.

Créez un petit serveur de test avec deux comptes et faites-lui signaler ce qu’il a reçu. Il doit renvoyer le compte déduit de Authorization, faire écho à l’en-tête Cookie entrant, définir un cookie propre au compte et exposer un point de terminaison de redirection. Le service doit rejeter une requête lorsque l’étiquette de compte du cookie diffère de celle du compte bearer. Le rejet rend l’erreur visible au lieu de laisser une couche d’affinité la dissimuler.

Exécutez cette séquence avec la configuration client exacte utilisée en production :

1. Envoyez une requête alpha via un pool neuf. Confirmez que la réponse définit `route=alpha`.
2. Envoyez une requête beta via la recherche de pool utilisée en production. Vérifiez que le serveur voit l’autorisation beta et un en-tête Cookie vide, sauf si beta possède un stockage distinct ne contenant que des cookies beta.
3. Lancez simultanément des requêtes alpha et beta sur HTTP/2. Faites attendre chaque point de terminaison avant de répondre afin que leurs flux se chevauchent, puis vérifiez séparément les identités et les cookies.
4. Renvoyez un défi 401 à chaque compte et vérifiez que la nouvelle tentative utilise son enregistrement d’identifiant initial.
5. Renvoyez une redirection vers une autre origine et vérifiez que la requête de suivi ne porte ni en-tête d’autorisation ni en-tête de cookie.

Les vérifications doivent enregistrer davantage que les codes de statut. Capturez l’identifiant d’enregistrement sélectionné, l’identifiant du pool, l’origine, la version du protocole, la cible de redirection, la présence d’un cookie et le compte de réponse. Ne consignez pas l’identifiant lui-même. Lorsque le test échoue, ces champs permettent de savoir si la fuite vient de la recherche du pool, des en-têtes ambiants, d’un stockage de cookies ou du middleware de nouvelle tentative.

Testez aussi le renouvellement des identifiants. Lancez une requête qui attend côté serveur, remplacez l’enregistrement d’identifiant, puis envoyez une nouvelle requête. La nouvelle action doit utiliser une nouvelle identité de pool. Décidez et documentez si l’action en attente s’achève avec son autorité initiale ou est annulée. Les deux choix peuvent se défendre ; changer silencieusement l’autorité en cours de route ne l’est pas.

Exécutez le test avec toute configuration de proxy prise en charge. L’état du proxy et celui de l’origine vivent souvent dans des couches de bibliothèque différentes, ce qui fait d’un test unitaire rassurant un véritable test d’intégration utile.

## Les actions des agents nécessitent une surface d’autorité plus réduite

Un agent autonome de programmation doit demander une action comme « appeler cette API approuvée avec l’enregistrement d’identifiant billing-read ». Il ne doit pas recevoir un jeton et construire un client large, de longue durée, capable de conserver l’état de session après le changement de tâche. L’exécuteur d’actions peut lier la destination approuvée, la méthode, l’enregistrement d’identifiant et la frontière client avant même d’envoyer le moindre octet.

Sallyport applique cette séparation en conservant les identifiants API et SSH dans son coffre chiffré et en exécutant lui-même l’action : l’agent reçoit ainsi le résultat, pas l’identifiant. Ce confinement n’est utile que si le chemin HTTP traite aussi chaque enregistrement d’identifiant comme son propre contexte d’autorité.

L’approbation ne remplace pas l’isolation du client. Un opérateur peut approuver une action beta légitime, tandis qu’un stockage de cookies partagé négligemment transforme cette action en requête mêlant alpha et beta. L’enregistrement d’approbation documente alors une action différente de celle que l’opérateur voulait autoriser. Placez la frontière sous l’écran d’approbation, là où les en-têtes, les nouvelles tentatives et la sélection du transport ont réellement lieu.

Pour une passerelle d’actions, consignez l’identifiant d’enregistrement et l’identité du pool dans l’événement d’audit, jamais le secret. Si un appelant signale une anomalie de compte, vous devez établir si l’exécuteur a sélectionné le bon identifiant et n’a attaché que l’état appartenant à cet enregistrement. Une piste d’activité inviolable aide à examiner cette question ; elle ne rend pas sûre une conception client ambiguë.

La première modification concrète est simple : trouvez chaque client HTTP partagé et listez ce qu’il mémorise au-delà d’une connexion ouverte. Si la réponse inclut des cookies, des réponses à des défis, des redirections, des certificats clients ou une identité de proxy, attribuez un propriétaire explicite à chaque élément avant que le prochain renouvellement de jeton ou la prochaine exécution simultanée d’agents ne transforme le bogue en problème coûteux.
