On this page

Security

Where does trust start and stop in a Stado deployment? This page names each credential class and what it opens, the shape of the host channel, what never leaves a host, how credentials rotate, and what enrollment actually proves. Command surfaces live in cli; this page is the boundary map.

Tokens and what each opens

There is no global dashboard bearer. Every API bearer is compared with a value held in Skarbiec. A missing verifier item is an error, not access: object, release and service routes report a redacted 503; host-health refuses with 401. These API bearers are distinct from Stado's Skarbiec bearer.

API bearer Verified against Opens
Object API token The configured namespace's Skarbiec item Object actions within that namespace and its prefix allowlist.
Release publisher token An item named exactly for the product in release_api.publishers Immutable release writes for that product prefix; the object token is not consulted.
Service deployer token An item named exactly for the product in service_api.deployers Declared service actions only.
Registry API client token The exact registry_api.clients.<client>.item and its token field Only the client's declared registry and cleanup actions. A Wisent account session does not open the API.
Host-health beacon token The token field of the item playing role host-health-api PUT /api/host-health only.

Stado's coordinator and API readers present the stado Skarbiec consumer, configured by secrets.skarbiec. Its grant lists exact item-field reads for the table above. Sharing the product identity does not grant access to an undeclared item or field. A missing capability names the item and leaves the dependent boundary shut. stado credentials grant show --host TARGET stado --token-file ABSOLUTE_PATH --json reports what the grant holds and whether the host's bearer matches. Verify a real request through the service route as well; a grant report does not prove that the consumer served it.

A rented workload agent and the host-health beacon retain scoped host-side grants. The beacon's stado-host-health-beacon consumer reads only its token item; an unreadable or over-broad grant refuses publication and leaves the prior beacon intact. These grants are not substitutes for Stado's coordinator bearer. A workload-agent grant cannot include cloud control-plane items.

Desktop's registry API client bearer comes from the owner-only file bound to its selected endpoint, normally ~/.stado/registry-api-desktop-token. It is not Stado's Skarbiec bearer. Missing client permission returns 401; a failed Skarbiec read or malformed client declaration returns 503. See Registry API access, Grant and Skarbiec and Stado.

The host channel

Every host mutation rides one shared SSH channel whose option set is derived from a single source (stado-rs/src/deploy/host_state/reboot.rs::ssh_reboot_argv, which deploy::host_channel::ssh_options derives from) rather than re-typed per command, so BatchMode=yes and StrictHostKeyChecking=accept-new cannot drift between the stado host and stado service commands. The remote program is fixed and narrow, it reports through the tab-delimited STADO_* marker protocol, and registry data never becomes a shell fragment — the splice rules that refuse it are in stado-rs/src/deploy/service/remote/validate.rs.

stado host exec selects a fixed approved operation; it is not a shell. Three independent barriers stand between operator input and the host, enforced in stado-rs/src/deploy/host_exec/allowlist/mod.rs:

  1. Character rejection. Every word must consist only of characters no shell treats specially; ;, |, &, $, backtick, quote, newline, redirection and glob are refused by name first.
  2. Exact allowlist match. The joined words must equal one approved entry exactly — no prefix match, no extra arguments, no operator-supplied path, because a command that took a path could read ~/.ssh/id_ed25519.
  3. Fixed argv. What runs is the matched entry's own compile-time argv of absolute paths. The operator's words select an entry; they never become part of the command line.

Extending the allowlist is a commit: each ApprovedCommand entry carries the fixed argv and a why field justifying unattended execution as the registry-managed login user. An entry without a defensible answer there does not belong in the table. Diagnostics are read-only; the fixed Probierz run-root preparation is the exception. No entry accepts an arbitrary path.

Account-owned diagnostics use fixed executable candidates, with home-relative paths expanded on the selected managed account. They do not start Brama's service launcher, acquire provider credentials or change an account.

Provider accounts belong to Brama

Host-exec does not approve provider sign-ins or chargeable model probes. It refuses an unapproved command before looking up the target or contacting a host, reports refused, and does not suggest that waiting will fix it. It does not keep compiled account identities, login references or cost consent.

Use Brama's account-bound sign-in with the selected subscription and an explicit reason. Brama Desktop offers Sign in on that account through its authenticated API; Weles runs the browser on the Stado-selected host. Stado Desktop's host observations remain diagnostics, not another way to authenticate a provider account.

Qualify the host-exec boundary

The real CLI journey lives in tests/host_exec/privacy.mjs:

STADO_HOST_EXEC_FIXTURE=<private-fixture-file> \
  node tests/host_exec/privacy.mjs --expected-revision <full-source-revision>

The fixture names binary, baseline_binary, baseline_revision, target, expected_hostname, home and config, and sets dedicated: true. Keep the fixture, home and configuration owner-only inside this checkout's ignored .build. The baseline must identify its exact source revision and still advertise the retired sign-ins and chargeable probe.

The runner snapshots both executables, records their digests and revisions, and reads the baseline's approvals without executing them. It checks that the candidate removed those approvals, then requests each retired operation against a fresh undeclared target and requires a non-retryable policy refusal. Real hostname -f reads before and after must match the independently supplied identity of the dedicated host. An unavailable host or wrong executable revision fails the run; it is not replaced by a local substitute.

The owner-only report under .build/host-exec-privacy-*/report.json retains commands, exit statuses and host receipts. This journey does not authenticate an account, send a model request, or qualify the HTTP or graphical surfaces.

GUI identity placement and Apple challenges

A trusted Apple device is a macOS user session, not a host-wide property. An apple-account identity binding therefore names both the target and the user. stado workload run gui-automation --target <target> with a status plan reports the current console user, the user whose TCC database carries the grants, the declared identity binding, the signed helper version, and separate gui-ready and apple-challenge-ready results. A helper installed for one user cannot make another user's session ready.

stado workload run gui-automation --target <target> with a grant-accessibility plan compiles the Apple challenge helper from the Stado source, signs it with the fixed com.wisent.stado.apple-challenge-capture identifier, installs it at /usr/local/libexec/stado-apple-challenge-capture, and grants the exact code requirement in the registry-bound user's TCC database. Its --preflight mode runs in that user's Aqua bootstrap, calls AXIsProcessTrusted() without the prompt option, and opens no consent window or Apple notification.

An authorized Weles login asks Stado to resolve both ends before it submits a password: one observed and drivable Apple-account holder, and the Weles broker on the execution target. Only after Apple displays the trusted-device prompt does the signed helper read it, and it reads structure, not words: a dialog (the accessibility dialog subrole) of an application in Apple's com.apple. bundle namespace. The allow prompt is the one with a default and a cancel button and no digits, and its default button is pressed; the code dialog is the one showing exactly one group of digits, which is written to an owner-only file under ~/.stado/work. Two such dialogs at once, or a code in a different process than the allow prompt, is refused by name, and the receipt lists every Apple dialog it saw with its digits masked. Stado removes that file after reading it and sends the digits only on standard input to skarbiec challenge-put on the execution host. The code never enters argv, the registry, a receipt, or a diagnostic.

What never leaves a host

Stored credential values are moved onto hosts or minted on them; they are not read back.

  • stado service secret sync in its item form resolves the Skarbiec item on the host, by the host's own Stado identity, so the value never travels on the channel and the operator's consumer needs no grant for it (deploy/service/ops/secrets/bearer.rs::sync_service_item_secret).
  • The value-carrying form reads the secret through the isolated service-verifier grant and carries it in the SSH request body; it is never printed or placed in argv (stado service secret sync --help). Existing unrelated variables in the env file stay on the host and never cross back to the operator.
  • stado service auth check with an item reference resolves the bearer on the host and reports only the HTTP outcome; the bearer itself never leaves the host (deploy/service/ops/secrets/bearer.rs::check_service_item_bearer).
  • Consumer grants can be reminted against the host's own authoritative vault and landed owner-only at their token path; the value never crosses the channel (deploy/service/ops/files/sync.rs::remint_consumer_grant_on_host).
  • SSH channel keys: stado fleet key generate prints the public half only; the private half never leaves the credential store (cli).
  • stado fleet key add <target> --from <path> describes the moved key by the SHA256 fingerprint and type ssh-keygen -lf prints. An answer without either is refused with that line (ssh-keygen -lf <path> answered no SHA256 fingerprint: …, … answered no key type: …) before anything is stored and before the source file is removed.

Rotation

Rotation is a first-class operation, not a re-enrollment:

  • Application credentials live in Skarbiec and are managed with stado credentials: put reads from STDIN, get is the one subcommand that renders a value, migrate moves every credential to a new backend and commits the selector, and token mint --host <owner> <CONSUMER> --capabilities read:<item>#<field> --audience <AUDIENCE> mints a consumer bearer for one field into an owner-only file (stado credentials --help).
  • Service runtime secrets rotate with stado service secret sync, which atomically replaces one variable in the unit's env file (a mode-600 atomic rename on the host, in the fixed remote program stado-rs/src/deploy/service/remote/scripts/query/secrets.rs) and restarts the service only when asked with --restart (stado service secret sync --help).
  • Channel keys rotate end to end with stado fleet key rotate, with rollback on failure (cli).

The rotation blast radius is the namespace boundary itself: the entitlements rotator holds its own object namespace (entitlements-rotator in ACTIVE_OBJECT_NAMESPACES, src/config/boundaries/object/mod.rs), so rotating one product's credential changes one Skarbiec verifier item and touches nothing another namespace uses.

Enrollment trust

A machine is not trusted because it answered SSH. Enrollment is agent-attested (examples, examples/fleet/): SSH reachability and hostname discovery create only a non-routable provisioning_targets entry, and the machine enters targets and a fleet only after the installed agent publishes fresh capacity with its Stado version. An unreachable host, failed installation, missing attestation, or registry conflict removes the provisioning entry and leaves no registered target. Once the registry catalog sets require_agent_attestation, the validator also rejects every local target without a valid receipt.

The join API is equally narrow: both invite-token routes are authorized by the invite token alone, and POST /api/fleet/join writes a pending request — it creates no registry entry and cannot modify one (cli).

The unauthenticated report is bounded before its body is read by dashboard.request_limits.fleet_join.request_bytes, including requests with query strings. Its trimmed string fields are bounded by fleet_join.field_bytes in the same declaration. Both values are required at API startup. Size and shape refusals name the failed field or body bound; they do not accept a token, spend an invitation or create an enrollment. The token check remains required after input validation, and hostname traversal checks remain in place.

The money boundary

Nothing in Stado links a billing account or raises spend by itself. Stado's billing credentials are readers — the Azure billing principal carries Billing account reader plus subscription Billing Reader, resolved from Skarbiec with exact per-field capabilities (cli) — and the autonomy loop carries a budget guard that blocks new cloud placement outright when the cost forecast exceeds the configured budget (stado-rs/src/coordinator/passes/autonomy.rs, autonomy). Spend follows submitted work under the scheduler's own cost limits (costs); it is never a side effect of holding a token.

Source: this website