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:
parent
dbda6fbde1
commit
cdaf5824ed
3 changed files with 94 additions and 0 deletions
|
|
@ -72,6 +72,14 @@ let
|
||||||
# would be an eval error rather than a silently missing guard.
|
# would be an eval error rather than a silently missing guard.
|
||||||
reservedOwners = import ../reserved-names.nix;
|
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
|
# 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
|
# 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
|
# 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}.
|
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
|
# A published target that is not an `https://host/path` url. Without
|
||||||
# this the split returns null and the failure surfaces as
|
# this the split returns null and the failure surfaces as
|
||||||
|
|
|
||||||
45
nix/reserved-hive-fragments.nix
Normal file
45
nix/reserved-hive-fragments.nix
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
# Words no HIVE name may CONTAIN — a substring blacklist, and the second half
|
||||||
|
# of `./reserved-names.nix`.
|
||||||
|
#
|
||||||
|
# Two files rather than one list, because the two differ in every dimension
|
||||||
|
# that matters and folding them would silently widen one of them:
|
||||||
|
#
|
||||||
|
# | | `reserved-names.nix` | this file |
|
||||||
|
# |---|---|---|
|
||||||
|
# | subject | agent AND hive names | hive names only |
|
||||||
|
# | match | equality | substring |
|
||||||
|
# | why | the name IS a value some component emits | the word is a fixed SEGMENT of a derived identifier |
|
||||||
|
#
|
||||||
|
# ⚠️ That last row is the whole argument. `hive-<name>` and `hive-<name>-agent`
|
||||||
|
# are built by composing a hive name with these words, so a hive called
|
||||||
|
# `foo-agent` produces an id another hive's AGENTS also produce — the composed
|
||||||
|
# strings collide even though the names do not. Equality cannot see that; only
|
||||||
|
# containment can.
|
||||||
|
#
|
||||||
|
# ⇒ And it is why the sender words (`operator`, `system`, `forge`, …) are NOT
|
||||||
|
# here: those are compared against a whole name, never embedded in one, so
|
||||||
|
# applying containment to them would refuse `forgeworks` for no failure that
|
||||||
|
# exists. `reserved-names.nix`'s own rule — an entry must name a real failure,
|
||||||
|
# not a word that looked risky — governs this file too.
|
||||||
|
#
|
||||||
|
# Read by `host-modules/swarm-otel.nix`, which owns the hive-roster assertion.
|
||||||
|
# Deliberately NOT exported to the Rust side: `HIVE_RESERVED_NAMES` answers
|
||||||
|
# "may an AGENT be called this", which stays an equality question.
|
||||||
|
[
|
||||||
|
# `hive-<name>` — every hive's own OIDC client id (`swarm-authelia.nix`'s
|
||||||
|
# `hiveClientPrefix`), and the string the queue's auth-callout responder
|
||||||
|
# strips to decide which hive a connection is.
|
||||||
|
"hive"
|
||||||
|
# `hive-<name>-agent` — the client its agent containers present
|
||||||
|
# (`agentClientSuffix`). A hive named `foo-agent` mints exactly the id hive
|
||||||
|
# `foo`'s agents do, and the responder resolves it as the agents: that hive
|
||||||
|
# silently receives an agent grant instead of its own, and a NATS denial
|
||||||
|
# arrives as a timeout, so nothing names the cause.
|
||||||
|
"agent"
|
||||||
|
# Already in `reserved-names.nix` as an exact match, listed again here for
|
||||||
|
# the wider rule: the swarm collector names components `<kind>/<owner>` with
|
||||||
|
# the hive name as owner, and a name merely CONTAINING it is not a collision
|
||||||
|
# today — but `swarm` is the one word this swarm composes identifiers from
|
||||||
|
# everywhere, so the operator's rule covers it and the two files agree.
|
||||||
|
"swarm"
|
||||||
|
]
|
||||||
|
|
@ -25,6 +25,9 @@
|
||||||
# `from`/`to`, or a component name the collector builds pipelines from — not a
|
# `from`/`to`, or a component name the collector builds pipelines from — not a
|
||||||
# word that merely looked risky. A name in here that nothing emits is a refusal
|
# word that merely looked risky. A name in here that nothing emits is a refusal
|
||||||
# with no failure behind it.
|
# with no failure behind it.
|
||||||
|
#
|
||||||
|
# ⚠️ Matched by EQUALITY. Words forbidden *inside* a hive name live in
|
||||||
|
# `./reserved-hive-fragments.nix` — read its header before merging the two.
|
||||||
[
|
[
|
||||||
# ---- message-layer senders -------------------------------------------
|
# ---- message-layer senders -------------------------------------------
|
||||||
# The human at the dashboard: a broker recipient (the T4LK box sends
|
# The human at the dashboard: a broker recipient (the T4LK box sends
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue