Understanding the threat model for self-hosted GitHub Actions runners. GitHub-hosted vs self-hosted comparison and secure deployment patterns.
Explains the threat model for self-hosted GitHub Actions runners, comparing risks vs GitHub-hosted. Use when deciding whether to deploy self-hosted runners or to audit their security posture.
/plugin marketplace add adaptive-enforcement-lab/claude-skills/plugin install secure@ael-skillsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
reference.mdscripts/example-1.mermaidSelf-hosted runners put your infrastructure in the execution path. One compromised runner job means lateral movement into your network. Deploy defensively.
The Risk
Self-hosted runners execute untrusted code from pull requests and workflow files. Without proper isolation, a malicious workflow can escape the runner, persist in your network, exfiltrate data from adjacent systems, or pivot to production infrastructure.
GitHub-hosted runners are ephemeral virtual machines that GitHub manages. Self-hosted runners are machines you operate. The security model changes completely:
Reality: Most teams deploy self-hosted runners without hardening, network isolation, or ephemeral job isolation.
flowchart TD
A["Malicious Workflow"] --> B["Self-Hosted Runner"]
B --> C["Attack Surface"]
C --> D["Runner Filesystem"]
C --> E["Network Access"]
C --> F["Cloud Metadata"]
C --> G["Adjacent Systems"]
D --> D1["Persistent Backdoor"]
D --> D2["Credential Theft"]
D --> D3["Code Injection"]
E --> E1["Internal Services"]
E --> E2["Databases"]
E --> E3["Production Systems"]
F --> F1["Cloud IAM Tokens"]
F --> F2["Instance Credentials"]
G --> G1["Lateral Movement"]
G --> G2["Data Exfiltration"]
D1 --> H["Breach Outcomes"]
D2 --> H
E3 --> H
F1 --> H
G2 --> H
H --> I["Production Compromise"]
H --> J["Data Breach"]
H --> K["Supply Chain Attack"]
%% Ghostty Hardcore Theme
style A fill:#f92572,color:#1b1d1e
style B fill:#fd971e,color:#1b1d1e
style C fill:#e6db74,color:#1b1d1e
style H fill:#66d9ef,color:#1b1d1e
style I fill:#f92572,color:#1b1d1e
style J fill:#f92572,color:#1b1d1e
style K fill:#f92572,color:#1b1d1e
Understanding the security trade-offs between GitHub-hosted and self-hosted runners.
| Aspect | GitHub-Hosted | Self-Hosted |
|---|---|---|
| Isolation Model | Ephemeral VM per job | Persistent runner (unless hardened) |
| Network Scope | Internet-only | Access to internal networks |
| Credential Exposure | GITHUB_TOKEN only | Cloud metadata, local creds, adjacent services |
| State Persistence | None (clean VM each job) | Filesystem persists between jobs |
| Security Responsibility | GitHub manages hardening | You manage OS, network, isolation |
| Update Management | GitHub maintains runner software | You maintain OS and runner software |
| Compliance Boundary | GitHub's infrastructure | Your infrastructure and policies |
| Cost Model | Free for public repos, usage-based for private | Infrastructure + management overhead |
| Attack Surface | Minimal (isolated, ephemeral) | High (persistent, networked, adjacent systems) |
Key Takeaway: GitHub-hosted runners are secure by default. Self-hosted runners require deliberate hardening.
Self-hosted runners introduce security risk. Only deploy them when specific requirements justify the operational burden.
Risk Mitigation: Network segmentation, ephemeral runners, minimal network scope
Risk Mitigation: Dedicated runner groups, workload-specific hardening
Risk Mitigation: Dedicated compliance-hardened infrastructure, audit logging
Risk Mitigation: License-dedicated runners with minimal scope
actions/cache or artifact storage instead of filesystem persistenceChoose a deployment model based on security requirements and operational constraints.
Description: Single runner process runs continuously on persistent VM or bare metal.
Characteristics:
Security Risk: High
When to Use: Never for production workloads. Only for isolated internal testing.
Attack Vectors:
Description: Fresh VM provisioned for each job, destroyed after completion.
Characteristics:
Security Risk: Medium
When to Use: Internal workloads requiring network access with strong isolation.
Attack Vectors:
Technologies: Packer for VM images, cloud-init for bootstrapping, Actions Runner Controller (ARC) for orchestration
Description: Fresh container provisioned for each job, destroyed after completion.
Characteristics:
Security Risk: Low (with proper hardening)
When to Use: Production workloads requiring self-hosted execution with strong security posture.
Attack Vectors:
Technologies: Podman for OCI containers, Kubernetes for orchestration, gVisor for container isolation
Timeline:
Impact: Full production compromise. Database exfiltration. Lateral movement across internal network.
Prevention: Ephemeral runners. Each job runs in fresh VM or container. No persistence between jobs.
Timeline:
Impact: Cloud account compromise. Unauthorized resource consumption. Potential data access.
Prevention: Network policies blocking metadata endpoints. Instance Metadata Service v2 (IMDSv2) requiring token headers. Ephemeral runners with minimal IAM permissions.
Timeline:
Impact: Network topology disclosure. Internal service discovery. Reconnaissance for future attacks.
Prevention: Network segmentation. Runner networks isolated from production systems. Egress filtering. Deny-by-default firewall rules.
Never reuse runner state between jobs.
Every job executes in a fresh environment (VM or container). Filesystem, network identity, and credentials start clean. Malicious job cannot plant persistence for future exploitation.
Implementation: Actions Runner Controller (ARC) with ephemeral mode, VM autoscaling groups with per-job lifecycle, container-based runners with destroy-on-completion.
Runners should not have default access to production systems.
Deploy runners in isolated network segments with explicit allow-lists for required internal services. Deny-by-default firewall rules. Egress filtering to prevent exfiltration.
Implementation: VPC/VNet segmentation, subnet-level network policies, deny-all egress with explicit allow rules for GitHub API and package registries.
Runners receive only credentials required for specific jobs.
No ambient credentials. No long-lived tokens. Use OIDC federation to mint short-lived credentials per job. Cloud IAM policies scoped to minimal required permissions.
Implementation: OIDC trust policies with subject claim validation, per-job temporary credentials, metadata endpoint blocking, runner-specific IAM roles.
Every runner action is logged and monitored.
Capture job execution logs, network connections, credential access, and system calls. Alert on anomalous behavior (unusual network destinations, metadata queries, privileged operations).
Implementation: Centralized log aggregation, CloudWatch/Stackdriver for cloud events, auditd for system calls, network flow logs, anomaly detection.
Organize runners by trust level and scope.
Separate runner groups for public repositories (untrusted) vs internal repositories (trusted). Different groups for production vs non-production workloads. Repository access restrictions per group.
Implementation: GitHub runner groups with repository allow-lists, workflow restrictions, required labels for sensitive runners.
Use this checklist when deploying or auditing self-hosted runners.
Timeline:
Impact: Full production compromise. Database exfiltration. Lateral movement across internal network.
Prevention: Ephemeral runners. Each job runs in fresh VM or container. No persistence between jobs.
Timeline:
Impact: Cloud account compromise. Unauthorized resource consumption. Potential data access.
Prevention: Network policies blocking metadata endpoints. Instance Metadata Service v2 (IMDSv2) requiring token headers. Ephemeral runners with minimal IAM permissions.
Timeline:
Impact: Network topology disclosure. Internal service discovery. Reconnaissance for future attacks.
Prevention: Network segmentation. Runner networks isolated from production systems. Egress filtering. Deny-by-default firewall rules.
See the full implementation guide in the source documentation.
Never reuse runner state between jobs.
Every job executes in a fresh environment (VM or container). Filesystem, network identity, and credentials start clean. Malicious job cannot plant persistence for future exploitation.
Implementation: Actions Runner Controller (ARC) with ephemeral mode, VM autoscaling groups with per-job lifecycle, container-based runners with destroy-on-completion.
Runners should not have default access to production systems.
Deploy runners in isolated network segments with explicit allow-lists for required internal services. Deny-by-default firewall rules. Egress filtering to prevent exfiltration.
Implementation: VPC/VNet segmentation, subnet-level network policies, deny-all egress with explicit allow rules for GitHub API and package registries.
Runners receive only credentials required for specific jobs.
No ambient credentials. No long-lived tokens. Use OIDC federation to mint short-lived credentials per job. Cloud IAM policies scoped to minimal required permissions.
Implementation: OIDC trust policies with subject claim validation, per-job temporary credentials, metadata endpoint blocking, runner-specific IAM roles.
Every runner action is logged and monitored.
Capture job execution logs, network connections, credential access, and system calls. Alert on anomalous behavior (unusual network destinations, metadata queries, privileged operations).
Implementation: Centralized log aggregation, CloudWatch/Stackdriver for cloud events, auditd for system calls, network flow logs, anomaly detection.
Organize runners by trust level and scope.
Separate runner groups for public repositories (untrusted) vs internal repositories (trusted). Different groups for production vs non-production workloads. Repository access restrictions per group.
Implementation: GitHub runner groups with repository allow-lists, workflow restrictions, required labels for sensitive runners.
Understanding the security trade-offs between GitHub-hosted and self-hosted runners.
| Aspect | GitHub-Hosted | Self-Hosted |
|---|---|---|
| Isolation Model | Ephemeral VM per job | Persistent runner (unless hardened) |
| Network Scope | Internet-only | Access to internal networks |
| Credential Exposure | GITHUB_TOKEN only | Cloud metadata, local creds, adjacent services |
| State Persistence | None (clean VM each job) | Filesystem persists between jobs |
| Security Responsibility | GitHub manages hardening | You manage OS, network, isolation |
| Update Management | GitHub maintains runner software | You maintain OS and runner software |
| Compliance Boundary | GitHub's infrastructure | Your infrastructure and policies |
| Cost Model | Free for public repos, usage-based for private | Infrastructure + management overhead |
| Attack Surface | Minimal (isolated, ephemeral) | High (persistent, networked, adjacent systems) |
Key Takeaway: GitHub-hosted runners are secure by default. Self-hosted runners require deliberate hardening.
See reference.md for complete documentation.