Saltar al contenido
Open Security
DevSecOps y Agentes Intermedio · 30 min

GitHub Actions: el secreto que terminó en los logs

Un secret bien guardado se filtra igual si lo imprimís, si corre en un PR de un fork, o si una acción de terceros lo lee. Acá ves cómo se escapan los secretos en CI y cómo cerrar cada puerta.

#devsecops#ci-cd#github-actions

Antes de empezar necesitás

  • Haber visto un workflow de GitHub Actions (archivo .yml en .github/workflows)
  • Idea básica de variables de entorno y secrets

Al terminar vas a poder

  • Entender cómo GitHub enmascara secrets y cuándo el masking falla
  • Reconocer el riesgo de pull_request_target y de los forks
  • Aplicar permisos mínimos al GITHUB_TOKEN
  • Fijar acciones de terceros por SHA, no por tag móvil

Guardar un secret en GitHub Actions es la parte fácil. La parte que filtra credenciales es todo lo que pasa después: alguien lo imprime en un log, un PR de un fork corre con permisos que no debería, o una acción de terceros que confiaste a ciegas lo lee y lo manda afuera. El secret estaba bien guardado; el pipeline lo dejó salir.

Vector 1: imprimirlo (directo o transformado)

El clásico. echo de un secret a veces se enmascara… y a veces no:

# ❌ Inseguro
- run: echo "token=${{ secrets.API_TOKEN }}" # GitHub lo tapa con ***
- run: echo "${{ secrets.API_TOKEN }}" | base64 # el base64 NO se enmascara

Vector 2: forks y pull_request_target

Acá está la trampa peligrosa. El trigger pull_request normal corre el código del fork sin acceso a tus secrets — a propósito. Pero pull_request_target corre con tus secrets y el GITHUB_TOKEN con permisos del repo base… ejecutando, si te descuidás, código que mandó un desconocido.

# ❌ Peligroso: secrets + checkout del código del PR de un fork
on: pull_request_target
jobs:
  build:
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }} # código no confiable
      - run: ./build.sh # corre con TUS secrets

Vector 3: el GITHUB_TOKEN con demasiados permisos

Cada workflow recibe un GITHUB_TOKEN. Por defecto puede tener permisos de escritura sobre el repo. Si una acción comprometida lo usa, puede pushear código o publicar releases. Acotalo:

# ✅ Permisos mínimos a nivel workflow
permissions:
  contents: read # solo leer el repo; subí a write solo el job que lo necesite
vt@labs:~
# Ver el permiso por defecto de Actions en el repo
gh api repos/:owner/:repo/actions/permissions/workflow

Vector 4: acciones de terceros por tag móvil

uses: alguien/accion@v3 apunta a un tag que el dueño (o quien comprometa su cuenta) puede mover a un commit malicioso. Vos creés que corrés v3; corrés lo que sea que apunte v3 hoy.

# ❌ Tag móvil: el contenido puede cambiar bajo tus pies
- uses: alguien/accion@v3

# ✅ Fijado por SHA: inmutable, audita lo que realmente corre
- uses: alguien/accion@a1b2c3d4e5f6... # commit SHA completo

El criterio

vector de fuga                   mitigación
echo / transformar un secret      no imprimir; loguear longitud o hash
pull_request_target + fork        sin checkout del código del fork
GITHUB_TOKEN con write global     permissions: contents: read por defecto
acción de terceros por @vX        fijar por SHA de commit

Lo que practicás en este lab

Llevátelo a tu repo si querés, pero no es obligatorio: es tu aprendizaje.

  • El antes/después de un workflow: permisos amplios → mínimos
  • Una tabla: vector de fuga → mitigación
  • Writeup de 2 líneas: cuál de los vectores era el más fácil de explotar

Reto

Tomá el workflow inseguro de abajo y cerralo: permisos mínimos del token, sin echo de secretos, y acciones fijadas por SHA. Escribí en dos líneas qué fuga concreta dejás de tener tras el fix.

Resolvelo y escribí dos líneas explicando qué pasó. Con eso lo fijás.

¿Hiciste el lab?

Si querés, guardá lo que hiciste (comandos, notas, un repo) para volver después. Y si encontrás un error o querés mejorar este lab, contribuí al repo. El progreso se guarda solo en tu navegador.