setup.md said Swarm SSO creates the operator's forge account, which was not true until the previous commits. It now says how: sign in to the forge once through authelia, then `swarmctl forge make-admin <you>`. sso.md says what that first login does and why ACCOUNT_LINKING is `login`. README, hivectl.md and forge.md drop `hivectl forge create-user`, and the swarmctl README gains `forge make-admin`. Refs #3782
125 lines
5.8 KiB
Markdown
125 lines
5.8 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 this binary **serves** no socket, publishes no HTTP route and has no
|
|
privileged helper. 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.
|
|
|
|
That rules out a way in, not a way out: `agent create` is a _client_ of
|
|
the controller's socket, because the work it asks for is a job graph only
|
|
the controller can queue (see below). Nothing here becomes reachable by
|
|
that.
|
|
|
|
## 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 |
|
|
| `SWARM_CONTROLLER_SOCKET` | the controller's unix socket, for `agent create` |
|
|
|
|
## `user add`
|
|
|
|
```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.
|
|
|
|
## `agent create`
|
|
|
|
```console
|
|
# swarmctl agent create scribe --hive alpha
|
|
queued: job node 42
|
|
agent "scribe" will be deployed to hive "alpha" once the job graph runs; `swarmctl` does not wait for it
|
|
```
|
|
|
|
`POST /api/agents` on the swarm-controller, over its unix socket
|
|
(`--controller-socket`, else `SWARM_CONTROLLER_SOCKET`, which the nix
|
|
module sets from the daemon's own `socketPath`). It prints the queued
|
|
job's node id **and stops there**.
|
|
|
|
It deliberately does not wait. The endpoint queues a DAG — SSO identity,
|
|
forge user, forge repo, repo membership, config-repo seed, then a deploy
|
|
message — and the last of those _publishes_: the hive's `hive-c0re` picks
|
|
it up and converges on its own clock, out of the controller's sight. So
|
|
even a fully settled graph would not mean the agent is up, and there is
|
|
nothing this CLI could wait for that would let it say so honestly. Watch
|
|
the swarm UI's job view for the rest.
|
|
|
|
No approval gate, for the same reason nothing else here has one: running
|
|
this binary already means being root on the controller's host.
|
|
|
|
`--hive` is required. It is an _address_ — where the deploy message goes
|
|
— not an attribute of the agent, so there is no sensible default. The
|
|
controller checks it against the swarm's hive roster and names the hives
|
|
that would have worked when it misses.
|
|
|
|
The request and response shapes are mirrored in `src/agent.rs` rather
|
|
than shared: the controller's own types are private to its binary, this
|
|
crate does not link it, and there is no wire-type crate between them.
|
|
Two fields out, two in, both ends validating — a drift shows up as a
|
|
400 naming the field.
|
|
|
|
## `forge make-admin`
|
|
|
|
```console
|
|
# swarmctl forge make-admin mara
|
|
forge: "mara" is now a site admin
|
|
```
|
|
|
|
`POST /api/forge/users/{name}/admin` on the swarm-controller, over the same
|
|
socket as `agent create`. It never creates an account: the forge makes a
|
|
person's on their first login through authelia, and until then this fails
|
|
saying so. Running it on a site admin changes nothing, and an agent's name is
|
|
refused. The response shape is mirrored in `src/forge.rs`, for the same
|
|
reason as `agent create`'s.
|