hyperhive/swarmctl/README.md
atlas 39b95c2ede treefmt: apply prettier
Pure `nix fmt` output from the commit before this one — no hand edits.
203 files: 52 md, 42 tsx, 32 js, 32 css, 21 ts, 13 html, 8 json, 3 mjs.

Reproduce with `nix develop -c nix fmt` on the parent commit; the result
should be byte-identical to this tree.

None of the 13 `.prettierignore` entries appears here — verified by
intersecting the changed-file list against the ignore file, with a
control proving the intersection finds a match when one exists.
2026-09-02 15:25:07 +02:00

69 lines
3.3 KiB
Markdown

# swarmctl
Swarm-level operator CLI. Runs as **root on the host running
`swarm-controller`**, and acts on that host directly.
Distinct from `hivectl`, which drives one hive's `hive-c0re` over its
admin socket. This crate does not link `swarm-controller`, for the same
reason `hivectl` does not link `hive-c0re`.
## Why root, and why no socket
The first verb writes authelia's users database. Making that write
rootless was examined and rejected — **relocating the file only turns a
write problem into a read problem.** Move `users.yml` into a directory
the controller owns and the controller can write it, but authelia then
has to read it across the same boundary in the other direction. Making
_that_ work needs either a hand-pinned gid (the container's uids are
allocated inside it, at activation — see the uid-assignment issue) or
world-readable password hashes. Both are worse than root.
So there is no socket, no HTTP route and no privileged helper here. When
a verb has to run as a non-root user or from another host, the answer is
a **group-gated admin socket**, separate from the controller's `0666`
gateway-facing one — not a widening of what root does here.
## One file, two writers
`users.yml` — authelia's own users database — is read and written
directly. There is no second store.
There used to be: a private `users.json` here, canonical, with `users.yml`
rendered from it, while `swarm-authelia-bridge` kept its own pair against
the _same_ physical file. Two canonical stores for one file is a seam, and
it bit — a writer whose own JSON was missing could not tell "nothing here
yet" from "someone else's users", and refused to write (#3422).
The argument for the split was that it let this crate work without a YAML
parser. It didn't: the JSON was read back on every run, so the round-trip
was already being paid — the two files differed only in _format_.
⚠️ The file is round-tripped, so **comments and hand-formatting do not
survive a write**. Values do, and so do keys this binary does not model.
See `swarm-authelia-bridge/README.md` for what both writers must uphold.
## Configuration
Every path comes from the nix module that installs the binary, because
every one is derived from an option that module owns. They are required
rather than defaulted — a default would be an address we _hope_ points at
something, and one that resolves cleanly to the wrong place is worse than
an error.
| variable | what |
| ------------------------------ | -------------------------------------------------------------------- |
| `SWARMCTL_AUTHELIA_BIN` | the **configured** authelia; argon2 params must match the verifier's |
| `SWARMCTL_AUTHELIA_USERS_FILE` | host-side path of the users database |
| `SWARMCTL_AUTHELIA_MACHINE` | container name, for `systemctl -M` |
| `SWARMCTL_AUTHELIA_UNIT` | authelia's unit inside that container |
## Usage
```console
# swarmctl user add mara --display-name "Mara" --group admins
```
The password is **generated by authelia** (`crypto hash generate argon2
--random`) and printed once. It is never passed on a command line:
`/proc/<pid>/cmdline` is world-readable, so a password in argv is readable
by any local process for the lifetime of the call.