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