> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pipefort.com/llms.txt
> Use this file to discover all available pages before exploring further.

# BEST-PRAC-3 — Self-hosted runner usage

> Self-hosted runners executing untrusted code can pivot into your internal infrastructure.

| Field    | Value         |
| -------- | ------------- |
| Category | `BEST-PRAC-3` |
| Severity | **LOW**       |
| Auto-fix | ✗             |

## What the check does

Flags any job whose `runs-on:` includes the label `self-hosted` — either as a scalar (`runs-on: self-hosted`) or as one element of a sequence (`runs-on: [self-hosted, linux, x64]`).

## Why it matters

Self-hosted runners execute workflow code on infrastructure **you control**. That's fine for private repos with trusted contributors. It's dangerous for repos that accept **pull requests from forks** — by default, a PR triggers workflows on the runner, so an attacker can submit a PR whose CI does anything the runner can reach: scan your VPC, exfiltrate keys mounted on the host, install persistence.

## Severity

This is flagged as **LOW** because self-hosted runners are often an intentional infrastructure choice (compliance, hardware needs, private network access). The check is a reminder to double-check the surrounding controls — not a blanket "don't do this."

## Mitigations

If you keep self-hosted runners:

* **Use ephemeral runners** that destroy themselves after each job — prevents persistence and cross-job contamination.
* **Don't run them on public-PR repos.** Either restrict the runner to private repos, or gate PR triggers on `pull_request` (not `pull_request_target`) plus `if: github.event.pull_request.head.repo.full_name == github.repository` to skip forks.
* **Network-isolate the runner.** Block egress to anything it doesn't strictly need; deny outbound to your VPC.
* **Don't mount host credentials.** No AWS instance profiles, no GCP service accounts on the runner machine itself — fetch them per-job via OIDC.
* **Run in containers.** Each job gets a fresh container; no shared state.

## Why no auto-fix

Migrating off self-hosted to GitHub-hosted (or vice versa) is an infra decision, not a YAML rewrite.
