Saltar al contenido

Temas

Secretos y CI/CD sin llaves con OIDC

Detectar secretos en commits e historial con gitleaks y GitHub secret scanning, por qué rotar lo filtrado, y eliminar las access keys de larga vida con federación OIDC de GitHub Actions a un rol IAM.

IntermedioEn progreso 6 min de lectura1 oct 2026#secretos#oidc#github-actions#iam#cicd#aws

Dos verdades incómodas sobre los secretos en el pipeline. La primera: tarde o temprano alguien comete uno —una access key, un token, una contraseña de base de datos— en un repositorio. La segunda: una vez en el historial de Git, ese secreto está filtrado para siempre, aunque borres el fichero en el commit siguiente. Este tema va de las dos mitades del problema: detectar y rotar lo que se filtra, y eliminar la categoría entera de las llaves de larga vida usando federación OIDCFederación OIDCRelación de confianza entre un proveedor OIDC (como GitHub Actions) y AWS que entrega credenciales temporales sin access keys de larga vida. para que el CI no tenga que guardar ninguna.

Secretos en el código: detección e historial#

Un secreto commiteado es distinto de un fichero borrado. Git guarda todo el historial: si en el commit a1b2c3 había una AKIA... y en d4e5f6 la quitaste, cualquiera que clone el repositorio puede sacarla con git log -p. Por eso la detección tiene que mirar dos cosas: los commits nuevos (para no añadir más) y el historial completo (para saber qué ya está expuesto).

  • gitleaks: escáner open source que busca patrones de secretos (claves AWS, tokens, claves privadas) en el árbol de trabajo y en todo el historial. Se ejecuta como hook de pre-commit, como job de CI y como barrido puntual del repositorio entero.
  • GitHub secret scanning: integrado en la plataforma. Detecta secretos de proveedores conocidos y, con push protection, bloquea el push que intenta introducir uno antes de que llegue al repositorio. Para patrones de proveedores soportados, hay además alertas y, en muchos casos, notificación automática al proveedor para revocarlos.
Terminal
# Barrido del historial completo del repositorio
gitleaks detect --source . --redact
# Como control en pre-commit: solo lo que se va a commitear
gitleaks protect --staged --redact

Rotar lo filtrado: lo único que de verdad lo corta#

Si un secreto se filtró, la única respuesta fiable es rotarlo: invalidar el valor antiguo y emitir uno nuevo. Limpiar el historial es higiene, pero no deshace la exposición; mientras la credencial siga siendo válida, sigue sirviendo a quien la copió.

  1. Revoca ya la credencial filtrada (desactiva la access key, borra el token, cambia la contraseña). Hazlo antes de limpiar nada.

  2. Emite una nueva y actualiza quien la use, idealmente en un gestor de secretos (AWS Secrets Manager), nunca de vuelta en el código.

  3. Investiga el uso: revisa CloudTrail por actividad de esa credencial entre la filtración y la revocación. Una key filtrada puede haberse usado ya.

  4. Limpia el historial si procede (git filter-repo) y previene la reincidencia con push protection y hooks.

En Citas quedó de todo esto una lección concreta: existía una access key legacy ci-deploy-legacy de cuando el pipeline guardaba llaves en GitHub. Mientras exista, es un secreto de larga vida que puede filtrarse. La solución no es protegerla mejor: es hacerla innecesaria.

CI/CD sin llaves: federación OIDC#

Una access key de IAM es un secreto de larga vida: si se filtra, sirve hasta que alguien la rota, y vive guardada como secret en GitHub para que el workflow la use. La federación OIDC elimina esa categoría entera. En lugar de una llave guardada, GitHub Actions presenta en cada ejecución un token OIDC firmado que lo identifica, y AWS STS lo cambia por credenciales temporales si cumple las condiciones de la trust policy del rol.

CI/CD sin llaves: federación OIDC a un rol IAM
GitHub Actionsrepo citas · mainOtro repo/ramarama devAWS STSAssumeRoleWithWebIdentityCredencialestemporales · 1 hTRUST POLICY · condicionessub: repo:clinicas/citas:ref:refs/heads/mainaud: sts.amazonaws.comsub: …refs/heads/mainsub: …refs/heads/devsub coincide ✓sub ≠ condición → AccessDeniedOIDC JWTcreds 1 hOIDC JWT

Paso 1 de 5Token OIDC

El workflow de GitHub Actions pide un token OIDC firmado que lo identifica: repositorio, rama y entorno van dentro del claim sub.

El mecanismo tiene dos piezas: un identity provider OIDC en IAM que confía en token.actions.githubusercontent.com, y un rol cuya trust policy restringe quién puede asumirlo. Ahí está toda la seguridad.

Secret de larga vida en GitHub

OIDC: credenciales temporales

.github/workflows/deploy.yml (inseguro)
# Access keys guardadas como secrets del repositorio
- name: Configurar credenciales
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-1
# La llave vive guardada, no caduca y sirve a quien la copie
.github/workflows/deploy.yml (OIDC)
permissions:
id-token: write # necesario para pedir el token OIDC
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::111122223333:role/citas-deploy
aws-region: eu-west-1
# Sin secretos: STS devuelve credenciales temporales en cada ejecución

Las condiciones de la trust policy: el detalle que importa#

Un token OIDC de GitHub Actions es criptográficamente válido para cualquier repositorio de GitHub. Lo que impide que el workflow de un desconocido asuma tu rol son las condiciones sobre los claims. Las dos que no pueden faltar:

  • token.actions.githubusercontent.com:sub: identifica el origen exacto. Su formato depende del disparador:
    • una rama: repo:clinicas/citas:ref:refs/heads/main
    • un tag: repo:clinicas/citas:ref:refs/tags/v1.2.0
    • un entorno: repo:clinicas/citas:environment:prod
    • un pull request: repo:clinicas/citas:pull_request
  • token.actions.githubusercontent.com:aud: la audiencia, que con la action oficial es sts.amazonaws.com.
citas-deploy · trust policy
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::111122223333:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:clinicas/citas:environment:prod"
}
}
}

Este patrón es el mismo que se trata con más profundidad en IAM avanzado (identidad y datos en AWS · IAM avanzado): la trust policy es, en el fondo, una política de confianza entre un proveedor de identidad externo y tu cuenta.

Tu turno

Migra un workflow de access keys a OIDC

Toma un workflow de GitHub Actions que hoy use AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY (en tu trabajo o tu lab).

  • Crea el identity provider OIDC de GitHub en IAM y un rol con una trust policy que fije el repositorio y la rama o el entorno exactos, y el aud.
  • Da al rol solo los permisos que el workflow necesita de verdad; no reutilices un rol de administración.
  • Sustituye los secrets por role-to-assume y añade permissions: id-token: write.
  • Ejecuta el workflow desde la rama permitida (debe funcionar) y desde otra rama (debe recibir AccessDenied).
  • Localiza y revoca cualquier access key de larga vida que ya no haga falta.
Campo Tu respuesta
Workflow
sub permitido (repo/rama/entorno)
¿Otra rama recibe AccessDenied?
Access keys revocadas

Reto

Borraste un fichero con una access key y reescribiste el historial con git filter-repo. ¿Estás a salvo?

▸Mostrar solución

No, o no del todo. Reescribir el historial limpia el repositorio, pero no invalida la credencial: si alguien la copió mientras estuvo expuesta, sigue siendo válida. Lo único que corta el riesgo es rotarla (desactivar la key y emitir una nueva) y revisar CloudTrail por si ya se usó. La limpieza del historial es un paso posterior, no el principal.

Reto

Tu trust policy comprueba el sub pero olvidaste el aud. ¿Qué expone eso?

▸Mostrar solución

Que aceptas tokens OIDC emitidos para otra audiencia. El aud ata el token al destinatario esperado (sts.amazonaws.com). Sin esa condición, un token válido de GitHub generado con otra audiencia podría encajar con tu sub y ampliar la superficie de confianza más de lo que crees. Comprueba siempre aud y sub.

En resumen#

  • Los secretos se filtran: detecta en commits nuevos (push protection, pre-commit) y en el historial completo (gitleaks, secret scanning).
  • Borrar el fichero no basta; el secreto vive en el historial y en los forks. Lo único que corta la exposición es rotar la credencial.
  • Las access keys de larga vida en CI son la categoría a eliminar, no a proteger mejor.
  • La federación OIDC cambia un token efímero por credenciales temporales; toda la seguridad está en las condiciones sub (repo + rama/entorno) y aud de la trust policy.
  • OIDC resuelve la autenticación; el rol sigue necesitando mínimo privilegio en sus permisos.

Referencias#

Notas relacionadas

ESC
↑↓ para navegar↵ para abriresc para cerrar