# Self-Hosted Runners Security Review Pack

> **Intro:** A self-hosted runner is a controlled remote code execution surface. The repository decides the job; the runner host pays the price. This page turns runner review into a repeatable Product Security assessment for GitHub Actions and GitLab CI.
>
> **What this page includes**
>
> * why self-hosted runners are high-value review targets;
> * GitHub and GitLab specific hardening guidance;
> * ephemeral versus persistent tradeoffs;
> * network, identity, executor, and secret review questions;
> * practical config snippets and review checklists.

## Why self-hosted runners matter

When a workflow or pipeline job runs on a self-hosted runner, repository-defined code can often:

* read runner-local files or caches;
* access cloud credentials or internal networks;
* reuse artifacts from prior jobs;
* attempt persistence on the host;
* pivot into nearby systems.

## What is different from hosted runners

### Hosted model

* ephemeral execution is the default service pattern;
* the platform vendor owns patching and base image lifecycle;
* blast radius tends to be more contained.

### Self-hosted model

* you own host hardening and updates;
* you own job isolation and cleanup;
* you own secrets exposure boundaries;
* you own network segmentation;
* you own the consequences of persistence.

## Core design principles

| Principle               | Practical meaning                                                          |
| ----------------------- | -------------------------------------------------------------------------- |
| ephemeral first         | assume each runner should process one job or a very narrow trust set       |
| no public trust mixing  | never let untrusted pull-request code share a persistent privileged runner |
| separate by trust tier  | build, test, release, and deploy should not all land on the same fleet     |
| narrow network reach    | most runners do not need production or internal admin-plane access         |
| short-lived credentials | prefer OIDC / workload identity or tightly scoped short-lived tokens       |
| observable cleanup      | logs, cleanup, and evidence must survive the runner instance               |

## GitHub Actions review guidance

### Current strongest pattern

Use **ephemeral self-hosted runners** where possible.

### Registration example — ephemeral runner

```bash
./config.sh \
  --url https://github.com/example-org \
  --token "$RUNNER_TOKEN" \
  --ephemeral \
  --labels linux,x64,build,isolated
```

### When disabling auto-update

```bash
./config.sh \
  --url https://github.com/example-org \
  --token "$RUNNER_TOKEN" \
  --ephemeral \
  --disableupdate
```

### GitHub runner hardening checklist

* prefer **ephemeral** runners over persistent ones;
* do not use self-hosted runners for public repos unless containment is unusually strong;
* keep runners out of production networks unless the job type absolutely requires it;
* use **OIDC** to cloud where possible instead of static long-lived secrets;
* keep environment secrets behind review gates where supported;
* forward runner logs externally before runner destruction;
* route jobs by labels and groups, not by hope.

### Example GitHub Actions workflow routed to a dedicated runner group

```yaml
name: release
on:
  push:
    tags:
      - 'v*'
permissions:
  id-token: write
  contents: read
jobs:
  deploy:
    runs-on: [self-hosted, linux, x64, prod-deploy]
    environment: production
    steps:
      - uses: actions/checkout@v4
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-prod-deploy
          aws-region: us-east-1
      - run: ./scripts/deploy.sh
```

## GitLab self-managed runner review guidance

### Executor choices

#### Shell executor

Use only for highly trusted jobs on tightly controlled hosts.

#### Docker executor

Usually safer than shell when **not privileged**, with careful image and volume policy.

#### Kubernetes executor

Good for ephemeral isolation when namespaces, service accounts, node placement, and egress are handled correctly.

#### VM-based isolation

Best for highest-trust or privilege-heavy jobs when ephemeral rebuild is possible.

### GitLab runner `config.toml` — safer Docker baseline

```toml
concurrent = 4
check_interval = 0

[[runners]]
  name = "ci-build-isolated"
  url = "https://gitlab.example.com"
  token = "REDACTED"
  executor = "docker"
  [runners.docker]
    image = "alpine:3.20"
    privileged = false
    pull_policy = "always"
    disable_cache = true
    volumes = ["/cache"]
    shm_size = 0
```

### GitLab runner `config.toml` — privileged exception pattern

```toml
[[runners]]
  name = "image-builder-privileged"
  url = "https://gitlab.example.com"
  token = "REDACTED"
  executor = "docker"
  [runners.docker]
    image = "docker:27"
    privileged = true
    pull_policy = "always"
```

If you must do this:

* isolate the runner onto dedicated ephemeral nodes or VMs;
* route only protected jobs to it;
* do not reuse it for general CI.

### GitLab Kubernetes executor example

```toml
[[runners]]
  name = "k8s-release"
  url = "https://gitlab.example.com"
  token = "REDACTED"
  executor = "kubernetes"
  [runners.kubernetes]
    image = "alpine:3.20"
    namespace = "gitlab-runners-release"
    service_account = "gitlab-runner-release"
    privileged = false
    pull_policy = "always"
    cpu_limit = "1000m"
    memory_limit = "1Gi"
```

## Network segmentation review

For both GitHub and GitLab, ask:

* can the runner reach cloud instance metadata endpoints?
* can it reach production databases or cluster control planes?
* can low-trust jobs reach internal package mirrors or secret stores?
* can one runner talk laterally to another runner subnet?

### AWS IMDS blocking example with iptables

```bash
sudo iptables -A OUTPUT -d 169.254.169.254 -j REJECT
```

## Secrets and identity

### Better patterns

* GitHub Actions OIDC to AWS/Azure/GCP;
* GitLab OIDC / workload federation where available;
* environment-scoped secrets only for trusted refs;
* separate deploy identity from build identity.

### Worse patterns

* long-lived cloud admin keys stored on the runner host;
* broad vault tokens reused for all jobs;
* single shared SSH key for deployment from every runner.

## Persistence and cleanup

### Post-job cleanup example

```bash
#!/usr/bin/env bash
set -euo pipefail
rm -rf "$RUNNER_WORKDIR/_work"/*
docker system prune -af || true
```

Cleanup scripts do not replace ephemeral design. They only reduce residue when persistence exists.

## Top 10 runner security issues

| Issue                               | Why it matters                      | Fix                                                               |
| ----------------------------------- | ----------------------------------- | ----------------------------------------------------------------- |
| shared persistent runners           | cross-job contamination             | ephemeral or trust-tiered isolation                               |
| shell executor on mixed-trust repos | host takeover risk                  | use container or VM isolation                                     |
| privileged Docker by default        | breakout and host compromise        | isolate privileged jobs to dedicated ephemeral fleet              |
| no metadata blocking                | cloud credential theft              | block IMDS / metadata access where unnecessary                    |
| static cloud keys on runner         | secret theft                        | OIDC or short-lived federated credentials                         |
| broad internal network access       | easy lateral movement               | runner subnet isolation and egress allowlists                     |
| stale runner versions               | feature and security drift          | automatic image refresh or disciplined update process             |
| no external logs                    | weak investigations                 | forward logs before destruction                                   |
| fork PRs on self-hosted runners     | trivial hostile code execution path | keep untrusted PRs on hosted or separate low-trust infrastructure |
| shared caches across trust zones    | artifact poisoning and data leakage | separate cache scope and cleanup aggressively                     |

## Review checklist

### Governance

* who can attach repos to this runner fleet?
* who can edit workflow files that route jobs here?
* are protected refs and environments actually enforced?

### Host security

* is the runner host hardened and patched?
* are admin tools, SSH keys, or cloud CLIs present when they are not needed?
* are OS logs and runner logs forwarded externally?

### Executor

* shell, Docker, Kubernetes, or VM?
* is privileged mode enabled?
* are images pinned and pull policies sane?

### Network

* which subnets, metadata endpoints, registries, clusters, and databases can jobs reach?
* can the fleet talk laterally?

### Secrets

* how do jobs obtain cloud access?
* are secrets withheld from low-trust refs?
* is there evidence of secret rotation?

## Cross-links

* [Runner Isolation and Trust Boundaries](/devsecops-cicd-and-supply-chain/index-1/runner-isolation-and-trust-boundaries.md)
* [GitHub Actions for Product Security](/devsecops-cicd-and-supply-chain/index-1/github-actions-for-product-security.md)
* [GitLab System Security Baseline](/devsecops-cicd-and-supply-chain/index-1/gitlab-system-security-baseline.md)
* [Workload Federation and Non-Human Identities](/architecture-api-crypto-and-identity/index-2/workload-federation-and-non-human-identities.md)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.product-security.expert/devsecops-cicd-and-supply-chain/index-1/self-hosted-runners-security-review-pack.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
