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

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