GitLab's integrated CI/CD is powerful, but a misconfigured pipeline can leak secrets, execute untrusted code, and provide attackers with a direct path to your production environment. This guide covers the essential security configuration for GitLab CI/CD.
Protected Variables
CI/CD variables often contain secrets: API keys, deployment credentials, registry tokens. GitLab lets you protect them:
Settings > CI/CD > Variables
For each secret variable:
- Protect variable: Only available in pipelines running on protected branches or tags.
- Mask variable: Hidden in job logs (but still accessible to scripts).
- Environment scope: Limit to specific environments (production, staging).
# .gitlab-ci.yml
deploy:
stage: deploy
script:
- deploy --token $DEPLOY_TOKEN
only:
- main
environment:
name: production
If DEPLOY_TOKEN is a protected variable scoped to the production environment and the main branch, it is not available in merge request pipelines from feature branches. This prevents a malicious merge request from exfiltrating the token.
Variable Masking Limitations
Masked variables are hidden in logs, but:
- The value must be at least 8 characters.
- The value cannot contain newlines.
- The masking is pattern-based and can be bypassed by encoding the value (base64, hex).
Do not rely on masking as your only protection. Use protected variables and limit job access.
Runner Security
Shared vs. Project-Specific Runners
Shared runners are used across multiple projects. If a runner executes untrusted code from one project, it could affect other projects using the same runner.
Best practice: Use project-specific or group-specific runners for sensitive projects.
Runner Isolation
Configure runners with Docker executor and proper isolation:
# config.toml
[[runners]]
executor = "docker"
[runners.docker]
image = "alpine:latest"
privileged = false
volumes = ["/cache"]
disable_entrypoint_overwrite = true
allowed_images = ["node:20-*", "python:3.12-*", "alpine:*"]
pull_policy = "always"
Critical settings:
privileged = false- Never enable privileged mode unless absolutely necessary (Docker-in-Docker).allowed_images- Restrict which Docker images jobs can use.pull_policy = "always"- Always pull fresh images to prevent using tampered local images.
Runner Tags
Use tags to route sensitive jobs to hardened runners:
deploy-production:
tags:
- production-runner
- isolated
script:
- deploy --env production
Pipeline Security
Protected Branches and Tags
Configure protected branches to control who can trigger pipelines:
Settings > Repository > Protected branches
Only merge requests from trusted branches should trigger deployment pipelines.
Merge Request Pipelines
Run security scans on merge request pipelines so vulnerabilities are caught before code reaches the main branch:
include:
- template: Security/SAST.gitlab-ci.yml
- template: Security/Dependency-Scanning.gitlab-ci.yml
- template: Security/Secret-Detection.gitlab-ci.yml
sast:
stage: test
rules:
- if: $CI_MERGE_REQUEST_IID
dependency_scanning:
stage: test
rules:
- if: $CI_MERGE_REQUEST_IID
Pipeline Approval Rules
For GitLab Ultimate, configure approval rules that require security team approval when security scans find critical issues:
# Require approval if SAST finds critical vulnerabilities
approval_rules:
- name: Security Review Required
approvals_required: 1
user_ids: [security_team_user_ids]
Built-in Security Scanning
GitLab provides several built-in security scanners. Enable them by including the appropriate templates:
SAST (Static Application Security Testing)
include:
- template: Security/SAST.gitlab-ci.yml
SAST supports many languages and detects common vulnerabilities like SQL injection, XSS, and hardcoded secrets.
Dependency Scanning
include:
- template: Security/Dependency-Scanning.gitlab-ci.yml
Scans your dependency manifests against vulnerability databases.
Container Scanning
include:
- template: Security/Container-Scanning.gitlab-ci.yml
container_scanning:
variables:
CS_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
Scans your Docker images for OS-level and application vulnerabilities.
Secret Detection
include:
- template: Security/Secret-Detection.gitlab-ci.yml
Scans commits for accidentally committed secrets.
DAST (Dynamic Application Security Testing)
include:
- template: Security/DAST.gitlab-ci.yml
dast:
variables:
DAST_WEBSITE: https://staging.yourapp.com
DAST tests your running application for vulnerabilities by simulating attacks.
Artifact Security
Artifact Expiration
Set expiration on build artifacts to prevent stale artifacts from persisting:
build:
artifacts:
paths:
- dist/
expire_in: 1 week
Artifact Access
CI artifacts are accessible to anyone with read access to the project. Do not store secrets or sensitive data in artifacts.
SBOM Generation in GitLab CI
Add SBOM generation to your pipeline:
generate-sbom:
stage: build
image: cyclonedx/cyclonedx-cli
script:
- cyclonedx-npm --output-file sbom.json
artifacts:
paths:
- sbom.json
reports:
cyclonedx: sbom.json
GitLab can ingest CycloneDX SBOMs and display dependency information in the project's Security dashboard.
How Safeguard.sh Helps
Safeguard.sh integrates with your GitLab CI/CD pipelines to provide cross-project dependency monitoring. While GitLab's built-in scanners work at the project level, Safeguard.sh aggregates findings across your entire GitLab instance. It ingests SBOMs and vulnerability scan results from every pipeline, provides a unified dashboard, and helps you track which projects need immediate attention. When a critical CVE emerges, Safeguard.sh shows you every affected project in your GitLab organization within minutes.