Skip to content

Signing in to the console

Two mechanisms, one per edition, and they mint the same kind of session.

  • Enterprise signs people in through an identity provider (OIDC/SSO).
  • Community exchanges an API key for a browser session. That is this page.

Both end at the same place: a vornik_session cookie the daemon authenticates on every request. Neither stores a password — vornik has no password store, and the users table has no password column.

What the Community exchange is for

An API key authenticates a possessor, not a person. It is the same credential whether a shell script uses it or you type it into a browser. The exchange turns "this caller holds a credential" into "this browser is acting as this account, for a bounded time, revocably" — which is the part Community was missing, rather than a login page.

Enabling it

Off by default. In the deployed config tree (~/.config/vornik/configs/ — the daemon reads only the deployed copy, never your source tree):

auth:
  session:
    credential_exchange: true
    lifetime: 168h          # optional, 7 days by default
    idle_timeout: 24h       # optional, empty disables idle cut-off

Restart the daemon — this is boot-time wiring, not hot-reloaded. You should see:

credential session exchange enabled: POST /api/v1/auth/session

If that line is absent the route answers 503 and nothing is broken on your side. Two gates decline quietly and each logs a Warn saying which: no identity core (the daemon cannot answer "whose key is this"), or no session store. A 503 means "this deployment does not offer the door", which is deliberately distinct from a rejected credential.

Using it

curl -X POST https://your-daemon/api/v1/auth/session \
     -H "Authorization: Bearer $VORNIK_API_KEY" -i

A 204 with three Set-Cookie headers is success. The key must already be mapped to an account — an unmapped key is refused exactly like an invalid one, on purpose (see Why every refusal looks the same).

The credential goes in the Authorization header and nowhere else. Never a query parameter, never a form field, never localStorage: a key in a URL reaches the access log, the browser history, the Referer of every subsequent navigation, and any proxy in between.

What a session may do

A session never carries more than the key that minted it currently has. Checked on every request, not at sign-in:

  • the key still exists, is not revoked and has not expired, and
  • it is still mapped to the same account, and
  • the account is not disabled.

Any of those failing returns 401 and ends the session immediately — not at a cache expiry. So revoking a key logs out the browsers it signed in, on their next request. That includes a key reassigned to somebody else: it stops carrying the previous owner's session.

Grants are not frozen into the session either. What the session may do is resolved from the identity core per request, the same way it is for chat and every other channel.

TLS

The exchange refuses to mint a session cookie over plaintext HTTP. A session cookie in the clear is the whole session: anything on the path can take it and become that account until it expires.

Terminate TLS in front of the daemon — every supported topology does. For a local development box only:

auth:
  session:
    allow_insecure_exchange: true   # development only

It logs a Warn on every boot while it is on, so a config copied from a dev box to a real one announces itself before a cookie travels in the clear.

Why every refusal looks the same

A bad key, a key mapped to nobody, a disabled account and a cross-origin request all return the same 401 with the same message. That is deliberate: the endpoint already tells a caller whether a credential is live, and that much is unavoidable in a login. What it must not do is tell them which of those applied — those are facts about somebody else's account.

Rate limiting is the one exception, answering 429 so a legitimate caller can back off. It shares one budget per key with the key-claim path, because both doors prove possession of the same credential. A consequence worth knowing: someone hammering a key on either door closes both for that key. If that locks an administrator out, the recourse is the local break-glass CLI on the daemon host.

Cross-site request forgery

Once a session exists, mutating requests that arrive with the cookie are checked twice: a same-origin signal (Sec-Fetch-Site, falling back to Origin, failing closed when neither is present), and a double-submit token.

If you are writing a browser client against this API, read the vornik_csrf cookie and echo it in the X-Vornik-CSRF header on every POST, PUT, PATCH and DELETE. A plain HTML form, which cannot set a header, may instead send the token as the form's first field, named vornik_csrf (url-encoded or multipart bodies only). Without it the request is refused with 403 CSRF_BLOCKED, and the message says which check failed: no same-origin signal, a missing token, or a token that did not match. The daemon logs the same cause alongside the signals that drove the decision.

The web console does all of this itself. If a console button answers "CSRF token missing" or "did not match", reload the page; the session may have been re-issued in another tab.

Callers using Authorization: Bearer — the CLI, MCP clients, A2A peers — skip all of this. A browser never attaches that header on its own, so those requests cannot be forged this way.

What this does not give you

  • No self-service anything. No sign-up, no password reset, no "forgot my access". An administrator issues credentials; someone who loses theirs needs another issued.
  • No SSO in Community. That is an Enterprise feature and deliberately stays one.
  • Machine credentials are unchanged. The CLI, A2A and agents keep using API keys directly and never take a session. A browser session is for a human in a browser; anything else holding one is a smell.