Cheat Sheet de Seguridad de GitLab CI: Variables, Runners, Entornos y OIDC

Por Qué Importa la Seguridad en GitLab CI

Los pipelines de GitLab CI/CD son potentes — pero con ese poder viene el riesgo. Una variable mal configurada puede filtrar secretos. Un runner sin alcance definido puede ejecutar código malicioso. Un entorno sin protección puede permitir que un desarrollador junior haga un despliegue directo a producción. Este cheat sheet te ofrece YAML listo para copiar y pegar para cada control crítico de seguridad en GitLab CI, desde variables protegidas hasta la federación OIDC.

Guarda esta página en tus favoritos. Úsala como referencia cada vez que configures un nuevo proyecto o audites uno existente.

1. Variables Protegidas, Enmascaradas y Ocultas

Las variables de GitLab CI/CD controlan cómo fluyen los secretos en tus pipelines. Configurar esto incorrectamente es la causa número uno de fugas de credenciales en CI/CD. Cada valor sensible debe estar protegido (disponible solo en ramas/tags protegidos), enmascarado (oculto en los logs del job) y, donde sea compatible, oculto (invisible en la interfaz después de la creación).

Configuración de Variables a través de la API

# Crear una variable protegida + enmascarada a través de la API de GitLab
curl --request POST \
  --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
  "https://gitlab.example.com/api/v4/projects/$PROJECT_ID/variables" \
  --form "key=AWS_SECRET_ACCESS_KEY" \
  --form "value=$MY_SECRET" \
  --form "protected=true" \
  --form "masked=true" \
  --form "variable_type=env_var"

Uso de Variables en .gitlab-ci.yml

variables:
  # Las variables a nivel de grupo o proyecto se inyectan automáticamente.
  # Las variables de tipo archivo se escriben en una ruta temporal.
  DEPLOY_TOKEN:
    description: "Token para despliegue en producción"
    value: ""  # Intencionalmente vacío — configurar en CI/CD Settings

deploy_production:
  stage: deploy
  script:
    - echo "Desplegando con token enmascarado..."
    - ./deploy.sh --token "$DEPLOY_TOKEN"
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
  environment:
    name: production

Reglas clave:

  • Nunca escribas secretos directamente en .gitlab-ci.yml — utiliza siempre la configuración de variables CI/CD.
  • Establece protected=true para que los secretos solo estén disponibles en ramas protegidas.
  • Establece masked=true para que los valores se oculten en los logs del job.
  • Usa variables a nivel de grupo para secretos compartidos entre proyectos (por ejemplo, credenciales de la nube).

2. Tipos de Runners y Alcance

Los runners ejecutan tus jobs de CI/CD. Si cualquier runner puede tomar cualquier job, un merge request malicioso podría exfiltrar secretos de un runner de producción. Un alcance adecuado es esencial.

Registro de Runner con Tags y Protección

# Registrar un runner con alcance solo para ramas protegidas
gitlab-runner register \
  --non-interactive \
  --url "https://gitlab.example.com" \
  --token "$RUNNER_REG_TOKEN" \
  --executor docker \
  --docker-image alpine:latest \
  --tag-list "production,protected" \
  --access-level ref_protected

Asignación de Jobs a Runners Específicos

# .gitlab-ci.yml — asegurar que los jobs de producción solo se ejecuten en runners protegidos
deploy_production:
  stage: deploy
  tags:
    - production
    - protected
  script:
    - kubectl apply -f k8s/production/
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
  environment:
    name: production

# Los jobs de desarrollo usan un runner separado y sin privilegios
test:
  stage: test
  tags:
    - shared
    - development
  script:
    - pytest tests/

Mejores prácticas:

  • Usa --access-level ref_protected para restringir los runners a ramas y tags protegidos.
  • Usa runners específicos del proyecto para cargas de trabajo sensibles — nunca compartas runners de producción entre proyectos no relacionados.
  • Prefiere runners efímeros (ejecutores Docker, Kubernetes) para que el entorno se destruya después de cada job.
  • Desactiva los shared runners en proyectos que manejan despliegues sensibles.

3. Entornos Protegidos con Aprobaciones

Los entornos protegidos añaden una puerta humana antes de los despliegues. Esta es tu última línea de defensa contra cambios no autorizados en producción.

Configuración del Entorno en .gitlab-ci.yml

# .gitlab-ci.yml — despliegue con protección de entorno
deploy_staging:
  stage: deploy
  script:
    - ./deploy.sh staging
  environment:
    name: staging
    url: https://staging.example.com
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

deploy_production:
  stage: deploy
  script:
    - ./deploy.sh production
  environment:
    name: production
    url: https://example.com
    deployment_tier: production
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: manual
      allow_failure: false

Configuración de Reglas de Aprobación a través de la API

# Proteger el entorno de producción con aprobaciones requeridas
curl --request POST \
  --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
  "https://gitlab.example.com/api/v4/projects/$PROJECT_ID/protected_environments" \
  --data '{"name": "production", "deploy_access_levels": [{"group_id": 9899826}], "required_approval_count": 2, "approval_rules": [{"group_id": 9899826, "required_approvals": 2}]}'

Configura esto en Settings > CI/CD > Protected Environments en la interfaz de GitLab. Requiere al menos dos aprobaciones para despliegues a producción. Restringe el acceso de despliegue a grupos o usuarios específicos — nunca a “All maintainers.”

4. Alcance de CI_JOB_TOKEN

CI_JOB_TOKEN es un token automático que GitLab inyecta en cada job. Por defecto, puede acceder a otros proyectos en tu grupo — un riesgo serio de movimiento lateral. Desde GitLab 16.0, deberías restringir su alcance.

Restricción del Acceso del Token

# Verificar el alcance actual de CI_JOB_TOKEN
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
  "https://gitlab.example.com/api/v4/projects/$PROJECT_ID/job_token_scope"

# Limitar CI_JOB_TOKEN para que solo acceda a proyectos específicos
curl --request PATCH \
  --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
  "https://gitlab.example.com/api/v4/projects/$PROJECT_ID/job_token_scope" \
  --data '{"enabled": true}'

# Añadir un proyecto a la lista de permitidos
curl --request POST \
  --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
  "https://gitlab.example.com/api/v4/projects/$PROJECT_ID/job_token_scope/allowlist" \
  --data '{"target_project_id": 12345}'

Uso Seguro de CI_JOB_TOKEN en Pipelines

# .gitlab-ci.yml — uso de CI_JOB_TOKEN para triggers entre proyectos
trigger_deploy:
  stage: deploy
  trigger:
    project: my-group/deploy-project
    branch: main
    strategy: depend
  # CI_JOB_TOKEN se usa automáticamente para el trigger.
  # El proyecto destino debe incluir este proyecto en su lista de permitidos.

Reglas clave: Habilita el límite de alcance del CI/CD job token en cada proyecto. Solo incluye en la lista de permitidos los proyectos específicos que genuinamente necesitan acceso entre proyectos. Audita las listas de permitidos trimestralmente.

5. OIDC id_tokens para AWS y GCP

La federación OIDC elimina por completo las credenciales de nube de larga duración en tus variables de CI/CD. GitLab emite un JWT de corta duración, y tu proveedor de nube lo intercambia por credenciales temporales. Este es el estándar de oro para la autenticación en la nube en pipelines.

Federación OIDC con AWS

# .gitlab-ci.yml — autenticación OIDC con AWS
deploy_aws:
  stage: deploy
  image: amazon/aws-cli:latest
  id_tokens:
    GITLAB_OIDC_TOKEN:
      aud: https://gitlab.example.com
  variables:
    ROLE_ARN: arn:aws:iam::123456789012:role/gitlab-ci-deploy
  script:
    - >
      export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s"
      $(aws sts assume-role-with-web-identity
      --role-arn $ROLE_ARN
      --role-session-name "GitLabCI-${CI_PROJECT_ID}-${CI_PIPELINE_ID}"
      --web-identity-token "$GITLAB_OIDC_TOKEN"
      --duration-seconds 3600
      --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]'
      --output text))
    - aws s3 sync ./build s3://my-production-bucket/
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
  environment:
    name: production

Federación de Identidades de Carga de Trabajo con GCP

# .gitlab-ci.yml — autenticación OIDC con GCP
deploy_gcp:
  stage: deploy
  image: google/cloud-sdk:latest
  id_tokens:
    GITLAB_OIDC_TOKEN:
      aud: https://gitlab.example.com
  script:
    - echo "$GITLAB_OIDC_TOKEN" > /tmp/gitlab_token.txt
    - >
      gcloud iam workload-identity-pools create-cred-config
      projects/$GCP_PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID
      --service-account="$GCP_SERVICE_ACCOUNT"
      --output-file=/tmp/gcp_creds.json
      --credential-source-file=/tmp/gitlab_token.txt
    - export GOOGLE_APPLICATION_CREDENTIALS=/tmp/gcp_creds.json
    - gcloud config set project $GCP_PROJECT_ID
    - gcloud run deploy my-service --image gcr.io/$GCP_PROJECT_ID/my-app:$CI_COMMIT_SHA
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
  environment:
    name: production

En el lado de la nube, configura la política de confianza para validar claims como project_path, ref y ref_protected de modo que solo proyectos y ramas específicos puedan asumir el rol.

6. Seguridad de Pipelines de Merge Request

Los pipelines de merge request se ejecutan sobre código que aún no ha sido revisado. Trátalos como no confiables. Nunca expongas secretos de producción a los pipelines de MR.

# .gitlab-ci.yml — reglas separadas para pipelines de MR vs. pipelines de rama
test:
  stage: test
  script:
    - pytest tests/
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == "main"

deploy_production:
  stage: deploy
  script:
    - ./deploy.sh production
  rules:
    # NUNCA ejecutar en pipelines de merge request
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      when: never
    - if: $CI_COMMIT_BRANCH == "main"
  environment:
    name: production

Controles críticos:

  • Usa rules: para asegurar que los jobs de despliegue nunca se ejecuten en pipelines de tipo merge_request_event.
  • Requiere que el pipeline sea exitoso antes de hacer merge (Settings > Merge Requests).
  • Habilita “Pipelines must succeed” y “All discussions must be resolved.”
  • Considera habilitar merged results pipelines para probar el estado post-merge.

7. Plantilla de Detección de Secretos

El escáner integrado de detección de secretos de GitLab captura credenciales accidentalmente comprometidas antes de que lleguen a la rama por defecto.

# .gitlab-ci.yml — incluir la plantilla de detección de secretos
include:
  - template: Jobs/Secret-Detection.gitlab-ci.yml

# Sobrescribir para que el pipeline falle si se encuentran secretos
secret_detection:
  variables:
    SECRET_DETECTION_HISTORIC_SCAN: "true"  # Escanear el historial completo de git
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  allow_failure: false  # Bloquear el pipeline al detectar secretos

Para un escaneo más completo, añade también las plantillas de SAST y dependency scanning:

include:
  - template: Jobs/Secret-Detection.gitlab-ci.yml
  - template: Jobs/SAST.gitlab-ci.yml
  - template: Jobs/Dependency-Scanning.gitlab-ci.yml

Revisa los hallazgos en el Security Dashboard (disponible en GitLab Ultimate) o analiza los artefactos JSON en los tiers inferiores.

8. Push Rules

Las push rules aplican políticas a nivel de Git — antes de que el código entre siquiera en un pipeline. Úsalas para prevenir que se suban secretos, imponer estándares en los mensajes de commit y restringir tipos de archivos.

# Configurar push rules a través de la API
curl --request PUT \
  --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
  "https://gitlab.example.com/api/v4/projects/$PROJECT_ID/push_rule" \
  --data '{"deny_delete_tag": true, "prevent_secrets": true, "commit_message_regex": "^(feat|fix|chore|docs|refactor|test|ci)\\(.*\\):.*", "max_file_size": 50, "member_check": true, "reject_unsigned_commits": true}'

Push rules recomendadas:

  • prevent_secrets: true — Rechaza pushes que contengan archivos que parezcan secretos (claves, tokens, certificados).
  • reject_unsigned_commits: true — Requiere commits firmados con GPG (GitLab Premium+).
  • commit_message_regex — Impone mensajes de commit convencionales para un registro de auditoría limpio.
  • max_file_size — Previene la subida accidental de binarios grandes.
  • member_check: true — Rechaza commits de personas que no son miembros del proyecto.

9. Timeouts de Jobs e interruptible

Los jobs desbocados desperdician recursos y pueden ser explotados para criptominería. Establece timeouts explícitos y marca los jobs no críticos como interruptible para que se cancelen cuando un nuevo pipeline comience en la misma rama.

# .gitlab-ci.yml — timeouts e interruptible
default:
  timeout: 30m          # Timeout global por defecto para todos los jobs
  interruptible: true   # Cancelar jobs en ejecución cuando se hace push de un nuevo commit
  retry:
    max: 1
    when:
      - runner_system_failure
      - stuck_or_timeout_failure

test:
  stage: test
  timeout: 15m
  interruptible: true
  script:
    - pytest tests/ --timeout=600

deploy_production:
  stage: deploy
  timeout: 20m
  interruptible: false  # NUNCA cancelar un despliegue a producción en ejecución
  script:
    - ./deploy.sh production
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: manual
  environment:
    name: production

Directrices:

  • Establece un timeout a nivel de proyecto en Settings > CI/CD > General Pipelines (recomendado: 60 minutos máximo).
  • Establece timeouts a nivel de job más ajustados que el valor por defecto del proyecto.
  • Marca los jobs de test y lint como interruptible: true para ahorrar capacidad de runners.
  • Marca los jobs de despliegue como interruptible: false para prevenir despliegues parciales.
  • Usa retry solo para fallos transitorios de infraestructura — nunca para fallos de tests.

Tabla de Referencia Rápida

Control Dónde Configurar Tier Mínimo
Variables Protegidas/Enmascaradas Settings > CI/CD > Variables Free
Alcance de Runners Settings > CI/CD > Runners Free
Entornos Protegidos Settings > CI/CD > Protected Environments Premium
Alcance de CI_JOB_TOKEN Settings > CI/CD > Token Access Free
OIDC id_tokens .gitlab-ci.yml Free
Detección de Secretos include: template Free (Ultimate para dashboard)
Push Rules Settings > Repository > Push Rules Premium
Timeouts de Jobs Settings > CI/CD + .gitlab-ci.yml Free

Lecturas Adicionales y Laboratorios

Continúa fortaleciendo tus pipelines de GitLab CI/CD con estos recursos relacionados:

La seguridad no es una configuración de una sola vez — es una práctica continua. Revisa la configuración de seguridad de tus pipelines trimestralmente, rota las credenciales, audita el acceso de los runners y mantén tu instancia de GitLab actualizada. Este cheat sheet te da los bloques de construcción. Ahora ve y asegura tus pipelines.