swarmctl: re-mint an existing agent's store identity

Agent creation at swarm level is event-driven and nothing sweeps for
agents missing a credential, so an agent created before a credential
joined the mint never receives one -- nothing comes back around to it.
Without a way to re-run the mint by hand, the only route to giving an
existing agent its queue credential would be to delete and recreate the
agent.

POST /api/agents/{name}/identity enqueues the same MintAgentIdentity
node POST /api/agents declares, rather than writing inline: a second
code path that mints an identity is a second place for the four strings
that have to agree to disagree. swarmctl agent mint-identity is the
operator end, the same POST-and-print-the-node-id shape agent create
already has.

--hive is required on both ends. Neither the CLI nor the controller
keeps a roster of which agent runs where, and the credentials this mints
name a hive, so a default would be a guess that hands an agent subjects
on a hive it does not run on.

Documents the backfill as a runbook step, and fills in the renewal cell
the credential matrix requires for the new row.
This commit is contained in:
atlas 2026-09-21 18:36:57 +02:00 committed by mara
commit 1442168715
6 changed files with 332 additions and 37 deletions

View file

@ -8,14 +8,12 @@
//! store, and its grant already covers exactly the objects written here
//! (`swarm-bao.nix`'s `controllerPolicyText`: `create/update` on
//! `secret/data/swarm/agents/*`, on `sys/policies/acl/hive-*`, and on
//! `auth/cert/certs/hive-*`). No new authority is asked for anywhere — the
//! agent's queue secret lives under the same `swarm/agents/<agent>` prefix the
//! certificate does, which is why adding it costs no grant on either side.
//! `auth/cert/certs/hive-*`). No new authority is asked for anywhere.
//!
//! **Two credentials, deliberately unrelated.** The certificate is how the
//! agent reaches the store; the queue secret is how it identifies itself to the
//! swarm queue. The second is not derived from the first, so renewing either is
//! a question that can be answered without reference to the other.
//! **Two credentials, deliberately unrelated.** The certificate reaches the
//! store; the queue secret identifies the agent to the swarm queue. Both sit
//! under `swarm/agents/<agent>`, so neither costs a grant — but the second is
//! not derived from the first, so either renews without reference to the other.
//!
//! Four separate strings have to agree before an agent can authenticate: the
//! policy's name, the cert-auth role's name, the certificate's common name,
@ -247,14 +245,12 @@ fn generate_queue_secret() -> Result<String> {
/// names the policy, so the other order leaves a window in which it points at
/// nothing.
///
/// ⚠️ **Step 3 is idempotent and step 2 is not.** Re-running this function
/// re-mints the agent's certificate — a fresh leaf the agent picks up on its
/// next boot — but leaves an existing queue secret exactly as it is. The
/// asymmetry is deliberate: an agent holds its queue secret in a live
/// connection, so replacing it would drop that agent off the queue until it
/// reconnected, and this function is re-run deliberately (by the backfill
/// route) against agents that are already running. Nothing here rotates a
/// queue secret; revoking one means deleting the path.
/// ⚠️ **Step 3 is idempotent and step 2 is not.** Re-running re-mints the
/// certificate — a fresh leaf the agent picks up on its next boot — but leaves
/// an existing queue secret alone. An agent holds that secret in a live
/// connection, and this function is re-run deliberately against agents that
/// are already running, so replacing it would drop them off the queue.
/// Nothing here rotates one; revoking means deleting the path.
///
/// # Errors
/// Anything that stops one of those five steps, with the step named. A