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 <you> --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.
This commit is contained in:
atlas 2026-08-12 17:46:15 +02:00
commit 470d2ad845
3 changed files with 87 additions and 0 deletions

View file

@ -15,6 +15,7 @@ Single nginx in front of every hyperhive web surface. Runs on the **host**, next
| `matrix.<hive>/` | `matrix.<hive>` | fluffychat-web static | `matrix.gui.enable` |
| `matrix.<hive>/config.json` | `matrix.<hive>` | inline JSON (FluffyChat boot config) | `matrix.gui.enable && domain != null` |
| `auth.<swarm>/` | `auth.<swarm>` | authelia (`9091`) | `swarm.authelia.enable` |
| `<swarm>/` | `<swarm>` | 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.

View file

@ -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

80
docs/swarm/ui.md Normal file
View file

@ -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 <you> --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.<swarm>` / `chat.<swarm>` / `auth.<swarm>`, 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.