In GitLab, how do shared, group, and project runners differ, and what does that choice mean for trust and queueing?
answer
- scope answers who may run code here
- instance, group, subgroup-inherited, project-locked
- privileged work never on the shared pool
- protected runners only take protected refs
basics
~20 sShared runners serve every project on the instance, group runners serve one group and its subgroups, and project runners are attached to specific projects. Narrowing the scope narrows who can send code to that machine, at the cost of queueing and idle capacity.
solid answer
~50 sScope is set when the runner is created and decides which projects may schedule jobs on it. **Shared** (instance) runners are available to every project, with a fair-use queue so one busy project cannot starve the rest; projects can also opt out of them. **Group** runners are inherited by every project in that group and its subgroups. **Project** runners are attached to named projects and are locked to them by default. The tradeoff is trust versus utilisation: a shared runner is efficient but any project's `.gitlab-ci.yml` — including a fork's — can run code on it, so it must never be privileged or hold deployment credentials. Dedicated project runners give you a machine only trusted repositories can reach, and can be marked protected (`access_level = ref_protected`) so they only accept jobs from protected branches and tags.
go deeper
Know the three scopes and that a runner's scope decides which projects can send it jobs. Recall that group runners are inherited by projects beneath the group.
Explain that scheduling a job means executing repository code, and therefore why privileged or credential-bearing runners must be scoped and protected rather than shared.
Show how you would tier a fleet: shared disposable capacity for tests, scoped runners for privileged builds, protected runners for deploys, and how fork merge requests force the design.
Own the platform policy — who may create runners at each scope, how authentication tokens are rotated and audited, and how you balance fleet utilisation against per-team isolation across the organization.
## Three scopes, one question: who may send me code? A GitLab runner's scope is chosen when it is created, and it is really an answer to a security question rather than a capacity one. **Instance (shared) runners** are visible to every project on the instance. On GitLab.com these are the hosted runners; self-managed instances usually keep a pool for general CI. Because everyone queues on the same fleet, GitLab applies a fair-use ordering so a monorepo pushing fifty pipelines an hour cannot lock out a small project. Administrators enable them instance-wide; individual projects can disable them if they want to force their own capacity. **Group runners** are created on a group and are usable by every project under it, including subgroups. This is the right unit for a department that wants its own hardware, or a specific toolchain image, without wiring every project by hand. **Project runners** are attached to one or more named projects. By default a project runner is *locked* to the project it was created for; unlocking it lets an owner assign it to additional projects. This is the scope you use for anything sensitive. ## Why scope is a security control A runner executes whatever `.gitlab-ci.yml` tells it to. That file is part of the repository, so the ability to schedule a job on a runner is the ability to run arbitrary code on it. That leads to a few hard rules: - A shared runner must be **disposable and unprivileged**. It should hold no long-lived credentials, no deploy keys, no cloud instance role that grants anything meaningful — because every project, and in public projects every contributor who can trigger a pipeline, can run code on it. - Anything privileged (a Docker-in-Docker image builder) or anything with production reach (a deployment runner) belongs on a **project or group runner**, not the shared pool. - Combine scope with the **protected** setting. A runner marked `ref_protected` only accepts jobs from protected branches and tags, so a feature branch cannot reach the machine that holds deployment credentials. A further control is the runner's `run_untagged` flag: leaving it off on a sensitive runner means jobs must deliberately name its tag, which prevents a stray untagged job from wandering onto it. This is defence in depth, not a boundary — tags are not authorization; scope and protection are. ## Registration has changed How a runner attaches to GitLab is version-dated. The historical flow used a **registration token** copied from the project, group, or admin area and passed to `gitlab-runner register`; anyone with the token could create runners at that scope, and the token was hard to rotate. Since GitLab 16.0 the recommended flow inverts this: you create the runner object in the UI or API first — choosing its scope, tags, protected status and untagged behaviour there — and GitLab returns a **runner authentication token** (the `glrt-` prefixed value) that the agent uses to authenticate. The token identifies a specific runner, so revoking one runner does not invalidate a shared secret, and the runner's configuration lives in GitLab rather than in whoever typed the register command. The legacy registration-token workflow has been deprecated and disabled by default in later versions, so say which you mean when you describe a setup. ## The queueing tradeoff Narrowing scope costs utilisation. A dedicated pair of project runners sits idle most of the day and then becomes a queue at 4pm on release day, while a shared fleet smooths that out across many teams. The usual resolution is tiered: - shared, unprivileged, autoscaled capacity for the bulk of test jobs; - a small group runner pool for teams with a specific hardware or toolchain need; - tiny, protected project runners for privileged builds and production deployments, sized for the release path only. And the fork case decides the top tier: if a project accepts merge requests from forks, the runner that serves them must be assumed hostile-facing, which in practice means shared, ephemeral, and credential-free.
- What does marking a runner protected change?With `access_level = ref_protected`, the runner only accepts jobs running against protected branches and protected tags. It is how you keep a deployment runner — and the credentials on it — unreachable from an ordinary feature branch, since a contributor cannot make an unprotected ref look protected.
- How has GitLab runner registration changed since version 16.0?The old flow passed a scope-wide registration token to `gitlab-runner register`. Since 16.0 you create the runner in the UI or API first, setting its tags, scope and protected status there, and GitLab issues a per-runner authentication token prefixed `glrt-`. Revocation is now per runner rather than invalidating a shared secret, and the legacy flow has been deprecated.
- Why should a shared runner never hold cloud credentials?Because any project on the instance can schedule a job there, and the job runs code from that project's repository. Ambient credentials — an instance role, a mounted key file, an environment variable in `config.toml` — become available to every pipeline on the platform. Credentials belong to jobs on scoped, protected runners, ideally short-lived and obtained per job.
saying these in an interview costs you the question
- Treating shared runners as safe to make privileged
- Assuming group runners must be assigned to each project manually
- Thinking tags restrict which projects can use a runner
- Believing a locked project runner can be borrowed by any project
- Describing registration tokens as the current recommended flow