Skip to content
Select themeSelect language

Runner-fleet security model

A runner executes real agent work with real credentials, so its security model is deliberate. This page explains the guarantees behind Set up a runner securely so you can reason about what a runner can and cannot reach.

A runner only ever connects out to the hub over HTTPS; the hub never connects in. There is no inbound port to expose, no broker to operate, and the trust boundary is one direction. A runner asks for work only when it has free capacity, which is also a natural backpressure mechanism.

Each runner authenticates with its own bearer token (scrn_…): 256 bits of CSPRNG entropy, stored only as a SHA-256 hash server-side. Rotation (POST /api/runners/{id}/rotate-token) mints a fresh token and invalidates the old one immediately — rotation is revocation. Tokens are returned exactly once, at registration or rotation, and never retrievable again.

The runner refuses a plaintext http:// hub URL at boot. A token and the per-run secret bundle must never cross a cleartext link. The only way to allow http:// is to set SUPACLOUD_RUNNER_ALLOW_INSECURE_TRANSPORT=true explicitly — acceptable solely on a trusted private network you fully control.

The runner holds no copy of the hub database and no long-lived platform secret. When it claims a task, the launch payload carries exactly the secrets that one task needs (provider keys, git keys, the scope-bound task session token) and nothing else:

  • The payload is encrypted at rest on the assignment with a scope-bound AAD; only a redacted preview is stored in clear, the ciphertext is revealed on claim and dropped when the task reaches a terminal state.
  • A denylist strips any hub-master secret (the credential-encryption key, vault credentials, the JWT/OIDC/MCP-confirmation secrets, the database URL, chat-bot and billing tokens) from the dispatch payload — defence-in-depth so such a secret can never reach a (possibly third-party) runner even by accident.

A remote run streams its events back to the hub, which ingests them into the same event pipeline a local run uses. Each relayed event is run through a redactor seeded from that assignment’s resolved secrets before it is persisted or emitted — so a secret echoed into an event by an agent is scrubbed on a remote run exactly as on a local one.

Crucially, a runner having no database does not weaken authorization. The agent’s MCP tool surface and blast-radius limits are enforced server-side, against the task’s session token, on every call — identical for a remote-runner agent and a local one. The runner cannot grant its agent more than the hub allows.

The hub marks a runner offline when its heartbeat goes stale past a bounded window (default 90 s, floor 45 s, SUPACLOUD_RUNNER_STALE_SECS), stops selecting it, and re-surfaces its outstanding work — both never-claimed and in-flight (claimed/running) — failing it, dropping its at-rest per-run secret, and freeing the task to re-dispatch. The claim itself is race-free (FOR UPDATE SKIP LOCKED) and a stale runner’s late terminal report is rejected rather than clobbering re-dispatched work.

Third-party onboarding: enrollment & the scoped runner JWT

Section titled “Third-party onboarding: enrollment & the scoped runner JWT”

The scrn_… bearer above is the right credential for a runner you operate. To onboard a runner on a host you trust less, a second credential class adds defence-in-depth so the operator never holds a long-lived hub credential and the runner can only ever serve the workspaces you scoped it to. The operator-facing steps are in Set up a runner securely; the trust model is:

  • Single-use, response-wrapped enrollment token. An admin mints a short-TTL enrollment token that carries a scope (trust class, tier, isolation, allowed workspaces + labels). The plaintext is returned exactly once and stored only as a hash. The runner exchanges it on boot, and that exchange burns the token atomically — it cannot be replayed, and a bad or expired token never leaves an orphan runner.
  • Scoped runner JWT — never a session. The exchange returns a short-lived (≤ 1 hour) scrnj_… JWT whose claims mirror the runner’s grant. It is signed with a domain-separated key derived from the session secret, so a runner JWT never verifies as a user session and a session never verifies as a runner JWT — cross-acceptance fails at the signature check. A required typ = runner claim is a belt-and-braces second discriminator. The runner refreshes the JWT before it expires, and each refresh re-checks the grant in the database, so revoking a workspace takes effect within one token lifetime rather than waiting for a redeploy.
  • Two trust classes. A trusted runner minted with no explicit workspaces holds the wildcard (any workspace) — reserve it for hosts you fully control. An edge runner is untrusted by default and fail-closed: it enrols with an empty workspace set and is dispatched nothing until an owner explicitly grants it a workspace, even while it is online and idle.
  • Grant enforced at dispatch. The runner’s grant is a runner_grants row — the authoritative, DB-mutable scope, never something the runner asserts. It is applied as a candidate filter before ranking: a runner can never be sent work outside its grant and can never widen its own scope. A non-empty label set on the grant further requires every task label to be granted (required ⊆ allowed). The pre-enrollment fleet is unaffected — a scrn_… runner with no grant row keeps the implicit trusted wildcard, so dispatch stays byte-identical.

By default only a system admin may mint enrollment tokens; the runner_onboarding_by_owner instance switch additionally lets organization / workspace owners onboard edge runners scoped to workspaces they own, never beyond their own scope.