swarm: mint a per-agent queue credential beside the agent's store identity
Every agent on a hive authenticates to the swarm queue with the same hive-scoped secret today, so at the auth callout one agent is indistinguishable from its co-hived neighbours and no subject can be scoped to one of them. Mint a secret per agent instead, at swarm level, into secret/swarm/agents/<agent>/queue -- inside the stanza every agent's ACL document already grants, so no policy changes and no existing agent's document is rewritten. It is written by the same node that already mints the agent's certificate, and read back under the agent's own token before that node reports success. The secret is not derived from the agent's mTLS identity: the two credentials answer different questions and coupling their lifetimes would mean renewing either implied renewing the other. Nothing here rotates a queue secret -- a re-run keeps the existing value and only corrects the principal it names, because this function is re-run deliberately against agents that are already connected. Revoking one means deleting the path. Nothing reads the new credential yet; this is the minting half.
This commit is contained in:
parent
11097ed336
commit
ffd5018b18
6 changed files with 359 additions and 19 deletions
|
|
@ -174,6 +174,31 @@ impl SecretStore {
|
|||
Ok(vaultrs::kv2::read(&self.inner, MOUNT, path).await?)
|
||||
}
|
||||
|
||||
/// Read the object stored at `path`, or `None` when nothing is stored
|
||||
/// there.
|
||||
///
|
||||
/// The verb for a caller whose write must be a no-op if the path is
|
||||
/// already populated. [`read`][Self::read] cannot serve that: it collapses
|
||||
/// *absent*, *denied* and *undecodable* into one `Err`, so a caller
|
||||
/// treating every failure as absence would overwrite a live credential
|
||||
/// whenever the store was merely unreachable.
|
||||
///
|
||||
/// **Only a 404 is absence.** A denial is a 403 and stays an error — a
|
||||
/// principal whose grant does not cover the path must not conclude the
|
||||
/// path is empty, which is how a write ends up clobbering something the
|
||||
/// caller was never allowed to see.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Vault`] for anything that is not a 404: a denial, an
|
||||
/// unreachable store, or an object that does not decode as `T`.
|
||||
pub async fn read_optional<T: DeserializeOwned>(&self, path: &str) -> Result<Option<T>, Error> {
|
||||
match vaultrs::kv2::read(&self.inner, MOUNT, path).await {
|
||||
Ok(value) => Ok(Some(value)),
|
||||
Err(vaultrs::error::ClientError::APIError { code: 404, .. }) => Ok(None),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Write `value` at `path`, creating a new version.
|
||||
///
|
||||
/// # Errors
|
||||
|
|
|
|||
Loading…
Reference in a new issue