Runner security model
A runner executes code you wrote (or someone opened a pull request with). Treat it as a place where arbitrary code runs, and design the trust boundaries around that.
Trust boundaries
Section titled “Trust boundaries”| Boundary | Protection |
|---|---|
| Runner to control plane | Outbound HTTPS only. The runner authenticates with a per-runner credential it obtained using the pool registration token. There is no inbound path into your network |
| Control plane to job | A job is a signed, leased assignment. It carries only what that job needs |
| Job to your cloud | Short-lived OIDC token ($SLIPWAY_ID_TOKEN, 15 minutes) used for workload identity federation. No long-lived cloud keys on the runner |
| Job to other jobs | Isolated per executor (below). Workspaces are not shared |
| Tenants | Hosted runners are never shared across orgs. Self-hosted pools are yours alone |
What a job receives
Section titled “What a job receives”- A fresh workspace and environment built from pipeline, job and step
env. - Secrets referenced in
env,withandservices.env, delivered for that job only and masked in log output on the runner before lines are shipped. - If
permissions.id_token: write, an OIDC token whose claims identify org, project, pipeline, run, job, ref, sha, environment, actor and runner pool.permissions:also narrows what the job token can do against the Slipway API (contents,artifacts,secrets,deployments). - Access to its cache and artifacts through the runner, not directly to storage.
Secrets are never placed on the command line of the runner process, never written to the log, and are not available to jobs that did not reference them. Environment-scoped secrets are only delivered to jobs bound to that environment, after protection rules pass.
Isolation by executor
Section titled “Isolation by executor”| Executor | What is isolated | What is not |
|---|---|---|
kubernetes |
Pod per job, optional namespace, service account and node selection per pool; deleted at the end | Shared node kernel (use sandboxed runtimes or dedicated nodes for untrusted code) |
docker |
Container per job on a user-defined network; allow_privileged is off by default; allowed_volumes restricts bind mounts |
Anything the container can reach through a mounted Docker socket (do not mount it into jobs) |
shell |
Workspace directory | Everything else on the host. A job can read other jobs’ files and the runner’s own credentials. Use only on dedicated, disposable machines |
Pull requests from forks
Section titled “Pull requests from forks”Do not run untrusted code on a runner that can reach production or hold secrets. Use a separate pool with a label such as untrusted, no secrets, an isolated network and ephemeral pods, and keep deploy jobs on a different pool.
Hardening checklist
Section titled “Hardening checklist”- Run the runner as an unprivileged user (the packages do; the image runs as a non-root user).
- Use the
kubernetesexecutor for shared or multi-team CI; avoid privileged containers and the Docker socket. - Give each pool the minimum network egress it needs. Block cloud metadata endpoints from job pods unless a job requires them.
- Keep registration tokens out of images and shell history: use
--token-fileor a secret manager. - Use workload identity (OIDC) instead of stored cloud credentials. See OIDC job tokens.
- Scope trust in your cloud: match on
environment,refandrunner_poolclaims, not just the org. - Keep the runner updated. Enable
self_updateor roll the image. - Use ephemeral runners (a new pod per job, or auto-scaled VMs) for anything that handles untrusted input.
- Watch the audit log for new runners, pool token creation and changes to runner pool settings.
Debug sessions
Section titled “Debug sessions”Self-hosted runners can hold a failed job (debug_hold, default 15 minutes) so an authorised user can open a shell inside it. Restrict who has that permission, since it is access to the job’s environment and secrets.