Cloud Security Wire
AWS Azure GCP RSS
Hardening Guide high

GitLab CI/CD Pipeline Security: Credential Theft Vectors and Hardening in 2026

GitLab CI/CD pipelines are an increasingly exploited attack surface. CI_JOB_TOKEN abuse, unprotected pipeline triggers, insecure remote includes, and OIDC misconfigurations can expose cloud credentials, registry tokens, and signing keys. This guide covers the attack paths and how to close them.

By Cloud Security Wire · ·
#GitLab#CI-CD#pipeline-security#CI_JOB_TOKEN#OIDC#supply-chain#secrets#DevSecOps#cloud-credentials#2026
High Severity

This issue has been assessed as high severity. Review affected configurations immediately.

GitLab CI/CD pipelines run with access to secrets, cloud credentials, and registry tokens that make them a high-value target. As GitHub Actions supply chain attacks grabbed headlines in 2025, GitLab environments went under-scrutinised — but the attack surface is comparable, and the exploitation patterns are well-established.

This guide covers the five most common pipeline credential theft paths in GitLab environments and the hardening steps for each.

The CI_JOB_TOKEN Scope Problem

CI_JOB_TOKEN is automatically available to every pipeline job. Its default scope in self-managed GitLab instances (and in some GitLab.com configurations) allows it to authenticate to other projects in the same namespace, clone repositories, and interact with the Container Registry.

If your CI pipeline builds a Docker image, the CI_JOB_TOKEN can, by default, pull images from any project in the same group. An attacker who gains pipeline execution — through a malicious merge request, a compromised runner, or a trigger token leak — can use the job token to exfiltrate code from sibling repositories.

Hardening:

In GitLab 16.0+, enable the job token allowlist for every project. This changes CI_JOB_TOKEN from a group-wide credential to a project-scoped one:

# .gitlab-ci.yml — no direct setting for this
# Configure via: Settings > CI/CD > Token Access > Limit access
# Or via API:
curl --request PUT "https://gitlab.example.com/api/v4/projects/${PROJECT_ID}/settings" \
  --header "PRIVATE-TOKEN: ${ADMIN_TOKEN}" \
  --data "restrict_user_defined_variables=true" \
  --data "ci_job_token_scope_enabled=true"

Also audit which projects are in your allowlist. Any project listed can clone your code using a job token obtained from a pipeline execution.

Exposed Trigger Tokens and API Tokens as CI Variables

Pipeline trigger tokens and API tokens stored as unprotected CI variables are accessible to any pipeline execution — including those triggered from unmerged branches or forks. When a developer adds REGISTRY_TOKEN or AWS_SECRET_ACCESS_KEY as a plain variable (not masked, not protected), it’s available in the environment of any job.

The attack path is straightforward: an external contributor opens a merge request that adds echo $AWS_SECRET_ACCESS_KEY | base64 to a job script. If the pipeline runs on the MR branch with unprotected variables, the credential is exfiltrated in the CI log output.

Hardening:

All secrets in CI/CD variables must be:

  1. Protected — only available to jobs running on protected branches/tags
  2. Masked — value is redacted from job logs
# Check all variables in your project via API:
curl "https://gitlab.example.com/api/v4/projects/${PROJECT_ID}/variables" \
  --header "PRIVATE-TOKEN: ${TOKEN}" | jq '.[] | select(.protected == false or .masked == false) | .key'

Any variable that fails this check is accessible to untrusted pipeline executions. Fix by re-creating with both flags set, or scoping to specific environments.

Additionally: never allow pipelines on fork merge requests to access protected variables by default. In GitLab, this is controlled at the project level under Settings > CI/CD > Merge request pipelines from forked projects.

Insecure Remote Includes

GitLab CI supports remote includes via include: remote: in .gitlab-ci.yml. If the included URL points to infrastructure you don’t control — a public GitHub file, a third-party domain, or even a GitLab project with its own contributors — a compromised or hijacked include can inject arbitrary pipeline steps into your build.

The attack mirrors GitHub Actions tag hijacking: compromise the remote resource, the downstream pipeline pulls and executes malicious content.

# INSECURE: pulls from an external URL
include:
  - remote: 'https://third-party.example.com/ci/shared-jobs.yml'

# SAFER: pull from a specific commit SHA in a controlled project
include:
  - project: 'your-group/shared-ci-templates'
    ref: '7f3a9c2d8e1b4f6a0c5d3e7b9f1a2c4d6e8f0a1b'  # pinned commit SHA
    file: '/templates/build.yml'

Pinning to a commit SHA is the equivalent of GitHub’s pinned SHA approach. Tags and branch names are mutable; commit SHAs are not. Audit all include: directives across your .gitlab-ci.yml files and enforce approved include sources in your CI configuration policy.

OIDC Misconfiguration: Overly Broad JWT Claims

GitLab CI/CD supports OpenID Connect (OIDC) to obtain short-lived cloud credentials without storing static secrets. When a GitLab pipeline job requests an AWS, Azure, or GCP credential via OIDC, the cloud provider validates the JWT claims GitLab issues.

The risk is in how the trust policy is configured on the cloud provider side. A common misconfiguration:

// AWS IAM trust policy — INSECURE: trusts any GitLab pipeline
{
  "StringEquals": {
    "gitlab.example.com:aud": "https://gitlab.example.com"
  }
}

This trusts any pipeline across the entire GitLab instance. An attacker with the ability to run a pipeline anywhere on the instance — including in a newly created test project — can assume the role.

Secure OIDC trust policy scoped to specific project and branch:

{
  "StringEquals": {
    "gitlab.example.com:aud": "https://gitlab.example.com",
    "gitlab.example.com:sub": "project_path:your-group/your-project:ref_type:branch:ref:main"
  }
}

The sub claim in GitLab’s JWT includes project_path, ref_type, and ref, which together scope the trust to a specific branch of a specific project. Production credentials should never be accessible from feature branches or from pipelines triggered by merge requests from external contributors.

GitLab’s JWT claim reference: the project_path, ref, and ref_type fields are the critical ones to lock down in cloud IAM trust policies.

Runner Registration Token Exposure

Self-managed GitLab instances use runner registration tokens to enrol CI runners. If a registration token is exposed — via .gitlab-ci.yml committed to a public repo, a misconfigured API call, or a compromised admin account — an attacker can register a malicious runner in your project or group.

A malicious registered runner receives pipeline jobs and can intercept secrets passed as environment variables, including masked variables, by writing custom runner scripts that dump the process environment before masking takes effect.

Hardening:

  • Rotate runner registration tokens immediately if there’s any suspicion of exposure. Tokens are managed under Settings > CI/CD > Runners.
  • GitLab 15.10+ introduces authentication tokens for individual runners rather than shared registration tokens. Migrate to this model — it eliminates the shared secret entirely.
  • Audit registered runners regularly: Settings > CI/CD > Runners shows all registered runners. Unrecognised runners should be removed and their registration investigated.
  • Restrict which runner tags are available to pipelines from merge requests and forks. Sensitive runners (with cloud credentials or access to production infrastructure) should require tags that aren’t available to untrusted pipelines.

Detection and Monitoring

GitLab’s audit events log runner registration, variable creation and modification, and pipeline trigger activity. Key events to monitor:

# Via GitLab API — recent audit events for suspicious CI activity
curl "https://gitlab.example.com/api/v4/audit_events?entity_type=Project&entity_id=${PROJECT_ID}" \
  --header "PRIVATE-TOKEN: ${TOKEN}" | \
  jq '.[] | select(.details.action | test("runner_registered|variable_created|trigger_token_created"))'

Alerts to configure:

  • Runner registered outside standard deployment tooling
  • CI variable created without protected or masked flags
  • Pipeline triggered from unrecognised source IP or user agent
  • Job log output matching patterns associated with credential exfiltration (base64 encoded strings, AWS key format AKIA*)

References

← All Analysis Subscribe via RSS