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

    The installer (not the bare binary) performs the exchange: the standalone supacloud --runner binary only ever reads SUPACLOUD_RUNNER_TOKEN (the scrnj_… JWT), never the enrollment token. To onboard without the installer, exchange the token yourself — the RunnerService.ExchangeToken gRPC RPC ({enrollment_token, name} → the JWT, ADR 0050) — then set SUPACLOUD_RUNNER_TOKEN=<the scrnj_ JWT> and run the binary or image exactly as in section 3.

  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 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, exchange the one-time enrollment token, install, and start supacloud --runner over TLS with no inbound ports. 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> bash

Windows (PowerShell):

Terminal window
$env:SUPACLOUD_URL = "https://supacloud.example.com"
$env:SUPACLOUD_ENROLLMENT_TOKEN = "<one-time-token>"
irm "$env:SUPACLOUD_URL/install-runner.ps1" | iex

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.

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.