From 3cee373cbdee7228124cf29d719257f8483759a0 Mon Sep 17 00:00:00 2001 From: webdevtodayjason Date: Mon, 21 Sep 2026 18:11:26 -0500 Subject: [PATCH] docs: sign in and accounts, the page for the login beside API keys A person signs in to the dashboard with a name and a password now, and the session lasts until sign out or until an admin revokes it. Programs keep presenting an API key. The docs had only the key half. - `security/sign-in.mdx`, new. Why there is a login, the first account on a fresh node and on a running fleet, signing in, the account page, the Users page and the two roles, the CLI, HTTPS before you hand out accounts (linking the TLS page), the two files and their modes, the guard rails, the open paths, the doctor's Login check, and a troubleshooting table: locked out, no accounts yet, a revoked session, a program answering 401. - `security/api-keys.mdx`. A note at the top that a key is for programs and a person signs in instead, and the two paths the sign-in needs in the exceptions table: `GET /api/auth/me` and `POST /api/auth/login`. The sentence that counted five exceptions no longer counts, since the count is what drifts. - `docs.json`. `security/sign-in` first in the Security group, next to API keys. No version number is claimed: the release that carries the login is named in the release notes. Links checked with `mintlify broken-links`. Co-authored-by: webdevtodayjason Co-authored-by: Claude Fable 5.1 --- docs.json | 1 + security/api-keys.mdx | 13 ++- security/sign-in.mdx | 201 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 security/sign-in.mdx diff --git a/docs.json b/docs.json index 33d8f5a..8e00cd5 100644 --- a/docs.json +++ b/docs.json @@ -84,6 +84,7 @@ { "group": "Security", "pages": [ + "security/sign-in", "security/api-keys", "security/tls", "security/rate-limits", diff --git a/security/api-keys.mdx b/security/api-keys.mdx index 363d592..d2e7aed 100644 --- a/security/api-keys.mdx +++ b/security/api-keys.mdx @@ -20,6 +20,13 @@ OpenAI-compatible API and every management route (load a model, unload one, key answers everyone who can reach it. + +**A key is for programs. A person signs in instead.** With auth on, the dashboard +takes a name and a password and keeps a session, so nobody has to paste a shared +secret into a browser. See [Sign in and accounts](/security/sign-in). Everything +on this page still applies to every program that calls this node. + + ## What the installer does | Situation | What happens | @@ -232,14 +239,16 @@ A wrong key gets a 401 with `Invalid API key`. ## The rule, and the paths that stay open -**With auth enabled, every path under `/api` and `/v1` needs the key.** There are -five exceptions and the static shell, each with a reason: +**With auth enabled, every path under `/api` and `/v1` needs the key.** The +exceptions are the static shell and these paths, each with a reason: | Path | Why it stays open | |---|---| | `/` and `/static/*` | The shell is files. Without it the browser cannot render the page that asks for a key | | `GET /api/health` | A liveness probe has no key. It carries the running `version`, which is what `ainode update` reads to verify a release on a node that requires one | | `GET /api/auth/status` | So the UI can say "this node wants a key" instead of rendering an empty page | +| `GET /api/auth/me` | So the page can ask whether somebody is signed in. See [Sign in and accounts](/security/sign-in) | +| `POST /api/auth/login` | The request that signs a person in cannot need a session first | | `GET /api/cluster/endpoint` | Node names, addresses and ports, nothing else. A client whose configured node is down has to be able to ask a reachable one where the rest of the fleet is, and the key it holds does not help it find an address. No model, no telemetry, no config, no key material | | `POST /api/cluster/join` | A node joining this cluster does not hold this cluster's key yet, so a single-use expiring [join token](/cluster/joining) is the credential instead. The route is rate limited to five attempts a minute per source address in place of a key | diff --git a/security/sign-in.mdx b/security/sign-in.mdx new file mode 100644 index 0000000..7e2cbc1 --- /dev/null +++ b/security/sign-in.mdx @@ -0,0 +1,201 @@ +--- +title: "Sign in and accounts" +description: "People sign in to the dashboard with a name and a password. Programs keep using an API key. The first account, the account page, managing users, and how to get back in." +icon: "user-lock" +--- + +With auth on, the dashboard asks a person to sign in. A name, a password, and the +session lasts until you sign out or until an admin revokes it. + +Programs are unchanged. A coding agent, [AINode Desktop](/desktop), the bench and +curl all keep presenting an API key as `Authorization: Bearer `. + +## Why there is a login + +An API key is a machine credential. It protects the port, and that is all it +does. Pasted into a browser it is one shared secret: the node cannot tell one +person from another, and revoking the key revokes everyone holding it. + +A login is the part that belongs to a person. Each person has their own name and +password, their sessions are their own, and an admin can take one person's access +away without touching anybody else. + +| Caller | What it presents | +|---|---| +| A person on the dashboard | A name and a password, then a session | +| A program: a coding agent such as dsh or Pi, [AINode Desktop](/desktop), the bench, curl | An API key, as `Authorization: Bearer ` | + +Both work at the same time on the same node. [API keys](/security/api-keys) +covers the key half. + +## The first account + +Accounts are made on the **master node**, from the CLI, because the first one +cannot be made from a page that needs an account to open. + +```bash +ainode auth user add --admin +``` + +It prompts for the password. In a script, `--password-stdin` reads it from the +pipe instead: + +```bash +printf '%s' "$PASSWORD" | ainode auth user add --admin --password-stdin +``` + +If the node does not require auth yet, turn it on: + +```bash +ainode auth enable +``` + + +Make the first account **before** you turn auth on. A node that requires auth +with no account is a node with a sign-in nobody can pass, and the way out is the +same command above, typed on the node itself. + + +### On a fresh node + +A fresh install already requires an API key, so `ainode auth enable` has nothing +to do (see [API keys](/security/api-keys)). One command is the whole setup: + +```bash +ainode auth user add --admin +``` + +Then open the dashboard and sign in. + +### On a fleet that is already running + +Run the same commands on the **master**. Accounts are managed there and +replicated to every node in the cluster automatically, so you add a person once +and they can sign in on any node. + +**Sessions are per node.** A replicated account is one account, but signing in +creates a session on the node whose dashboard you opened. Open another node's +dashboard and you sign in again. + +## Signing in + +Open the node's dashboard and type your name and password. That is the whole +flow. The session lasts until you sign out, or until an admin revokes it. A +revoked session ends wherever it was open. + +## Your account page + +Yours whatever your role: + +- **Change your password.** +- **See your sessions and revoke them**, one per browser you have signed in from. + A machine you left signed in somewhere else is one click to end. + +## Managing users + +Admins get a **Users** page: add an account, remove one, reset somebody's +password, disable an account and enable it again. Disabling keeps the account and +refuses the sign-in, which is what you want for somebody who is coming back. + +| Role | What it can do | +|---|---| +| `admin` | Manage users, manage API keys, and turn the auth switch on and off. Everything a member can do as well | +| `member` | Use the dashboard | + +Two roles, and that is the whole list. + +## From the CLI + +```bash +ainode auth user add --admin # an admin account. Prompts for the password +ainode auth user add # a member account +ainode auth user list # the accounts that exist +ainode auth user passwd # set somebody a new password +ainode auth user disable # keep the account, refuse the sign-in +ainode auth user enable # let them back in +ainode auth user remove # delete the account + +ainode auth session list # who is signed in +ainode auth session revoke # end one session +ainode auth session clear # end all of them + +ainode auth status # the switch, the keys, and now the users + # and the sessions as well +``` + +Every one of them takes `--help` for its exact arguments. Run the `user` commands +on the master, since that is where accounts are managed. The `ainode auth key` +commands are unchanged and live on [API keys](/security/api-keys). + +## Serve it over HTTPS + +A sign-in over plain HTTP sends the password in the clear. Anything on the path +between the browser and the node can read it, and on a LAN that is more machines +than you think. So put the node behind HTTPS before you hand out accounts. + +[TLS](/security/tls) is one command. It serves HTTPS on its own port and leaves +plain HTTP exactly where it was, so nothing else in the fleet has to change. + +The session cookie is marked **Secure** when the node is served over HTTPS, so on +that port the browser will not send it back over a plain HTTP request. + +## What is stored, and where + +| File | What is in it | +|---|---| +| `/users.json` | The accounts. Password **hashes** only, never a password. Mode 0600 | +| `/auth.json` | The API keys, hashed, and whether this node requires one | + +`` is `~/.ainode` unless you moved it. Passwords are hashed with +scrypt. + +## What is in place + +One line each: + +- **The session cookie is HttpOnly**, so no script on the page can read it. +- **It is marked Secure over HTTPS**, so it does not travel on a plain HTTP + request to that node. +- **Every write from the dashboard carries `X-AINode-Client: dashboard`**, a + header a browser will not attach to a cross-site request. That is the CSRF + guard: another site can make your browser send your cookie, it cannot make it + send that header. +- **Ten failed sign-ins in five minutes pause that address.** The pause lifts on + its own, and how long it lasts is described in the release notes. +- **Passwords are scrypt-hashed**, so the file holds no password to steal. + +## The paths that stay open + +With auth on, everything under `/api` and `/v1` needs a credential. The +exceptions are the static shell and these paths, two of which the sign-in itself +needs: + +| Path | Why it stays open | +|---|---| +| `/` and `/static/*` | The shell is files. Without it the browser cannot render the page that asks you to sign in | +| `/api/health` | A liveness probe holds no credential | +| `/api/auth/status` | So the page can say this node wants one, instead of rendering blank | +| `/api/auth/me` | So the page can ask whether you are signed in | +| `/api/auth/login` | The request that signs you in cannot need a session first | +| `/api/cluster/endpoint` | Names, addresses and ports, so a client stranded by its own node can find a reachable one | +| `POST /api/cluster/join` | A node joining this cluster holds none of this cluster's credentials yet, so a single-use [join token](/cluster/joining) is the credential instead | + +Matching is on the path, for every method. +[API keys](/security/api-keys) carries the rest of the rule. + +## What `ainode doctor` says + +`ainode doctor` has a **Login** check, printed with the other auth lines. What it +verdicts on is described in the release notes. See [Doctor](/ops/doctor) for the +rest of the report. + +## Troubleshooting + +| What happened | What to do | +|---|---| +| **Locked out.** The password is gone, and so is the only admin's | On the master, at the node's own terminal: `ainode auth user passwd `. The CLI runs on the node, so it needs no session | +| **No accounts yet**, and the node wants a sign-in | `ainode auth user add --admin` on the master, then sign in | +| **You were signed in and now you are not** | The session ended: you signed out, or an admin revoked it. Sign in again. `ainode auth session list` shows which sessions are still live | +| **Too many wrong passwords** | Ten failed sign-ins in five minutes pause that address. Wait, then try again | +| **A program started answering 401** | Programs do not sign in, they present a key. Mint one with `ainode auth key create --name `: [API keys](/security/api-keys) | +| **You can sign in on one node and not another** | Sessions are per node, so each dashboard asks once. If the account itself is not on that node, check the node actually joined the cluster: [Joining a node](/cluster/joining) |