docs(sso): document the machine surface, and stop restating it in nix

`docs/swarm/sso.md` described a person in a browser. The swarm's other
callers — the telemetry collector, the queue's auth-callout responder, each
hive's agents — hold no session and follow no redirect, and nothing operator-
facing said how they authenticate. Its relying-party table is forge and
matrix, both browser surfaces.

The new section carries what `swarm-authelia.nix` was holding in comments:
one client per hive because identity belongs to the directory, the audience
being that client id rather than a parallel naming scheme, and signed rather
than opaque tokens because the collector verifies offline against
`/jwks.json` while the queue introspects.

It also states the fail-closed rule once, in the place a reader looks before
touching a vhost: an error page answers 200, and `auth_request` reads any 2xx
as access granted. That shape has now appeared three times — this module's
`/api/` prefix and both of victorialogs' routes — which is what makes it
documentation rather than a comment.

The two comment blocks those replace shrink to the part that is genuinely
local: the submodule-typing reason these clients are a definition rather than
an append, and a loud warning against folding the machine prefix back into
`/`. The security warning stays at the site; only its consequence list moves.

Comments 495 -> 465 lines. Option `description` strings are untouched: they
are the source `pkgs.nixosOptionsDoc` renders into the operator's options
reference, so trimming one would delete published documentation rather than a
duplicate.
This commit is contained in:
atlas 2026-09-02 02:53:51 +02:00 committed by mara
commit 9b14014077
2 changed files with 73 additions and 41 deletions

View file

@ -122,13 +122,10 @@ let
# be what an operator sees, not a coercion error from here.
cookieDomain = if swarmDomain == null then "invalid" else swarmDomain;
# One machine client per hive in the roster. A hive's identity belongs
# to the DIRECTORY, not to whichever service happens to consume it:
# the rule is that a hive's credentials all derive from the SAME
# identity, so one hive holds ONE client and mints a different token
# per service from it. Were the queue to declare this list, the next
# consumer would collide on the same client id — and only at the
# moment it landed.
# One machine client per hive in the roster. The model — why identity is
# per hive rather than per service, why `audience` is the client id, and
# why these tokens are signed rather than opaque — is in
# `docs/swarm/sso.md`, § Machine clients.
#
# Fed to the option as a DEFINITION in the config block below, rather
# than appended to the declared list downstream. That is what puts it
@ -137,19 +134,6 @@ let
# and a field later added to the submodule then existed on the
# declared entries and not on these, which is an eval error reachable
# only once hive identities are on.
#
# `audience` is the hive's own client id rather than a second per-hive
# string invented here. A swarm service that has to tell hives apart
# needs one name per hive that both sides already agree on, and the
# client id is that name — published as `hiveClientPrefix` for exactly
# this reason. Minting a parallel naming scheme would be a second thing
# to keep in step, and the one that drifts is the one nobody tests.
#
# `RS256` because a resource server that cannot call this provider back
# is a real case here: the swarm's telemetry collector verifies tokens
# offline against `/jwks.json`, and an opaque token gives it nothing to
# verify. The queue's auth-callout responder introspects instead, which
# is a different question asked of the same token.
hiveClients = lib.mapAttrsToList (name: _: {
id = "${cfg.hiveClientPrefix}${name}";
description = "HyperHive hive ${name}";
@ -1063,28 +1047,13 @@ in
# `proxy_intercept_errors` / `error_page` are set inside that
# location rather than at server level, so they do not reach here.
#
# ⚠️ THIS IS A SECURITY BOUNDARY, not a tidy-up. `error_page … =
# /__hive_sso_unavailable` takes its status from the redirected
# location, which serves a FILE — so it answers **200**. Every
# machine caller then receives a success carrying an HTML body
# instead of the 502 that actually happened:
# ⚠️ THIS IS A SECURITY BOUNDARY, not a tidy-up. Do not fold it back
# into `/`: the friendly error page answers **200**, and
# `auth_request` reads any 2xx as ACCESS GRANTED. Full consequence
# list in `docs/swarm/sso.md`, § Machine callers must fail closed.
#
# - `/api/authz/auth-request` — nginx `auth_request` treats any
# 2xx as success, so a down authelia would mean ACCESS GRANTED
# - `/api/oidc/introspection` — a token check answering 200
# - `/api/oidc/token`, `/.well-known/openid-configuration` — a
# client parsing an error page as its JSON document
#
# Measured with both controls rather than reasoned: against a dead
# upstream a subrequest through `/` serves the protected content,
# and through this prefix it denies, matching a direct dial. The
# browser arm confirms `/` still serves the friendly page — a fix
# that silently deleted it would pass the deny check alone.
#
# The split is by AUDIENCE, not by path list: a human typing the
# URL gets a page explaining that SSO is down, and every `/api/`
# and `/.well-known/` caller — including the login page's own XHR —
# gets the status. Enumerating individual endpoints would leave the
# The split is by AUDIENCE, not by path list — including the login
# page's own XHR. Enumerating individual endpoints would leave the
# next one added silently intercepted.
locations."/api/" = {
proxyPass = "http://127.0.0.1:${toString cfg.port}";