Subshell Docs
Nodes

Updating a Node

Move an enrolled node to a newer binary, from the page or the machine, and what rollback means there.

A node replaces its own binary and restarts into the new one, from the machine's keyboard or from the control plane's page, and both routes run one implementation. An update is a restart with a file swap in front of it, so it carries the restart's permissions, the restart's refusals, and one addition: the safety transaction that means a bad update puts the old binary back by itself.

First: is there a newer node binary, and can you use it?

On the machine:

subshell update --check

It names the newest release the release source publishes that the publisher signed; an unsigned or unverifiable release is refused by name, never offered. One thing it cannot tell you is whether your plane can talk to that version: the binary holds no REST credential, so it cannot ask. The control plane knows: Settings → Updates lists every node's version against what it offers, and a node's own page says in plain words when the installed binary is too old or speaks a protocol the plane does not: the version floors are explained here. --to <version> is how you act on an answer you read there.

Updating from the machine

subshell update            # move to the newest release
subshell update --to 0.9.0 --yes
subshell update --from ./subshell-node-cli-linux-x64 --yes

--from installs a local file instead of downloading, the path for an air-gapped fleet (with SUBSHELL_RELEASE_URL set empty, every networked update refuses and names --from instead of failing quietly). --force allows a downgrade. Note where the caution lives: it is the page-commanded update that refuses before downloading anything when the restart it will request cannot mean what it says (an unsupervised node, or a service definition that would close live panes without force), because a refusal there arrives before 70 MB has crossed the wire. The CLI at the keyboard is less paranoid: it installs first, and if the manager cannot or should not be restarted, it says so honestly with the new binary already in place. What the CLI checks before the binary itself moves is not only what the machine can answer locally (no downgrades without --force, an installed binary that can actually be resolved) but also the release's publisher: release-manifest.json and its signature are two small reads, verified against a key compiled into this binary, and only then is the artifact downloaded. The digest the downloaded bytes are hashed against comes from that signed manifest, never from the .sha256 sidecar. Any candidate, downloaded or copied with --from, must then run and report the exact version it claims.

Updating from the page

The Nodes section of the Components table under Settings → Updates offers the same thing remotely: owner or edit grantee, browser session only, never the control plane host's own row, and audited as a node update. The version it offers is the newest your plane can actually talk to, not simply the newest published.

What crosses the wire is a signed update command naming a download URL, a digest, and the release's signed manifest with its signature. The node verifies that signature against its own compiled-in publisher key before it replaces anything: the plane's digest is a belt, not the anchor. Even a control plane persuaded, or compromised, after composing the command cannot get the node to install bytes the publisher did not sign; the plane can order an update, only the publisher's key can authorise what runs. A node too old to verify the manifest would silently ignore it and install on the old rule, so your plane refuses to send one an update at all, and the node's page says so. The URL embeds a one-time download token: held in memory, single-use, ten minutes, bound to that one node and one platform, and refused on the checksum routes. It is not a credential in any general sense: the node key still does nothing on the REST API; this token buys exactly one download of a file the release source publishes publicly anyway.

Two honest consequences:

  • The download URL is built from the plane's configured address, so an instance configured with a loopback base URL will tell remote nodes to fetch from 127.0.0.1, the enrollment trap, in a new place. If remote updates fail at the download step on an instance whose base URL is loopback, that is the reason.
  • A plane restart forgets outstanding tokens. The node's download then fails, and the update is reported as failed, which is the accurate report, not a silent retry.

The update is a transaction the new binary completes

Whoever swaps the files cannot see whether the new binary works; the next boot can. So the updater writes a pending marker, keeps the old binary as <binary>.previous, and swaps. The node that boots next settles it:

  • The plane accepts it (the new binary connects and is treated as a working node), and the marker and the .previous are deleted. Update over.
  • The plane refuses it (wrong version, wrong protocol) and the marker says the refusal followed an update: the node renames .previous back, records the failure, and exits so the service manager respawns the version that worked, on a machine nobody visited. That swap-back is the node's entire rollback: a node has no database, so unlike a server update there is no data half to restore.
  • A refusal with no marker is just the ordinary "your node is too old" case, and the node does not invent a rollback for an update that never happened.

At any point afterwards, subshell update --rollback puts the kept .previous binary back, if one exists.

What an update actually trusts

Both ends download code from the release source, and neither has to believe it. Every release carries release-manifest.json plus a detached minisign signature over its exact bytes (release-manifest.json.sig), made with the publisher keypair, and every path that installs one (this machine's subshell update, the page-commanded update, the server's own) verifies that signature against a public key compiled into the product and takes the install digest from the signed manifest's assets map, never from the .sha256 sidecar. The release source still chooses which signed release you get: it can withhold updates or replay any release the publisher ever signed, but it cannot choose what you run. Authenticity is the publisher's key; TLS is only delivery.

The accounted exceptions: the install one-liners still verify against the sidecar (the machine they are building holds no key yet), which stays sound because the server refuses to fetch or cache a node artifact whose manifest does not verify, so the bytes the script digests were signed before they were ever served (unless you published the artifact into the downloads directory yourself: those sit outside the signed feed, digest-verified only, put there by this machine's own operator). And subshell update --from <path> is signature-free: a file you named is your decision. If your SUBSHELL_RELEASE_URL points somewhere else, you have moved delivery, not trust. Setting it empty disables every update and fetch path by name: the air-gapped configuration, where --from and hand-installed binaries are the whole story.

See also

Edit on GitHub

Last updated on

On this page