Appearance
Secrets HTTP API
This is the reference for the Secret Store HTTP surface. It maps the FetchNodeSecret operation to its OpenAPI schema, the NSK authentication seam, the ReBAC gate, the response headers, and the closed Problem.code taxonomy, and the ListProjectSecrets inventory operation (the secrets OpenAPI tag) to its metadata-only contract. The wire-contract origin is api/openapi/plexsphere-v1.yaml; this doc is a map, not a duplicate contract.
The Secret Store is the platform's only meet-and-rewrap point between the OpenBao backend and the per-Node Node Secret Key (NSK). A fetch reads the backend payload, recovers the calling Node's NSK, and rewraps the payload in-process so that only NSK-wrapped ciphertext ever crosses the wire. The cleartext payload and the recovered NSK are zeroed on every exit path; nothing is persisted, logged, cached, or placed on the event bus. For the bounded-context narrative — the ubiquitous language, the byte-level rewrap diagram, the rate-limit matrix, the audit schema, and the threat model — see the Secret Store context reference under ../../contexts/.
Operations
| Method | Path | Operation ID | Auth | ReBAC gate | Audit relation | Body cap |
|---|---|---|---|---|---|---|
| GET | /v1/nodes/{id}/secrets/{name} | FetchNodeSecret | NSK bearer | read on secret:<id> | secrets.read (one granted row per authorized read) | 1 MiB response |
| GET | /v1/projects/{project_id}/secrets | ListProjectSecrets | operator bearer | read on project:<id> | secrets.project_inventory_list | n/a |
ListProjectSecrets returns a name-ordered, cursor-paginated page of inventory metadata for the owning Project — each entry carrying only the secret name and its current_version, never a payload, an OpenBao path, or any wrapping material, so an operator can audit what a Project exposes without ever seeing a value. The application service owns the project:<id>#read hard gate and the granted / insufficient_relation audit emission, exactly like the fetch service; the pagination cursor is opaque and HMAC-signed, so a tampered cursor surfaces as 400 on the next call. The remainder of this page describes the FetchNodeSecret rewrap pipeline.
The ReBAC relation the gate checks is read on the object secret:<secret-uuid>; the audit relation stamped on the emitted row is secrets.read (the operation name, distinct from the ReBAC relation). The authorization subject is the canonical node subject node:<node-uuid> — the Node identity the NSK middleware authenticated; a Project-scoped service-identity translation is a deferred seam. These values are the running-binary contract; see the Secret Store context audit schema for the full row shape.
- The response body is the raw envelope
<12-byte nonce> || <ciphertext + 16-byte GCM tag>, served asapplication/octet-stream. The caller recovers the seeded cleartext byte-for-byte withAES-256-GCM-Openunder its NSK. - The handler ships behind a fail-closed scaffold gate — when the Secret Store backend is not configured (the
PLEXSPHERE_SECRETS_OPENBAO_MOUNTopt-in is unset), the surface is disabled and every request returns501 secrets_not_provisioned. The wiring is opt-in on the OpenBao mount (notPLEXSPHERE_DSN): the surface is either fully wired or fully off.
Authentication
FetchNodeSecret does not use the operator bearer scheme. It authenticates the Node against the per-Node NSK plaintext supplied in the Authorization: Bearer header, exactly like the heartbeat and endpoint-observation surfaces. Only a Node proving possession of its NSK can reach the rewrap pipeline. A missing, malformed, or revoked credential surfaces as 401 unauthorized before any persistence read.
Path & query parameters
| Parameter | Type | Required | Notes |
|---|---|---|---|
id (path) | string (uuid) | yes | Node identifier (UUIDv7) — the secret-fetch scope. |
name (path) | string | yes | Secret name within the owning Project. Lower-case, starts with a letter, then letters, digits, hyphen, and underscore, 1 to 63 characters (^[a-z][a-z0-9_-]{0,62}$). |
version (query) | integer | no | OpenBao KV v2 version to fetch. Minimum 1. Latest-only in the running binary — the selector is accepted but the production backend serves the latest version regardless (see Version selection). |
Authorization (header) | string | yes | Bearer <NSK plaintext> — the per-Node Node Secret Key. |
Version selection
The running binary is latest-only. The production backend shim reads the latest KV v2 version through OpenBao's KVGet and has no versioned read, so:
- When
versionis omitted, the latest version is served. - When
versionnames a specific version, the selector is accepted but ignored — the read degrades to "serve latest" rather than failing, and the version the backend actually returned is reported on the response header. A real versioned read is a deferred seam that lands when the platform OpenBao client grows aGetVersioncapability; the version-aware path is held open in the backend shim (secretsKVBackendin../../../cmd/plexsphere/secrets_factory_prod.go) but is not wired today.
The version actually served is always reported in the X-Plexsphere-Secret-Version response header, and that served version wins over the metadata current version when the backend has promoted a version server-side.
Success response
A 200 carries the rewrapped envelope plus three required response headers:
| Header | Meaning |
|---|---|
X-Plexsphere-Secret-Version | The version actually served (the backend-returned version). |
X-Plexsphere-Secret-KID | The NSK key id used to wrap the envelope, so the caller can pick the right NSK to unwrap with during a key-rotation overlap window. |
Cache-Control | Always no-store — the ciphertext envelope must not be retained by any intermediary or client cache. |
The 200 response is marked with the x-plexsphere-no-payload-logging vendor extension. The extension marks the contract guarantee that the opaque ciphertext body is never echoed onto a loggable rendered surface; a Spectral rule refuses any future change that would name the secret payload in a response description or example.
Error taxonomy
All error responses use the shared Problem envelope (application/problem+json); the 403 path uses the PermissionDenied shape for the ReBAC denial. The closed Problem.code set this surface emits:
| HTTP status | Problem.code | Trigger |
|---|---|---|
| 401 | unauthorized | NSK in the Authorization: Bearer header is missing, malformed, or revoked. |
| 403 | (PermissionDenied) | The NSK authenticates but the calling Node (subject node:<id>) lacks the read relation on secret:<id> that gates visibility of the addressed secret; the denial carries reason = insufficient_relation and the correlation_id that pairs with the audit entry. Surfaced only after authentication, so the endpoint cannot be used as a secret-name oracle. |
| 404 | secret_not_found | No metadata row exists for the addressed Project and name. No audit row is emitted — nothing was authorized. |
| 404 | secret_version_not_found | The backend reports the secret's value is absent at the served (latest) version — the metadata row exists but the KV value was never written or was deleted. |
| 429 | per_node_rate_limited | The per-Node fetch rate-limit bucket is exhausted; the Retry-After header carries the wait in seconds. |
| 429 | per_domain_rate_limited | The per-Domain fetch rate-limit bucket is exhausted; the Retry-After header carries the wait in seconds. |
| 503 | openbao_unavailable | The backend is sealed or unreachable for an authorized read. A backend-unavailable read that follows a granted authorization still emits one granted audit row. |
| 501 | secrets_not_provisioned | The OpenBao mount opt-in (PLEXSPHERE_SECRETS_OPENBAO_MOUNT) is unset, so the rewrap pipeline is disabled in this build. |
| 500 | internal | Server-side failure path; the wire body stays generic and no backend or driver text is interpolated into it. |
The 429 and 503 arms distinguish operational outcomes (carried on the fetch-duration metric and the rate-limit counter) from authorization outcomes (carried on the hash-chained audit log). A read that was authorized but failed at the backend still records a granted audit row, so the audit chain reflects the authorization decision rather than the backend's health.
Cross-references
./nodes.md— sibling Fleet read surface; the secret fetch endpoint hangs off the same/v1/nodes/{id}root as the mesh heartbeat, endpoint-observation, and capability surfaces../node-events.md— the mesh tag's signed SSE replay and reconciliation surfaces that share the per-Node NSK authentication seam.../../contexts/— bounded-context references, including the Secret Store narrative that explains the rewrap pipeline, the rate-limit matrix, and the threat model../index.md— platform-wide/v1HTTP surface map.../../../api/openapi/plexsphere-v1.yaml— authoritative OpenAPI contract; this doc is a map, not a duplicate.../../../internal/secrets— the Secret Store bounded context: the domain aggregate, value objects, the fetch application service, and the no-plaintext-column persistence layer.