Skip to content

Security: farzai/github-runner-railway

Security

SECURITY.md

Security

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.

What a job can see

  • 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 sudo and the docker group 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.

Rules for the operator

  • 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_SCOPES decides what is served: list private repositories one by one, and use an org: 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_target or workflow_run with a checkout of untrusted code on these runners, and keep deployment secrets out of workflows that run on pull_request from 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_RUNNERS and MAX_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 variant

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 pools in one runner project

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 JIT config

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.

Controller

  • 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.

Reporting

Open a private security advisory on this repository.

There aren't any published security advisories