diff --git a/docs/swarm/README.md b/docs/swarm/README.md index 2729f9d1..e5ae679e 100644 --- a/docs/swarm/README.md +++ b/docs/swarm/README.md @@ -338,7 +338,7 @@ On a host that runs the queue and the IdP itself, all three default to the local ones and there is nothing to set. Any other hive needs them spelled out, and needs the secret to physically be there: the swarm does not distribute it. Copy `hive-.secret` out of the swarm host's -`swarm.authelia.hostClientSecretDir` with whatever secret management the +`deploy.authelia.hostClientSecretDir` with whatever secret management the deployment already uses. The identity is not a choice — a hive authenticates as `hive-` diff --git a/nix/host-modules/deploy.nix b/nix/host-modules/deploy.nix index f1964fc9..83d1e732 100644 --- a/nix/host-modules/deploy.nix +++ b/nix/host-modules/deploy.nix @@ -93,6 +93,18 @@ in [ "services" "hyperhive" "swarm" "authelia" "enable" ] [ "services" "hyperhive" "deploy" "authelia" "enable" ] ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "swarm" "authelia" "usersFile" ] + [ "services" "hyperhive" "deploy" "authelia" "usersFile" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "swarm" "authelia" "hostClientSecretDir" ] + [ "services" "hyperhive" "deploy" "authelia" "hostClientSecretDir" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "swarm" "authelia" "hostUsersFile" ] + [ "services" "hyperhive" "deploy" "authelia" "hostUsersFile" ] + ) (lib.mkRenamedOptionModule [ "services" "hyperhive" "swarm" "nats" "enable" ] [ "services" "hyperhive" "deploy" "nats" "enable" ] diff --git a/nix/host-modules/hive-forge/default.nix b/nix/host-modules/hive-forge/default.nix index b89b5120..a171553f 100644 --- a/nix/host-modules/hive-forge/default.nix +++ b/nix/host-modules/hive-forge/default.nix @@ -428,7 +428,7 @@ in host, which copies the token out). Same role for this token as - `services.hyperhive.swarm.authelia.hostClientSecretDir` plays + `services.hyperhive.deploy.authelia.hostClientSecretDir` plays for the OIDC secret: a **host**-local path (not inside any container), read directly by `swarm-controller.nix`'s `LoadCredential` when the controller runs on this same host. @@ -1258,7 +1258,7 @@ in script = '' set -euo pipefail - src=${lib.escapeShellArg "${autheliaCfg.hostClientSecretDir}/${cfg.sso.clientId}.secret"} + src=${lib.escapeShellArg "${deployCfg.authelia.hostClientSecretDir}/${cfg.sso.clientId}.secret"} dst=${lib.escapeShellArg "/var/lib/nixos-containers/hive-forge${forgeSecretPath}"} # authelia's container is up, but its first-boot generator may diff --git a/nix/host-modules/hive-matrix.nix b/nix/host-modules/hive-matrix.nix index 1438b020..49861540 100644 --- a/nix/host-modules/hive-matrix.nix +++ b/nix/host-modules/hive-matrix.nix @@ -799,7 +799,7 @@ in script = '' set -euo pipefail - src=${lib.escapeShellArg "${autheliaCfg.hostClientSecretDir}/${cfg.sso.clientId}.secret"} + src=${lib.escapeShellArg "${deployCfg.authelia.hostClientSecretDir}/${cfg.sso.clientId}.secret"} dst=${lib.escapeShellArg "/var/lib/nixos-containers/hive-matrix${toString deployCfg.matrix.sso.clientSecretFile}"} # authelia's container is up, but its first-boot generator may diff --git a/nix/host-modules/local-defaults.nix b/nix/host-modules/local-defaults.nix index afa94b7b..7f23761e 100644 --- a/nix/host-modules/local-defaults.nix +++ b/nix/host-modules/local-defaults.nix @@ -137,6 +137,6 @@ in # evaluation of a single-host swarm. config.services.hyperhive.deploy.swarm-controller.queue.clientSecretFile = lib.mkIf cfg.deploy.singleHostSwarm ( - lib.mkDefault "${config.services.hyperhive.swarm.authelia.hostClientSecretDir}/swarm-controller.secret" + lib.mkDefault "${config.services.hyperhive.deploy.authelia.hostClientSecretDir}/swarm-controller.secret" ); } diff --git a/nix/host-modules/otel.nix b/nix/host-modules/otel.nix index 9bff01ae..95ed69e1 100644 --- a/nix/host-modules/otel.nix +++ b/nix/host-modules/otel.nix @@ -234,11 +234,11 @@ in if config.services.hyperhive.deploy.authelia.enable && config.services.hyperhive.hiveName != null then - "${config.services.hyperhive.swarm.authelia.hostClientSecretDir}/" + "${config.services.hyperhive.deploy.authelia.hostClientSecretDir}/" + "${config.services.hyperhive.swarm.authelia.hiveClientPrefix}${config.services.hyperhive.hiveName}.secret" else null; - defaultText = lib.literalExpression ''"''${swarm.authelia.hostClientSecretDir}/''${swarm.authelia.hiveClientPrefix}''${hiveName}.secret" when this host runs the swarm's IdP, else null''; + defaultText = lib.literalExpression ''"''${deploy.authelia.hostClientSecretDir}/''${swarm.authelia.hiveClientPrefix}''${hiveName}.secret" when this host runs the swarm's IdP, else null''; example = "/var/lib/secrets/hive-telemetry.secret"; description = '' Absolute path to this hive's OAuth2 client secret, used to diff --git a/nix/host-modules/swarm-authelia.nix b/nix/host-modules/swarm-authelia.nix index afb989f6..0c0e7e30 100644 --- a/nix/host-modules/swarm-authelia.nix +++ b/nix/host-modules/swarm-authelia.nix @@ -421,25 +421,6 @@ in ''; }; - usersFile = lib.mkOption { - type = lib.types.str; - default = "${stateDir}/users.yml"; - defaultText = lib.literalExpression ''"/var/lib/authelia-swarm/users.yml"''; - description = '' - Path (inside the container) of authelia's file users database. - - Written by `swarm-authelia-bridge`, not by hand: agents come and - go continuously, so the subject set is dynamic and belongs to a - program. `swarm-controller` cannot write this file itself — a - different uid owns it — so the bridge is the only writer, - running inside this same container as this file's actual owner. - This module only guarantees the file *exists* and is valid YAML - at first boot, so authelia starts with no subjects rather than - failing to start — a provider with nobody in it yet is the - correct state before anything has provisioned users. - ''; - }; - oidc.hiveIdentities = lib.mkOption { type = lib.types.bool; default = deployCfg.nats.enable; @@ -718,43 +699,6 @@ in ''; }; - hostClientSecretDir = lib.mkOption { - type = lib.types.str; - readOnly = true; - default = "/var/lib/nixos-containers/${cfg.machine}${clientsDir}"; - description = '' - Where the minted client secrets sit **as seen from the host** — - `.secret` holds a plaintext, `.digest` the hash authelia - itself reads. - - Published for the same reason as `hostUsersFile`: the plaintext's - other reader lives in a **different container**, and containers - that share this host's network namespace still have separate - filesystem roots. The host is the only place both trees are - addressable, so the host is where a delivery step has to run. - - ⚠️ Nothing here exists until authelia's **first boot** has run. - A consumer must wait for it — it cannot be a `bindMounts` source, - because nixos-container refuses to start when a bind source is - missing, and that turns a fresh hive into a boot-order deadlock. - ''; - }; - - 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. - - Published for callers that only ever need to *read* the file - (e.g. an operator diagnosing a bad entry). `swarm-authelia-bridge` - itself never uses this path — it runs inside the container, as - the file's own owner, and writes the in-container path directly. - ''; - }; - bridgePackage = lib.mkOption { type = lib.types.package; defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarm-authelia-bridge"; @@ -804,6 +748,75 @@ in }; }; + # What stays above is what authelia IS to every hive: where it answers + # (`url`), the OIDC register every service checks itself against, its port. + # What the host running the container decides is here, and all three of + # these are the same kind of thing — a filesystem path that only exists on + # the machine that runs `swarm-authelia`. A hive that does not run it has + # nothing at any of them. `enable` already lives in ./deploy.nix, which + # also carries the renames. + # + # ⚠️ `usersFile` is a path *inside* the container and still belongs here: + # a path's scope is the scope of the filesystem it names, and that + # filesystem is this host's container root. + options.services.hyperhive.deploy.authelia = { + usersFile = lib.mkOption { + type = lib.types.str; + default = "${stateDir}/users.yml"; + defaultText = lib.literalExpression ''"/var/lib/authelia-swarm/users.yml"''; + description = '' + Path (inside the container) of authelia's file users database. + + Written by `swarm-authelia-bridge`, not by hand: agents come and + go continuously, so the subject set is dynamic and belongs to a + program. `swarm-controller` cannot write this file itself — a + different uid owns it — so the bridge is the only writer, + running inside this same container as this file's actual owner. + This module only guarantees the file *exists* and is valid YAML + at first boot, so authelia starts with no subjects rather than + failing to start — a provider with nobody in it yet is the + correct state before anything has provisioned users. + ''; + }; + + hostClientSecretDir = lib.mkOption { + type = lib.types.str; + readOnly = true; + default = "/var/lib/nixos-containers/${cfg.machine}${clientsDir}"; + description = '' + Where the minted client secrets sit **as seen from the host** — + `.secret` holds a plaintext, `.digest` the hash authelia + itself reads. + + Published for the same reason as `hostUsersFile`: the plaintext's + other reader lives in a **different container**, and containers + that share this host's network namespace still have separate + filesystem roots. The host is the only place both trees are + addressable, so the host is where a delivery step has to run. + + ⚠️ Nothing here exists until authelia's **first boot** has run. + A consumer must wait for it — it cannot be a `bindMounts` source, + because nixos-container refuses to start when a bind source is + missing, and that turns a fresh hive into a boot-order deadlock. + ''; + }; + + hostUsersFile = lib.mkOption { + type = lib.types.str; + readOnly = true; + default = "/var/lib/nixos-containers/${cfg.machine}${deployCfg.authelia.usersFile}"; + description = '' + `usersFile` as seen from the **host** — the container's root + prefixed onto the path authelia sees. + + Published for callers that only ever need to *read* the file + (e.g. an operator diagnosing a bad entry). `swarm-authelia-bridge` + itself never uses this path — it runs inside the container, as + the file's own owner, and writes the in-container path directly. + ''; + }; + }; + config = lib.mkIf (hyperhiveCfg.enable && deployCfg.authelia.enable) { # The derived half of the client list, declared the same way an # operator declares one. Everything downstream then reads a single @@ -1169,7 +1182,7 @@ in # it. authelia refuses to start without one, and the # alternative to an empty file is a placeholder account — # which is a credential nobody meant to create. - users=${lib.escapeShellArg cfg.usersFile} + users=${lib.escapeShellArg deployCfg.authelia.usersFile} if [ ! -s "$users" ]; then echo "users: {}" > "$users" echo "seeded empty users database at $users" @@ -1178,7 +1191,7 @@ in ''; }; - # The only process allowed to write `cfg.usersFile` — see that + # The only process allowed to write `deployCfg.authelia.usersFile` — see that # option's doc comment, and the crate's own README for the full # "why does an unprivileged swarm-controller need a bridge at # all" reasoning. Runs as `unitName` (`authelia-swarm`) — THE @@ -1217,7 +1230,7 @@ in # canonical stores for one physical file, which is what # made `swarm agent create` refuse to start on a hive whose # `users.yml` already held users. - SWARM_AUTHELIA_BRIDGE_USERS_FILE = cfg.usersFile; + SWARM_AUTHELIA_BRIDGE_USERS_FILE = deployCfg.authelia.usersFile; # The CONFIGURED authelia, not whatever is on `PATH`: the # argon2 parameters baked into a hash have to match the # verifier's — same reasoning as `swarmctl`'s own @@ -1336,7 +1349,7 @@ in # 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; + path = deployCfg.authelia.usersFile; watch = true; }; diff --git a/nix/host-modules/swarm-controller.nix b/nix/host-modules/swarm-controller.nix index fe1c505e..85d9bee6 100644 --- a/nix/host-modules/swarm-controller.nix +++ b/nix/host-modules/swarm-controller.nix @@ -29,7 +29,7 @@ let # The CONFIGURED authelia, not whatever is on PATH: the argon2 # 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_USERS_FILE = deployCfg.authelia.hostUsersFile; }; forgeCfg = config.services.hyperhive.swarm.forge; diff --git a/nix/host-modules/swarm-grafana.nix b/nix/host-modules/swarm-grafana.nix index 3ec186e0..58e7fce0 100644 --- a/nix/host-modules/swarm-grafana.nix +++ b/nix/host-modules/swarm-grafana.nix @@ -478,7 +478,7 @@ in script = '' set -euo pipefail - src=${lib.escapeShellArg "${autheliaCfg.hostClientSecretDir}/${cfg.oidc.clientId}.secret"} + src=${lib.escapeShellArg "${deployCfg.authelia.hostClientSecretDir}/${cfg.oidc.clientId}.secret"} dst=${lib.escapeShellArg "/var/lib/nixos-containers/${cfg.machine}${secretPath}"} # authelia's container is up, but its first-boot generator may still diff --git a/nix/host-modules/swarm-nats.nix b/nix/host-modules/swarm-nats.nix index 605c5c4a..031eeedc 100644 --- a/nix/host-modules/swarm-nats.nix +++ b/nix/host-modules/swarm-nats.nix @@ -57,7 +57,7 @@ let responderConfigured = deployCfg.nats.autoGenerateCallout || (deployCfg.nats.calloutUserSeedFile != "" && deployCfg.nats.calloutIssuerSeedFile != ""); - clientSecretSource = "${autheliaCfg.hostClientSecretDir}/${cfg.clientId}.secret"; + clientSecretSource = "${deployCfg.authelia.hostClientSecretDir}/${cfg.clientId}.secret"; introspectionUrl = "${toString autheliaUrl}/api/oidc/introspection"; # Where the responder's seeds actually come from. One name for two diff --git a/nix/host-modules/swarm-otel.nix b/nix/host-modules/swarm-otel.nix index a6e8f278..9b8c0adf 100644 --- a/nix/host-modules/swarm-otel.nix +++ b/nix/host-modules/swarm-otel.nix @@ -654,7 +654,7 @@ in script = '' set -euo pipefail - src=${lib.escapeShellArg "${autheliaCfg.hostClientSecretDir}/${cfg.clientId}.secret"} + src=${lib.escapeShellArg "${deployCfg.authelia.hostClientSecretDir}/${cfg.clientId}.secret"} dst=${lib.escapeShellArg "/var/lib/nixos-containers/${cfg.machine}${collectorSecretInContainer}"} # authelia's container is up, but its first-boot generator may diff --git a/nix/host-modules/swarm.nix b/nix/host-modules/swarm.nix index bf0a73cc..8867cc85 100644 --- a/nix/host-modules/swarm.nix +++ b/nix/host-modules/swarm.nix @@ -483,8 +483,11 @@ in clientSecretFile = lib.mkOption { type = lib.types.nullOr lib.types.str; default = - if queueLocal then "${swarmCfg.authelia.hostClientSecretDir}/hive-${cfg.hiveName}.secret" else null; - defaultText = lib.literalExpression ''"''${swarm.authelia.hostClientSecretDir}/hive-''${hiveName}.secret" when this host runs both the queue and the IdP, else null''; + if queueLocal then + "${deployCfg.authelia.hostClientSecretDir}/hive-${cfg.hiveName}.secret" + else + null; + defaultText = lib.literalExpression ''"''${deploy.authelia.hostClientSecretDir}/hive-''${hiveName}.secret" when this host runs both the queue and the IdP, else null''; example = "/var/lib/secrets/swarm-queue-client.secret"; description = '' Path to a file holding the plaintext client secret for this @@ -498,7 +501,7 @@ in this host. On any other hive the secret has to get here somehow, and the swarm does not distribute it — copy it out of the swarm host's - {option}`services.hyperhive.swarm.authelia.hostClientSecretDir` + {option}`services.hyperhive.deploy.authelia.hostClientSecretDir` with whatever secret management this deployment already uses. ''; };