On this page

Stado network

Stado runs its own network between the fleet's hosts and makes it every host's preferred path. One expired credential at an outside overlay provider does not cut the fleet apart: a host on the Stado network is reached through Stado's own node, and an outside overlay such as Tailscale is an alternate path the operator may declare, never one the fleet needs.

What it is

Every host that ran stado network up keeps a node: a listener on a port the host's own system handed out, run by its own unit (a launchd job, or a systemd user unit), separate from the host's agent so that a broken agent still leaves the host reachable.

Two nodes speak the Noise IK_25519_ChaChaPoly_BLAKE2s handshake, the construction WireGuard is built on, with the vetted snow library. Each side proves the static key the registry declares for its host, and a node admits only a key the registry declares for a fleet host. After the handshake the caller asks for one port, and the node opens it on its own loopback only when it is a port that host's registry SSH paths name. Everything else Stado sends between hosts already travels inside SSH, so the resolver tunnel, the registry authority, the store and the health beacons move onto the network with it.

There is no network interface and no address range: a host's address on the network is its registry name under .stado.internal (the top-level domain reserved for private use, so no public name answers for it), and user@<host>.stado.internal is an ordinary registry SSH destination. Stado's own SSH client dials it through the network; OpenSSH reaches it through ProxyCommand=stado network connect %h %p, which Stado adds itself. Because the network already proved the machine by its registry key, the first SSH host key seen under a network name is recorded in ~/.ssh/known_hosts; a changed key under a recorded name is still refused.

Keys

stado network up generates the host's X25519 key pair on the host and keeps it owner-only at ~/.stado/network/key. Only the public key leaves the host, in the registry. The private key is never carried anywhere, so it is not placed in Skarbiec: a vault the network must first reach cannot hold the key that reaches it.

The key and the node's port are settled before up reads the registry authority, because neither needs it. A host that cannot read the authority yet keeps its key, and its next up declares that same key instead of minting another.

Joining from a host that is cut off

A host joining the network has no path to the registry authority through the network yet: the authority's node admits only declared keys. Its read goes over the other paths the registry declares for the authority's host, all started at once and bounded together by the host's resolver refresh interval, so a dead path costs nothing while another answers. A lan path names the host's multicast DNS name; a host whose own resolver does not answer .local names (Linux without nss-mdns) asks the link itself, twice over three seconds (RFC 6762 §5.2), and keeps the answer for the rest of the command for its 120-second TTL, so the registry write and the reads after it do not ask the link again.

Peers: the registry

targets[].network is the peer list:

"network": {
  "public_key": "<base64 X25519 public key>",
  "endpoints": ["build-host.local:52817", "192.168.1.20:52817"],
  "relay": {"attach_retry_seconds": 30}
}

endpoints are measured by up: the host's multicast DNS name when the link answers it with this host's address, and the private and public IPv4 addresses of its interfaces that are up, each with the node's port. Shared address space (100.64.0.0/10, which carrier-grade NAT and overlays hand out) is never published. --endpoint HOST:PORT adds one peers on other networks can reach, and --no-direct declares none of the measured ones. relay is present only on a relay.

A node reads peers from its host's registry copy, so reaching a peer never first needs the registry authority the network may be the way to. A host or key the copy does not know asks the authority once.

Relays

A host no peer can dial (behind NAT, with no forwarded port) keeps a standing attachment to every relay its registry copy declares. A caller that cannot dial a host directly asks a relay for it; the relay asks the attached host to dial back, and the caller then runs a second, end-to-end handshake with the host through the relay, which carries only ciphertext. A relay is a host with an endpoint every host can reach, declared with stado network up --relay --attach-retry-seconds SECONDS --endpoint HOST:PORT; the retry period is the operator's, and a node whose attachment ended waits that long before attaching again. Direct endpoints and relays are tried together, and the first to answer carries the connection.

Paths

up makes user@<host>.stado.internal the host's primary path only after the node answered: its own SSH server greeting has come back through the node at a declared endpoint. The previous primary becomes the alternate its address names — lan for a multicast DNS name or a private address, tailscale for shared address space, direct otherwise — and a host without a lan path gets one. The order is the network, then the LAN, then whatever the operator declared beside them. down takes the node out of the registry, makes the first alternate the primary again, and removes the unit; the key stays.

Enrollment with stado fleet enroll --bootstrap runs stado network up on the machine right after installing Stado, so a new host's preferred path is the Stado network from its first day. Machine-initiated enrollment prints the same step after stado bootstrap --local.

Commands

stado network up [--host NAME] [--endpoint HOST:PORT]... [--no-direct] [--relay --attach-retry-seconds SECONDS] [--json]
stado network status [--json]
stado network down [--host NAME] [--json]
stado network serve
stado network connect HOST PORT

up runs on the host it joins. --host names that host: when this machine's registry copy tells it apart from this machine, that host's own installed Stado runs network up over its host channel — the node, its key and its unit are that host's — and its receipt is printed here, so a new host joins from the operator's machine. When the copy cannot tell, up runs here, and a --host the authority's registry names as another machine is refused with --host <name> is not this machine: the registry identifies this machine (<hostname>) as <other>; stado network runs on the host it joins. Its receipt carries the key, the port, the node's pid, the endpoints, the unit file, the SSH greeting that came back through the node, the path changes and the registry generation. up refuses a host with nothing to declare: <host> would declare no endpoint and no relay could carry it: this host measured none on its interfaces; --endpoint HOST:PORT declares a reachable one. One whose registry authority did not answer is refused with the authority's own answer and the key it kept: `network up` declares the node in the registry, whose authority did not answer: <answer>; this host's key <public key> is kept, and the next `network up` declares it. A node that cannot start is reported with its own reason, for example the network node (pid <pid>) stopped: the node cannot listen on port <port> ….

A node an older install left a cron fallback for (@reboot /bin/sh ~/.stado/bin/run-com.wisent.stado-network.sh, written when no launchd domain answered) was started twice at every boot: cron's copy took the port first and the node its unit com.wisent.stado-network started failed to bind and was started again every ten seconds. The node its unit started now reads who listens on its port when the port is taken. A listener whose parent ran that unit's cron wrapper is ended (SIGTERM, SIGKILL after five seconds), the cron line and the wrapper are removed as an install removes them, and the node binds and says port <port> was held by pid <pid>, a copy of this node cron started from <wrapper> beside the unit com.wisent.stado-network; it was ended. Any other holder is left alone and the refusal above stands. A cron line that cannot be removed fails the start, naming the copy it ended and that the next boot starts it again. The journey tests/network/cron_copy.mjs runs this on a dedicated macOS account.

status asks every declared peer now, over the network itself, and lists it reached with the endpoint or relay that carried the question, or unreached with each way's answer. The peers are asked by the status command's own process, not by the node, so they are listed under peers, as this command reaches them:. It also compares the image the node executes with the program the node was started as: a node left on a Stado image an install has since replaced reads serving, but on a Stado image an install has since replaced at <program>; stado release local converge restarts it, and --json names it under replaced_image (or why it could not be read, under image_unread). Such a node was refused every LAN peer while status reached them all. It exits non-zero when this host's own node is not serving, or serves a replaced image: <host>'s Stado network node executes a Stado image an install has since replaced at <program>, and macOS refuses such a process the local network while it allows the installed file; stado release local converge restarts it.

Which hosts reach which is answered only from each host, since a node one host reaches another may not, so another host's own view is read through its host channel: stado host exec <target> -- stado network status --json is an approved host-exec command, which dials each declared peer's node once and changes nothing. Every other network verb stays refused by the host-exec allowlist.

down refuses to leave a host with no path at all: registry host <host> has no SSH path but the Stado network, so taking the network down would leave it unreachable; stado registry host path set lan --ssh USER@HOST declares another first.

Stado Desktop offers the same three operations for the API host it is connected to, in the console's network group.

Source: this website