Best-effort serve for bad stored config + admin self-lockout guards

Serving never refuses to start because of stored realm config: the
registry build sanitizes best-effort and warns — misfiled origin entries
are reclassified (a cross-domain origins entry is served as a related
origin) or dropped, collisions resolve first-come-wins, over-cap related
lists truncate, unsalvageable realms are skipped. Fixing the stored
config stays the admin interface's job, and it stays reachable on any
working realm. Only a config with no servable realm at all is fatal.

Admin realm writes stay strict and gain self-lockout guards: an update
that would leave the admin's current host unable to run ceremonies for
the realm they are on is refused (unless an auth host takes over
ceremonies), and deleting the realm currently in use is refused.
This commit is contained in:
2026-09-06 22:52:00 +00:00
parent b9e6f4bc27
commit f5bd0a469c
6 changed files with 438 additions and 24 deletions
+31 -16
View File
@@ -172,9 +172,9 @@ class Config(msgspec.Struct, omit_defaults=True):
outside it, are capped (default 5), and must not collide with another
realm's rp-id/auth-host/related origins nor fall inside another
realm's domain. Misfiled entries (cross-domain in `origins`, in-domain
in `related_origins`) are rejected. These rules are enforced both at
startup and at admin write time. Origins are never _implicitly_
cross-domain.
in `related_origins`) are rejected. These rules are enforced at admin
write time; at startup the stored config is sanitized best-effort
instead (§3.2). Origins are never _implicitly_ cross-domain.
### 3.2 CLI: bootstrap (`paskia init`) vs. serve (`paskia`)
@@ -201,12 +201,18 @@ instance:
selects `<rp-id>.paskiadb` by name; the others are left in place.
- **`paskia`** — serve. Takes **no realm options**; only `--listen`
(per-run override of stored `Config.listen`, never persisted). Startup:
open `paskia.kantadb`validate the stored realm set cross-realm
(rp-ids distinct; auth hosts distinct from each other and from every
rp-id; related origins capped and collision-free) → build the realm
registry serve. The serve command never converts databases: with no
`paskia.kantadb`, the startup error points at `paskia init`, or at
`paskia migrate` when legacy `*.paskiadb` candidates are present.
open `paskia.kantadb`sanitize the stored realm set best-effort →
build the realm registry → serve. Sanitization never refuses to start:
misfiled origin entries are reclassified (a cross-domain `origins`
entry is served as a related origin) or dropped, colliding
auth-hosts/related origins resolve first-come-wins, over-cap related
lists truncate, and unsalvageable realms are skipped — each producing
a startup warning, because fixing the stored config is the admin
interface's job and it must stay reachable to do so. Only a config
with no servable realm at all is fatal. The serve command never
converts databases: with no `paskia.kantadb`, the startup error points
at `paskia init`, or at `paskia migrate` when legacy `*.paskiadb`
candidates are present.
Nested rp-ids are allowed (longest-suffix dispatch determinism). Adding a
child rp-id moves **no data** — users are global; only new ceremonies
@@ -375,9 +381,16 @@ effective_auth_host(realm) = realm.auth_host or first_configured_auth_host or No
Changing a realm's rp-id itself is **not supported** (it would orphan
every credential stamped with the old rp-id) — delete and recreate
instead.
- Delete: refused for the last remaining realm and while any credential
- Delete: refused for the last remaining realm, while any credential
carries the realm's rp-id (re-enroll or delete those credentials
first); cascades nothing else (users/orgs are global).
first), and for the realm the admin is currently using; cascades
nothing else (users/orgs are global).
- **Lockout guard**: an update that would make the admin's current
host unable to run passkey ceremonies for the realm they are on is
refused (unless an auth host takes over ceremonies — it is always
allowed). Fixing a broken stored config is always possible: serve
sanitizes it best-effort (§3.2) so the admin interface stays
reachable on a working realm.
- The admin UI has a Realms section with a table (rp-id, name,
effective auth host, sign-in site and related domain counts),
per-row edit/delete and an add-realm dialog. The dialog edits the
@@ -431,9 +444,9 @@ effective_auth_host(realm) = realm.auth_host or first_configured_auth_host or No
background cleanup task (DB is global).
- The kanta bootstrap hook only ever fires for a database created by
`paskia init` or `paskia migrate`; the serve command never bootstraps.
- The registry is built from the stored `Config` after open; per-realm
`Passkey` instances constructed (each realm's origins validated at
startup — fail-fast, including related-origin cap checks).
- The registry is built from the stored `Config` after open, sanitized
best-effort (§3.2) so startup never fails on config content; per-realm
`Passkey` instances are constructed from the sanitized config.
- The admin-credential check runs at serve startup and reprints a usable
registration link when the admin lacks a credential under the default
realm (§9).
@@ -476,8 +489,10 @@ effective_auth_host(realm) = realm.auth_host or first_configured_auth_host or No
origins sharing an rp-id share one security boundary — do not mix trust
levels within a realm.
- **Realm administration**: realm create/update/delete is gated on
`auth:admin` — deployment-wide by design; validation runs on every
write, not just at startup.
`auth:admin` — deployment-wide by design. Writes are strictly
validated (cross-realm rules plus self-lockout guards); startup never
refuses a stored config — it sanitizes with warnings so the admin
interface stays reachable to fix problems.
- **Passkeys**: rp-id binding browser-enforced and server-recorded;
ceremonies, credential scans, exclude/allow lists all scoped to the
origin realm's rp-id. No cross-realm oracle in the scan.