Self-Hosted Runner Hardening Workflow
Isolate runners per trust level, run jobs in containers or VMs, restrict network and filesystem access, and treat runners as untrusted infrastructure.
Objective
Harden self-hosted CI runners so a malicious job cannot compromise the host or reach production.
Environment
- Self-hosted runner infrastructure
- Container or VM isolation for jobs
Steps
1. Separate runners by trust level: public,
Separate runners by trust level: public, internal, and production.
Expected state: Untrusted jobs cannot reach trusted networks.
2. Run jobs inside a fresh container
Run jobs inside a fresh container or VM that is destroyed after.
Expected state: Jobs do not persist state on the host.
3. Restrict filesystem and network access
Restrict filesystem and network access to what the build needs.
Expected state: Lateral movement is limited.
4. Treat runner hosts as untrusted
Treat runner hosts as untrusted and avoid long-lived credentials on them.
Expected state: A host compromise is contained.
Decision points
Persistent or ephemeral runners?
- Ephemeral
- Persistent with cleanup
Ephemeral for untrusted jobs; persistent only for controlled internal jobs with strict cleanup.
Allow production access from any runner?
- No, separate production runners
- Yes with guards
No; production access only from dedicated, tightly controlled runners.
Expected outcome
Runners isolated by trust level, jobs ephemeral, network and filesystem restricted, and no long-lived secrets on hosts.
Verification
Confirm a job cannot read the host filesystem beyond its workspace or reach production networks, and that runner hosts rotate regularly.
Cleanup
Record the runner inventory and trust classifications.
Overview
Harden self-hosted CI runners so a malicious job cannot compromise the host or reach production. Confirm a job cannot read the host filesystem beyond its workspace or reach production networks, and that runner hosts rotate regularly.
Steps
- Separate runners by trust level: public, internal, and production. Expected state: Untrusted jobs cannot reach trusted networks.
- Run jobs inside a fresh container or VM that is destroyed after. Expected state: Jobs do not persist state on the host.
- Restrict filesystem and network access to what the build needs. Expected state: Lateral movement is limited.
- Treat runner hosts as untrusted and avoid long-lived credentials on them. Expected state: A host compromise is contained.
# run a job in an ephemeral container with limited mounts
docker run --rm --network none -v "$PWD:/work" -w /work ubuntu:24.04 ./build.sh
Verification
- Runners isolated by trust level, jobs ephemeral, network and filesystem restricted, and no long-lived secrets on hosts.
Decision points
- Persistent or ephemeral runners? Options: Ephemeral; Persistent with cleanup. Recommended: Ephemeral for untrusted jobs; persistent only for controlled internal jobs with strict cleanup.
- Allow production access from any runner? Options: No, separate production runners; Yes with guards. Recommended: No; production access only from dedicated, tightly controlled runners.