aboutsummaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-07 22:35:09 +0100
committertv <tv@krebsco.de>2026-11-07 22:35:09 +0100
commitec70060ae5fa665953741da921e28031b56bc5e0 (patch)
treecd1f6f5ae90d3ad87b9667da20d6e85b62394ab1 /docs
parenteed8f86912f53f549d9f9068f9bdf2c8e23aa0f7 (diff)
auth: logging in, and tokens as bearers
POST /auth/login (by htpasswd or the proxy), /auth/refresh, /auth/logout, GET /auth/whoami; every request may bring Authorization: Bearer instead of a password. --token-lifetime and --session-lifetime. Auth plan M2.
Diffstat (limited to 'docs')
-rw-r--r--docs/access.md32
-rw-r--r--docs/api.md5
-rw-r--r--docs/example.toml9
-rw-r--r--docs/openapi.yaml112
-rw-r--r--docs/trust.md3
5 files changed, 158 insertions, 3 deletions
diff --git a/docs/access.md b/docs/access.md
index db86e21..e8c5455 100644
--- a/docs/access.md
+++ b/docs/access.md
@@ -73,6 +73,38 @@ A user the node lets in but the registry doesn't have may do nothing (`403`).
whole swarm (its members, its remakes, its users), so it is given apart, never by
accident with the data. A user who may do both has both.
+## Tokens
+
+A user logs in on any node, by what that node checks (its `--htpasswd`, or a proxy's
+`--user-header`), and gets a token every node of the swarm takes, sent as
+`Authorization: Bearer <token>` instead of a password:
+
+```sh
+curl -u alice -X POST http://localhost:3000/auth/login # {"token": "ct1.…", …}
+curl -H "Authorization: Bearer ct1.…" http://other-node:3000/store
+```
+
+* **Who signs it**: the node it logged in on, with its node key. Every node takes a
+ token signed by a member of the swarm (the member list gives the keys: a node ID is
+ its key), and none signed by another or since forgotten.
+* **What it says**: who, how it proved it, when it ends, and optionally less than the
+ user may do (`limit`, capabilities as `POST /admin/users/{name}` takes them): never
+ what the user may do. Every node judges a request by the registry it holds then, so
+ a capability granted or revoked counts on a node as soon as the change reaches it.
+* **How long**: `--token-lifetime` (12 hours), refreshed (`POST /auth/refresh`, on any
+ node, which signs the new one) until `--session-lifetime` (7 days) after the login;
+ then the user logs in again. A refreshed token may say less (`limit`), never more.
+* **Revoking**: a user disabled or removed is refused at once; all of a user's tokens,
+ by the user (`POST /auth/logout`) or by one who may manage users (`DELETE
+ /admin/users/{name}/tokens`), which sets `tokensNotBefore` in its entry: tokens issued
+ before aren't taken any more. A token of a node forgotten by the swarm isn't taken
+ either. In a swarm without a registry, a token is taken until it expires.
+* **What it means to trust**: any member could sign a token for any user, as it could
+ write anything as anyone ([`trust.md`](trust.md)). A token is a bearer's: whoever holds it is
+ the user until it ends, so it goes over TLS only, as passwords do.
+
+`GET /auth/whoami` says who a request's user is, how it proved it, and what it may do.
+
## Managing users
Through `/admin/users` ([`openapi.yaml`](openapi.yaml), Users): looking needs `read` on
diff --git a/docs/api.md b/docs/api.md
index 5486378..8c4a534 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -31,9 +31,10 @@ curl -u alice:secret -X POST 'localhost:3000/store/stats/visits?increment=1' #
* Bodies are JSON (`application/json`, the default), text (`text/plain`, stored as a
string), or a value of a type of its own (Types); answers are JSON, or as `Accept`
asks. Numbers in JSON are stored as integers where they fit, as floats otherwise.
-* Every request needs a user from the node's `--htpasswd` (basic authentication), or
+* Every request needs a user from the node's `--htpasswd` (basic authentication),
named by a reverse proxy in front that authenticated it (`--user-header`;
- [`operations.md`](operations.md), Behind a reverse proxy). What each may do, the swarm's user
+ [`operations.md`](operations.md), Behind a reverse proxy), or a token from logging in on any node
+ of the swarm (`Authorization: Bearer`, [`access.md`](access.md), Tokens). What each may do, the swarm's user
registry says, by path ([`access.md`](access.md)): `403` where it doesn't let the user, and
of a part it may only partly read, a read shows the keys on the way to what it may.
In a swarm without a registry, users are all alike: each can read and write
diff --git a/docs/example.toml b/docs/example.toml
index b98752d..ab8237c 100644
--- a/docs/example.toml
+++ b/docs/example.toml
@@ -73,6 +73,15 @@ htpasswd = "/etc/continuum/htpasswd"
# 0: never.
# stream-heartbeat = "30s"
+# How long a token this node issues (POST /auth/login, /auth/refresh) is taken, by every
+# node of the swarm (docs/access.md, Tokens). Refreshed before it ends, it lasts until
+# the login's end (session-lifetime).
+# token-lifetime = "12h"
+
+# How long after a login (POST /auth/login) its tokens may be refreshed; then the user
+# logs in again.
+# session-lifetime = "7d"
+
# Where the reverse proxies whose user-header is believed connect from: IP addresses (or
# ranges, as ADDRESS/BITS), and unix for the API's Unix sockets (bind-addr). On a host
# with other users, leave out the addresses and have the proxy use a Unix socket: others
diff --git a/docs/openapi.yaml b/docs/openapi.yaml
index 5bf5a50..64c5b9b 100644
--- a/docs/openapi.yaml
+++ b/docs/openapi.yaml
@@ -26,6 +26,7 @@ servers:
security:
- basic: []
+ - bearer: []
tags:
- name: Store
@@ -38,6 +39,10 @@ tags:
description: |
Replacing the swarm's document with a new epoch (compaction, replacement,
import), and what happened to local writes that a remake couldn't place.
+ - name: Auth
+ description: |
+ Logging in on this node for a token every node of the swarm takes, and using it
+ (docs/access.md, Tokens).
- name: Users
description: |
The swarm's users and what each may do (docs/access.md): reading needs `read` on
@@ -733,6 +738,91 @@ paths:
'401':
$ref: '#/components/responses/Unauthorized'
+ /auth/login:
+ post:
+ tags: [Auth]
+ summary: Log in for a token
+ description: |
+ As a user this node lets in (htpasswd, or a trusted proxy's user-header): a token
+ signed by this node, taken by every node of the swarm until `expiresAt`
+ (`token-lifetime`), refreshable until `sessionExpiresAt` (`session-lifetime`).
+ With `limit`, the token gives only what it allows too.
+ security:
+ - basic: []
+ requestBody:
+ required: false
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Limit'
+ responses:
+ '200':
+ description: The token.
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/TokenAnswer' }
+ '400': { description: A token instead of a password, or a limit that isn't one. }
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403': { description: Not a registered, enabled user. }
+ '503': { description: Not ready yet. }
+
+ /auth/refresh:
+ post:
+ tags: [Auth]
+ summary: Refresh a token
+ description: A new token for the same login, signed by this node; with `limit`, narrower still (within the token's).
+ security:
+ - bearer: []
+ requestBody:
+ required: false
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Limit'
+ responses:
+ '200':
+ description: The token.
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/TokenAnswer' }
+ '400': { description: No token, or a limit wider than the token's. }
+ '401': { description: The token isn't taken, or its login ended. }
+
+ /auth/logout:
+ post:
+ tags: [Auth]
+ summary: End all of your logins
+ description: Every token of the user, of every login, no longer taken (`tokensNotBefore`).
+ responses:
+ '204': { description: Done. }
+ '409': { description: No user registry; tokens are taken until they expire. }
+
+ /auth/whoami:
+ get:
+ tags: [Auth]
+ summary: Who you are here
+ description: The user, how it proved it (`htpasswd`, `header`, `token`), its registry entry's capabilities, and its token's claims if it came with one.
+ responses:
+ '200':
+ description: Who.
+ content:
+ application/json:
+ schema:
+ type: object
+
+ /admin/users/{name}/tokens:
+ parameters:
+ - { name: name, in: path, required: true, schema: { type: string } }
+ delete:
+ tags: [Users]
+ summary: Revoke every token of a user
+ responses:
+ '204': { description: Revoked. }
+ '403': { description: May not manage users. }
+ '404': { description: No such user. }
+ '409': { description: No user registry. }
+
/admin/users:
get:
tags: [Users]
@@ -1468,6 +1558,10 @@ components:
basic:
type: http
scheme: basic
+ bearer:
+ type: http
+ scheme: bearer
+ description: A token from `POST /auth/login` on any node of the swarm (docs/access.md, Tokens).
parameters:
key:
@@ -1637,6 +1731,24 @@ components:
$ref: '#/components/schemas/Error'
schemas:
+ Limit:
+ type: object
+ properties:
+ limit:
+ type: array
+ items: { $ref: '#/components/schemas/CapabilityRequest' }
+ TokenAnswer:
+ type: object
+ properties:
+ token: { type: string, description: '`ct1.…`: send it as `Authorization: Bearer <token>`.' }
+ user: { type: string }
+ issuer: { type: string, description: The node that signed it. }
+ expiresAt: { type: integer, description: Unix ms. }
+ sessionExpiresAt: { type: integer, description: Unix ms. }
+ limit:
+ type: array
+ nullable: true
+ items: { $ref: '#/components/schemas/CapabilityRequest' }
CapabilityRequest:
type: object
required: [path, rights]
diff --git a/docs/trust.md b/docs/trust.md
index fd37638..8b0f72a 100644
--- a/docs/trust.md
+++ b/docs/trust.md
@@ -34,7 +34,8 @@ secret like the swarm key: readable by its owner only (a node warns otherwise; a
it creates is).
**The HTTP API.** Every endpoint but the API description needs a user from
-`--htpasswd`, or one a reverse proxy names (`--user-header`): the node believes that
+`--htpasswd`, one a reverse proxy names (`--user-header`), or a token a node of the
+swarm issued to one of those ([`access.md`](access.md), Tokens): the node believes that
header from `--trusted-proxies` alone, but from those blindly, so whoever can send
requests from there can be any user. With the default loopback addresses, that is every
user of the host; with only the API's Unix socket (`--trusted-proxies unix`), those its