DEV Community

Cover image for 🛡️ IMDSv1 vs IMDSv2 en EC2: SSRF, métricas, riesgos y una migración segura
Terry Quispe Paniagua
Terry Quispe Paniagua

Posted on

🛡️ IMDSv1 vs IMDSv2 en EC2: SSRF, métricas, riesgos y una migración segura

¿Qué riesgo existe realmente al mantener IMDSv1 habilitado en una instancia EC2?

En 2019, el incidente de Capital One mostró que el acceso no autorizado al EC2 Instance Metadata Service (IMDS) puede formar parte de una cadena de ataque con consecuencias muy serias.

El incidente terminó afectando información de más de 100 millones de personas.

Más adelante veremos qué ocurrió y qué papel tuvo el metadata service.

Pero primero necesitamos entender una pieza que muchas instancias EC2 todavía pueden tener habilitada:

IMDSv1.

Migrar parece sencillo (a IMDSv2). En una instancia existente, desde la consola de EC2 podemos modificar las opciones de metadata y cambiar:

La propia consola muestra una advertencia importante antes de hacer el cambio:

Asegúrese de que la instancia no realiza llamadas a IMDSv1 antes de configurar IMDSv2 en obligatorio.

AWS incluso nos indica revisar la métrica MetadataNoToken de CloudWatch.

Esa advertencia resume perfectamente el problema que vamos a probar en este laboratorio.

Mantener IMDSv1 representa un riesgo de seguridad, pero exigir IMDSv2 sin revisar primero las dependencias puede romper aplicaciones, agentes, SDKs o scripts antiguos.

En este laboratorio vamos a revisar ambos lados:

La estrategia será:


¿Qué es EC2 Instance Metadata Service?

Amazon EC2 Instance Metadata Service, conocido como IMDS, permite que software ejecutándose dentro de una instancia EC2 consulte información sobre la propia instancia.

IMDS utiliza la dirección link-local:

169.254.169.254
Enter fullscreen mode Exit fullscreen mode

Por ejemplo, mientras IMDSv1 está permitido podemos consultar directamente distintos datos de la instancia.

AMI ID

curl http://169.254.169.254/latest/meta-data/ami-id
Enter fullscreen mode Exit fullscreen mode

Tipo de instancia

curl http://169.254.169.254/latest/meta-data/instance-type
Enter fullscreen mode Exit fullscreen mode

Hostname

curl http://169.254.169.254/latest/meta-data/hostname
Enter fullscreen mode Exit fullscreen mode

IAM role

Si la instancia tiene un instance profile asociado:

curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
Enter fullscreen mode Exit fullscreen mode

En nuestro laboratorio debería devolver:

IMDSLabRole
Enter fullscreen mode Exit fullscreen mode

Credenciales temporales IAM

El nombre del role forma parte del siguiente path:

ROLE=$(curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/)

curl "http://169.254.169.254/latest/meta-data/iam/security-credentials/$ROLE"
Enter fullscreen mode Exit fullscreen mode

La respuesta incluye las credenciales temporales asociadas al instance profile:

{
  "Code": "Success",
  "AccessKeyId": "ASIA...REDACTED",
  "SecretAccessKey": "REDACTED",
  "Token": "REDACTED",
  "Expiration": "..."
}
Enter fullscreen mode Exit fullscreen mode

Cuando una instancia tiene un IAM role asociado mediante un instance profile, las aplicaciones pueden obtener credenciales temporales desde IMDS sin almacenar access keys dentro del servidor.

Conceptualmente:

El bucket se muestra únicamente como ejemplo. Podría tratarse de cualquier recurso o servicio sobre el que el role tenga permisos.

Este mecanismo es completamente normal y forma parte del funcionamiento de muchos workloads sobre EC2.

El problema aparece cuando analizamos cómo se accede al metadata service.


IMDSv1 vs IMDSv2: ¿qué cambia realmente?

Con IMDSv1, consultar metadata es básicamente una petición HTTP directa.

Por ejemplo:

curl http://169.254.169.254/latest/meta-data/instance-id
Enter fullscreen mode Exit fullscreen mode

El flujo es muy simple:

Aplicación
     │
     │ GET
     ▼
169.254.169.254
     │
     ▼
Metadata
Enter fullscreen mode Exit fullscreen mode

No existe ningún token previo.

IMDSv2 cambia este comportamiento agregando una sesión basada en token.

Primero debemos solicitar el token:

TOKEN=$(curl -sS -X PUT \
  "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
Enter fullscreen mode Exit fullscreen mode

Luego utilizamos ese token en las consultas:

curl -sS \
  -H "X-aws-ec2-metadata-token: $TOKEN" \
  http://169.254.169.254/latest/meta-data/instance-id
Enter fullscreen mode Exit fullscreen mode

El flujo cambia a:

Consola vs API, CLI y Terraform

Aquí existe una diferencia de nombres que puede generar confusión.

Cuando modificamos una instancia existente desde la consola, AWS nos muestra:

IMDSv2 = Opcional
Enter fullscreen mode Exit fullscreen mode

o:

IMDSv2 = Obligatorio
Enter fullscreen mode Exit fullscreen mode

En cambio, mediante API, AWS CLI, SDKs o Infrastructure as Code, esta misma configuración se representa mediante HttpTokens.

La equivalencia es:

No son configuraciones diferentes.

Son dos formas de representar la misma opción de EC2.

Al lanzar una instancia nueva, la consola también puede mostrar esta configuración como Metadata version, por ejemplo V2 only (token required).

Esta se encuentra en la sección de detalles avanzados al final de las configuraciones.


¿Qué es el límite de saltos de respuesta HTTP PUT?

En la misma pantalla de configuración aparece:

Límite de saltos de respuesta HTTP PUT
Enter fullscreen mode Exit fullscreen mode

Esta opción está relacionada específicamente con el mecanismo de sesión de IMDSv2.

Recordemos que primero solicitamos un token:

El hop limit determina cuántos saltos de red puede recorrer la respuesta que contiene ese token.

AWS permite valores entre 1 - 64

En un escenario simple como nuestro laboratorio:

un valor de 1 es suficiente. Sin embargo, con contenedores puede existir una capa adicional.

En ese escenario, un hop limit de 1 puede impedir que la respuesta con el token llegue correctamente al proceso dentro del contenedor.

Por eso AWS recomienda considerar:

cuando IMDSv2 se utiliza desde entornos de contenedores.

En nuestro laboratorio utilizamos:

http_put_response_hop_limit = 1
Enter fullscreen mode Exit fullscreen mode

porque la aplicación se ejecuta directamente sobre la instancia EC2.

Es importante no confundir ambos parámetros:


¿Por qué IMDSv1 sigue representando un riesgo de seguridad?

IMDSv1 no es por sí mismo una vulnerabilidad.

El problema aparece cuando otra vulnerabilidad permite que un atacante consiga realizar peticiones desde la propia instancia.

Por ejemplo:

Si IMDSv1 está disponible, una petición HTTP sin token puede ser suficiente para consultar metadata.

Y si la instancia tiene un IAM role:

El impacto posterior dependerá completamente de los permisos asignados a ese role.

Obtener credenciales temporales de un role con permisos mínimos no equivale a comprometer toda una cuenta AWS.

Por eso este escenario también demuestra la importancia de aplicar IAM least privilege.


¿Qué es SSRF y por qué importa aquí?

Uno de los escenarios que puede permitir alcanzar IMDS es Server-Side Request Forgery, normalmente abreviado como SSRF.

La idea es relativamente sencilla.

Imaginemos una aplicación que permite recibir una URL para consultar su contenido.

Por ejemplo:

/fetch?url=https://example.com
Enter fullscreen mode Exit fullscreen mode

Desde nuestro equipo podríamos llamar:

curl "http://PUBLIC_IP:8080/fetch?url=https://example.com"
Enter fullscreen mode Exit fullscreen mode

Aquí existen realmente dos destinos diferentes.

El primero:

http://PUBLIC_IP:8080/fetch
Enter fullscreen mode Exit fullscreen mode

es nuestra aplicación ejecutándose en EC2.

El segundo:

url=https://example.com
Enter fullscreen mode Exit fullscreen mode

es un parámetro que indica qué URL debe consultar la aplicación desde el servidor.

El flujo normal sería:

Hasta aquí no existe ningún problema.

El riesgo aparece cuando la aplicación permite controlar libremente el valor de url.

En lugar de:

url=https://example.com
Enter fullscreen mode Exit fullscreen mode

podemos indicar:

url=http://169.254.169.254/latest/meta-data/instance-id
Enter fullscreen mode Exit fullscreen mode

La petición completa queda así:

curl "http://PUBLIC_IP:8080/fetch?url=http://169.254.169.254/latest/meta-data/instance-id"
Enter fullscreen mode Exit fullscreen mode

La parte crítica está después de ?url=:

http://PUBLIC_IP:8080/fetch
│
└── Aplicación vulnerable ejecutándose en EC2

?url=
│
└── Parámetro controlado por quien realiza la petición

http://169.254.169.254/latest/meta-data/instance-id
│
└── URL que le estamos indicando a la EC2 que consulte
Enter fullscreen mode Exit fullscreen mode

Nuestro equipo no está accediendo directamente al metadata service mediante:

169.254.169.254
Enter fullscreen mode Exit fullscreen mode

Estamos enviando una petición a la aplicación y consiguiendo que sea la propia EC2 quien realice una segunda petición.

El flujo real es:

Ese es el punto clave del SSRF:

la petición hacia el recurso interno la realiza el servidor, no el atacante directamente.

Esto también explica por qué una instancia EC2 no necesita tener una IP pública para que este riesgo exista.

Podríamos tener perfectamente:

La aplicación puede ser accesible mediante un ALB mientras la instancia continúa teniendo acceso local a IMDS.

SSRF está clasificado como:

CWE-918
Server-Side Request Forgery
Enter fullscreen mode Exit fullscreen mode

MITRE ATT&CK también contempla específicamente el acceso al metadata service de proveedores cloud:

Esto nos ayuda a separar correctamente los conceptos:

SSRF
  │
  │ vulnerabilidad / vector de ataque
  ▼
Instance Metadata
  │
  ▼
Credenciales
Enter fullscreen mode Exit fullscreen mode

IMDSv2 no corrige una vulnerabilidad SSRF.

Lo que hace es agregar una capa adicional de protección para dificultar determinados caminos hacia el metadata service.


Un caso real: Capital One

En 2019, Capital One sufrió uno de los incidentes más conocidos relacionados con este tipo de cadena de ataque.

Una debilidad en el procesamiento de determinadas peticiones permitió alcanzar recursos internos.

Desde allí fue posible acceder al EC2 Instance Metadata Service y obtener credenciales temporales asociadas a un IAM role.

Esas credenciales fueron utilizadas posteriormente para acceder a información almacenada en S3.

El incidente afectó información de más de 100 millones de personas.

Es importante no simplificar el incidente diciendo:

Capital One fue comprometido únicamente por utilizar IMDSv1.

Eso sería incorrecto.

Existió una cadena de condiciones:

IMDSv1 fue una pieza dentro de esa cadena.

IMDSv2 todavía no existía cuando ocurrió el incidente. AWS lo lanzó posteriormente en 2019 como una mejora de defensa en profundidad para EC2 Instance Metadata Service.

Hoy tenemos esa protección disponible.

Por eso la pregunta ya no debería ser si conviene mantener IMDSv1.

La pregunta es:

¿Cómo migramos a IMDSv2 sin romper los workloads que todavía dependen de IMDSv1?

Eso es precisamente lo que vamos a probar.


Arquitectura del laboratorio

Quiero mantener este laboratorio deliberadamente pequeño para concentrarnos en el comportamiento de IMDS y no en componentes adicionales.

Toda la infraestructura se despliega con Terraform.

⚠️ Disclaimer del laboratorio

Este laboratorio despliega intencionalmente una aplicación vulnerable a SSRF con fines educativos.

Ejecútalo únicamente en una cuenta o entorno AWS que controles o para el que tengas autorización explícita.

El Security Group limita el acceso al puerto 8080 a tu propia IP y el IAM role utilizado en el laboratorio no necesita permisos sobre recursos reales.

Durante las pruebas podremos consultar credenciales temporales desde IMDS para demostrar el riesgo. No publiques AccessKeyId, SecretAccessKey ni Token reales, aunque sean temporales.

Al finalizar las pruebas, revisa la sección Limpieza del laboratorio para eliminar los recursos y restaurar las configuraciones regionales modificadas durante el laboratorio.

El código está disponible aquí:

GitHub: https://github.com/RedCaraxes/aws-imdsv1-to-imdsv2-lab

Terraform crea:

Internet
   │
   │ TCP/8080
   │ Source: My IP
   ▼
Public Subnet
   │
   ▼
EC2 t3.nano
Amazon Linux 2023
   │
   ├── IMDSLabRole
   ├── Aplicación vulnerable de prueba
   │
   └── Instance Metadata Service

CloudWatch Dashboard
├── MetadataNoToken
└── MetadataNoTokenRejected
Enter fullscreen mode Exit fullscreen mode

El laboratorio incluye una VPC, una subnet pública, una instancia EC2 t3.nano, un Security Group que permite acceso al puerto 8080 únicamente desde nuestra IP y un IAM role llamado:

IMDSLabRole
Enter fullscreen mode Exit fullscreen mode

El role no necesita permisos sobre recursos reales.

El objetivo es demostrar que el metadata service puede exponer credenciales temporales asociadas al instance profile, no utilizar esas credenciales contra otros servicios.

La aplicación de prueba se instala automáticamente mediante user_data y expone:

/health
/legacy
/v2
/fetch?url=
Enter fullscreen mode Exit fullscreen mode

Cada endpoint tiene un propósito:

/health
   └── validar que la aplicación está disponible

/legacy
   └── realizar una consulta IMDSv1 sin token

/v2
   └── obtener un token y consultar IMDSv2 correctamente

/fetch?url=
   └── realizar una petición hacia una URL controlada
       por el usuario para demostrar SSRF
Enter fullscreen mode Exit fullscreen mode

La instancia comienza con:

Consola:
IMDSv2 = Opcional

Terraform:
http_tokens = "optional"

Resultado:
IMDSv1 ✅
IMDSv2 ✅
Enter fullscreen mode Exit fullscreen mode

También configuramos:

http_put_response_hop_limit = 1
Enter fullscreen mode Exit fullscreen mode

porque la aplicación se ejecuta directamente sobre la instancia y no dentro de un contenedor.

La IP pública existe únicamente para simplificar el laboratorio.

Como vimos anteriormente, una arquitectura real podría mantener la instancia en una subnet privada detrás de un Application Load Balancer.


Costo estimado del laboratorio

Este laboratorio utiliza recursos con costo.

En us-east-1, manteniendo la infraestructura desplegada durante aproximadamente 24 horas, el costo debería estar alrededor de:

EC2 t3.nano        ~ $0.125
IPv4 pública       ~ $0.120
EBS gp3 8 GiB      ~ $0.021
--------------------------------
Total aproximado   ~ $0.27 / día
Enter fullscreen mode Exit fullscreen mode

El tráfico generado por las pruebas es mínimo y las métricas MetadataNoToken y MetadataNoTokenRejected son métricas nativas de EC2.

El dashboard de CloudWatch puede quedar cubierto por la cuota gratuita de hasta tres dashboards de 50 métricas cada uno. Si la cuenta ya supera esa cuota, aplicará el precio estándar de CloudWatch.

El valor puede variar según la región, el tiempo que mantengas los recursos desplegados y las condiciones de facturación de tu cuenta.

Por eso, al terminar las pruebas recuerda ejecutar la sección Limpieza del laboratorio.


Levantando el laboratorio

Clonamos el repositorio:

git clone https://github.com/RedCaraxes/aws-imdsv1-to-imdsv2-lab.git
cd aws-imdsv1-to-imdsv2-lab
Enter fullscreen mode Exit fullscreen mode

Creamos nuestro archivo de variables:

cp terraform.tfvars.example terraform.tfvars
Enter fullscreen mode Exit fullscreen mode

Configuramos nuestra IP pública:

allowed_cidr = "YOUR_PUBLIC_IP/32"
Enter fullscreen mode Exit fullscreen mode

Y mantenemos inicialmente:

http_tokens = "optional"
Enter fullscreen mode Exit fullscreen mode

Desplegamos:

terraform init
terraform plan
terraform apply
Enter fullscreen mode Exit fullscreen mode

Luego obtenemos la IP pública:

export PUBLIC_IP=$(terraform output -raw public_ip)
Enter fullscreen mode Exit fullscreen mode

Podemos verificarla:

echo "$PUBLIC_IP"
Enter fullscreen mode Exit fullscreen mode

Y validar que la aplicación está funcionando:

curl "http://$PUBLIC_IP:8080/health"
Enter fullscreen mode Exit fullscreen mode

La respuesta debería ser:

ok
Enter fullscreen mode Exit fullscreen mode

Estado inicial: IMDSv1 e IMDSv2 habilitados

Como configuramos IMDSv2 como Opcional, nuestra instancia acepta inicialmente ambos mecanismos:

IMDSv1 ✅
IMDSv2 ✅
Enter fullscreen mode Exit fullscreen mode

Primero comprobaremos ambos comportamientos antes de probar el escenario SSRF.


Validando IMDSv1

El endpoint:

/legacy
Enter fullscreen mode Exit fullscreen mode

realiza una petición hacia IMDS sin utilizar token.

Ejecutamos:

curl -i "http://$PUBLIC_IP:8080/legacy"
Enter fullscreen mode Exit fullscreen mode

La respuesta debería devolver el instance-id:

HTTP/1.0 200 OK

i-xxxxxxxxxxxxxxxxx
Enter fullscreen mode Exit fullscreen mode

Esto demuestra que una petición sin token todavía puede acceder al metadata service.


Validando IMDSv2

Ahora usamos:

/v2
Enter fullscreen mode Exit fullscreen mode

Este endpoint primero obtiene correctamente un token y luego consulta IMDS utilizando ese token.

Ejecutamos:

curl -i "http://$PUBLIC_IP:8080/v2"
Enter fullscreen mode Exit fullscreen mode

La respuesta devuelve el mismo instance-id.

Conceptualmente:

Aplicación
    │
    │ PUT /latest/api/token
    ▼
IMDS
    │
    │ Token
    ▼
Aplicación
    │
    │ GET instance-id + token
    ▼
IMDS
Enter fullscreen mode Exit fullscreen mode

Tenemos entonces:

IMDSv1 ✅
IMDSv2 ✅
Enter fullscreen mode Exit fullscreen mode


Probando primero un destino normal

Antes de apuntar hacia IMDS, comprobemos el comportamiento normal del endpoint vulnerable.

Ejecutamos:

curl "http://$PUBLIC_IP:8080/fetch?url=https://example.com"
Enter fullscreen mode Exit fullscreen mode

El flujo es:

Nuestro equipo
     │
     ▼
Aplicación EC2
     │
     ▼
example.com
Enter fullscreen mode Exit fullscreen mode

La aplicación recibe el valor de:

url=
Enter fullscreen mode Exit fullscreen mode

y realiza la petición desde la instancia.

Ahora cambiaremos únicamente ese valor.


Probando IMDSv1 mediante SSRF

Intentemos consultar el instance-id:

curl "http://$PUBLIC_IP:8080/fetch?url=http://169.254.169.254/latest/meta-data/instance-id"
Enter fullscreen mode Exit fullscreen mode

Recordemos qué significa esta petición:

http://$PUBLIC_IP:8080/fetch
        │
        └── endpoint de nuestra aplicación

?url=
        │
        └── parámetro que controla el destino

http://169.254.169.254/latest/meta-data/instance-id
        │
        └── URL que la aplicación consultará desde la EC2
Enter fullscreen mode Exit fullscreen mode

Por tanto:

Nuestro equipo
     │
     │ solicita /fetch
     ▼
Aplicación vulnerable
     │
     │ consulta el valor enviado en ?url=
     ▼
169.254.169.254
     │
     ▼
Instance ID
Enter fullscreen mode Exit fullscreen mode

La respuesta devuelve:

i-xxxxxxxxxxxxxxxxx
Enter fullscreen mode Exit fullscreen mode

Ya demostramos que nuestra aplicación vulnerable puede alcanzar IMDSv1.


Accediendo al metadata del IAM role

Podemos consultar qué IAM role está asociado cambiando únicamente el destino enviado mediante url=:

curl "http://$PUBLIC_IP:8080/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/"
Enter fullscreen mode Exit fullscreen mode

La aplicación recibe:

url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
Enter fullscreen mode Exit fullscreen mode

y realiza esa petición desde la propia instancia.

La respuesta debería mostrar:

IMDSLabRole
Enter fullscreen mode Exit fullscreen mode

Existe otro endpoint debajo de ese path que contiene las credenciales temporales utilizadas por la instancia.

Una respuesta tendría campos similares a:

{
  "Code": "Success",
  "AccessKeyId": "ASIA...REDACTED",
  "SecretAccessKey": "REDACTED",
  "Token": "REDACTED"
}
Enter fullscreen mode Exit fullscreen mode

Nunca publiques esos valores reales.

En nuestro laboratorio no necesitamos utilizar esas credenciales contra otros recursos (aunque ya evidenciamos eso antes).

Ya tenemos la evidencia necesaria:

SSRF
  ↓
IMDSv1
  ↓
Metadata del IAM role
Enter fullscreen mode Exit fullscreen mode

Antes de migrar: revisar MetadataNoToken

Aquí retomamos la advertencia que vimos al inicio en la consola de EC2.

Antes de cambiar IMDSv2 de:

Opcional
Enter fullscreen mode Exit fullscreen mode

a:

Obligatorio
Enter fullscreen mode Exit fullscreen mode

AWS recomienda comprobar si todavía existen llamadas IMDSv1.

¿Cómo podemos saberlo?

EC2 publica de forma nativa en CloudWatch la métrica:

MetadataNoToken
Enter fullscreen mode Exit fullscreen mode

No necesitamos instalar CloudWatch Agent ni crear una métrica personalizada.

MetadataNoToken registra llamadas al metadata service realizadas sin token mientras IMDSv1 todavía está permitido.

Nuestro laboratorio ya debería estar generándolas.

Terraform crea un dashboard de CloudWatch para visualizar:

MetadataNoToken
MetadataNoTokenRejected
Enter fullscreen mode Exit fullscreen mode

Es importante aclarar que Terraform no crea ni habilita estas métricas.

Son métricas nativas de EC2.

El dashboard existe únicamente para hacer más cómoda la visualización durante la transición.

Después de nuestras pruebas observamos:

MetadataNoToken > 0
Enter fullscreen mode Exit fullscreen mode

Eso confirma que existen llamadas a IMDS sin token.

En nuestro laboratorio sabemos exactamente de dónde vienen.

En producción podría tratarse de:

script legacy
AWS SDK antiguo
agente de monitoreo
aplicación
proceso de bootstrap
user-data
herramientas personalizadas
Enter fullscreen mode Exit fullscreen mode

Por eso:

MetadataNoToken > 0
Enter fullscreen mode Exit fullscreen mode

significa que no deberíamos exigir IMDSv2 todavía sin investigar.


¿Cuánto tiempo deberíamos observar la métrica?

Ver:

MetadataNoToken = 0
Enter fullscreen mode Exit fullscreen mode

durante cinco minutos no demuestra necesariamente que el workload ya no depende de IMDSv1.

Un proceso podría ejecutarse:

cada hora
diariamente
durante un deployment
durante el arranque de la instancia
durante un backup
durante un failover
Enter fullscreen mode Exit fullscreen mode

El período de observación debe representar el comportamiento real del workload.

Conceptualmente:

MetadataNoToken > 0
        │
        ▼
Identificar dependencia
        │
        ▼
Actualizar aplicación / SDK / script
        │
        ▼
Observar nuevamente
        │
        ▼
MetadataNoToken = 0
durante un período representativo
Enter fullscreen mode Exit fullscreen mode

Recién entonces podemos avanzar.


Simulando una dependencia legacy

En nuestro laboratorio:

/legacy
Enter fullscreen mode Exit fullscreen mode

representa un software antiguo que consulta IMDS sin token.

Ejecutamos:

curl "http://$PUBLIC_IP:8080/legacy"
Enter fullscreen mode Exit fullscreen mode

Mientras IMDSv2 esté configurado como Opcional, el endpoint funciona.

Conceptualmente está haciendo algo equivalente a:

curl http://169.254.169.254/latest/meta-data/instance-id
Enter fullscreen mode Exit fullscreen mode

Una implementación compatible con IMDSv2 necesita primero obtener un token:

TOKEN=$(curl -sS -X PUT \
  "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
Enter fullscreen mode Exit fullscreen mode

y posteriormente utilizarlo:

curl -sS \
  -H "X-aws-ec2-metadata-token: $TOKEN" \
  http://169.254.169.254/latest/meta-data/instance-id
Enter fullscreen mode Exit fullscreen mode

En una migración real aquí deberíamos actualizar las dependencias que todavía utilizan IMDSv1:

scripts
SDKs
agentes
aplicaciones
Enter fullscreen mode Exit fullscreen mode

hasta eliminar las llamadas sin token.


Exigiendo IMDSv2

Una vez identificadas y corregidas las dependencias podemos realizar el cambio que vimos al inicio del artículo.

En la consola:

IMDSv2

Opcional
   ↓
Obligatorio
Enter fullscreen mode Exit fullscreen mode

Mediante API o AWS CLI:

HttpTokens

optional
   ↓
required
Enter fullscreen mode Exit fullscreen mode

Y en nuestro Terraform cambiamos (terraform.tfvars):

http_tokens = "optional"
Enter fullscreen mode Exit fullscreen mode

por:

http_tokens = "required"
Enter fullscreen mode Exit fullscreen mode

Aplicamos:

terraform plan
terraform apply
Enter fullscreen mode Exit fullscreen mode

La modificación se aplica in-place sobre la instancia.

No requiere reemplazarla ni reiniciarla, y EC2 continúa ejecutándose durante el cambio.

Sin embargo, eso no significa que el cambio sea transparente para el workload.

En cuanto HttpTokens = required queda aplicado, cualquier aplicación, agente o script que todavía dependa de IMDSv1 comenzará a recibir rechazos.

Ahora el comportamiento esperado es:

IMDSv1 ❌
IMDSv2 ✅
Enter fullscreen mode Exit fullscreen mode

¿Qué podría haberse roto?

Después del cambio volvemos a ejecutar:

curl -i "http://$PUBLIC_IP:8080/legacy"
Enter fullscreen mode Exit fullscreen mode

El endpoint intenta realizar la misma llamada sin token.

Pero IMDSv1 ya no está permitido.

Nuestro pequeño workload legacy deja de funcionar.

Ahora imaginemos que ese mismo comportamiento formara parte de:

inicio de una aplicación
deployment
monitoreo
obtención de credenciales
automatización
Enter fullscreen mode Exit fullscreen mode

El cambio de seguridad podría convertirse directamente en un incidente.

Por eso:

Un cambio hacia una configuración más segura todavía puede convertirse en una migración insegura.

El objetivo no es solamente activar IMDSv2.

El objetivo es hacerlo sin romper los workloads.


Validando que IMDSv2 continúa funcionando

Después de exigir IMDSv2 repetimos:

curl -i "http://$PUBLIC_IP:8080/v2"
Enter fullscreen mode Exit fullscreen mode

Este endpoint obtiene primero un token de IMDSv2 y lo utiliza para consultar el metadata service.

Después de configurar HttpTokens = required, la petición continúa devolviendo correctamente el instance-id.

Esto confirma un punto importante:

exigir IMDSv2 no deshabilita Instance Metadata Service.

Lo que cambia es el mecanismo de acceso. /v2 obtiene el token y lo utiliza en la petición hacia IMDS, mientras que los clientes que intentan acceder sin token son rechazados.

Conceptualmente:

IMDSv1 ❌
IMDSv2 ✅
Enter fullscreen mode Exit fullscreen mode

El problema no es el metadata service.

El problema son los clientes que todavía intentan acceder sin token.


Probando nuevamente el mismo SSRF

Ahora repetimos exactamente la misma petición utilizada antes:

curl "http://$PUBLIC_IP:8080/fetch?url=http://169.254.169.254/latest/meta-data/instance-id"
Enter fullscreen mode Exit fullscreen mode

La aplicación vulnerable sigue recibiendo:

url=http://169.254.169.254/latest/meta-data/instance-id
Enter fullscreen mode Exit fullscreen mode

y sigue intentando realizar:

GET http://169.254.169.254/latest/meta-data/instance-id
Enter fullscreen mode Exit fullscreen mode

Pero ahora IMDS exige primero:

PUT /latest/api/token
Enter fullscreen mode Exit fullscreen mode

y posteriormente espera:

X-aws-ec2-metadata-token
Enter fullscreen mode Exit fullscreen mode

La misma SSRF simple que anteriormente podía consultar metadata deja de funcionar.

Conceptualmente:

Pero esto no significa que hayamos corregido nuestra aplicación.

La aplicación sigue teniendo SSRF.

IMDSv2 agrega defensa en profundidad alrededor del metadata service.

La vulnerabilidad de la aplicación también debe ser corregida.


Después de migrar: MetadataNoTokenRejected

Cuando IMDSv2 está configurado como:

Obligatorio
Enter fullscreen mode Exit fullscreen mode

la configuración correspondiente es:

HttpTokens = required
Enter fullscreen mode Exit fullscreen mode

Las llamadas IMDSv1 ya no son aceptadas.

Aquí aparece otra métrica nativa de EC2:

MetadataNoTokenRejected
Enter fullscreen mode Exit fullscreen mode

Esta métrica registra intentos de acceder al metadata service sin token después de que IMDSv2 pasó a ser obligatorio.

Nuestra prueba:

curl "http://$PUBLIC_IP:8080/legacy"
Enter fullscreen mode Exit fullscreen mode

genera precisamente este tipo de intento.

La prueba SSRF también continúa intentando llegar a IMDS sin token.

Conceptualmente:

En CloudWatch deberíamos observar:

MetadataNoTokenRejected > 0
Enter fullscreen mode Exit fullscreen mode

Esto nos da observabilidad en ambos lados del cambio:

ANTES DE LA MIGRACIÓN

¿Todavía existen llamadas IMDSv1?
          │
          ▼
MetadataNoToken


DESPUÉS DE LA MIGRACIÓN

¿Algo todavía intenta utilizar IMDSv1?
          │
          ▼
MetadataNoTokenRejected
Enter fullscreen mode Exit fullscreen mode

Evitando que IMDSv1 vuelva a aparecer

Ya migramos nuestra instancia.

Pero todavía existe otro problema.

Nada evita necesariamente que alguien lance mañana otra EC2 permitiendo nuevamente IMDSv1.

Por eso el último paso debe ser evitar regresiones.


A nivel de instancia

Para una instancia específica:

HttpTokens = required
Enter fullscreen mode Exit fullscreen mode

es suficiente para exigir IMDSv2 en ese recurso.

Pero administrar esto instancia por instancia no escala.


Valores predeterminados a nivel de cuenta

Podemos establecer IMDSv2 como valor predeterminado regional:

aws ec2 modify-instance-metadata-defaults \
  --region us-east-1 \
  --http-tokens required
Enter fullscreen mode Exit fullscreen mode

Podemos comprobar la configuración con:

aws ec2 get-instance-metadata-defaults \
  --region us-east-1
Enter fullscreen mode Exit fullscreen mode

Pero existe una diferencia importante:

Un valor predeterminado no necesariamente implica que la configuración sea obligatoria.

Un parámetro especificado explícitamente durante el lanzamiento puede tener mayor precedencia que el valor predeterminado de cuenta.


Exigir IMDSv2 a nivel de cuenta

AWS también permite exigir el comportamiento:

aws ec2 modify-instance-metadata-defaults \
  --region us-east-1 \
  --http-tokens required \
  --http-tokens-enforced enabled
Enter fullscreen mode Exit fullscreen mode

Podemos verificarlo:

aws ec2 get-instance-metadata-defaults \
  --region us-east-1
Enter fullscreen mode Exit fullscreen mode

El resultado debería incluir algo similar a:

{                                                                                                                                                 
    "AccountLevel": {
        "HttpTokens": "required",
        "ManagedBy": "account",
        "HttpTokensEnforced": "enabled"
    }
}
Enter fullscreen mode Exit fullscreen mode

Con enforcement habilitado, una nueva instancia no puede lanzarse si el resultado fuera permitir IMDSv1.

Pero existe un detalle importante:

habilitar enforcement a nivel de cuenta no migra automáticamente las instancias existentes que todavía tienen HttpTokens = optional.

Esas instancias deben analizarse y migrarse.

Desde la consola también podemos configurarlo desde EC2 → Atributos de la cuenta.


Resultado final

Comenzamos con:

Aplicación
     │
     │ SSRF
     ▼
IMDSv1
     │
     ▼
Metadata del IAM role
Enter fullscreen mode Exit fullscreen mode

Pero también vimos que exigir IMDSv2 sin revisar previamente nuestras dependencias puede romper software legítimo.

Por eso una migración controlada debería seguir:

La recomendación final no es simplemente:

El objetivo no es solamente terminar con una configuración más segura.

El objetivo es llegar a ella sin convertir el hardening en el siguiente incidente.

Limpieza del laboratorio

Al terminar las pruebas eliminamos primero los recursos administrados por Terraform:

terraform destroy
Enter fullscreen mode Exit fullscreen mode

Durante el laboratorio también modificamos los defaults de IMDS a nivel de cuenta y región mediante AWS CLI.

Esos cambios no forman parte del estado de Terraform, por lo que debemos restaurarlos por separado.

Si antes del laboratorio no teníamos valores explícitos configurados, podemos volver a no-preference:

aws ec2 modify-instance-metadata-defaults \
  --region us-east-1 \
  --http-tokens no-preference \
  --http-tokens-enforced no-preference
Enter fullscreen mode Exit fullscreen mode

Verificamos el resultado:

aws ec2 get-instance-metadata-defaults \
  --region us-east-1
Enter fullscreen mode Exit fullscreen mode

Podríamos volver a obtener una respuesta como:

{
  "AccountLevel": {
    "ManagedBy": "account"
  }
}
Enter fullscreen mode Exit fullscreen mode

Si la cuenta ya tenía una configuración específica antes del laboratorio, restaura esos valores originales en lugar de utilizar no-preference.

Referencias

Top comments (0)