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:
parent
9c601c4166
commit
4384a1fffa
6 changed files with 584 additions and 6 deletions
68
swarm-secret-client/src/lib.rs
Normal file
68
swarm-secret-client/src/lib.rs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
//! The swarm's secret-store client: where a credential lives, and how both ends
|
||||
//! reach it.
|
||||
//!
|
||||
//! The HTTP is [`vaultrs`]'s job. What this crate owns is the *agreements* —
|
||||
//! the path a credential is written to and read from ([`path`]), the field its
|
||||
//! bytes live in, and the translation from this deployment's environment into
|
||||
//! a logged-in client ([`client`]). Each of those is a thing the controller and
|
||||
//! a hive must say identically, so it is said once here.
|
||||
|
||||
pub mod client;
|
||||
pub mod path;
|
||||
|
||||
pub use client::SecretStore;
|
||||
|
||||
/// What can go wrong between "we have a client certificate" and "we have the
|
||||
/// credential".
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// A name that would have addressed something other than what the caller
|
||||
/// meant. See [`path`].
|
||||
#[error("{kind} name {value:?} is not a single path segment of [A-Za-z0-9_-]")]
|
||||
PathSegment {
|
||||
/// Which name was rejected — `agent` or `account`.
|
||||
kind: &'static str,
|
||||
/// The offending value, quoted in the message because the caller
|
||||
/// usually got it from config and needs to see which one.
|
||||
value: String,
|
||||
},
|
||||
|
||||
/// A variable the store's address or identity comes from is unset or
|
||||
/// empty. Named rather than defaulted: a wrong store address fails much
|
||||
/// later and much less clearly than a missing one.
|
||||
#[error("{0} is unset or empty")]
|
||||
MissingEnv(&'static str),
|
||||
|
||||
/// A client-certificate file named by the environment could not be read.
|
||||
#[error("reading {path} (from {var}): {source}")]
|
||||
Identity {
|
||||
/// The variable that named the file.
|
||||
var: &'static str,
|
||||
/// The path it named.
|
||||
path: String,
|
||||
/// The underlying IO failure.
|
||||
source: std::io::Error,
|
||||
},
|
||||
|
||||
/// The address would not parse into a URL the client can use.
|
||||
#[error("the store's settings are unusable: {0}")]
|
||||
Settings(String),
|
||||
|
||||
/// The store refused us, was unreachable, or answered something we could
|
||||
/// not parse.
|
||||
#[error(transparent)]
|
||||
Vault(#[from] Box<vaultrs::error::ClientError>),
|
||||
|
||||
/// The client certificate and key did not form a usable identity, or the
|
||||
/// CA bundle did not parse.
|
||||
#[error("building the TLS identity: {0}")]
|
||||
Tls(#[source] reqwest::Error),
|
||||
}
|
||||
|
||||
impl From<vaultrs::error::ClientError> for Error {
|
||||
fn from(e: vaultrs::error::ClientError) -> Self {
|
||||
// Boxed because `ClientError` is large enough that carrying it inline
|
||||
// makes every `Result` in the crate pay for the rare arm.
|
||||
Self::Vault(Box::new(e))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue