A loopback broker with exact capabilities.
The HTTP surface binds to loopback, authorizes every operation against an exact structured capability, and returns stable failure classes without turning absence into permission disclosure.
Transport and identity#
serve binds 127.0.0.1:8787 by default. There is no remote listener and no TLS termination in this binary; the boundary is the loopback interface. A deployment that exposes it through a tunnel or TLS edge owns that transport, firewall, certificate, and supervision boundary.
Consumer routes authenticate with two headers: X-Consumer naming the consumer, and an Authorization bearer header carrying its bearer. The grant must carry the exact capability the route checks. Direct consumers present an opaque bearer whose hash is registered in the vault. Acquisition consumers present a signed workload proof to issue a short-lived, one-use bearer; they never receive the owner's decryption authority.
Mutating routes serialize process-wide behind a write lock, and every request opens the vault fresh, so a grant minted while the broker is running takes effect on the next request. All routes run through a fixed worker set and a bounded queue; a full queue is refused before it can consume cryptographic or descriptor capacity.
Health#
- GET /livez reports process liveness only, touching no vault, keyring, or journal.
- GET /readyz proves the journal can lock and write, opens the vault, and decrypts the deterministic and configured canary items. It reports current and maximum cryptographic and GPG executor occupancy, and answers 503 when the infrastructure beneath it cannot serve.
- GET /health is a compatibility alias for readiness.
Items, fields, and identity#
- POST /v1/items/list returns metadata for items covered by the caller's read grants; it never returns values.
- POST /v1/items/read accepts one id and one field and requires read:item#field. A trashed item is 410, a pre-v2 envelope is 409, ciphertext that will not open is 503, and an in-flight adopt candidate is refused rather than spent.
- PUT /v1/items requires stage:item#field. acquire mode creates a provider-verified managed item; stage mode updates a field controlled by that exact writer.
- DELETE /v1/items requires trash:item and refuses lifecycle-owned or differently controlled records.
- POST /v1/tokens/introspect requires introspect and answers what an inbound bearer is — an identity and its capabilities, never a value. It exists so a gateway can decide whether to serve a request without keeping a copy of every credential in the fleet.
Acquisitions and credential operations#
- POST /v1/acquisitions takes X-Consumer and the workload proof fields and sends no standing authorization bearer at all. It verifies the proof and issues an opaque short-TTL bearer bound to that workload, consumer, item, and field.
- POST /v1/acquisitions/read presents the issued bearer, returns only the bound field, and atomically consumes the bearer before the value is returned. Replay, expiry, or any binding mismatch is unauthorized. The answer also carries the item's declared provider when the item declares one, and omits that key entirely when it does not, so absence cannot be read as a declaration. Only provider is disclosed; the rest of the item's context stays sealed and still requires a read capability naming it.
- POST /v1/credential/operations requires lifecycle:item and submits or resumes one operation. The body carries no directory identity and no provider: both come from the sealed item contract. adopt is refused here because its password exists only on operator stdin.
- GET /v1/credential/operations for one item id requires the same lifecycle capability and returns the persisted state with its receipt and quarantine block. The poll can commit a staged revision, so it serializes with every other writer. lifecycle never authorizes reading a credential value.
Synchronization, donations, and enrollment#
- GET /v1/vault requires sync:pull and serves the whole ciphertext vault document; items are served exactly as stored and are never decrypted here.
- GET /v1/owner-pubkey needs no capability and returns the vault owner's armored public key, so a donor can seal a donation to this vault. The public half is not secret.
- POST /v1/donations requires donate for that exact item id and enqueues one sealed item into the donation inbox for owner review. An existing id admits the donation only when its recorded writer matches the donor's claim.
- POST /v1/enroll requires enroll for that exact uid. A replica sends its armored public key and item ids, and the source registers the key and re-seals exactly those items to it.
Unversioned aliases and operator routes#
Three unversioned routes remain reachable: a metadata listing, an item-count summary, and a grant-gated login resolution whose emitted values land in an owner-only file rather than in the response. All three are gated.
The metadata listing and the item-count summary are the pre-versioned spellings of the item index, and they now answer through the same consumer grant and the same per-item filter as the versioned item listing. A caller presenting no usable grant is refused with 403 and a body naming a required consumer grant. A caller presenting one sees only the items its read grants cover, and the summary counts that filtered set rather than the whole vault. They were previously left ungated on the strength of a documented claim that existing callers depended on them; a search across every product in this workspace found no caller of either, so the claim was retired and the routes were gated. They stay reachable rather than deleted so that a caller nobody found reads a refusal naming the missing grant instead of a not-found that could equally mean a wrong port or a dead daemon.
The versioned operator prefix serves the local console with the same authority as the CLI. Every route is a POST with a JSON body that may name its vault for that request; the local keyring decides what opens. Item reads and writes, TOTP and route resolution return their actual results. POST /v1/operator/grants/issue returns the complete grant issuance document, including the bearer for a direct grant or the acquisition instructions for a workload-bound grant. The stored grant still keeps only its hash.
Failure contract#
Statuses and the 400-character bound on a failure detail are Skarbiec's own. Every error code is drawn from the fleet's shared failure package, pinned by commit, so a caller deciding whether to retry reads one definition rather than parsing prose. Four codes are emitted: not_found, config, rate_limit, and infra_down.
- 401 means the presented bearer or workload proof does not authorize this exact call. A refused acquisition names no failed check.
- 403 means the consumer is not authorized for the exact item and field; existence is deliberately not disclosed.
- 409 with config identifies a conflicting state or an unsupported legacy shape, such as a pre-v2 envelope.
- 410 with not_found identifies a trashed item, and the detail names the restore that would bring it back.
- 503 with infra_down identifies infrastructure that cannot open or serve the encrypted value; 503 with rate_limit identifies a full request queue and is not a statement about the caller's authority.