Watch
0
0
Fork
You've already forked hyperhive
0

swarm: refuse swarm.hives keys that are not valid hive identifiers

A hive key becomes a certificate subject and a directory name, but only the
reserved-name and fragment guards looked at it, so `a/b` or `../x` evaluated
and misbehaved later. Assert at eval time that every key is 1-63 characters
of [a-z0-9-], the rule `Ident` applies in hive-types, and name the offending
keys in the message.
This commit is contained in:
atlas 2026-10-12 20:07:55 +02:00 • committed by mara
commit 7ee6e776a3
3 changed files with 91 additions and 1 deletions

View file

@ -63,4 +63,27 @@ in
problem = "has name(s) containing a reserved word"; problem = "has name(s) containing a reserved word";
forbiddenLabel = "Forbidden as substrings"; forbiddenLabel = "Forbidden as substrings";
}; };
# Every name must be a valid hive `Ident` (hive-types/src/lib.rs): 1-63
# characters of `[a-z0-9-]`, with no leading-character rule. A hive name is
# spliced into certificate subjects and filesystem paths, where `/` and `..`
# change meaning.
mustBeIdent =
{ option, names }:
let
offenders = lib.filter (n: builtins.match "[a-z0-9-]+" n == null || lib.stringLength n > 63) names;
in
{
assertion = offenders == [ ];
message = ''
${option} has name(s) that are not valid identifiers: ${
lib.concatMapStringsSep ", " (x: "'${x}'") offenders
}
Required: 1-63 characters, each one of [a-z0-9-]
A hive name becomes a certificate subject and a directory name, so a
`/` or `..` in it names something else. `Ident` in
hive-types/src/lib.rs applies the same rule. Rename the hive.
'';
};
} }

View file

@ -369,6 +369,10 @@ in
designed for it. designed for it.
''; '';
} }
(nameGuards.mustBeIdent {
option = "services.hyperhive.swarm.hives";
names = hiveNames;
})
(nameGuards.mustNotEqual { (nameGuards.mustNotEqual {
option = "services.hyperhive.swarm.hives"; option = "services.hyperhive.swarm.hives";
names = hiveNames; names = hiveNames;

View file

@ -126,7 +126,38 @@ let
swarm.hives."h1-agent".domain = "a.t.local"; swarm.hives."h1-agent".domain = "a.t.local";
}; };
# Markers from `lib/name-guards.nix`'s two `problem` strings. Matching the # 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. # problem rather than the `why` prose keeps the messages rewordable.
equalityGuardFired = equalityGuardFired =
h: lib.any (a: !a.assertion && lib.hasInfix "has reserved name(s)" a.message) h.assertions; h: lib.any (a: !a.assertion && lib.hasInfix "has reserved name(s)" a.message) h.assertions;
@ -136,7 +167,39 @@ let
lib.any ( lib.any (
a: !a.assertion && lib.hasInfix "has name(s) containing a reserved word" a.message a: !a.assertion && lib.hasInfix "has name(s) containing a reserved word" a.message
) h.assertions; ) 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 = [ 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 # `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 # cert-auth role accepts*, which is a value an operator sets, so a