From 470d2ad845c2d0c1f0dd7b4f5d5d71fe0cf58d95 Mon Sep 17 00:00:00 2001 From: atlas Date: Wed, 12 Aug 2026 17:46:15 +0200 Subject: [PATCH] docs(3167): the swarm UI page, and the group step that gates it New docs/swarm/ui.md (split-page shape, per the docs rule), linked from the swarm README and added to the gateway's vhost map. Leads with the step that separates 'protected' from 'locked out': swarmctl user add --group operators. auth_request asks whether there is a session; the access_control rule is what makes it mean operator, and an account created before the rule existed has no groups. Also records the four wiring sites a swarm service name needs, with the certificate one called out - serviceDomains is both the sub-CA's nameConstraints set and the leaf's SANs, and the apex is a sibling of the other three rather than a parent, so nothing issues for it implicitly. --- docs/gateway.md | 1 + docs/swarm/README.md | 6 ++++ docs/swarm/ui.md | 80 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 docs/swarm/ui.md diff --git a/docs/gateway.md b/docs/gateway.md index 6b65fe85..fd86ef1f 100644 --- a/docs/gateway.md +++ b/docs/gateway.md @@ -15,6 +15,7 @@ Single nginx in front of every hyperhive web surface. Runs on the **host**, next | `matrix./` | `matrix.` | fluffychat-web static | `matrix.gui.enable` | | `matrix./config.json` | `matrix.` | inline JSON (FluffyChat boot config) | `matrix.gui.enable && domain != null` | | `auth./` | `auth.` | authelia (`9091`) | `swarm.authelia.enable` | +| `/` | `` | swarm-ui dist (static), behind an authelia subrequest | `swarm.ui.enable` | The authelia vhost is declared only by the host that **runs** authelia, not by every hive that uses it — a client hive knows the swarm's `authelia.url` but must not answer for a name it doesn't serve. Its server name is exactly `swarm.authelia.domain`: authelia validates `authelia_url ⊂ session cookie domain` at startup, so a near-miss is a container that refuses to boot. It carries no `auth_basic` — the login page must not sit behind the login mechanism it replaces — and sets the four `X-Forwarded-{Proto,Host,Uri,For}` headers, since authelia decides by the *original* request rather than the hop it sees. diff --git a/docs/swarm/README.md b/docs/swarm/README.md index fc6f3c79..810d0213 100644 --- a/docs/swarm/README.md +++ b/docs/swarm/README.md @@ -94,6 +94,12 @@ Which secrets the SSO provider generates, which one has a reader in another container, and the three ways that one gets delivered: [`sso.md`](sso.md). +## Swarm UI + +The operator-only web surface on the swarm apex, why reaching it needs +the `operators` group rather than just a session, and the four sites a +swarm service name has to be wired into: [`ui.md`](ui.md). + ## The swarm's hive directory ```nix diff --git a/docs/swarm/ui.md b/docs/swarm/ui.md new file mode 100644 index 00000000..cfdc46c6 --- /dev/null +++ b/docs/swarm/ui.md @@ -0,0 +1,80 @@ +# Swarm UI + +The swarm's own web surface, served by the gateway on the **swarm apex** +(`services.hyperhive.swarm.domain`) and readable only by operators. + +Distinct from the per-hive dashboard, which lives on the hive domain and +answers for one host. This one is the view *across* hives. + +## Enabling + +```nix +services.hyperhive.swarm.ui.enable = true; # defaults to swarm.controller.enable +``` + +Derived from the controller rather than from `enableRequiredServices`: +the UI is a view onto the controller's state and reaches it over that +daemon's socket, so the host that runs the controller is the host that +can serve the UI. A hive that merely *uses* a swarm has nothing to serve. + +`swarm.ui.domain` defaults to the swarm apex and can be pinned, the same +way `swarm.forge.domain` and `swarm.matrix.gatewayHost` can. + +> The apex must differ from `services.hyperhive.domain`. The gateway's +> default server already answers for the hive domain, and two vhosts +> claiming one `server_name` do not error — nginx picks one — so this is +> an assertion rather than a runtime surprise. + +## 🔑 You must be in the `operators` group + +**This is the step that separates "protected" from "locked out".** The +vhost's `auth_request` asks authelia *"is there a session"*; the rule +that makes it mean *"is this an operator"* is an `access_control` entry +requiring `group:operators`. An account without that group authenticates +fine and still gets bounced. + +```sh +swarmctl user add --group operators +``` + +An account created before this existed has no groups. Re-add it with the +flag — `swarmctl` treats an existing entry as the canonical store, so the +group is what changes. + +Why a group and not a list of usernames: agents are getting authelia +accounts of their own (matrix SSO), and *authenticated* would then +include every agent in the hive. The group is the only thing standing +between "an operator's page" and "anyone with a session". + +## What it costs to be reachable + +The apex is published to the hive's resolver like every other swarm +service, so agent containers can resolve it. That is deliberate and it is +not a hole: **reachability is not the access control here.** An agent +that resolves the name and connects still has no operator session, and +the subrequest denies it. + +## Four wiring sites + +Adding a swarm service name means touching all four. Missing one ships as +a different flavour of "works from the host, broken from a container": + +| site | file | +| --- | --- | +| vhost | `nix/host-modules/hive-gateway/vhosts.nix` | +| **certificate name** | `nix/host-modules/swarm.nix` (`serviceDomains`) | +| DNS record | `nix/host-modules/hive-gateway/dnsmasq.nix` | +| local-dev hosts | `nix/host-modules/hive-gateway/default.nix` | + +⚠️ The certificate one is the least obvious and the most visible when +missed. `serviceDomains` is *both* the services sub-CA's +`nameConstraints` set and the leaf's SAN list, and the apex is a +**sibling** of `forge.` / `chat.` / `auth.`, not a +parent — no CA in the hierarchy issues for it implicitly. Left out, the +vhost falls back to the hive leaf and the swarm's front page opens with a +name mismatch. + +## Cross-references + +- [`sso.md`](sso.md) — the authelia instance itself, and the user store. +- [`../gateway.md`](../gateway.md) — the full vhost map and TLS modes.