Modelos de Ejecución CI/CD y Supuestos de Confianza: Guía de Seguridad

Introducción

Los pipelines CI/CD se encuentran entre los componentes más privilegiados de cualquier organización de software moderna. Clonan código fuente, acceden a secrets, construyen artefactos y despliegan en producción — frecuentemente con mínima supervisión humana. Sin embargo, a pesar de este extraordinario nivel de acceso, los modelos de confianza que sustentan estos pipelines rara vez se hacen explícitos.

Cuando un pipeline se ejecuta, responde implícitamente una cadena de preguntas de seguridad: ¿Quién activó esta ejecución? ¿Qué código se está ejecutando? ¿Qué identidad asume el pipeline? ¿A qué recursos puede acceder? En la mayoría de las organizaciones, estas preguntas son respondidas por configuraciones predeterminadas en lugar de decisiones de seguridad deliberadas.

Esta guía mapea cómo funcionan los diferentes modelos de ejecución CI/CD, dónde se asume la confianza frente a dónde se verifica, y cómo fortalecer sus pipelines contra los patrones de ataque del mundo real que explotan estas brechas. Ya sea que utilice GitHub Actions, GitLab CI u otra plataforma, las dinámicas de confianza subyacentes son universales — y comprenderlas es esencial para proteger su cadena de suministro de software.

¿Qué es un Modelo de Ejecución CI/CD?

Un modelo de ejecución CI/CD define el ciclo de vida completo de cómo se activa el código del pipeline, dónde se ejecuta físicamente, qué identidad asume durante la ejecución y a qué recursos puede acceder. Es, en esencia, la arquitectura de seguridad de su capa de automatización.

Todo modelo de ejecución debe responder cuatro preguntas fundamentales:

  • Trigger: ¿Qué evento inicia el pipeline y quién o qué está autorizado para causar ese evento?
  • Entorno: ¿Dónde se ejecuta el código del pipeline — en qué infraestructura, con qué sistema operativo y con qué grado de aislamiento?
  • Identidad: ¿Qué credenciales, tokens o cuentas de servicio posee el pipeline en ejecución?
  • Acceso: ¿A qué sistemas downstream, secrets, registries y objetivos de despliegue puede llegar el pipeline?

La forma en que se responden estas preguntas varía drásticamente entre los entornos de ejecución:

Runners Alojados en SaaS

Plataformas como GitHub Actions (runners alojados por GitHub) y los runners compartidos de GitLab.com proporcionan máquinas virtuales efímeras gestionadas por el proveedor de CI/CD. Cada job normalmente obtiene una VM nueva que se destruye después de la ejecución. La plataforma gestiona los parches, el aislamiento y el ciclo de vida. La contrapartida es que usted confía en las garantías de aislamiento del proveedor — no puede inspeccionar ni controlar la infraestructura subyacente.

Self-Hosted Runners

Las organizaciones despliegan sus propios agentes runner en infraestructura que controlan — VMs, bare metal o pods de Kubernetes. Esto proporciona control total sobre el entorno de ejecución, pero traslada la responsabilidad del aislamiento, los parches y la gestión de credenciales enteramente al operador. Un self-hosted runner mal configurado es uno de los vectores más comunes para el movimiento lateral en ataques CI/CD.

Ejecución Containerizada

Muchos pipelines ejecutan jobs dentro de contenedores, ya sea en infraestructura self-hosted o en clústeres de Kubernetes gestionados. La ejecución basada en contenedores proporciona aislamiento a nivel de proceso y entornos reproducibles, pero los contenedores no son fronteras de seguridad de la misma manera que las VMs. El acceso compartido al kernel, los volúmenes montados y la exposición del Docker socket pueden socavar el modelo de aislamiento.

Ejecución Serverless y Bajo Demanda

Algunos sistemas CI/CD modernos (como AWS CodeBuild o ciertas configuraciones de Buildkite) levantan cómputo completamente bajo demanda para cada job. Estos modelos ofrecen fuertes garantías de aislamiento ya que cada ejecución obtiene una instancia de cómputo dedicada y de corta duración, pero introducen complejidad en torno al bootstrapping de credenciales y el control de acceso a la red.

Comprender qué modelo utiliza su organización — y las propiedades de seguridad que proporciona y las que no — es la base para razonar sobre la confianza en CI/CD.

Fronteras de Confianza en CI/CD

Una frontera de confianza existe donde el control pasa de una entidad o sistema a otro. En CI/CD, hay varias fronteras de confianza críticas, y las fallas en cualquiera de ellas pueden llevar a un compromiso total del pipeline.

Del Repositorio de Código Fuente al Trigger del Pipeline

La primera frontera de confianza está entre el repositorio de código y el mecanismo de trigger del pipeline. Cuando un desarrollador hace push de un commit o abre un pull request, la plataforma CI/CD decide si ejecutar un pipeline y cómo hacerlo. La pregunta crítica es: ¿quién puede activar la ejecución del pipeline y puede controlar qué código ejecuta el pipeline?

En muchas configuraciones, cualquier persona que pueda abrir un pull request — incluyendo contribuidores externos a repositorios públicos — puede activar la ejecución del pipeline. Si la definición del pipeline proviene de la rama del PR, el contribuidor efectivamente controla el código que se ejecuta en su entorno CI.

De la Definición del Pipeline al Entorno de Ejecución

La segunda frontera de confianza separa la definición del pipeline (el archivo YAML, el Jenkinsfile, el script de build) del entorno donde se ejecuta. Las preguntas clave incluyen: ¿Tiene el runner acceso a la red? ¿Puede el pipeline instalar software arbitrario? ¿Puede modificar el propio runner para futuros jobs?

En runners compartidos o persistentes, una definición de pipeline maliciosa podría instalar un backdoor que persiste a través de ejecuciones de jobs posteriores — afectando repositorios y equipos completamente diferentes.

Del Entorno de Ejecución a Secrets y Credenciales

Los pipelines necesitan credenciales para realizar trabajo útil: tokens de API, claves de proveedores cloud, contraseñas de registries, claves de firma. La frontera de confianza entre el entorno de ejecución y el almacén de secrets determina a qué puede acceder un pipeline comprometido. El acceso excesivamente amplio a secrets es una de las configuraciones erróneas más comunes y peligrosas en CI/CD.

De la Salida del Build al Objetivo de Despliegue

La frontera de confianza final está entre lo que el pipeline produce (una imagen de contenedor, un binario, un plan de Terraform) y el sistema donde se despliega esa salida. Si la identidad del pipeline que construye un artefacto es la misma que lo despliega en producción, no hay separación de funciones. Un solo paso de build comprometido puede llevar directamente a un compromiso de producción.

Mapeando las Zonas de Confianza

Conceptualmente, un pipeline CI/CD atraviesa cuatro zonas de confianza:

Zona 1: Control de Código Fuente (Estaciones de trabajo de desarrolladores, ramas, PRs)
   ↓ [Frontera de trigger]
Zona 2: Definición del Pipeline (YAML/config parseado por la plataforma CI)
   ↓ [Frontera de ejecución]
Zona 3: Entorno de Ejecución (Runner, contenedor, VM — con secrets)
   ↓ [Frontera de despliegue]
Zona 4: Objetivos de Despliegue (Producción, staging, registries, APIs cloud)

Cada flecha representa una frontera de confianza. Los controles de seguridad deben existir en cada transición: reglas de protección de ramas en la frontera de trigger, aislamiento del runner en la frontera de ejecución, credenciales con alcance limitado en la frontera de secrets, y aprobaciones de despliegue en la frontera de despliegue.

Modelo de Ejecución de GitHub Actions

GitHub Actions es una de las plataformas CI/CD más ampliamente adoptadas, y su modelo de ejecución tiene varias características de confianza únicas que vale la pena comprender en profundidad.

Runners Alojados por GitHub vs Self-Hosted

Los runners alojados por GitHub son VMs efímeras aprovisionadas por GitHub para cada job. Se ejecutan en infraestructura de Azure, se destruyen después de que cada job se completa y proporcionan un fuerte aislamiento entre jobs. Los self-hosted runners, por el contrario, son máquinas que usted registra con GitHub. Persisten entre jobs, pueden acumular estado y — críticamente — cualquier repositorio en la organización con acceso al runner puede ejecutar código en él.

Para los self-hosted runners, GitHub advierte explícitamente: no utilice self-hosted runners con repositorios públicos. Cualquier fork puede enviar un pull request que active un workflow, y ese workflow se ejecuta en su infraestructura con su acceso a la red.

Permisos y Alcance del GITHUB_TOKEN

Cada ejecución de workflow recibe un GITHUB_TOKEN automático con permisos limitados al repositorio. Por defecto, este token tiene permisos amplios de lectura/escritura sobre el contenido del repositorio, paquetes, issues y más. La clave permissions le permite restringir este token solo a lo necesario:

permissions:
  contents: read
  packages: write
  id-token: write   # Para federación OIDC

Establecer los permisos de nivel superior en read-all o incluso vacío ({}) y luego otorgar permisos específicos por job es un paso de hardening crítico. Sin esto, cualquier paso comprometido en cualquier job tiene acceso de escritura a su repositorio.

Workflows de Fork PRs: pull_request vs pull_request_target

Esta es una de las fronteras de confianza más peligrosas en GitHub Actions. El evento pull_request ejecuta la definición del workflow desde la rama head del PR — lo que significa que el contribuidor controla el código del workflow — pero, de manera crítica, no tiene acceso a los secrets del repositorio. El evento pull_request_target ejecuta el workflow desde la rama base (la rama main de su repositorio) pero tiene acceso a los secrets.

El peligro surge cuando los workflows de pull_request_target hacen checkout del código de la rama head del PR:

# PELIGROSO: pull_request_target con checkout explícito del código del PR
on: pull_request_target

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      # Esto ahora ejecuta CÓDIGO NO CONFIABLE con acceso a SECRETS
      - run: npm install && npm test

Este patrón le da a un atacante la capacidad de ejecutar código arbitrario con acceso a los secrets de su repositorio. Es el ejemplo canónico de Poisoned Pipeline Execution en GitHub Actions.

Reusable Workflows y Delegación de Confianza

Los reusable workflows le permiten centralizar la lógica del pipeline en un repositorio compartido y llamarlo desde otros repositorios. Cuando se invoca un reusable workflow, se ejecuta con los permisos y secrets del workflow que lo llama. Esto crea una cadena de delegación de confianza: usted confía en que el código del reusable workflow (en otro repositorio) manejará sus secrets de manera responsable.

Fije los reusable workflows a un SHA de commit específico, no a una rama o tag:

jobs:
  deploy:
    uses: my-org/shared-workflows/.github/workflows/deploy.yml@a1b2c3d4e5f6
    secrets: inherit

Reglas de Protección de Environments

Los Environments de GitHub proporcionan una frontera de confianza crítica para los workflows de despliegue. Puede configurar revisores requeridos, temporizadores de espera y restricciones de rama en los environments. Cuando un job hace referencia a un environment, debe satisfacer las reglas de protección antes de que los secrets asociados con ese environment estén disponibles:

jobs:
  deploy-production:
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://example.com
    steps:
      - name: Deploy
        run: ./deploy.sh
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.PROD_AWS_KEY }}

Esto asegura que, incluso si se activa un workflow, las credenciales de producción no se exponen sin aprobación humana.

Modelo de Ejecución de GitLab CI

GitLab CI tiene un modelo de ejecución diferente con sus propias características de confianza, particularmente en torno al alcance de los runners y la protección de variables.

Shared Runners vs Group Runners vs Project Runners

GitLab ofrece tres niveles de alcance para runners. Los shared runners (en GitLab.com, son gestionados por GitLab) están disponibles para todos los proyectos. Los group runners están disponibles para todos los proyectos dentro de un grupo de GitLab. Los project runners están dedicados a un solo proyecto. El alcance determina el radio de explosión de un runner comprometido — un compromiso de shared runner afecta a todos los proyectos, mientras que un compromiso de project runner se contiene a un solo proyecto.

Para cargas de trabajo sensibles, siempre prefiera runners específicos del proyecto con el etiquetado apropiado:

deploy-production:
  stage: deploy
  tags:
    - production-runner
    - isolated
  script:
    - ./deploy.sh
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

Protected Branches y Protected Variables

El mecanismo de protected variables de GitLab es un control de confianza clave. Las variables marcadas como «protected» solo se exponen a pipelines que se ejecutan en ramas protegidas o tags protegidos. Esto significa que un pipeline activado por un merge request desde una rama de feature — o peor, desde un fork — no tendrá acceso a las variables protegidas.

Este es el mecanismo principal de GitLab para prevenir la exposición de secrets a código no confiable:

# En .gitlab-ci.yml, las variables protegidas solo están disponibles en ramas protegidas
deploy:
  stage: deploy
  script:
    - echo "Deploying with $PRODUCTION_API_KEY"
  rules:
    - if: $CI_COMMIT_BRANCH == "main"  # main es una rama protegida
  environment:
    name: production

Alcance y Limitaciones del CI_JOB_TOKEN

Cada job de GitLab CI recibe un CI_JOB_TOKEN, un token generado automáticamente con alcance al proyecto. Por defecto, este token puede acceder a recursos de otros proyectos, lo que crea una relación de confianza implícita. GitLab le permite restringir el acceso del CI_JOB_TOKEN configurando una lista de permitidos de proyectos a los que se puede acceder — un paso de hardening crítico que limita el movimiento lateral si un pipeline es comprometido.

En la configuración de su proyecto bajo CI/CD → Token Access, restrinja el alcance del token solo a los proyectos con los que su pipeline genuinamente necesita interactuar.

Merge Request Pipelines y Fronteras de Confianza

GitLab distingue entre pipelines de rama y pipelines de merge request. Los pipelines de merge request se ejecutan en el contexto del merge request y tienen acceso a un conjunto limitado de variables predefinidas. Para pipelines activados por merge requests desde forks, GitLab no expone variables protegidas ni secrets a nivel de proyecto — esta es una frontera de confianza intencional.

Sin embargo, los pipelines que se ejecutan sobre el resultado fusionado (el merge_request_event con pipelines de resultados fusionados habilitados) aún ejecutan el código del fork. Si su definición de pipeline permite la ejecución de código arbitrario y el job tiene acceso a secrets a través de variables no protegidas, esto aún puede ser explotado.

Fallas Comunes en los Supuestos de Confianza

Comprender los modelos de ejecución es importante, pero el verdadero valor viene de reconocer los patrones que llevan al compromiso. Estas son las fallas en los supuestos de confianza que aparecen repetidamente en las brechas CI/CD del mundo real.

Poisoned Pipeline Execution (PPE)

Poisoned Pipeline Execution ocurre cuando un atacante puede modificar la definición del pipeline que se ejecuta en un contexto privilegiado. Esta es la clase más prevalente de vulnerabilidad CI/CD. Ocurre cuando:

  • Un pull request activa un workflow que usa la versión del archivo del pipeline del PR
  • Ese workflow tiene acceso a secrets o credenciales de despliegue
  • No hay una puerta de revisión o aprobación entre el PR y la ejecución del pipeline

El atacante modifica el YAML del pipeline (o un script que este llama) para exfiltrar secrets, inyectar backdoors en los artefactos de build, o pivotar hacia sistemas internos.

Asumir Aislamiento del Runner en Infraestructura Compartida

Cuando múltiples equipos o proyectos comparten runners — especialmente self-hosted runners — frecuentemente existe una suposición implícita de aislamiento que en realidad no existe. Un job ejecutándose en un self-hosted runner compartido puede ser capaz de:

  • Leer archivos dejados por jobs anteriores (credenciales en caché, artefactos de build)
  • Acceder al Docker socket e inspeccionar o modificar otros contenedores
  • Alcanzar recursos de red interna disponibles para el host del runner
  • Instalar backdoors persistentes en el runner para futuros jobs

Cuentas de Servicio con Permisos Excesivos

Un patrón preocupantemente común es otorgar a la cuenta de servicio CI/CD acceso administrativo amplio — «solo para que las cosas funcionen». Un rol IAM de AWS con AdministratorAccess, una cuenta de servicio de Kubernetes con cluster-admin, o una cuenta de SQL en la nube con privilegios de DBA. Cuando cualquier paso del pipeline se ve comprometido, el atacante hereda todos estos permisos.

Confianza Implícita en Actions y Templates de Terceros

Usar GitHub Actions de la comunidad o templates de GitLab CI significa ejecutar código de otra persona en su pipeline con sus secrets. Cuando referencia uses: some-org/some-action@v2, está confiando en que:

  • El código de la action no es malicioso
  • Los mantenedores de la action no han sido comprometidos
  • El tag v2 no ha sido movido para apuntar a código diferente
  • Las dependencias de la action son confiables

Las referencias de tags son mutables. Un atacante que comprometa el repositorio de una action puede mover el tag v2 a un commit malicioso, y cada pipeline que referencia ese tag ejecutará el nuevo código en su próxima ejecución.

Confusión de Identidad entre Build-Time y Deploy-Time

Muchos pipelines usan una sola identidad (cuenta de servicio, rol IAM o token) tanto para construir como para desplegar. Esta confusión significa que un compromiso durante la fase de build — que maneja código no confiable — da acceso directo a los objetivos de despliegue. La identidad de build solo debería poder producir artefactos. Una identidad de deploy separada y más restringida debería usarse para desplegar esos artefactos en producción.

Fortalecimiento de los Supuestos de Confianza

Con el modelo de amenazas claro, aquí están las mitigaciones concretas que alinean los controles con las fronteras de confianza.

Condiciones de Trigger Explícitas y Filtros de Rama

Nunca permita triggers de pipeline sin restricciones. Limite qué eventos pueden activar qué workflows, y asegúrese de que los pipelines privilegiados solo se ejecuten en ramas de confianza:

# GitHub Actions: restringir despliegue solo a la rama main
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
    # Solo activar en PRs dirigidos a main; el código del PR se ejecuta sin secrets

jobs:
  deploy:
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: ./deploy.sh
# GitLab CI: usar rules para restringir jobs sensibles
deploy-production:
  stage: deploy
  script:
    - ./deploy.sh
  rules:
    - if: $CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE != "merge_request_event"
      when: manual
      allow_failure: false
  environment:
    name: production

Permisos Mínimos de Token

Aplique el principio de mínimo privilegio a cada token en su pipeline. En GitHub Actions, establezca permisos predeterminados restrictivos y otorgue permisos específicos por job:

# Establecer valores predeterminados restrictivos a nivel de workflow
permissions: read-all

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build

  deploy:
    needs: build
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write  # Solo para OIDC, sin escritura al repositorio
    environment: production
    steps:
      - run: ./deploy.sh

En GitLab, restrinja el alcance del CI_JOB_TOKEN en la configuración del proyecto y use variables protegidas exclusivamente para credenciales sensibles.

Runners Efímeros y Aislados

Siempre que sea posible, use runners efímeros que se crean nuevos para cada job y se destruyen inmediatamente después. Esto elimina los ataques basados en persistencia y la fuga de datos entre jobs. Para entornos self-hosted, herramientas como el Actions Runner Controller (ARC) de GitHub para Kubernetes o el runner con autoescalado de GitLab en AWS/GCP pueden aprovisionar pods o VMs de runner efímeros para cada job.

Propiedades clave de una configuración de runner fortalecida:

  • Sin almacenamiento persistente entre jobs
  • Sin Docker socket compartido
  • Segmentación de red limitando el acceso solo a los endpoints requeridos
  • Sin capacidad para que el job modifique la configuración del propio runner

Fijar Actions e Imágenes por SHA

Las referencias mutables (nombres de ramas, tags como v2) pueden ser cambiadas por mantenedores upstream — o atacantes. Fijar a un SHA de commit específico asegura que el código exacto que revisó es lo que se ejecuta en su pipeline:

# En lugar de esto (tag mutable):
- uses: actions/checkout@v4

# Use esto (SHA inmutable):
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1

El mismo principio aplica a las imágenes de contenedores. Use digests de imágenes en lugar de tags:

# En lugar de:
image: node:20-alpine

# Use:
image: node@sha256:a1b2c3d4e5f6...  # fijar a un digest específico

Herramientas como Dependabot y Renovate pueden crear PRs automáticamente para actualizar los SHAs fijados cuando se lanzan nuevas versiones, así obtiene tanto seguridad como mantenibilidad.

Separación de Identidades de Build y Deploy

Implemente identidades distintas para las fases de build y deploy. La identidad de build debería tener:

  • Acceso de lectura al código fuente
  • Acceso de escritura al almacenamiento de artefactos (container registry, bucket S3)
  • Sin acceso a entornos de producción

La identidad de deploy debería tener:

  • Acceso de lectura al almacenamiento de artefactos
  • Acceso de escritura al objetivo de despliegue específico
  • Sin acceso al código fuente ni la capacidad de activar builds

Use federación OIDC donde sea posible para eliminar por completo las credenciales de larga duración. Tanto GitHub Actions como GitLab CI soportan tokens OIDC que pueden intercambiarse por credenciales de corta duración del proveedor cloud:

# GitHub Actions OIDC con AWS
jobs:
  deploy:
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502
        with:
          role-to-assume: arn:aws:iam::123456789012:role/deploy-production
          aws-region: us-east-1
# GitLab CI OIDC con AWS
deploy:
  stage: deploy
  id_tokens:
    AWS_TOKEN:
      aud: https://gitlab.com
  script:
    - >
      STS_CREDENTIALS=$(aws sts assume-role-with-web-identity
      --role-arn arn:aws:iam::123456789012:role/deploy-production
      --web-identity-token $AWS_TOKEN
      --role-session-name "gitlab-ci-${CI_JOB_ID}")
    - export AWS_ACCESS_KEY_ID=$(echo $STS_CREDENTIALS | jq -r '.Credentials.AccessKeyId')
    - ./deploy.sh

Conclusión

Todo pipeline CI/CD tiene un modelo de confianza. La pregunta es si ese modelo de confianza fue diseñado intencionalmente o surgió accidentalmente de configuraciones predeterminadas y soluciones rápidas.

El modelo de ejecución que elija — alojado en SaaS, self-hosted, containerizado o serverless — determina las propiedades de seguridad base de su pipeline. Pero el modelo de ejecución por sí solo no es suficiente. La confianza debe ser explícitamente delimitada en cada transición: del código fuente al trigger, del trigger a la ejecución, de la ejecución a los secrets, y del build al despliegue.

Los patrones cubiertos en esta guía — Poisoned Pipeline Execution, abuso de runners compartidos, identidades con permisos excesivos, referencias mutables de actions y la confusión de identidades build/deploy — no son teóricos. Son las técnicas reales utilizadas en ataques a la cadena de suministro del mundo real, desde el compromiso de SolarWinds hasta la brecha de Codecov y más allá.

Comience mapeando sus fronteras de confianza actuales. Identifique dónde la confianza se asume en lugar de verificarse. Luego aplique las medidas de fortalecimiento de forma sistemática: restrinja triggers, minimice permisos, aísle runners, fije dependencias y separe identidades. Trate su pipeline CI/CD con el mismo rigor que aplica a su infraestructura de producción — porque en la práctica, es la puerta de entrada de su infraestructura de producción.