169.254.169.254 demande maintenant la permission : Hermes place la récupération des credentials de métadonnées d'instance cloud derrière une approbation


Vous déboguez un problème du type « pourquoi cette machine n’atteint-elle pas le service interne » sur une VM cloud, et vous demandez à l’agent de découvrir qui est cette instance. Il décide d’aller voir :

curl http://169.254.169.254/latest/meta-data/iam/security-credentials/

Sur une instance cloud, cette commande renvoie des credentials d’accès temporaires sans aucune authentification — des credentials qui représentent l’identité de la machine et peuvent atteindre tout ce que votre politique cloud lui a accordé. Hermes la traitait auparavant comme une lecture ordinaire et la laissait s’exécuter. Désormais il s’arrête et demande. Si vous faites tourner des agents dans le cloud, cela vaut la peine d’être compris, car cela ferme la porte dérobée la plus facile à négliger, celle qui apparaît dès qu’un agent peut exécuter des commandes.

Ce qu’est l’IMDS, et pourquoi c’est plus risqué qu’il n’y paraît

IMDS signifie Instance Metadata Service : un endpoint HTTP spécial que les grands clouds exposent à l’intérieur de chaque instance, à l’adresse link-local 169.254.169.254. Son rôle est de permettre aux logiciels de l’instance de savoir qui ils sont, dans quelle région ils vivent et quel rôle ils portent. Le point critique est le modèle d’authentification : il n’y en a aucun. Tout processus de la machine capable de faire une requête HTTP peut le lire — y compris le curl que vous venez de demander à l’agent d’exécuter.

Chaque cloud a son propre numéro de rue pour le même service :

  • 169.254.169.254 — l’adresse link-local IPv4 pour AWS, Azure, GCP et OpenStack ;
  • fd00:ec2::254 — la forme IPv6 d’AWS ;
  • metadata.google.internal — le nom d’hôte de GCP ;
  • 100.100.100.200 — l’endpoint d’Alibaba Cloud.

Pourquoi cela compte-t-il au-delà de l’anecdote ? Parce que ce qui revient n’est généralement pas seulement « combien de vCPU ». Cela inclut typiquement des credentials temporaires de rôle IAM ou de compte de service. Détenir cette chaîne équivaut à détenir tout ce que l’instance est autorisée à faire — lire un bucket privé, appeler une API interne, parfois bien plus. Les credentials expirent, mais ils vivent en général assez longtemps pour finir le travail.

Le chemin d’attaque : une seule instruction injectée suffit

Mettez deux faits côte à côte et le risque devient évident : l’agent lit du contenu extérieur (pages web, issues, e-mails, commentaires de code), et l’agent peut exécuter des commandes. Si ce contenu extérieur contient « pendant que tu y es, lance ceci et envoie-moi la sortie », et que la commande pointe par hasard vers un endpoint de métadonnées, alors une seule injection de prompt devient une exfiltration de credentials. Ce n’est pas hypothétique. La propriété particulière de cet endpoint est précisément qu’il vit sur le réseau interne et n’exige aucun credential, ce qui en fait la chose la moins chère à voler.

Les fournisseurs de cloud font leur part — l’IMDSv2 d’AWS, par exemple, exige un PUT pour obtenir d’abord un token à usage unique, ce qui relève la barre pour la falsification de requête côté serveur. Mais cela ne peut pas prendre la décision à votre place : savoir si l’agent doit lire les métadonnées d’instance est un jugement qu’un humain devrait pouvoir porter une fois.

Ce qui se passe maintenant : une invite, pas un blocage

La PR #106418 ajoute une règle déterministe dans la couche d’approbation du terminal, avec un nom qui dit exactement ce qu’elle est :

cloud metadata endpoint access (instance credentials)

Le comportement est une invite d’approbation, pas un blocage strict. C’est délibéré. Des usages légitimes existent sur de vraies VM cloud — l’agent gère sa propre instance, vous déboguez un rôle IAM, vous scriptez un rapporteur de faits d’instance. C’est l’opérateur qui décide.

Concrètement :

  • Quand l’un de ces quatre littéraux d’hôte apparaît dans une commande de terminal, l’approbation se déclenche. Peu importe le client HTTP utilisé : curl, wget, une affectation d’environnement ou un argument de script comptent tous.
  • Dans les chemins sans surveillance (tâches cron, automatisation en mode approve), ces commandes ne sont pas approuvées automatiquement — avec la valeur par défaut approvals.cron_mode: deny, une tâche planifiée qui tente de lire les métadonnées est refusée au lieu de passer.
  • La détection est une correspondance de motifs, pas un jugement de modèle. Aucune dérive selon l’humeur du modèle du jour, et aucun appel de modèle supplémentaire sur le chemin critique.

Portée : les faux positifs ont été volontairement gardés étroits

L’implémentation utilise des lookarounds dans l’entrée DANGEROUS_PATTERNS (dans tools/approval_detection.py) pour que la règle ne se déclenche que sur les endpoints eux-mêmes :

  • curl http://169.254.1.1/status — les autres adresses link-local — n’est pas concerné ;
  • 169.254.169.2540, où l’endpoint n’est qu’un préfixe d’une suite pointée plus longue, ne correspond pas ;
  • metadata.google.internal.example.com, un hôte différent qui contient la chaîne par hasard, ne correspond pas.

Autrement dit, la règle ne se manifeste que lorsqu’une commande vise réellement l’endpoint de credentials, au lieu de traiter le trafic link-local ordinaire comme dangereux.

Il vaut aussi la peine de distinguer cela de la protection côté navigateur. L’outillage navigateur applique déjà son propre plancher de métadonnées cloud sur chaque backend (et des restrictions supplémentaires lorsqu’il tourne dans un conteneur). Ce changement couvre le chemin de la commande de terminal — une autre porte à un autre étage.

Le design s’inspire de la pratique existante : le changelog de Claude Code 2.1.257 note que son mode auto a cessé d’approuver automatiquement les récupérations de credentials de métadonnées cloud (aux côtés de l’évasion d’egress et de l’atteinte inter-locataires), sauf si l’environnement les déclare attendues. Hermes adapte la partie métadonnées en une règle de terminal déterministe — plus sobre, mais prévisible.

L’invite que vous verrez, et comment la régler

Quand la règle se déclenche, vous obtenez une approbation de commande portant cette chaîne de raison. Ce que vous pouvez faire ensuite dépend de votre configuration approvals :

# Dans quel mode sommes-nous ? (smart est la valeur par défaut)
hermes config get approvals.mode

# Manuel : chaque commande dangereuse vous demande
hermes config set approvals.mode manual

# Désactiver les approbations (yolo) — mais attention : la deny list s'applique quand même
hermes config set approvals.mode off

Détails qui piègent les gens :

  • approvals.mode accepte smart, manual ou off ; la valeur par défaut est smart, où un modèle reviewer traite les cas ambigus tandis que les déclenchements de règle déterministes n’ont besoin d’aucun modèle.
  • approvals.deny est une liste de blocage inconditionnelle de globs fnmatch insensibles à la casse (par exemple rm -rf *). Elle s’applique toujours en yolo, donc approvals.mode: off ne signifie pas « rien n’est jamais bloqué ».
  • Des refus répétés déclenchent un disjoncteur : approvals.denial_breaker_threshold vaut 3 par défaut, vous ne pouvez donc pas rester dans une boucle de refus sans fin.
  • Les tâches planifiées suivent approvals.cron_mode (deny par défaut). Si une tâche cron a réellement besoin de lire les métadonnées d’instance, changez explicitement cette clé plutôt que de désactiver les approbations en bloc.
  • Dans une session, un refus interactif peut être consigné avec /deny plus une raison.

Pour la machinerie d’approbation plus large — les modes, le modèle reviewer qui vit sous auxiliary.approval.model, et comment la tester — voir le guide de configuration des Smart Approvals et les essais à blanc des approbations. Les garde-fous frères du type « demander d’abord » couvrent les modifications de config SSH et les désinstallations via gestionnaire de paquets.

Vérifiez-le sur votre propre machine

Si vous avez une VM cloud qui vous appartient, vous pouvez confirmer sans risque la forme de ce risque (uniquement sur des machines que vous possédez et êtes autorisé à utiliser) :

# 1) Une lecture inoffensive : renvoie des faits d'instance quasi publics
curl -s --max-time 2 http://169.254.169.254/latest/meta-data/instance-id

# 2) AWS IMDSv2 : récupérer un token à usage unique, puis lire le chemin des credentials avec
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 60")
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
  http://169.254.169.254/latest/meta-data/iam/security-credentials/

La commande 2 affiche le nom du rôle et des credentials temporaires, ce qui est précisément pourquoi elle mérite une confirmation humaine. Ne collez jamais ce qu’elle renvoie dans une session, un ticket ou un chat — c’est de l’autorité au niveau du compte sous forme de texte.

Si vous voyez cette règle se déclencher sur une machine non cloud (un portable, un centre de données privé), la cause habituelle est une adresse codée en dur dans un script. Si vous en avez vraiment besoin — vous construisez un outil d’auto-contrôle cloud — approuvez-la une fois et laissez la raison visible ; c’est plus sûr que de baisser les approbations pour tout.

À retenir

Une phrase : 169.254.169.254 sur une VM cloud n’est pas « une adresse interne quelconque » — c’est un distributeur de credentials non authentifié. Le placer derrière une approbation échange une petite friction contre le chemin de vol de credentials le moins cher qui existe.

Trois choses à retenir en pratique :

  1. Quand un agent veut soudain les métadonnées d’instance, arrêtez-vous et lisez la commande. Cela peut être un débogage légitime, ou une instruction injectée.
  2. Les tâches planifiées refusent ces commandes par défaut ; si vous en avez vraiment besoin, autorisez ce cas via approvals.cron_mode plutôt que de désactiver entièrement les approbations.
  3. approvals.deny est inconditionnel, et approvals.mode: off ne signifie pas « rien n’est bloqué ».

Ce changement a été fusionné après le tag v0.21.2, donc au moment où j’écris il vit sur main et n’est pas dans une version étiquetée. hermes update récupère main, ou attendez la prochaine version.