deploy: move authelia's three host paths out of swarm.authelia

`usersFile`, `hostClientSecretDir` and `hostUsersFile` are filesystem
paths that only exist on the machine running the `swarm-authelia`
container. A hive that does not run it has nothing at any of them, so
they fail the swarm-wide test the namespace's header states.

`usersFile` is a path *inside* the container and still belongs on the
deploy side: a path's scope is the scope of the filesystem it names, and
that filesystem is this host's container root.

The two `host*` options are `readOnly` and derived, so nothing can set
them through the rename shims — those serve readers. The shims are still
required: `mkRenamedOptionModule` is what keeps an out-of-tree module
reading the old path resolving at all.

Seven reads went through an alias rather than a full path
(`autheliaCfg.hostClientSecretDir` in five modules, `.hostUsersFile` in a
sixth, `swarmCfg.authelia.hostClientSecretDir` in a seventh). Every one
of those files already binds `deployCfg`, so the repoint needed no new
alias. Four more sites were prose, including one in `docs/`, which no
grep restricted to `nix/` would have found.
This commit is contained in:
atlas 2026-09-07 15:08:50 +02:00 committed by mara
commit 9c09653603
12 changed files with 102 additions and 74 deletions

View file

@ -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-<hiveName>.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-<hiveName>`

View file

@ -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" ]

View file

@ -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

View file

@ -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

View file

@ -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"
);
}

View file

@ -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

View file

@ -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**
`<id>.secret` holds a plaintext, `<id>.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**
`<id>.secret` holds a plaintext, `<id>.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;
};

View file

@ -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;

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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.
'';
};