Skip to content
Select themeSelect language

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. Set one environment variable on the server and restart it:

    SUPACLOUD_HUB_MODE=true

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

  2. 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 }
  1. 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 }
    }'
  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.

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 both connectors: true and 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.

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.

  1. 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": []
    }'

    class is trusted or edge; tier is wasm, docker or both; isolation is container or microvm. 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.

  2. 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 \
    bash

    The runner binary itself redeems the enrollment token: on boot, in --runner mode, it calls the RunnerService.ExchangeToken gRPC RPC ({enrollment_token, name, capabilities} → the scoped scrnj_… JWT, ADR 0050) and advertises capabilities.labels (folded lowercase from SUPACLOUD_RUNNER_LABELS) plus the backends/isolation of 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, set SUPACLOUD_RUNNER_ENROLLMENT_TOKEN (and SUPACLOUD_RUNNER_LABELS) and run the binary or image exactly as in section 3 — or, if you drive the exchange yourself, set SUPACLOUD_RUNNER_TOKEN=<the scrnj_ JWT> instead.

  3. 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’s token_expires_at / the JWT exp — and each refresh calls the hub’s grant-rechecking RunnerService.RefreshToken gRPC RPC, so revoking a workspace takes effect within one token lifetime. To pin the cadence explicitly, set SUPACLOUD_RUNNER_TOKEN_REFRESH_SECS to the number of seconds-before-expiry at which to re-exchange (0, the default, means derive it).

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 to runner-token under the runner’s state directory — /var/lib/supacloud-runner when the installer runs as root, otherwise $XDG_STATE_HOME/supacloud-runner; %LOCALAPPDATA%\supacloud\state on Windows. (The runner binary’s built-in fallback is $XDG_STATE_HOME/supacloud-runner/runner-token, or %LOCALAPPDATA%\supacloud\runner-token on 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 ExchangeToken call — 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).

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.

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):

Terminal window
curl -fsSL "$SUPACLOUD_URL/install-runner.sh" \
| SUPACLOUD_ENROLLMENT_TOKEN=<one-time-token> \
SUPACLOUD_RUNNER_LABELS=gpu,blender \
bash

Windows (PowerShell):

Terminal window
$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" | iex

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

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 documented nohup start and prints the follow-up command; force the fallback with SUPACLOUD_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) or LaunchAgent (user) with RunAtLoad + KeepAlive; stdout/stderr go to runner.out.log / runner.err.log in the state directory.
  • Windows: a scheduled task — running as SYSTEM at startup when the installer is elevated, otherwise as the current user at logon (falling back to the per-user Run key where Task Scheduler is locked down). A scheduled task is used rather than sc.exe/New-Service because 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 to logs\runner.log under 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:

Terminal window
curl -fsSL "$SUPACLOUD_URL/install-runner.sh" | bash -s -- --uninstall
Terminal window
.\install-runner.ps1 -Uninstall

SUPACLOUD_RUNNER_INSTALL_DIR, SUPACLOUD_RUNNER_STATE_DIR and SUPACLOUD_RUNNER_TOKEN_FILE place the binary, the state directory and the token file explicitly.

SUPACLOUD_HUB_URL=https://supacloud.example.com
SUPACLOUD_RUNNER_TOKEN=scrn_xxxxxxxxxxxxxxxxxxxxxxxx
SUPACLOUD_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-agents

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

  1. Put the variables above in a .env next to the binary (or export them), then:

    Terminal window
    supacloud --runner
  2. 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
  1. The runner needs a Docker host to launch agent containers — install Docker Desktop (WSL 2 backend) on the worker.

  2. 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 --runner

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

  1. Build (or obtain) a runner binary compiled with the microvm-backend feature.

  2. 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=microvm
    SUPACLOUD_RUNNER_MICROVM_CMD=/usr/local/bin/launch-microvm
  3. Enroll (or re-enroll) the runner with isolation: microvm so the hub’s fail-closed dispatch filter routes microVM-required tasks to it and only it.

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.

  1. On the hub (server). Serve TLS directly and require client certificates:

    SUPACLOUD_TLS_CERT_PATH=/etc/supacloud/tls/hub.crt
    SUPACLOUD_TLS_KEY_PATH=/etc/supacloud/tls/hub.key
    SUPACLOUD_TLS_CLIENT_CA_PATH=/etc/supacloud/tls/runner-ca.crt

    SUPACLOUD_TLS_CERT_PATH + SUPACLOUD_TLS_KEY_PATH turn on TLS; SUPACLOUD_TLS_CLIENT_CA_PATH additionally requires and verifies a client cert against that CA. A connection without a trusted client cert is rejected at the handshake.

  2. On each runner. Present a client identity and (optionally) pin the hub CA:

    SUPACLOUD_RUNNER_TLS_CLIENT_CERT_PATH=/etc/supacloud/tls/runner.crt
    SUPACLOUD_RUNNER_TLS_CLIENT_KEY_PATH=/etc/supacloud/tls/runner.key
    SUPACLOUD_RUNNER_TLS_CA_PATH=/etc/supacloud/tls/hub-ca.crt # optional: pin the hub CA
  1. 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.

  2. The runner’s row flips pending → online on 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>"
  3. 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.