feat(#3089): add swarmctl and a user-add verb for the swarm's SSO

The swarm-authelia module states that its users database is written by
swarm-controller, but nothing ever granted the means. This adds the tool
that does it.

swarmctl runs as root on the controller's host and acts directly. The
rootless alternative was examined and does not work: relocating the users
file into a directory the controller owns only turns a write problem into
a read problem, because authelia must then reach across the same boundary
in the other direction. Making that read work needs either a hand-pinned
gid or world-readable password hashes.

The user store is two files, one authoritative: users.json is canonical,
users.yml is a rendered artifact. That split is what lets the crate work
without a YAML parser -- the workspace has none, and adding one costs a
crates.io fetch, a lock update and a vendor hash for a schema we fully
control and only ever emit.

Passwords are generated by authelia rather than passed to it: argv is
world-readable, so a password on a command line is readable by any local
process for the lifetime of the call.

The three derived facts swarmctl needs about the authelia container --
machine, unit and the host-side users path -- become readOnly options on
the authelia module rather than literals repeated at the call site.
This commit is contained in:
atlas 2026-08-10 21:11:41 +02:00 committed by mara
commit 9e44efa01f
11 changed files with 995 additions and 1 deletions

View file

@ -148,6 +148,14 @@ hand-maintained per-file tree drifts out of sync with the code.
unix socket the gateway's nginx proxies to — ⚠️ **the socket's
directory is its access control**; the constraint that governs it is in
the crate's README, and a unit test pins the path.
- **`swarmctl/`** — swarm-level operator CLI, installed by the
swarm-controller module on the host that runs the daemon. Runs as
**root and acts directly** — no socket, no HTTP route, no priv helper;
the crate's README records why the rootless shape was examined and
rejected. Does not link `swarm-controller`, mirroring `hivectl` ÷
`hive-c0re`. ⚠️ Its user store is **two files, one authoritative**:
`users.json` is canonical, authelia's `users.yml` is a *rendered
artifact* that is written and never read back.
### External dependencies with no directory here