Skip to content

API & OpenAPI docs

Everything the admin UI does, it does over Taranac’s own REST API — the web interface is just a client. That API is fully described by an OpenAPI schema generated from the code itself, and the platform can serve that schema as interactive documentation: Swagger UI, Redoc and the raw openapi.json.

That documentation surface is off by default. It is a single environment flag away, and this page explains what you get when you turn it on, why you probably should not leave it on in production, and where to browse it without installing anything.

One flag gates all three routes. Off (the default) — /docs, /redoc and /openapi.json return 404. On — they are served, and served without authentication.

The quickest way to see the API is the public demo instance, which runs with the docs surface enabled on purpose:

The demo runs the same images as your install, so what you read there is what your instance exposes.

RouteWhat it is
/docsSwagger UI — the flat, ordered tag list, with an Authorize button and try-it-out request forms
/redocRedoc — the same schema rendered as a reference document, with tags nested into domain groups (Network, AAA · TACACS+, NAC, Configuration Tracker, …)
/openapi.jsonThe raw OpenAPI schema, for client generators, Postman/Insomnia imports and API diffing

Note that all three are mounted at the application root, not under /api/. The bundled edge (the edge container) already proxies them; a third-party reverse proxy in front of Taranac that only forwards /api/* will not reach them.

DOCS_ENABLED lives in the .env file in your deployment bundle directory and defaults to false:

.env
DOCS_ENABLED=false # Swagger/Redoc/openapi.json — keep off in production

Set it to true and recreate the api container so it picks the new value up:

Terminal window
./taranac up -d # recreates containers whose env changed

To turn it back off, set it to false and run ./taranac up -d again. When the flag is off, FastAPI is built with no documentation URLs at all: the three routes 404, and the schema is never even generated.

When the flag is on, admins get an API item in the sidebar’s Advanced section that opens /docs in a new tab. The UI reads the flag from the backend (it is carried on /auth/me as docs_enabled), so the link can never point at a disabled endpoint — no flag, no link.

The schema is not raw FastAPI output — the tag taxonomy, the per-tag descriptions and the Markdown overview at the top are maintained in the product as a first-class surface, and CI fails if a route carries a tag that is not described. The conventions it documents are the same ones the whole API follows:

ConventionBehaviour
Base pathManagement endpoints live under /api/v1. What sits outside it: the health probes (/healthz, /ready), the three documentation routes above, and EST enrollment at /.well-known/est/. The deliberately public surfaces (captive-portal flows, CRL and certificate downloads) keep the /api/v1 prefix — they are public by their guard, not by their path.
AuthPOST /api/v1/auth/login returns a token; send it as Authorization: Bearer <token>. In Swagger UI, paste it into Authorize. A login that runs into a second factor answers mfa_required with a short-lived mfa_token instead — the session token then comes from POST /api/v1/mfa/verify.
Token purposeEvery JWT states what it is for. The Authorization header accepts purpose: session and nothing else unless an endpoint widens that explicitly — the forced-password-change token (password_change) opens POST /api/v1/auth/change-password, and the token redeemed from an e-mailed setup link (mfa_enrollment, 15 minutes) opens the enrollment endpoints. Anything else is a 401. The half-authenticated tokens are never bearer tokens at all: mfa and the portal’s byod_mfa are spent in the body of their own verify call, and the two are not interchangeable.
AuthorizationEndpoints are gated by RBAC permissions — GET /api/v1/rbac/schema lists them. Admin accounts bypass the gate.
ErrorsA consistent body { "detail", "code", "field" }, localized from the Accept-Language header (en / ru).
ValidationRequest bodies are strict: unknown fields are rejected with 422, lengths and ranges are bounded, blank optional text normalizes to null.
TimestampsStored and returned in UTC (ISO-8601); clients localize.
PaginationList endpoints accept page, per_page, search, sort_by, sort_order and return { items, total, total_by_filter, page, per_page, pages }.
Rate limitsNot global. A handful of endpoints an attacker would hammer — login, password reset, POST /api/v1/mfa/verify, the portal’s guest flows — are limited per client IP. Over the limit is 429 with a Retry-After header and the body { "detail", "retry_after" }, which is deliberately not the standard error envelope. /mfa/verify also carries a per-token budget: five wrong codes burn that token’s jti, and the check runs before the provider does, so a burnt token costs no TOTP maths and sends no push.
Response headersEvery response carries X-Request-ID (echoed from the request if you send one — quote it in a support ticket) and X-API-Version: 1. A dataset export adds X-Taranac-Export-Rows, plus X-Taranac-Export-Truncated: true when the row ceiling cut it short — check it, or a truncated export looks complete.

Tags are grouped by domain — Authentication & Access, Identity, Network, AAA · TACACS+, AAA · RADIUS, AAA · Policy & Logs, NAC, NAC · PKI, Configuration Tracker, Monitoring & Ops, System Settings, Reports, System — which is the fastest way to find the endpoints behind a screen you are looking at in the UI.

Not everything that talks to this API logs in. These surfaces are described in the same schema, and they are worth knowing before you write a client — or point a scanner at an instance and wonder why something answered:

SurfaceHow it authenticates
GET /.well-known/est/cacerts, GET /.well-known/est/csrattrsNothing, per RFC 7030. Like the two below, they answer only while the nac.est_enabled setting is on.
POST /.well-known/est/simpleenrollHTTP Basic — verified against the local users table by argon2id hash, so a directory-only account (which has no local hash) is refused — or mutual TLS.
POST /.well-known/est/simplereenrollMutual TLS only. Both enrollment routes answer 202 with Retry-After when the request lands in the approval queue instead of being issued.
GET /api/v1/public/crl/{ca_id}.crl and .pemNothing. This is the CRL distribution point embedded in every certificate the internal CA issues, so it has to answer clients that will never log in.
GET / POST /api/v1/public/cert-download/{token}The token in the URL. GET previews and spends nothing; POST consumes it, single-use.
GET / POST /api/v1/public/est-approval/{token}The one-time token from the approval e-mail. GET renders the request, POST decides it.
The captive portal’s guest flow under /api/v1/captive-portal/The guest session token; the sponsor’s decision arrives on GET /api/v1/captive-portal/sponsor/callback, rate-limited per IP. The /sessions routes on the same prefix are ordinary RBAC-gated admin endpoints.
/api/v1/mfa/internal/The X-Internal-Key shared secret — these are called by the RADIUS/NAC daemons during a live authentication, never by a browser.
/api/v1/ncm/collector/A one-time enrollment token, then per-request signing by the enrolled collector.

Mutual TLS reaches the API as headers — X-SSL-Client-Verify, X-SSL-Client-S-DN, X-SSL-Client-Serial — set by whatever terminates the client-certificate handshake. They are believed only when the same request carries an X-Est-Proxy-Key matching the nac.est_mtls_proxy_secret setting, compared in constant time; with no secret configured, mutual TLS counts as not deployed and those headers are ignored outright. The bundled frontend vhost blanks all four on every inbound request, so a client cannot smuggle its own.

  • CLI & scripts — the ./taranac wrapper and the .env file this flag lives in
  • Settings reference — the runtime settings surface, edited in the UI rather than in .env
  • RBAC — the permissions that gate every endpoint
  • Versioning & releases — what a version covers, and what that means for API stability