Compare commits
13 changed files with 74 additions and 135 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
|
||||
`deploy.authelia.hostClientSecretDir` with whatever secret management the
|
||||
`swarm.authelia.hostClientSecretDir` with whatever secret management the
|
||||
deployment already uses.
|
||||
|
||||
The identity is not a choice — a hive authenticates as `hive-<hiveName>`
|
||||
|
|
|
|||
|
|
@ -93,19 +93,6 @@ in
|
|||
[ "services" "hyperhive" "swarm" "authelia" "enable" ]
|
||||
[ "services" "hyperhive" "deploy" "authelia" "enable" ]
|
||||
)
|
||||
(lib.mkRenamedOptionModule
|
||||
[ "services" "hyperhive" "swarm" "authelia" "usersFile" ]
|
||||
[ "services" "hyperhive" "deploy" "authelia" "usersFile" ]
|
||||
)
|
||||
# `hostClientSecretDir` and `hostUsersFile` moved in the same commit and
|
||||
# deliberately have NO entry here. A rename module contributes a
|
||||
# *definition* to its target, and both are `readOnly`, which refuses a
|
||||
# second one — so a shim for either makes every evaluation fail with
|
||||
# "set multiple times", naming the option's own default as the rival
|
||||
# definition. Nothing could have set them anyway: both are derived and
|
||||
# read-only, so the shim's only possible customer is a reader of the old
|
||||
# path, and that reader now gets "option does not exist" instead, which
|
||||
# says the same thing sooner.
|
||||
(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.deploy.authelia.hostClientSecretDir` plays
|
||||
`services.hyperhive.swarm.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 "${deployCfg.authelia.hostClientSecretDir}/${cfg.sso.clientId}.secret"}
|
||||
src=${lib.escapeShellArg "${autheliaCfg.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 "${deployCfg.authelia.hostClientSecretDir}/${cfg.sso.clientId}.secret"}
|
||||
src=${lib.escapeShellArg "${autheliaCfg.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.deploy.authelia.hostClientSecretDir}/swarm-controller.secret"
|
||||
lib.mkDefault "${config.services.hyperhive.swarm.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.deploy.authelia.hostClientSecretDir}/"
|
||||
"${config.services.hyperhive.swarm.authelia.hostClientSecretDir}/"
|
||||
+ "${config.services.hyperhive.swarm.authelia.hiveClientPrefix}${config.services.hyperhive.hiveName}.secret"
|
||||
else
|
||||
null;
|
||||
defaultText = lib.literalExpression ''"''${deploy.authelia.hostClientSecretDir}/''${swarm.authelia.hiveClientPrefix}''${hiveName}.secret" when this host runs the swarm's IdP, else null'';
|
||||
defaultText = lib.literalExpression ''"''${swarm.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,6 +421,25 @@ 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;
|
||||
|
|
@ -699,6 +718,43 @@ 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";
|
||||
|
|
@ -748,75 +804,6 @@ 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
|
||||
|
|
@ -1182,7 +1169,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 deployCfg.authelia.usersFile}
|
||||
users=${lib.escapeShellArg cfg.usersFile}
|
||||
if [ ! -s "$users" ]; then
|
||||
echo "users: {}" > "$users"
|
||||
echo "seeded empty users database at $users"
|
||||
|
|
@ -1191,7 +1178,7 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
# The only process allowed to write `deployCfg.authelia.usersFile` — see that
|
||||
# The only process allowed to write `cfg.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
|
||||
|
|
@ -1230,7 +1217,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 = deployCfg.authelia.usersFile;
|
||||
SWARM_AUTHELIA_BRIDGE_USERS_FILE = cfg.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
|
||||
|
|
@ -1349,7 +1336,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 = deployCfg.authelia.usersFile;
|
||||
path = cfg.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 = deployCfg.authelia.hostUsersFile;
|
||||
SWARMCTL_AUTHELIA_USERS_FILE = autheliaCfg.hostUsersFile;
|
||||
};
|
||||
|
||||
forgeCfg = config.services.hyperhive.swarm.forge;
|
||||
|
|
|
|||
|
|
@ -478,7 +478,7 @@ in
|
|||
script = ''
|
||||
set -euo pipefail
|
||||
|
||||
src=${lib.escapeShellArg "${deployCfg.authelia.hostClientSecretDir}/${cfg.oidc.clientId}.secret"}
|
||||
src=${lib.escapeShellArg "${autheliaCfg.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 = "${deployCfg.authelia.hostClientSecretDir}/${cfg.clientId}.secret";
|
||||
clientSecretSource = "${autheliaCfg.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 "${deployCfg.authelia.hostClientSecretDir}/${cfg.clientId}.secret"}
|
||||
src=${lib.escapeShellArg "${autheliaCfg.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,11 +483,8 @@ in
|
|||
clientSecretFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default =
|
||||
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'';
|
||||
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'';
|
||||
example = "/var/lib/secrets/swarm-queue-client.secret";
|
||||
description = ''
|
||||
Path to a file holding the plaintext client secret for this
|
||||
|
|
@ -501,7 +498,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.deploy.authelia.hostClientSecretDir`
|
||||
{option}`services.hyperhive.swarm.authelia.hostClientSecretDir`
|
||||
with whatever secret management this deployment already uses.
|
||||
'';
|
||||
};
|
||||
|
|
|
|||
|
|
@ -118,24 +118,6 @@ let
|
|||
swarm.nats.calloutIssuerSeedFile = "/run/secrets/nats-issuer.seed";
|
||||
};
|
||||
|
||||
# Seventh split slice. Only `usersFile` has a rename entry: the other two
|
||||
# movers are `readOnly`, and a rename module contributes a definition, which
|
||||
# a read-only option refuses — see ./host-modules/deploy.nix. So the two arms
|
||||
# below have different jobs. `usersFile` tests the rename; the nats one tests
|
||||
# that a reader repointed to the new namespace still renders the derived
|
||||
# path, which is the failure this slice could actually have shipped — seven
|
||||
# of those reads went through an alias a path-shaped grep cannot see.
|
||||
autheliaOldPath = hive {
|
||||
deploy.authelia.enable = true;
|
||||
deploy.nats.enable = true;
|
||||
swarm.authelia.usersFile = "/var/lib/test-authelia/users.yml";
|
||||
swarm.nats.autoGenerateCallout = false;
|
||||
swarm.nats.calloutUserPublicKey = "UTESTUSERPUBKEYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
swarm.nats.calloutIssuerPublicKey = "ATESTISSUERPUBKEYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
swarm.nats.calloutUserSeedFile = "/run/secrets/nats-user.seed";
|
||||
swarm.nats.calloutIssuerSeedFile = "/run/secrets/nats-issuer.seed";
|
||||
};
|
||||
|
||||
controllerOldPath = hive {
|
||||
deploy.swarm-controller.enable = true;
|
||||
swarm.controller.socketPath = "/run/test-ctrl/ctrl.sock";
|
||||
|
|
@ -369,20 +351,6 @@ let
|
|||
rule: lib.hasInfix "/run/test-grafana-sock" rule
|
||||
) grafanaOldPath.systemd.tmpfiles.rules;
|
||||
}
|
||||
{
|
||||
name = "a config written against the pre-rename authelia usersFile still reaches the bridge";
|
||||
ok =
|
||||
autheliaOldPath.containers.swarm-authelia.config.systemd.services.swarm-authelia-bridge.environment.SWARM_AUTHELIA_BRIDGE_USERS_FILE
|
||||
== "/var/lib/test-authelia/users.yml";
|
||||
}
|
||||
{
|
||||
# Not a rename test. `hostClientSecretDir` is `readOnly`, so the fixture
|
||||
# cannot define it; what can break is a reader left pointing at the
|
||||
# namespace it moved out of. Five modules read this through an
|
||||
# `autheliaCfg` alias, where a path-shaped grep does not see it.
|
||||
name = "a consumer of authelia's host client-secret dir renders it from the deploy namespace";
|
||||
ok = lib.hasInfix "/var/lib/nixos-containers/swarm-authelia/var/lib/authelia-swarm/oidc-clients/" autheliaOldPath.systemd.services.swarm-nats-auth-secrets.script;
|
||||
}
|
||||
{
|
||||
# The gateway's per-name issuer choice. If this ever collapses to a
|
||||
# constant, every swarm-service vhost serves a certificate its CA
|
||||
|
|
|
|||
Loading…
Reference in a new issue