Self-Hosted CI Runners Are Shared Secrets: Threat Modelling Your Build Infrastructure
A self-hosted CI runner is a machine that executes code written by whoever can open a pull request. Teams adopt them for cost, for hardware access or for network reach into private systems. Those same properties make the runner a high-value target, and the threat model is usually written down nowhere.
Why the reader needs this
Managed runners are ephemeral by default and isolated from your network. Self-hosted runners frequently are not. They persist between jobs, they hold deploy credentials, and they can reach internal services that are not exposed to the internet. A single malicious workflow file then becomes a path from "someone can comment on a repository" to "someone can reach production".
Technical context: the trust boundary
Continuous integration systems generally treat the repository as the unit of trust. Anyone who can push a branch, open a fork pull request or modify a workflow file is, in effect, asking the runner to execute their code. Providers have added mitigations, but the fundamental shape is unchanged:
- Workflow definitions are code. A change to
.github/workflows/*.ymlor its equivalent is a change to what runs on your infrastructure. - Fork pull requests execute in the base repository's context in many configurations. Secrets are usually withheld from forks, but the runner's network position is not.
- Caches and artifacts cross job boundaries. A cache written by one job can be read by another that trusts the cache key.
- Runner labels are often guessable. An attacker who can submit a workflow can request a specific runner group. The result is that the runner is a shared resource with a shared secret store, and the isolation between two jobs on the same runner is weaker than most operators assume. ## A concrete failure chain This chain is a composition of publicly documented weaknesses in self-hosted CI, presented as a reasoning model rather than a reproduced incident.
- An attacker opens a pull request against a public repository that uses a persistent self-hosted runner.
- The pull request modifies a workflow file or adds a test that runs during the pull request build.
- Because the runner is persistent, the job can write to paths outside the workspace: shell profiles, the runner's own service configuration, or a directory used by the next job.
- A later job, triggered by a maintainer's merge, executes on the same runner and inherits the modification.
- That job has access to deployment credentials, because release jobs usually do. The attacker never needed a secret in the first pull request. They needed the runner to survive until a privileged job ran on it. ## Defensive implications Make runners ephemeral. A runner that is destroyed after every job cannot carry state into the next one. This is the single highest-value change and it also removes a large class of cache-poisoning and persistence attacks. Separate trust tiers. Untrusted pull request builds and release builds should not share a runner group, a network segment or a credential store. If they must share hardware, use a hypervisor boundary rather than a container boundary. Require approval for first-time contributors. Most platforms support this. It converts an automatic execution into a human decision, which is the correct default for a public repository. Scope credentials to the job. Prefer short-lived tokens issued per job over long-lived deploy keys. A credential that expires in fifteen minutes is far less useful to an attacker who has to wait for a merge. Treat workflow files as privileged changes. Require review from a code owner for any change under the workflow directory, and consider a separate approval for changes to runner configuration. Monitor for persistence. Alert on writes outside the workspace, on new scheduled tasks or services, and on outbound connections from runner hosts to unexpected destinations. These are the observable traces of the chain above. ## Limits of this analysis Ephemeral runners cost money and complicate caching and incremental builds. Network segmentation between build tiers adds operational work. The trade-off is real, and the right answer depends on what the runner can reach. A runner that can only read a public repository is a different risk than a runner that holds a cloud administrator role. The threat model should be written down for each tier rather than assumed. ## References
- GitHub Docs, "Security hardening for GitHub Actions": https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions
- GitHub Docs, "About self-hosted runners": https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners
- GitLab Docs, "Runner security": https://docs.gitlab.com/runner/security/
- OWASP Top 10 CI/CD Security Risks: https://owasp.org/www-project-top-10-ci-cd-security-risks/
Top comments (0)