Safeguard
Security Guides•

Self-hosted GitHub runners: security risks and safer setups

Self-hosted runners can be persistently compromised by untrusted workflow code. How they differ from hosted runners and how to reduce the risk.

Safeguard Security Team
3 min read

A self-hosted runner is a machine you own that runs workflow jobs. GitHub warns it has no guarantee of a clean, ephemeral VM, so untrusted workflow code can compromise it persistently. Avoid it for public repositories, keep the machine minimal, and use single job runners where you can.

Hosted versus self-hosted

GitHub's documentation says that, except single CPU runners, each GitHub-hosted runner is a new virtual machine. A job starts clean. Single CPU runners run in a container on a shared VM, so the isolation story is different there.

Self-hosted runners are yours to maintain. GitHub's hardening guide states they do not have guarantees about running in ephemeral clean VMs and can be persistently compromised by untrusted code in a workflow.

What persistent compromise means

If job A leaves a modified tool, a planted script or a cached credential on the machine, job B inherits it. Job B might be a release job with access to signing keys or production secrets.

The guide also warns that on a private repository, anyone who can open a pull request may be able to run code on the runner, depending on your workflow triggers.

Risk reducers

From the GitHub guidance and general practice:

  • Do not use them for public repositories. GitHub advises against it.
  • Minimize what is on the box. Keep sensitive data and network access to sensitive services low.
  • Use just-in-time runners. These perform at most one job. GitHub notes hardware reuse can still expose information, so combine with fresh images.
  • Separate runner groups. Put release jobs on dedicated runners that untrusted workflows cannot select.
  • Restrict network egress. A runner that cannot reach the internet freely is harder to abuse.
  • Rebuild from an image on a schedule. Do not let a runner age for months.

Triggers matter as much as hosts

Most runner compromises start with a workflow that runs untrusted code. Review pull_request_target use, as covered in fork safety, and keep fork pull requests off privileged runners.

Environment secrets also do not become safe just because they are scoped. The environments documentation notes that self-hosted runners are not isolated, so treat those secrets like repository secrets.

A quick audit

  1. List every self-hosted runner and which repositories can use it.
  2. Mark which ones handle release or deploy jobs.
  3. Check whether any are reachable by workflows triggered from forks.
  4. Find the oldest runner image. If it is more than a few months old, rebuild.
  5. Write down what credentials exist on each machine. Remove what is not needed.

Safeguard asset discovery can help inventory systems, and the free scan at gold.safeguard.sh/scan covers dependencies in your repositories.

Quick check

Are GitHub-hosted runners always clean per job?

For most sizes, each runner is a new VM. Single CPU runners run in a container on a shared VM, so read the docs for the size you use.

Is a private repository safe for self-hosted runners?

Not automatically. Anyone who can open a pull request that triggers a workflow may reach the runner.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.