diff --git a/nix/host-modules/swarm-authelia.nix b/nix/host-modules/swarm-authelia.nix index 1ff0ea7d..7cb4b7c8 100644 --- a/nix/host-modules/swarm-authelia.nix +++ b/nix/host-modules/swarm-authelia.nix @@ -91,7 +91,7 @@ let # declared entries and not on these, which is an eval error reachable # only once hive identities are on. hiveClients = lib.mapAttrsToList (name: _: { - id = "hive-${name}"; + id = "${cfg.hiveClientPrefix}${name}"; description = "HyperHive hive ${name}"; kind = "machine"; redirectUris = [ ]; @@ -508,6 +508,26 @@ in # the call site: the machine and unit names are derived from # `instance` here, so a second copy elsewhere is a second thing to # keep in step, and the one that drifts is the one nobody tests. + hiveClientPrefix = lib.mkOption { + type = lib.types.str; + readOnly = true; + default = "hive-"; + description = '' + Prefix of the OAuth2 client id minted for each hive in + `services.hyperhive.swarm.hives` — the client for hive `alpha` is + `${config.services.hyperhive.swarm.authelia.hiveClientPrefix}alpha`. + Read-only for the same reason as `machine` and `unit`: it is what + this module produces, published so a consumer does not carry a + second copy. + + The consumer that matters is the queue's auth-callout responder, + which decides *which hive* a connection is by stripping this + prefix off the introspected client id. Split the two spellings and + every hive is denied — as a timeout, indistinguishable from a hive + that simply has not reported. + ''; + }; + machine = lib.mkOption { type = lib.types.str; readOnly = true; diff --git a/nix/host-modules/swarm-controller.nix b/nix/host-modules/swarm-controller.nix index 9c8b1fca..3e28e43a 100644 --- a/nix/host-modules/swarm-controller.nix +++ b/nix/host-modules/swarm-controller.nix @@ -41,7 +41,7 @@ let # belongs to the responder. One identity per principal — the rule is that # a principal's credentials all derive from the same identity, not that # the swarm has one. - queueClientId = "swarm-controller"; + queueClientId = cfg.queueClientId; # `LoadCredential` and not a copy-oneshot, which is where this deliberately # differs from the callout responder: that one delivers INTO a container, @@ -114,6 +114,25 @@ let in { options.services.hyperhive.swarm.controller = { + queueClientId = lib.mkOption { + type = lib.types.str; + readOnly = true; + default = "swarm-controller"; + description = '' + The OAuth2 client id the controller presents to the swarm queue. + Read-only: it is what this module registers, published so the + auth-callout responder can be told which client may read every + hive's key without repeating the string. + + The responder decides that from a client id, and a client id it + does not recognise is **denied**. A denial reaches a NATS client + as a timeout rather than an error, and a controller that cannot + read looks exactly like a swarm where no hive has reported yet — + so a drift between these two spellings is invisible at the point + it is introduced and misattributed everywhere it shows up. + ''; + }; + enable = lib.mkOption { type = lib.types.bool; default = false; diff --git a/nix/host-modules/swarm-nats.nix b/nix/host-modules/swarm-nats.nix index 91aa40ee..bfb83083 100644 --- a/nix/host-modules/swarm-nats.nix +++ b/nix/host-modules/swarm-nats.nix @@ -9,6 +9,10 @@ let autheliaCfg = config.services.hyperhive.swarm.authelia; autheliaUrl = autheliaCfg.url; networkCfg = config.services.hyperhive.network; + # Read even when the controller runs on a different host: what is needed + # is the client id that module *declares*, which is the same string + # everywhere, not whether the daemon happens to be enabled here. + controllerCfg = config.services.hyperhive.swarm.controller; # The account the callout responder authenticates as, and the account # authorized clients are placed in. Two accounts rather than one: an @@ -531,6 +535,15 @@ in # be the same string — which is why both come from one let. "--account ${lib.escapeShellArg clientAccount}" "--introspection-url ${lib.escapeShellArg introspectionUrl}" + # Both of these name a principal some OTHER module mints, + # so both are read out of that module rather than spelled + # again here — same argument as `--account` above, one + # level wider. The responder denies a client id it does + # not recognise, and a NATS denial arrives as a timeout, + # so a drift here is silent at the point of change and + # misattributed at the point of failure. + "--hive-client-prefix ${lib.escapeShellArg autheliaCfg.hiveClientPrefix}" + "--reader-client ${lib.escapeShellArg controllerCfg.queueClientId}" ]; # Every credential arrives by `LoadCredential` and is named # on the command line only as a **path** — `argv` is