swarm: refuse hive names that CONTAIN a word identifiers are composed from

Hive-scoped identifiers are built by joining a hive name with fixed words —
`hive-<name>`, `hive-<name>-agent` — so a hive called `foo-agent` produces
exactly the id hive `foo`'s agent containers produce. The queue's responder
resolves it as the agents, and that hive silently receives an agent grant
instead of its own; a NATS denial reaches a client as a timeout, so nothing
names the cause. Equality cannot see this: the two NAMES differ, only the
composed strings collide.

Deliberately a second file rather than three more entries in
`reserved-names.nix`. That list is matched by equality against a whole name and
every entry is a value some component emits; applying containment to it would
refuse `forgeworks` and `operator-hq` for failures that do not exist. The two
lists differ in subject (hive-only vs both), matcher (substring vs equality)
and admission rule, and each file's header says so.

No Rust change: `HIVE_RESERVED_NAMES` answers "may an AGENT be called this",
which stays an equality question — an agent name is never composed into these
identifiers.

Measured against the predicate rather than assumed:

  pr1ma=ok alpha=ok forgeworks=ok operator-hq=ok
  foo-agent=REFUSED beehive=REFUSED swarm=REFUSED myswarmx=REFUSED

The four `ok` cases are the control: `forgeworks` and `operator-hq` are exactly
what a merged list would have refused.
This commit is contained in:
atlas 2026-08-31 18:24:19 +02:00 committed by mara
commit cdaf5824ed
3 changed files with 94 additions and 0 deletions

View file

@ -72,6 +72,14 @@ let
# would be an eval error rather than a silently missing guard.
reservedOwners = import ../reserved-names.nix;
# The second, narrower blacklist: words no hive name may CONTAIN, because
# they are fixed segments of identifiers built FROM a hive name
# (`hive-<name>`, `hive-<name>-agent`). Substring rather than equality is
# the whole point — a hive called `foo-agent` collides in the composed
# string while colliding with nothing in the list above. That file explains
# why it is not merged into this one.
reservedFragments = import ../reserved-hive-fragments.nix;
# A published target is declared as ONE url, because that url is also the
# audience its token is minted for — but prometheus wants the same fact in
# three fields. Split it here rather than asking a service to state it
@ -685,6 +693,44 @@ in
Rename the hive. Reserved: ${lib.concatMapStringsSep ", " (n: "'${n}'") reservedOwners}.
'';
}
{
# The substring half, and a genuinely different failure from the one
# above: these words are fixed SEGMENTS of identifiers composed from a
# hive name, so the collision is between two composed strings while
# the names themselves differ. `hive-foo-agent` is hive `foo-agent`'s
# own client id and hive `foo`'s AGENT client id at the same time; the
# queue's responder resolves it as the agents, so `foo-agent` silently
# receives an agent grant instead of its own — and a NATS denial
# reaches a client as a timeout, so nothing names the cause.
#
# Lives here rather than in `swarm-authelia.nix` (which composes those
# ids) because this module already owns the hive-roster check and a
# second site for "is this hive name legal" is how the two drift.
assertion =
!lib.any (h: lib.any (frag: lib.hasInfix frag h) reservedFragments) (
lib.attrNames hyperhiveCfg.swarm.hives
);
message = ''
services.hyperhive.swarm.hives contains ${
lib.concatMapStringsSep ", " (h: "'${h}'") (
lib.filter (h: lib.any (frag: lib.hasInfix frag h) reservedFragments) (
lib.attrNames hyperhiveCfg.swarm.hives
)
)
}, which CONTAIN a word the swarm composes identifiers from.
Hive-scoped identifiers are built by joining a hive name with these
words `hive-<name>` for a hive, `hive-<name>-agent` for its agent
containers so a name containing one produces an identifier that is
also somebody else's. The failure is a wrong grant, not an error:
the client authenticates and receives permissions meant for another
principal.
Rename the hive. Forbidden as substrings: ${
lib.concatMapStringsSep ", " (n: "'${n}'") reservedFragments
}.
'';
}
{
# A published target that is not an `https://host/path` url. Without
# this the split returns null and the failure surfaces as