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.
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#
-
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 usuarioaws iam put-user-policy --user-name ci-deploy-legacy \--policy-name IR-Quarantine-2026-10-11 \--policy-document file://deny-all.json -
Investigar — qué hizo y desde dónde. Antes de borrar nada, lee CloudTrail. Filtra por la
accessKeyIdpara 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, errorCodeFROM <event-data-store-id>WHERE userIdentity.accessKeyId = 'AKIA...7QXZ'AND eventTime >= '2026-10-11 00:00:00'ORDER BY eventTime ASC;Anota el
last-usedde 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. -
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
CreateAccessKeysobre 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 -
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.
-
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
# Secreto estático guardado en el repo/CI, válido hasta que alguien lo roteAWS_ACCESS_KEY_ID=AKIA...7QXZAWS_SECRET_ACCESS_KEY=wJalr...EXAMPLE# Si se filtra, es acceso válido a tu cuenta hasta que lo detectespermissions: id-token: write # el runner pide un token OIDC efímero contents: readsteps: - 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 filtrarTu 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:
- Detona la técnica con Stratus Red Team (
aws.persistence.iam-backdoor-usercrea una segunda clave: tu “persistencia” a cazar). - Ejecuta el playbook en orden y cronometra de la detección a la contención.
- Escribe la consulta de CloudTrail que lista todo lo que hizo tu
accessKeyId. - 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.