hyperhive/docs/swarm/ui.md
atlas 6ca4887af4 docs(#3422): the user store is one file, not two
Six places asserted the old design as fact, and none of them mention the
change by name -- the class of doc breakage that is found by asking what
a diff made untrue, not by grepping for a feature:

- swarmctl/README.md and swarm-authelia-bridge/README.md both described
  their own private canonical store. The bridge's "known limitation"
  section described the seam as unsolved; it is what this fixes, so it
  becomes what both writers must uphold instead.
- docs/swarm/{sso,ui,secrets}.md described a rendered artifact.
- The repo CLAUDE.md entry for swarmctl said the same.
- docs/tools/swarmctl-cli.md is regenerated (CI diffs it against the
  clap tree), picking up the removed --store flag.

Operator-facing where it is read: the hand-editing consequence (values
survive a rewrite, comments do not) is stated in sso.md, where an
operator is being told to edit the file, rather than only in a module doc.
2026-08-18 10:34:00 +02:00

113 lines
5 KiB
Markdown

# 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 `admins` 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:admins`. An account without that group authenticates
fine and still gets bounced.
```sh
swarmctl user add <you> --group admins
```
`admins` deliberately, not a new word: [`../setup.md`](../setup.md) has
told every operator to create exactly that group since the bootstrap step
existed, so an account made by following the guide already passes. This
is the first rule that *consumes* a group name — inventing a second one
would have meant those accounts silently failing a check they were
supposed to pass.
An account created without any group needs re-adding with the flag —
`swarmctl` reads the existing entry out of `users.yml`, 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.
## Two wiring sites
Adding a swarm service name means touching two things. Missing the
second ships as a different flavour of "works from the host, broken from
a container":
| site | file |
| --- | --- |
| vhost + `gateway.localNames` | the service's own module (e.g. `nix/host-modules/swarm-ui.nix`) |
| **certificate name** | `nix/host-modules/swarm.nix` (`serviceDomains`) |
The DNS record and the local-dev `/etc/hosts` entry need no separate
edit: both are derived from `services.hyperhive.gateway.localNames`,
which a service's own module already has to push its domain into to be
resolvable — see `nix/host-modules/hive-gateway/dnsmasq.nix` and
`.../default.nix`'s `networking.hosts`. `vhosts.nix` itself is scoped to
the surface the hive's own domain serves (dashboard, per-agent routing,
matrix discovery); a swarm service declares its own vhost next to its
own options, the way `swarm-ui.nix` and `swarm-authelia.nix` do.
⚠️ 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.
## Quick links
The swarm UI's header carries a single 🔗 button, visible on every route,
opening a popover of links to other swarm-wide services — authelia,
matrix, forge, this UI's own swagger docs. Backed by `GET /api/links`
(swarm-controller), which serves `services.hyperhive.swarm.controller.links`
(a `listOf { label, icon, url }`, same shape as the per-agent
`hyperhive.dashboardLinks`).
Rather than one central hardcoded list, each service's own module
contributes its own entry when it's actually enabled on the controller's
host — `swarm-authelia.nix`, `hive-matrix.nix` and `hive-forge/default.nix`
all do, the same list-merge idiom `gateway.localNames` uses above. Adding a
link for a new service is a nix-only change to that service's own module
(or an operator adding an entry directly); no swarm-controller or swarm-ui
change needed. Empty list hides the button rather than showing an empty
popover.
## 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.