GitLab CI security depends on runner trust boundaries, protected variables and secrets, pipeline policy controls, and securing the GitLab Runner configuration. As an integrated CI/CD platform, security spans both GitLab server hardening and runner isolation.
# Register runner with specific tags and locked status
gitlab-runner register \
--url https://gitlab.example.com/ \
--registration-token <token> \
--executor docker \
--description "docker-runner-protected" \
--tag-list "protected,docker" \
--locked true \
--access-level not_protected \
--docker-privileged false \
--docker-volumes /var/run/docker.sock:/var/run/docker.sock
# /etc/gitlab-runner/config.toml example
[[runners]]
name = "secure-docker-runner"
url = "https://gitlab.example.com/"
token = "<runner-token>"
executor = "docker"
[runners.docker]
tls_verify = false
image = "alpine:latest"
privileged = false
disable_entrypoint = false
disable_cache = false
volumes = ["/cache"]
shm_size = 0
network_mode = "bridge"
pull_policy = "always"
[runners.cache]
Type = "s3"
Shared = true
privileged = false unless explicitly requiredpull_policy = "always" for fresh images[[runners]]
name = "kubernetes-runner"
url = "https://gitlab.example.com/"
token = "<runner-token>"
executor = "kubernetes"
[runners.kubernetes]
namespace = "gitlab-runners"
privileged = false
allow_privilege_escalation = false
image = "alpine:latest"
service_account = "gitlab-runner"
# .gitlab-ci.yml - Secure variable usage
variables:
DATABASE_URL:
value: "" # Set via GitLab UI as protected/masked
description: "Production database connection string"
stages:
- test
- deploy
test:
stage: test
script:
- echo "Running tests with secure DB connection"
variables:
NODE_ENV: test
# Add protected variable via GitLab API
curl --request POST --header "PRIVATE-TOKEN: <token>" \
--url "https://gitlab.example.com/api/v4/projects/<id>/variables" \
--data "key=API_KEY" \
--data "value=<secret-value>" \
--data "protected=true" \
--data "masked=true" \
--data "environment_scope=production"
# Avoid exposing secrets in logs
deploy:
stage: deploy
script:
- echo "Deploying application..." # Never echo secrets
- ./deploy.sh # Script handles secrets internally
environment:
name: production
url: https://app.example.com
set +x in shell scripts# .gitlab-ci.yml with security controls
stages:
- security
- test
- build
- deploy
# Security scanning
sast:
stage: security
variables:
SAST_EXCLUDED_PATHS: "vendor,node_modules"
# Require manual approval for production
deploy_production:
stage: deploy
script:
- ./deploy.sh production
environment:
name: production
when: manual
only:
- main
needs:
- security_scan
- test
# Secure include usage
include:
# Prefer local templates
- local: '.gitlab-ci-templates.yml'
# Remote templates from trusted sources only
- remote: 'https://trusted-internal-gitlab.example.com/templates/security-scan.yml'
# Avoid including from untrusted external repos
# Restrict pipeline execution on merge requests
merge_request_pipeline:
stage: test
script:
- echo "Running tests for merge request"
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
changes:
- "**/*.js"
- "**/*.ts"
# Review project access levels
curl --header "PRIVATE-TOKEN: <token>" \
"https://gitlab.example.com/api/v4/projects/<id>/members"
# Check protected branches
curl --header "PRIVATE-TOKEN: <token>" \
"https://gitlab.example.com/api/v4/projects/<id>/protected_branches"
# Check runner status
sudo gitlab-runner verify
# Review runner logs
sudo tail -f /var/log/gitlab-runner/runner.log
# Check GitLab audit logs (admin only)
curl --header "PRIVATE-TOKEN: <admin-token>" \
"https://gitlab.example.com/api/v4/audit_events"
# Check GitLab Runner version
gitlab-runner --version
# Verify runner configuration
sudo gitlab-runner verify --delete
# Review runner config
sudo cat /etc/gitlab-runner/config.toml | grep -E "privileged|executor|token"
# Check listening ports
sudo ss -tulpn | grep -E ':80|:443|:9252'
# List registered runners
curl --header "PRIVATE-TOKEN: <token>" \
"https://gitlab.example.com/api/v4/runners" | jq '.[] | {id, name, active, locked}'
# Check CI variables (project level)
curl --header "PRIVATE-TOKEN: <token>" \
"https://gitlab.example.com/api/v4/projects/<id>/variables" | jq '.[] | {key, protected, masked}'
# Review recent pipelines
curl --header "PRIVATE-TOKEN: <token>" \
"https://gitlab.example.com/api/v4/projects/<id>/pipelines?per_page=10" | jq '.[] | {id, status, ref}'
# Check for privileged runners
grep -r "privileged.*=.*true" /etc/gitlab-runner/ 2>/dev/null