Skip to content

Runner Groups and CI Infrastructure at Scale

Lesson 5 of 9Advanced15 min readGit at Scale & Enterprise Engineering · Enterprise OperationsVerified: GitHub Actions runner documentation, September 2026

A runner is a machine that executes whatever a workflow tells it to, on a network, with credentials.

Runner groups are how you decide which workflows get which machines — and therefore which workflows can reach your production database, your internal registry, and your cloud metadata endpoint.

A runner group is a set of runners with an access policy determining which organisations and repositories may use them.

That policy is a security boundary. A runner in a group reachable by every repository is a machine every repository can execute code on.

Hosted runners are managed by GitHub, ephemeral by construction, and have no access to your internal networks.

Self-hosted runners are yours, and their network position is the whole question.

The design principle: a runner should be reachable only by the repositories that need what it can reach.

A workflow job is assigned to a runner, and on that runner it executes with the job’s permissions and the runner’s network access.

Two things determine what a compromised workflow can do: the token permissions, which Actions governance covers, and the runner’s reachability, which this does.

A runner with access to a production network is a production access path. Any repository able to send jobs to it can reach that network, regardless of what that repository is for.

Which makes the access policy the control. A runner group containing machines on the production network, reachable by all repositories, means every repository in the organisation has a path to production.

And it explains the common failure: a runner set up for one team’s deployment, added to the default group because that was the easy option, now reachable by four hundred repositories.

Group by what the runner can reach, not by which team owns it.

A default group with no special access. Runners that can reach the internet and nothing internal. Most jobs belong here, and it is safe for it to be broadly reachable.

A group per sensitive network zone. Runners with access to production, to a particular internal service, or to a restricted environment — each in its own group, reachable only by the repositories that deploy there.

A group for large or specialised runners. More memory, more CPU, a GPU, a specific architecture. Reachability here is a cost control rather than a security one, though it is still worth restricting.

Avoid a group per team. It duplicates the ownership model that teams and repository permissions already express, and it produces dozens of groups whose distinctions nobody remembers.

The test for a group’s existence: what can these runners reach that the default group cannot? If the answer is nothing, the group is organisational rather than a boundary, and it probably should not exist.

Runner groups exist at two levels, and the choice affects both reach and administration.

Enterprise-level runner groups can be shared across organisations. Their access policy selects which organisations may use them, and then which repositories within those organisations.

Organisation-level runner groups are scoped to one organisation.

Enterprise-level is the right home for shared infrastructure — a pool of large runners, or a set of runners with access to a shared internal service. One pool, one policy, one set of machines to maintain.

Organisation-level suits infrastructure a single organisation owns and nobody else should reach.

The administration question follows the level. An enterprise-level group is managed by enterprise administrators, which means a team needing a change raises a request. An organisation-level group can be managed by organisation owners, which is faster and less controlled.

Prefer enterprise-level for anything with privileged network access. The slower change process is a feature when the group is a production access path.

And prefer as few groups as the boundaries require, at whichever level. Groups proliferate the same way rulesets and roles do, and for the same reason: each new one is individually justified.

HostedSelf-hosted
Managed byGitHubYou
LifecycleEphemeralYours to decide
Internal network accessNoYes, by design
PatchingGitHubYou
Cost modelMetered minutesYour infrastructure
Repository cachingNoPossible
Safe for public repositoriesYesNo

Prefer hosted where it works. No infrastructure, no patching, ephemeral by construction, and no internal network exposure. The cost is metered minutes, and for most workloads that is cheaper than the engineering time to run the alternative.

Self-hosted is warranted when you need internal network access, specific hardware, a persistent repository cache for a very large monorepo, or a cost profile that metered minutes does not fit.

And it comes with obligations: patching, monitoring, isolation, lifecycle management, and the network design above.

A runner that is destroyed after each job carries nothing between jobs.

Which matters because a persistent runner accumulates state: cached credentials, files written by a previous job, a modified environment, and anything a compromised job deliberately left behind.

A persistent runner shared between teams is a lateral movement path. Team A’s job leaves something on the machine; team B’s job runs next.

Ephemeral is the correct default for self-hosted runners, and modern runner orchestration — running each job in a fresh container or virtual machine — makes it practical.

The trade-off is the repository cache. An ephemeral runner starts with no clone, which is exactly the cost that CI clone optimisation addresses. The resolution is usually ephemeral job environments with a shared read-only local Git mirror — isolation preserved, transfer cost reduced.

If you must run persistent runners, reset the workspace hard between jobs and keep the group narrow.

Groups control access. Labels control selection, and confusing the two produces a security model that does not hold.

A job requests runners by label. A runs-on value naming a label routes the job to a runner carrying it.

Labels are not a security boundary. Anybody who can write a workflow in a repository that can reach a group can request any label within it. A label named production does not restrict anything; it selects.

The boundary is the group’s access policy, always. If a repository should not reach a runner, that must be expressed in the group, not by hoping nobody uses the label.

Which means labels should describe capability, not permission. linux-x64, gpu, large are useful. production-only is misleading, because it implies a restriction the mechanism does not provide.

Keep the label vocabulary small and documented. An estate with forty ad-hoc labels has jobs queuing forever because they request a label nothing carries — a failure mode that presents as a hung job with no error.

Watch for jobs waiting on labels. A job that never starts is usually requesting a combination no runner satisfies, and it will wait indefinitely rather than failing.

Runner group plus label is the full addressing model. The group says who may ask; the label says what they get. Design both, and never rely on the second for the first.

Concretely, what does a compromised job on a shared runner reach?

The filesystem, including anything a previous job left. Credentials written to disk, cached artifacts, checked-out source from another team’s repository.

The runner’s process environment, potentially including configuration.

The network, from the runner’s position. Internal services, databases, other runners.

The cloud metadata endpoint, if the runner is a cloud instance — which may yield the instance’s role credentials, and those are frequently broader than any workflow’s.

Other jobs, if the runner executes several concurrently.

The runner’s own credentials, used to communicate with GitHub.

Which is a substantial list, and it is the argument for ephemerality and for narrow groups stated concretely rather than in principle.

Block the metadata endpoint where the runner does not need it. This is a specific, high-value hardening step that is frequently overlooked: a workflow that can query instance metadata may obtain credentials nobody intended it to have.

Run each job in its own container or virtual machine, destroyed afterwards. This addresses most of the list at once, and it is what modern runner orchestration provides by default.

Runner registration is a privileged action. Somebody able to register a runner into a group can receive jobs intended for that group, including their secrets.

Restrict who can register. Registration tokens should be treated as credentials — short-lived, scoped, and not shared.

Inventory what exists. Self-hosted runners accumulate, and an estate typically has runners registered years ago for projects that ended, still listening, still unpatched.

Monitor for offline runners that are still registered. Either they should be removed, or something is broken.

Monitor for new registrations. Low volume, high consequence, and worth alerting on.

Patch them. A long-lived runner is a long-lived machine, and it needs the same patching regime as any other server. This obligation is the one most often forgotten, because runners do not feel like servers.

Most organisations inherit runner infrastructure rather than designing it. The audit that establishes the position:

List every runner group, at both levels, with its access policy.

For each group, count the repositories that can reach it. The number for any group with privileged network access is the finding.

For each self-hosted runner, establish what it can reach. Not what it is documented to reach — what its network position actually permits. This frequently requires talking to whoever runs the network.

Find runners reachable by public repositories. This is the severe finding, and it should be resolved the same day.

Find runners nobody claims. Registered years ago, still online, still receiving jobs, unpatched. Every estate has some.

Find persistent runners shared between teams. Lateral movement paths.

Check whether the metadata endpoint is reachable from runners on cloud instances.

Check patch levels. Both the runner software and the host.

Rank by reachability times exposure. A runner that can reach production and is reachable by four hundred repositories is the first item, regardless of anything else on the list.

Expect the audit to take longer than the remediation. Establishing what a machine can reach is genuinely harder than changing a group policy, and it is the part that requires cooperation from outside the platform team.

Runner demand is spiky. Idle overnight, saturated at midday, and a fixed pool is either wasteful or a bottleneck.

Autoscaling addresses both, creating runners on demand and destroying them afterwards — which also delivers ephemerality naturally.

The considerations:

Scale-up latency. A job waiting for a runner to be provisioned is a job that is slower than one on a warm pool. A small warm pool plus autoscaling above it is the usual compromise.

Cost visibility. Autoscaled infrastructure costs vary with usage, and the bill is a different shape from metered minutes. Attribute it.

Capacity limits. An autoscaler with no upper bound is an outage waiting for a runaway workflow.

Image management. Each runner starts from an image, and that image needs building, patching and versioning like any other.

Which is a real engineering commitment. Self-hosted autoscaled runners are a platform to operate, and the decision to run them should account for that rather than treating it as a configuration exercise.

The observation that resolves most runner-group security problems: frequently, no runner needs production network access at all.

OIDC removes the credential problem. A workflow can exchange its identity for short-lived cloud credentials, authenticating to a cloud API over the internet rather than needing a network path.

Which means a hosted runner can deploy to production, because it is calling a cloud API, not reaching into a private network.

The cases that genuinely need a network path are narrower than they appear: a database migration against a private database, a deployment to infrastructure with no public API, an internal artifact registry.

And several of those have alternatives. A migration can be run by a job inside the target environment, triggered by CI, rather than by CI reaching in. A private registry can be reachable over an authenticated public endpoint.

Which reframes the design. Instead of “how do we secure runners with production access”, ask “which of these actually need it”. The answer is usually a small number, and securing three runner groups is far more tractable than securing thirty.

This is the highest-leverage change available in CI infrastructure security, and it is usually framed as a cloud authentication improvement rather than a runner one. Both are true.

If you run them, this is the operational surface you have taken on.

Patching. The runner host’s operating system, the runner software, and the tools in the image. A runner is a server.

Image lifecycle. Runners start from an image; that image needs building, testing, versioning and rebuilding as its contents age. This is a pipeline of its own.

Capacity monitoring. Queue depth and wait times. A saturated pool presents as slow CI, and developers will attribute the slowness to their own builds.

Cost attribution. Infrastructure cost by group, so the teams generating demand see it.

Health monitoring. Runners that go offline, runners that accept a job and stall, runners that consistently fail.

Disk management. Long-lived runners fill up with build artifacts and container images, and the failure mode is jobs failing for reasons that look unrelated.

Security monitoring. Registration events, unexpected network connections, processes that outlive their job.

None of this is exotic, and all of it is work. The honest comparison against hosted runners includes it, and a team choosing self-hosted for cost reasons should price the operational burden alongside the infrastructure.

Public repositories able to use self-hosted runners. Arbitrary code execution on your network by anybody.

One default group containing everything. Every repository can reach every runner’s network.

Grouping by team rather than by reachability. The group stops being a boundary.

Persistent runners shared between teams. A lateral movement path.

No runner inventory. Machines listening for jobs that nobody remembers.

Unpatched runners. They are servers and they are treated as configuration.

Registration tokens treated casually. They grant the ability to receive jobs and secrets.

Autoscaling with no upper bound. A runaway workflow becomes an infrastructure incident.

Self-hosted by default. Hosted runners are simpler, safer and frequently cheaper in total.

Between standard hosted runners and self-hosted infrastructure sits an option that resolves many self-hosted decisions.

GitHub offers hosted runners with more resources than the standard configuration — more CPU, more memory, and other configurations depending on availability.

Which addresses the most common non-security reason for going self-hosted: a build that does not fit in the standard runner.

They remain ephemeral and managed. No patching, no image pipeline, no capacity monitoring, and no internal network exposure.

Access to them is governed by runner groups, in the same way as self-hosted runners, which makes cost control possible — a group containing expensive runners, reachable only by the repositories that need them.

Check current availability, configurations and pricing rather than assuming; the offerings change.

The decision to evaluate: if the reason for self-hosted runners is capacity rather than network access, larger hosted runners may remove the requirement entirely — and with it the operational burden described above.

Where network access is genuinely required, self-hosted remains the answer, and the group design above is how you contain it.

An enterprise with four hundred repositories, deployments to a cloud, and one legacy system behind a private network.

Default group: hosted runners, all repositories. Standard configuration. Every build, test and lint job runs here. No internal network access, ephemeral, managed by GitHub. This covers the overwhelming majority of jobs.

Large group: larger hosted runners, restricted repositories. For the handful of builds that genuinely need more resources. Restricted for cost rather than security, and the restriction is what keeps the bill predictable.

Cloud deployment: no special group. Deployments authenticate via OIDC to the cloud API from the default group. No runner needs privileged network position, and this is where the design earns its simplicity.

Legacy group: self-hosted, ephemeral, three repositories. Runners with access to the private network for the one legacy system. Ephemeral job containers, metadata endpoint blocked, patched by the platform team, and reachable by exactly the three repositories that deploy that system.

Public repositories: hosted only, and explicitly excluded from every self-hosted group.

Four groups total, of which one is self-hosted and it is narrowly scoped. The operational burden is proportionate, the security boundary is stated, and every group’s existence has a one-sentence justification.

The instinct that this must be more complicated is worth resisting. Complexity in runner infrastructure is almost always a symptom of self-hosting things that did not need to be self-hosted.

A runner group answers “which repositories may execute code on these machines”. Because a machine’s value to an attacker is what it can reach, the group must be designed around network position rather than ownership. A runner that can reach production, reachable by every repository, means every repository can reach production.

  • A runner group’s access policy determines which organisations and repositories may use its runners, and is a security boundary
  • Self-hosted runners must never be reachable by public repositories
  • Group runners by what they can reach, not by which team owns them
  • Hosted runners are ephemeral and have no internal network access; prefer them where they work
  • Ephemeral self-hosted runners prevent state — including anything deliberately left — from crossing between jobs
  • Registration is privileged, and registration tokens are credentials
  • Self-hosted runners are servers requiring inventory, monitoring and patching
  • Autoscaling delivers ephemerality naturally and introduces latency, cost and capacity considerations
  • A group that grants no additional reachability is organisational rather than a boundary

Use an organisation with self-hosted runners, or reason from documentation.

  1. List every runner group and its access policy. Predict: how many repositories can reach the broadest group?

  2. For each group, state what its runners can reach that hosted runners cannot.

  3. Find any group reachable by a public repository. Predict: does one exist?

  4. List every self-hosted runner with its last-seen time. Find the oldest registration.

  5. Determine whether runners are ephemeral or persistent.

  6. Find out who can register a runner, and how registration tokens are handled.

  7. For one production-adjacent runner group, list the repositories that can use it. Justify each.

  8. Design the group structure you would build from scratch, by reachability.

Engineering Team Onboarding SystemA 30-day Git and GitHub programme with standards templates, assessments and governance checklists.