nix: address the swarm IdP by its domain, not by who runs it

`swarm.authelia.url` defaulted to `https://<domain>` only when this host
ran the container, and to `null` otherwise — so the address a client is
given was a statement about co-location rather than about the swarm. A
swarm has one SSO provider; every hive addresses the same name and
resolution decides which address that reaches, exactly as
`swarm.otel.domain` already works.

The option stays nullable: "this swarm has no IdP" is still expressible,
it is just now something an operator states rather than something not
running the container produces. The Grafana fixture that exercised the
no-IdP refusal says it explicitly.

Closes #4536
This commit is contained in:
atlas 2026-09-19 20:39:19 +02:00 committed by mara
commit 4b6214305f
4 changed files with 61 additions and 14 deletions

View file

@ -70,10 +70,12 @@ there is one IdP and one auth path.
- **`deploy.authelia`** — run the container here.
- **`swarm.authelia.url`** — where clients go to authenticate.
Present on **every** hive, defaulting to this host's own instance only
when this module is the thing running it; otherwise `null`, and a hive
joining someone else's swarm sets it explicitly. Null means "no SSO
configured", and consumers say so rather than guessing an address.
Present on **every** hive and the same value on all of them:
`https://<swarm.authelia.domain>`, whether or not this host runs the
container. The name is what a client is given; resolution decides which
address it reaches. Set it explicitly when joining a swarm whose IdP is
under another name, or to `null` to say the swarm has no SSO — consumers
then refuse rather than guessing an address.
swarm-controller writes the users database, not by hand: hive-c0re
creates and destroys agents continuously, so the subject set is dynamic.

View file

@ -458,22 +458,25 @@ in
url = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = if deployCfg.authelia.enable then "https://${cfg.domain}" else null;
defaultText = lib.literalExpression ''if enable then "https://''${domain}" else null'';
default = "https://${cfg.domain}";
defaultText = lib.literalExpression ''"https://''${domain}"'';
example = "https://auth.example.com";
description = ''
Base URL clients are sent to for authentication the half of
this module that exists on **every** hive, not just the one
running the container.
Defaults to this host's own instance **only when this module is
the thing running it**; in that case the URL is not a guess, it
is where this module just put the container. Otherwise `null`,
and a hive that federates with a swarm sets it explicitly to
wherever the swarm's authelia lives. Null means "no SSO
configured" and consumers say so rather than inventing an
address an endpoint baked in as a fallback is one that
resolves cleanly and points at the wrong machine.
Names {option}`domain`, and does **not** ask whether this host
runs the container: a swarm has one SSO provider, so every hive
addresses the same name and resolution decides where it is
dnsmasq locally on the host serving the vhost, the real network
anywhere else. There is no loopback-vs-remote branch to get
wrong, the same way {option}`services.hyperhive.swarm.otel.domain`
has none.
Still nullable, for a deployment that has to say "this swarm has
no IdP" explicitly; consumers refuse rather than invent an
address when it is null.
'';
};

View file

@ -73,12 +73,17 @@ let
# The mirror image: the identity is placed, and the swarm names no IdP. The
# other half of "SSO must always be configured", and isolated the same way —
# exactly one thing wrong, so the arm reads one refusal.
#
# The null is now written out: `swarm.authelia.url` defaults to the swarm's
# IdP name on every hive, so "this swarm has no IdP" is a thing an operator
# states rather than a thing not running the container produces.
grafanaNoSso = hive {
deploy.grafana.enable = true;
deploy.grafana.plugins = [ ];
deploy.grafana.package = pkgs.emptyDirectory;
deploy.bao.clientCertFile = "/etc/pki/bao-client.pem";
deploy.bao.clientKeyFile = "/etc/pki/bao-client-key.pem";
swarm.authelia.url = null;
};
# Did ./host-modules/swarm-grafana.nix refuse this host, and for which of its

View file

@ -59,6 +59,15 @@ let
swarm.nats.calloutUserSeedFile = "/run/secrets/nats-user.seed";
swarm.nats.calloutIssuerSeedFile = "/run/secrets/nats-issuer.seed";
};
# A hive running NOTHING of the swarm's own services — no IdP here, no
# `swarm.authelia.url` set by hand. The whole point of the fixture is what it
# does *not* say: it is the shape whose IdP address used to be null, and
# before that a co-location-derived guess.
autheliaNotColocated = hive { };
# The same swarm's IdP host, for the arm that the two agree.
autheliaColocated = hive { deploy.authelia.enable = true; };
cases = [
{
# Reads the RENDERED settings, not the option: `calloutBlocks {…} // {
@ -116,6 +125,34 @@ let
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;
}
{
# Config NAMES the IdP; it never computes where the IdP is. Both arms
# matter together: the address is the swarm's name on a hive that runs
# nothing, and it is the SAME string on the hive that serves the vhost —
# so a re-introduced co-location branch shows up as the two disagreeing
# rather than as a value that merely looks plausible on one of them.
name = "the swarm IdP address is its domain on every hive, co-located or not";
ok =
autheliaNotColocated.services.hyperhive.swarm.authelia.url
== "https://${autheliaNotColocated.services.hyperhive.swarm.authelia.domain}"
&&
autheliaColocated.services.hyperhive.swarm.authelia.url
== autheliaNotColocated.services.hyperhive.swarm.authelia.url;
}
{
# …and the name it resolves through is a DOMAIN, not a host this config
# picked. `127.0.0.1` is what the old default rendered on the IdP host,
# the bridge address is what the queue's defaults still render, and
# neither is an address a client may be handed: the domain is allowed to
# resolve differently in different places, which is the whole property.
name = "the swarm IdP address names no host address";
ok =
let
url = autheliaNotColocated.services.hyperhive.swarm.authelia.url;
bridgeIp = autheliaNotColocated.services.hyperhive.network.bridgeIp;
in
!(lib.hasInfix "127.0.0.1" url) && !(lib.hasInfix "localhost" url) && !(lib.hasInfix bridgeIp url);
}
];
in
runGroup "nats-authelia" cases