Watch
0
0
Fork
You've already forked hyperhive
0

swarm: revoke an agent's queue credential when it is declared destroyed

A per-agent queue credential is minted at agent creation and nothing has
ever removed it. An agent declared destroyed loses its container and
keeps its credential: a bearer secret recovered from a snapshot or a
stale capture still authenticates as that agent, so the set of usable
credentials only grows.

Delete the path the mint published, on the one transition that ends an
agent's life. It mirrors step 3 of `mint_and_verify` and no other step:
the leaf, the ACL document and the cert role are what a hive uses to
collect an agent's secrets and are re-minted on every run of the mint.

Every version, not the newest. The mint rewrites the path when the
principal it names needs correcting, so KV v2's plain delete would leave
the identical secret readable at ?version=N. That is a separately-ACL'd
path, hence the second stanza in the controller's grant -- `delete` on
metadata discloses nothing, and `update` on the data path already lets
this principal destroy any agent credential's usability.

The destroy is not blocked by a failed revocation: the declaration is
already published and refusing the call would leave an operator with an
agent they cannot tear down. The failure is logged at error instead,
naming the agent, since a silent orphan is the fault being removed.
This commit is contained in:
atlas 2026-09-23 11:56:30 +02:00 • committed by mara
commit 8caf688ee4
5 changed files with 229 additions and 12 deletions

View file

@ -220,6 +220,54 @@ pub async fn mint_and_verify(agent: &str) -> Result<()> {
Ok(())
}
/// Revoke `agent`'s queue credential: delete the path
/// [`mint_and_verify`]'s step 3 published, and everything ever written at it.
///
/// The undo of that one step and of no other. The leaf, the ACL document and
/// the cert-auth role that make up the rest of an agent's identity stay where
/// they are — they are what a *hive* uses to collect an agent's secrets, they
/// are minted afresh on every run of the mint, and tearing them down is not
/// what the queue credential outliving its holder is about.
///
/// **Deletes every version, not the newest one.** The mint rewrites this path
/// whenever the principal it names has to be corrected, so a soft delete would
/// leave the identical secret sitting in version history, readable at
/// `?version=N` by anything that can read the path at all — a value still
/// recoverable has not been revoked. See
/// [`SecretStore::delete_all_versions`][swarm_secret_client::SecretStore::delete_all_versions].
///
/// **Idempotent**: revoking an agent that never had a credential, or one
/// already revoked, succeeds and says so. A teardown that runs twice is
/// ordinary, and a second run that failed would be a worse fault than the one
/// this exists to fix.
///
/// # Errors
/// When the store cannot be reached or refuses the delete. The caller decides
/// what that costs — `set_agent_state` logs it and lets the destroy proceed,
/// since a credential that is still live is a smaller harm than an agent that
/// cannot be torn down.
pub async fn revoke_queue_credential(agent: &str) -> Result<()> {
let queue_path = queue::agent_queue_path(agent)?;
let store = crate::store::connect()
.await
.context("logging in to the swarm secret store")?;
store
.delete_all_versions(&queue_path)
.await
.with_context(|| format!("revoking the agent queue credential at {queue_path}"))?;
// At `info` and unconditional: an unlogged revocation is indistinguishable
// from a leak, and this line is the only record an operator has that the
// credential stopped being usable. It cannot say whether one was there —
// the controller's grant on these paths is write-only by design, so it
// deletes blind.
tracing::info!(
agent,
%queue_path,
"agent queue credential revoked: every version of the path deleted"
);
Ok(())
}
/// The consumer of everything [`mint_and_verify`] wrote: log in **as the
/// agent**, with the leaf just issued, and read back both paths just
/// published.