Why a shared GitLab runner fleet needs a trust model first
A shared runner fleet works when runner scope, tags and protected branches map to trust levels. Before you size anything, decide which code is trusted, which jobs can touch production credentials, and which projects must never share an executor with them. Consider a platform team serving many product teams on one self-managed GitLab instance. Every project can register jobs, and without a deliberate model, a merge request from a small experimental project can run on the same machine that holds a deployment credential for a regulated system. The trust model decides where that boundary sits.
GitLab Runner is the application that executes CI/CD jobs on machines you operate. It is distributed under the MIT license and supports shell, Docker and autoscaling executors across clouds and hypervisors, so the same product can serve a small build machine and a fleet that scales with demand. Runners can be registered at instance, group or project scope, and each runner can carry tags that jobs use to request a specific kind of machine. Those three mechanisms are the controls you will rely on: scope limits which projects can use a runner, tags route workload types to the right hardware, and protected branch settings determine which jobs can receive deployment variables.
Split the fleet when the trust levels differ, not when one team asks for its own machines. Typical reasons include production deployment jobs, untrusted forks or external contributors, builds that need privileged container modes, and workloads with different operating systems or licensing requirements. Confirm with your security team which projects are trusted, and confirm in your own GitLab version how runner scope and protected branch restrictions interact for each runner you plan to register. The detailed fleet layout, executor choices and cache design follow in the sections below.
Choosing instance, group or project runners by trust level
An instance runner is registered once for the whole GitLab installation and is available to every project that allows instance runners. It is the broadest scope on the platform, so it suits only work you would be comfortable running for any team on the instance. Treat it as shared infrastructure owned by the platform team.
- Who can use it: every project on the instance that has instance runners enabled, including projects that teams create on their own.
- Trust level: the lowest. Use it for build tooling your platform team controls, and keep deployments and code from outside your organization off it.
- How it is registered: an administrator registers it for the instance. Confirm the current steps and token flow in the GitLab Runner documentation before you script the process.
- Where it creates risk: a project that runs untrusted code can reach the same manager, host or shared cache that other teams' jobs depend on, so one malicious merge request can affect many projects.
- Practical limit: if you keep the instance scope, pair it with runner tags so that only approved jobs are routed to it.
Before you rely on it, check which users can create projects and whether instance runners are enabled for them by default.
A group runner is registered to one group and is available to the projects inside that group and its subgroups. It fits a business unit or product line whose members share a trust boundary, which makes it the middle scope on the ladder.
- Who can use it: projects in the group and its subgroups, so group membership defines the audience.
- Trust level: medium. It suits a team's own build and test workloads where every maintainer is known and access is reviewed.
- How it is registered: a group owner or maintainer registers it from the group's CI/CD settings. Confirm the required role and steps for your GitLab version in the GitLab Runner documentation.
- Where it creates risk: anyone who can push to any project in the group can change pipeline configuration and run jobs on the shared runner, so a contractor added to one project may reach a runner used by sensitive projects.
- Hierarchy effect: subgroups inherit the runner. This is convenient, but a new subgroup can pick up the runner without a separate review.
Check the membership of the group and every subgroup, then confirm which projects in it handle production credentials.
A project runner is registered to a single project and runs only that project's jobs unless you explicitly share it. It gives the strongest separation between teams, which makes it the usual choice for deployments, signing and other workloads that touch production.
- Who can use it: the one project it is assigned to, plus any other projects you deliberately share it with.
- Trust level: the highest. Use it for protected deployments and sensitive builds where the project's maintainers are the only people who should influence the job.
- How it is registered: a project maintainer or owner registers it in the project's CI/CD settings. Confirm the current steps and whether sharing is available in your version.
- Where it creates risk: the runner is only as trusted as the people who can change
.gitlab-ci.ymland approve merges, so a broad maintainer list weakens the boundary. - Operational cost: each project runner is another host or configuration to patch, monitor and keep on the same version as GitLab, which becomes a burden as the number of projects grows.
Before you dedicate runners, list the maintainers, check whether each runner is shared anywhere and decide who can change protected pipeline settings.
Runner tagging and scoping options compared
This table compares the runner scopes and tagging choices available for a shared fleet, showing what each one isolates, where it fits and what it costs in flexibility or operational effort. Read each row against your trust levels: the more a runner is shared, the more carefully its jobs must be separated from privileged work. Confirm current behavior of each option in the GitLab Runner documentation before you finalize the design.
| Option | What it isolates | Typical use | Main trade-off |
|---|---|---|---|
| Instance runners | Nothing by default. Any project on the instance can use them unless access is restricted by tags or runner settings. | General build and test capacity for many teams with low-risk, similar workloads. | Broadest blast radius. A compromised job or project can reach any shared runner, so keep privileged work off them. |
| Group runners | Access limited to projects within one GitLab group and its subgroups. | A team or business unit with its own deployment or toolchain needs. | Capacity is split by group, so idle capacity in one group does not help another. Confirm how group sharing is configured in your edition. |
| Project runners | Access limited to a single project. | Projects with unusual hardware, secrets exposure concerns or regulated deployment paths. | Highest isolation but the most runners to register, patch and monitor. Usually reserved for a small number of sensitive projects. |
| Protected-only runners | Jobs that run only on protected branches or tags, so unreviewed branches cannot reach the runner. | Production deployments and release jobs that need privileged credentials or environments. | Feature branches cannot test against these runners, so pipelines need a separate path for pre-merge checks. Confirm current protected-branch behavior in the Runner docs. |
| Untagged runners | Nothing specific. Jobs without tags can match any runner that accepts untagged jobs. | Low-risk jobs where any available runner is acceptable. | Hard to reason about under load. Untagged jobs can land on privileged or specialized runners, so many designs disable them. |
| Workload tags (build, test, deploy, large-memory) | Jobs to runners with matching capabilities, such as memory, hardware or network access. | Routing heavy builds to larger machines and deployment jobs to a hardened pool. | Tags are labels, not a security boundary. Pair them with protected-only runners or scope settings when trust levels differ. |
Isolating untrusted projects from privileged jobs
Shared runner fleets fail when untrusted merge requests and privileged deployment jobs run on the same machines. The six cards below are the decisions that separate those workloads, and each one names what to verify before the fleet takes production traffic.
Separate runner managers per trust zone
Each runner manager holds registration credentials and can start jobs on the machines it controls, so one manager shared across trust levels is a shared trust boundary. Decide how many zones you need, typically untrusted project code, internal build work and deployment, and give each zone its own manager and its own registration. Verify that a job from one zone cannot be routed to a runner registered for another, using tags and protected settings that you have tested rather than assumed.
Choose the executor per workload
The executor decides how much of the host a job can reach. Docker, the Kubernetes executor and autoscaling VMs each offer a different isolation boundary, cost profile and operational load. Match the executor to the zone: ephemeral autoscaled VMs or pods for untrusted code, and a tighter, more controlled executor for deployments. Confirm the executor you pick against the workloads you actually run, including any jobs that need the full host or attached devices.
Avoid privileged Docker mode on shared runners
Running jobs with privileged Docker mode gives code inside the container near root access to the host. On a runner that other teams share, any project can use that access to reach other jobs or the manager. Decide which builds truly need to build container images, and move those to a dedicated runner or a rootless build tool. Verify that the shared runner configuration does not set privileged mode for general-purpose tags.
Protect variables and environments for deployment
Deployment credentials should be visible only to jobs running on protected branches or tags, and only to the environments that need them. Mark variables as protected and masked, and use environment scopes so a feature branch cannot reach production secrets. Confirm with a test merge request from an unprotected branch that the deploy job cannot read production values, and review who can change protected branch rules, because that control is only as strong as its approvers.
Avoid sharing the Docker socket
Mounting the host Docker socket into job containers lets any job control the host Docker daemon, which bypasses the isolation the executor is meant to provide. It is a common shortcut for image builds and a frequent source of cross-project exposure. Replace socket mounts with the Docker-in-Docker service pattern on isolated runners, or with a build tool that does not need a daemon. Check the runner config and the job templates in your shared CI library for socket mounts before the fleet opens to more teams.
Use ephemeral executors that discard state
A reused runner keeps files, caches, credentials and Docker layers from earlier jobs, so one project can leave data that a later job from another project reads. Ephemeral executors start each job on a clean machine or container and remove it afterward. Decide what must persist, such as dependency caches, and place it in a shared cache with explicit access rules. Verify that a job on a second run cannot read a file written by a job from another project.
How to build the fleet in sequence
This sequence turns a set of teams and workloads into a runner fleet where each job lands on a runner that matches its trust level, and where deployments can only run from protected branches. Each step closes with a check so the fleet can be verified before teams depend on it.
Inventory teams and workloads
The platform team lists every team that will push code, the projects they own, and the kinds of jobs those projects run, such as unit tests, container builds, infrastructure changes, deployments and platform-specific builds. Include jobs that need Docker, jobs that need a full host, and jobs that touch production credentials. Ask each team lead to confirm the list. The step is done when every active project maps to a team, a workload type and a named owner, and the list has been reviewed by the teams it describes.
Classify trust levels for each workload
Sort the inventory into trust tiers. A common split separates internal, reviewed code from projects that accept external contributions, and separates ordinary build jobs from jobs that hold deployment or cloud credentials. Record which tier may use which runner scope. Security and platform leads sign off on the tiers. The check is a written matrix that states, for each tier, whether jobs may run on shared runners, on group runners or only on dedicated project runners.
Define the tag taxonomy
Agree on a small set of runner tags that describe capability and trust, such as the executor type, the operating system, the presence of a privileged or deployment role, and the workload class. Tags are how jobs select runners, so keep names stable and documented. Avoid tags that encode a team name for shared capacity. The check is a tag reference that platform engineers and project maintainers can read, and a sample
.gitlab-ci.ymlfragment that selects a runner by tag.Create runner managers per zone with authentication tokens
Provision one runner manager per network zone or trust boundary, using the autoscaling or Kubernetes executor your platform choice requires. Register each runner with an authentication token rather than a legacy registration token, so that runner creation can be tracked and rotated. Keep the manager in a zone that matches its trust tier, and keep the manager and runner versions aligned with the GitLab version you run. The check is that each manager appears as online in the GitLab UI with the expected tags and executor, and that the configuration file is under version control.
Register instance, group and project runners
Assign runners to scopes that match the trust matrix. Instance runners serve the lowest-trust, general build workload. Group runners serve teams that share a group and a common toolchain. Project runners serve projects that hold sensitive credentials or need a dedicated environment. Confirm the sharing settings on each scope so that a project cannot pick up a runner it should not use. The check is a test job from one project per scope that shows the job picked up only by the intended runner.
Restrict deployment runners to protected branches and environments
Mark deployment runners as protected so that they only accept jobs from protected branches or tags. Pair this with protected environments and the matching deployment approvals, so that a job cannot reach production credentials from an unreviewed branch. Review which projects are allowed to use these runners and remove any that do not need them. The check is a pipeline on an unprotected branch that fails to start on the deployment runner, and a pipeline on a protected branch that does start.
Verify isolation with an untrusted merge request
Run test pipelines that simulate untrusted code. Open a merge request from a fork or an external-style branch that attempts to read variables, reach the runner host, and select a privileged tag. Record what the job can see and where it runs. Repeat the test for each runner scope and each executor. Ask the security team to review the results before any external contributions are accepted. The check is a documented set of test results showing that untrusted jobs cannot reach protected variables or deployment runners.
Document ownership and operating procedures
Record who owns each runner manager, each scope, each tag and each protected deployment path. Write down how runners are added, rotated, drained and removed, and who approves changes to the trust matrix. Link the procedures to the teams that use the fleet. Confirm that ownership is reflected in the GitLab settings and in the team onboarding guide. The check is a named owner for every runner and tag, and a procedure that a new platform engineer can follow without asking the original builder.
Security and operational considerations for shared runners
A shared runner fleet concentrates risk: one compromised job can reach credentials, networks and other teams' code. The controls below work together, so plan them as one set rather than one at a time. Each card names a decision to make and a check to run before teams start depending on the fleet.
Keep runner versions aligned
GitLab advises keeping the Runner's major and minor versions aligned with the GitLab instance. A fleet that drifts across autoscaled VMs, Kubernetes images and manual installs makes job behavior and security fixes hard to predict. Pin the Runner version in your image or template build, record it per runner, and include Runner upgrades in the same change calendar as GitLab upgrades. Confirm the supported pairing in the Runner documentation before each upgrade.
Limit secrets exposed to jobs
Any variable a job can read is available to every script that runs in it, including dependencies pulled in by a merge request. Scope CI/CD variables to protected branches and protected tags where possible, mark sensitive values as masked, and avoid placing long-lived cloud keys on runner hosts. Prefer short-lived credentials issued per job. Review which runners can see deployment secrets, since that decision decides how far a single compromised job can reach.
Control outbound network access
Shared runners often have broad egress so jobs can pull packages and reach registries. That same access lets a malicious job send source code or secrets to an external host. Decide which destinations each runner pool needs, such as internal package mirrors, container registries and your GitLab instance, and enforce those rules at the network layer rather than in job scripts. Test the rules with a deliberately blocked request before relying on them.
Rotate runner authentication tokens
Runners authenticate to GitLab with tokens. Legacy registration tokens and long-lived runner tokens remain valid until someone revokes them, so a copied config.toml can keep working after a host is retired. Define an owner and a rotation interval for each runner group, store tokens in your secrets manager, and revoke tokens for decommissioned runners. Confirm with your GitLab version which token types it still accepts before you write the rotation runbook.
Log job and runner activity
When a shared fleet is compromised, the first question is which jobs ran, on which runner, with which branch and user. Collect runner service logs and host logs centrally, and retain GitLab job records according to your policy. Check that log forwarding covers autoscaled instances that are destroyed after use, since short-lived hosts take their local logs with them. Define who reviews unusual job patterns, such as unexpected egress or jobs launched by new accounts.
Roll out hardening gradually
GitLab's hardening guidance covers application settings, CI/CD, configuration files, the operating system and alignment with NIST SP 800-53 control families. GitLab describes this guidance as a work in progress to be rolled out gradually. Treat it as a backlog, not a checklist. Apply changes in a staging instance first, record which settings were changed and why, and re-test runner isolation after each change so that a setting does not silently break deployments.
Questions teams ask before designing a shared runner fleet
How much effort does designing a shared runner fleet take?
The effort depends mostly on how many teams share the fleet, how many distinct trust levels they need, and how many workload types must run. A design that separates instance, group and project runners, tags runners by workload, and keeps deployment runners limited to protected branches is a bounded piece of work. Effort rises when teams need different cloud accounts, different network paths to internal systems, or different hardware such as GPUs or macOS builders. Ask any partner for a written scope that lists the trust levels, executors, cloud targets and number of teams covered before agreeing to a figure. Also ask which parts are one-time design work and which recur, such as adding a new team or rotating runner tokens.
Can our own team design a shared runner fleet without outside help?
Yes, if the team already runs GitLab and has engineers who understand Linux, container runtimes and the cloud accounts that will host the runners. The hard parts are less about GitLab configuration and more about trust decisions: which projects can run privileged jobs, who may register project runners, and how deployment credentials stay away from untrusted code. Teams that lack this experience often find the gaps during their first incident rather than during design. An outside partner can review the design, build the first fleet and document the operating model, while your staff keep ownership. Either way, have security sign off on the trust map before any runner receives credentials.
What does the design need from our platform and security teams?
The design needs input from three groups, each with a specific role:
- Platform engineering: cloud accounts, network paths to the GitLab instance and to internal registries, and the autoscaling or Kubernetes target.
- Security: the trust classification of each project, rules for protected branches and environments, and review of who can register runners.
- Identity and access: how GitLab groups map to teams and who approves new runner registrations.
Platform teams also need to decide where runner managers run, because the manager holds credentials that can create cloud instances. Security should review the cache location and the secrets that jobs can read. Agree on these owners before the build starts, since most delays come from waiting on an unassigned decision.
How long does it take to move teams onto new runners?
The migration period depends on how many pipelines reference existing runner tags, how many teams must change their .gitlab-ci.yml files, and whether the old and new runners can run side by side. A common approach is a parallel cutover: new runners get new tags, teams move one group at a time, and the old runners are retired after the last pipeline has moved. Duration is driven by team availability and by how many pipelines depend on deployment credentials, because those need the most testing. Ask for a cutover plan with named checkpoints and a rollback path, and confirm with the vendor how long the old runner version remains supported. GitLab's maintenance policy describes which release lines receive fixes, so check it when you set the retirement date.
Do GitLab pricing tiers change how we design runners?
Runner design itself does not depend on the tier. GitLab Runner is distributed under the MIT license, and the self-managed runner features described in the docs apply to a fleet you operate. The tier matters for the GitLab instance features your pipelines use. The GitLab pricing page lists Free at 0 US dollars, Premium at 29 US dollars per user per month billed annually, and Ultimate as custom priced, with all three available for self-managed installations. The pricing page lists advanced CI/CD and priority support for Premium, and security and compliance features for Ultimate. If your design relies on a specific CI/CD or security capability, confirm that it is included in your tier on the pricing page before you commit the design.
Who should own runner upgrades and version alignment?
Whoever owns the GitLab instance should own runner versions too, because the GitLab documentation advises keeping Runner major and minor versions aligned with GitLab. Each runner fleet adds a separate upgrade stream, so the operating model needs a named owner, a schedule tied to GitLab's monthly and patch releases, and a test runner that receives updates first. Runners on autoscaled cloud instances usually update through a new machine image rather than in place, which means the image build pipeline is part of the ownership question. Ask for a written runbook covering who approves an upgrade, how jobs drain safely, and how a failed rollout is reversed.
What should we check before accepting a runner design?
Check that the design answers four questions in writing: which trust level each project belongs to, which runner types each trust level may use, where deployment credentials are readable, and how a runner is removed when a team leaves. Ask the designer to show the trust map and the tag list, and to explain how an untrusted project is kept away from privileged jobs. Confirm the features the design depends on against the GitLab pricing page and the GitLab Runner documentation, since capabilities differ by tier and version. Finally, ask for a test plan that proves an unprotected branch cannot reach a deployment runner before the fleet opens to teams.
How a deployment job reaches a protected runner
A deployment job reaches a protected runner only when three filters agree: the pipeline is running on a protected branch or tag, the job's tags match a runner that is marked as protected, and the runner's own settings allow it to pick up that job. The protected runner flag is the control that matters most here. When a runner is set to run only protected jobs, GitLab will not offer it work from unprotected branches or merge request pipelines, even if the tags line up. A merge request from a feature branch therefore cannot borrow the deployment credentials sitting on that runner, because the job never reaches it.
The sequence starts when a merge request is merged or a protected branch receives a push. GitLab creates a pipeline, evaluates each job's rules or only conditions, and then matches each job's tags against the runners available to the project. Runners are visible to a project through three scopes (instance, group and project), and a protected runner is only eligible when the ref is protected. Confirm in your own instance how protected branches and protected tags are defined, because a loosely maintained protection list is the most common way a deployment job ends up on a runner it should never see. Check the tags and environment keys in the job definition as well, since a missing tag can silently send a deployment to a general-purpose runner.
Two checks belong in your rollout plan before any production credential goes near the fleet. First, run a deliberate test from an unprotected feature branch that targets the deployment tag and confirm the job stays pending instead of starting. Second, review the runner's config.toml and registration settings to confirm that protected-only mode and locked-to-project settings match what your team expects, since the runner, not the pipeline, enforces them. Keep a record of those test results, because auditors and security reviewers will ask how the separation was verified, and the record also shows which tag names carry deployment authority.
When a partner should design the fleet
An outside team is worth engaging when the runner fleet has to serve several teams with different trust levels, when the deployment sits under regulatory or network constraints, or when you are replacing an existing runner setup that already has jobs, tags and caches depending on it. In each case the design choices are hard to reverse. Moving a project from a shared instance runner to a group runner, or changing which tags gate deployments, touches pipelines owned by many people. A partner earns its place by settling those decisions before the first team is onboarded, and by writing down how each one was decided.
DeployOpen plans, implements and hardens self-hosted GitLab inside your own infrastructure, including your cloud accounts, data centers or air-gapped networks, so the runner managers, executors and cache storage stay under your control. Most engagements are fixed-scope, with scope, deliverables and timelines agreed in writing before work starts. A quote follows from that written scope, so the questions to bring to a partner are specific: which teams and trust levels are in scope, which executors and cloud platforms are involved, whether an existing runner estate must be migrated in parallel, and which compliance evidence the fleet must produce. Ask each bidder to state which of these they will deliver and which they expect you to supply.
Keep your own team in the loop even when a partner leads. The people who will run the fleet after handover should own the trust-tier decisions, the protected-branch rules and the tag catalog, because those rules will change as new projects arrive. Ask for the design record, the configuration as code and a runbook for onboarding a new team, and confirm before signing whether the engagement includes support after go-live or whether that is a separate retainer. Ongoing support, upgrades and continuous testing are available as retainers, so the post-launch arrangement should be priced and scoped separately from the initial build.
How DeployOpen scopes and delivers runner fleet design
DeployOpen plans and implements self-managed GitLab inside infrastructure you control, including the runner fleets that execute CI/CD jobs. For shared runner fleet design, we start from your teams, trust levels and deployment paths, then deliver a documented fleet that your own engineers can operate.
How we scope
- We agree a written, fixed scope that lists the runner scopes, executors and trust zones covered
- We inventory the projects, teams and deployment targets that will share runners, with their trust levels
- We confirm which cloud accounts, data centers or air-gapped networks will host the runner managers and workers
- Ongoing support, upgrades and continuous testing are available as a retainer after handover
How we build
- We configure instance, group and project runners with tags that map to workload types and trust levels
- We restrict deployment jobs to protected branches and separate runners for privileged and untrusted projects
- We verify isolation by testing that untrusted jobs cannot reach privileged runners, secrets or shared caches
- We keep Runner versions aligned with the GitLab version and record each configuration choice
How we hand over
- We document the runner topology, tag catalog, trust boundaries and the reasoning behind each decision
- We provide runbooks for registering, rotating, scaling and retiring runners in your environment
- We walk your platform and security teams through the configuration and the verification results
- We agree the handover evidence list in writing before the engagement starts
How we support
- We review new projects and requests against the trust model and tag catalog you approved
- We plan Runner upgrades alongside GitLab upgrade stops and test them before production rollout
- We can run recurring security checks on runner configuration as part of a retainer
- We help your team confirm changes against current GitLab documentation before each upgrade window

