# `checks.module-eval-name-guards` — see ./lib.nix for the shared # rationale (why this suite exists, naming convention, "evaluates # not executes"). { pkgs, lib, self, nixosSystem, }: let inherit (import ./lib.nix { inherit pkgs lib self nixosSystem ; }) hive runGroup ; # One host per guard. Each guard reports every offender in one message # (`../host-modules/lib/name-guards.nix`), so one host carrying every # colliding name still tells each name's arm apart: a name its arm misses # is absent from the offenders line. # # Every fixture runs with the collector explicitly OFF. That is the whole # property: the guards live where `swarm.hives` is declared, so they run in a # deployment that has a secret store and no collector, rather than being # assertions inside swarm-otel's own `mkIf`. # Every cert-auth subject an operator can override, set to a name with no # reserved fragment and taken by a hive. `controllerCommonName`'s default # (`swarm-controller`) contains `swarm` and is caught by the substring guard # whatever the cert-auth arm does, so the default could not tell the two # apart. # # The fixed subjects are a LIST, and a list with one consulted element and # one dead one looks identical from the first element's case: each element # needs its own name in the offenders line. matrix-ctl's grant names its # leaves, not a whole prefix, so a hive that inherited a dead entry there # would silently gain the ability to overwrite the swarm's matrix credential. # # 🩸 The two per-hive subjects have a different shape: those roles are # written PER HIVE, so the subject a hive must not be is # `-` rather than the prefix itself. Reserving only # the prefix would leave the composed spelling free, and a hive taking it # would present a leaf the other hive's role accepts — a hive reading another # hive's queue credential, the exact widening the split exists to avoid. The # collision is with the OTHER hive's role, hence `other` beside `qr-other` # and `hl-other`. hiveNamedAfterEverySubject = hive { deploy.swarm-otel.enable = false; deploy.bao = { controllerCommonName = "ctl"; secretPublisherCommonName = "pubctl"; matrixCtlCommonName = "mintctl"; grafanaOidcCommonName = "gfctl"; otelOidcCommonName = "otctl"; forwarderOidcCommonName = "fwctl"; forgePublisherCommonName = "fpctl"; granterCommonName = "grctl"; queueAgentCommonNamePrefix = "qr"; hiveLeafCommonNamePrefix = "hl"; }; swarm.hives = { ctl.domain = "ctl.t.local"; pubctl.domain = "p.t.local"; mintctl.domain = "m.t.local"; gfctl.domain = "g.t.local"; otctl.domain = "o.t.local"; fwctl.domain = "f.t.local"; fpctl.domain = "fp.t.local"; grctl.domain = "gr.t.local"; other.domain = "other.t.local"; qr-other.domain = "q.t.local"; hl-other.domain = "hl.t.local"; }; }; # The control for every refusal below: same shape, a roster nothing objects # to. It carries both sides of the identifier rule's edges — 63 characters, # and digits and hyphens in every position (the rule has no # leading-character restriction). hiveNamesAllLegal = hive { deploy.swarm-otel.enable = false; deploy.bao.controllerCommonName = "ctl"; swarm.hives.${lib.concatStrings (lib.replicate 63 "a")}.domain = "l.t.local"; swarm.hives."0-h-9".domain = "h.t.local"; }; hiveNameWithComposedWord = hive { deploy.swarm-otel.enable = false; swarm.hives."h1-agent".domain = "a.t.local"; }; # The key characters are checked by the same module, so these run with the # collector off too. `a/b` adds a certificate-subject component; `../x` # leaves a directory named after the hive; 64 characters is one over the cap. hiveNamesNotIdents = hive { deploy.swarm-otel.enable = false; swarm.hives = { "a/b".domain = "a.t.local"; "../x".domain = "x.t.local"; ${lib.concatStrings (lib.replicate 64 "a")}.domain = "l.t.local"; }; }; # The first line of a guard's message: the problem and its offenders. The # lines after it list the forbidden values, which name a subject whether or # not any hive took it. offendersLine = a: lib.head (lib.splitString "\n" a.message); # Markers from `lib/name-guards.nix`'s `problem` strings. Matching the # problem rather than the `why` prose keeps the messages rewordable. equalityGuardFired = h: lib.any (a: !a.assertion && lib.hasInfix "has reserved name(s)" a.message) h.assertions; # True when the equality guard names `name` as an offender. equalityGuardRefuses = h: name: lib.any ( a: !a.assertion && lib.hasInfix "has reserved name(s)" (offendersLine a) && lib.hasInfix "'${name}'" (offendersLine a) ) h.assertions; fragmentGuardFired = h: lib.any ( a: !a.assertion && lib.hasInfix "has name(s) containing a reserved word" a.message ) h.assertions; # True when the identifier guard refuses `key`. identGuardRefuses = h: key: lib.any ( a: !a.assertion && lib.hasInfix "not valid identifiers" a.message && lib.hasInfix "'${key}'" a.message ) h.assertions; identGuardFired = h: lib.any (a: !a.assertion && lib.hasInfix "not valid identifiers" a.message) h.assertions; cases = [ { name = "a hive key containing a slash is refused, naming the key"; ok = identGuardRefuses hiveNamesNotIdents "a/b"; } { name = "a hive key that climbs out of its directory is refused, naming the key"; ok = identGuardRefuses hiveNamesNotIdents "../x"; } { name = "a hive key over 63 characters is refused"; ok = identGuardRefuses hiveNamesNotIdents (lib.concatStrings (lib.replicate 64 "a")); } { # Controls: without them an identifier guard that refuses everything # passes the three cases above. name = "63-character and digit-and-hyphen hive keys pass the identifier guard"; ok = !(identGuardFired hiveNamesAllLegal); } { # `ctl` is in no deny list — it is reserved *because it is the subject a # cert-auth role accepts*, which is a value an operator sets, so a # literal deny entry could never have covered it. name = "a hive named after a cert-auth subject is refused, with the collector off"; ok = equalityGuardRefuses hiveNamedAfterEverySubject "ctl"; } { # 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 = equalityGuardRefuses hiveNamedAfterEverySubject "pubctl"; } { # And the third, for the reason the second one's comment gives one list # element earlier. `certAuthCns` is where a role added beside the others # has to register itself, and nothing but a case per element notices when # one forgets. name = "a hive named after matrix-ctl's subject is refused too"; ok = equalityGuardRefuses hiveNamedAfterEverySubject "mintctl"; } { # The fourth and fifth, for the reason the case above gives: `certAuthCns` # is where a role added beside the others registers itself, and nothing # but a case per element notices when one forgets. These two are the # subjects of the readers that fetch Grafana's and the collector's OIDC # client secrets. name = "a hive named after either OIDC-secret reader's subject is refused too"; ok = equalityGuardRefuses hiveNamedAfterEverySubject "gfctl" && equalityGuardRefuses hiveNamedAfterEverySubject "otctl"; } { # And the store forwarder's, for the same reason one element later. name = "a hive named after the store forwarder's OIDC-reader subject is refused too"; ok = equalityGuardRefuses hiveNamedAfterEverySubject "fwctl"; } { # And the forge publisher's: a hive holding a leaf it accepts could # replace the token the controller provisions every forge object with. name = "a hive named after the forge publisher's subject is refused too"; ok = equalityGuardRefuses hiveNamedAfterEverySubject "fpctl"; } { # And the granter's, whose role is root-equivalent: a hive holding a leaf # it accepts could grant itself anything. name = "a hive named after the bao granter's subject is refused too"; ok = equalityGuardRefuses hiveNamedAfterEverySubject "grctl"; } { # 🩸 The per-hive half, and the one a prefix-only reservation would miss: # the role is `-`, so the reserved string has to be composed # against every declared hive. Here hive `qr-other` collides with the role # written for hive `other` — a leaf that reads a credential belonging to a # hive that is not it. name = "a hive named after another hive's per-hive reader subject is refused"; ok = equalityGuardRefuses hiveNamedAfterEverySubject "qr-other"; } { # A hive taking this spelling would present a leaf hive `other`'s # gateway-leaf role accepts, and could ask the store for that hive's # certificate. The control is the legal roster below. name = "a hive named after another hive's gateway-leaf subject is refused"; ok = equalityGuardRefuses hiveNamedAfterEverySubject "hl-other"; } { # 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. name = "a legal hive roster trips neither name guard"; ok = !(equalityGuardFired hiveNamesAllLegal) && !(fragmentGuardFired hiveNamesAllLegal); } { # The substring guard has to work with the collector off too. # `h1-agent` mints exactly the client id hive `h1`'s agents present. name = "a hive name containing a composed-identifier word is refused, with the collector off"; ok = fragmentGuardFired hiveNameWithComposedWord; } { # ⚠️ The control that makes "with the collector off" mean anything. If a # fixture silently had swarm-otel enabled, all three cases above would # pass while testing the arrangement they exist to rule out. name = "the guard fixtures really do have the collector disabled"; ok = !hiveNamedAfterEverySubject.services.hyperhive.deploy.swarm-otel.enable && !hiveNamesAllLegal.services.hyperhive.deploy.swarm-otel.enable && !hiveNameWithComposedWord.services.hyperhive.deploy.swarm-otel.enable; } ]; in runGroup "name-guards" cases