Fuga de secretos de GitHub Actions: cómo investigarla
Cómo se filtran los secretos de GitHub Actions pese al enmascarado ***: workflows envenenados, ramas nuevas, salida codificada y actions de terceros.
En resumen. Un workflow puede leer todos los secretos de su ámbito, y el enmascarado solo oculta el valor exacto. echo "$KEY" | base64 | base64 lo imprime en claro. Los atacantes llegan ahí haciendo push de un workflow modificado a una rama nueva, abusando de pull_request_target o comprometiendo una action de terceros (tj-actions/changed-files, 2025). Las evidencias: workflows.created_workflow_run en una rama nunca vista en el registro de auditoría, más el archivo de logs de la ejecución, donde se ven tanto el script del paso como el blob codificado. Descarga los archivos antes de que alguien los borre y después rota todos los secretos que podía leer el workflow.
GitHub Actions es donde el código fuente se convierte en acceso al cloud. Los workflows que despliegan en producción guardan como secretos claves de AWS, tokens de registros de contenedores y claves de firma. Para un atacante que tiene un token con el ámbito workflow, o un punto de apoyo en una action de la que dependes, exfiltrar esos secretos suele ser el verdadero objetivo.
Cómo llegan los secretos al atacante
| Vía | Qué ocurre | Dónde se ve |
|---|---|---|
| Workflow enviado a una rama nueva | El atacante hace push de un .github/workflows/*.yml modificado a una rama recién creada. Un disparador push lo ejecuta con los secretos del repositorio | git.push + workflows.created_workflow_run con un head_branch nuevo |
Mal uso de pull_request_target / workflow_run | Código no fiable de una PR se ejecuta en un contexto privilegiado | Ejecuciones disparadas por PR desde forks; consulta GitHub Security Lab sobre las "pwn requests" |
| Action de terceros comprometida | Una etiqueta que usas apunta ahora a código malicioso | Todas las ejecuciones que usaron la etiqueta, con la misma salida extraña |
| Runner self-hosted | Secretos y tokens leídos desde un host persistente | Consulta seguridad de los runners self-hosted |
| Protección de entorno eliminada | Se borran los revisores obligatorios o las políticas de ramas, así que los secretos del entorno quedan accesibles | environment.remove_protection_rule, environment.delete |
ATT&CK tiene ahora una técnica para esta familia, T1677 Poisoned Pipeline Execution. La entrada del glosario sobre poisoned pipeline execution explica las variantes directa e indirecta.
Un ejemplo público
En marzo de 2025, la CISA emitió una alerta sobre el compromiso de tj-actions/changed-files (CVE-2025-30066) y del relacionado reviewdog/action-setup (CVE-2025-30154). Las etiquetas de versión de la action se modificaron para que apuntaran a código malicioso que volcaba los secretos de CI en los logs de los workflows. En los repositorios públicos, cualquiera puede leer esos logs. Todas las organizaciones que usaban una etiqueta desplazada tuvieron que revisar los logs de sus ejecuciones del periodo afectado. La pregunta de detección era: ¿cuáles de nuestras ejecuciones imprimieron un blob codificado en ese paso?
Por qué el enmascarado no es un control
GitHub sustituye por *** en los logs los valores de los secretos registrados. La referencia de uso seguro es explícita: la ocultación no está garantizada, y un secreto transformado (Base64, codificado para URL) debe registrarse por separado para que se enmascare. Por tanto:
echo "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" | base64 -w0 | base64 -w0
imprime una cadena larga con la que no coincide ninguna máscara. Decodifícala dos veces y tendrás el par de claves. Hay variantes con xxd, rev, od, troceando el valor, con toJSON(secrets) o enviando el valor directamente fuera con curl -d a un servicio de captura de peticiones. La entrada del glosario sobre el enmascarado de secretos resume qué cubre el enmascarado.
Las evidencias, por orden de valor
1. El archivo de logs de la ejecución
Repositorio → Actions → la ejecución → ⋯ → Download log archive. El ZIP contiene un log combinado por job y un fichero por paso. Incluye el script del paso (el bloque ##[group]Run … repite los comandos) y su salida. Es el único lugar donde puedes ver qué hizo el workflow con un secreto. Por defecto, los logs se conservan 90 días, y cualquiera con acceso de escritura puede borrarlos. Descárgalos lo primero.
El analizador lee estos ZIP directamente. Se les aplican dos reglas:
actions-secret-exfil(crítica): una línea de script que menciona un secreto, token, clave, env o variable$y la pasa por un codificador (base64,xxd,rev,od), vuelca el entorno (printenv,env |,toJSON(secrets)) o envía datos a un endpoint externo (curl -d,wget --post,/dev/tcp/, servicios habituales de captura de peticiones y de túneles). La simple decodificación (base64 -d) sin llamada de red queda excluida para limitar los falsos positivos. Si la línea menciona credenciales de AWS, Google Cloud o Azure, el hallazgo muestra el siguiente paso en el cloud.actions-encoded-output(media): una línea de salida que contiene una cadena base64 o hexadecimal ininterrumpida de 60 caracteres o más. Así es como se ve una clave codificada dos veces.
Cada línea de log aparece dos veces en un archivo (fichero combinado y fichero por paso). El analizador deduplica, así que un paso malicioso da un único hallazgo.
2. Los eventos de ejecución de workflows en el registro de auditoría
workflows.created_workflow_run y workflows.completed_workflow_run llevan repo, name (el workflow), event (push, pull_request, workflow_dispatch…), head_branch, head_sha, workflow_run_id y el actor. La regla workflow-new-branch (baja) señala un workflow que se ejecuta en una rama en la que nunca se había ejecutado, para los disparadores push y workflow_dispatch. Por sí sola es ruidosa, ya que cualquier rama de funcionalidad la activa. Se vuelve significativa cuando el actor o el token tienen otros hallazgos, o cuando el nombre de la rama parece de mantenimiento (ci/cache-fix, dependabot-fix) y la rama se borró después.
3. Eventos Git
Un git.push justo antes de la ejecución, desde el mismo token y la misma IP, vincula el cambio del workflow con la credencial. Sin eventos Git sigues teniendo head_sha, que puedes buscar en el repositorio si la rama todavía existe.
4. Cambios en los metadatos de los secretos
repo.create_actions_secret, org.update_actions_secret y environment.create_actions_secret (regla actions-secret-created, baja) forman parte del trabajo rutinario de CI. Cobran importancia cuando el actor es sospechoso. Un atacante puede sustituir un secreto de despliegue para redirigir los artefactos. Ten en cuenta lo que el registro de auditoría no registra: que un workflow lea un secreto no es un evento.
Alcance y remediación
- Lista todas las ejecuciones del workflow modificado, y de cualquier workflow en las ramas del atacante, con los valores de
workflow_run_iddel registro de auditoría. - Rota todos los secretos que podía leer el workflow: los secretos del repositorio, de la organización (si el repositorio tiene acceso a ellos) y de los entornos. Incluye los que no se imprimieron. Un script que ejecuta
envlo envía todo. - Borra los logs de las ejecuciones que imprimieron secretos, después de archivarlos para el caso, para que los blobs codificados dejen de poder leerse en la interfaz.
- Elimina las ramas del atacante y revisa
.github/workflowsen todas las ramas restantes. - Sigue las claves hasta AWS, Google Cloud o Azure: consulta claves cloud filtradas.
El endurecimiento que reduce de verdad la exposición
- Fija las actions de terceros a un SHA de commit completo. GitHub lo describe como la única forma de usar una action como versión inmutable.
- Configura el permiso predeterminado de
GITHUB_TOKENcomo solo lectura y concede escritura job a job. - Protege los secretos de despliegue con entornos que exijan revisores y restrinjan las ramas. El analizador señala cuándo se elimina esa protección (
environment-protection-removed, alta). - Sustituye las claves cloud almacenadas por federación OIDC. Un token de corta duración, limitado a un repositorio y a una rama, vale mucho menos para un atacante.
- Exige la revisión de CODEOWNERS en
.github/workflows/y protégelo con un ruleset de repositorio. - Usa las comprobaciones de Scorecard de OpenSSF (permisos de tokens, dependencias fijadas, workflows peligrosos) para encontrar patrones de riesgo.
Preguntas frecuentes
¿Pueden filtrarse los secretos de GitHub Actions aunque aparezcan enmascarados como ***?
Sí. El enmascarado sustituye el valor exacto del secreto en los logs. Un script que lo codifica en base64, lo invierte o lo trocea imprime una cadena que ya no coincide, así que se muestra en claro y puede decodificarse. La propia documentación de GitHub advierte de que la ocultación no está garantizada para los secretos transformados.
¿Muestra el registro de auditoría cuándo un workflow lee un secreto?
No. El registro de auditoría registra la creación, actualización o eliminación de secretos, y la creación y finalización de las ejecuciones de workflows. Leer un secreto durante una ejecución no es un evento de auditoría, así que el log de la ejecución es la evidencia de lo que un paso hizo con él.
Artículos relacionados
- Claves cloud filtradas en GitHub Actions: el salto al cloud
- ¿Protección de ramas desactivada? Auditar los rulesets de GitHub
- El recorrido ficticio de northwind-labs: una clave de AWS en doble base64 en un log de ejecución
- OWASP Top 10 CI/CD Security Risks