Wingsmith
Overview

Authentication

Control plane API authentication: principals, sessions, refresh tokens and service credentials.

This page covers the control plane API. Calling a model through the gateway uses a tenant key instead; see Calling the gateway.

Every route under /v1 requires Authorization: Bearer <token>, except the three auth verbs themselves. /health, /metrics and the OpenAPI routes are outside /v1 and need no credential.

Principals

Four principal kinds exist, and the kind is the whole of a principal’s authority. What each kind may reach is decided in one function on the plane, deny by default, matched on the route template instead of the raw path. There are no per-route scopes or roles.

KindWhoCredentialReaches
adminthe operator of this plane, through the console or hgra session from POST /v1/auth/login with the admin passwordevery route except the gateway’s three
tenanta tenant’s own console sessiona session from POST /v1/auth/login with the tenant’s slug and a tenant tokenGET under /v1/serving only
gatewaythe inference gatewaythe service token in HANGAR_GATEWAY_SERVICE_TOKENGET /v1/endpoints/resolved, GET /v1/gateway/tenants, POST /v1/clusters/tunnel-auth, and nothing else
operatorone cluster’s operatorthat cluster’s enrolment tokenno HTTP route at all; it opens hub sessions

A credential of the wrong kind for a route is 403 forbidden. A missing, unrecognized, expired or tampered one is 401 not_authorized.

Admin sessions

There is one admin identity and every admin session is that operator. Wingsmith does not model several people with different permissions over one fleet.

POST /v1/auth/login takes {"password": "..."} (with slug omitted or "admin") and returns:

{
  "access_token": "hgrs1...",
  "refresh_token": "hgrr1...",
  "expires_in": 900,
  "kind": "admin",
  "tenant": null
}

The access token, not the password, is the bearer credential on every request after. It is short-lived and signed. It is prefixed hgrs1 where a refresh token is prefixed hgrr1, so the two cannot be confused.

POST /v1/auth/refresh takes {"refresh_token": "..."} and answers with the same fields, rotating the refresh token as it does so: the one it was given stops working, so a stolen refresh token and the real one cannot both stay valid.

Late rotation within 10 seconds is allowed, so two console tabs refreshing at the same moment do not break each other.

POST /v1/auth/logout takes the same body, ends the session along with every refresh token rotated from it and every access token it issued, and answers 204.

Sending the password itself as a per-request credential is intentionally not supported: it would end up in proxy logs and could not be rotated independently of the person who knows it.

Absolute session lifetime

Sessions have an absolute lifetime of HANGAR_REFRESH_TTL_SECONDS from sign-in (default 604800, seven days). Access tokens last HANGAR_SESSION_TTL_SECONDS (default 900). After that, even a refresh token that has not expired is refused, and the user must sign in again.

Tenant sessions

A tenant signs in with its slug and a tenant token (hgrt1..., minted under /v1/tenants/{ref}/tokens and shown once):

{"slug": "acme", "password": "hgrt1..."}

The answer carries kind: "tenant" and the tenant’s slug, and the session reaches its own serving view and nothing else.

Revoking the token or disabling the tenant ends its sessions on their next request, because a session token is checked against its live session on every call.

A tenant API key (hgrk1..., /v1/tenants/{ref}/keys) is a different credential: it calls the gateway, never this API. See Manage tenants.

Gateway auth

The gateway is a service principal, not an admin. It reads the two tables it routes on and verifies agent tunnels, and an admin session is refused on all three of its routes.

The tenant feed carries every tenant’s live key hashes, which must never reach a console response, and tunnel verification turns an enrolment token into a cluster identity, which a human credential has no reason to do.

Under HANGAR_APP_ENV=local with no token configured the plane accepts a fixed local token so a fresh checkout’s gateway can start. Outside local an unset token means no gateway principal resolves at all.

Cluster enrolment tokens

A cluster’s enrolment token is the operator’s credential and is bound to that cluster. It authenticates the cluster’s operator to the hub and its serving agent to the gateway. This API never accepts one in Authorization.

There is no shared or fleet-wide operator token. The hub refuses anything that is not a live enrolment token of an enabled cluster, and the session it opens writes observed state for that cluster only.

The one HTTP route that handles an enrolment token takes it in a request body, from the gateway, and answers with a cluster identity instead of a session.

Failed sign-ins

Login is budgeted. After ten failed logins in a rolling minute from one address, the route answers 429 rate_limited without verifying the credential. A process-wide ceiling backs this for a caller spread across many addresses. The check runs before the password hash, because the cost being bounded is the CPU an anonymous caller can spend.

Development overrides

HANGAR_ADMIN_AUTH=disabled turns off the admin half of this check for local development only: every request without a credential then resolves to the admin principal. The plane refuses to start with it outside HANGAR_APP_ENV=local, logs a warning naming the exposure on every startup while it is active, and it never affects the gateway, tenant or enrolment-token checks.

Browser writes

A browser write must come from an allowed origin and carry JSON. A POST, PUT, PATCH or DELETE whose Origin names neither an origin in HANGAR_ALLOWED_ORIGINS nor the plane’s own host is 403 forbidden, and any request body that is not application/json is 415 unsupported_media_type. A caller that is not a browser sends no Origin and is unaffected.