← Blog ·

Secretos en GitHub Actions: patrones seguros y los 5 errores típicos

Casi todos los repositorios que usan GitHub Actions manejan CI secrets: tokens de despliegue, claves de proveedores cloud, credenciales de bases de datos, API keys de terceros. Y casi todos cometen los mismos cinco errores, una y otra vez, aunque usen secrets.MY_TOKEN correctamente en la sintaxis.

Esta guía cubre patrones concretos para proteger GitHub Actions secrets y los fallos más comunes que los invalidan.

Por qué los secretos de CI son distintos (y más peligrosos)

Un secreto de CI no vive solo en tu cabeza o en tu portátil: pasa por runners efímeros, logs que se archivan, workflows reutilizables, acciones de terceros y, a veces, pull requests de forks. Cada uno de esos puntos es una superficie de fuga adicional. GitHub cifra los secretos en reposo y los enmascara en los logs, pero eso no protege contra un workflow mal configurado que los imprime, los pasa a una acción no confiable o los reutiliza donde no debería.

Error 1: hardcodear secretos o subir .env al repo

El clásico: alguien copia una clave de API en el YAML del workflow "solo para probar", o un .env con credenciales reales acaba en el commit. Una vez en el historial de git, ese secreto está comprometido, aunque lo borres después.

Patrón correcto: todo secreto va a Settings → Secrets and variables → Actions, nunca al código:

gh secret set STRIPE_API_KEY --body "sk_live_..." --repo tu-org/tu-repo

Y en el workflow, referencia por nombre:

steps:
 - name: Deploy
 env:
 STRIPE_API_KEY: ${{ secrets.STRIPE_API_KEY }}
 run: ./deploy.sh

Error 2: volcar secretos en los logs

GitHub enmascara automáticamente el valor exacto de un secret si aparece literal en la salida, pero eso falla en cuanto el secreto se transforma: se codifica en base64, se concatena en una URL, o se imprime con set -x dentro de un script que no usa la variable de entorno esperada.

# Mal: set -x expone todo lo que se ejecuta, incluidas variables interpoladas
set -x
curl "https://api.example.com?key=$API_KEY"

Patrón correcto: evita set -x en pasos que tocan secretos, y si necesitas depurar, hazlo con el secreto redactado explícitamente:

echo "::add-mask::$API_KEY"
curl -s -o /dev/null -w "%{http_code}" "https://api.example.com?key=$API_KEY"

Error 3: credenciales de larga duración en vez de OIDC

Muchos pipelines siguen usando AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY guardados como secreto permanente. Si ese secreto se filtra, el atacante tiene acceso indefinido hasta que alguien lo rote manualmente.

Patrón correcto: usa OpenID Connect para obtener credenciales de corta duración en cada ejecución, sin guardar ninguna clave estática:

permissions:
 id-token: write
 contents: read

steps:
 - uses: aws-actions/configure-aws-credentials@v4
 with:
 role-to-assume: arn:aws:iam::123456789012:role/github-deploy
 aws-region: eu-west-1

AWS, GCP y Azure soportan este flujo. No hay secreto que rotar ni que filtrar porque no existe: el token se emite y expira en minutos.

Error 4: permisos de GITHUB_TOKEN demasiado amplios

Por defecto, muchos repos heredan permisos de escritura amplios para el token automático de cada ejecución. Si una acción de terceros con una dependencia comprometida corre en tu pipeline, hereda ese mismo alcance.

Patrón correcto: declara permisos mínimos explícitos a nivel de workflow, y amplíalos solo en el job que los necesita:

permissions:
 contents: read

jobs:
 release:
 permissions:
 contents: write
 steps:
 - run: gh release create ...

Error 5: compartir secretos con PRs de forks o con todos los entornos

Si usas pull_request_target (necesario para dar acceso a secrets en PRs de forks), estás ejecutando código no confiable con acceso a tus secretos de producción. Y si no separas environments, un mismo secreto de producción queda disponible en cualquier rama, incluida una de prueba.

Patrón correcto: usa Environments con reviewers obligatorios para producción:

jobs:
 deploy-prod:
 environment:
 name: production
 url: https://app.tuproducto.com
 steps:
 - run: ./deploy.sh

GitHub bloqueará el job hasta que un reviewer aprobado dé luz verde, y el secreto de producción nunca es visible para jobs fuera de ese environment.

Lo que GitHub Actions no resuelve: secretos de desarrollo y del asistente de IA

Todo lo anterior protege los secretos que corren en el pipeline. Pero en la era del código generado por IA, buena parte de las fugas ocurre antes: en el .env local, en el prompt que un asistente de IA usa para probar una API, en un notebook que alguien sube sin darse cuenta. Ese problema es distinto al de CI y merece su propia solución — lo tratamos en detalle en /ai-security.

Es precisamente el hueco que cubre Koove: en vez de que tu agente de IA o tú mismo peguéis claves reales en ficheros de entorno, las guardáis una vez con la CLI:

koove set STRIPE_API_KEY sk_live_xxx --env prod

El código solo referencia el nombre, el secreto se cifra de extremo a extremo (X25519 + AES-256-GCM) y el servidor de Koove nunca ve el valor en claro. La app o el backend autorizado lo descifra solo tras verificar App Attest / Play Integrity y biometría — documentado en /docs/sdk y en el detalle de arquitectura de /security. No sustituye a los secrets de GitHub Actions para tu pipeline, pero cierra el flanco de los secretos de desarrollo que normalmente ni siquiera pasan por CI.

Checklist rápido

  • Ningún secreto en el historial de git ni en .env versionado
  • permissions: explícito y mínimo en cada workflow
  • OIDC configurado para tu proveedor cloud, sin claves estáticas
  • Environments con reviewers para cualquier despliegue a producción
  • Revisión periódica de qué acciones de terceros tienen acceso a qué secretos

Si además quieres cerrar el flanco de los secretos que maneja tu asistente de IA o tu entorno local, date una vuelta por /register y prueba Koove.

Código generado por IA

Tu IA escribe el código. ¿Quién guarda los secretos?

El fallo de seguridad más común en apps generadas con IA son las credenciales expuestas. Con Koove, tu propio asistente guarda cada token desde la CLI — cifrado en tu máquina, nunca en el código ni en el repo.