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.
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
# 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.