Un job de GitHub Actions es un buen lugar del que robar secretos. Contiene claves de despliegue, tokens de registros y credenciales de la nube, y ejecuta código que otra persona escribió hace un minuto: acciones de terceros, paquetes y sus scripts de instalación. La gestión de secretos de GitHub protege frente a accidentes. No se diseñó para detener código que ya se está ejecutando dentro del job.

Este artículo explica cómo funcionan los secretos en GitHub Actions, las cinco formas en que se filtran en la práctica y los controles que cierran cada una.

Cómo funcionan los secretos de GitHub Actions

Los secretos existen en tres niveles: organización, repositorio y entorno. Cuando el mismo nombre existe en más de un nivel, gana el más específico: el entorno sobre el repositorio, y el repositorio sobre la organización.

Algunos límites y reglas que conviene conocer:

  • Tamaño y cantidad. Hasta 1.000 secretos de organización, 100 secretos de repositorio y 100 secretos de entorno, cada uno de 48 KB como máximo. Un workflow ve todos los secretos de repositorio y de entorno, pero solo los 100 primeros secretos de organización por orden alfabético.
  • Los forks no reciben nada. Aparte de GITHUB_TOKEN, los secretos no se pasan a los workflows activados por una pull request desde un fork.
  • Dependabot se trata como un fork. Los workflows activados por Dependabot reciben un token de solo lectura y solo los secretos de Dependabot, no los secretos de Actions.
  • Los workflows reutilizables necesitan que se les pasen los secretos. secrets: inherit los pasa todos de forma implícita, dentro de la misma organización o empresa. Los secretos de entorno no se pueden pasar a través de workflow_call.
  • Los entornos controlan el acceso. Con revisores obligatorios, un job no puede leer los secretos de un entorno hasta que alguien lo apruebe. Las reglas de ramas de despliegue limitan qué ramas pueden usar el entorno.

Qué hace el enmascaramiento y qué no

GitHub oculta los valores de los secretos en los logs, pero esa ocultación depende de encontrar una coincidencia exacta. La propia documentación de GitHub enumera dónde falla:

  • un secreto dentro de datos estructurados como JSON o YAML
  • un valor derivado de un secreto, como un token intercambiado por otro token, salvo que lo registre con ::add-mask::
  • un secreto transformado, por ejemplo codificado en base64

El enmascaramiento es una protección contra imprimir un secreto por error. No hace nada contra un código que primero codifica el secreto.

Cómo se filtran los secretos desde los workflows

1. Una acción comprometida los imprime

En marzo de 2025 un atacante comprometió tj-actions/changed-files (CVE-2025-30066), una acción utilizada por más de 23.000 repositorios. Movió las etiquetas de versión existentes de la acción a un commit malicioso, de modo que todos los workflows que hacían referencia a una etiqueta como @v45 ejecutaron el código del atacante. Solo se salvaron los workflows fijados a un SHA de commit completo.

La carga útil leía los secretos de la memoria del runner y los imprimía en el log del job, codificados dos veces en base64 para que el enmascaramiento no los detectara. En los repositorios públicos, cualquiera puede leer esos logs. El compromiso partió de otra acción, reviewdog/action-setup (CVE-2025-30154), y antes de eso de un token de acceso personal robado mediante un workflow pull_request_target.

2. Un workflow pull_request_target ejecuta código no fiable

pull_request_target se ejecuta en el contexto del repositorio base, con sus secretos y un token con permiso de escritura, incluso cuando la pull request procede de un fork. Eso solo es seguro mientras el workflow no ejecute nunca código de la pull request. Cuando un workflow hace checkout del head de la pull request y lo compila, cualquiera que pueda abrir una pull request puede leer todos los secretos del workflow. GitHub Security Lab llama a esto «pwn requests».

GitHub ha ido restringiéndolo. Desde diciembre de 2025, pull_request_target siempre utiliza el archivo de workflow de la rama por defecto. Desde julio de 2026, actions/checkout se niega a hacer checkout del código de un fork en workflows pull_request_target que usan patrones peligrosos conocidos, y las protecciones de ejecución de workflows, disponibles de forma general desde septiembre de 2026, desactivarán pull_request_target por defecto en los repositorios públicos sin una política de eventos a partir del 2 de noviembre de 2026.

3. Se inyecta una entrada no fiable en un script

Las expresiones de los workflows se expanden antes de que se ejecute el shell. Un paso como este:

- run: echo "Title: ${{ github.event.issue.title }}"

ejecuta lo que contenga el título de la issue. Un atacante abre una issue con un comando de shell como título y el job lo ejecuta, con los secretos del job a su alcance. En su lugar, pase los valores no fiables a través de una variable de entorno, para que el shell los trate como datos:

- env:
    TITLE: ${{ github.event.issue.title }}
  run: echo "Title: $TITLE"

4. Las cachés y los artefactos los sacan fuera

  • Artefactos. En 2024, la investigación ArtiPACKED de Unit 42 mostró que actions/checkout escribe GITHUB_TOKEN en .git/config por defecto, de modo que subir el espacio de trabajo como artefacto filtra el token a cualquiera que pueda descargarlo. upload-artifact excluye los archivos ocultos por defecto desde septiembre de 2024, y actions/checkout v6 guarda la credencial fuera del directorio de checkout, pero las versiones fijadas más antiguas y los pasos de subida personalizados siguen filtrándolo.
  • Cachés. Un atacante que pueda ejecutar código en cualquier workflow de un repositorio puede llenar su caché, expulsar las entradas reales y plantar una envenenada bajo la misma clave. Un workflow privilegiado posterior la restaura. En mayo de 2026, esta cadena, que partió de pull_request_target y terminó con un token OIDC leído de la memoria del runner, permitió a un atacante publicar 84 versiones maliciosas de 42 paquetes de TanStack.

5. Una dependencia los envía a un servidor

Los scripts de instalación se ejecutan con el mismo acceso que el resto del job. El gusano Shai-Hulud, visto por primera vez en npm en septiembre de 2025, se ejecutaba durante npm install, recopilaba tokens de npm, tokens de GitHub y claves de la nube del entorno y los publicaba en repositorios públicos de GitHub. Su oleada de noviembre de 2025 se ejecutaba en preinstall y registraba la máquina infectada como runner autoalojado, para que el atacante pudiera volver más tarde.

Esta vía no necesita ningún error en su workflow. Basta con una sola versión comprometida en cualquier punto del árbol de dependencias, y los secretos salen por la red.

Cómo protegerlos

Sustituya las claves de la nube almacenadas por OIDC. En lugar de guardar una clave de AWS, Azure o Google Cloud como secreto, deje que el job solicite un token de corta duración al emisor OIDC de GitHub y lo intercambie con el proveedor de la nube. La política de confianza decide qué repositorio, rama o entorno puede asumir el rol, y la credencial caduca con el job. El job necesita permissions: id-token: write. Los tokens OIDC aún se pueden robar de un job en ejecución, como demostró TanStack, pero un token robado vale mucho menos que una clave que no caduca nunca.

Fije las acciones a un SHA de commit completo. La documentación de GitHub lo llama la única forma de usar una acción como versión inmutable. Las etiquetas se pueden mover; un SHA, no. Desde agosto de 2025, la política de acciones permitidas puede imponer la fijación por SHA y bloquear acciones concretas en toda una organización. Dependabot y Renovate pueden mantener actualizados los SHA fijados.

Conceda a GITHUB_TOKEN lo mínimo que necesite. Establezca el valor por defecto en solo lectura y conceda permisos de escritura por job. Las organizaciones y los repositorios nuevos usan por defecto solo lectura desde febrero de 2023, pero los más antiguos mantuvieron el valor permisivo salvo que alguien lo cambiara. Consulte permisos de GITHUB_TOKEN: mínimo privilegio para cada workflow.

Limite los secretos al paso que los necesita. Un secreto en el env: de un paso solo está en el entorno de ese paso. Un secreto definido a nivel de job es visible para todos los pasos, incluidas todas las acciones de terceros.

Ponga los secretos de producción detrás de entornos. Los revisores obligatorios y las reglas de ramas de despliegue hacen que un workflow en una rama de funcionalidad, o uno activado por una pull request, no pueda leerlos.

Trate pull_request_target y las entradas no fiables como ejecución de código. Evite pull_request_target salvo que lo necesite, no compile nunca en él código de una pull request y pase los datos de eventos a través de variables de entorno. CodeQL puede comprobar los workflows en busca de patrones de inyección y de pwn request; está disponible de forma general para GitHub Actions desde abril de 2025.

Active el escaneo de secretos y la protección de push. Detectan secretos confirmados en el repositorio, una filtración distinta de las anteriores pero igual de habitual.

Restrinja adónde puede conectarse el job. La mayoría de los jobs necesitan una lista corta de destinos: registros de paquetes, registros de contenedores y unas pocas API. Si el job solo puede llegar a esos destinos, un secreto leído por código malicioso no tiene adónde ir. Consulte cómo detiene una política de egress la exfiltración desde GitHub Actions.

Rote tras cualquier exposición. Si un workflow ejecutó una acción o un paquete comprometidos, dé por robados todos los secretos que pudo leer. Revóquelos y vuelva a emitirlos todos, no solo los que crea que se usaron.

Dónde encaja CRACI

CRACI ejecuta sus jobs de GitHub Actions en sus propios runners: usted cambia runs-on a craci, y los workflows, los secretos y el historial de ejecuciones siguen en GitHub. Dos de las cosas que hace apuntan a las filtraciones anteriores.

  • Una política de egress que, si falla, bloquea. Usted define a qué fuentes de paquetes, registros y hosts puede llegar un job. La política se valida antes de que empiece el job y se aplica en el runner, de modo que una conexión a cualquier otro lugar se bloquea, y usted recibe una alerta cuando se infringe la política.
  • Un registro de lo que hizo el job. CRACI registra todos los paquetes que cada job descargó realmente, incluidos los paquetes restaurados desde cachés, y una traza de las conexiones de red del job. Cuando se anuncie el próximo paquete comprometido, podrá ver qué builds lo descargaron y a qué se conectaron, en lugar de adivinarlo a partir de lockfiles.

Cada job se ejecuta en su propia máquina virtual, de modo que nada de lo que deje un job está ahí para el siguiente. CRACI no busca secretos en su código; mantenga el escaneo de secretos de GitHub para eso.

Reserve una demo para ver el registro de uno de sus propios builds.