From e8ff633c6b973dd847cb1dfec4099a900b819c68 Mon Sep 17 00:00:00 2001 From: atlas Date: Fri, 11 Sep 2026 22:39:06 +0200 Subject: [PATCH] swarm-bao, swarm: give the secret publisher its own subject, reserved like the controller's MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The unit that will copy authelia's minted OIDC client secrets into the store needs an identity of its own. Not the controller's: that grant includes rewriting every hive's policy and login role, which a unit whose whole job is copying one file has no business holding. The subject joins `certAuthCns`, so it is unrepresentable as a hive name for the same reason the controller's is — cert auth trusts the CA, and a hive's own leaf carries its name as the CN. The module-eval case collides with the SECOND list element and leaves the controller's subject at its default. A list with one consulted element and one dead one is indistinguishable from the first element's case, so without this the addition could be inert and nothing would say so. Refs #3853 --- nix/host-modules/swarm-bao.nix | 23 +++++++++++++++++++++++ nix/host-modules/swarm.nix | 5 ++++- nix/module-eval.nix | 20 ++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/nix/host-modules/swarm-bao.nix b/nix/host-modules/swarm-bao.nix index d18b825c..100a26c1 100644 --- a/nix/host-modules/swarm-bao.nix +++ b/nix/host-modules/swarm-bao.nix @@ -554,6 +554,29 @@ in ''; }; + secretPublisherCommonName = lib.mkOption { + type = lib.types.str; + default = "swarm-secret-publisher"; + example = "swarm-secret-publisher.svc"; + description = '' + Subject the store's secret-publisher cert-auth role accepts — the + identity that copies authelia's minted OIDC client secrets into the + store so a hive that does not host authelia can read its own. + + A **second** identity rather than reuse of + {option}`services.hyperhive.deploy.bao.controllerCommonName`: the + controller's grant includes rewriting every hive's policy and login + role, which is not something a unit whose whole job is copying one file + should be able to do. This one's grant is `create`/`update` under the + hive prefix and nothing else. + + ⚠️ Same collision as its sibling above, and the same answer: ./swarm.nix + feeds this value into the guard on + {option}`services.hyperhive.swarm.hives`, so a hive named after it fails + evaluation rather than silently receiving the publisher's grants. + ''; + }; + serverCaFile = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; diff --git a/nix/host-modules/swarm.nix b/nix/host-modules/swarm.nix index 6052cc8f..0267d1c6 100644 --- a/nix/host-modules/swarm.nix +++ b/nix/host-modules/swarm.nix @@ -43,7 +43,10 @@ let # every role added beside these widens what a hive name must not collide # with. A hive's own leaf carries its name as the CN, so a hive named after # one of these presents a certificate that role accepts. - certAuthCns = [ deployCfg.bao.controllerCommonName ]; + certAuthCns = [ + deployCfg.bao.controllerCommonName + deployCfg.bao.secretPublisherCommonName + ]; # Public hostnames of the swarm's own services, in declaration order. # `serviceDomains` below is this set sorted + deduplicated. diff --git a/nix/module-eval.nix b/nix/module-eval.nix index dea14c02..b408746a 100644 --- a/nix/module-eval.nix +++ b/nix/module-eval.nix @@ -539,6 +539,15 @@ let deploy.bao.controllerCommonName = "ctl"; }; + # The reserved subjects are a LIST, and a list with one consulted element and + # one dead one looks identical from the first element's case. This fixture + # collides with the SECOND, leaving the controller's at its default. + hiveNamedAfterPublisherSubject = hive { + deploy.swarm-otel.enable = false; + deploy.bao.secretPublisherCommonName = "pubctl"; + swarm.hives.pubctl.domain = "p.t.local"; + }; + hiveNameWithComposedWord = hive { deploy.swarm-otel.enable = false; swarm.hives."h1-agent".domain = "a.t.local"; @@ -565,6 +574,17 @@ let equalityGuardFired hiveNamedAfterCertSubject && lib.any (a: !a.assertion && lib.hasInfix "'ctl'" a.message) hiveNamedAfterCertSubject.assertions; } + { + # Every cert-auth subject is reserved, not just the first one in the + # list. Without this case the second element could be dead and the case + # above would still pass. + name = "a hive named after the secret publisher's subject is refused too"; + ok = + equalityGuardFired hiveNamedAfterPublisherSubject + && lib.any ( + a: !a.assertion && lib.hasInfix "'pubctl'" a.message + ) hiveNamedAfterPublisherSubject.assertions; + } { # Without this the case above proves nothing: an arm that fires for every # roster is not a guard, and `hives` is non-empty in both fixtures.