Disposable runner patterns for GitHub Actions. Container-based, VM-based, and ARC deployment strategies with complete state isolation between jobs.
Provides disposable GitHub Actions runners that self-destruct after each job, preventing backdoors and credential theft. Use when workflows need complete state isolation or you're replacing persistent runners that retain malicious modifications between jobs.
/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.
examples.mdreference.mdscripts/example-1.shscripts/example-2.shscripts/example-3.iniscripts/example-4.shscripts/example-5.shscripts/example-6.shscripts/example-7.jsonscripts/example-8.yamlPersistent runners are persistence vectors. Deploy disposable infrastructure instead.
The Goal
Every job executes in a fresh environment. Malicious workflows cannot plant backdoors because the execution environment is destroyed after completion. State isolation prevents cross-job contamination.
See the full implementation guide in the source documentation.
Persistent runners retain state between jobs. One compromised workflow means every subsequent job inherits the malicious modifications.
Ephemeral Benefits:
Persistent Runner Risks:
Choose based on security requirements, provisioning speed, and infrastructure constraints.
| Model | Isolation Level | Provisioning Time | Security Risk | Best For |
|---|---|---|---|---|
| Container | Process + Network | 5-30 seconds | Low | Production workloads with frequent job execution |
| VM | Full virtualization | 30-120 seconds | Very Low | High-security workloads requiring hardware isolation |
| ARC (Kubernetes) | Pod + Node isolation | 10-60 seconds | Low-Medium | Organizations with existing Kubernetes infrastructure |
Fresh container per job. Fast provisioning, minimal attack surface, strong isolation with gVisor.
Rootless containers with automatic cleanup.
#!/bin/bash
# /opt/runner-orchestrator/run-ephemeral-job.sh
# Ephemeral runner using Podman rootless containers
set -euo pipefail
RUNNER_VERSION="2.311.0"
RUNNER_IMAGE="ghcr.io/actions/runner:${RUNNER_VERSION}"
RUNNER_TOKEN="${1:?Runner registration token required}"
RUNNER_NAME="ephemeral-$(date +%s)-$(openssl rand -hex 4)"
RUNNER_LABELS="self-hosted,ephemeral,container"
echo "==> Starting ephemeral runner: ${RUNNER_NAME}"
# Pull latest runner image
podman pull "${RUNNER_IMAGE}"
# Run container with strict isolation
podman run \
--rm \
--name "${RUNNER_NAME}" \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,nodev,size=2G \
--tmpfs /opt/runner/_work:rw,noexec,nosuid,nodev,size=8G \
--security-opt no-new-privileges=true \
--security-opt label=type:runner_t \
--cap-drop ALL \
--network slirp4netns:allow_host_loopback=false \
--env RUNNER_TOKEN="${RUNNER_TOKEN}" \
--env RUNNER_NAME="${RUNNER_NAME}" \
--env RUNNER_LABELS="${RUNNER_LABELS}" \
--env RUNNER_EPHEMERAL=true \
"${RUNNER_IMAGE}"
echo "==> Runner ${RUNNER_NAME} completed and destroyed"
Security Features:
--read-only: Immutable root filesystem prevents persistent modifications--tmpfs: Temporary writable storage with noexec to block malicious binaries--security-opt no-new-privileges: Prevents privilege escalation--cap-drop ALL: Removes all Linux capabilities--network slirp4netns: User-mode networking without host network accessRUNNER_EPHEMERAL=true: Runner deregisters after single jobEnhanced container isolation using gVisor user-space kernel.
#!/bin/bash
# Ephemeral runner with gVisor container runtime
set -euo pipefail
# Requires gVisor runsc runtime configured
# See: https://gvisor.dev/docs/user_guide/install/
RUNNER_VERSION="2.311.0"
RUNNER_IMAGE="ghcr.io/actions/runner:${RUNNER_VERSION}"
RUNNER_TOKEN="${1:?Runner registration token required}"
RUNNER_NAME="gvisor-ephemeral-$(date +%s)-$(openssl rand -hex 4)"
echo "==> Starting gVisor-isolated runner: ${RUNNER_NAME}"
podman run \
--rm \
--runtime /usr/local/bin/runsc \
--name "${RUNNER_NAME}" \
--read-only \
--tmpfs /tmp:rw,size=2G \
--tmpfs /opt/runner/_work:rw,size=8G \
--security-opt no-new-privileges=true \
--cap-drop ALL \
--network slirp4netns \
--env RUNNER_TOKEN="${RUNNER_TOKEN}" \
--env RUNNER_NAME="${RUNNER_NAME}" \
--env RUNNER_EPHEMERAL=true \
"${RUNNER_IMAGE}"
gVisor Benefits:
Automatic provisioning on boot with systemd unit.
# /etc/systemd/system/github-runner-ephemeral@.service
# Systemd template for ephemeral container runners
[Unit]
Description=GitHub Actions Ephemeral Runner (Container %i)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=github-runner
Environment=RUNNER_VERSION=2.311.0
Environment=RUNNER_IMAGE=ghcr.io/actions/runner:${RUNNER_VERSION}
Environment=RUNNER_TOKEN_FILE=/etc/github-runner/token
ExecStartPre=/usr/bin/podman pull ${RUNNER_IMAGE}
ExecStart=/opt/runner-orchestrator/run-ephemeral-job.sh $(cat ${RUNNER_TOKEN_FILE})
Restart=always
RestartSec=10
TimeoutStopSec=30
# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadOnlyPaths=/
ReadWritePaths=/opt/github-runner
[Install]
WantedBy=multi-user.target
# Enable multiple concurrent ephemeral runners
systemctl enable github-runner-ephemeral@{1..5}.service
systemctl start github-runner-ephemeral@{1..5}.service
Full VM per job. Strongest isolation, slower provisioning, higher resource overhead.
Provision fresh VM for each job using cloud autoscaling.
#!/bin/bash
# Create GCP instance template for ephemeral runners
set -euo pipefail
PROJECT_ID="my-gcp-project"
REGION="us-central1"
ZONE="${REGION}-a"
TEMPLATE_NAME="github-runner-ephemeral-$(date +%Y%m%d-%H%M%S)"
SERVICE_ACCOUNT="github-runner@${PROJECT_ID}.iam.gserviceaccount.com"
# Create instance template with startup script
gcloud compute instance-templates create "${TEMPLATE_NAME}" \
--project="${PROJECT_ID}" \
--machine-type=e2-medium \
--image-family=ubuntu-2204-lts \
--image-project=ubuntu-os-cloud \
--boot-disk-size=20GB \
--boot-disk-type=pd-standard \
--service-account="${SERVICE_ACCOUNT}" \
--scopes=cloud-platform \
--metadata=enable-oslogin=TRUE \
--metadata-from-file=startup-script=/opt/runner-orchestrator/vm-startup.sh \
--tags=github-runner,ephemeral \
--network-interface=network=default,no-address
# Create managed instance group with autoscaling
gcloud compute instance-groups managed create github-runners-ephemeral \
--project="${PROJECT_ID}" \
--base-instance-name=runner \
--template="${TEMPLATE_NAME}" \
--size=0 \
--zone="${ZONE}"
# Configure autoscaling based on job queue
gcloud compute instance-groups managed set-autoscaling github-runners-ephemeral \
--project="${PROJECT_ID}" \
--zone="${ZONE}" \
--min-num-replicas=0 \
--max-num-replicas=10 \
--cool-down-period=60 \
--mode=on \
--scale-based-on-cpu \
--target-cpu-utilization=0.6
#!/bin/bash
# /opt/runner-orchestrator/vm-startup.sh
# GCP VM startup script for ephemeral runner
set -euo pipefail
echo "==> Configuring ephemeral runner VM"
# Install runner
mkdir -p /opt/actions-runner && cd /opt/actions-runner
curl -o actions-runner-linux-x64-2.311.0.tar.gz \
-L https://github.com/actions/runner/releases/download/v2.311.0/actions-runner-linux-x64-2.311.0.tar.gz
tar xzf actions-runner-linux-x64-2.311.0.tar.gz
rm actions-runner-linux-x64-2.311.0.tar.gz
# Fetch registration token from Secret Manager
RUNNER_TOKEN=$(gcloud secrets versions access latest --secret=github-runner-token)
RUNNER_NAME="vm-ephemeral-$(hostname)-$(date +%s)"
RUNNER_LABELS="self-hosted,ephemeral,vm,gcp"
# Register runner (ephemeral mode)
./config.sh \
--url https://github.com/my-org/my-repo \
--token "${RUNNER_TOKEN}" \
--name "${RUNNER_NAME}" \
--labels "${RUNNER_LABELS}" \
--ephemeral \
--unattended
# Run single job
./run.sh
# Self-destruct after job completion
echo "==> Job complete, destroying VM"
gcloud compute instances delete "$(hostname)" --zone="$(gcloud compute instances list --filter="name=$(hostname)" --format="value(zone)")" --quiet
Pre-baked VM image with security hardening applied.
{
"builders": [
{
"type": "googlecompute",
"project_id": "my-gcp-project",
"source_image_family": "ubuntu-2204-lts",
"zone": "us-central1-a",
"image_name": "github-runner-hardened-{{timestamp}}",
"image_family": "github-runner-hardened",
"ssh_username": "packer",
"machine_type": "e2-medium",
"disk_size": 20
}
],
"provisioners": [
{
"type": "shell",
"script": "scripts/hardening/os-baseline.sh"
},
{
"type": "shell",
"script": "scripts/hardening/cis-benchmarks.sh"
},
{
"type": "shell",
"script": "scripts/hardening/firewall-rules.sh"
},
{
"type": "shell",
"script": "scripts/install-runner.sh"
},
{
"type": "shell",
"inline": [
"echo 'Hardened runner image build complete'",
"echo 'Image includes: OS hardening, firewall, audit logging, runner software'",
"echo 'Startup script will configure ephemeral mode at boot'"
]
}
]
}
Kubernetes-native runner orchestration with pod-level isolation.
Deploy ARC controller to Kubernetes cluster.
# arc-controller-install.yml
# Install Actions Runner Controller using Helm
*See [reference.md](reference.md) for additional techniques and detailed examples.*
## Examples
See [examples.md](examples.md) for code examples.
## Full Reference
See [reference.md](reference.md) for complete documentation.
## References
- [Source Documentation](https://adaptive-enforcement-lab.com/secure/github-actions-security/)
- [AEL Secure](https://adaptive-enforcement-lab.com/secure/)