agent: fetch this agent's own swarm-queue credential from the store

Every agent on a hive authenticates to the swarm queue with the same
hive-scoped OIDC client, so at the auth callout one agent is
indistinguishable from its co-hived neighbours. The commit before this
one mints a secret per agent at swarm level into
secret/swarm/agents/<agent>/queue; nothing read it.

Read it here, and read it from the container itself. A hive courier in
the path would be the hive vouching for which agent this is, which is
the property a per-agent credential exists to remove -- so the agent
logs in to the store with the certificate hive-agent-bao-identity
already proves it can log in with, and reads its own path. The store
certificate is for reaching the store and nothing else: what the new
unit writes to /run is the secret it read back, and nothing hands a
BAO_CLIENT_* path to anything queue-shaped.

The read needs no policy change. render_agent grants read on
secret/data/swarm/agents/<agent>/*, which covers this path and the
bao-mtls one beside it alike -- which is also why this unit degrades
where the identity check fails. A refusal this unit sees and that check
did not cannot be a policy that drifted; it is an object not yet minted,
the ordinary state of every agent created before its swarm knew to mint
one.

The harness resolves the path and reports which credential this agent
can present. It does not yet present it: the auth-callout responder
still verifies only the hive-scoped token, and an agent offering a
credential nothing on the other end reads back would simply be refused.
Teaching swarm-nats-auth to read the same path is the next slice.
This commit is contained in:
atlas 2026-09-21 20:06:09 +02:00 committed by mara
commit afdfce67ec
4 changed files with 416 additions and 2 deletions

View file

@ -41,6 +41,7 @@ let
agentNoBao = agentWith { };
agentBaoIdentity = machine: machine.systemd.services.hive-agent-bao-identity;
agentQueueCredential = machine: machine.systemd.services.hive-agent-queue-credential;
cases = [
{
# Both ids or neither: the secret authenticates nobody without the id it
@ -164,6 +165,93 @@ let
name = "an agent told no store address runs no identity check";
ok = !(agentNoBao.systemd.services ? hive-agent-bao-identity);
}
{
# The per-agent credential's fetch rides on the same store identity the
# check above proves, because there is one identity per agent. A fetch
# unit loading different ids would be a second certificate nothing
# mints.
name = "the queue-credential fetch presents the agent's own store identity";
ok =
let
u = agentQueueCredential agentBao;
in
builtins.elem "hive-agent-bao-cert" u.serviceConfig.LoadCredential
&& u.environment.BAO_CLIENT_CERT == "%d/hive-agent-bao-cert"
&& u.environment.BAO_CLIENT_KEY == "%d/hive-agent-bao-key";
}
{
# Same 403-not-a-miss reason as every other reader here: the path
# `swarm_secret_client::queue::agent_queue_path` builds is the one this
# agent's own policy stanza covers. Built from the agent's own name,
# because the name is what makes it this agent's credential and not a
# neighbour's — which is the entire point of minting one per agent.
name = "the queue-credential fetch reads the agent's own per-agent path";
ok =
let
m = agentBao;
name = m.services.hyperhive.agent.user.name;
in
lib.hasInfix "secret/swarm/agents/${name}/queue" (agentQueueCredential m).script;
}
{
# ⛔ The store certificate authenticates against the store and nothing
# else. It must never reach the queue, so nothing in this unit may hand
# a `BAO_CLIENT_*` path to anything queue-shaped.
name = "the queue-credential fetch never points the queue at the store certificate";
ok =
let
u = agentQueueCredential agentBao;
harness = agentHarness agentBao;
in
!(lib.hasInfix "nats" u.script)
&& !(lib.any (lib.hasPrefix "BAO_") (builtins.attrNames harness.environment));
}
{
# A secret fetched at boot has no business surviving one, and the
# directory has to be the unit's own so the file is owned by the agent
# rather than needing a mode change. `RemainAfterExit` is what keeps
# systemd from removing it out from under the harness.
name = "the fetched credential lands in a runtime directory the unit keeps alive";
ok =
let
u = agentQueueCredential agentBao;
c = u.serviceConfig;
in
c.RuntimeDirectory == "hive-agent-queue-credential"
&& c.RemainAfterExit
&& lib.hasInfix "/run/hive-agent-queue-credential/secret" u.script;
}
{
# 🩸 Degrades where the identity check fails, and the reason is that
# both reads are governed by one policy stanza: a refusal this unit
# sees and that check did not is an object not yet minted, not a policy
# that drifted. An agent created before its swarm minted one is a
# deployment doing nothing wrong.
name = "a per-agent credential that was never minted does not fail the unit";
ok = lib.hasInfix "exit 0" (agentQueueCredential agentBao).script;
}
{
# The harness reads a path and never a value — the same discipline
# ../agent-modules/queue.nix keeps for the hive-scoped secret. Not
# `%d`: this one is not a systemd credential, it is a file the
# container fetched for itself.
name = "the harness is handed the fetched credential as a path";
ok =
let
m = agentBao;
in
(agentHarness m).environment.HIVE_AGENT_QUEUE_AGENT_SECRET_FILE
== m.services.hyperhive.agent.queue.agentSecretFile;
}
{
# The absence arm. An agent whose swarm gave it no store has nothing to
# log in with, so there is nothing to fetch with either — and the
# harness is then told no path rather than one that never fills.
name = "an agent told no store address fetches no queue credential";
ok =
!(agentNoBao.systemd.services ? hive-agent-queue-credential)
&& !((agentHarness agentNoBao).environment ? HIVE_AGENT_QUEUE_AGENT_SECRET_FILE);
}
];
in
runGroup "agent-queue-bao" cases