GitHub Actions self-hosted runners
Most teams reach for GitHub Actions self-hosted runners because builds need to reach a private database or internal registry, or because the workload needs compute GitHub doesn't sell. The remaining reasons are that compliance requires the execution environment to sit inside a controlled boundary, or that the hosted queue has become the slowest part of the pipeline. Any of those justifies the move.
What doesn't justify it is a vague preference for owning the hardware. If your team has no capacity to own images and patching, hosted runners remain the safer choice. Public repositories are a harder line: GitHub recommends that self-hosted runners rarely be used with them, because untrusted pull request code can compromise the runner. Decide ownership first. The architecture follows from who carries the pager.
Threats to contain
A runner is a machine that downloads code from the internet and executes it with whatever credentials and network reach you gave it. GitHub's own documentation is blunt about what that means: self-hosted runners "can be persistently compromised by untrusted code in a workflow". Self-hosted runners expand the trust boundary because workflow code executes on infrastructure and network paths you control, and every control in this architecture exists to contain that.
The trust boundary is the workflow trigger. A push to main executes reviewed code. A pull_request from a fork executes code nobody has read yet, on the same fleet, unless you separate them deliberately.
Malicious workflow code
A contributor opens a pull request that modifies the workflow file or slips an unsanitized expression into a run block. On a persistent runner, the payload writes itself into a hidden directory and waits. Sysdig's threat research team documented exactly this pattern in the wild, with runner processes launched from paths like ~/.dev-env and runners registered under attacker-chosen names.
From there, the attacker inherits the next job's GITHUB_TOKEN and any secret a later job references. The attacker also inherits any network the host can see. Build outputs are the quieter risk. Code that alters a compiled artifact after tests pass leaves no failing check behind.
Supply chain compromise
In March 2025, an attacker rewrote the version tags of tj-actions/changed-files so that mutable references like @v45 pointed at malicious code that dumped runner memory into workflow logs. The action was used by more than 23,000 repositories, and CISA added the vulnerability to its Known Exploited Vulnerabilities catalog. Repositories that pinned to a commit SHA were unaffected. Forensics later narrowed confirmed secret exposure to 218 repositories, which is small comfort if yours is one of them.
Caches carry the same risk with less visibility. Adnan Khan, the researcher who named the technique, showed that a low-privilege workflow can flood the cache until eviction clears legitimate entries, then re-register those keys with poisoned ones. The release workflow restores them at full speed and publishes the result. On GitHub Actions self-hosted runners, a shared on-disk cache widens that path further.
Internal lateral movement
The reason you self-host is the reason a compromise hurts. A runner with a route to internal subnets and a long-lived cloud key is a beachhead with credentials. Block egress to the cloud metadata endpoint, because a job that reads node credentials inherits the whole node's role.
Scope matters too. When GitHub Actions self-hosted runners are registered at the organization level, GitHub schedules jobs from multiple repositories onto the same runner, so one careless repository defines the blast radius for all of them.
Reference architecture

Separate three paths and the design mostly writes itself. The control path is GitHub talking to your orchestrator. The execution path is the runner doing work in an isolated subnet with allowlisted egress. The observability path ships logs and metrics off the runner to a store the runner cannot write to a second time.
Two principles sit underneath those paths. The first is that compute is ephemeral: one job per runner, then the host is destroyed. GitHub recommends ephemeral runners for autoscaling and discourages persistent runners in autoscaled fleets, so treat this as a baseline requirement rather than a scaling optimization. Second, identity is short-lived and federated, as the next section describes.
Caches and artifacts live in a remote store, scoped by trust level, because a shared disk defeats the point of destroying the host. Runner groups then decide which repositories reach which fleet, and for GitHub Actions self-hosted runners that group boundary is your first and cheapest containment control. The broader cloud DevOps operating model should reinforce those boundaries.