From 4b6214305f4fba4cdd658f1420561475a7786f53 Mon Sep 17 00:00:00 2001 From: atlas Date: Sat, 19 Sep 2026 20:39:19 +0200 Subject: [PATCH] nix: address the swarm IdP by its domain, not by who runs it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `swarm.authelia.url` defaulted to `https://` 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 --- docs/swarm/services.md | 10 ++++---- nix/host-modules/swarm-authelia.nix | 23 ++++++++++-------- nix/module-eval/grafana.nix | 5 ++++ nix/module-eval/nats-authelia.nix | 37 +++++++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 14 deletions(-) diff --git a/docs/swarm/services.md b/docs/swarm/services.md index b3e88c19..5e39a192 100644 --- a/docs/swarm/services.md +++ b/docs/swarm/services.md @@ -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://`, 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. diff --git a/nix/host-modules/swarm-authelia.nix b/nix/host-modules/swarm-authelia.nix index c1c0d035..f95659c0 100644 --- a/nix/host-modules/swarm-authelia.nix +++ b/nix/host-modules/swarm-authelia.nix @@ -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. ''; }; diff --git a/nix/module-eval/grafana.nix b/nix/module-eval/grafana.nix index c1008194..524d0622 100644 --- a/nix/module-eval/grafana.nix +++ b/nix/module-eval/grafana.nix @@ -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 diff --git a/nix/module-eval/nats-authelia.nix b/nix/module-eval/nats-authelia.nix index 4f343c04..cd72aa1b 100644 --- a/nix/module-eval/nats-authelia.nix +++ b/nix/module-eval/nats-authelia.nix @@ -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