swarm: name the agent client after its hive, not after "agent"
`agent-<hive>` reads as "the agent named <hive>" — which is the one thing that identity does not carry, since it is minted per hive. It becomes `hive-<hive>-agent`: the hive's own id, extended. The rename is not a string swap. `hive-foo-agent` satisfies the hive parse too (it strips to a hive named `foo-agent`), so the responder's agent rule now runs BEFORE its hive rule — most specific wins. Hive-first would have handed every agent its hive's grant, including writing that hive's status key, with nothing to report it: the client authenticates and is merely able to do more than it should. `Policy::new`'s overlap check goes with the prefix it was written for. The invariant the suffix form needs instead is that the suffix is non-empty: an empty one makes `strip_suffix` succeed on every hive id, so the two principals become one string and whichever arm runs first answers for both. The suffix form also introduces a collision the prefix form did not have: a hive genuinely named `foo-agent` mints `hive-foo-agent`, which is hive `foo`'s agent id. The responder cannot see it — it has no roster, deliberately — so `swarm-authelia.nix` asserts at eval that no hive name ends with the suffix. The existing duplicate-id assertion does not cover this: it fires only when both `foo` and `foo-agent` are on the roster, and with `foo-agent` alone there is no duplicate, just a hive quietly receiving its agents' grant. A test written by analogy with `the_prefix_alone_names_no_hive` failed, correctly — `hive--agent` is a hive named `-agent` under the hive parse, which this module cannot rule out. It now asserts only the part this module owns: no empty hive name is ever expanded into a subject.
This commit is contained in:
parent
fe9417ae52
commit
780df10d9d
4 changed files with 181 additions and 83 deletions
|
|
@ -156,17 +156,24 @@ let
|
|||
# stop being static, which is the same problem the users-database writer
|
||||
# already solves for identities.
|
||||
#
|
||||
# 🏷️ The id EXTENDS the hive's own (`hive-<name>-agent`) rather than taking a
|
||||
# prefix of its own (`agent-<name>`), because that reads as *the agent called
|
||||
# `<name>`* — which is the one thing this identity does not carry. The cost is
|
||||
# that the two ids are no longer distinguishable by prefix, so the responder's
|
||||
# agent rule must be tried BEFORE its hive rule; `policy.rs` says so at the
|
||||
# match site, and the assertion below covers the case that ordering cannot.
|
||||
#
|
||||
# Mirrors `hiveClients` field for field so the two stay comparable. The
|
||||
# signing algorithm is not load-bearing here — this client's only consumer is
|
||||
# the queue's auth-callout responder, which *introspects* rather than
|
||||
# verifying offline — but it matches its sibling rather than inventing a
|
||||
# second answer to a question nobody asked.
|
||||
agentClients = lib.mapAttrsToList (name: _: {
|
||||
id = "${cfg.agentClientPrefix}${name}";
|
||||
id = "${cfg.hiveClientPrefix}${name}${cfg.agentClientSuffix}";
|
||||
description = "HyperHive agents on hive ${name}";
|
||||
kind = "machine";
|
||||
redirectUris = [ ];
|
||||
audience = [ "${cfg.agentClientPrefix}${name}" ];
|
||||
audience = [ "${cfg.hiveClientPrefix}${name}${cfg.agentClientSuffix}" ];
|
||||
accessTokenSignedResponseAlg = "RS256";
|
||||
}) hyperhiveCfg.swarm.hives;
|
||||
|
||||
|
|
@ -446,7 +453,7 @@ in
|
|||
Mint machine clients per hive in
|
||||
{option}`services.hyperhive.swarm.hives`, so each hive can
|
||||
authenticate to swarm services as itself: `hive-<name>` for the
|
||||
hive's own daemons, and `agent-<name>` for the agent containers
|
||||
hive's own daemons, and `hive-<name>-agent` for the agent containers
|
||||
running on it.
|
||||
|
||||
Two clients rather than one because they are not the same
|
||||
|
|
@ -706,19 +713,22 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
agentClientPrefix = lib.mkOption {
|
||||
agentClientSuffix = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
readOnly = true;
|
||||
default = "agent-";
|
||||
default = "-agent";
|
||||
description = ''
|
||||
Prefix of the OAuth2 client id minted for the *agents* of each hive
|
||||
in `services.hyperhive.swarm.hives` — agents on hive `alpha` all
|
||||
present
|
||||
`${config.services.hyperhive.swarm.authelia.agentClientPrefix}alpha`.
|
||||
Suffix appended to a hive's own client id to name the client its
|
||||
*agent containers* present — agents on hive `alpha` all present
|
||||
`${config.services.hyperhive.swarm.authelia.hiveClientPrefix}alpha${config.services.hyperhive.swarm.authelia.agentClientSuffix}`.
|
||||
Read-only for the same reason as `hiveClientPrefix`, and read by the
|
||||
same consumer with the same failure mode: a split spelling denies
|
||||
every agent as a timeout.
|
||||
|
||||
A suffix on the hive's id rather than a prefix of its own, because
|
||||
`agent-alpha` reads as *the agent named alpha* — which is precisely
|
||||
what this identity does not say.
|
||||
|
||||
One id per hive rather than per agent, because agents are created at
|
||||
runtime and a per-agent client would make creating one a config
|
||||
change plus a reload. The consequence is that this identity says
|
||||
|
|
@ -937,6 +947,38 @@ in
|
|||
+ "services.hyperhive.swarm.hives; rename the hive or the "
|
||||
+ "colliding client.";
|
||||
}
|
||||
{
|
||||
# A hive whose name ENDS with the agent suffix mints an id the
|
||||
# queue's responder reads as somebody else's agents:
|
||||
# `hive-foo-agent` parses as *the agents of hive foo* before it
|
||||
# parses as *the hive foo-agent*, because the agent rule is the
|
||||
# more specific one and therefore runs first.
|
||||
#
|
||||
# ⚠️ NOT covered by the duplicate-id assertion above, and the gap is
|
||||
# the interesting half. That one fires only when BOTH `foo` and
|
||||
# `foo-agent` are on the roster, because only then are two clients
|
||||
# actually named the same string. With `foo-agent` alone there is no
|
||||
# duplicate and nothing to see — the hive simply receives its
|
||||
# agents' grant instead of its own, and a NATS denial arrives as a
|
||||
# timeout, so the symptom names nothing.
|
||||
#
|
||||
# Checkable here and nowhere downstream: the responder runs in a
|
||||
# container with no view of the roster (`policy.rs`'s `hive_name`
|
||||
# says why that is deliberate), so this is the last place that knows
|
||||
# both the naming scheme and the set of names.
|
||||
assertion =
|
||||
!cfg.oidc.hiveIdentities
|
||||
|| !lib.any (h: lib.hasSuffix cfg.agentClientSuffix h) (lib.attrNames hyperhiveCfg.swarm.hives);
|
||||
message =
|
||||
"services.hyperhive.swarm.hives contains "
|
||||
+ lib.concatMapStringsSep ", " (h: "'${h}'") (
|
||||
lib.filter (h: lib.hasSuffix cfg.agentClientSuffix h) (lib.attrNames hyperhiveCfg.swarm.hives)
|
||||
)
|
||||
+ " — names ending with '${cfg.agentClientSuffix}', the suffix that "
|
||||
+ "marks a hive's agent containers. Such a hive's own client id is "
|
||||
+ "indistinguishable from another hive's agent client, and the "
|
||||
+ "queue resolves it as the agents. Rename the hive.";
|
||||
}
|
||||
# The two obligations `authelia.bearer.authz` carries. Authelia
|
||||
# enforces both itself — but in its `preStart` validator, so a
|
||||
# violation produces a green `nixos-rebuild switch` and an authelia
|
||||
|
|
|
|||
|
|
@ -769,7 +769,7 @@ in
|
|||
# so a drift here is silent at the point of change and
|
||||
# misattributed at the point of failure.
|
||||
"--hive-client-prefix ${lib.escapeShellArg autheliaCfg.hiveClientPrefix}"
|
||||
"--agent-client-prefix ${lib.escapeShellArg autheliaCfg.agentClientPrefix}"
|
||||
"--agent-client-suffix ${lib.escapeShellArg autheliaCfg.agentClientSuffix}"
|
||||
"--reader-client ${lib.escapeShellArg controllerCfg.queueClientId}"
|
||||
];
|
||||
# Every credential arrives by `LoadCredential` and is named
|
||||
|
|
|
|||
Loading…
Reference in a new issue