Secretos en CI/CD: tu pipeline es la bóveda real de producción
Los secretos en CI/CD son secretos de producción, aunque el job diga test. Si un atacante controla un workflow durante seis minutos, importa qué puede desplegar, publicar, leer o exfiltrar.
Los secretos en CI/CD suelen tener más poder que la aplicación que despliegan. Un servicio productivo quizá accede a una base de datos, una cola y un bucket. Un pipeline muchas veces puede leer credenciales cloud, publicar paquetes, asumir roles de deploy, firmar artefactos, descargar dependencias privadas, escribir releases, actualizar infraestructura y exponer logs de cada paso intermedio.
Eso convierte a CI/CD en un runtime privilegiado, no en una capa de conveniencia. El error peligroso es tratar los workflows como pegamento mientras el código de aplicación recibe toda la atención de seguridad. Si un atacante controla el pipeline durante seis minutos, la pregunta correcta no es "¿pasaron los tests?". Es qué puede desplegar, publicar, rotar, leer o exfiltrar antes de que un límite lo detenga.
Los secretos en CI/CD son secretos de producción, no variables de build
Los secretos en CI/CD son secretos de producción porque el pipeline suele tener la autoridad que cambia producción. Un DATABASE_URL en un job de test puede ser inocuo. Un rol cloud que puede desplegar, leer parameter stores, actualizar Kubernetes, publicar contenedores o subir paquetes no se vuelve inocuo por vivir dentro de un workflow.
La etiqueta "CI" esconde el privilegio. Los jobs de build parecen temporales. Los runners parecen descartables. Los logs parecen operativos. Pero un runner de vida corta puede emitir un token cloud, publicar un paquete malicioso, subir un artefacto contaminado o filtrar una clave de firma durante los pocos minutos en que existe. La duración del runtime no reduce el blast radius cuando la credencial puede cambiar la realidad de los clientes.
El primer diagnóstico es brutalmente simple:
- Si este workflow se compromete, ¿qué puede desplegar?
- ¿En qué registries puede publicar?
- ¿Qué entornos puede leer?
- ¿Qué secretos aparecen en memoria, variables de entorno, logs, caches o artefactos?
- ¿Qué credenciales habría que rotar después de una ejecución sospechosa?
Cualquier respuesta que diga "todo lo del repositorio" ya es un hallazgo. La comodidad a nivel repositorio no es un modelo de seguridad. Es una forma de hacer que cada workflow sea tan sensible como el job más privilegiado del repo.
Cada workflow es un límite de seguridad
Cada workflow es un límite de seguridad porque los triggers deciden qué código corre con qué confianza. La diferencia entre pull_request, pull_request_target, push, workflow_dispatch y workflow_run no es solo ergonomía. Es la diferencia entre código no confiable, código confiable, código disparado por humanos, contexto heredado y autoridad productiva.
El patrón de falla más común es cruzar ese límite por accidente. Un workflow de pull request hace checkout de código desde un fork externo, instala dependencias, ejecuta scripts de build, restaura caches compartidos o llama a un reusable workflow que hereda secretos. El YAML parece normal. El modelo de confianza está roto.
Un modelo más seguro separa cuatro fases:
| Fase | Nivel de confianza | Autoridad permitida |
|---|---|---|
| Construir código no confiable | Bajo | Leer source, correr tests, sin secretos, sin publish. |
| Verificar artefactos | Medio | Leer artefactos, validar provenance, sin deploy. |
| Publicar paquetes | Alto | Token de registry u OIDC acotado, sin secretos ajenos. |
| Desplegar producción | Máximo | Entorno protegido, aprobación, rol cloud acotado. |
Un workflow puede contener esas fases, pero un job no debería mezclarlas. Un job que construye código de un fork no debería recibir id-token: write, autoridad de publicación, secretos de deploy ni permisos amplios de GITHUB_TOKEN. Un job que despliega producción debería partir de un artefacto limpio y confiable, no de un cache mutable restaurado desde trabajo anterior.
La disciplina de blast radius que se aplica a sistemas agentic también aplica acá. El marco de blast radius en agentic coding sirve porque las acciones de CI también viven en una escalera: leer archivos, correr tests, escribir artefactos, publicar paquetes, modificar infraestructura y desplegar producción. Cada peldaño necesita un gate distinto.
Los secretos en CI/CD deben vivir por job, no por repositorio
Los secretos en CI/CD deberían acotarse al job más pequeño que los necesita, no a todos los workflows de un repositorio. Los secretos a nivel repositorio son fáciles de configurar y difíciles de razonar. Hacen que el repositorio parezca el límite de seguridad cuando el límite real es el contexto de ejecución.
Un token de publicación de paquetes no debería existir en un job de lint. Un rol de deploy productivo no debería estar disponible para una matriz de tests. Una clave service_role no debería ser alcanzable por ningún workflow que ejecute código no confiable. El artículo sobre seguridad de Supabase en producción explica por qué esas claves son especialmente sensibles; CI vuelve la misma clave más peligrosa cuando puede filtrarse por logs, artefactos o scripts.
El patrón base en GitHub Actions debería ser negación explícita:
name: ci
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
- run: npm ci
- run: npm test
deploy:
runs-on: ubuntu-latest
needs: test
environment: production
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
- run: ./scripts/deploy.shLo importante no es el YAML exacto. Lo importante es la postura: read-only por defecto, escalamiento por job, deploy detrás de un environment y OIDC solo donde el job necesita pedir identidad. Si un job no necesita un permiso, no debería recibirlo.
Los secretos también necesitan nombre y ownership. AWS_ACCESS_KEY_ID dice casi nada. PROD_DEPLOY_ROLE_BILLING_API dice scope, entorno e intención. Durante un incidente, esa diferencia ahorra horas porque los responders pueden identificar qué rotar antes de que el atacante obtenga una segunda ejecución.
OIDC ayuda solo cuando las trust policies son estrechas
OIDC ayuda a quitar secretos cloud de larga vida de CI/CD, pero no vuelve seguro un workflow amplio. Cambia la forma de la credencial. En vez de guardar un secreto estático, el job prueba su identidad y recibe un token de vida corta desde un proveedor cloud, registry o secrets manager.
Eso es una mejora real. Las claves de larga vida en secretos del repositorio amplían la ventana de exposición. Los tokens OIDC expiran rápido y pueden atarse a claims como repositorio, branch, environment, actor, path del workflow o propiedades custom. El detalle crítico es que la trust policy debe ser lo bastante estrecha como para significar algo.
Una policy peligrosa de OIDC dice, en la práctica: "cualquier workflow de este repositorio puede asumir este rol". Una policy más segura dice: "solo este path de workflow, en esta branch protegida, para este environment, desde este repositorio, puede asumir este rol". Esa diferencia decide si OIDC es least privilege o una versión de vida más corta del mismo exceso.
OIDC debería combinarse con tres controles:
- Dar
id-token: writesolo al job que necesita el token. - Atar la trust policy externa a branch, environment, workflow y repository claims.
- Aislar jobs de publish y deploy de código no confiable, caches mutables y acciones de terceros innecesarias.
Las credenciales de vida corta siguen importando en memoria. Un token que expira en minutos puede robarse durante los minutos en que es válido. Por eso importan el aislamiento del runner, los límites de cache, el pinning de actions y el monitoreo de egress. OIDC elimina una clase de secreto. No elimina la necesidad de diseñar el workflow como límite de confianza.
La respuesta a incidentes empieza con inventario de credenciales
La respuesta a incidentes de CI/CD empieza antes del incidente con una lista de credenciales que cada workflow puede tocar. Sin ese inventario, una ejecución sospechosa se convierte en adivinanza. Los equipos revisan YAML, buscan variables de entorno, inspeccionan logs y rotan lo primero que recuerdan.
Un inventario útil mapea cada workflow contra autoridad:
| Workflow | Secretos o identidades | Sistemas externos | Owner de rotación |
|---|---|---|---|
ci.yml | ninguno | descarga de paquetes | Plataforma |
publish.yml | OIDC para npm trusted publishing | npm registry | Developer platform |
deploy-api.yml | rol cloud de deploy | Kubernetes, registry, secrets manager | Infraestructura |
migrate.yml | rol de migración de base de datos | base de datos productiva | Backend |
Este inventario cambia la respuesta. Si publish.yml se comporta raro, el equipo sabe qué permisos de registry, artefactos, attestations, versiones de paquete y logs del runner revisar. Si deploy-api.yml corre desde un actor o branch inesperado, el equipo sabe qué rol cloud y environment de deploy revocar primero.
El playbook de rotación debe estar escrito antes:
- Deshabilitar el workflow sospechoso.
- Revocar sesiones cloud activas y trust OIDC cuando sea posible.
- Rotar secretos estáticos alcanzables por el workflow.
- Invalidar tokens de registry y credenciales de publish.
- Reconstruir artefactos desde un runner limpio.
- Revisar logs, caches, artefactos y destinos de red saliente.
- Restaurar deploy solo después de corregir el límite del workflow.
El peor momento para descubrir que un secreto despliega todos los servicios es después de que un runner ya lo filtró.
¿Qué secretos y permisos de CI/CD son demasiado amplios?
Los secretos y permisos de CI/CD son demasiado amplios cuando sobreviven fuera del job, environment y workflow que los necesitan. Una buena prueba es si la credencial puede explicarse en una frase sin decir "por si acaso".
¿Los workflows de pull request deberían acceder a credenciales cercanas a producción?
Los workflows de pull request no deberían acceder a credenciales cercanas a producción cuando ejecutan código influenciado por la pull request. Pueden etiquetar, comentar, lintar, testear o construir sin autoridad sensible. Cualquier workflow que combine input de contribución externa con secretos necesita revisión de diseño.
¿Es aceptable el scope de secretos a nivel repositorio?
El scope a nivel repositorio es aceptable solo para credenciales de bajo impacto o repositorios chicos con pocos workflows y ownership claro. Cuando un repositorio tiene workflows separados para build, publish, deploy, migración y release, el scope de repositorio queda demasiado grueso. Scope por environment, workflow o job debería reemplazarlo cuando la plataforma lo permita.
¿OIDC reemplaza la rotación de secretos?
OIDC reduce la cantidad de secretos de larga vida que requieren rotación, pero no elimina el trabajo de rotar. Todavía pueden existir credenciales estáticas para registries, bases de datos, webhooks, herramientas de firma y sistemas legacy. Las trust policies de OIDC también necesitan revisión después de comportamiento sospechoso en workflows.
¿Qué permiso debería removerse primero?
El primer permiso a remover es la autoridad amplia de escritura en jobs que no publican, despliegan ni mutan estado. Conviene empezar por el GITHUB_TOKEN por defecto, tokens de package registry, roles cloud de deploy e id-token: write. Un job de build read-only es más fácil de confiar y de investigar.
La visión opuesta sostiene que permisos estrictos frenan delivery
La visión opuesta sostiene que los permisos estrictos de CI frenan delivery porque los equipos necesitan automatización para moverse rápido. Nadie quiere que cada deploy quede bloqueado por approvals, que cada workflow se divida en cinco archivos o que cada pedido de secreto se convierta en ticket. Esa preocupación es válida. Un modelo de seguridad que hace doloroso el camino normal termina siendo evitado.
La respuesta no es volver rígido a CI. La respuesta es hacer explícito el privilegio. La mayoría de los jobs debería ser rápida porque casi no tiene autoridad. Los pocos jobs que pueden publicar, desplegar, migrar o cambiar estado cloud merecen gates más estrechos porque su modo de falla no es un build rojo. Su modo de falla es un release comprometido, datos filtrados o un incidente productivo creado por la automatización que debía prevenirlo.
Lo que importa recordar
- Los secretos en CI/CD son secretos de producción cuando pueden publicar, desplegar, firmar o leer sistemas sensibles.
- Cada trigger de workflow define un límite de confianza, no solo un evento de automatización.
- Los secretos deberían acotarse a jobs, environments y workflows en vez de repositorios completos.
- OIDC elimina claves cloud de larga vida solo cuando las trust policies son estrechas y auditables.
id-token: writepertenece únicamente a jobs que necesitan identidad externa.- La respuesta a incidentes empieza con un inventario de credenciales mapeado a workflows y owners.
- Un pipeline seguro no es lento por defecto; es explícito sobre qué jobs cargan peligro.
Conclusión
El pipeline ya forma parte de la superficie de ataque productiva. Construye artefactos, prueba identidad, publica releases, despliega infraestructura y muchas veces carga las credenciales que recuperan o destruyen el sistema. Proteger solo la aplicación y dejar CI/CD amplio e implícito equivale a cerrar la puerta principal y dejar abierta la consola de releases.
La seguridad de CI/CD mejora cuando los equipos dejan de preguntar si un workflow es cómodo y empiezan a preguntar qué autoridad tiene. El objetivo práctico no es YAML perfecto. Es automatización acotada: código no confiable sin secretos, publish sin autoridad ajena, deploy detrás de gates explícitos e inventario que permita rotar antes de entrar en pánico.


