# 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. 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//cmdline` is world-readable, so a password in argv is readable by any local process for the lifetime of the call.