Seguridad en GitHub Actions: permisos mínimos, secretos y OIDC paso a paso
Publicado el
Si diseñas pipelines en GitHub Actions para proyectos Astro desplegados en Netlify —o cualquier stack similar— es probable que ya controles linting, tests y performance budgets. Pero hay una pregunta que muchos pipelines no responden bien: ¿qué pasa si alguien compromete un token, abre una PR maliciosa o fuerza un push a main?
Este artículo cubre cuatro capas de defensa que puedes aplicar de forma progresiva: permisos mínimos en GITHUB_TOKEN, gestión de secretos con scopes por entorno, OIDC para eliminar credenciales estáticas y branch protection rules que refuercen todo lo anterior. Al final, un checklist auditable para revisarlo periódicamente.
1. Por qué la seguridad de tu pipeline importa (y se ignora)
La mayoría de proyectos pequeños y medianos arrancan con workflows que tienen permisos amplios por defecto. Funciona, así que nadie lo revisa. El problema aparece cuando:
- Un colaborador abre una PR que ejecuta un workflow con acceso a secretos de producción.
- Un token de deploy se filtra en logs porque no se enmascaró correctamente.
- Alguien hace push directo a
mainy el deploy a producción se ejecuta sin revisión.
Ninguno de estos escenarios es teórico. Son errores documentados en proyectos reales y repositorios open source. La buena noticia: GitHub Actions proporciona herramientas nativas para mitigarlos sin añadir complejidad innecesaria.
2. Permisos mínimos para GITHUB_TOKEN
Desde 2023, GitHub permite configurar los permisos por defecto de GITHUB_TOKEN a nivel de repositorio y de organización. La primera medida es restringir los permisos globales a solo lectura y declarar explícitamente lo que cada workflow necesita.
2.1. Configuración a nivel de repositorio
En Settings → Actions → General → Workflow permissions, selecciona Read repository contents and packages permissions. Esto fuerza que cada workflow declare los permisos que necesita.
2.2. Declaración explícita en el workflow
En la raíz del archivo YAML, añade un bloque permissions con solo lo que el job necesita:
permissions:
contents: read
pull-requests: write # solo si el workflow comenta en PRs
deployments: write # solo si registra deployments
Si un job necesita permisos distintos a otros, declara permissions a nivel de job en lugar de a nivel de workflow:
jobs:
lint-and-test:
permissions:
contents: read
runs-on: ubuntu-latest
steps:
# ...
deploy:
permissions:
contents: read
deployments: write
id-token: write # necesario para OIDC
runs-on: ubuntu-latest
steps:
# ...
Regla práctica: si no sabes qué permisos necesita tu workflow, empieza con permissions: {} (ninguno) y ve añadiendo según los errores que aparezcan. Es más seguro iterar desde cero que recortar desde write-all.
3. Gestión de secretos con scopes por entorno
GitHub permite crear Environments (entornos) con secretos y variables independientes. Esto significa que puedes tener un secreto NETLIFY_AUTH_TOKEN distinto para dev, staging y production, y cada uno con sus propias reglas de acceso.
3.1. Crear entornos separados
En Settings → Environments, crea al menos staging y production. Para cada uno puedes configurar:
- Reviewers obligatorios: el deploy a producción requiere aprobación manual de al menos un revisor.
- Wait timer: un temporizador de espera antes de que el deploy se ejecute (útil para cancelar a tiempo).
- Branch filters: limitar qué ramas pueden desplegar a ese entorno (por ejemplo, solo
mainpuede desplegar a producción).
3.2. Referenciar el entorno en el workflow
jobs:
deploy-production:
runs-on: ubuntu-latest
environment:
name: production
url: https://adrianmariscal.es
steps:
- name: Deploy a Netlify
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
run: netlify deploy --prod
Al declarar environment: production, GitHub Actions solo inyecta los secretos de ese entorno, no los globales de otro scope. Si alguien intenta ejecutar ese job desde una rama que no es main, el branch filter lo bloquea antes de exponer el token.
3.3. Secretos de repositorio vs. secretos de entorno
| Tipo | Alcance | Cuándo usarlos |
|---|---|---|
| Secreto de repositorio | Todos los workflows y ramas | Tokens que no tienen impacto en producción (ej: CODECOV_TOKEN) |
| Secreto de entorno | Solo el job que declare ese environment | Tokens de deploy, API keys de servicios en producción |
Error frecuente: poner el token de Netlify como secreto de repositorio en lugar de secreto de entorno. Así cualquier workflow en cualquier rama puede acceder a él.
4. OIDC: deploys sin credenciales estáticas
OpenID Connect (OIDC) permite que tu workflow obtenga un token de corta duración directamente del proveedor (AWS, GCP, Azure) sin necesidad de almacenar credenciales de larga duración como secretos.
4.1. Cómo funciona
- El workflow solicita un JWT firmado por GitHub con información sobre el repositorio, la rama y el job.
- El proveedor cloud (o cualquier servicio compatible con OIDC) verifica la firma y emite un token temporal.
- El workflow usa ese token temporal para ejecutar la acción (deploy, acceso a un bucket, etc.).
- El token expira automáticamente en minutos.
La ventaja principal: no hay secreto que rotar, filtrar ni revocar. Si alguien intercepta el token, no le sirve porque ya expiró o porque la policy del proveedor valida el repositorio y la rama de origen.
4.2. Configuración del workflow para OIDC
El primer requisito es declarar el permiso id-token: write:
jobs:
deploy:
permissions:
id-token: write
contents: read
runs-on: ubuntu-latest
steps:
- name: Obtener credenciales temporales
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/github-deploy
aws-region: eu-west-1
4.3. ¿Y para Netlify?
A fecha de este artículo, Netlify no soporta OIDC nativo para autenticar deploys desde GitHub Actions. Si usas Netlify CLI, el NETLIFY_AUTH_TOKEN sigue siendo necesario. En ese caso, combina OIDC para servicios que sí lo soporten (AWS, GCP) y refuerza el token de Netlify con secretos por entorno y branch filters, como se describe en la sección 3.
Si tu stack incluye funciones serverless en AWS o almacenamiento en un bucket, esas partes sí pueden usar OIDC y eliminar credenciales estáticas para el resto del pipeline.
5. Branch protection rules
Las branch protection rules son la última línea de defensa: aseguran que nadie (incluido el propietario del repositorio) pueda saltarse las validaciones del pipeline.
5.1. Configuración recomendada para main
En Settings → Branches → Branch protection rules, crea una regla para main con:
- Require a pull request before merging: obliga a pasar por PR; nadie hace push directo.
- Require approvals (1 mínimo): al menos una persona revisa antes de mergear.
- Require status checks to pass before merging: selecciona los jobs de tu pipeline (lint, test, build) como checks obligatorios.
- Require branches to be up to date before merging: evita merge de ramas desactualizadas que podrían romper main.
- Do not allow bypassing the above settings: incluso los admins deben seguir las reglas.
5.2. Rulesets (alternativa moderna)
Desde 2023, GitHub ofrece Rulesets como evolución de las branch protection rules. Los rulesets permiten las mismas restricciones pero con mejor granularidad: puedes aplicarlos por patrón de rama, por tag, y gestionarlos a nivel de organización. Si trabajas con un modelo dev → staging → main, los rulesets facilitan aplicar reglas distintas a cada rama protegida sin repetir configuración.
5.3. Proteger también dev y staging
Si usas ramas intermedias, aplica un nivel de protección proporcional:
dev: require status checks (lint + test). No exigir aprobación manual.staging: require status checks + 1 aprobación. Branch filter en el entorno staging.main: máxima protección. Status checks + aprobación + no bypass + environment protection.
6. Errores frecuentes que exponen tu pipeline
Estos son los problemas más comunes que suelen pasar desapercibidos:
Logs que imprimen secretos. Si un paso ejecuta echo $NETLIFY_AUTH_TOKEN por error, el token aparece en los logs del workflow. GitHub intenta enmascarar secretos conocidos, pero no siempre lo logra con valores derivados o concatenados. Revisa los logs de tus últimas ejecuciones.
pull_request_target con checkout del fork. El evento pull_request_target tiene acceso a los secretos del repositorio, no del fork. Si además haces checkout del código del fork (ref: ${{ github.event.pull_request.head.sha }}), estás ejecutando código no revisado con acceso a secretos de producción.
Actions de terceros sin versión fija. Usar uses: some-action@main significa que el autor puede cambiar el código en cualquier momento. Fija siempre la versión con el hash del commit:
# Mal
uses: netlify/actions/cli@main
# Bien
uses: netlify/actions/cli@abcdef1234567890abcdef1234567890abcdef12
Workflows que no restringen workflow_dispatch. Si habilitas ejecución manual con workflow_dispatch pero no añades condiciones, cualquier colaborador con permisos de escritura puede disparar un deploy a producción desde cualquier rama.
No rotar secretos. Si usas tokens estáticos (como NETLIFY_AUTH_TOKEN), establece una rotación periódica —cada 90 días es un intervalo razonable— y documenta quién tiene acceso.
7. Checklist auditable de seguridad CI/CD
Revisa estos puntos cada vez que modifiques tu pipeline o cada trimestre como mínimo:
Permisos:
- Los permisos por defecto del repositorio están configurados como solo lectura.
- Cada workflow declara
permissionsexplícitamente. Ninguno usawrite-all. - Los jobs que no necesitan secretos no tienen acceso a ellos.
Secretos:
- Los tokens de deploy están en secretos de entorno, no de repositorio.
- Cada entorno (
staging,production) tiene sus propios secretos. - Los secretos se rotan según un calendario definido.
- Los logs de las últimas 5 ejecuciones no contienen valores sensibles.
OIDC:
- Los servicios que soportan OIDC (AWS, GCP, Azure) lo usan en lugar de tokens estáticos.
- El claim del JWT restringe repositorio, rama y entorno.
Branch protection:
mainrequiere PR, aprobación, status checks y no permite bypass.- Las ramas intermedias tienen protección proporcional al riesgo.
workflow_dispatchsolo es accesible desde ramas protegidas o con condiciones.
Actions de terceros:
- Todas las actions de terceros están fijadas con hash de commit.
- Se revisan los changelogs antes de actualizar a una nueva versión.
Monitorización:
- Las alertas de Dependabot están activas para el repositorio.
- El audit log de la organización (si aplica) se revisa periódicamente.
8. Conclusión
La seguridad de un pipeline no es un paso extra al final del proyecto: es una propiedad del sistema que se diseña desde el principio y se verifica de forma continua. Las cuatro capas descritas —permisos mínimos, secretos por entorno, OIDC y branch protection— se complementan entre sí y ninguna por separado es suficiente.
Si mantienes un proyecto Astro desplegado en Netlify (o cualquier combinación de generador estático + hosting), aplica estas medidas de forma progresiva: empieza por los permisos y los secretos por entorno, que son los cambios con mayor impacto y menor fricción. OIDC y rulesets pueden venir después, cuando el flujo esté estabilizado.
El checklist de la sección 7 está pensado para revisarlo cada trimestre. Si lo integras en tu proceso de mantenimiento, la seguridad del pipeline deja de ser una tarea pendiente y se convierte en parte del sistema.