CoursesSecure CI/CD with GitLabSecuring the pipeline itself

Runner isolation & protected runners

Keep feature branches off production runners.

Advanced14 min · lesson 6 of 17

The most dangerous pipeline scenario is untrusted code on a trusted runner. CI runs the code in a merge request, and if a fork or feature-branch MR executes on the same runner that holds your production deploy credentials, an attacker’s MR can simply print or exfiltrate those secrets. Runner isolation exists to make sure the runner that can reach production never runs code that has not been reviewed.

Segregate runners by trust
1fork / MR jobs
untrusted code
2shared/untrusted runner
no prod secrets
3protected-branch jobs
reviewed code
4protected runner
holds deploy creds
Protected runners only pick up jobs from protected branches/tags — reviewed code — so untrusted MRs never touch production credentials.

Protected runners

A runner can be marked "protected," which means it only runs jobs from protected branches and tags. Pair that with protected variables (only exposed on protected refs) and you get a clean split: untrusted MR pipelines run on ordinary runners with no production secrets, while the runner that holds deploy credentials only ever executes reviewed code from main. A feature-branch job physically cannot land on the production runner.

Ephemeral, isolated execution

Beyond who-runs-where, harden how jobs run. Use the docker or kubernetes executor so each job gets a fresh, disposable container/pod and nothing persists between jobs — a poisoned build cannot leave a backdoor for the next one. Avoid the shell executor (jobs share the host) and privileged mode. On the Kubernetes executor, apply the same pod hardening from the container course: non-root, no privilege escalation, dropped capabilities.

config.toml (Kubernetes executor)
[[runners]]
executor = "kubernetes"
[runners.kubernetes]
privileged = false
[runners.kubernetes.pod_security_context]
run_as_non_root = true
run_as_user = 10001
# build images with Kaniko/BuildKit-rootless, not privileged DinD
Fork pipelines + shared privileged runners = takeover
The classic CI compromise: a shared, privileged runner that also runs pipelines from forks or unreviewed MRs. An attacker opens an MR whose job reads every CI variable it can reach and roots the runner host. Protected runners for protected refs, ordinary isolated runners for everyone else, and never privileged mode on a runner that untrusted code can reach.