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:
parent
9d55cba786
commit
9c09653603
12 changed files with 102 additions and 74 deletions
|
|
@ -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>`
|
||||
|
|
|
|||
|
|
@ -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" ]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
'';
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue