ATALAIA
  1. Inicio
  2. Recursos
  3. Blog
  4. Fija tus actions: un arreglo de una tarde
Pipeline · Guía

Fija tus actions: un arreglo de una tarde

Las etiquetas se mueven, los SHA no. Fija cada action, deja que un bot las mantenga al día y reduce el token a lo que necesita cada job.

3 min de lecturaEquipo Atalaia

GitHub ActionsCI/CDHardening

La mayoría de los ficheros de workflow referencian actions como actions/checkout@v4. Parece una versión, pero es un tag de git, y quien controle el repositorio puede apuntar un tag a cualquier commit que quiera, incluido uno que ayer no existía. Tu pipeline ejecuta entonces el código nuevo con tus secretos, en el siguiente push, sin que cambie una sola línea en tu repositorio.

La solución no es glamurosa y no necesita una herramienta nueva. Es una tarde de fijar versiones, un pequeño fichero de configuración y un par de líneas de permisos. Este es el orden en que lo haríamos.

Por qué las etiquetas son el punto débil

Una referencia por tag se resuelve en tiempo de ejecución. Si un atacante consigue acceso de escritura al repositorio de una action, o a la cuenta de un maintainer, puede hacer force-push de un tag a un commit malicioso y todos los workflows que usen ese tag lo recogen. Esto es justo lo que pasó en el compromiso de tj-actions/changed-files en marzo de 2025, donde versiones retagueadas volcaron secretos de CI en los logs de build.

Un SHA de commit completo de 40 caracteres es distinto. Nombra un árbol exacto de ficheros. Nadie puede hacer que apunte a otro sitio. Los SHAs cortos y los nombres de branch no cuentan: solo el hash completo es inmutable.

Fija cada action de terceros

Primero averigua qué estás usando. Esto lista cada línea uses: que no sea ya un SHA completo, ignorando las actions locales:

grep -rn "uses:" .github/workflows \
  | grep -v "./" \
  | grep -Ev "@[0-9a-f]{40}"

Puedes resolver una etiqueta a mano con git ls-remote, pero una herramienta es más rápida y menos propensa a errores. La herramienta open source pinact reescribe las etiquetas a SHAs y deja la versión como comentario al final, para que las personas sigan pudiendo leerlo:

pinact run

# before
- uses: actions/setup-node@v4
# after
- uses: actions/setup-node@<full-40-char-sha> # v4.1.0

Dos casos límite. Los pasos basados en Docker que usan docker:// deben fijarse por digest de imagen, no por tag. Y los workflows reutilizables llamados desde otros repositorios necesitan el mismo tratamiento que las actions.

Deja que un bot mantenga los pins al día

La objeción habitual al pinning es que dejas de recibir correcciones. No es así, si Dependabot vigila el ecosistema github-actions. Entiende los pines por SHA con comentarios de versión y abre una pull request que actualiza ambos.

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
    groups:
      actions:
        patterns: ["*"]

Agrupar deja el trabajo en una pull request a la semana en lugar de una docena. Revísala como cualquier otro cambio de dependencias: lee el diff de la action cuando el salto sea grande o el publisher sea pequeño.

Recorta el GITHUB_TOKEN

Fijar limita qué código se ejecuta. Los permisos limitan lo que ese código puede hacer si algo sale mal de todos modos. Pon la organización o el repositorio en solo lectura por defecto, declara los permisos al principio de cada workflow y amplíalos solo en el job que lo necesite:

permissions:
  contents: read

jobs:
  release:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      id-token: write

Un permissions: {} vacío también es válido, para jobs que solo hacen lint o tests. Ya que estás, busca claves cloud de larga duración en los secretos y sustitúyelas por federación OIDC donde tu proveedor lo soporte.

La trampa de pull_request_target

pull_request_target se ejecuta en el contexto del repositorio base, con acceso a secretos y a un token con permisos de escritura. Existe para que los workflows puedan etiquetar o comentar pull requests de forks. La trampa es hacer checkout del código del fork y ejecutarlo:

on: pull_request_target
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@<sha>
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - run: npm install && npm test   # runs attacker code with your secrets

Lo mismo ocurre al interpolar campos no confiables, como el título de una pull request, directamente en un script de run:. Pásalos mejor por una variable de entorno, para que el shell los trate como datos.

Compruébalo en 10 minutos

  1. Ejecuta el grep de arriba y cuenta las referencias sin fijar.
  2. Ejecuta zizmor .github/workflows. Señala actions sin fijar, permisos excesivos, triggers arriesgados e inyección de plantillas.
  3. Ejecuta actionlint para detectar los errores de sintaxis que a veces introduce fijar versiones.
  4. Revisa el ajuste del repositorio de permisos de workflow por defecto y ponlo en solo lectura.
  5. Añade el fichero de Dependabot y haz merge de la primera actualización agrupada.

Esa es la tarde. Hecho esto, cada cambio en lo que corre en tu pipeline llega como una pull request revisada, no como un retag silencioso.

Dónde encaja en la línea

En la torre (en desarrollo):

Habladlo

Una llamada de 30 minutos. Sin diapositivas, sin lista de precios y con un siguiente paso en cualquier caso.