This fleet is meant for private repositories of one owner or team. Every job is still treated as hostile: a dependency, a compromised account or a fork pull request can run code on a runner.
- The runner container holds only its own JIT config (a one-job registration, useless after the job). It has no GitHub App key, no webhook secret and no Railway token: those live in the controller, which runs in a separate Railway project so a job cannot reach it over the private network.
- The image removes
sudoand thedockergroup from the runner user and drops Railway's own variables before the runner starts. - One container per job: nothing a job leaves behind (processes, files, caches) can reach the next job.
- Private repositories only. Runner services share a Railway private network, so a job from a
public repository (anyone can open a pull request) could reach something a concurrent job started.
Keep public repositories on GitHub-hosted runners.
RUNNER_SCOPESdecides what is served: list private repositories one by one, and use anorg:scope only when every repository the App can see there is private. - In each organization and repository that uses these runners: Settings, Actions, General, "Approval for running fork pull request workflows": Require approval for all outside collaborators.
- Do not use
pull_request_targetorworkflow_runwith a checkout of untrusted code on these runners, and keep deployment secrets out of workflows that run onpull_requestfrom forks. - For an
org:scope, use a dedicated runner group limited to selected repositories with "Allow public repositories" off. - Install the GitHub App only where it is needed: Administration write is broad. Two Apps (for example one per organization) limit what one leaked key reaches.
- Keep nothing else in the runner Railway project, and run the controller in a different project.
- Cap spend with
MAX_RUNNERSandMAX_JOB_MINUTES. Railway's usage limits are per workspace, and its hard limit stops every workload in the workspace, so use a soft limit (alert) unless the workspace holds nothing else. - Pin the images to a version or digest.
The sudo- image lets a job become root inside its own container. Other jobs' containers, the
controller and the host stay out of reach only as far as Railway's container isolation holds, and
root makes an escape attempt easier. A job can rewrite anything in its own container, and if the
platform grants it network capabilities such as CAP_NET_RAW it can send raw packets on the shared
private network. A compromised dependency in a sudo-pool job gets root in that container (which holds
no deploy secrets, only its own JIT config). If your other pools are more sensitive, run the sudo pool
in a separate Railway environment or project. Serve only private repositories with it, give it its
own label and service prefix, and keep the default images (sudo-free) wherever a workflow can do
without it.
Two controllers (for example the plain and the Postgres image) can share the runner project: each lists
and removes only services under its own RUNNER_SERVICE_PREFIX, and only takes jobs that ask for its
label. Their runners share the private network, so both pools must serve the same trust level
(private repositories). The Postgres helper ci-postgres listens on 127.0.0.1 only.
The runner service holds its JIT config as an environment variable and the runner reads it at start,
so a job on that runner can read it (through its own environment or /proc). It registers only that
one runner, which is deleted when the job ends; the impact is limited to the job that already runs
on it. The image also unsets Railway's own variables before the runner starts.
- Verifies every webhook with HMAC SHA-256 and refuses everything when no secret is set.
- Trusts no event for what it says: it only nudges a poll; creation is driven by what GitHub reports.
- Creates runners only for the scopes in
RUNNER_SCOPES, and requires a dedicated runner group (never the Default group) for an org scope. - Never follows an HTTP redirect (the App token and the Railway token must go only where they were pointed) or a pagination link to another host or scheme.
- Never logs the App key, the webhook secret, a JIT config or the Railway token.
Open a private security advisory on this repository.