Cambiar tema

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

Easton editorial illustration: large CI pipeline shield, immutable commit block, secret vault, token-scope ring, audit trail

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 ejemplo workflow_run.create, workflow_run.complete
  • actor: quien dispara — usuario, app o github-actions[bot]
  • repo: ruta del repositorio
  • token_scopes: alcance de permisos del token usado
  • request_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. 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. 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. 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?
Las etiquetas son mutables: el mantenedor del repositorio o un atacante puede apuntar una etiqueta a un commit nuevo en cualquier momento. El SHA es inmutable; mientras no actualices la referencia, ejecutas exactamente ese código. El incidente tj-actions demostró el riesgo de manipular etiquetas.
¿Cómo controlar los permisos de GITHUB_TOKEN?
Usa la clave `permissions` para declarar explícitamente los permisos necesarios. Una vez que defines permissions, los permisos no especificados pasan automáticamente a none. Se recomienda configurar el modo de permisos en Restricted en Settings > Actions > General del repositorio.
¿Cómo detectar si un workflow ha sido comprometido?
Revisa los eventos workflow_run en el registro de auditoría de la organización y busca acceso anómalo, IPs desconocidas o fallos frecuentes. Exporta los logs a un sistema SIEM y configura alertas automáticas.
¿Qué ventajas tiene OIDC + Vault frente a los Secrets integrados?
Los Secrets integrados de GitHub son credenciales de larga duración: si el repositorio se compromete, se filtran todos. OIDC + Vault permite acceso sin credenciales almacenadas: GitHub se autentica ante Vault y obtiene un token de corta duración que expira en 1 hora, reduciendo mucho la ventana de ataque.
¿Se pueden usar Actions de terceros?
Sí, pero con cautela. Prioriza Actions oficiales, fija referencias con SHA y audita dependencias con regularidad. La hoja de ruta 2026 de GitHub introducirá bloqueo de dependencias a nivel de workflow para gestionar Actions de terceros con más seguridad.

12 min de lectura · Publicado el: 16 may 2026 · Actualizado el: 21 ago 2026

Comentarios

Inicia sesión con GitHub para dejar un comentario

Easton BlogEaston Blog