Skip to content

Bootstrap-tokens HTTP API

This is the reference for the bootstrap-tokens HTTP surface. It maps each operation to its OpenAPI schema, the per-call ReBAC permission gate (or, for the registration seam, the BootstrapToken precedence check that replaces it), the audit-row taxonomy, and the closed Problem.code set the handlers emit.

The wire-contract origin is api/openapi/plexsphere-v1.yaml; this doc is a map, not a duplicate contract. For the bounded-context narrative (the psb_<env>_<token-id>_<kind>_<random> plaintext format, the Argon2id hash-only persistence, the one-shot/idempotent consume protocol, the registration five-step atomic commit, the IP allocator semantics, the NSK issuance and wrap ledger) see ../../contexts/identity/bootstrap-tokens.md and ../../contexts/identity/registration.md. For operator recipes see ../../how-to/enrolment/issue-a-bootstrap-token.md and ../../how-to/enrolment/register-a-node.md.

Operations

MethodPathOperation IDReBAC gateAudit relationBody cap
POST/v1/projects/{project_id}/bootstrap-tokensIssueBootstrapTokenproject#manage on parent Projectbootstrap_token.issue8 KiB
GET/v1/projects/{project_id}/bootstrap-tokensListBootstrapTokensproject#read (gate) + per-row bootstrap_token#read filterbootstrap_token.listn/a
GET/v1/projects/{project_id}/bootstrap-tokens/{id}GetBootstrapTokenMetadatabootstrap_token#readbootstrap_token.readn/a
DELETE/v1/projects/{project_id}/bootstrap-tokens/{id}RevokeBootstrapTokenbootstrap_token#manage (or project#manage)bootstrap_token.revoken/a
POST/v1/registerPostRegister(none — BootstrapToken plaintext is the credential)node.register8 KiB
  • IssueBootstrapToken returns the plaintext exactly once in the 201 response. Persistence stores only the Argon2id hash; once the response body has been written the plaintext can no longer be retrieved through any API surface. The Spectral plexsphere-write-once-post-must-be-issue-response rule guards the token field's x-plexsphere-once: true marker.
  • RevokeBootstrapToken is a terminal aggregate transition — a token that is already consumed or already revoked rejects the call with 409 so the operator can distinguish "I beat the redeemer" from "the redeemer beat me".
  • PostRegister is the unauthenticated bootstrap seam. The BootstrapToken plaintext IS the credential; the caller does NOT pass an Authorization header. Denials surface from BootstrapToken validation (a Problem, NOT a PermissionDenied), not from a ReBAC check. The endpoint sits on the authn middleware bypass list for the same reason /v1/auth/sign-in does — a substrate without credentials must be able to reach it in order to obtain them.

Issuance invariants

The aggregate enforces three issuance invariants on IssueBootstrapToken:

FieldConstraintFailure
kindMust be one of node, bridge.400 invalid_kind
env_prefixMatches ^[a-z]+$.400 invalid_env_prefix
ttl_secondsInside [300, 86400].400 invalid_ttl

Violations surface as 400 from validation, not as a SQL CHECK failure — the validator runs before any database access, so a bad issuance request never produces a partially-persisted aggregate.

Registration atomic commit

PostRegister performs the following in one pgx transaction:

  1. Public-key gate. The inbound 32-byte X25519 public key is validated for length (after base64 decode) and rejected if it matches the all-zero small-order point. This gate runs before any token consumption attempt so a malformed key cannot also waste a token.
  2. BootstrapToken consume. The plaintext is run through bootstraptokens.Validator.Consume for one-shot, project-scoped, kind-scoped enforcement. A canonical denial reason (see error taxonomy) surfaces as 403.
  3. Mesh-IP allocation. Drawn from the Domain pool by default, from the optional Project sub-range when one applies. Pool exhaustion or allocator contention surfaces as 503 and does NOT consume the BootstrapToken.
  4. NSK issuance. The per-Node Node Secret Key plaintext (32 bytes, base64) plus the wrapped persistence form is produced through the configured wrap-key adapter. The plaintext is returned to the caller exactly once (x-plexsphere-once: true).
  5. Persist. Node + IP allocation + NSK + outbox event are persisted in the same transaction. A partial failure rolls back every step including the BootstrapToken consume, so the operator can retry the same plaintext after fixing the root cause.

Path & query parameters

OperationParameterTypeRequiredNotes
every Issue/List/Get/Revokeproject_id (path)string (uuid)yesOwning Project (UUIDv7). Shared BootstrapTokenProjectID parameter component. Malformed → 400 invalid_project_id.
Get / Revokeid (path)string (uuid)yesBootstrapToken identifier (UUIDv7).
ListBootstrapTokenscursor (query)stringnoOpaque HMAC-signed continuation. Tampered → 400 invalid_cursor.
ListBootstrapTokenslimit (query)integerno[1, 200], default 50. Out-of-range → 400 invalid_limit.

Schemas

The OpenAPI spec is the authoritative source for field shapes. The schemas this surface uses are:

  • Issuance: BootstrapTokenIssueRequest, BootstrapTokenIssueResponse (plaintext one-shot), BootstrapTokenMetadata (read view), BootstrapTokenList.
  • Registration: RegisterRequest, RegisterResponse (carries node_id, mesh_ip, signing_public_key, signing_key_id, nsk, peer_snapshot, domain_mesh_cidr), RegisterPeer.

The RegisterResponse.nsk and the BootstrapTokenIssueResponse.token fields are the only /v1 fields marked x-plexsphere-once: true; the Spectral guard rule keeps the marker pinned so a future spec edit cannot accidentally relax the one-shot semantic.

Error taxonomy

All error responses use the shared Problem envelope (application/problem+json). The 403 path on the authenticated operations uses the richer PermissionDenied shape carrying the ReBAC denial reason, traversed relation_path, and request correlation_id. PostRegister is the exception: its 403 body is a plain Problem because the denial originates from BootstrapToken validation, not from a ReBAC check.

CodeStatusWhereMeaning
invalid_project_id400every operationMalformed project_id UUID.
invalid_kind400Issuekind not in {node, bridge}.
invalid_env_prefix400Issueenv_prefix does not match ^[a-z]+$.
invalid_ttl400IssueTTL outside [300, 86400].
invalid_cursor / invalid_limit400ListPagination guards.
public_key_invalid400RegisterPublic key length wrong, all-zero, or unparseable. BootstrapToken NOT consumed.
kind_mismatch403RegisterToken kind does not match the redemption attempt.
project_mismatch403RegisterToken issued for a different Project.
token_consumed403RegisterPlaintext was already redeemed.
token_expired403RegisterTTL elapsed.
token_revoked403RegisterOperator revoked the token before redemption.
nonce_collision403RegisterReplay nonce already used inside the Project.
not_found404Get / Revoke / RegisterBootstrapToken, Project, or Resource not resolvable. The Register branch does NOT consume the token.
token_terminal409RevokeToken is already consumed or revoked.
register_invalid422RegisterBody parsed but failed application-boundary invariants (empty plaintext, zero project_id, empty resource_id).
pool_exhausted / subrange_exhausted / allocator_contention503RegisterMesh-IP allocator could not place the Node. BootstrapToken NOT consumed.
internal500every operationServer-side failure path. NSK issuance, outbox append, or any other infrastructure fault.

token_consumed and nonce_collision are deliberately surfaced as 403 rather than 409 so the redeemer cannot distinguish "I have already used this token" from "someone else already used this token" via the status code alone — the canonical denial reason in Problem.code is the disambiguation, but only callers who already know the plaintext can interpret it.

Cross-references

  • ../../contexts/identity/bootstrap-tokens.md — bounded-context reference for the BootstrapToken aggregate, the plaintext format, the Argon2id hash-only persistence, the one-shot Validator/Consume protocol, and the issuance invariants.
  • ../../contexts/identity/registration.md — bounded-context reference for the registration five-step atomic commit, the NSK issuance and wrap ledger, the IP allocator semantics, and the threat model for the unauthenticated bootstrap seam.
  • ../../how-to/enrolment/issue-a-bootstrap-token.md — operator recipe for issuing a BootstrapToken via plexctl.
  • ../../how-to/enrolment/register-a-node.md — operator recipe for redeeming a BootstrapToken end-to-end against the kind dev, including the X25519 keypair generation, the NSK persistence, and the verification checklist.
  • ../../../api/openapi/plexsphere-v1.yaml — OpenAPI 3.1 spec; the BootstrapToken* and PostRegister operations and the BootstrapTokenIssueRequest / BootstrapTokenIssueResponse / BootstrapTokenMetadata / BootstrapTokenList / RegisterRequest / RegisterResponse / RegisterPeer schemas.
  • ../../../tests/e2e/identity/register/ — Chainsaw e2e suite that issues + redeems + replays a Node registration end-to-end, including the ciphertext / plaintext round-trip on a fixture Project secret.