switchyardd Admin API — Reference¶
This is a curated overview of the switchyardd admin API. It does not
duplicate every field, request/response schema, or exact status code — the
authoritative, always-in-sync machine contract is the generated OpenAPI 3.1
document at GET /v1/openapi.json (view it interactively at GET /docs).
If this file and /v1/openapi.json ever disagree, the OpenAPI document
wins: it is generated from the handler annotations in
switchyardd/src/admin.rs, so it cannot drift the way hand-written prose
can.
Endpoints¶
| Method | Path | Purpose |
|---|---|---|
| GET | /v1/status |
Node status |
| GET | /v1/plugins |
Per-plugin state and capabilities |
| GET | /v1/routes |
Configured routes with policy/render detail |
| GET | /v1/config |
Loaded config as YAML (secrets unresolved) |
| GET | /v1/config/prev |
A retained previous revision (?n=1..5, newest first) |
| PUT | /v1/config |
Replace and apply the config (keeps up to 5 rotated backups) |
| POST | /v1/config/validate |
Validate a config document without applying it |
| POST | /v1/config/rollback |
Roll back to the previous applied config |
| GET | /v1/queue |
Queue counts, or a delivery listing with ?state= |
| GET | /v1/messages/{id} |
Delivery trace for one message |
| GET | /v1/public |
Publicly exposed services |
| GET | /v1/limits |
Configured quotas and transport budgets |
| GET | /v1/identities |
Verified identity links |
| POST | /v1/identities/link |
Request an identity link (sends a challenge) |
| DELETE | /v1/identities/link/{id} |
Remove an identity link |
| GET | /v1/identities/challenges |
Pending identity link challenges |
| GET | /v1/federation |
Federation peers |
| GET | /v1/discovery |
RFDP: this node's advert and known peer adverts |
| GET | /v1/events |
Live event feed (Server-Sent Events) |
| GET | /metrics |
Prometheus metrics (text exposition, not JSON) |
| GET | /v1/openapi.json |
This API's own OpenAPI 3.1 document |
/v1/events streams text/event-stream: ingress, delivery, plugin,
link_verified, config_applied, federation, and advert events, each
with its own payload shape described in the OpenAPI document rather than a
fabricated JSON request/response schema (SSE doesn't fit one). /metrics
is Prometheus text exposition, not JSON — see /v1/openapi.json for the
full metric name list.
Status codes actually returned (not aspirational ones) are documented per
operation in /v1/openapi.json — e.g. /v1/identities/link returns
202/400/409, /v1/config/rollback returns 200/404/409/500, DELETE
/v1/identities/link/{id} returns 204/404. Read the spec for the current,
authoritative set.
Transport & access¶
The admin API is served only over a Unix domain socket
(<data_dir>/admin.sock by default) — there is no TCP listener, and none
is planned for switchyardd itself. Every example below reaches that same
socket by a different path; none of them add authentication, because there
isn't any at this layer (see the next section).
switchyardctl— the bundled CLI client.switchyardctl status,switchyardctl routes, etc.switchyardctl openapidumps the raw/v1/openapi.jsondocument to stdout (byte-for-byte, unformatted, so it's pipeable straight to a file):switchyardctl openapi > relayfabric-openapi.json.switchyardctl docsprints the browsing recipe below, filled in with whatever--socketyou gave it.- Browse
/docslocally — forward a TCP port to the socket withsocat, then point a browser at it:
- Browse
/docson a remote host — tunnel the socket over SSH instead (nosocatneeded on the remote end):
- Headless / scripting —
switchyardctl openapi, orcurl --unix-socket <data_dir>/admin.sock http://localhost/v1/openapi.json, needs no browser at all.
The full machine-readable contract is always GET /v1/openapi.json; the
interactive, try-it-out UI is GET /docs (and /docs/) — a self-contained
Swagger UI with no external CDN or script host, since it only ever loads
/v1/openapi.json (a relative URL, so it works through any of the above
transports unmodified).
Access control & security model¶
There is no daemon-layer authentication or authorization on this API. That is a deliberate, not-yet-filled gap, and this document states it plainly rather than implying a protection that doesn't exist:
- Access control is entirely the OS filesystem's: the admin socket lives
inside
data_dir, whichswitchyarddcreates (and, if it already exists with looser permissions, tightens) to mode0700. A0700directory cannot even be traversed by another UID, so any process running as the same UID as the daemon has full admin access — every route above, including config replace/rollback and identity linking — androotalways does, regardless of UID. There is no per-route, no per-user, and no read-only tier: reaching the socket at all is equivalent to full admin. As a second belt on top of the0700directory, bothadmin.sockandplugins.sockare themselves explicitly locked to mode0600right afterbind()(bind()alone leaves a socket file's mode umask-derived, not tightened) — so even if the parent directory's permissions were ever loosened by mistake, the socket files stay owner-only. This is still filesystem-only, defense-in-depth on the same same-UID boundary described above, not a new one: it adds no authentication and no per-route/per-user distinction. switchyarddbinds no network listener by default. Nothing here is reachable over a network unless an operator deliberately exposes it (an SSH tunnel, asocatforward, a reverse proxy) — and doing so without adding an auth layer in front of it extends the "same-UID = full admin" trust boundary to whoever can reach that new listener.GET /docsinherits exactly this same socket boundary and adds nothing of its own: it's served by the same daemon, over the same socket, with no login screen, no session, and no separate permission check. Anyone who can reach/v1/openapi.jsoncan reach/docs, and vice versa.- Real authentication/authorization — TLS, WebAuthn/passkeys, RBAC, session
expiration, audit logging, and role separation that keeps
identity-linking permissions apart from route-management permissions
(
docs/webui-notes.md§78, since correlation data is more sensitive than routing config) — is explicitly the job of the separaterelayfabric-uiservice described indocs/webui-notes.md, fronting this socket. That service is not built yet. Until it exists, do not expose this API to anyone you wouldn't give a shell on this host.
The OpenAPI document's info.description states this same boundary, and
deliberately declares no securityScheme — inventing a bearer/OAuth scheme
the daemon doesn't implement would mislead client generators into thinking
one exists.
Security modes (sealed routing, SPEC §113)¶
A route's security_mode is gateway (default — this daemon reads and may
transform plaintext, SPEC §113.1's renamed TRANSLATE) or sealed
(SPEC §113.1's renamed OPAQUE: the origin edge gateway AEAD-seals the
payload — X25519 + XChaCha20-Poly1305, algorithm-tagged for future PQ
agility — for the destination edge gateway's sealed_key; every
intermediate/transit node carries ciphertext only). sealed requires every
destination to be a fed:<peer> peer with a config-pinned sealed_key
(federation.peers[].sealed_key); --check-config rejects a sealed route
otherwise. Node-level privacy.minimum_security (gateway default,
sealed) is a floor: --check-config rejects any route whose effective mode
falls below it, rather than silently allowing a downgrade.
A route's own security_mode: sealed is best-effort only — an operator can
still edit that same route to security_mode: gateway in place, and the
edit reloads cleanly (already-queued deliveries then egress as cleartext);
for a hard, reload-enforced guarantee that a route can never be downgraded,
set the node's privacy.minimum_security: sealed instead.
Downgrade refusal (§113.2): a sealed inbound message is never silently
decrypted onto a route that refuses to terminate it. That refusal is gated
by privacy.allow_gateway_decryption (node-level, default true) or its
per-route override — false means this route will not be a
sealed→plaintext termination point, and a sealed inbound message aimed at
it dead-letters SECURITY_DOWNGRADE_REFUSED, never translated.
allow_protocol_downgrade is parsed and stored (config-only, this phase)
but is NOT a separate enforcement point yet — phase-1's actual
downgrade-refusal gate is allow_gateway_decryption above; this is
documented rather than left to look enforced when it isn't. Rejected sealed
inbound (any reason — unsupported algorithm, tampered ciphertext, wrong
recipient, downgrade refusal, etc.) is never persisted to
/v1/queue?state=dead_letter: writing a just-decrypted refusal into a
queryable table would make the refusal cosmetic, not real. Watch the
relayfabric_sealed_egress_total / relayfabric_sealed_ingress_total /
relayfabric_sealed_rejected_total counters on GET /metrics instead.
Claim discipline (§113.6): sealed routing is zero-knowledge / blind
payload routing — NOT anonymity. Nodes still observe timing, sizes,
interfaces, and addresses. Nothing in this API, in switchyardctl, or in
any future WebUI may describe sealed mode as anonymous, hidden, or
untraceable — only as payload confidentiality between the origin and
destination edge gateways.
Privacy¶
- Identity references are masked in every response that isn't the raw
config itself — verification codes are never exposed by any API
response, and
/v1/identities/challengesreturns only a masked target ref and expiry. GET /v1/configreturns the raw YAML with secret references (${env:...}) intact and unresolved — never the resolved secret value. No handler on this API ever echoes a resolved secret back to a caller./v1/events(SSE) is an advisory, best-effort live feed for UI/tooling convenience, not a durable or complete audit log — a client that disconnects misses events emitted while it was away; use/v1/queueand/v1/messages/{id}for the persisted, queryable state.