Set up a runner securely
This guide adds a runner — a separate machine that pulls and executes agent tasks — to a SupaCloud install. The runner connects outbound-only over HTTPS, so it works behind NAT with no inbound ports. For the concepts (pull transport, the per-run secret model, the two meanings of “runner fleet”) see the explanation The runner fleet.
1. Turn hub mode on (server)
Section titled “1. Turn hub mode on (server)”-
Set one environment variable on the server and restart it:
SUPACLOUD_HUB_MODE=trueThe default is
false. While off, every task runs locally regardless of how many runners are registered — which is what makes hub mode safe to flip off in a hurry without de-registering anything. -
Confirm it is on:
Terminal window curl https://supacloud.example.com/api/runners/config \-H "Authorization: Bearer <admin-session-or-API-token>"# → { "hub_mode_enabled": true }
2. Register the worker node (server)
Section titled “2. Register the worker node (server)”-
As a system admin, register the runner and capture the token:
Terminal window curl -X POST https://supacloud.example.com/api/runners \-H "Authorization: Bearer <admin-session-or-API-token>" \-H "Content-Type: application/json" \-d '{"name": "gpu-worker-01","capabilities": { "images": ["unified"], "max_parallel": 2 }}' -
The response includes the plaintext bearer token (
scrn_…) exactly once — the server stores only its SHA-256 hash. Copy it now; you will hand it to the worker in the next step. If you lose it, rotate the token rather than re-registering.
Advertise Wasm connector and script execution
Section titled “Advertise Wasm connector and script execution”A runner can also opt in to running Wasm on its node — both marketplace connector nodes and free-form scripts (JS / TS / Python). Both run inside the same WASI 0.2 sandbox (per-run memory + wall-clock caps, deny-all SSRF-vetted egress) behind the runner’s own OS boundary. Two capability keys gate it:
connectors: true— the runner opts in to running connector-node Wasm. The hub never dispatches a connector run to a node without this flag.wasm.script_languages— the script languages whose interpreter the runner’s image actually provisioned. The hub dispatches a script run only to a runner that advertises bothconnectors: trueand the script’s language here (a script runs Wasm on the node, so the node must have opted into Wasm execution and have the interpreter present).
{ "name": "wasm-worker-01", "capabilities": { "images": ["unified"], "connectors": true, "wasm": { "script_languages": ["js", "ts", "py"] } }}List only languages whose interpreter the runner has actually provisioned
under SUPACLOUD_SCRIPT_RUNTIME_DIR (built reproducibly by
scripts/build-script-runtimes.sh). The matcher is fail-closed: an absent or
malformed wasm key means the runner advertises no script language and is never
sent a script — so a pre-existing fleet is wholly unaffected, and adding the key
is a safe, monotonic opt-in.
When a script is dispatched to the fleet the hub ships metadata only — the language plus the source and inputs as data — and a SHA-256 integrity anchor of its own interpreter. It never ships the multi-MB interpreter. The runner resolves its own local, trusted interpreter for that language and re-checks the SHA before running; a mismatch (version skew or tamper) fails closed. A request that tried to smuggle interpreter bytes is rejected as a protocol violation. The interpreter is always a runner-local trusted asset — a payload-shipped one is never executed.
This is governed instance-wide by SUPACLOUD_CONNECTOR_EXECUTOR_MODE
(in_process — the default — / pooled / runner_fleet); set it to
runner_fleet to dispatch connector and script runs to the fleet. With the
default in_process mode, scripts run on the hub exactly as before — this is a
zero-config no-op for existing deployments.
Onboard a third-party runner (enrollment)
Section titled “Onboard a third-party runner (enrollment)”Section 2 mints a long-lived scrn_… bearer for a runner you operate. To
bring up a runner on a host you trust less — a customer’s machine, edge compute,
a partner’s GPU box — use the enrollment flow instead. You hand the operator
a single-use, short-lived enrollment token; their runner exchanges it on boot
for a scoped, short-lived scrnj_… JWT and refreshes it automatically. The
operator never holds a long-lived hub credential, and the runner can only ever
serve the workspaces you scoped it to. For the trust model behind this, see
Runner-fleet security.
-
Mint the enrollment token (admin). Choose the trust class, execution tier, isolation and the workspaces the runner may serve:
Terminal window curl -X POST https://supacloud.example.com/api/runners/enroll \-H "Authorization: Bearer <admin-session-or-API-token>" \-H "Content-Type: application/json" \-d '{"class": "edge","tier": "docker","isolation": "container","allowed_workspace_ids": ["<workspace-uuid>"],"allowed_labels": []}'classistrustedoredge;tieriswasm,dockerorboth;isolationiscontainerormicrovm. The response carries the plaintext enrollment token exactly once (the server stores only its hash) and a short expiry. Copy it now and hand it to the operator over a secure channel. -
Onboard the worker with the installer (operator). The operator runs the hosted one-line installer, which downloads the signed runner artifact, verifies its checksum and signature, exchanges the one-time enrollment token for a scoped
scrnj_…JWT, and boots the runner with that JWT — outbound-only, no inbound ports:Terminal window curl -fsSL https://supacloud.example.com/install-runner.sh | \SUPACLOUD_HUB_URL=https://supacloud.example.com \SUPACLOUD_RUNNER_ENROLLMENT_TOKEN=<one-time-token> \SUPACLOUD_RUNNER_NAME=customer-edge-01 \SUPACLOUD_RUNNER_LABELS=windows,unity,gpu \bashThe runner binary itself redeems the enrollment token: on boot, in
--runnermode, it calls theRunnerService.ExchangeTokengRPC RPC ({enrollment_token, name, capabilities}→ the scopedscrnj_…JWT, ADR 0050) and advertisescapabilities.labels(folded lowercase fromSUPACLOUD_RUNNER_LABELS) plus thebackends/isolationof the backend it was configured with. The installer only carries the single-use token and the labels; it never holds a bearer. To onboard without the installer, setSUPACLOUD_RUNNER_ENROLLMENT_TOKEN(andSUPACLOUD_RUNNER_LABELS) and run the binary or image exactly as in section 3 — or, if you drive the exchange yourself, setSUPACLOUD_RUNNER_TOKEN=<the scrnj_ JWT>instead. -
The runner refreshes its JWT automatically. The exchange burns the enrollment token (it cannot be replayed) and yields a scoped
scrnj_…JWT carrying the runner’s trust class, tier, isolation and allowed workspaces. The JWT lives at most one hour; the runner daemon re-fetches a fresh one before it expires — by default at roughly half the token TTL, derived from the grant’stoken_expires_at/ the JWTexp— and each refresh calls the hub’s grant-recheckingRunnerService.RefreshTokengRPC RPC, so revoking a workspace takes effect within one token lifetime. To pin the cadence explicitly, setSUPACLOUD_RUNNER_TOKEN_REFRESH_SECSto the number of seconds-before-expiry at which to re-exchange (0, the default, means derive it).
Restart and reboot: token persistence
Section titled “Restart and reboot: token persistence”The enrollment token is single-use: once the runner redeems it, the token is gone and cannot be replayed. An enrolled runner therefore persists the scoped JWT it received and reads it back on the next start:
- The JWT is written to
SUPACLOUD_RUNNER_TOKEN_FILE; the installers set this torunner-tokenunder the runner’s state directory —/var/lib/supacloud-runnerwhen the installer runs as root, otherwise$XDG_STATE_HOME/supacloud-runner;%LOCALAPPDATA%\supacloud\stateon Windows. (The runner binary’s built-in fallback is$XDG_STATE_HOME/supacloud-runner/runner-token, or%LOCALAPPDATA%\supacloud\runner-tokenon Windows.) - The write is atomic (a temp file in the same directory, then a rename) and the file mode is 0600.
- At boot the runner reads the file before it would exchange anything. A
still-valid token is used as-is — no second
ExchangeTokencall — so a reboot no longer needs a fresh enrollment token. An expired, malformed, or group/world-readable file is discarded, and the runner re-enrolls instead. - Every successful JWT refresh writes the fresh token back, so the file tracks the live credential.
The file is a bearer secret at rest: keep the runner host and its state
directory private, and note that --uninstall removes it. To reuse a persisted
runner identity when updating the binary, run the installer without
SUPACLOUD_ENROLLMENT_TOKEN; supplying a token always forces a fresh enrollment
(the installer drops the old file so the new token is redeemed).
3. Bring the runner online (worker node)
Section titled “3. Bring the runner online (worker node)”The runner is the same SupaCloud binary run in --runner mode. Pick the
platform you are installing on. In every case the configuration is the same set
of environment variables; only how you launch differs.
One-line installers (signed binary)
Section titled “One-line installers (signed binary)”Once your release channel hosts the signed binaries, the installers do the whole
onboarding for you on a worker node — detect the platform, download the pinned
binary, verify the checksum and signature, install, and boot
supacloud-runner --runner over TLS with no inbound ports. The runner itself
redeems the one-time enrollment token and advertises its labels as capabilities,
so the operator is back in the UI in seconds. The only secret you supply is the
single-use enrollment token from the mint step; the script body embeds none.
Linux / macOS (the exact one-liner the SupaCloud UI hands you):
curl -fsSL "$SUPACLOUD_URL/install-runner.sh" \ | SUPACLOUD_ENROLLMENT_TOKEN=<one-time-token> \ SUPACLOUD_RUNNER_LABELS=gpu,blender \ bashWindows (PowerShell):
$env:SUPACLOUD_URL = "https://supacloud.example.com"$env:SUPACLOUD_ENROLLMENT_TOKEN = "<one-time-token>"$env:SUPACLOUD_RUNNER_LABELS = "gpu,blender"irm "$env:SUPACLOUD_URL/install-runner.ps1" | iexThe SupaCloud UI shows both one-liners after minting (the runner host need not
be the browser’s OS). Labels are folded to lowercase and deduplicated before they
are sent, so a hand-typed GPU and a suggested gpu are one label — the fleet
matches labels by exact equality, and an unfolded GPU would advertise something
no task could ever ask for.
Useful overrides (env vars, both installers): SUPACLOUD_RUNNER_NAME (defaults to
the hostname), SUPACLOUD_RUNNER_VERSION / SUPACLOUD_DOWNLOAD_BASE (pin a
specific release or artifact host), and SUPACLOUD_RUNNER_DRY_RUN=1 to print the
resolved download + verify + start plan without touching the network.
The hub redirects these paths to the published, stamped installer on your release feed — it does not carry the script itself, so there is exactly one copy. Configure the feed with two server-side variables:
SUPACLOUD_RUNNER_VERSION— the release version, e.g.v0.7.0.SUPACLOUD_RUNNER_DOWNLOAD_BASE— the base URL of the release feed, e.g.https://git.blockworx.tech/api/packages/bw-public/generic/supacloud-runner.
The redirect target is <download base>/<version>/install-runner.{sh,ps1}. Until
both are set, the entry points answer 503 — onboard from the Docker image or a
self-built binary in the meantime (the enrollment token works identically). The
version/download base and the signature anchors are baked into the published
installer by runner-release.yml; the hub never stamps or rewrites it.
Autostart service and uninstall
Section titled “Autostart service and uninstall”The one-line installers register an autostart service, so the runner comes back after a reboot and can be decommissioned cleanly:
- Linux: a systemd unit — a system unit when run as root, a per-user unit
(
systemctl --user, with lingering enabled so it starts without a login) otherwise. When systemd is unavailable the installer falls back to a documentednohupstart and prints the follow-up command; force the fallback withSUPACLOUD_RUNNER_NO_SERVICE=1. Logs go to the journal (journalctl -u supacloud-runner -f) or, in the fallback, the state directory. - macOS: a launchd
LaunchDaemon(root) orLaunchAgent(user) withRunAtLoad+KeepAlive; stdout/stderr go torunner.out.log/runner.err.login the state directory. - Windows: a scheduled task — running as
SYSTEMat startup when the installer is elevated, otherwise as the current user at logon (falling back to the per-userRunkey where Task Scheduler is locked down). A scheduled task is used rather thansc.exe/New-Servicebecause the runner is a console binary that does not implement the Service Control Protocol and would otherwise fail to start (Windows error 1053). Logs go tologs\runner.logunder the install directory.
Re-running an installer is idempotent: it replaces the binary and restarts the
service, and it reuses the persisted token unless you supply a fresh
SUPACLOUD_ENROLLMENT_TOKEN. To remove everything — the service, the binary, and
the persisted token — run:
curl -fsSL "$SUPACLOUD_URL/install-runner.sh" | bash -s -- --uninstall.\install-runner.ps1 -UninstallSUPACLOUD_RUNNER_INSTALL_DIR, SUPACLOUD_RUNNER_STATE_DIR and
SUPACLOUD_RUNNER_TOKEN_FILE place the binary, the state directory and the token
file explicitly.
Common configuration (all platforms)
Section titled “Common configuration (all platforms)”SUPACLOUD_HUB_URL=https://supacloud.example.comSUPACLOUD_RUNNER_TOKEN=scrn_xxxxxxxxxxxxxxxxxxxxxxxxSUPACLOUD_RUNNER_NAME=gpu-worker-01# Optional cadence overrides (sensible defaults shown):# SUPACLOUD_RUNNER_HEARTBEAT_SECS=30# SUPACLOUD_RUNNER_POLL_SECS=3# Docker network the runner puts agent containers on (must exist on the worker):# AGENT_NETWORK=supacloud-agentsThe hub URL must be https://. A plaintext http:// hub is refused at boot
unless you explicitly set SUPACLOUD_RUNNER_ALLOW_INSECURE_TRANSPORT=true — only
ever acceptable on a trusted private network.
Linux / macOS
Section titled “Linux / macOS”-
Put the variables above in a
.envnext to the binary (or export them), then:Terminal window supacloud --runner -
Or with the Docker image (mount the Docker socket so the runner can launch agent containers on its own host):
Terminal window docker run -d --name supacloud-runner \-e SUPACLOUD_HUB_URL=https://supacloud.example.com \-e SUPACLOUD_RUNNER_TOKEN=scrn_xxxxxxxxxxxxxxxxxxxxxxxx \-e SUPACLOUD_RUNNER_NAME=gpu-worker-01 \-v /var/run/docker.sock:/var/run/docker.sock \<your-supacloud-image> --runner
Windows
Section titled “Windows”-
The runner needs a Docker host to launch agent containers — install Docker Desktop (WSL 2 backend) on the worker.
-
In PowerShell, set the variables for the session and launch the binary:
Terminal window $env:SUPACLOUD_HUB_URL = "https://supacloud.example.com"$env:SUPACLOUD_RUNNER_TOKEN = "scrn_xxxxxxxxxxxxxxxxxxxxxxxx"$env:SUPACLOUD_RUNNER_NAME = "win-worker-01".\supacloud.exe --runnerFor an always-on worker, run the same command as a service (e.g. with NSSM) or run the Docker image command from the Linux/macOS tab inside WSL 2.
microVM execution backend (strongest isolation)
Section titled “microVM execution backend (strongest isolation)”The default docker backend runs each task in a container on the runner’s local
Docker host. For an untrusted or edge runner, the microVM backend is the
strongest isolation tier: each claimed task runs in its own short-lived
hardware-virtualised guest (a Firecracker/Kata-class microVM), so a compromised
agent process is contained behind a VM boundary, not just a kernel namespace. For
how the tiers compare, see Runner-fleet defense-in-depth →
isolation tiers.
-
Build (or obtain) a runner binary compiled with the
microvm-backendfeature. -
Provision a microVM launcher on the worker (e.g. a Firecracker/Kata wrapper), then select the backend and point the runner at it:
SUPACLOUD_RUNNER_EXECUTION_BACKEND=microvmSUPACLOUD_RUNNER_MICROVM_CMD=/usr/local/bin/launch-microvm -
Enroll (or re-enroll) the runner with
isolation: microvmso the hub’s fail-closed dispatch filter routes microVM-required tasks to it and only it.
Secure the runner transport with mTLS
Section titled “Secure the runner transport with mTLS”By default the runner trusts the hub over one-way TLS (the hub presents a server certificate; the runner verifies it against the system trust store). To require a mutual TLS handshake — so the hub also authenticates each runner by client certificate, a strong second factor on top of the bearer token — configure both ends. mTLS is optional; with none of these set, transport is exactly as today.
-
On the hub (server). Serve TLS directly and require client certificates:
SUPACLOUD_TLS_CERT_PATH=/etc/supacloud/tls/hub.crtSUPACLOUD_TLS_KEY_PATH=/etc/supacloud/tls/hub.keySUPACLOUD_TLS_CLIENT_CA_PATH=/etc/supacloud/tls/runner-ca.crtSUPACLOUD_TLS_CERT_PATH+SUPACLOUD_TLS_KEY_PATHturn on TLS;SUPACLOUD_TLS_CLIENT_CA_PATHadditionally requires and verifies a client cert against that CA. A connection without a trusted client cert is rejected at the handshake. -
On each runner. Present a client identity and (optionally) pin the hub CA:
SUPACLOUD_RUNNER_TLS_CLIENT_CERT_PATH=/etc/supacloud/tls/runner.crtSUPACLOUD_RUNNER_TLS_CLIENT_KEY_PATH=/etc/supacloud/tls/runner.keySUPACLOUD_RUNNER_TLS_CA_PATH=/etc/supacloud/tls/hub-ca.crt # optional: pin the hub CA
4. Verify
Section titled “4. Verify”-
On start the runner makes one heartbeat call. If the token is rejected it exits immediately with a clear error; if the hub is momentarily unreachable it starts anyway and keeps retrying.
-
The runner’s row flips
pending → onlineon its first successful heartbeat. Confirm from the server:Terminal window curl https://supacloud.example.com/api/runners \-H "Authorization: Bearer <admin-session-or-API-token>" -
Launch a task whose image the runner advertises. With hub mode on and a matching online runner, the server dispatches it to the runner and the live event stream shows progress exactly as for a local run.