Tenki’s startup program is live: up to $50K in credits and grants.Apply
Runners

Self-Hosted Runners in 2026: ARC, Security, Cost

Eddie Wang
Eddie Wangengineering

Share Article:

Self-hosted runners are GitHub Actions' escape hatch. When hosted runners are too slow, too expensive, or too locked-down, you bring your own machines and register them with GitHub's orchestration layer. The tradeoff is real control for real responsibility: you own the VMs, the network, the patching cadence, and every CVE that lands between update cycles.

This guide covers the architecture from registration tokens through ARC-managed Kubernetes scale sets, walks through a practical security hardening checklist, and maps exactly where managed runner providers like Tenki collapse that operational surface so your platform team doesn't have to carry it.

How Self-Hosted Runner Registration Works

Every self-hosted runner starts with a registration token. You generate one from your repository, org, or enterprise settings, then pass it to the runner binary during ./config.sh. The token is short-lived (one hour), but once registration completes the runner stores a long-lived OAuth credential in a .credentials file on disk. That credential is what lets the listener process maintain a persistent HTTPS long-poll connection to GitHub's Actions service.

The listener idles until GitHub dispatches a Job Available message whose runs-on labels match. At that point the runner binary clones the job workspace, executes steps, streams logs back over HTTPS, and reports final status.

Runners can be scoped at three levels: repository, organization, and enterprise. Repository runners serve a single repo. Org-level runners can process jobs across multiple repos, which reduces machine count but widens the blast radius if one gets compromised. Enterprise runners add another tier that spans organizations.

Ephemeral vs. Persistent Runners

By default, self-hosted runners are persistent. They pick up a job, complete it, and loop back to the listener. The filesystem persists between runs. Caches from previous builds stick around, which can speed up subsequent jobs, but it also means credentials, temp files, and artifacts from one workflow leak into the next. A malicious workflow step can drop a binary into the workspace that a later, more-privileged job unwittingly executes.

Ephemeral runners fix this. Pass --ephemeral during configuration and the runner deregisters itself after completing a single job. The process exits, and your provisioning layer (Terraform, ASG, ARC, whatever) tears down the VM or container and spins up a fresh one. Clean filesystem. No carryover.

The cost is cold-start overhead. Every job spins a new machine and re-pulls dependencies. There's no free lunch here: you either accept the cache risk of persistent runners or pay the latency tax of ephemeral ones.

GitHub also supports just-in-time (JIT) runners via the REST API. Instead of pre-registering a runner and waiting for work, you request a JIT configuration token when a job is queued, boot a fresh machine, pass the token with ./run.sh --jitconfig, and the runner executes exactly one job. This is what ARC uses internally for its ephemeral runner sets.

Actions Runner Controller: Kubernetes-Native Autoscaling

Actions Runner Controller (ARC) is a Kubernetes operator maintained under the actions/actions-runner-controller GitHub org. It replaced the older community-maintained Summerwind controller in 2023 and is now the officially supported path for running self-hosted runners on Kubernetes.

How the Scale Set Architecture Works

ARC ships as two Helm charts. The first deploys the controller manager into your cluster. The second creates an AutoScalingRunnerSet custom resource, which is the actual runner pool.

The flow, simplified:

  1. The controller registers a runner scale set with the GitHub Actions service and obtains a runner group ID.
  2. A listener pod opens an HTTPS long-poll connection to GitHub and waits for job dispatch messages.
  3. When a workflow triggers and the job's runs-on matches the scale set name or labels, the listener receives a Job Available message.
  4. The listener patches the EphemeralRunnerSet with the desired replica count via the Kubernetes API.
  5. The EphemeralRunner controller requests a JIT config token and boots a runner pod. The pod registers, executes the job, and is deleted upon completion.

If a runner pod fails, the controller retries up to five times. If no runner accepts the job within 24 hours, GitHub unassigns it.

What ARC Requires You to Maintain

ARC is powerful, but it shifts the operational surface rather than removing it. You're now responsible for:

  • A production Kubernetes cluster. ARC needs a cluster to run. That cluster needs its own node pools, RBAC policies, ingress rules, monitoring, and upgrade path.
  • Helm chart upgrades. ARC releases new controller and scale-set chart versions regularly. Falling behind means missing bug fixes and security patches to the controller itself.
  • Custom runner images. GitHub publishes a minimal runner container image, but most teams need additional tooling. That means maintaining your own Dockerfile, rebuilding on a schedule, and scanning for vulnerabilities in every layer.
  • GitHub App or PAT credentials. ARC authenticates to GitHub via a GitHub App installation or a personal access token stored as a Kubernetes secret. Either credential needs rotation and monitoring.
  • Scaling configuration. You set min/max replicas, scale-down delays, and pod resource requests. Get these wrong and you either burn cloud spend on idle pods or queue jobs during peak.

Security Hardening Checklist for Self-Hosted Runners

GitHub's own docs are blunt: self-hosted runners "should almost never be used for public repositories." Anyone who can open a pull request against your repo can trigger a workflow on your runner. On a persistent runner that means code execution on a machine that might still hold credentials from a previous job.

Even for private repos, the threat model isn't empty. Anyone with read access can fork the repo and open a PR, potentially compromising the runner environment and accessing secrets. Here's a practical hardening checklist.

1. Use Ephemeral Runners

Non-negotiable. The --ephemeral flag ensures the runner deregisters after one job. Pair it with VM or container teardown so the filesystem is destroyed. Persistent runners in 2026 are a known-bad pattern for any environment running untrusted or semi-trusted code.

2. Scope Runners with Runner Groups

Org-level runners that serve every repo create a massive blast radius. Use runner groups to restrict which repositories and workflows can target which runners. A compromised workflow in a low-trust repo shouldn't be able to land on the same runner pool that handles your production deploys.

3. Restrict Network Access

Runners need outbound HTTPS to GitHub's API endpoints and whatever registries your builds pull from. They don't need access to your production VPC, your cloud provider's metadata service, or the internal network at large. Use network policies (in Kubernetes) or firewall rules (on VMs) to deny everything except the minimum required egress. The cloud metadata endpoint at 169.254.169.254 deserves a specific block rule; it's a well-known privilege escalation vector.

4. Pin Actions to Full-Length Commit SHAs

This applies to all runners, but it's especially critical on self-hosted ones where the blast radius is larger. Tags are mutable. A compromised action repo can re-point v4 to a malicious commit. Pinning to a full SHA makes the reference immutable. GitHub now offers repository and org-level policies to enforce SHA pinning.

5. Minimize GITHUB_TOKEN Permissions

Set the default GITHUB_TOKEN permission to read-only at the org level, then grant write scopes per-job where needed. On a compromised self-hosted runner, a write-scoped token lets an attacker push code directly to your repo.

6. Patch the Runner Binary and Base Image

GitHub's hosted runners get a fresh image every week. Your self-hosted runners don't. The runner binary itself receives automatic updates by default, but the underlying OS, container base image, and any pre-installed tooling only get patched when you patch them. Establish a rebuild cadence (weekly is the minimum for production) and scan images with Trivy or Grype before pushing.

7. Use OIDC Instead of Long-Lived Secrets

If your workflows deploy to AWS, GCP, or Azure, configure OpenID Connect federation so the runner obtains short-lived credentials scoped to the specific workflow run. No static keys sitting in GitHub Secrets for an attacker to exfiltrate.

8. Use CODEOWNERS to Guard Workflow Files

Add .github/workflows/ to your CODEOWNERS file with your platform team as reviewers. Any PR that modifies a workflow file then requires explicit approval before merge. This doesn't prevent exploitation of existing workflows, but it stops new attack vectors from being introduced silently.

The Hidden Operational Surface of Self-Hosting

Security hardening is the checklist everyone thinks about. The slower bleed is operational. Platform teams that self-host runners take on a set of ongoing responsibilities that compound over time.

Runner binary CVEs. The runner application itself is a .NET binary that receives regular security updates. By default it auto-updates, but if you've pinned versions for stability (common in regulated environments), you're carrying the exposure window between the CVE disclosure and your next rollout.

Base image drift. GitHub rebuilds its hosted runner images weekly with updated packages, new tool versions, and patched dependencies. On self-hosted runners, images only change when your team rebuilds them. Drift between what developers expect (the latest ubuntu-latest) and what the runner provides (your last rebuild) creates subtle, hard-to-debug CI failures.

Cloud cost visibility. Self-hosted runners are "free" in GitHub's billing, but the compute isn't free. The EC2 instances, the EKS cluster, the NAT gateway, the EBS volumes, the data transfer charges -- all of it hits your cloud bill as generic compute. Attributing CI cost per-repo or per-team requires custom tagging and a FinOps practice that most teams don't build until spend is already out of control.

On-call burden. When jobs queue because the ARC cluster hit a node limit, or a runner pod is stuck in CrashLoopBackOff, or the listener lost its GitHub connection, someone on your team gets paged. That person needs to understand Kubernetes, the ARC CRDs, and the GitHub Actions API simultaneously. It's a niche skill set that's hard to distribute across a team.

Where Managed Runners Eliminate the Overhead

The reason managed runner providers exist is that most of the self-hosting surface is undifferentiated operational work. Your competitive advantage is your product, not your ability to patch container base images on a weekly cadence.

Tenki's managed runners collapse the self-hosted runner surface point by point:

  • Ephemeral by default. Every Tenki runner operates on an ephemeral VM that's destroyed after the job completes. No persistent filesystem, no credential carryover, no need to configure --ephemeral yourself.
  • Patched centrally. Tenki maintains the runner images and base OS. Runner binary updates, base image patches, and tooling refreshes happen on Tenki's side. Your team doesn't rebuild images or track CVEs in the runner stack.
  • No ARC cluster to maintain. You don't run a Kubernetes cluster for CI. No Helm chart upgrades, no CRD versions to track, no listener pods to monitor. Scaling is handled by Tenki's infrastructure on bare-metal servers, which is also why Tenki runners benchmark 30% faster than GitHub-hosted runners in real-world workflows.
  • Cost visibility built in. At $0.002/core/minute, billing is per-minute and per-workflow. No cloud cost attribution puzzles, no surprise NAT gateway charges. You see exactly what each repo costs.
  • One YAML line to migrate. Swap your runs-on label and you're done. No infrastructure provisioning, no networking changes, no registration tokens to manage.

Tenki also offers a Code Reviewer that operates as a review gate at the merge boundary. For teams that self-host runners partly to control what code ships, this is worth noting: the runner isn't where code quality enforcement should happen. A purpose-built review gate catches issues before CI even runs.

When Self-Hosting Still Makes Sense

Self-hosting isn't always the wrong call. There are legitimate cases where it's the only option:

  • Air-gapped environments. If compliance requires that build artifacts never leave your network, no external runner provider can help.
  • Specialized hardware. GPU builds, FPGA synthesis, or builds that need access to on-premises hardware (like firmware flashing) require physical machines you control.
  • Network-local services. Integration tests that hit databases or services that can't be exposed to the public internet.

For everything else, the question is whether the operational cost of self-hosting is justified by the control it provides. Most teams that run the math discover it isn't. The infrastructure, the on-call rotations, the image maintenance, and the security hardening all cost engineering hours that could go toward shipping product.

A Practical Decision Framework

Before committing to self-hosted runners, run through these questions:

  1. Do you need hardware or network access that no provider offers? If yes, self-host. If no, keep reading.
  2. Is the main driver cost? Calculate the fully-loaded cost of self-hosting (cloud compute, engineering time for maintenance, on-call hours) vs. a managed provider. Managed runners at $0.002/core/minute with no infrastructure overhead frequently come out cheaper.
  3. Is the main driver performance? Managed providers running on bare metal (like Tenki) typically outperform self-hosted runners on shared cloud VMs. You'd need dedicated bare-metal instances to match, which pushes cost even higher.
  4. Do you have the platform team to support it? ARC requires Kubernetes expertise. VM-based fleets require Terraform/Packer expertise. Both require GitHub Actions API knowledge and a patching rotation. If your platform team is already stretched, adding runner infrastructure to their plate is a real risk.

If you're evaluating self-hosted runners because GitHub-hosted runners are too slow or too expensive, start by testing a managed provider instead. The migration is a single runs-on label change, there's no infrastructure to set up, and you can run a side-by-side comparison in an afternoon.

Tags

#cicd-security#fleet-management#arm64-runners#runner-version-enforcement

Recommended for you

What's next in your stack.