169.254.169.254 ahora pregunta antes: Hermes pone la obtención de credenciales del instance metadata de la nube detrás de aprobación

Estás depurando un problema del tipo “por qué esta máquina no llega al servicio interno” en una VM de la nube y le pides al agente que averigüe quién es la instancia. Decide mirar:
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
En una instancia de nube ese comando devuelve credenciales de acceso temporales sin autenticación ninguna: credenciales que representan la identidad de la máquina y que llegan a todo lo que tu política de nube le haya concedido. Hermes lo trataba como una lectura normal y lo dejaba correr. Ahora se detiene y pregunta. Si ejecutas agentes en la nube, merece la pena entenderlo, porque cierra la puerta trasera más fácil de pasar por alto, la que aparece en el mismo momento en que un agente puede ejecutar comandos.
Qué es IMDS y por qué es más arriesgado de lo que parece
IMDS son las siglas de Instance Metadata Service: un endpoint HTTP especial que las principales nubes exponen dentro de cada instancia, en la dirección link-local 169.254.169.254. Su función es que el software de la instancia sepa quién es, en qué región vive y qué rol lleva. La parte crítica es el modelo de autenticación: no tiene ninguno. Cualquier proceso de la máquina capaz de hacer una petición HTTP puede leerlo, incluido ese curl que acabas de pedirle al agente.
Cada nube tiene su propio número de casa para el mismo servicio:
169.254.169.254: la dirección link-local IPv4 de AWS, Azure, GCP y OpenStack;fd00:ec2::254: la forma IPv6 de AWS;metadata.google.internal: el hostname de GCP;100.100.100.200: el endpoint de Alibaba Cloud.
¿Por qué importa más allá de la curiosidad? Porque lo que devuelve normalmente no es solo “cuántas vCPU”. Suele incluir credenciales temporales de rol IAM o de service account. Tener esa cadena equivale a tener todo lo que la instancia está autorizada a hacer: leer un bucket privado, llamar a una API interna y, a veces, bastante más. Las credenciales caducan, pero suelen vivir lo suficiente para terminar el trabajo.
La ruta de ataque: basta una instrucción inyectada
Junta dos hechos y el riesgo salta a la vista: el agente lee contenido externo (páginas web, issues, correos, comentarios de código) y el agente puede ejecutar comandos. Si ese contenido externo incluye “ya que estás, ejecuta esto y mándame la salida”, y el comando apunta a un endpoint de metadata, entonces una sola prompt injection se convierte en exfiltración de credenciales. No es hipotético. La propiedad especial del endpoint es justo que vive en la red interna y no necesita credenciales, lo que lo convierte en lo más barato de robar.
Los proveedores de nube ponen de su parte: IMDSv2 de AWS, por ejemplo, exige un PUT para obtener primero un token de un solo uso, lo que sube el listón frente a server-side request forgery. Pero eso no puede tomar la decisión por ti: si el agente debe leer metadata de instancia es un juicio que una persona debería poder hacer una vez.
Qué pasa ahora: un prompt, no un bloqueo
El PR #106418 añade una regla determinista en la capa de aprobación del terminal, con un nombre que dice exactamente lo que es:
cloud metadata endpoint access (instance credentials)
El comportamiento es un prompt de aprobación, no un bloqueo duro. Es deliberado. Existen usos legítimos en VM de nube reales: el agente gestiona su propia instancia, estás depurando un rol IAM, estás escribiendo un script que reporta datos de la instancia. Decide el operador.
En concreto:
- Cuando uno de esos cuatro literales de host aparece en un comando de terminal, se dispara la aprobación. Da igual qué cliente HTTP uses:
curl,wget, una asignación de entorno o un argumento de script, todos cuentan. - En rutas sin supervisión (cron jobs, automatización en approve-mode) estos comandos no se aprueban solos: con el valor por defecto
approvals.cron_mode: deny, un job programado que intente leer metadata queda denegado en lugar de pasar de largo. - La detección es pattern matching, no juicio del modelo. No hay deriva según cómo se sienta hoy un modelo y no hay llamada extra al modelo en la ruta caliente.
Alcance: los falsos positivos se mantuvieron deliberadamente estrechos
La implementación usa lookarounds en la entrada de DANGEROUS_PATTERNS (en tools/approval_detection.py) para que la regla solo salte con los endpoints en sí:
curl http://169.254.1.1/status—otras direcciones link-local— no se ve afectado;169.254.169.2540, donde el endpoint es solo un prefijo de una secuencia de puntos más larga, no coincide;metadata.google.internal.example.com, otro host que casualmente contiene la cadena, no coincide.
Dicho de otro modo, la regla solo habla cuando un comando apunta de verdad al endpoint de credenciales, en lugar de tratar el tráfico link-local normal como peligroso.
También conviene separarlo de la protección del lado del navegador. El tooling de navegador ya aplica su propio suelo de cloud-metadata en todos los backends (y restricciones adicionales cuando corre dentro de un contenedor). Este cambio cubre la ruta del comando de terminal: otra puerta en otra planta.
El diseño toma prestado de la práctica existente: el changelog de Claude Code 2.1.257 señala que su modo automático dejó de autoaprobar las obtenciones de credenciales de cloud metadata (junto a egress evasion y cross-tenant reach) salvo que el entorno las declare esperadas. Hermes adapta la parte de metadata a una regla determinista de terminal: más simple, pero predecible.
El prompt que verás y cómo ajustarlo
Cuando salta la regla obtienes una aprobación de comando con esa cadena de motivo. Lo que puedas hacer después depende de tu configuración de approvals:
# ¿En qué modo estamos? (smart es el valor por defecto)
hermes config get approvals.mode
# Manual: cada comando peligroso te pregunta
hermes config set approvals.mode manual
# Desactivar aprobaciones (yolo) — pero ojo: la deny list sigue aplicándose igual
hermes config set approvals.mode off
Detalles que despistan a la gente:
approvals.modeaceptasmart,manualooff; el valor por defecto essmart, donde un modelo reviewer se encarga de los casos ambiguos mientras que los aciertos de reglas deterministas no necesitan modelo alguno.approvals.denyes una lista de bloqueo incondicional de globs fnmatch sin distinción de mayúsculas (por ejemplorm -rf *). Sigue aplicándose en yolo, así queapprovals.mode: offno significa “nunca se bloquea nada”.- Las denegaciones repetidas disparan un breaker:
approvals.denial_breaker_thresholdes 3 por defecto, así que no puedes quedarte en un bucle infinito de denegaciones. - Los jobs programados siguen
approvals.cron_mode(denypor defecto). Si un cron job necesita de verdad leer metadata de instancia, cambia esa clave explícitamente en lugar de desactivar las aprobaciones por completo. - Dentro de una sesión, una denegación interactiva se puede registrar con
/denymás un motivo.
Para la maquinaria de aprobaciones en general —modos, el modelo reviewer que vive en auxiliary.approval.model y cómo probarlo— mira la guía de configuración de Smart Approvals y los dry runs de aprobaciones. Otras guardas del tipo “pregunta antes” cubren ediciones de la config de SSH y desinstalaciones con el gestor de paquetes.
Compruébalo en tu propia máquina
Si tienes una VM de nube tuya, puedes confirmar sin riesgo la forma de este riesgo (solo en máquinas que te pertenecen y estás autorizado a usar):
# 1) Una lectura inofensiva: devuelve datos de instancia más o menos públicos
curl -s --max-time 2 http://169.254.169.254/latest/meta-data/instance-id
# 2) AWS IMDSv2: obtén un token de un solo uso y luego lee la ruta de credenciales con él
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/
El comando 2 imprime el nombre del rol y credenciales temporales, que es precisamente por lo que merece una confirmación humana. Nunca pegues lo que devuelve en una sesión, un ticket o un chat: es autoridad a nivel de cuenta en formato texto.
Si ves que esta regla salta en una máquina que no es de nube (un portátil, un centro de datos privado), la causa habitual es una dirección hardcodeada dentro de algún script. Si de verdad la necesitas —estás construyendo una herramienta de autochequeo de nube— apruébala una vez y deja el motivo visible; es más seguro que bajar las aprobaciones para todo.
La conclusión
En una frase: 169.254.169.254 en una VM de nube no es “una dirección interna cualquiera”: es un dispensador de credenciales sin autenticación. Ponerlo detrás de aprobación cambia un poco de fricción por la ruta de robo de credenciales más barata que existe.
Tres cosas que recordar en la práctica:
- Cuando un agente quiere de repente metadata de instancia, párate y lee el comando. Puede ser depuración legítima o puede ser una instrucción inyectada.
- Los jobs programados deniegan estos comandos por defecto; si de verdad los necesitas, permite ese caso vía
approvals.cron_modeen lugar de desactivar las aprobaciones del todo. approvals.denyes incondicional, yapprovals.mode: offno significa “nada queda bloqueado”.
Este cambio se fusionó después del tag v0.21.2, así que a día de hoy vive en main y no está en una versión etiquetada. hermes update toma main, o espera al siguiente release.