feat(3150): deliver the OIDC client secret from authelia to the homeserver

Runs on the host: the two containers share a network namespace but not a
filesystem root, so this is the only place both trees are addressable.

A copy rather than a bindMounts entry. nixos-container refuses to start
when a bind source is missing, and the secret does not exist until
authelia's first boot has minted it. The registration token dodges that
with an activation script that pre-creates the file; that is unavailable
here, because tuwunel requires the secret to exist and be non-empty, so a
placeholder would satisfy the mount and then stop the homeserver.

Bounded wait then fail, never a silent skip: authelia's container can be
up while its generator is still minting.
This commit is contained in:
atlas 2026-08-14 10:56:34 +02:00
commit f022e97813

View file

@ -660,6 +660,66 @@ in
lib.mkDefault matrixSecretPath
);
# The delivery. It runs on the HOST because that is the only place both
# container trees are addressable: they share this host's network
# namespace, which makes them feel co-located, but their filesystem
# roots are separate — the homeserver cannot open a path inside
# authelia's tree however local the port looks.
#
# ⚠️ Deliberately a copy and not a `bindMounts` entry.
# nixos-container refuses to start when a bind source is missing, and
# this secret does not exist until authelia's first boot has minted it
# — so binding it would make the homeserver wait on a file that waits
# on a container that starts after it. On a fresh hive that is a
# permanent stall presenting as "matrix is broken", several layers from
# its cause.
#
# The registration token above dodges that with an activation script
# that pre-creates the file. ⚠️ That dodge is NOT available here:
# tuwunel requires the secret file to exist *and be non-empty*, so a
# zero-byte placeholder would satisfy the bind mount and then stop the
# homeserver from starting.
systemd.services.hive-matrix-oidc-secret = lib.mkIf ssoLocal {
description = "deliver the homeserver's OIDC client secret from authelia";
after = [ "container@${autheliaCfg.machine}.service" ];
requires = [ "container@${autheliaCfg.machine}.service" ];
before = [ "container@hive-matrix.service" ];
wantedBy = [ "container@hive-matrix.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
SyslogIdentifier = "hive-matrix-oidc-secret";
};
path = [ pkgs.coreutils ];
script = ''
set -euo pipefail
src=${lib.escapeShellArg "${autheliaCfg.hostClientSecretDir}/${cfg.sso.clientId}.secret"}
dst=${lib.escapeShellArg "/var/lib/nixos-containers/hive-matrix${toString cfg.sso.clientSecretFile}"}
# authelia's container is up, but its first-boot generator may
# still be minting. Bounded wait, then fail: a silent skip here
# produces a homeserver whose SSO login dead-ends, which is the
# failure this whole design is trying not to ship.
for _ in $(seq 1 60); do
[ -s "$src" ] && break
sleep 2
done
if [ ! -s "$src" ]; then
echo "authelia has not minted $src after 120s" >&2
exit 1
fi
# root-owned 0400, and deliberately NOT the forge's `stat -c %u`
# uid discovery: that reads the service's state dir to learn which
# uid to hand the file to, and tuwunel runs under `DynamicUser`, so
# there is no stable uid to discover. It never reads this path
# directly anyway — `LoadCredential` does, as root, before the
# sandbox and the dynamic user exist.
install -D -m 0400 -o root -g root "$src" "$dst"
'';
};
# ⚠️ Deliberately NO `networking.hosts` entry for authelia's name, and
# the difference from hive-forge (which needs one) is worth stating:
# that container resolves through the host's resolvers, where the swarm