diff --git a/docs/tools/swarmctl-cli.md b/docs/tools/swarmctl-cli.md index d38368fc..68acc6ce 100644 --- a/docs/tools/swarmctl-cli.md +++ b/docs/tools/swarmctl-cli.md @@ -26,8 +26,6 @@ swarm-level operator CLI * `--authelia-bin ` — authelia binary used to hash passwords. The argon2 parameters must match the verifier's, so this has to be the *configured* package rather than whatever is on `PATH` * `--users-file ` — Host-side path of authelia's users database — i.e. the path inside the container, prefixed with the container's root -* `--machine ` — Machine name of the authelia container, for `systemctl -M` -* `--unit ` — authelia's systemd unit inside that container * `--store ` — Canonical user store diff --git a/nix/host-modules/swarm-authelia.nix b/nix/host-modules/swarm-authelia.nix index 7cb4b7c8..cd63c2a6 100644 --- a/nix/host-modules/swarm-authelia.nix +++ b/nix/host-modules/swarm-authelia.nix @@ -967,7 +967,16 @@ in server.address = "tcp://127.0.0.1:${toString cfg.port}"; log.level = "info"; - authentication_backend.file.path = cfg.usersFile; + # `watch` is load-bearing, not a convenience: authelia reads + # this file once at startup, and `swarm-authelia-bridge` writes + # it to create agent identities while being unable to restart + # authelia — running unprivileged is the whole reason it may + # write the file at all. Without this, an identity it creates is + # real on disk and invisible until something unrelated restarts. + authentication_backend.file = { + path = cfg.usersFile; + watch = true; + }; # ⚠️ `one_factor` as the DEFAULT means "any authenticated # user", which is authentication, not authorisation. The diff --git a/nix/host-modules/swarm-controller.nix b/nix/host-modules/swarm-controller.nix index 3e28e43a..44464fd4 100644 --- a/nix/host-modules/swarm-controller.nix +++ b/nix/host-modules/swarm-controller.nix @@ -29,8 +29,6 @@ let # parameters baked into a hash have to match the verifier's. SWARMCTL_AUTHELIA_BIN = "${autheliaCfg.package}/bin/authelia"; SWARMCTL_AUTHELIA_USERS_FILE = autheliaCfg.hostUsersFile; - SWARMCTL_AUTHELIA_MACHINE = autheliaCfg.machine; - SWARMCTL_AUTHELIA_UNIT = autheliaCfg.unit; }; natsCfg = config.services.hyperhive.swarm.nats; diff --git a/swarmctl/src/main.rs b/swarmctl/src/main.rs index b3ff4dc2..925605f6 100644 --- a/swarmctl/src/main.rs +++ b/swarmctl/src/main.rs @@ -69,12 +69,6 @@ struct PathArgs { /// the container, prefixed with the container's root. #[arg(long, value_name = "PATH")] users_file: Option, - /// Machine name of the authelia container, for `systemctl -M`. - #[arg(long, value_name = "NAME")] - machine: Option, - /// authelia's systemd unit inside that container. - #[arg(long, value_name = "UNIT")] - unit: Option, /// Canonical user store. #[arg(long, value_name = "PATH")] store: Option, @@ -83,8 +77,6 @@ struct PathArgs { struct Paths { authelia_bin: PathBuf, users_file: PathBuf, - machine: String, - unit: String, store: PathBuf, } @@ -93,8 +85,6 @@ impl PathArgs { Ok(Paths { authelia_bin: path_from(self.authelia_bin, "SWARMCTL_AUTHELIA_BIN")?, users_file: path_from(self.users_file, "SWARMCTL_AUTHELIA_USERS_FILE")?, - machine: string_from(self.machine, "SWARMCTL_AUTHELIA_MACHINE")?, - unit: string_from(self.unit, "SWARMCTL_AUTHELIA_UNIT")?, store: self .store .or_else(|| std::env::var_os("SWARMCTL_STORE").map(PathBuf::from)) @@ -108,11 +98,6 @@ fn path_from(flag: Option, env: &str) -> Result { .with_context(|| missing(env)) } -fn string_from(flag: Option, env: &str) -> Result { - flag.or_else(|| std::env::var(env).ok()) - .with_context(|| missing(env)) -} - fn missing(env: &str) -> String { format!( "{env} is unset and no flag was given — swarmctl is installed and \ @@ -358,7 +343,12 @@ fn publish(paths: &Paths, store: &UserStore) -> Result<()> { write_atomic(&paths.store, &format!("{store_json}\n"))?; write_atomic(&paths.users_file, &rendered)?; - restart_authelia(&paths.machine, &paths.unit) + // No restart: authelia watches this file + // (`authentication_backend.file.watch`). Deliberately not `swarmctl`'s job + // — `swarm-authelia-bridge` writes the same file and *cannot* restart + // anything, since running unprivileged is the whole reason it may write + // it. A reload that depends on which process wrote is not a reload. + Ok(()) } /// Load the canonical store, or start an empty one if this deployment has @@ -456,29 +446,6 @@ fn parse_field(stdout: &str, marker: &str) -> Option { .filter(|value| !value.is_empty()) } -/// authelia re-reads its file backend at startup, so a users change needs -/// a restart. -/// -/// The file watcher (`authentication_backend.file.watch`) would remove -/// this step entirely, and is deliberately not relied on: it could not be -/// verified against the pinned build, and it carries two unknowns — -/// whether the watch survives the `rename(2)` used above, and whether it -/// can observe a partially written file. An explicit restart assumes -/// nothing. -fn restart_authelia(machine: &str, unit: &str) -> Result<()> { - let status = Command::new("systemctl") - .args(["-M", machine, "restart", unit]) - .status() - .context("running systemctl")?; - if !status.success() { - bail!( - "restarting {unit} in {machine} failed ({status}); the users file is \ - already written, so re-running the restart by hand completes the change" - ); - } - Ok(()) -} - /// Replace `path`'s contents atomically, preserving the existing owner /// and mode. /// diff --git a/swarmctl/src/users.rs b/swarmctl/src/users.rs index 8d38e911..400d89a3 100644 --- a/swarmctl/src/users.rs +++ b/swarmctl/src/users.rs @@ -495,7 +495,7 @@ mod tests { ..UserUpdate::default() }, ) - .expect_err("a no-op must not restart authelia"); + .expect_err("a no-op must not rewrite the user database"); assert!(err.to_string().contains("nothing to change"), "{err}"); }