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