swarm-secret-client: the agreements both ends of the store must share

mara ruled (a) on #3726: a thin workspace crate over `vaultrs` rather than
keeping bao access in nix and having each end trigger units. The HTTP is the
SDK's job; what this crate owns is the things the controller and a hive must
say *identically*, and which have no other home because neither end is senior
to the other.

Three such agreements:

`path::matrix_account` builds where a credential lives. It is fallible rather
than a `format!`, because both names reach it from elsewhere -- the agent name
from the topology, the account name from an agent's own config -- and a `/` or
`..` in either does not produce a malformed path, it produces a valid path to
a *different agent's* secret. The charset mirrors the KV bucket-name rule.

`Credential`'s `value` field is not a free choice: glue-matrix-bao-token.nix
reads the store with `bao kv get -field=value`, so the name is load-bearing
for a consumer no Rust test can reach. A test pins the serialised shape.

`client::Settings` reads BAO_ADDR / BAO_CLIENT_CERT / BAO_CLIENT_KEY /
BAO_CACERT explicitly instead of letting vaultrs fall through to its own
defaults, which look for VAULT_ADDR / VAULT_CLIENT_CERT / VAULT_CLIENT_KEY.
Every unit in this tree sets the BAO_ spellings, so the defaults would yield a
client with no identity at all -- surfacing as a TLS handshake failure, which
names neither the missing variable nor the reason.

The env read is split from the connect so every misconfiguration arm is
testable without a reachable store and without touching process-global env.

Dependency impact, measured against the lock at forge/main rather than assumed:
native-tls 0 -> 0, openssl-sys 0 -> 0, one reqwest (0.13.4) which vaultrs
shares, and 10 new crates that are all derive/proc-macro helpers.

Refs #3726
This commit is contained in:
atlas 2026-09-02 22:27:58 +02:00 committed by mara
commit 4384a1fffa
6 changed files with 584 additions and 6 deletions

View file

@ -0,0 +1,98 @@
//! Where a credential lives, for both ends of the store.
//!
//! The controller writes and a hive reads, and neither is senior to the other,
//! so the path they must agree on is built here rather than formatted at each
//! call site.
use crate::Error;
/// The KV v2 mount every swarm secret lives under.
///
/// A literal because the store's existing reader already hardcodes the same
/// one (`secret/swarm/matrix/registration-token`); an option nobody sets would
/// be two ways to say one thing.
pub const MOUNT: &str = "secret";
/// The prefix under [`MOUNT`] owned by per-agent credentials.
pub const AGENT_PREFIX: &str = "swarm/agents";
/// A path segment that cannot change the path's shape.
///
/// The charset is deliberately narrower than what the store accepts: a `/`
/// turns one agent's segment into another agent's directory, and `..` walks
/// out of the prefix entirely. Both are names this crate receives from
/// elsewhere — an agent name from the topology, an account name from an
/// agent's own config — so neither is trusted to be well-formed here.
fn checked_segment(kind: &'static str, value: &str) -> Result<(), Error> {
if value.is_empty() {
return Err(Error::PathSegment {
kind,
value: value.to_owned(),
});
}
if !value
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
{
return Err(Error::PathSegment {
kind,
value: value.to_owned(),
});
}
Ok(())
}
/// The path holding `agent`'s token for the external matrix account `account`.
///
/// # Errors
/// [`Error::PathSegment`] when either name contains anything but
/// `[A-Za-z0-9_-]`, which is what keeps one agent's name from addressing
/// another agent's secret.
pub fn matrix_account(agent: &str, account: &str) -> Result<String, Error> {
checked_segment("agent", agent)?;
checked_segment("account", account)?;
Ok(format!("{AGENT_PREFIX}/{agent}/matrix/{account}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_well_formed_pair_lands_under_the_agent_prefix() {
let p = matrix_account("atlas", "ops-relay").expect("both segments are legal");
assert_eq!(p, "swarm/agents/atlas/matrix/ops-relay");
assert!(p.starts_with(AGENT_PREFIX));
}
#[test]
fn a_segment_cannot_escape_its_own_directory() {
// Each of these is a *different* way to address another agent's tree,
// and the last two are the ones a charset check catches but a
// `contains("..")` check does not.
for bad in [
"../argus",
"atlas/../argus",
"atlas/matrix",
"a b",
"a.b",
"",
] {
assert!(
matrix_account(bad, "ops-relay").is_err(),
"agent segment {bad:?} must be refused"
);
assert!(
matrix_account("atlas", bad).is_err(),
"account segment {bad:?} must be refused"
);
}
}
#[test]
fn the_legal_charset_is_actually_reachable() {
// The control for the test above: if `checked_segment` rejected
// everything, the escape cases would pass for the wrong reason.
assert!(matrix_account("a-b_C9", "d-e_F0").is_ok());
}
}