feat(#3089): add swarmctl and a user-add verb for the swarm's SSO
The swarm-authelia module states that its users database is written by swarm-controller, but nothing ever granted the means. This adds the tool that does it. swarmctl runs as root on the controller's host and acts directly. The rootless alternative was examined and does not work: relocating the users file into a directory the controller owns only turns a write problem into a read problem, because authelia must then reach across the same boundary in the other direction. Making that read work needs either a hand-pinned gid or world-readable password hashes. The user store is two files, one authoritative: users.json is canonical, users.yml is a rendered artifact. That split is what lets the 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. Passwords are generated by authelia rather than passed to it: argv is world-readable, so a password on a command line is readable by any local process for the lifetime of the call. The three derived facts swarmctl needs about the authelia container -- machine, unit and the host-side users path -- become readOnly options on the authelia module rather than literals repeated at the call site.
This commit is contained in:
parent
3a69ad4256
commit
9e44efa01f
11 changed files with 995 additions and 1 deletions
|
|
@ -150,10 +150,54 @@ in
|
|||
is the correct state before anything has provisioned users.
|
||||
'';
|
||||
};
|
||||
|
||||
# Derived facts, exposed for consumers that have to act on this
|
||||
# container **from outside it** — `swarmctl` is the first, and it
|
||||
# needs all three. Read-only options rather than literals repeated at
|
||||
# the call site: the machine and unit names are derived from
|
||||
# `instance` here, so a second copy elsewhere is a second thing to
|
||||
# keep in step, and the one that drifts is the one nobody tests.
|
||||
machine = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
readOnly = true;
|
||||
default = "swarm-authelia";
|
||||
description = ''
|
||||
Name of the nixos-container authelia runs in. Read-only: it is
|
||||
what this module declares, published so callers of
|
||||
`systemctl -M` and `/var/lib/nixos-containers/<name>` do not
|
||||
have to hardcode it.
|
||||
'';
|
||||
};
|
||||
|
||||
unit = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
readOnly = true;
|
||||
default = "${unitName}.service";
|
||||
description = ''
|
||||
authelia's systemd unit *inside* the container. Read-only, and
|
||||
derived from the instance name exactly like the unit itself.
|
||||
'';
|
||||
};
|
||||
|
||||
hostUsersFile = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
readOnly = true;
|
||||
default = "/var/lib/nixos-containers/${cfg.machine}${cfg.usersFile}";
|
||||
description = ''
|
||||
`usersFile` as seen from the **host** — the container's root
|
||||
prefixed onto the path authelia sees.
|
||||
|
||||
The distinction is load-bearing: the users database is written
|
||||
from the host by a program that does not live in this container,
|
||||
while authelia only ever sees the inner path. Handing the wrong
|
||||
one to either side yields a file nobody reads rather than an
|
||||
error.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf (hyperhiveCfg.enable && cfg.enable) {
|
||||
containers.swarm-authelia = {
|
||||
containers.${cfg.machine} = {
|
||||
autoStart = true;
|
||||
ephemeral = false;
|
||||
# Shared host netns, like the forge and matrix containers: the
|
||||
|
|
|
|||
Loading…
Reference in a new issue