On this page

Object authorization

The object API authorizes an action in two stages:

  1. the caller bearer selects one namespace policy;
  2. the object API verifier uses its own host-local Skarbiec grant to read the exact namespace credential items declared in object_api.namespaces.

The probierz policy must grant every prefix in queue::copy::CANONICAL_PREFIXES for get, put, list, stat and delete, because that list is what the queue reads and writes. stado config validate, config set and host config set refuse a policy that leaves one out, naming it, and stado doctor on the host fails object-auth with the same sentence. A prefix the binary writes without a grant on the host makes the object API answer every agent claim with 401 while the agent's capacity broadcast keeps saying the host is alive, so the gap is refused when the policy is written rather than found at run time.

The verifier bearer lives in WC_OBJECT_SKARBIEC_TOKEN_FILE on the object API host. To restore its grant without moving the bearer off that host:

stado repair stado --step object-verifier --target <target> --json

The command derives the item set from object_api.namespaces, asks Skarbiec on the target to bind the existing bearer to that exact set, and reports item names and expiry only. It never prints the bearer.

The verifier keeps each namespace's bearer with the version Skarbiec read it under (the item, its item_uid and its revision) and asks Skarbiec, on every object request, only for the item's current version (POST /v1/items/revision, which decrypts nothing). The same version answers from the kept bearer; a new one reads the bearer again. A rotated or replaced namespace bearer is therefore refused on the next request, with no grace window. A vault read that fails is a 503 object authorization unavailable; no bearer kept from earlier stands in for it. The release publisher's bearer is read on every request. The verifier grant needs only read of each item's token: the revision route takes the same grant. A vault older than item revisions answers no version, and the verifier refuses with the vault's answer names no item version, naming the Skarbiec release to install.

Release publication has a separate verifier and per-product policy set. Reconcile the verifier to the complete declared publisher set:

stado repair stado --step release-verifier --target <target> --json

The command compares the caller's release_api.publishers with the configuration the target actually consumes and refuses before mutation when they differ. It copies every controller-owned publisher item into the target-local shadows and binds the existing verifier bearer to exactly their token reads, removing capabilities for retired publishers without rotating or printing the bearer. Release preflight proves the caller credential with an authenticated operation under the same product prefix. A public releases/ stat proves neither boundary.

Use stado storage stat <stado-uri> --json as the smallest final check. present and absent are both authoritative answers. 503 object authorization unavailable means the verifier boundary failed; it is not evidence that the requested object is absent.

A publishing client's product declaration

A client publishing one product may set WC_RELEASE_API_PUBLISHERS to that product's entry. This client behavior requires Stado 0.16.35 or newer:

WC_RELEASE_API_PUBLISHERS='{"wisent-backend":{"item":"wisent-backend-release-publisher","prefix":"wisent-backend/"}}' \
  stado storage put stado://releases/wisent-backend/api/sha256/<digest>.tar.gz \
  <archive> --if-absent --content-type application/gzip

By default the client reads the selected item's token field with its configured Skarbiec consumer and grant file. An isolated publisher that already holds that product's bearer may instead set STADO_RELEASE_PUBLISHER_TOKEN_FILE to an owner-only file containing the bearer alone, without a trailing newline. The server still verifies it against the selected product's publisher item. An unreadable, empty or malformed explicit file fails by name without falling back to another credential. STADO_API_TOKEN remains the generic object credential and is not substituted for a release publisher.

Unrelated products need not appear in this client's environment, and an undeclared product still fails with release_api.publishers declares no publisher for <key>.

Invalid JSON or a malformed entry instead reports release_api.publishers is invalid: followed by the actual configuration errors. Fix that declaration; repeating the upload does not repair it. The serving release API and stado config validate still require their complete active-publisher table. This client override does not change server authorization or reconcile the server's grants.

The object API is the host Stado process

The object API is not a unit of its own. It is served by the one Stado process on the host, the unit com.wisent.stado, which runs $HOME/.stado/bin/stado, the managed binary every roll moves. So stado release version declare --host <host> --binary stado --version <v> followed by stado release version converge --host <host> --binary stado moves the object API with the rest of Stado, and the deploy-control-plane job needs no step of its own for it.

A host that still runs the object API under its former label hands it over: when the init system starts stado serve --api as com.wisent.stado, the process retires that predecessor before it binds, provided it serves the same storage root, and logs [stado] predecessor UNIT: STATE (DETAIL). stado release catalog declare-publisher --reload HOST=UNIT refuses any unit other than the host Stado process, because only that process serves the release API.

A unit's declared program must exist in the incoming archive. stado service update --from-archive checks the archive's member list and refuses a mismatch before relinking current, naming the declared program and available members. A successful extraction alone cannot prove that launchd can execute the installed program.

stado service update --from-archive and --rollback-to resolve the host, its SSH destination and the unit from the registry this machine already holds, never from the registry authority's snapshot: they are how the services that snapshot is served through (the vault, the resolver) are repaired while it stands unanswered. With a store on this disk that store is read; otherwise the last-known-good copy, announced on stderr as installing onto <host> from this machine's record from the last-known-good registry copy from <age>s ago (<path>, read_at <time>, generation <g>), without asking the registry authority. With no copy the command refuses: a local install resolves <host> without the registry authority, and this machine holds no registry to resolve it from: no registry document at <path>; any registry read that reaches the authority writes that copy. The channel key for the vault's own host is chosen from the same copy. --from-artifact reads the fleet's store and asks the authority as every fleet command does.

When the service's catalog installation recipe declares state through Stado after install (Brama's brama maintain schedule pinned to its host), stado service update declares it too, for the host it updated, under the id stado product install derives for that installation, so where it already stands nothing changes. --json reports each step under declarations ({"argv", "ran"}; null when the recipe declares nothing). A declaration that fails ends the update with <host>: <service> was updated, and the state its installation recipe declares could not be declared: <cause>, naming stado repair <service> --step declarations --target <host> --apply, which declares it on its own.

Reading a slow object API

Each stado serve component runs on its own thread with a single-threaded runtime, so the object API serves every connection on one thread, interleaved where a route waits: a route that holds the thread holds every request behind it. Each request it answers is logged with the time its route took: [dashboard] "<method> <path> HTTP/1.1" <status> - <milliseconds> ms, in the host Stado unit's log (stado service logs com.wisent.stado --host <host> --lines <n>). A client waiting on it sees the same stall from its side in stado resolver status: the stado-object-api connections the resolver still holds unanswered, each with the seconds it has waited.

A read of one object asks the store for that object's own metadata by listing its exact path. On a local store that listing walks only the entries of the object's directory whose name begins with the object's name, never the rest of its namespace, so one read costs that object alone, and every listing walks on the runtime's blocking pool rather than on the thread that serves requests.

Source: this website