Watch
0
0
Fork
You've already forked hyperhive
0

agents: re-pull the agent's own bao mTLS leaf on a timer

swarm-controller re-issues an agent's store leaf at half its validity, but
the running container kept presenting the copy its hive delivered at
start, so a re-issued leaf reached the agent only on its next restart.

hive-agent-bao-identity now seeds a live copy of the pair in its own
runtime directory from the delivered credentials, and re-runs every 10
minutes: it logs in with the live leaf, reads its own
swarm/agents/<agent>/bao-mtls (a read render_agent already grants), and
when the leaf changed, logs in with the new pair and swaps it in only if
the store accepts it.

Every unit in the container that logs in to the store presents the live
pair: hive-agent (queue-identity.nix), hive-matrix-daemon,
hive-subagent-daemon, hive-agent-forge-token, hive-agent-forge-accounts
and hive-agent-github-token. They keep loading the server CA as a
credential. The long-running daemons are ordered after the refresher so
the live pair exists when they start.

It renews only while the held leaf is still valid: the login that reads
the new leaf is made with the current one.

Refs #5017
This commit is contained in:
atlas 2026-10-11 03:05:59 +02:00 • committed by mara
commit c6717bccb0
17 changed files with 308 additions and 166 deletions

View file

@ -75,19 +75,17 @@ let
}
{
# One store identity per agent: the fetch presents the same certificate
# the identity check proves. Every `BAO_*` value is an address or a
# `%d/` path, never a value.
# the identity check keeps current. Every `BAO_*` value is an address or
# a path, never a value.
name = "the fetch presents the agent's own store identity, by path";
ok =
let
u = fetchUnit agentForgeBao;
e = u.environment;
in
builtins.elem "hive-agent-bao-cert" u.serviceConfig.LoadCredential
&& builtins.elem "hive-agent-bao-key" u.serviceConfig.LoadCredential
&& e.BAO_ADDR == baoAddr
&& e.BAO_CLIENT_CERT == "%d/hive-agent-bao-cert"
&& e.BAO_CLIENT_KEY == "%d/hive-agent-bao-key";
e.BAO_ADDR == baoAddr
&& e.BAO_CLIENT_CERT == "/run/hive-agent-bao-identity/cert"
&& e.BAO_CLIENT_KEY == "/run/hive-agent-bao-identity/key";
}
{
# The field is the secret. It goes to a file by redirect and is never
@ -201,8 +199,7 @@ let
in
u.serviceConfig.User == name
&& u.serviceConfig.UMask == "0077"
&& builtins.elem "hive-agent-bao-cert" u.serviceConfig.LoadCredential
&& u.environment.BAO_CLIENT_CERT == "%d/hive-agent-bao-cert";
&& u.environment.BAO_CLIENT_CERT == "/run/hive-agent-bao-identity/cert";
}
{
# Files a hive wrote keep working until the operator re-links them.

View file

@ -82,11 +82,9 @@ let
in
u.serviceConfig.User == name
&& u.serviceConfig.UMask == "0077"
&& builtins.elem "hive-agent-bao-cert" u.serviceConfig.LoadCredential
&& builtins.elem "hive-agent-bao-key" u.serviceConfig.LoadCredential
&& u.environment.BAO_ADDR == baoAddr
&& u.environment.BAO_CLIENT_CERT == "%d/hive-agent-bao-cert"
&& u.environment.BAO_CLIENT_KEY == "%d/hive-agent-bao-key";
&& u.environment.BAO_CLIENT_CERT == "/run/hive-agent-bao-identity/cert"
&& u.environment.BAO_CLIENT_KEY == "/run/hive-agent-bao-identity/key";
}
{
# A file a hive wrote keeps working until the operator links a token in

View file

@ -116,20 +116,17 @@ let
# The daemon reads this agent's tokens from the store ITSELF, as itself,
# from inside this container — `main` too, since the swarm mints it and
# no hive does. So it needs the same identity ./agent-forge-bao.nix's
# fetch carries, in its own credentials directory.
# fetch carries: the live pair, and the server CA from its credentials.
name = "the matrix daemon carries this agent's own store identity";
ok =
let
u = daemon agentMatrixBao;
in
u.serviceConfig.LoadCredential == [
"hive-agent-bao-cert"
"hive-agent-bao-key"
"hive-agent-bao-server-ca"
]
u.serviceConfig.LoadCredential == [ "hive-agent-bao-server-ca" ]
&& u.environment.BAO_ADDR == "https://bao.t.local:8200"
&& u.environment.BAO_CLIENT_CERT == "%d/hive-agent-bao-cert"
&& u.environment.BAO_CLIENT_KEY == "%d/hive-agent-bao-key";
&& u.environment.BAO_CLIENT_CERT == "/run/hive-agent-bao-identity/cert"
&& u.environment.BAO_CLIENT_KEY == "/run/hive-agent-bao-identity/key"
&& builtins.elem "hive-agent-bao-identity.service" u.after;
}
{
# The agent's name, and nothing derived from it: the daemon builds its

View file

@ -87,20 +87,30 @@ let
&& builtins.elem "hive-agent-bao-server-ca" c;
}
{
# `%d` and not a path under the agent's state dir: the host file is
# `0600` to the hive daemon, so the only copy this unprivileged unit can
# open is the one systemd puts in its own credentials directory. The
# address is the option's value rather than a literal that agrees with
# it today.
name = "the identity check presents its certificate out of the credentials directory";
# The live pair sits in the unit's own runtime directory, which outlives
# each run so the timer's next run logs in with what the last one
# wrote. The address is the option's value rather than a literal that
# agrees with it today.
name = "the identity check presents the live pair it keeps in its runtime directory";
ok =
let
e = (agentBaoIdentity agentBao).environment;
u = agentBaoIdentity agentBao;
e = u.environment;
in
e.BAO_CLIENT_CERT == "%d/hive-agent-bao-cert"
&& e.BAO_CLIENT_KEY == "%d/hive-agent-bao-key"
e.BAO_CLIENT_CERT == "/run/hive-agent-bao-identity/cert"
&& e.BAO_CLIENT_KEY == "/run/hive-agent-bao-identity/key"
&& u.serviceConfig.RuntimeDirectory == "hive-agent-bao-identity"
&& u.serviceConfig.RuntimeDirectoryPreserve == "yes"
&& e.BAO_ADDR == agentBao.services.hyperhive.agent.bao.addr;
}
{
# A leaf the controller re-issues reaches a running container only
# through this re-run, and an active unit cannot be started again.
name = "the identity check re-runs on a timer";
ok =
(agentBaoIdentity agentBao).serviceConfig.RemainAfterExit == false
&& agentBao.systemd.timers.hive-agent-bao-identity.timerConfig.OnUnitInactiveSec == "10min";
}
{
# Same 403-not-a-miss reason as the hive-side readers: the path
# `swarm_secret_client::mtls::identity_path` builds is the one this
@ -133,22 +143,22 @@ let
{
# Nothing about the identity may be printed, and the read-back is where
# that could slip: `bao kv get` on this path answers with certificate
# material, and the object beside it is a private key. The check needs
# only whether the read succeeded.
# material and a private key. The answer goes to a file in the unit's
# own `0700` directory.
#
# The path goes through `lib.escapeShellArg` here for the same reason the
# module passes it through one — that helper decides whether an argument
# needs quotes at all, and this one (only `[a-z0-9/-]`) comes back bare.
# Spelling the quotes in by hand asserts a rendering nixpkgs chooses
# rather than the redirect this property is about.
name = "the identity check discards what it reads back";
name = "the identity check writes what it reads back to a file";
ok =
let
m = agentBao;
name = m.services.hyperhive.agent.user.name;
arg = lib.escapeShellArg "secret/swarm/agents/${name}/bao-mtls";
in
lib.hasInfix "bao kv get -field=cert ${arg} >/dev/null" (agentBaoIdentity m).script;
lib.hasInfix "bao kv get -format=json ${arg} >\"$raw\"" (agentBaoIdentity m).script;
}
{
# The absence arm, and what makes the four above able to fail. An agent
@ -160,17 +170,16 @@ let
}
{
# The harness reads its per-agent queue secret from the store itself,
# under the same store identity the check above proves, because there is
# one identity per agent.
# under the same store identity the check above keeps current, because
# there is one identity per agent.
name = "the harness presents the agent's own store identity";
ok =
let
u = agentHarness agentBao;
in
builtins.elem "hive-agent-bao-cert" u.serviceConfig.LoadCredential
&& builtins.elem "hive-agent-bao-key" u.serviceConfig.LoadCredential
&& u.environment.BAO_CLIENT_CERT == "%d/hive-agent-bao-cert"
&& u.environment.BAO_CLIENT_KEY == "%d/hive-agent-bao-key"
u.environment.BAO_CLIENT_CERT == "/run/hive-agent-bao-identity/cert"
&& u.environment.BAO_CLIENT_KEY == "/run/hive-agent-bao-identity/key"
&& builtins.elem "hive-agent-bao-identity.service" u.after
&& u.environment.BAO_ADDR == agentBao.services.hyperhive.agent.bao.addr;
}
{
@ -200,7 +209,7 @@ let
in
!(u.environment ? BAO_ADDR)
&& !(u.environment ? HIVE_AGENT_NAME)
&& !(builtins.elem "hive-agent-bao-cert" (u.serviceConfig.LoadCredential or [ ]));
&& !(builtins.elem "hive-agent-bao-server-ca" (u.serviceConfig.LoadCredential or [ ]));
}
];
in

View file

@ -113,15 +113,11 @@ let
lib.all
(
u:
u.serviceConfig.LoadCredential == [
"hive-agent-bao-cert"
"hive-agent-bao-key"
"hive-agent-bao-server-ca"
]
u.serviceConfig.LoadCredential == [ "hive-agent-bao-server-ca" ]
&& u.environment.HIVE_AGENT_NAME == "a1"
&& u.environment.BAO_ADDR == "https://bao.t.local:8200"
&& u.environment.BAO_CLIENT_CERT == "%d/hive-agent-bao-cert"
&& u.environment.BAO_CLIENT_KEY == "%d/hive-agent-bao-key"
&& u.environment.BAO_CLIENT_CERT == "/run/hive-agent-bao-identity/cert"
&& u.environment.BAO_CLIENT_KEY == "/run/hive-agent-bao-identity/key"
)
[
(subagent claudeBao)
@ -135,15 +131,11 @@ let
let
u = subagent opencodeBao;
in
u.serviceConfig.LoadCredential == [
"hive-agent-bao-cert"
"hive-agent-bao-key"
"hive-agent-bao-server-ca"
]
u.serviceConfig.LoadCredential == [ "hive-agent-bao-server-ca" ]
&& u.environment.HIVE_AGENT_NAME == "a1"
&& u.environment.BAO_ADDR == "https://bao.t.local:8200"
&& u.environment.BAO_CLIENT_CERT == "%d/hive-agent-bao-cert"
&& u.environment.BAO_CLIENT_KEY == "%d/hive-agent-bao-key";
&& u.environment.BAO_CLIENT_CERT == "/run/hive-agent-bao-identity/cert"
&& u.environment.BAO_CLIENT_KEY == "/run/hive-agent-bao-identity/key";
}
{
name = "an opencode agent with no store hands its subagent daemon no store identity";