Saltar al contenido

Temas

Playbook: una llave de acceso se filtró en GitHub

El playbook paso a paso ante una access key de AWS filtrada —detectar, contener, investigar, erradicar y recuperar— y la lección arquitectónica que lo cierra: OIDC sin llaves.

“Una access key de AWS aparece commiteada en un repositorio público. Son las 03:00. ¿Qué haces, en orden?” Es la pregunta de entrevista más repetida para un rol de Cloud Security, porque toca todo a la vez: IAM, detección, forense y arquitectura. La respuesta mala es “la borro y ya”. La buena es un playbook que contiene sin destruir evidencia, mide el daño y arregla la causa raíz. Vamos con él, sobre la llave heredada ci-deploy-legacy de Citas.

Playbook: una llave de acceso se filtró en GitHub
Repo públicoAKIA…ci-deploy-legacyAtacanteGuardDutyTú · IRRol federadoOIDC · temporalclave en gitllamadas APIalertadesactivaUnauthorizedAccess:IAMUserInactive · deny-allsin llave que filtrarCloudTrail · qué tocó la clavepersistencia → eliminarGetCallerIdentityListBucketsGetObject ×1.2kCreateAccessKeyAKIA…AKIA…APIfindingdisable

Paso 1 de 7La llave se filtra

Una access key heredada del CI (ci-deploy-legacy) acaba commiteada en un repositorio público. A partir de aquí es una credencial válida en manos de cualquiera.

El reloj empieza al detectar#

Dos fuentes te avisan, y conviene tener ambas:

  • El secret scanning del repositorio (GitHub avisa, y AWS recibe la notificación de llaves expuestas públicamente).
  • GuardDutyAmazon GuardDutyServicio gestionado de detección de amenazas de AWS que analiza CloudTrail, VPC Flow Logs y DNS y genera hallazgos (findings)., con hallazgos como UnauthorizedAccess:IAMUser/MaliciousIPCaller (uso desde una IP maliciosa) o, el más directo, CredentialAccess:IAMUser/CompromisedCredentials, que marca una credencial que la inteligencia de amenazas de AWS ya sabe expuesta.

Cuando AWS detecta una llave pública, además adjunta automáticamente la política gestionada AWSCompromisedKeyQuarantineV2 al usuario y abre un caso de soporte. Es una red de seguridad, no tu plan: la cuarentena limita el daño, pero la respuesta la conduces tú.

El playbook, paso a paso#

  1. Contener — desactivar la llave y revocar sesiones. Lo primero es cortar el acceso. Desactiva (no borres todavía) la access key para preservar su historial de uso, y niega al principal mientras investigas.

    1-contener.sh
    # Desactivar la clave (reversible, conserva el last-used)
    aws iam update-access-key --user-name ci-deploy-legacy \
    --access-key-id AKIA...7QXZ --status Inactive
    # Cortar en seco: deny-all temporal al usuario
    aws iam put-user-policy --user-name ci-deploy-legacy \
    --policy-name IR-Quarantine-2026-10-11 \
    --policy-document file://deny-all.json
  2. Investigar — qué hizo y desde dónde. Antes de borrar nada, lee CloudTrail. Filtra por la accessKeyId para reconstruir toda la actividad de la credencial: reconocimiento, lecturas, descargas, creación de recursos.

    CloudTrail Lake · qué tocó la clave
    SELECT eventTime, eventName, eventSource, sourceIPAddress, userAgent, errorCode
    FROM <event-data-store-id>
    WHERE userIdentity.accessKeyId = 'AKIA...7QXZ'
    AND eventTime >= '2026-10-11 00:00:00'
    ORDER BY eventTime ASC;

    Anota el last-used de la clave antes de borrarla: esa información se pierde al eliminarla. La lógica de qué pudo hacer la credencial depende de sus permisos efectivos; repásala con la lógica de evaluación de IAM.

  3. Erradicar — rotar y cazar persistencia. Rota el secreto en su origen (el pipeline) y busca lo que el atacante haya sembrado para volver: usuarios, roles o llaves nuevos. El clásico es una segunda access key creada con CreateAccessKey sobre el mismo usuario, justo para sobrevivir a la rotación de la primera.

    3-cazar-persistencia.sh
    # ¿Access keys que no deberían existir?
    aws iam list-access-keys --user-name ci-deploy-legacy
    # ¿Usuarios o roles creados en la ventana del incidente? (vía CloudTrail)
    # eventName = CreateUser, CreateRole, CreateAccessKey, CreateLoginProfile
  4. Recuperar. Con la persistencia eliminada y el secreto rotado, restablece el servicio: vuelve a habilitar el pipeline con su nueva identidad y vigila de cerca la cuenta por si reaparece actividad. Confirma que ninguna credencial comprometida sigue activa.

  5. Lecciones — que no vuelva a poder pasar. La causa raíz no es “alguien commiteó una clave”: es que existía una clave de larga duración que commitear. La corrección definitiva es arquitectónica.

La lección: OIDC sin llaves#

Citas migró GitHub Actions a un rol vía OIDC; ci-deploy-legacy era un resto de antes. Con OIDC, el pipeline obtiene credenciales temporales asumiendo un rol mediante un token de confianza federada: no hay ninguna access key de larga duración que guardar, rotar ni, sobre todo, filtrar. El incidente de esta nota simplemente no tiene superficie donde ocurrir. El cómo se configura está en secretos y CI/CD con OIDC.

Vulnerable

Seguro

Antes · llave de larga duración (vulnerable)
# Secreto estático guardado en el repo/CI, válido hasta que alguien lo rote
AWS_ACCESS_KEY_ID=AKIA...7QXZ
AWS_SECRET_ACCESS_KEY=wJalr...EXAMPLE
# Si se filtra, es acceso válido a tu cuenta hasta que lo detectes
Después · OIDC sin llaves (seguro)
permissions:
id-token: write # el runner pide un token OIDC efímero
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::111122223333:role/citas-ci-deploy
aws-region: eu-west-1
# credenciales temporales; nada que filtrar

Tu turno

Ejecuta el playbook en tu laboratorio

En una cuenta sandbox propia, crea un usuario IAM de prueba con una access key y simula la fuga:

  1. Detona la técnica con Stratus Red Team (aws.persistence.iam-backdoor-user crea una segunda clave: tu “persistencia” a cazar).
  2. Ejecuta el playbook en orden y cronometra de la detección a la contención.
  3. Escribe la consulta de CloudTrail que lista todo lo que hizo tu accessKeyId.
  4. Rehaz el pipeline de prueba con OIDC y borra el usuario. Comprueba que ya no hay ninguna llave de larga duración en la cuenta.

Nunca lo hagas con credenciales reales ni en cuentas que no sean tuyas.

Reto

¿Por qué desactivar la clave antes de borrarla?

▸Mostrar solución

Porque al borrar una access key pierdes su información de último uso (last-used), que es evidencia para la investigación. Desactívala primero (corta el acceso igual), lee su historial en CloudTrail, y bórrala al final en la erradicación.

Reto

¿Qué buscas exactamente al 'cazar persistencia'?

▸Mostrar solución

Cualquier mecanismo que el atacante dejó para volver aunque rotes el secreto: una segunda access key (CreateAccessKey), un perfil de consola nuevo (CreateLoginProfile), usuarios o roles creados, políticas con confianza hacia cuentas externas, y credenciales temporales (GetFederationToken) aún vigentes.

En resumen#

  • Orden: detectar → contener (desactivar + deny) → investigar (CloudTrail) → erradicar (rotar + cazar persistencia) → recuperar → lecciones.
  • Desactiva la clave (no la borres aún) para conservar su historial; niega al usuario porque las credenciales temporales sobreviven a la desactivación de la llave.
  • Investiga con CloudTrail filtrando por accessKeyId: define el alcance real del incidente.
  • La persistencia favorita es una segunda CreateAccessKey; búscala siempre.
  • La causa raíz es tener llaves de larga duración. La solución es OIDC (CI) e Identity Center (personas): sin llave que filtrar, no hay incidente.

Referencias#

Notas relacionadas

ESC
↑↓ para navegar↵ para abriresc para cerrar