The HTTP API#
The agent serves one HTTP API and, optionally, the web UI from the same origin and the
same port. Everything is JSON, everything below /api/ needs a principal except five
routes, and every mutation needs the operator role and — for configuration changes — a
licence. This page is what holds for every endpoint; the
endpoint index is the list.
There is no OpenAPI document. Nothing in the tree generates one, so the endpoint index is the contract, and request and response bodies are documented only where they matter.
Base URL#
| Install | Base URL |
|---|---|
Appliance (ISO or .deb) | https://<router>:8443 |
Developer install (install/install.sh) | http://127.0.0.1:8090, or https:// if you gave it a certificate |
The shipped systemd unit passes --addr 0.0.0.0:8443 --tls-self-signed. The agent's
default with no flags is 127.0.0.1:8090 and no TLS. See
Ports and listeners.
Examples on this site use the convention SUPPORT.md uses:
R=https://192.0.2.1:8443
T=wh_0123456789abcdef0123456789abcdef0123456789abcdef
curl -sk "$R/api/system" -H "Authorization: Bearer $T"-k is there because the appliance's certificate is self-signed by default. Drop it
once you have installed a real one.
Content types#
Responses are application/json — writeJSON sets the header on every path, including
errors. Two exceptions:
| Endpoint | Content type |
|---|---|
GET /metrics | text/plain; version=0.0.4; charset=utf-8 |
GET /api/stream | A WebSocket upgrade, then JSON text frames |
Request bodies are JSON everywhere the agent reads one. Handlers that treat a body as
optional decode it and ignore the error, so a POST with no body gets the documented
defaults rather than a 400 — POST /api/commit with no body commits without a confirm
window, and POST /api/rollback with no body rolls back to revision 1.
The shape of an error#
Every error is a JSON object with one key:
{"error": "commit failed: Set failed"}Four responses carry a second key beside error:
| Extra key | On | Meaning |
|---|---|---|
license | Every 402 | The licence state: unlicensed, expired, revoked, invalid, or the literal feature when the plan lacks a feature. |
totp_required | POST /api/auth/login 401 | The password was right; a second factor is needed. |
totp_enrolment_required | 403 on any route, and on a successful login | The session may reach only the enrolment routes until a code proves the secret. |
command | POST /api/capture 501 | The monitor traffic command to run by hand. |
Messages are written to be read by a person. Where the failure came from the router,
the router's own words are passed through verbatim after a short prefix, so a 502 says
what the router said rather than what the agent guessed.
Request body limits#
A cap is applied in front of the mux, before any handler reads a body, because a cap that each new handler has to remember is the same hole again.
| Limit | Applies to |
|---|---|
| 1 MiB | Every route by default |
| 16 MiB | POST /api/stage, POST /api/stage/remove, POST /api/configure, POST /api/config/load, POST /api/reconcile, POST /api/fleet/{id}/configure |
| none | GET /api/stream, which hijacks the connection and frames its own reads |
The larger limit is spelled out per route rather than inferred, so adding a route does not silently widen it.
Timeouts#
| Setting | Value | Why |
|---|---|---|
ReadHeaderTimeout | 10 s | |
ReadTimeout | 60 s | |
WriteTimeout | none | The telemetry stream and slow op-mode calls legitimately outlive any figure that would suit a normal request. |
IdleTimeout | 120 s | |
| Router read budget | 20 s per browser request | One request may not spend longer than this talking to the router, however many reads it fans out to. |
Behind that, the agent holds three HTTP clients for the router: 30 s for reads, 3
minutes for /configure and /config-file, and 15 minutes for image pulls, reboots and
traceroute. A commit is deliberately not cancelled when the browser tab closes — the
caller must be able to find out whether it landed.
Transport and headers#
TLS is floored at 1.2 with an explicit suite list, and X25519 / P-256 / P-384 curve preferences; TLS 1.3 picks its own suites. Every response carries:
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: no-referrer
Cross-Origin-Opener-Policy: same-origin
Permissions-Policy: geolocation=(), microphone=(), camera=(), interest-cohort=()
Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline';
img-src 'self' data:; font-src 'self' data:; connect-src 'self' ws: wss:;
frame-ancestors 'none'; base-uri 'none'; form-action 'self'Strict-Transport-Security: max-age=31536000; includeSubDomains is added only when a
certificate is configured — sending HSTS from a plain-HTTP listener would be a
promise the agent cannot keep.
Observation, logging and panics#
Every request is counted and logged before it is authenticated, which is why the metric
label for a path the mux does not register collapses to /api/{unknown} rather than
minting a series per request. One line per request:
level=INFO msg=request method=GET path=/api/interfaces status=200 bytes=8123 ms=2 ip=192.0.2.44WARN at 4xx, ERROR at 5xx. A handler that panics is caught, logged with its stack,
and answered with 500 {"error":"internal error — see the agent log"} rather than
taking the router's management plane down.
What the API is not#
See also#
- Authenticating — the two credentials and the CSRF rule.
- Endpoint index — every route.
- Role matrix — who may call what.
- Status codes — what each code means here.
- What a read hides
- The telemetry stream
- Prometheus metrics
- The agent and the router — what sits where.
- Every command is shown
Checked against#
agent/main.go (runDaemon, routes, writeJSON, writeError,
writeStatus),
agent/security.go (securityHeaders, limitBodies,
bodyLimit, observe, recoverPanics, routeLabel),
agent/vyos.go (newVyosClient, clientFor, routerBudget),
agent/metrics.go,
docs/deploy.md,
docs/security.md.