Prácticas de seguridad en GitHub Actions: 3 protecciones clave del incidente tj-actions

En marzo de 2025, un GitHub Advisory volvió a marcar mis límites. tj-actions/changed-files — una Action que llevaba tres años usando — quedó marcada como CVE-2025-30066. Más de 23.000 repositorios quedaron expuestos de un día para otro al riesgo de filtración de Secrets.
Lo que más me inquietó fue el método del ataque. El atacante robó primero un PAT de un proyecto y luego manipuló las etiquetas de versión de tj-actions, redirigiendo código que parecía seguro hacia scripts maliciosos. Esos Secrets — claves de AWS, contraseñas de base de datos, API tokens — empezaron a fluir en silencio hacia logs públicos.
Me sentí bastante mal. El pipeline de CI/CD que yo había configurado se había convertido en trampolín del atacante. Revisé todos mis repositorios: referencias de Actions, permisos de GITHUB_TOKEN, configuración de auditoría. Me llevó todo un día descubrir cuántos detalles había ignorado durante mucho tiempo.
Este artículo es la lista de protección que armé después de ese susto. Hablamos del patrón de los ataques a la cadena de suministro, de la gestión correcta de Secrets, del control fino de permisos de GITHUB_TOKEN y de las novedades de la hoja de ruta de seguridad de GitHub 2026 que conviene preparar con antelación.
Lo importante: todo esto lo puedes configurar ya, sin esperar a que salgan funciones nuevas.
Cómo leer el ataque de cadena de suministro en CI/CD a partir del incidente tj-actions
Cómo ocurrió el ataque
Empecemos por tj-actions/changed-files. Es muy popular en GitHub: detecta qué archivos cambiaron en un PR, y muchos pipelines de CI/CD la usan. Varios de mis proyectos también dependían de ella para despliegues incrementales.
La cadena del ataque fue más o menos así:
El atacante robó primero un PAT del proyecto reviewdog/action-setup. Ese PAT tenía permisos de escritura en el repositorio. Con el token, manipuló las etiquetas de versión del repositorio tj-actions: la etiqueta v45, que apuntaba a código seguro, quedó redirigida en silencio a un commit nuevo con lógica maliciosa.
Los proyectos que seguían usando uses: tj-actions/changed-files@v45 descargaron el código malicioso sin saberlo. El script hacía algo simple: imprimir todos los Secrets del entorno de CI/CD en los logs. Los logs son públicos, y los Secrets se filtraron.
Según el GitHub Advisory, más de 23.000 repositorios se vieron afectados. El equipo de seguridad de Coinbase reveló después que el ataque impactó datos de más de 70.000 clientes.
El error que cometí: etiqueta frente a SHA fijo
Al revisar mis repositorios, vi que en muchos sitios seguía usando referencias por etiqueta:
# Mal ejemplo: etiqueta mutable
- name: Check changed files
uses: tj-actions/changed-files@v45
Las etiquetas son vivas. El mantenedor del repositorio — o un atacante con permisos de escritura — puede apuntarlas a un commit nuevo en cualquier momento. Tú crees que usas v45, pero en realidad puede estar ejecutándose código manipulado.
La forma correcta es fijar con el SHA completo:
# Correcto: SHA completo
- name: Check changed files
uses: tj-actions/changed-files@6cbf527e7a7b6d61c4e7f25e5ce5f7b7c8f3c72a
El SHA es fijo. Mientras no actualices la referencia, siempre ejecutas el mismo código.
Mientras corregía, no dejaba de reprocharme: esto es seguridad básica, ¿cómo pude pasarlo por alto tanto tiempo?
El costo de confiar en Actions de terceros
El incidente tj-actions también dejó claro otro problema: damos demasiada confianza a Actions de terceros.
Cualquier Action con bastantes stars y uso se mete directo en el CI/CD de producción. Pero, ¿qué nivel de conciencia de seguridad tiene el mantenedor? ¿Y si le roban el PAT?
En la hoja de ruta de seguridad 2026, GitHub propone bloqueo de dependencias a nivel de workflow: algo parecido a package-lock.json, con todos los SHA de Actions en un archivo de bloqueo. Aún no está disponible, pero ya podemos hacerlo a mano: convertir cada referencia a SHA y auditar con regularidad.
Otra recomendación: reducir la cantidad de Actions de terceros. Si una Action oficial resuelve el caso, no uses una de terceros. Para detectar archivos cambiados, por ejemplo, actions/checkout más un script shell suele bastar; no hace falta depender de tj-actions.
Tres niveles de gestión de Secrets y prácticas avanzadas
Los tres niveles de almacenamiento de Secrets en GitHub
Los Secrets integrados de GitHub se organizan en tres niveles: organización, repositorio y entorno.
Los Secrets de organización se comparten entre repositorios; encajan para claves de AWS o tokens de nube. Los de repositorio solo valen en ese repo; sirven para contraseñas de base de datos del proyecto. Los de entorno son más finos y pueden combinarse con reglas de protección — por ejemplo, exigir aprobación del PR antes de acceder a Secrets de producción.
En almacenamiento, GitHub cifra con libsodium sealed box. Tras guardarlos, no se pueden volver a leer; solo están disponibles en ejecución mediante el contexto secrets:
steps:
- name: Deploy to AWS
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
Un error habitual es interpolar Secrets directamente en comandos shell:
# Peligroso: el Secret puede aparecer en los logs
- name: Configure AWS
run: aws configure set aws_access_key_id ${{ secrets.AWS_ACCESS_KEY_ID }}
Si el valor del Secret tiene caracteres especiales o el comando falla, el log puede exponerlo. Lo correcto es pasarlo por variables de entorno:
# Seguro: pasar por variable de entorno
- name: Configure AWS
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
run: aws configure set aws_access_key_id "$AWS_ACCESS_KEY_ID"
Enmascaramiento dinámico: ::add-mask::
A veces los datos sensibles no son Secrets predefinidos, sino tokens generados en tiempo de ejecución. Ahí puedes usar ::add-mask:: para enmascararlos dinámicamente:
- name: Generate temporary token
run: |
token=$(generate-token.sh)
echo "::add-mask::$token"
echo "TOKEN=$token" >> $GITHUB_ENV
El valor enmascarado aparece como *** en los logs. Pero el enmascaramiento debe ocurrir antes de imprimir el valor; si imprimes primero y enmascaras después, el log ya habrá quedado expuesto.
Opción avanzada: integración OIDC con HashiCorp Vault
Si tu proyecto exige más seguridad, los Secrets integrados de GitHub pueden no bastar. Las credenciales de larga duración viven en GitHub; si el repositorio cae, se pierden todas.
Una mejor opción es HashiCorp Vault con OIDC (OpenID Connect) para acceso sin credenciales almacenadas. GitHub Actions demuestra ante Vault quién es; Vault valida y emite un token de corta duración. Así no guardas credenciales permanentes en GitHub.
Los pasos de configuración son más o menos estos:
Paso 1: configurar el rol OIDC en Vault
Vault debe confiar en el proveedor OIDC de GitHub. Configura un rol que defina qué repositorios pueden obtener qué Secrets:
resource "vault_jwt_auth_backend_role" "github_actions" {
backend = "jwt"
role_name = "github-actions-role"
bound_audiences = ["https://github.com/your-org"]
user_claim = "repository"
role_type = "jwt"
token_policies = ["ci-policy"]
token_ttl = "1h"
}
Paso 2: usar vault-action en GitHub Actions
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Import Secrets from Vault
uses: hashicorp/[email protected]
id: vault
with:
url: https://vault.example.com:8200
role: github-actions-role
method: jwt
secrets: |
secret/data/ci/aws accessKey | AWS_ACCESS_KEY_ID ;
secret/data/ci/aws secretKey | AWS_SECRET_ACCESS_KEY
- name: Deploy with AWS credentials
run: |
echo "Accessing AWS with Vault-provided credentials"
aws s3 ls
En este flujo, vault-action usa automáticamente el token OIDC que proporciona GitHub para autenticarse en Vault. Vault devuelve los Secrets e inyecta variables de entorno. El token expira a la hora; en la siguiente ejecución se obtiene uno nuevo.
Azure Key Vault también admite integración OIDC similar; la configuración es parecida, con azure/login y Azure Key Vault Secrets.
Control de permisos de GITHUB_TOKEN en la práctica
Qué es GITHUB_TOKEN
Cada ejecución de GitHub Actions recibe automáticamente un GITHUB_TOKEN: un token OAuth temporal para operar el repositorio actual — crear releases, hacer push, comentar PRs.
El problema: los permisos por defecto de GITHUB_TOKEN son demasiado amplios.
En versiones antiguas de GitHub Actions, GITHUB_TOKEN tenía permisos de lectura y escritura casi completos. Un workflow podía modificar el repositorio, crear ramas y hacer push con libertad. Si un workflow se explota (por ejemplo, un script malicioso en un PR), GITHUB_TOKEN se convierte en arma del atacante.
Configuración completa de la clave permissions
En abril de 2021, GitHub introdujo la clave permissions para acotar con precisión el alcance de GITHUB_TOKEN.
La sintaxis básica es esta:
permissions:
actions: read|write|none # Gestionar Actions
contents: read|write|none # Contenido del repositorio
issues: read|write|none # Operaciones en Issues
packages: read|write|none # GitHub Packages
pull-requests: read|write|none # Operaciones en PRs
security-events: read|write|none # Reporte de eventos de seguridad
deployments: read|write|none # Estado de despliegues
statuses: read|write|none # Estado de commits
Un mecanismo clave: en cuanto defines permissions en el workflow, todo permiso no especificado pasa a none. Esa es la frontera de mínimo privilegio.
Permisos a nivel de workflow frente a nivel de job
permissions puede declararse en dos niveles: workflow (global) y job (local).
Los permisos de workflow aplican a todos los jobs:
name: CI Pipeline
permissions:
contents: read # Todos los jobs leen el repo por defecto
issues: write # Todos los jobs pueden escribir Issues
jobs:
lint:
runs-on: ubuntu-latest
steps:
- run: echo "Lint with read-only access"
test:
runs-on: ubuntu-latest
steps:
- run: echo "Test with read-only access"
Los permisos de job pueden sobrescribir la configuración del workflow:
name: Release Pipeline
permissions:
contents: read # Solo lectura por defecto
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read # Hereda solo lectura
steps:
- run: echo "Build needs read only"
release:
runs-on: ubuntu-latest
permissions:
contents: write # Escritura para crear Release
packages: write # Publicar en Packages
steps:
- name: Create Release
uses: actions/create-release@v1
Un caso real: en un proyecto, el job build solo necesitaba leer código y el job release debía crear Release y subir imagen Docker. Con permisos por job, aunque comprometan build, no pueden escribir en el repositorio.
Modos de permisos a nivel de repositorio
Además del workflow, en la configuración del repositorio hay dos modos:
- Permissive (permisivo): GITHUB_TOKEN tiene permisos de lectura y escritura por defecto. Aplica cuando el workflow no declara
permissions. - Restricted (restringido): GITHUB_TOKEN solo lee contents y packages por defecto. Hace falta declarar permisos extra para escribir.
Conviene poner todos los repositorios en modo Restricted. En Settings > Actions > General, en “Workflow permissions”, elige “Read repository contents and packages permissions”.
Así, aunque un workflow olvide permissions, no tendrá permisos de más. La frontera de seguridad queda cerrada a nivel de repositorio.
Registros de auditoría y revisión de cumplimiento
Eventos de ejecución de workflows en el registro de auditoría
Desde febrero de 2021, GitHub incluyó en el registro de auditoría de la organización los eventos de ejecución de Actions. Puedes rastrear quién disparó qué workflow, con qué permisos y qué Secrets se accedieron.
Entrada: Settings > Security > Audit log de la organización.
Campos clave:
action: tipo de evento, por ejemploworkflow_run.create,workflow_run.completeactor: quien dispara — usuario, app ogithub-actions[bot]repo: ruta del repositoriotoken_scopes: alcance de permisos del token usadorequest_id: ID de seguimiento para correlacionar logs
Un uso práctico: cuando detectas una ejecución anómala, rastrea con el registro de auditoría. Si un Secret se accede de forma extraña, busca eventos workflow_run para encontrar al responsable.
API de auditoría empresarial
Si tu organización usa GitHub Enterprise, puedes consultar todas las operaciones con la Audit Log API:
curl -H "Authorization: Bearer YOUR_TOKEN" \
"https://api.github.com/enterprises/YOUR_ENTERPRISE/audit-log?phrase=workflow_run"
La respuesta JSON incluye el detalle de cada evento. Puedes exportarla a un SIEM (Splunk, Datadog, etc.) para monitoreo continuo.
Cumplimiento normativo en breve
Si tu proyecto debe cumplir SOC 2 o ISO 27001, la seguridad del CI/CD es obligatoria. El registro de auditoría es la evidencia directa: demuestra que puedes rastrear actividad de CI/CD, detectar anomalías y responder.
Una configuración recomendada: exportar el registro a un sistema externo y definir alertas para eventos críticos, por ejemplo:
- Aumento brusco de fallos en workflows
- Acceso anómalo a Secrets (muchas lecturas en poco tiempo)
- Workflows disparados desde IPs desconocidas
Adelanto a la hoja de ruta de seguridad de GitHub 2026
En marzo de 2026, GitHub publicó la hoja de ruta de seguridad de Actions con seis funciones importantes. Algunas ya están disponibles; otras siguen en desarrollo.
Bloqueo de dependencias a nivel de workflow
Es la respuesta oficial a ataques como tj-actions. Similar a package-lock.json, fija todas las referencias de Actions a SHA. En el workflow sigues escribiendo uses: tj-actions/changed-files@v45, pero el archivo de bloqueo guarda el SHA correspondiente. Cuando el mantenedor actualiza la Action, el bloqueo no cambia solo — debes revisar y actualizar a propósito.
Aún no está disponible, pero ya puedes fijar SHA manualmente.
Firewall de salida nativo en capa 7
Control nativo del acceso de red externo del pipeline de CI/CD. Por ejemplo, limitar el workflow para que solo hable con tu API de AWS y no con cualquier sitio.
Hoy esto suele requerir runners autohospedados y políticas de red personalizadas. Cuando salga en 2026, los runners en la nube de GitHub también podrán configurar firewall de salida.
Scoped Secrets
Control más fino del alcance de los Secrets. Por ejemplo, que un Secret solo lo use una rama o un job concreto. Hoy el alcance es a nivel de repositorio o entorno; la granularidad es insuficiente.
Controles de ejecución basados en políticas
Definir límites de confianza, aprobaciones y compuertas de attestación. Por ejemplo, exigir aprobación manual antes de ejecutar workflows de PRs desde forks, o que un workflow pase un escaneo de seguridad antes de correr.
Es parecido a las reglas de protección de entornos, pero con más granularidad y lógica más compleja.
Actions Data Stream
Visibilidad en tiempo real de la actividad de CI/CD. Como el registro de auditoría, pero con envío en vivo en lugar de consulta posterior. Integrable con SIEM para monitoreo en tiempo real.
Declaraciones de atributos personalizados OIDC
Refuerza la autenticación con proveedores cloud. El token OIDC puede llevar más atributos — etiquetas del repositorio, información de rama — para que Vault o AWS autoricen con más precisión.
Conclusión
Desde el incidente tj-actions, las tres lecciones centrales para mí son:
Primero, fijar con SHA. No uses etiquetas para Actions de terceros; usa el SHA completo. Es la lección que pagaron 23.000 repositorios.
Segundo, mínimo privilegio. GITHUB_TOKEN trae demasiados permisos por defecto; limítalos con permissions y pon el repositorio en modo Restricted.
Tercero, registro de auditoría. Los eventos de Actions ya están en auditoría; revisa ejecuciones anómalas y exporta a sistemas de monitoreo.
Todo esto lo puedes configurar hoy. No hace falta esperar las novedades de 2026 ni cambiar de herramientas. Abre tu repositorio, revisa referencias de Actions, permisos y auditoría. En media hora tienes la protección básica.
Al final, la seguridad del CI/CD no es magia avanzada, sino hábito en los detalles. Cada SHA fijo y cada permiso de menos reducen el riesgo. tj-actions demostró que el atacante no necesita trucos sofisticados: basta con que descuidemos un detalle pequeño.
Flujo de endurecimiento de seguridad en GitHub Actions
Desde el fijado por SHA hasta el control de permisos: tres pasos clave para reforzar la seguridad del CI/CD.
⏱️ Estimated time: 30 min
- 1
Step 1: Fijar referencias de Actions con SHA
Revisa todos los archivos de workflow y cambia `uses: action@tag` por `uses: action@full-sha` para evitar la manipulación de etiquetas. Usa herramientas como `pinact` para convertir referencias existentes en lote. - 2
Step 2: Configurar la clave permissions
Añade la clave permissions a nivel de workflow o de job, concediendo solo los permisos necesarios. Por ejemplo: `permissions: contents: read` para operaciones de solo lectura, `permissions: contents: write` para crear releases. Configura el repositorio en modo Restricted como red de seguridad. - 3
Step 3: Activar el monitoreo del registro de auditoría
Consulta los eventos de ejecución de workflows de Actions en Settings > Security > Audit log de la organización. Configura alertas para eventos clave: picos de fallos en workflows, acceso anómalo a Secrets, workflows disparados desde IPs desconocidas. Exporta los logs a un sistema SIEM para monitoreo continuo.
FAQ
¿Por qué fijar con SHA en lugar de usar referencias por etiqueta?
¿Cómo controlar los permisos de GITHUB_TOKEN?
¿Cómo detectar si un workflow ha sido comprometido?
¿Qué ventajas tiene OIDC + Vault frente a los Secrets integrados?
¿Se pueden usar Actions de terceros?
12 min de lectura · Publicado el: 16 may 2026 · Actualizado el: 21 ago 2026
Guía completa de GitHub Actions
Si llegaste desde búsqueda, lo más rápido es ir al artículo anterior o siguiente de esta misma serie.
Anterior
Desarrollo de composite Actions en GitHub Actions: guía completa de action.yml al Marketplace
Guía práctica del desarrollo de composite Actions en GitHub Actions: estructura de action.yml, configuración de inputs/outputs, paso de secrets, estrategias de versionado y publicación en Marketplace para dominar la componentización de CI/CD
Parte 9 de 10
Siguiente
Este es el artículo más reciente de la serie por ahora.



Comentarios
Inicia sesión con GitHub para dejar un comentario