Free — no signup required

Self-Hosted Runner Security

2 min read

The "Public Fork" Attack

Self-hosted runners are virtual machines or physical servers that you manage and register with GitHub. They are useful for accessing private internal resources, using specialized hardware (GPUs, ARM), or reducing compute costs for high-volume workflows. However, they introduce a critical security risk when attached to public repositories.

The attack scenario, step by step:

  1. You attach a self-hosted runner to your public open-source-app repository. The runner sits inside your corporate network and has access to internal services.
  2. A malicious actor forks your repository.
  3. They modify .github/workflows/ci.yml to add a step: run: curl http://internal-service.corp/admin/dump-data | exfil.sh.
  4. They open a Pull Request against your repository.
  5. By default, the pull_request trigger runs the attacker's modified workflow on your self-hosted runner, inside your corporate network.

The attacker now has arbitrary code execution inside your network perimeter. They can enumerate internal services, exfiltrate secrets from the runner's environment, or pivot to other systems.

The defenses:

  1. Never attach self-hosted runners to public repositories unless the runners are fully ephemeral (a fresh VM is created for each job and destroyed immediately after). Ephemeral runners limit the blast radius because there is no persistent state to steal.

  2. Use pull_request_target with extreme caution. This trigger runs the workflow from the base branch (your trusted code), not the PR branch. It is designed for workflows that need write permissions (like posting a comment on a PR). However, if you add a checkout step that checks out the PR's code and then executes it, you have recreated the vulnerability. The rule: never execute code from an untrusted PR in a pull_request_target workflow.

  3. Use Environment protection rules. Configure a production environment with required reviewers. Any job targeting that environment pauses and waits for a human to approve it. External contributors cannot bypass this gate.

  4. Scope runner labels tightly. Do not use a single runner group for both public and private repositories. Isolate runners by trust level.

The safe pattern for a job that must run on a self-hosted runner and could be reached by an external contributor's workflow is to gate it behind an environment requiring manual approval — the job pauses and waits for a trusted reviewer before it ever touches the runner.

The architectural principle: Treat self-hosted runners like production servers. They have network access, cached credentials, and persistent state. Any code that runs on them should be held to the same trust standard as code you deploy to production.

This is one of 18 chapters

Every chapter is free — Kubernetes, Terraform, SRE, distributed systems, and more — with fast daily review built in.

Sign up free