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