Self-hosting GitLab keeps code and pipelines on your own infrastructure, but it also means every misstep in runner isolation, token scope, or artifact storage lands squarely on your security team. This guide focuses exclusively on GitLab self-managed instances, showing how attackers chain pipeline weaknesses into full infrastructure compromise—and how to prevent it.
Scope reminder: Exercise these techniques only in GitLab environments you control or have written authorization to test.
GitLab CI/CD is powerful, but most pipelines and runners are built for cooperative developers, not adversaries. In real compromises, attackers chain misconfigured runners, over-privileged tokens, inherited templates, stale credentials, open artifacts, and shared caches to pivot across repos, workloads, and cloud environments.
Legal reminder: Only test on environments you own or have explicit authorization to assess.
Threat Model Snapshot
| Layer | Weakness | What attackers gain |
|---|---|---|
| Runners | Shared runners, privileged Docker, leaked registration tokens | Execute arbitrary code, read secrets, move laterally |
| Tokens | Overpowered CI_JOB_TOKEN, unscoped deploy tokens |
Cross-project data access and API abuse |
| Pipeline Definition | Untrusted includes, template hijacking | Org-wide pipeline compromise |
| Artifacts | Public artifacts, leaked logs, exposed .env |
Secret theft, staging ground for supply-chain attacks |
| Registry | Weak deploy tokens, unverified images | Poisoned images reaching prod |
| SCM | Weak branch protection, MR bypass | Injection of malicious pipeline jobs |
High-Risk Misconfigurations You Will See Everywhere
1. Leaked Runner Registration Tokens (GL-01)
If a registration token appears in a repo, screenshot, CI variable, Slack paste, or log, attackers register their own runner and GitLab dutifully sends them real jobs.
gitlab-runner register \
--url https://gitlab.internal \
--registration-token GR1342... \
--executor shell
From there attackers:
- Drain job secrets to their host via stdout or artifact downloads.
- Dump artifacts that contain credentials or signed binaries.
- Pivot into AWS/GCP/Azure using deploy keys harvested from jobs.
- Re-upload poisoned builds to the registry under trusted tags.
This is one of the most common GitLab CI takeovers; treat runner tokens like production passwords.
2. Overpowered CI_JOB_TOKEN (GL-02)
Every job automatically receives CI_JOB_TOKEN, and many orgs leave it with broad API rights.
curl -H "JOB-TOKEN: $CI_JOB_TOKEN" \
https://gitlab/api/v4/groups/123/projects
With no scoping, a malicious job can read sibling repos, download others' artifacts, trigger downstream pipelines, or retrieve container registry credentials—turning a single compromised pipeline into group-wide visibility.
3. GL-03 — Unprotected Runner Tags
What it is: Jobs can specify runner tags (e.g., prod, windows). If privileged runners share those tags without extra gating, attackers can land their code on sensitive hosts.
Attack path
- Attacker crafts a job with
tags: ["prod"]or similar high-value label. - Job lands on a deployment runner that has access to internal networks, production clusters, or cloud credentials.
Impact: Secrets exposure, lateral network access, and unauthorized production deployments.
Mitigations
- Mark privileged runners as protected and tie tag usage to protected branches or
rulesconditions. - Employ runner policies (per-project registration) and ensure runner hosts use least-privilege identities plus network egress controls.
4. GL-04 — Pipeline Artifacts Exposure
What it is: Artifacts (build outputs, .env files, SBOMs) from public projects or misconfigured permissions can be downloaded by anyone with the job URL.
Attack path
- Adversary enumerates job IDs or artifact endpoints.
- Downloads archives that contain credentials, configs, or internal binaries.
Impact: Leakage of secrets, internal certificates, or proprietary code.
Mitigations
- Set
artifacts: { public: false, expire_in: <short> }and keep sensitive projects private. - Require authentication/approval before artifact download, shorten retention, and scan artifacts for sensitive patterns.
5. GL-05 — Includes from Untrusted Repos
What it is: include:remote or cross-project includes pull CI templates from other repos. If that repo is compromised, the attacker dictates your pipeline steps.
Attack path
- Pipeline references remote template.
- Remote repo owner (or attacker) modifies template with malicious stages.
- Your pipeline executes the payload with your project permissions.
Impact: Arbitrary code execution, artifact tampering, and organization-wide compromise.
Mitigations
- Pin includes to commit SHAs, host shared templates in locked-down repos, and require approvals for include changes.
- Prefer group-managed templates and restrict who can modify them.
6. GL-06 — Protected Variables Misuse
What it is: Protected variables should only appear in protected branches, but weak branch protection or compromised maintainers allow attackers to trigger pipelines that expose those secrets.
Attack path
- Attacker gains push/merge rights (phishing, credential theft, lax CODEOWNERS).
- Triggers pipeline on a protected branch that consumes the protected variables.
- Exfiltrates via logs, artifacts, or network calls.
Impact: Loss of production credentials, tokens, and access to downstream infrastructure.
Mitigations
- Enforce strict CODEOWNERS + multi-reviewer approvals for protected branches.
- Use external secret stores (Vault, cloud KMS) and require gated deployments for jobs accessing sensitive variables.
Runner Exploitation & Lateral Movement
Privileged Docker Executors
Many teams run GitLab runners with Docker-in-Docker in privileged mode. Privilege escalation is trivial:
services:
- docker:dind
script:
- docker run -v /:/host alpine chroot /host sh
Mounting the Docker socket gives an attacker full host control, which in turn exposes other pipelines, cached credentials, and any secrets stored on the runner. Breakouts here routinely lead to AWS/Azure/GCP credential theft and lateral movement into internal networks.
Shared Runners = Shared Blast Radius
In most real environments a single runner pool serves dozens of projects and sits deep inside the corporate network. Work directories, caches, and container layers persist across jobs, and egress is rarely restricted. One compromised repo buys attackers internal network scanning, cached files from other projects, kubeconfigs from IaC repos, and credentials under /home/gitlab-runner/.docker.
Mitigation: Provide dedicated runners per project or sensitivity tier, enforce CPU/memory quotas, disable privileged Docker, wipe caches between jobs, and apply SELinux/AppArmor profiles plus egress firewalls.
Cloud Credential Exposure from Runners
GitLab runners often hold AWS access keys, GCP service accounts, Azure SP secrets, kubeconfigs with cluster-admin privileges, or SSH deploy keys. Attackers routinely hit metadata endpoints to escalate further:
# AWS metadata
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
# GCP metadata
curl -H "Metadata-Flavor: Google" \
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
Defense: Remove long-lived cloud keys from runners, rely on short-lived workload identities (OIDC, Workload Identity Federation), and lock runner network ACLs so metadata services are unreachable.
Pipeline Definition & Template Attacks
6. Remote Include Hijacking
.gitlab-ci.ymlsupportsinclude:remote. Compromising the referenced repo gives attackers arbitrary job execution.- Defense: Pin includes to commit SHAs, host them in locked-down projects, and turn on Pipeline Editor approval rules for include changes.
7. Rules/Only Bypass
- Attackers add
rules: - when: alwaysto ensure malicious jobs run even on forks. - Fix: Configure
Instance > Settings > CI/CD > Pipeline configurationto require approvals for.gitlab-ci.ymledits in critical projects and leverage Compliance Pipelines.
8. Cache & Dependency Proxy Poisoning
- Issue: Shared cache keys (e.g.,
node_modules) or dependency proxy state can be seeded with backdoors via untrusted branches. - Mitigation: Include
$CI_COMMIT_SHAor pipeline IDs in cache keys, restrict dependency proxy use to trusted projects, and scan cached archives before reuse.
9. Docker-in-Docker Breakout
- Running Docker executor with
privileged = truegrants access to the host Docker socket, enabling full runner takeover and lateral movement. - Defense: Prefer Kubernetes or Firecracker runners, disable privileged mode, or use Kata Containers for hardware isolation.
Artifacts, Registry, and Logs
10. Artifact Leakage
artifacts:publicdefaults totrueon public projects; direct URLs leak.env, kubeconfigs, or SBOMs.- Fix: Set
artifacts: { public: false, expire_in: 1 day }, move sensitive outputs to locked S3 buckets, and enforce Access Tokens for downloads.
11. Registry Credential Reuse & Poisoning
- Jobs commonly echo
CI_REGISTRY_PASSWORDinto docker login logs or commit them to build scripts, gifting attackers registry tokens. - Once authenticated, adversaries push poisoned images under legitimate tags (
app:latest) and most CD systems deploy them automatically. - Mitigation: Use
--password-stdin, rotate deploy tokens, require signed images, and block registry access from untrusted networks via firewall rules.
12. Trace & Log Scraping
- Developers frequently use
set -x,echo $SECRET_KEY, or rundocker loginwithout--password-stdin, leaving plaintext secrets in logs. - GitLab stores traces in
/var/opt/gitlab/gitlab-rails/shared/builds/; attackers with filesystem access, backup archives, or public trace links can full-text search for credentials. - Defense: Enable Secret Detection masking, redact logs (turn
set -xoff), enforce log retention, and encrypt backups with KMS keys plus strict access logging.
13. Cache Mounts to Runner Host
- HostPath caches (e.g.,
/cache) reused between jobs leak artifacts across tenants. - Fix: Use per-project cache volumes, run
gitlab-runnerwith--cache-dirinside tmpfs, and wipe caches after each job.
Real Exploitation Patterns Observed
| Vulnerability | Realistic attack | What attacker gets |
|---|---|---|
| Runner registration token leak | Register rogue runner | All pipeline secrets plus persistent foothold |
Overpowered CI_JOB_TOKEN |
Query APIs across group | Source code, artifacts, merge requests |
| Untrusted include | Modify central template repo | Org-wide CI compromise |
| Public artifacts | Download env files | Cloud keys, internal APIs |
| Privileged runner | Abuse docker.sock |
Full runner host takeover |
| Weak branch protection | Push to protected branch | Production secrets leak |
Platform & Infrastructure Weaknesses
14. Outdated GitLab Version = Known CVEs
- Self-managed instances lag behind releases; CVEs like GLSA-2023-11 allow SSRF, 2FA bypass, or pipeline impersonation.
- Recommendation: Track GitLab Security Releases, automate upgrades, and stage patches in non-prod first.
15. OAuth/Application Secrets Sitting in DB
- GitLab stores integration secrets in PostgreSQL; DB access = API takeover.
- Defense: Encrypt disks, restrict DB access via TLS + mTLS, monitor
gitlab-psqlqueries, and rotate integration secrets regularly.
16. Backup Archives with Everything
gitlab-backuptarballs contain repos, wikis, registry, CI traces, and secrets. Unencrypted backups or cloud buckets without MFA are low-hanging fruit.- Mitigation: Encrypt backups, store in segregated accounts, and test restore procedures with strict access logging.
Hardening Recommendations
- Ephemeral Runners: Auto-scale Docker/Kubernetes runners per job, destroy VM or pod immediately after completion.
- Network Isolation: Place runners in dedicated subnets with egress allowlists (GitLab, registry, package mirrors). Deny direct access to prod clusters.
- Secrets Management: Move high-value secrets to Vault/GCP Secret Manager; fetch short-lived tokens via
vault kv getor JWT auth inside jobs. - Compliance Pipelines: Use GitLab's compliance framework to enforce mandatory jobs (SAST, signing, policy checks) regardless of engineers'
.gitlab-ci.ymledits. - Artifact Integrity: Sign container images with Cosign, verify signatures in deploy stages (Kyverno
verifyImages, admission controllers). - RBAC for Maintainers: Limit who can edit pipeline files; require approvals from a security group for CI config changes in crown-jewel projects.
Additional GitLab-Specific Hardening Suggestions
- Enforce "pipelines only from protected branches/forks" for production projects.
- Require maintainer/security approval for any
.gitlab-ci.ymlor include template changes. - Block pipeline execution originating from forks on sensitive repositories.
- Audit pipeline + runner activity; alert on runner registration, token usage, and remote include modifications.
CI Security Tooling: What to Run and Where
1. Dependency Scanning (SCA)
- Purpose: Detect vulnerable OS or language packages early.
- Tools: GitLab Dependency Scanning, Snyk, OWASP Dependency-Check, Grype.
- Placement: Merge-request pipelines and release builds.
dependency_scanning:
stage: test
image: registry.gitlab.com/gitlab-org/security-products/dependency-scanning:latest
script:
- dependency-scanning run
artifacts:
reports:
dependency_scanning: gl-dependency-report.json
2. Static Application Security Testing (SAST)
- Purpose: Catch code risks (SQLi, XSS, unsafe deserialization) before merge.
- Tools: GitLab SAST, Semgrep, SonarQube, CodeQL.
- Placement: Fast MR pipelines plus deeper scheduled scans on default branch.
3. Dynamic Application Security Testing (DAST)
- Purpose: Probe running apps for runtime flaws.
- Tools: GitLab DAST (ZAP), OWASP ZAP, Burp Suite automation.
- Placement: Against review apps/staging only; isolate from production data.
4. Container/Image Scanning
- Purpose: Identify vulnerable libraries inside container images.
- Tools: Trivy, Clair, Anchore, GitLab Container Scanning.
- Placement: Immediately after
docker buildand before pushing to the registry.
container_scan:
stage: test
image: aquasec/trivy:latest
script:
- trivy image --exit-code 1 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA || true
allow_failure: false
5. Secret Detection
- Purpose: Prevent API keys/tokens entering git history and artifacts.
- Tools: GitLab Secret Detection, truffleHog, git-secrets, detect-secrets.
- Placement: Pre-commit hooks plus MR pipelines; rotate any detected secret immediately.
6. SBOM & Provenance
- Purpose: Generate SBOMs (CycloneDX/SPDX) and SLSA provenance for each build artifact.
- Tools: Syft, GitLab SBOM reports, SLSA generators.
- Placement: During build stage; store SBOMs with artifacts/registry metadata and enforce attestation checks downstream.
7. Artifact Signing & Verification
- Purpose: Guarantee only trusted artifacts deploy.
- Tools: Sigstore (cosign/rekor), Notary v2, GPG.
- Placement: Sign immediately after tests pass; verify signatures during deployment/admission.
8. Policy Enforcement / Admission Controls
- Purpose: Enforce org policies (signed images, no critical CVEs, approved base images).
- Tools: OPA/Gatekeeper, Kyverno, GitLab compliance pipelines.
- Placement: Pipeline gates plus Kubernetes admission controllers.
9. Runtime / Post-Deployment Monitoring
- Purpose: Detect suspicious behavior once workloads run.
- Tools: Falco, Aqua, Prisma Cloud, cloud provider security suites.
- Placement: Staging/prod clusters with SIEM alerting.
10. Secret Management & OIDC
- Purpose: Replace long-lived secrets with short-lived workload identities.
- Tools: GitLab OIDC → cloud IAM, HashiCorp Vault JWT, cloud workload federation.
- Placement: Jobs fetch secrets on the fly; nothing stored in repos or group variables.
Where to Place Scanners in the Pipeline
- Pre-commit: Lightweight secret/regex checks and policy-as-code linters.
- Merge-request: Fast SAST, dependency scan, container scan, secret detection; block merges on critical findings.
- Nightly/full builds: Deep SAST, DAST, full container sweep, SBOM generation, provenance attestations.
- Release: Artifact signing, provenance generation, final policy verification, compliance checks.
- Deployment gates: Verify signatures/SBOMs; enforce admission policies; re-check critical CVEs.
- Post-deploy: Runtime detections, anomaly monitoring, drift detection against SBOM/provenance.
Practical Tips for Integrating Tooling
- Shift left but keep depth: quick checks on MRs, deeper scans asynchronously to avoid blocking dev velocity.
- Fail pipelines sensibly: block on high/critical; warn or ticket medium/low severities.
- Surface findings directly in MR security reports for faster triage and developer ownership.
- Automate triage: map scanner severity to remediation SLAs and auto-create issues/incidents.
- Isolate untrusted code by running forks on restricted runners with no secrets and minimal network access.
- Use short-lived credentials via OIDC or Vault issuance; avoid static deploy tokens in variables.
- Centralize audit logs (pipelines, runner registrations, token usage) in your SIEM with alerting playbooks.
Tooling Security Teams Use for GitLab CI Audits
| Tool | Usage | Notes |
|---|---|---|
| Trivy | Scan repos, pipelines, and built images (e.g., trivy fs . for secrets) |
Pairs well with IaC repos to find leaked credentials |
| Gitleaks | Detect secrets committed to repos | Run in MR pipeline plus scheduled scans for legacy projects |
| CI Lint / Pipeline Lint API | Validate .gitlab-ci.yml for edge cases |
Great for testing rules bypass attempts before exploiting |
| GitLab API scripts | Enumerate runners, tokens, variable exposure | Red teams script this to hunt orphaned runners & tokens |
| Container scanners | Validate produced images in registry | Prevent registry poisoning before deployment |
| Custom crawlers | Find public artifacts/logs | Common in bug bounty engagements to harvest secrets |
Attacker Runbook: How GitLab CI/CD Is Actually Compromised
- Harvest tokens: Scrape repos, CI variables, logs, and UIs for runner registration tokens, deploy keys, and cloud creds.
- Register rogue runner: Use any leaked registration token to start intercepting real jobs.
- Dump environments: Pull
.env, kubeconfigs, SSH keys, and cloud credentials from job artifacts or/tmp. - API pivoting: Abuse
CI_JOB_TOKENto enumerate other projects, download artifacts, or trigger downstream pipelines. - Backdoor builds: Poison caches, templates, or registry images so future builds propagate the malware.
- Persist in supply chain: Push malicious artifacts/images (often unsigned) so production deploys the payload automatically.
- Compromise cloud: Use leaked keys or metadata tokens to take over control planes and keep lateral movement going.
Defensible GitLab Architecture Checklist
- Short-lived tokens only (Vault, OIDC, workload identity)
- No shared runners for sensitive repos
- No privileged Docker executors
- Protected and reviewed CI templates
- Strict CODEOWNERS plus MR approvals
- Private artifacts with short TTL
CI_JOB_TOKENscoping enabled- Registry requires signatures
- GitLab updated monthly
- Encrypted backups in isolated storage
- Egress restrictions on runners
- Secrets never stored in GitLab UI for production
Detection & Response Playbook
| Step | Action | GitLab artifact |
|---|---|---|
| 1 | Detect suspicious pipeline edits | Enable Audit Events for .gitlab-ci.yml changes; send to SIEM |
| 2 | Contain | Pause affected runners via gitlab-runner unregister, revoke tokens, disable project pipelines |
| 3 | Eradicate | Rotate CI/CD variables, revoke deploy tokens, rebuild runner images |
| 4 | Recover | Re-run trusted pipelines, reissue artifacts/signatures |
| 5 | Review | Analyze admin/audit_log, Sidekiq logs, and container registry access logs |
Rapid Response Playbook
| Threat | Immediate action | Long-term fix |
|---|---|---|
| Rogue runner detected | Unregister runner, rotate all registration tokens | Add approval workflow and monitoring for runner registrations |
| Backdoored pipeline definition | Lock template repo, revert malicious commits | Enforce compliance pipelines and signed templates |
| Artifact or secret leak | Revoke/rotate exposed keys immediately | Enforce short-lived secrets plus automated scanning |
| Registry poisoning | Block affected tags, rebuild images from source | Require image signing and admission checks |
| Runner host compromised | Rebuild runner AMI/base image | Use ephemeral runners only, remove privileged executors |
Helpful commands:
# List recent runner registrations
sudo gitlab-rails runner 'puts Ci::Runner.last(10).map { |r| "#{r.description}: #{r.token_expires_at}" }'
# Audit CI variable exposure
sudo gitlab-rails console <<'RUBY'
Ci::Variable.where(protected: false).limit(20).each { |v| puts "#{v.key} in #{v.project.full_path}" }
RUBY
# Find public artifacts
curl -s --header "PRIVATE-TOKEN: $PAT" \
"https://gitlab.internal/api/v4/projects/:id/jobs/artifacts/main/download?job=build"
Quick Hardening Checklist
| Control | Status Goal |
|---|---|
| GitLab version | < 30 days behind latest security release |
| Runner isolation | Dedicated runners per project/env, no Docker socket mounts |
| Token hygiene | ci_job_token_scope enabled, registration tokens rotated quarterly |
| Secret storage | All prod secrets sourced from external vault, not GitLab UI |
| Artifact retention | 7 days max, private by default, signed images |
| Backup security | Encrypted, stored off-site with MFA + access logging |
By PlaidNox DevSecOps Team
Published Nov 2025