Each container now runs the journal forwarder the CI runner's, Grafana's and the log store's containers run, pushing to a receiver of its own on the swarm collector (serviceProducers hive-forge and hive-matrix). Each authenticates as a new machine OIDC client of its own (swarm.forge.otel.clientId, swarm.matrix.otel.clientId), registered by a glue file wherever authelia runs, minted by authelia and published to the store by the secret publisher. The forge's and the homeserver's browser login clients are unchanged. The client ids are swarm-forge-collector and swarm-matrix-collector, not hive-<x>-collector, which is the client id a hive named <x>-collector gets. The fetch unit inside each container reads the client secret with the store identity the host already holds for that service. The forge publisher's leaf is handed into the forge container as an nspawn system credential, as the grafana and log store containers do. matrix-ctl's leaf is already bound into the homeserver's container, so its forwarder reads it at those paths, as swarm-matrix-appservice-publish does; with no matrix-ctl identity the homeserver's forwarder is off. Each of the two policies gains one read stanza on its forwarder's client path. The host units that use those identities are unchanged. The two containers drop --link-journal=host, and the host collector stops reading them.
329 lines
13 KiB
Nix
329 lines
13 KiB
Nix
# `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
|
|
;
|
|
|
|
# The hive-name guards, 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`.
|
|
#
|
|
# ⚠️ `controllerCommonName` is overridden to a name containing NO reserved
|
|
# fragment. Its default (`swarm-controller`) contains `swarm` and is caught
|
|
# by the substring guard whatever the cert-auth arm does — so a fixture using
|
|
# the default could not tell the two apart, and the arm under test would pass
|
|
# on the neighbour's work.
|
|
hiveNamedAfterCertSubject = hive {
|
|
deploy.swarm-otel.enable = false;
|
|
deploy.bao.controllerCommonName = "ctl";
|
|
swarm.hives.ctl.domain = "ctl.t.local";
|
|
};
|
|
|
|
# The control for both arms below: same shape, a roster nothing objects to.
|
|
hiveNamesAllLegal = hive {
|
|
deploy.swarm-otel.enable = false;
|
|
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";
|
|
};
|
|
|
|
# The THIRD element of the same list, colliding on its own so neither of the
|
|
# two above can carry it. matrix-ctl's grant names its leaves, not a whole
|
|
# prefix, which is exactly why a dead entry here would be easy to miss: a
|
|
# hive that inherited it would not obviously break anything, it would
|
|
# silently gain the ability to overwrite the swarm's matrix credential.
|
|
hiveNamedAfterMatrixCtlSubject = hive {
|
|
deploy.swarm-otel.enable = false;
|
|
deploy.bao.matrixCtlCommonName = "mintctl";
|
|
swarm.hives.mintctl.domain = "m.t.local";
|
|
};
|
|
|
|
# The two OIDC-secret readers' subjects, fixed strings like the three above.
|
|
hiveNamedAfterGrafanaOidcSubject = hive {
|
|
deploy.swarm-otel.enable = false;
|
|
deploy.bao.grafanaOidcCommonName = "gfctl";
|
|
swarm.hives.gfctl.domain = "g.t.local";
|
|
};
|
|
|
|
hiveNamedAfterOtelOidcSubject = hive {
|
|
deploy.swarm-otel.enable = false;
|
|
deploy.bao.otelOidcCommonName = "otctl";
|
|
swarm.hives.otctl.domain = "o.t.local";
|
|
};
|
|
|
|
# The store forwarder's OIDC reader, the fifth fixed subject.
|
|
hiveNamedAfterForwarderOidcSubject = hive {
|
|
deploy.swarm-otel.enable = false;
|
|
deploy.bao.forwarderOidcCommonName = "fwctl";
|
|
swarm.hives.fwctl.domain = "f.t.local";
|
|
};
|
|
|
|
# The forge publisher's, the one writer of the controller's forge token.
|
|
hiveNamedAfterForgePublisherSubject = hive {
|
|
deploy.swarm-otel.enable = false;
|
|
deploy.bao.forgePublisherCommonName = "fpctl";
|
|
swarm.hives.fpctl.domain = "fp.t.local";
|
|
};
|
|
|
|
# The granter's, the one subject whose role may write every `swarm-*` grant.
|
|
hiveNamedAfterGranterSubject = hive {
|
|
deploy.swarm-otel.enable = false;
|
|
deploy.bao.granterCommonName = "grctl";
|
|
swarm.hives.grctl.domain = "gr.t.local";
|
|
};
|
|
|
|
# 🩸 A different shape from every fixture above: the queue-credential roles
|
|
# are written PER HIVE, so the subject a hive must not
|
|
# be is `<prefix>-<some hive's name>` 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 — which is a hive reading
|
|
# another hive's queue credential, the exact widening the split exists to
|
|
# avoid.
|
|
#
|
|
# Two hives here, not one: the collision is with the OTHER hive's role.
|
|
hiveNamedAfterPerHiveReaderSubject = hive {
|
|
deploy.swarm-otel.enable = false;
|
|
deploy.bao.queueAgentCommonNamePrefix = "qr";
|
|
swarm.hives.other.domain = "o.t.local";
|
|
swarm.hives.qr-other.domain = "q.t.local";
|
|
};
|
|
|
|
# The per-hive gateway-leaf subject, composed the same way.
|
|
hiveNamedAfterHiveLeafSubject = hive {
|
|
deploy.swarm-otel.enable = false;
|
|
deploy.bao.hiveLeafCommonNamePrefix = "hl";
|
|
swarm.hives.other.domain = "o.t.local";
|
|
swarm.hives.hl-other.domain = "hl.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.
|
|
hiveNameWithSlash = hive {
|
|
deploy.swarm-otel.enable = false;
|
|
swarm.hives."a/b".domain = "a.t.local";
|
|
};
|
|
|
|
hiveNameWithParentDir = hive {
|
|
deploy.swarm-otel.enable = false;
|
|
swarm.hives."../x".domain = "x.t.local";
|
|
};
|
|
|
|
# Both sides of the length cap: 63 characters passes, 64 does not.
|
|
hiveNameAtLengthCap = hive {
|
|
deploy.swarm-otel.enable = false;
|
|
swarm.hives.${lib.concatStrings (lib.replicate 63 "a")}.domain = "l.t.local";
|
|
};
|
|
|
|
hiveNameOverLengthCap = hive {
|
|
deploy.swarm-otel.enable = false;
|
|
swarm.hives.${lib.concatStrings (lib.replicate 64 "a")}.domain = "l.t.local";
|
|
};
|
|
|
|
# Digits and hyphens in every position: the rule has no leading-character
|
|
# restriction.
|
|
hiveNameWithDigitsAndHyphens = hive {
|
|
deploy.swarm-otel.enable = false;
|
|
swarm.hives."0-h-9".domain = "h.t.local";
|
|
};
|
|
|
|
# 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;
|
|
|
|
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 hiveNameWithSlash "a/b";
|
|
}
|
|
{
|
|
name = "a hive key that climbs out of its directory is refused, naming the key";
|
|
ok = identGuardRefuses hiveNameWithParentDir "../x";
|
|
}
|
|
{
|
|
name = "a hive key over 63 characters is refused";
|
|
ok = identGuardFired hiveNameOverLengthCap;
|
|
}
|
|
{
|
|
# 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 hiveNameAtLengthCap)
|
|
&& !(identGuardFired hiveNameWithDigitsAndHyphens)
|
|
&& !(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 =
|
|
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;
|
|
}
|
|
{
|
|
# 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 =
|
|
equalityGuardFired hiveNamedAfterMatrixCtlSubject
|
|
&& lib.any (
|
|
a: !a.assertion && lib.hasInfix "'mintctl'" a.message
|
|
) hiveNamedAfterMatrixCtlSubject.assertions;
|
|
}
|
|
{
|
|
# 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 =
|
|
equalityGuardFired hiveNamedAfterGrafanaOidcSubject
|
|
&& lib.any (
|
|
a: !a.assertion && lib.hasInfix "'gfctl'" a.message
|
|
) hiveNamedAfterGrafanaOidcSubject.assertions
|
|
&& equalityGuardFired hiveNamedAfterOtelOidcSubject
|
|
&& lib.any (
|
|
a: !a.assertion && lib.hasInfix "'otctl'" a.message
|
|
) hiveNamedAfterOtelOidcSubject.assertions;
|
|
}
|
|
{
|
|
# 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 =
|
|
equalityGuardFired hiveNamedAfterForwarderOidcSubject
|
|
&& lib.any (
|
|
a: !a.assertion && lib.hasInfix "'fwctl'" a.message
|
|
) hiveNamedAfterForwarderOidcSubject.assertions;
|
|
}
|
|
{
|
|
# 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 =
|
|
equalityGuardFired hiveNamedAfterForgePublisherSubject
|
|
&& lib.any (
|
|
a: !a.assertion && lib.hasInfix "'fpctl'" a.message
|
|
) hiveNamedAfterForgePublisherSubject.assertions;
|
|
}
|
|
{
|
|
# 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 =
|
|
equalityGuardFired hiveNamedAfterGranterSubject
|
|
&& lib.any (
|
|
a: !a.assertion && lib.hasInfix "'grctl'" a.message
|
|
) hiveNamedAfterGranterSubject.assertions;
|
|
}
|
|
{
|
|
# 🩸 The per-hive half, and the one a prefix-only reservation would miss:
|
|
# the role is `<prefix>-<hive>`, 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 =
|
|
equalityGuardFired hiveNamedAfterPerHiveReaderSubject
|
|
&& lib.any (
|
|
a: !a.assertion && lib.hasInfix "'qr-other'" a.message
|
|
) hiveNamedAfterPerHiveReaderSubject.assertions;
|
|
}
|
|
{
|
|
# 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 =
|
|
equalityGuardFired hiveNamedAfterHiveLeafSubject
|
|
&& lib.any (
|
|
a: !a.assertion && lib.hasInfix "'hl-other'" a.message
|
|
) hiveNamedAfterHiveLeafSubject.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.
|
|
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 =
|
|
!hiveNamedAfterCertSubject.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
|