Introducción
La mayoría de los pipelines CI/CD comienzan con un objetivo simple: llevar el código desde la máquina de un desarrollador a producción lo más rápido posible. En el camino, alguien crea una cuenta de servicio, le otorga permisos amplios, almacena las credenciales como un secreto del pipeline y sigue adelante. Funciona. Los builds pasan, los despliegues tienen éxito y nadie vuelve a pensar en ello — hasta que un atacante compromete ese pipeline y descubre que tiene las llaves de todo el reino.
El problema no es que los equipos sean descuidados. El problema es que las herramientas CI/CD hacen que sea fácil otorgar privilegios excesivos y difícil aplicar controles granulares. La ruta predeterminada en la mayoría de las plataformas es una identidad única con acceso amplio ejecutando cada etapa del pipeline. Principios de seguridad como la separación de responsabilidades (SoD) y el mínimo privilegio parecen una carga burocrática cuando estás intentando entregar funcionalidades.
No lo son. Son controles de ingeniería que limitan el radio de impacto cuando — no si — algo sale mal. Esta guía explica cómo aplicar ambos principios a los pipelines CI/CD de una manera que fortalezca su postura de seguridad sin destruir la velocidad de entrega.
Por Qué los Pipelines Acumulan Privilegios
Antes de profundizar en las soluciones, vale la pena entender cómo los pipelines terminan con permisos excesivos en primer lugar. El patrón es notablemente consistente entre organizaciones.
El problema de la cuenta de servicio única
Comienza con una única cuenta de servicio — a menudo nombrada algo como ci-deployer o pipeline-bot — que se crea durante la configuración inicial del CI/CD. Esta cuenta necesita obtener código, así que recibe acceso de lectura al repositorio. Luego necesita subir imágenes Docker, así que recibe acceso de escritura al registro. Luego necesita desplegar en staging, luego en producción, luego gestionar infraestructura. En semanas, esta única identidad puede construir, probar, desplegar y acceder a datos de producción. Se ha convertido en una llave maestra.
God tokens
Estrechamente relacionado está el «god token» — un token de acceso personal o clave API con acceso de lectura/escritura a todo. Estos tokens a menudo son creados por un administrador durante la configuración, almacenados como una variable CI/CD y nunca rotados. Típicamente sobreviven a la persona que los creó, y nadie recuerda exactamente qué permisos tienen.
Conveniencia sobre seguridad
Las organizaciones frecuentemente usan un único pool de runners para todos los entornos. La misma máquina que ejecuta pruebas unitarias en pull requests no confiables de forks también tiene acceso de red a la infraestructura de producción. El razonamiento es directo: mantener pools de runners separados es operacionalmente costoso. Pero esta conveniencia significa que un pull request malicioso podría potencialmente acceder a credenciales de producción.
Falta de límites de identidad entre etapas
La mayoría de los pipelines ejecutan todas las etapas bajo la misma identidad. La etapa de build, la etapa de test, la etapa de deploy — todas comparten la misma cuenta de servicio, los mismos secretos y el mismo acceso de red. No hay límite entre «compilar código» y «subir a producción». Desde una perspectiva de seguridad, estos son niveles de confianza fundamentalmente diferentes que nunca deberían compartir una identidad.
Separación de Responsabilidades en CI/CD
La separación de responsabilidades es un principio de diseño de controles que asegura que ninguna entidad única tenga suficiente acceso para completar un proceso crítico por sí sola. En CI/CD, esto significa dividir deliberadamente las operaciones del pipeline entre diferentes identidades, aprobaciones y límites de confianza.
Principios fundamentales para SoD en pipelines
- Ninguna identidad única debería construir Y desplegar en producción. La identidad que compila código y produce artefactos no debería ser la misma identidad que sube esos artefactos a la infraestructura de producción.
- Los autores de código no deberían aprobar sus propios despliegues. La persona que escribe código no debería ser la única aprobadora de que ese código llegue a producción. Como mínimo, debe estar involucrado un segundo humano.
- Las definiciones del pipeline deberían estar protegidas del código que procesan. Los archivos de workflow que definen cómo se construye y despliega el código no deberían ser modificables por el mismo proceso que ejecuta el código de la aplicación.
- Los artefactos de build deberían ser inmutables una vez producidos. Una vez que una etapa de build produce un artefacto, ninguna etapa posterior debería poder modificarlo. El artefacto que fue probado es el artefacto que se despliega.
Mapeando SoD a las etapas del pipeline
Un pipeline bien diseñado mapea la separación de responsabilidades a etapas discretas, cada una con su propia identidad y permisos:
- Build — Compila código, resuelve dependencias, produce artefactos. Requiere acceso de lectura al código fuente y registros de dependencias. Sin acceso a objetivos de despliegue.
- Test — Ejecuta pruebas unitarias, pruebas de integración, escaneos de seguridad. Requiere acceso de lectura a artefactos e infraestructura de pruebas. Sin acceso a secretos de producción.
- Sign — Firma criptográficamente los artefactos que pasan todas las verificaciones. Requiere acceso a las claves de firma pero nada más. Esta etapa actúa como una puerta de control.
- Stage — Despliega en un entorno de staging para validación final. Requiere acceso de escritura solo a staging. Sin credenciales de producción disponibles.
- Deploy — Promueve el artefacto firmado y probado a producción. Requiere acceso de escritura a producción, controlado por aprobación manual. Identidad diferente a la de build.
Cada límite de etapa es un límite de confianza. Las credenciales no fluyen entre etapas a menos que se otorguen explícitamente.
Patrones de Mínimo Privilegio
El mínimo privilegio significa otorgar solo los permisos mínimos requeridos para una tarea específica, durante el menor tiempo necesario. En CI/CD, esto se traduce en patrones concretos que varían según la plataforma.
GitHub Actions: permisos por job
GitHub Actions proporciona un bloque permissions que controla los alcances del GITHUB_TOKEN generado automáticamente. Por defecto, este token tiene acceso amplio de lectura/escritura. Siempre deberías restringirlo.
Establece valores predeterminados restrictivos a nivel de workflow y otorga permisos adicionales solo a los jobs específicos que los necesiten:
# .github/workflows/deploy.yml
name: Build and Deploy
# Restrict default permissions for ALL jobs in this workflow
permissions:
contents: read
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
# This job inherits the workflow-level permissions: contents read only
steps:
- uses: actions/checkout@v4
- run: make build
- uses: actions/upload-artifact@v4
with:
name: app-binary
path: dist/
security-scan:
needs: build
runs-on: ubuntu-latest
permissions:
security-events: write # Only this job can write security findings
contents: read
steps:
- uses: actions/checkout@v4
- uses: github/codeql-action/analyze@v3
deploy-staging:
needs: [build, security-scan]
runs-on: ubuntu-latest
environment: staging # Scoped to staging secrets only
permissions:
contents: read
id-token: write # OIDC token for cloud auth — no static credentials
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/staging-deployer
aws-region: us-east-1
- run: ./scripts/deploy.sh staging
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment: production # Requires manual approval + different secrets
permissions:
contents: read
id-token: write
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/production-deployer
aws-region: us-east-1
- run: ./scripts/deploy.sh production
Puntos clave en esta configuración: cada job declara solo los permisos que necesita, los jobs de despliegue usan OIDC para credenciales de corta duración en lugar de secretos estáticos, y staging y producción usan roles IAM diferentes con distintos niveles de acceso.
GitLab CI: variables protegidas y runners
GitLab CI ofrece un conjunto diferente pero igualmente poderoso de controles para implementar el mínimo privilegio:
# .gitlab-ci.yml
stages:
- build
- test
- deploy-staging
- deploy-production
build:
stage: build
tags:
- shared-runners # Non-privileged runners for build
script:
- make build
artifacts:
paths:
- dist/
test:
stage: test
tags:
- shared-runners
script:
- make test
dependencies:
- build
deploy-staging:
stage: deploy-staging
tags:
- deploy-runners # Dedicated runners with network access to staging
environment:
name: staging
script:
- ./scripts/deploy.sh staging
# CI_JOB_TOKEN is automatically scoped to this project
# Staging secrets are only available on protected branches
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
deploy-production:
stage: deploy-production
tags:
- production-runners # Isolated runners with production network access
environment:
name: production
script:
- ./scripts/deploy.sh production
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: manual # Requires manual approval
allow_failure: false # Block the pipeline if not approved
En GitLab, use variables protegidas para que las credenciales de producción solo estén disponibles para ramas y tags protegidos. Use runners protegidos para asegurar que los jobs de despliegue a producción solo se ejecuten en infraestructura aislada y reforzada. Marque los secretos de producción como enmascarados para prevenir la exposición accidental en los logs.
Cuentas de servicio por etapa con roles IAM delimitados
Más allá de la plataforma CI en sí, aplique el mínimo privilegio a nivel del proveedor de nube. Cada etapa del pipeline debería autenticarse como una cuenta de servicio diferente con permisos estrictamente delimitados:
- Etapa de build: Acceso de solo lectura a repositorios de código fuente y registros de dependencias. Acceso de escritura al almacén de artefactos (por ejemplo, bucket S3, registro de contenedores) pero nada más.
- Etapa de test: Acceso de solo lectura a artefactos. Permiso para crear y destruir infraestructura de pruebas efímera, pero sin acceso a staging o producción.
- Etapa de deploy: Acceso de escritura al objetivo de despliegue específico (por ejemplo, un namespace de Kubernetes específico, un servicio ECS específico). Sin acceso a otros entornos o servicios.
Credenciales de corta duración vía OIDC
Las credenciales estáticas almacenadas como secretos CI/CD son un riesgo. No expiran, son difíciles de rotar y son objetivos atractivos para los atacantes. El enfoque moderno es usar federación OIDC para que su plataforma CI/CD intercambie un token de corta duración firmado por la plataforma por credenciales temporales en la nube:
- GitHub Actions puede asumir roles IAM de AWS, cuentas de servicio de GCP o identidades administradas de Azure usando el permiso
id-token: write— sin necesidad de secretos almacenados. - GitLab CI soporta OIDC nativamente a través de su
CI_JOB_JWTo la palabra claveid_tokens, permitiendo el mismo patrón. - Estas credenciales típicamente duran de 15 a 60 minutos y están delimitadas al job específico que las solicitó.
Si todavía está usando claves de acceso estáticas en los secretos de su pipeline, migrar a OIDC debería ser una iniciativa de alta prioridad.
Protección de las Definiciones del Pipeline
Pipeline-as-code es poderoso, pero introduce un problema de confianza sutil: si un atacante puede modificar la definición del pipeline, controla cómo se construye, prueba y despliega el código. Proteger los archivos de configuración del pipeline es tan importante como proteger el código de la aplicación en sí.
CODEOWNERS para archivos de workflow
Use un archivo CODEOWNERS para requerir que equipos específicos revisen los cambios en la configuración CI/CD:
# .github/CODEOWNERS
/.github/workflows/ @your-org/platform-security
/.gitlab-ci.yml @your-org/platform-security
/Jenkinsfile @your-org/platform-security
/terraform/ @your-org/infrastructure
Esto asegura que nadie pueda modificar las definiciones del pipeline sin la aprobación del equipo responsable de la seguridad CI/CD. Combine esto con reglas de protección de ramas que requieran la aprobación de CODEOWNERS.
Protección de ramas en directorios de configuración del pipeline
Aplique reglas de protección de ramas que prevengan pushes directos a ramas que contienen definiciones de pipeline. Como mínimo:
- Requiera revisiones de pull request antes de fusionar cambios en archivos de workflow.
- Requiera que las verificaciones de estado pasen (incluyendo escaneos de seguridad de los propios cambios del workflow).
- Deshabilite force pushes a ramas protegidas.
- Requiera commits firmados para cambios en la configuración del pipeline.
Plantillas de pipeline inmutables
Tanto GitHub Actions como GitLab CI soportan referenciar definiciones de pipeline desde repositorios externos gestionados centralmente:
GitHub Actions reusable workflows:
# In your repository's workflow, reference a centrally managed template
jobs:
deploy:
uses: your-org/shared-workflows/.github/workflows/secure-deploy.yml@v2.1.0
with:
environment: production
artifact-name: app-binary
secrets: inherit
GitLab CI includes desde repositorios protegidos:
# .gitlab-ci.yml
include:
- project: 'platform-team/ci-templates'
ref: 'v3.0.0'
file: '/templates/secure-deploy.yml'
# Local jobs can use templates but cannot override protected stages
deploy-production:
extends: .secure-deploy-template
variables:
TARGET_ENV: production
Al fijar versiones específicas (tags o SHA de commits) y restringir quién puede modificar el repositorio de plantillas, se asegura de que los equipos individuales no puedan alterar los controles de seguridad integrados en el proceso de despliegue.
Previniendo pipelines que se auto-modifican
Un pipeline nunca debería poder modificar su propia definición. Esté atento a estos patrones:
- Pasos del pipeline que escriben en
.github/workflows/o.gitlab-ci.ymly hacen commit de los cambios. - Generación dinámica de pipelines que obtiene configuración de fuentes no confiables.
- Variables del pipeline que pueden anular configuraciones críticas de seguridad como qué runner usar o en qué entorno desplegar.
Si necesita comportamiento dinámico en el pipeline, use plantillas parametrizadas con un conjunto fijo de entradas permitidas en lugar de permitir la modificación arbitraria de la lógica del pipeline.
Controles de Despliegue
Los controles de despliegue son las puertas entre las etapas del pipeline que imponen supervisión humana y cumplimiento de políticas. Son donde la separación de responsabilidades se vuelve tangible.
Revisores requeridos y aprobaciones manuales
Los despliegues a producción deberían requerir aprobación explícita de alguien diferente al autor del código. Ambas plataformas principales soportan esto nativamente:
- GitHub Environments permiten configurar revisores requeridos. Cuando un job de workflow referencia un entorno con reglas de protección, el pipeline se pausa hasta que un revisor autorizado aprueba.
- GitLab Protected Environments restringen qué usuarios o grupos pueden activar despliegues a entornos específicos. Combinado con
when: manual, esto crea una puerta de aprobación.
GitHub Environments con reglas de protección
Los GitHub Environments son un mecanismo poderoso para controles de despliegue. Configúrelos con:
- Revisores requeridos: Especifique individuos o equipos que deben aprobar antes de que el job se ejecute. Use al menos dos revisores para producción.
- Temporizador de espera: Agregue un retraso entre la aprobación y la ejecución, dando tiempo para detectar errores o coordinarse con ventanas de cambio.
- Ramas de despliegue: Restrinja qué ramas pueden desplegar al entorno. Producción solo debería aceptar despliegues desde
mainorelease/*. - Secretos de entorno: Almacene credenciales a nivel de entorno, no a nivel de repositorio. Esto asegura que los secretos de staging no estén disponibles para los jobs de producción y viceversa.
Congelamiento de despliegues y ventanas de cambio
Implemente congelamientos de despliegue durante períodos críticos del negocio (por ejemplo, Black Friday, fin de trimestre) mediante:
- Uso de reglas de protección de entornos programadas que bloqueen automáticamente los despliegues durante ventanas definidas.
- Requerir aprobaciones adicionales durante períodos de congelamiento en lugar de bloquear completamente — las correcciones de emergencia deberían seguir siendo posibles con supervisión elevada.
- Registrar todas las excepciones al congelamiento con fines de auditoría.
Canary y despliegue progresivo como mecanismo de control
Las estrategias de despliegue progresivo no son solo una preocupación de disponibilidad — son un control de seguridad. Si un artefacto comprometido pasa todas las demás verificaciones, un despliegue canary limita el radio de impacto:
- Despliegue al 1-5% del tráfico primero con verificaciones de salud automatizadas.
- Requiera una segunda aprobación manual para proceder más allá de la etapa canary.
- Automatice el rollback si las tasas de error o la latencia superan los umbrales.
- Trate la etapa canary como un entorno separado con sus propios requisitos de aprobación.
Auditoría y Responsabilidad
La separación de responsabilidades y el mínimo privilegio solo son efectivos si puede verificar que se están siguiendo. La auditoría y la responsabilidad cierran el ciclo proporcionando evidencia de quién hizo qué, cuándo y por qué.
Logs de ejecución del pipeline como registros de auditoría
Las plataformas CI/CD mantienen logs detallados de cada ejecución del pipeline. Trate estos logs como datos de auditoría relevantes para la seguridad:
- Retenga los logs del pipeline por al menos 90 días (más si su marco de cumplimiento lo requiere).
- Exporte los logs a un almacén de logs centralizado y resistente a manipulaciones (por ejemplo, SIEM, CloudWatch Logs, o un bucket S3 dedicado con object lock).
- Asegúrese de que los logs capturen qué identidad activó el pipeline, qué secretos fueron accedidos (pero no sus valores) y qué aprobaciones fueron otorgadas.
Vinculando despliegues a commits y aprobadores
Cada despliegue a producción debería ser rastreable hasta:
- El SHA del commit Git específico que fue desplegado.
- El pull request que introdujo el cambio, incluyendo todos los revisores y aprobadores.
- El ID de ejecución del pipeline y la persona que aprobó la puerta de despliegue.
- El digest del artefacto (SHA de imagen de contenedor, hash del binario) que fue desplegado.
Esta cadena de trazabilidad debería ser automatizada. Si alguien pregunta «qué está corriendo en producción y quién lo aprobó», la respuesta debería estar disponible en segundos, no en horas.
Logs de auditoría en la nube para cambios iniciados por el pipeline
Las acciones del pipeline dejan rastros en los logs de auditoría del proveedor de nube. Correlacione estos con sus logs CI/CD:
- AWS CloudTrail registra cada llamada API realizada por el rol IAM asumido por su pipeline. Haga referencia cruzada del ID de ejecución del pipeline con el
userIdentity.sessionContextde CloudTrail para vincular los cambios en la nube a ejecuciones específicas del pipeline. - GCP Cloud Audit Logs proporcionan trazabilidad similar para las acciones de cuentas de servicio.
- Azure Activity Logs capturan las modificaciones de recursos realizadas por los service principals del pipeline.
Alertas sobre escalamiento de privilegios o evasión de políticas
Configure alertas para eventos que indiquen que sus controles están siendo eludidos:
- Un pipeline accediendo a secretos que no debería necesitar (por ejemplo, la etapa de build accediendo a credenciales de base de datos de producción).
- Cambios en las reglas de protección de ramas o archivos CODEOWNERS.
- Modificaciones a las políticas IAM adjuntas a las cuentas de servicio del pipeline.
- Ejecuciones de pipeline activadas desde ramas no protegidas desplegando a entornos protegidos.
- Aprobaciones de despliegue otorgadas por la misma persona que escribió el código.
Alimente estas alertas en su flujo de trabajo de operaciones de seguridad y trátelas con la misma urgencia que los incidentes de producción.
Anti-Patrones Comunes
Saber qué evitar es tan importante como saber qué implementar. Estos son los anti-patrones que vemos con más frecuencia en las evaluaciones de seguridad CI/CD.
Token de administrador único para todas las operaciones CI/CD
Un token de acceso personal con alcance de administrador, creado por un ingeniero senior, almacenado como un secreto del repositorio y usado por cada job en cada pipeline. Cuando ese ingeniero deja la organización, nadie revoca el token porque nadie sabe qué se romperá. Este es el anti-patrón más común y más peligroso.
Solución: Reemplace con cuentas de servicio por etapa usando federación OIDC. Sin tokens estáticos, sin identidades compartidas.
Deshabilitar la protección de ramas «temporalmente»
Un despliegue está bloqueado porque una verificación requerida está fallando. Alguien deshabilita la protección de ramas para hacer push directamente a main, con la intención de rehabilitarla después. Se olvidan, o la rehabilitan pero se pierden una configuración. Mientras tanto, la ventana sin protección permitió un push directo que evitó la revisión de código.
Solución: Nunca deshabilite la protección de ramas. Si una verificación requerida está fallando, corrija la verificación o use un proceso de emergencia que requiera múltiples aprobaciones y cree un registro de auditoría.
Runners compartidos entre producción y cargas de trabajo de PR
Los pipelines de pull request de forks se ejecutan en la misma infraestructura que tiene acceso de red a sistemas de producción. Un PR malicioso puede exfiltrar secretos, acceder a servicios internos o pivotar a la infraestructura de producción.
Solución: Use pools de runners separados. Las cargas de trabajo no confiables (PRs, especialmente de forks) se ejecutan en runners efímeros y aislados sin acceso a entornos sensibles. Los runners de despliegue a producción están restringidos solo a ramas protegidas.
Acceso manual por SSH cuando los pipelines fallan
Cuando un pipeline de despliegue falla, un ingeniero se conecta por SSH directamente a un servidor de producción y despliega manualmente. Esto evita cada control en el pipeline: revisión de código, pruebas automatizadas, firma de artefactos, aprobación de despliegue y registro de auditoría.
Solución: Invierta en la fiabilidad del pipeline para que la intervención manual sea raramente necesaria. Cuando sea necesaria, use un procedimiento de emergencia (break-glass) que requiera múltiples aprobaciones, cree un registro de auditoría y active una revisión post-incidente.
Conclusión
La separación de responsabilidades y el mínimo privilegio no son una carga burocrática impuesta por un equipo de cumplimiento. Son controles de ingeniería que reducen directamente el radio de impacto de los incidentes de seguridad en su pipeline de entrega de software.
Un paso de build comprometido con mínimo privilegio puede producir un artefacto malicioso — pero no puede desplegar ese artefacto en producción. Una credencial de despliegue comprometida delimitada a staging no puede alcanzar producción. Un pull request malicioso procesado por un runner aislado no puede exfiltrar secretos de producción. Cada control limita lo que un atacante puede lograr en cada etapa.
Comience auditando los permisos actuales de su pipeline. Identifique cada cuenta de servicio, cada credencial almacenada y cada secreto al que su pipeline puede acceder. Mapee cada uno a la etapa y tarea específica que lo necesita. Luego comience a reducir el alcance: reemplace credenciales estáticas con OIDC, divida las cuentas de servicio únicas en identidades por etapa, agregue puertas de aprobación para despliegues a producción y proteja las definiciones de su pipeline con CODEOWNERS y protección de ramas.
No tiene que hacerlo todo de una vez. Cada mejora incremental reduce el riesgo. Pero comience — porque su pipeline CI/CD es probablemente el sistema más privilegiado y menos examinado en toda su infraestructura.