drop the shared hive agent login
Every agent now connects to the queue and reads logs with its own secret from the store (#5181), so the per-hive `hive-<h>-agent` OIDC client has no reader left. Remove it end to end: - hive-c0re no longer forwards `hive-queue-agent-{secret,client-id}` into containers, and no longer forwards HIVE_AGENT_OIDC_TOKEN_ENDPOINT; the agent module that loaded them (nix/agent-modules/queue.nix) is gone, its options refused with a removal message. - The host reader (glue-queue-agent-credential.nix, swarm-bao-queue-agent), its store role and policy unit (swarm-bao-queue-agent-policy), its PKI leaf and the deploy.bao.queueAgent* / queue.agentCredentialDir options are gone; the options are refused with a removal message. - authelia mints no `hive-<h>-agent` client, and swarm-secret-publish no longer writes swarm/hives/<h>/queue/agent. - swarm-secret-client drops queue::agent_client_path / queue::Credential. - HIVE_AGENT_NATS_URL is now gated on the agents' queue address alone. The log store's authelia check stays for operators. swarm-nats-auth still refuses a `-agent` client id; that refusal and `agentClientSuffix` are left for a follow-up. Refs #4459 Refs #4765
This commit is contained in:
parent
d19bd83aa5
commit
868c6cf96c
41 changed files with 286 additions and 1410 deletions
|
|
@ -14,8 +14,7 @@
|
|||
# is why: the host file is `0600` to the hive daemon and this unit runs as the
|
||||
# unprivileged agent user. nspawn's `--load-credential` is read by the
|
||||
# container manager as root and re-exposed under this unit's own `User=`; a
|
||||
# bind would deliver a private key this user cannot open. Same answer, same
|
||||
# reason, as ./queue.nix's credential pair.
|
||||
# bind would deliver a private key this user cannot open.
|
||||
#
|
||||
# This is the courier's other end: the hive-side collector has no purpose
|
||||
# without it, so the two ship together and neither is reachable alone.
|
||||
|
|
@ -63,12 +62,11 @@ let
|
|||
# Where the identity lives in the store, spelled from the same pieces the
|
||||
# publisher uses. `swarm_secret_client::mtls::identity_path` builds
|
||||
# `swarm/agents/<agent>/bao-mtls` and `path::MOUNT` is `secret`; this
|
||||
# literal is the nix half of that one agreement, exactly as
|
||||
# ../host-modules/glue-queue-agent-credential.nix spells its own.
|
||||
# literal is the nix half of that one agreement.
|
||||
identityPath = "secret/swarm/agents/${agentName}/bao-mtls";
|
||||
|
||||
# The address is the whole switch — no separate `enable`, the same shape
|
||||
# ./queue.nix and ./logs.nix gate themselves with. A hive that has a store
|
||||
# ./logs.nix gates itself with. A hive that has a store
|
||||
# forwards its address and every agent on it gets the check; a hive that has
|
||||
# none forwards nothing and no agent does. An `enable` beside it would be a
|
||||
# knob whose only correct setting is whatever the address already says, and
|
||||
|
|
@ -153,7 +151,7 @@ in
|
|||
UMask = "0377";
|
||||
# Bare ids, no paths: the terse `LoadCredential=` form that inherits a
|
||||
# credential the service *manager* received, which is what the
|
||||
# container manager passed in. ./queue.nix states the same shape.
|
||||
# container manager passed in.
|
||||
LoadCredential = [
|
||||
certCredential
|
||||
keyCredential
|
||||
|
|
|
|||
|
|
@ -34,6 +34,12 @@ let
|
|||
sets it. Pin it host-side with
|
||||
services.hyperhive.deploy.hive-controller.claudeCodePackage.
|
||||
'';
|
||||
queueRemoved = ''
|
||||
The harness reads the queue's address at runtime from HIVE_AGENT_NATS_URL,
|
||||
which the hive forwards into every container, and authenticates with the
|
||||
agent's own secret from the swarm secret store, so no agent build sets a
|
||||
queue option. Remove the setting.
|
||||
'';
|
||||
|
||||
# `claude` on PATH. A hive that pins a claude-code
|
||||
# (`services.hyperhive.deploy.hive-controller.claudeCodePackage`) hands its
|
||||
|
|
@ -81,7 +87,6 @@ in
|
|||
./otel.nix
|
||||
./otel-removed.nix
|
||||
./packages.nix
|
||||
./queue.nix
|
||||
./queue-identity.nix
|
||||
./user.nix
|
||||
./screen.nix
|
||||
|
|
@ -101,6 +106,8 @@ in
|
|||
"agent"
|
||||
"claudeCodePath"
|
||||
] claudeCodePathRemoved)
|
||||
(lib.mkRemovedOptionModule [ "services" "hyperhive" "agent" "queue" "natsUrl" ] queueRemoved)
|
||||
(lib.mkRemovedOptionModule [ "services" "hyperhive" "agent" "queue" "tokenEndpoint" ] queueRemoved)
|
||||
];
|
||||
|
||||
options.services.hyperhive.agent.icon = lib.mkOption {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
# This agent's own credential for the swarm queue, read from the store by the
|
||||
# harness itself.
|
||||
#
|
||||
# The credential beside it in ./queue.nix is keyed per **hive**: one OIDC
|
||||
# client minted at deploy time and handed to every agent container on the
|
||||
# hive, so at the queue's auth callout one agent is indistinguishable from its
|
||||
# co-hived neighbours. This one is keyed per **agent** — `swarm-controller`
|
||||
# mints it at agent creation into `swarm/agents/<agent>/queue`
|
||||
# (`swarm_secret_client::queue::agent_queue_path`), at swarm level, with no
|
||||
# hive anywhere in the chain.
|
||||
# Keyed per **agent**, so at the queue's auth callout one agent is
|
||||
# distinguishable from its co-hived neighbours: `swarm-controller` mints it at
|
||||
# agent creation into `swarm/agents/<agent>/queue`
|
||||
# (`swarm_secret_client::queue::agent_queue_path`), at swarm level, with no hive
|
||||
# anywhere in the chain.
|
||||
#
|
||||
# The agent authenticates to the store as itself, with the certificate
|
||||
# ./bao.nix already proves it can log in with, and reads its own path. A hive
|
||||
|
|
@ -42,11 +40,7 @@ let
|
|||
# too.
|
||||
serverCaCredential = "hive-agent-bao-server-ca";
|
||||
|
||||
# The store's address is the whole switch, exactly as in ./bao.nix — and
|
||||
# deliberately *not* the queue coordinates in ./queue.nix. Those are the
|
||||
# hive's, and gating a swarm-minted per-agent credential on a hive-level
|
||||
# option would put the hive back in a path whose entire purpose is not
|
||||
# having one.
|
||||
# The store's address is the whole switch, exactly as in ./bao.nix.
|
||||
configured = cfg.addr != null;
|
||||
in
|
||||
{
|
||||
|
|
@ -54,7 +48,7 @@ in
|
|||
systemd.services.hive-agent = {
|
||||
# A bare id, no path: the terse `LoadCredential=` form that inherits a
|
||||
# credential the service *manager* received, which is what the container
|
||||
# manager passed in. ./bao.nix and ./queue.nix state the same shape.
|
||||
# manager passed in. ./bao.nix states the same shape.
|
||||
serviceConfig.LoadCredential = [ serverCaCredential ];
|
||||
# The live certificate pair exists once ./bao.nix's unit has run.
|
||||
after = [ "hive-agent-bao-identity.service" ];
|
||||
|
|
|
|||
|
|
@ -1,117 +0,0 @@
|
|||
# The hive's shared swarm-queue client credential, imported into the harness
|
||||
# unit's credentials directory for `swarm-logs` (./logs.nix) to authenticate
|
||||
# with. The harness itself connects to the queue with the agent's own
|
||||
# credential (./queue-identity.nix) and reads neither file.
|
||||
#
|
||||
# The credential is two files, the secret and the client id beside it, so a
|
||||
# reader never spells `hive-<name>-agent` a second time.
|
||||
#
|
||||
# ⚠️ The credential arrives as a systemd credential and NOT as a bind mount,
|
||||
# and the mode is why: the host file is `root:0600` and the reader runs as the
|
||||
# unprivileged agent user. nspawn's `--load-credential` (written by
|
||||
# `hive_c0re::lifecycle::host_config`) is read by the container manager as
|
||||
# root and re-exposed under this unit's own `User=`; a bind would deliver a
|
||||
# file the agent user cannot open.
|
||||
{
|
||||
lib,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.services.hyperhive.agent.queue;
|
||||
configured = cfg.natsUrl != null && cfg.tokenEndpoint != null;
|
||||
|
||||
# The two ids `hive_c0re::lifecycle::host_config` forwards under. Neither
|
||||
# side can discover the other's spelling, so a rename is a rename there too.
|
||||
secretCredential = "hive-queue-agent-secret";
|
||||
clientIdCredential = "hive-queue-agent-client-id";
|
||||
|
||||
# Where systemd materialises this unit's credentials. `%d` only expands
|
||||
# *inside* a unit, so `swarm-logs` (./logs.nix), which is not one, needs the
|
||||
# literal; deriving it from the unit name here is what keeps the two from
|
||||
# disagreeing about which unit's credentials they mean.
|
||||
credentialsDir = "/run/credentials/hive-agent.service";
|
||||
in
|
||||
{
|
||||
options.services.hyperhive.agent.queue = {
|
||||
natsUrl = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "tls://nats.example.com:4222";
|
||||
description = ''
|
||||
Where the swarm queue listens, as this container reaches it.
|
||||
|
||||
Set by the generated meta flake from the host's
|
||||
{option}`services.hyperhive.deploy.hive-controller.queue.agentNatsUrl`,
|
||||
which names the queue rather than a loopback address — inside this
|
||||
container `127.0.0.1` is the agent itself.
|
||||
|
||||
`null` means this hive has not been given the queue's address for its
|
||||
agents: the harness unit imports no credential, and the harness fails
|
||||
at boot, since it cannot run without a queue connection. It is
|
||||
deliberately not defaulted to anything: a guessed address builds fine
|
||||
and talks to the wrong machine.
|
||||
'';
|
||||
};
|
||||
|
||||
tokenEndpoint = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "https://auth.example.com/api/oidc/token";
|
||||
description = ''
|
||||
The swarm IdP's OAuth2 token endpoint, where `swarm-logs` mints a
|
||||
`client_credentials` token for the client named in the delivered
|
||||
credential.
|
||||
|
||||
The harness unit imports that credential only when this and
|
||||
{option}`services.hyperhive.agent.queue.natsUrl` are both set.
|
||||
'';
|
||||
};
|
||||
|
||||
# One declaration, two readers. The credential ids are this module's —
|
||||
# `hive_c0re::lifecycle::host_config` and the `LoadCredential=` below are
|
||||
# the pair that has to agree on them — and a second consumer that spelled
|
||||
# them again would be a second source of truth for a string whose
|
||||
# mismatch is a file that is simply not there.
|
||||
clientIdFile = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
readOnly = true;
|
||||
default = "${credentialsDir}/${clientIdCredential}";
|
||||
description = ''
|
||||
Path the agent's OIDC client id is delivered at, for a consumer
|
||||
outside the harness unit. Read-only: it is a fact about where the
|
||||
credential lands, not a knob — see {option}`services.hyperhive.agent.logs.queryUrl`
|
||||
for the consumer this exists for.
|
||||
'';
|
||||
};
|
||||
|
||||
clientSecretFile = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
readOnly = true;
|
||||
default = "${credentialsDir}/${secretCredential}";
|
||||
description = ''
|
||||
Path the agent's OIDC client secret is delivered at. Read-only for
|
||||
the same reason as {option}`services.hyperhive.agent.queue.clientIdFile`.
|
||||
|
||||
🩸 A PATH and never a value. The file is `0400` to the agent user and
|
||||
is read at the moment of a token request; nothing in this tree puts
|
||||
its contents in an environment variable, where `/proc/<pid>/environ`
|
||||
would publish them to every process in the container.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf configured {
|
||||
systemd.services.hive-agent = {
|
||||
# Bare ids, no paths: this is the terse `LoadCredential=` form that
|
||||
# inherits a credential the service *manager* received, which is what
|
||||
# the container manager passed in. `man systemd.exec` also makes that
|
||||
# form non-fatal when the credential is absent, which is exactly the
|
||||
# behaviour a hive whose publisher has not run yet needs.
|
||||
serviceConfig.LoadCredential = [
|
||||
secretCredential
|
||||
clientIdCredential
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
Loading…
Reference in a new issue