How do I isolate self-hosted CI/CD runners and build an ephemeral, clean-after-each-run environment?
Question
Due to our source-code security policy we can't use cloud runners (GitHub Hosted); we run Self-Hosted Runners on our own VPSes. But a test or a buggy script can damage the server's root dir (`rm -rf /`) or take over the box by manipulating the Docker socket (`/var/run/docker.sock`). How do I architect full isolation and an environment that's wiped after each pipeline (ephemeral) on a self-hosted runner?
Answer
Short answer: a persistent self-hosted runner that runs arbitrary code on a box with root and the real Docker socket is a server takeover waiting to happen. Build on two principles: ephemerality and isolation.
The root of the risk: you’re running code you don’t trust on a fully-privileged, persistent machine.
- Ephemerality: each job runs in a fresh, throwaway environment. Switch the runner to a “register → run one job → deregister” model; each job runs in a single-use VM or container that’s destroyed when it finishes. Nothing persists between jobs, so a poisoned workspace can’t affect the next job.
- Isolation: don’t give the runner host root or the real Docker socket. Run jobs in unprivileged containers (or rootless Docker / a VM per job); drop capabilities, never use
--privileged, and mount nothing sensitive. Handing over/var/run/docker.sockis equivalent to handing over root. - Never run untrusted fork PRs on a self-hosted runner. A PR from a fork means arbitrary code on your infrastructure; put these behind required approval. This is the most overlooked and most dangerous vector.
- Network-segment runners away from prod. Even if a job escapes, it shouldn’t reach prod resources. On tooling: ephemeral runners via Actions Runner Controller on Kubernetes, or autoscaling VMs recreated for each job.
Bottom line: I’d set up ephemeral one-job runners + unprivileged isolation + a fork-PR ban + network segmentation, and I’d treat every job as hostile. The power of a self-hosted runner is control, but the cost is responsibility — I explain how container isolation works and why handing over the socket is handing over root, from the ground up, in the hub Docker post.
Related Reading
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.