Every hive's homeserver loaded a per-hive `hyperhive` appservice
registration, and the swarm minted and delivered an as_token for it
through bao. No client presented that token: swarm-controller creates
every `@hive-<hive>` sender and every agent account with the swarm
appservice token (hive_sender.rs, agent_token.rs), and no Rust code read
the per-hive one.
Removed, with everything that existed only to serve it:
- hive-matrix.nix: the `hyperhive` registration, its activation-time
render, the host bind mount of /var/lib/hyperhive/matrix-appservice,
its LoadCredential entry, and the internal options
`deploy.matrix.appserviceTokenFile` / `appserviceRegistrationScript`.
tuwunel now loads only the swarm registration.
- glue-matrix-bao-token.nix and its `swarm-bao-matrix-token` unit.
- glue-bao-tls.nix: the matrix-token leaf and its defaults.
- swarm-bao.nix: the `swarm-matrix-token-<hive>` roles and policies
(`swarm-bao-matrix-token-policy`), and the options
`deploy.bao.matrixTokenClient{Cert,Key}File` and
`matrixTokenCommonNamePrefix`, now mkRemovedOptionModule.
- swarm-secret-publisher.nix: the per-hive token mint and its state dir.
- swarm.nix: the reserved `<matrixTokenCommonNamePrefix>-<hive>` CN.
- swarm-secret-client: `matrix::appservice_token_path`.
The swarm registration (swarm-matrix-ctl, swarm-controller) is untouched.
Comments and docs that pointed at the removed glue file now point at
glue-queue-agent-credential.nix, which has the same reader shape.
Refs #4428
249 lines
10 KiB
Nix
249 lines
10 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 — which used to skip
|
|
# them entirely, because they were 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 is one path rather than 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 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";
|
|
};
|
|
|
|
# Markers from `lib/name-guards.nix`'s two `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;
|
|
cases = [
|
|
{
|
|
# `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 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 came along in the move and has to still work.
|
|
# `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
|