On this page

Skarbiec and Stado

Skarbiec is Stado's credential service. Stado decides where a credential is needed, which process may request it, and how the consuming service is reconciled. Skarbiec owns the encrypted value, the item schema, recipient encryption, consumer grants, one-field reads, rotation, audit, and ciphertext synchronization.

The deployed fleet has one logical Skarbiec service. More than one host may carry an encrypted copy for availability or recovery, but they are not independent secret stores: Skarbiec synchronizes ciphertext between declared copies, while Stado places the service, publishes its address, selects the credential-store backend, and keeps consumers bound to the same logical service.

The boundary

Stado owns Skarbiec owns
Registry pointers such as account_ref Item ids, kinds, fields, context, tags, and recipients
Service placement and the service directory Per-recipient GPG ciphertext
The consumer identity assigned to each Stado boundary Grants authorizing one consumer, action, item, and optional field
Owner-only paths containing consumer tokens Minting, validating, expiring, and revoking those tokens
Delivery to a host or service without printing the value Decrypting an authorized field and returning only that field
Release, install, health, and reconciliation of the Skarbiec service Vault writes, audit, recovery, bonds, and ciphertext synchronization

Stado configuration and registry documents contain coordinates, never secret values. A checked-in configuration may name an item, field, consumer, token-file path, or logical service. It must not contain the value stored at that coordinate.

One logical service, several files

A path such as ~/.stado/skarbiec.vault.json identifies a file, not a separate product or authority. The logical service is established by three facts agreeing:

  1. the Stado registry declares the Skarbiec service and its placement;
  2. the Stado service directory resolves the consumer to that service;
  3. the answering Skarbiec instance serves the expected item and grants.

Replica and recovery files remain encrypted. Skarbiec's bond and sync contracts move the encrypted vault document or items resealed to the destination owner; Stado does not copy plaintext between hosts. A file with the same item id but outside the declared service and synchronization topology is not an alternative source.

Use stado credentials vault list to inventory which hosts carry Skarbiec vault files. The command reports only owner and count metadata; it does not retrieve item names or values. Use Skarbiec's own sync status and audit surfaces to establish whether replicas agree.

Each host's authority says which vault its own operations resolve to. A host that declares secrets.skarbiec.vault_file reports declared (or declared-absent when it does not hold that file). A host that declares no vault file but routes to the owner through secrets.skarbiec.url reports reads_owner: it holds no vault by design, and copies lists every vault file still on it — copies nobody reads, left to retire. On such a host Skarbiec itself refuses to open its fallback file ~/.local/share/skarbiec/skarbiec.vault.json unless the caller names it with SKARBIEC_VAULT_FILE. Only a host that declares neither is refused, with <host> declares no vault authority; add it to secrets.skarbiec.vault_file.

stado credentials vault show applies the same rule to the machine it runs on, and the owner check needs this machine's own registry name. When the directory names the fleet vault's owner but this machine's name cannot be read — its hostname matches no registry target, or the local registry snapshot cannot be read — the verdict is host_unreadable with the refusal which registry host this machine is could not be read (<cause>), so whether it may hold the fleet vault owned by <owner> cannot be judged, and the command exits non-zero. The local vault files are not judged in that state: a non-owner that cannot say who it is must not be told it owns the fleet vault.

On one machine, Stado writes through the vault named by secrets.skarbiec.vault_file. When nothing names one, it looks in ~/.local/share/skarbiec/skarbiec.vault.json, ~/.stado/skarbiec.vault.json and ~/skarbiec.vault.json. If more than one of those files claims the same owner and exactly one of them holds every item of the others at the same or a later revision, the others are stale copies: Stado uses that one and says so on stderr (owner vault: <path> holds every item of the other vaults of owner <owner> at the same or a later revision; using it). If an item exists only in another file, or another file holds it at a later revision, choosing would hide that item. Stado then refuses with this machine holds <n> vaults that all claim owner <owner>: … and names both files with their item counts. Nothing is merged.

A candidate file that exists and cannot be read stops discovery: Stado refuses with cannot read the vault candidate <path>: <cause>; no owner vault is chosen while one candidate cannot be read, and the credentials diagnostics and the fleet doctor's owner-vault check report the same cause, instead of passing over a file that may be the vault and choosing another candidate. A readable file that is not a vault object, or names no owner, is still not a candidate.

Retiring a copy

A copy can still hold the only instance of a credential — one written into it while something on that machine still treated it as the vault. Deleting the file by hand deletes that credential, so a copy is retired with:

stado credentials vault retire <path> --owner <vault-owner-host> [--apply] [--json]

Without --apply the command only compares. It reads the copy's item metadata locally and the owner's with skarbiec list --all over the host channel, and reports held_by_owner and every live copy item in missing_on_owner, each with a reason: only_in_copy (the owner holds neither its id nor its item_uid, which a rename keeps) or newer_in_copy (the copy's updated_at is later than the owner's). The owner's trashed items count as held, so nothing the owner removed on purpose comes back.

With --apply each missing item is decrypted locally and written to the owner with the same set-json path stado credentials item put uses: under the owner's id when the owner holds it older (keeping the owner's tags), under the copy's id with the copy's tags when the owner lacks it. When the owner's Skarbiec answers <holder> already holds exactly this payload, the owner has that secret under another id; it is listed under held_under_another_id with the owner's sentence. Anything the owner cannot take is listed under not_moved with why: a legacy envelope (run SKARBIEC_VAULT_FILE=<path> skarbiec upgrade --apply first), an id outside the host channel's alphabet such as a credential-lifecycle record directory:credential/<name>, or an owner refusal such as managed:weles is reserved for authenticated Weles writes. The owner's inventory is then read again, and the file is removed only when the owner holds every live item of the copy; otherwise it is kept and the command exits with refused (items it could not write) or infra_down (items still absent after a write).

Refusals: on a machine that declares secrets.skarbiec.vault_file (this machine declares its own vault …); on one that declares neither a vault file nor a route; a path with no file (no vault file at <path>, not_found); a file skarbiec vaults does not list as a vault on this machine; and a vault of another owner (<path> belongs to owner <a>, and the vault on <host> belongs to <b>: it is a different vault, not a copy of the fleet's, and nothing is moved or removed).

How a Stado process reads one field

The ordinary read path is deliberately narrower than a whole-vault or whole-item read:

  1. Stado selects the configured credential store. The persisted selector is credentials.store; STADO_CREDENTIALS_STORE may request a change but cannot silently switch the running process to a different store.
  2. The caller chooses one exact item id and field.
  3. Its Skarbiec binding supplies three values: service URL, consumer name, and owner-only token-file path.
  4. Skarbiec checks that the consumer grant authorizes the requested action, item, and field.
  5. Skarbiec decrypts that field with the recipient key available to the broker and returns only the field.
  6. The caller either uses the value in memory or writes it to its final owner-only runtime file. It must not put the value in argv, logs, registry documents, job documents, or machine responses.

For an operator-visible read through Stado:

stado credentials get github --field username

For the underlying Skarbiec CLI contract:

skarbiec get github --field username

Both commands keep the item id and field separate. get is value-bearing by definition; metadata-only inspection uses stado credentials ls, stado credentials vault items, skarbiec list, or the dedicated status commands.

Product identity and exact grants

Stado uses one Skarbiec identity for its coordinator, APIs, publication, signing and credential CLI: consumer stado, configured by secrets.skarbiec.url, .consumer and .token_file (or WC_SKARBIEC_URL, WC_SKARBIEC_CONSUMER, WC_SKARBIEC_TOKEN_FILE). stado config show reports the resolved endpoint, consumer and bearer path without printing the bearer. Every grant remains exact: a read of role#field requires the capability on the item that plays the role in the stado grant. A missing capability refuses the operation.

Secrets are asked for by role

Nothing in Stado's configuration, a release recipe, a job or a schedule names a vault item. Each names a role — what the secret is for, such as huggingface, cloud-gcp or a release's product name — and the vault decides which item plays it: the one live item carrying the Skarbiec tag stado:role:<role>. The item's own id is random, is read only after that selection, and decides nothing, so renaming or replacing an item changes no behaviour anywhere.

  • A read selects among the items the reading consumer's grant exposes. No item in the role reads as absent; two items in one role are refused (2 items … carry stado:role:<role>; exactly one item may play role <role>), because reading either would be a guess.
  • stado credentials item put --host <vault owner> --role <ROLE> --type <KIND> writes the secret for a role: the item playing it is rotated in place, and when none does a new item is created under a random id with the tag.
  • stado credentials grant add --host <vault owner> <CONSUMER> --role <ROLE> --field <FIELD> --token-file <PATH> grants a consumer one field of the item playing the role. A first agent grant translates every agent.skarbiec.secret_fields entry the same way when it is issued.
  • stado fleet doctor compares the roles the agent grant's items play with agent.skarbiec.roles; an exposed item that plays no role is shown as (no role).
  • Moving a host from item names to roles: stado config validate refuses a document that still holds agent.skarbiec.items. stado config migrate-identities carries that list into agent.skarbiec.roles (the names jobs read as role#field in agent.skarbiec.secret_fields are those roles), withdraws — and names — any secret_fields entry whose role the agent never held, sets backend.messaging.skarbiec.items to the notifier's roles (apns, fcm, device-registry, and email-provider where an email item was declared), and makes each publisher and deployer entry name its own product, in one validated write that preserves the prior document. stado release local install runs the incoming binary's config migrate-identities when that binary's config validate refuses the host, and installs only when the migrated document passes; a refusal names both answers. stado release catalog enroll <checkout> writes a product's roles into agent.skarbiec.roles and agent.skarbiec.secret_fields on every builder. The items themselves take their roles with stado credentials item retag --host <vault owner> <item> --tags …,stado:role:<role>.

An agent on a rented machine uses a scoped *-agent grant and agent.skarbiec; the coordinator bearer must not be projected onto that machine. A local agent may use stado. The roles the agent can read remain declared separately under agent.skarbiec.roles and backend.messaging.skarbiec.items; those lists are not identities.

The retired identity sections (credentials.admin, alerts.skarbiec, object_api.skarbiec, release_api.skarbiec, release.publisher_skarbiec, machine_api.skarbiec, service_api.skarbiec, rate_limit.skarbiec, integration.skarbiec) and per-role messaging URL, consumer and token settings are refused by stado config validate. A stale document cannot select an obsolete bearer.

For an existing host, inspect its grants with stado credentials grant show --host <target> stado --token-file <absolute-bearer-path> and the corresponding old consumers. Then merge them on the vault owner:

stado credentials grant consolidate --host <target> \
  --from <retired-consumer> --from <another-retired-consumer> \
  --token-file <absolute-existing-stado-bearer-path> --json
stado config migrate-identities
stado config validate

consolidate verifies the existing Stado bearer before writing, combines capabilities without placing bearer bytes on the command line or operator machine, and reads the resulting grant back. It refuses a missing grant, invalid bearer, or acquire/lifecycle capability that Skarbiec forbids combining with reads. It leaves old grants in place while readers cut over. migrate-identities removes retired keys in one validated atomic write, preserves the prior config beside it, and refuses an absent Stado bearer or invalid resulting document. Run it on each host that owns a config file. Revoke old grants only after the new readers and service status work; neither migration command revokes a running consumer.

The bearer file is an owner-only regular file. Never commit it, put its value in a unit definition, or pass its value on argv. The secret values it opens stay in Skarbiec.

Release publication

A release writer reads the token field of the product-named item in release_api.publishers as stado. stado release catalog declare-publisher <product> --owner <vault-host> --client <release-host> declares that item and grants Stado its field read on the vault owner. A declaration whose item differs from its product is refused. The product API bearer in that item is distinct from the Skarbiec bearer. A missing or revoked field grant refuses publication, including on a replica; a replica cannot widen its own permissions. Workers that receive a product publication bearer may still select it explicitly with STADO_RELEASE_PUBLISHER_TOKEN_FILE.

stado release catalog enroll <checkout> also checks the publisher of every immutable build input. The configuration reader has already verified that its item is the product name and its prefix is <product>/; enrollment does not infer the product from a role suffix. When that publisher's token is absent or its read is refused with 403 or 404, enrollment uses the existing fleet publisher declaration operation for that product. Other credential-service failures stop enrollment without rotating a bearer. declare-publisher, input enrollment and config migrate-identities all write the same product-named identity. They do not relax the prefix, item or grant validators.

Enrollment also grants the running service's own Skarbiec consumer exactly its runtime.grants on the vault owner and delivers that bearer to every rollout target. It reads the grant the vault records first (stado credentials grant show) and mints only when that differs from the declaration. A record it cannot read — a refused grant show, an answer that is not JSON or one without a capabilities list — is not read as an empty grant: the bearer is still minted, since a product enrolling for the first time has none, but the cause is printed and returned as recorded_grant_unread in the runtime-grant step.

Run node tests/release_publishers/migration.mjs --bearer-file <existing-stado-bearer> from the Stado checkout. It uses an isolated home under .build/real-tests/release-publishers/, checks the persisted publisher identity, preserves and checks the backup, repeats the migration, and requires an invalid product prefix to be refused without a write. Its report binds source, binary digest, commands and exits. Enrollment separately requires stado release catalog enroll and a consuming build; Stado Desktop's release operations use that same enrollment path.

Service discovery

Consumers do not carry a hand-maintained remote Skarbiec URL. Stado's service directory materializes the selected local forward as:

${STADO_FORWARDS_DIR:-$HOME/.stado/forwards}/skarbiec.local

The file is owner-owned, not group- or world-writable, and contains exactly one bounded https URL or one loopback http URL with an explicit port. Skarbiec's credential lifecycle client fails closed when the file is absent, unsafe, malformed, stale, or points to an endpoint it cannot use.

The forward is derived from the registry's service declaration. Editing skarbiec.local by hand creates a second address source and is not a repair; reconcile the service directory instead.

Fleet host accounts

A host's operating-system account is stored as a Skarbiec host-account item. The Stado target carries only:

{
  "account_ref": "host-account-item-id"
}

Stado follows that pointer when an operation genuinely needs the account. The item remains typed separately from a web login, so browser tooling cannot enumerate a fleet host account as a form credential.

To place one field on one host without printing it:

stado service token-file sync <service> --host <host> --item <item> --field password --token-file <basename>

The destination is owner-only. The calling consumer must hold the exact field grant, and the answering Skarbiec service must have loaded that grant.

Service runtime secrets

stado service secret sync synchronizes one Skarbiec field into one variable in one managed service's runtime environment file.

stado service secret sync <service> \
  --host <target> \
  --item <item> \
  --field token \
  --variable SERVICE_TOKEN \
  --env-file ~/.config/<service>/service.env

The runtime file is replaced atomically and remains owner-only. The value is never placed in argv or printed. Restart is the service's own operation, not this command's.

stado service auth check verifies the installed bearer against a read-only endpoint. With --repair, Stado synchronizes the field again and re-checks, so a stale bearer is repaired by the product rather than by hand.

A consumer's grants are declared, not remembered

A consumer that reads a credential needs a Skarbiec grant. Expressed only as flags somebody has to remember, a grant looks like this:

stado service grant sync brama --host <target> \
  --consumer oko-model-router-client \
  --capability read:oko-model-router#token \
  --token-file oko-model-router-skarbiec-token

Flags nobody remembers turn into grants issued from a shell, sometimes into a vault replica its owner overwrites, and Tama's block-one-off-repairs refuses that shape outright.

The grant belongs where the consumer is authorized: in the service directory, beside it.

"consumers": {
  "oko": {
    "capabilities": ["model-routing"],
    "grants": [
      {
        "consumer": "oko-model-router-client",
        "capabilities": ["read:oko-model-router#token"],
        "token_file": "oko-model-router-skarbiec-token"
      }
    ]
  }
}

stado service grant show <service> reads those declarations and prints what minting would use — the authorized consumer, the Skarbiec consumer, its capabilities, the token file and the audience — and mints nothing. stado service grant mint <service> mints every declared grant through the same path grant sync uses; --consumer narrows either to one. A service whose consumers declare nothing is refused with the exact path a declaration goes to, and an unauthorized consumer with the consumers that exist.

A declaration is written with stado registry set --path service_directory.services.<service>.consumers.<consumer>.grants --value <json>, and that write waits for the fleet: a host whose Stado predates the field parses the directory strictly, rejects a consumer field it does not know and resolves nothing at all, so the write is refused while any host declares a Stado older than 0.21.35, naming those hosts. A declared version is not an installed one — stado release version show --host <target> --binary stado says what a host actually runs, and stado release version converge brings it forward.

Jobs and scheduled workloads

A scheduled Stado command may declare scoped environment secrets as coordinates:

ENV_NAME=ROLE#FIELD

The schedule stores the role. Resolution happens at execution time through the workload's own consumer grant. Provider control-plane roles cloud-gcp, cloud-azure and cloud-aws must not be granted to a general workload agent; provider adapters receive their own identities.

Job JSON, queue state, output artifacts, status reports, and failure documents must remain plaintext-free. A missing or revoked grant fails that workload or route; it does not broaden access or substitute an ambient environment variable.

Stado's credential commands

stado credentials is the operator surface over the selected credential store:

stado credentials put <item> --type <kind>    # value document on stdin
stado credentials get <item> --field <field>
stado credentials ls --json
stado credentials rm <item>
stado credentials doctor --json
stado credentials vault items <vault> --json
stado credentials migrate --to <selector>
stado credentials grant show --host <target> stado --token-file <path> --json
stado credentials grant consolidate --host <target> --from <old-consumer> --token-file <absolute-path> --json
stado config migrate-identities
stado config validate

A backend change is a migration, not an environment toggle. If the requested selector differs from the persisted selector, normal credential access fails until stado credentials migrate copies the active items, verifies exact values, commits the selector, and removes the old source. A failed migration rolls back rather than allowing two active stores.

Use Skarbiec's CLI for Skarbiec-owned lifecycle operations such as consumer registration, acquisition, recipient sharing, rotation, recovery, audit, bond management, and ciphertext synchronization. Stado does not reimplement those contracts.

grant show reports the recorded capabilities and tests a named bearer without printing its value. grant consolidate refuses missing source grants and a bearer that cannot exercise Stado's existing grant; it preserves old grants for a verified cutover. config migrate-identities preserves the original file and refuses a result that fails validation or has no local Stado bearer.

Starting a program with its credentials

stado credentials run is the launcher a consumer is started through: the workload gets its own fields, in its own environment, and nowhere else.

stado credentials run \
  --env WISENT_APP_AGENT_AUTH_SECRET=role:jeden-agent-auth#agent_auth_secret \
  --env BRAMA_TOKEN=role:jeden-model-router#token \
  -- jeden run

Each --env NAME=ITEM#FIELD names one variable and the field it takes; ITEM is an item id, or role:ROLE for the one live item playing that role. The fields are read from the selected store, or, with --route <url> --consumer <consumer> --grant-file <file>, under that consumer's own grant, which asks Skarbiec for role:ROLE because a scoped consumer cannot list the vault. No value is printed, written to a file or put in argv. Stado then replaces itself with the program (execvp), so the program's exit status, signals and terminal are its own.

It refuses before starting anything: an --env that is not NAME=ITEM#FIELD or NAME=role:ROLE#FIELD (naming what is missing), a variable a shell cannot export, a variable named twice, a role no item or several items play, and an absent or empty field, each prefixed with the variable and the program it was for (BRAMA_TOKEN for jeden: credential item "…" has no non-empty string field "token"). A program that cannot be started is refused with the variables it would have received and the operating system's cause.

Failure behavior

Failure Result
Missing item or field The exact operation fails; Stado does not guess a replacement.
No item carries the role read (stado credentials get <role>) Refused as not found, naming the consumer and the tag stado:role:<role>: the item may exist untagged or outside the consumer's grant. Tag it with stado credentials item retag --host <vault owner> <item> --tags stado:role:<role> (retag replaces the item's tags, so list the ones it keeps beside it), or grant the consumer the item that carries it. --field does not help here; it is suggested only when the vault answers a whole-item read with 400 because it serves one field at a time.
Consumer lacks the field grant Skarbiec returns an authorization refusal; widening the caller is not an automatic repair.
Token file missing, empty, or not owner-only Stado refuses before sending the request.
skarbiec.local missing or unsafe Endpoint resolution fails closed; reconcile the service directory.
Skarbiec unavailable Secret-dependent operations fail; unrelated queue and compute work may continue when it needs no secret. The object API answers each request whose bearer it cannot compare with 503 and the server's own wait line as cause, and never holds it behind another request's read (Machine Interface).
A process holds the vault's keyring lock The vault's gpg waits on the lock and, when it gives up, answers 503 naming the holder (waiting for lock (held by <pid>); <lock> is held by pid <pid> on <host> (<what it runs>), taken <when>). The vault host's own object API then runs the declared skarbiec crypto repair once for that holder and reports it in /api/state.json; stado doctor names the holder and the repair; stado repair skarbiec --step crypto --target <vault host> --apply or stado host exec <vault host> -- skarbiec recover-daemons runs it on demand (Declared repair).
Requested store differs from persisted selector All ordinary access stops until verified migration completes.
An item listing that omits an item's deleted flag Refused as an invalid item-list response. Skarbiec's item list states the flag for every item and the file store has no trash, so a listing without it is not read as all live.
stado credentials migrate meets a source item listed without a type Refused by item id before anything is copied: the destination would have to store it under a type nobody stated.
stado credentials migrate names the store already in use The selector is committed and nothing is copied: credentials already live in <locator>: the selector is kept and nothing was copied.
Replica has the same item id but is outside the declared topology It is not used as a fallback. Repair service placement or synchronization.
Runtime secret is stale stado service auth check identifies the failing boundary; --repair performs one declared synchronization and recheck.

No path silently reads a checked-in .env, a cloud CLI login, an unrelated local vault, or a provider's ambient credential chain merely because the declared Skarbiec read failed.

Diagnostics

Use these surfaces in order:

stado capabilities --json
stado credentials doctor --json
stado credentials vault list
stado service directory show --json
stado service auth check <service> --host <target> --url <loopback-url> ...
stado host exec <vault host> -- skarbiec doctor
skarbiec status
skarbiec doctor
skarbiec audit-verify
skarbiec sync-status

stado host exec <vault host> -- skarbiec doctor runs the vault's own diagnosis where the vault is, through the host channel: the vault file, the audit chain, the GnuPG daemons, the canonical endpoint and the consumer grants, with nothing decrypted. stado host exec <vault host> -- skarbiec recover-daemons is the one repair approved beside it: Skarbiec's own GnuPG daemon replacement, which also releases a keyring lock whose holder can no longer let go; it reads no value, needs no unlock material and restarts no unit.

They answer different questions: whether the adapter exists, whether this host can open the selected store, where vault files live, where the service directory routes consumers, whether a service's installed bearer works, whether Skarbiec itself is ready, whether its audit chain is intact, and whether encrypted replicas agree.

Release and installation

Stado is also Skarbiec's release carrier. .wisent-release.json declares the native build recipes; Stado builds the supported platforms, stores immutable artifacts, signs release receipts, promotes candidate and stable receipts without rebuilding, and reconciles the selected release on hosts. The running broker is never overwritten in place with a mutable latest binary.

Release delivery and credential rotation remain independent. A Skarbiec binary can be upgraded without changing item values or grants; a credential or consumer grant can be rotated without rebuilding Stado or Skarbiec.

  • Grant — the consumer/item/field authorization boundary.
  • Security — what each Stado bearer opens and what never leaves a host.
  • Service directory — how skarbiec.local is materialized.
  • Configuration — the complete deployed configuration surface.
  • Operations — service and host reconciliation.
  • Integrations — support status and promotion requirements.

Source: this website