Hardening self-hosted GitLab runners
Isolate jobs, scope CI tokens, and pin executors so one poisoned pipeline can't pivot into your whole GitLab runner fleet or leak another team's secrets.
A self-hosted GitLab runner executes whatever a pipeline script contains — including malicious code from a fork merge request or a compromised dependency install step. If runners are privileged, share a Docker socket, reuse dirty workspaces, or hold org-wide deploy tokens, one bad job owns the host and every other project's CI variables. Hardening is isolation and least privilege: treat each job as untrusted code on a machine that also holds secrets.
You will disable privileged mode, never mount /var/run/docker.sock, use Kaniko or Buildah for image builds, scope CI_JOB_TOKEN to the minimum project access, and assign dedicated runners per team instead of one shared fleet. Protected variables, protected branches, and fork MR rules complete the picture in Secure CI/CD with GitLab.
Every checkbox in config.toml is a decision about whether a poisoned pipeline becomes a host takeover.
Do not run privileged
Privileged Docker executors exist mainly for Docker-in-Docker image builds — and they are equivalent to giving every job root on the runner host. Use Kaniko or Buildah inside an unprivileged container to push images instead. If you must use Docker, use a dedicated isolated builder pool with no access to production secrets — not the same runners that deploy prod.
[[runners]]name = "acme-unprivileged"executor = "docker"[runners.docker]privileged = falsedisable_cache = falsevolumes = ["/cache"]# NEVER: "/var/run/docker.sock:/var/run/docker.sock"
Scope the tokens and variables
The job token is short-lived but powerful within GitLab's API — restrict which projects it can reach via CI/CD job token scope settings. Mark deploy credentials as protected so only protected branches and tags receive them. Fork MR pipelines from external contributors should not run deploy jobs at all; use rules: and manual approval for contributions that need CI but not secrets.
grep privileged /etc/gitlab-runner/config.tomlprivileged = truegrep docker.sock /etc/gitlab-runner/config.toml/var/run/docker.sock:/var/run/docker.sockeither finding = treat runner as compromised, rotate all CI secretsSeparate runners by trust zone
Tag runners so production deploy jobs land only on hardened hosts in a locked network segment; build and test jobs run on a general pool with fewer secrets. Auto-scaled runners should ephemeralize disks so one job cannot leave artifacts for the next. Keep runner manager and worker patches current — stale runner versions are a known attack surface.
Where this goes next
Runner isolation lets you enforce SAST, secret scanning, and signed-image gates without fearing that a fork MR steals production kubeconfig. Wire scanning jobs on shared runners and deploy jobs on locked runners with OIDC to your cluster. Secure CI/CD with GitLab covers runners, protected pipelines, environments, and supply-chain controls as one system.
Go deeper in a courseSecure CI/CD with GitLabRunner isolation, scoped tokens, protected pipelines, and scanning gates.View course