| Filename | Latest commit message | Latest commit date |
|---|---|---|
The second half of making one file canonical. swarmctl kept its own private JSON store and rendered users.yml from it, against the same physical file the bridge wrote -- the seam that made `swarm agent create` refuse to run. - users.yml is read before it is written, so users another writer added are loaded rather than treated as a file to refuse or clobber. The overwrite guard and the seed check go with the second store: they existed to police two stores that could disagree. - serde_norway replaces the hand-rolled emitter. Unknown top-level and per-user keys round-trip through `extra`, so two writers cannot delete each other's fields. - The synthetic email moves from render time to the write path and is stored. With the file as the store, "rendered but not persisted" has nowhere left to live, and mara ruled the stored address correct. - `--store` is removed rather than deprecated: a flag whose only remaining effect is nothing reads as accepted and does nothing. Three tests asserted the emitter's exact bytes, and one asserted the guard. Rewritten rather than deleted -- as round-trips for the former, and inverted for the latter, since "a populated file is READ" is the behaviour this change is for and deleting its test would leave it unpinned. |
||
| .. | ||
| src | ||
| Cargo.toml | ||
| README.md | ||
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.
Two files, one of them authoritative
users.json— canonical, ours, JSON.users.yml— a rendered artifact for authelia. Written, never read back.
The split is what lets this 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.
The shortcut of writing JSON into the .yml (JSON being a subset of
YAML) is deliberately not taken: authelia refuses to start on a users file
it cannot parse, so that file fronts the whole SSO provider's boot, and
"almost certainly parses" is not a claim worth betting a boot on without
running it.
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 |
SWARMCTL_STORE |
canonical store (defaults to the controller's state dir) |
Usage
# 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.