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.
69 lines
3.1 KiB
Markdown
69 lines
3.1 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.
|