The agreement half of delivering the agent queue principal's client secret through the store. No producer yet, so nothing writes this path — the unit that does lands in the same PR, with the write grant it needs. queue.rs is the sibling matrix.rs prescribes for a second kind of secret rather than another field on a shared struct. Keyed per HIVE, not per agent: the queue identity is minted once per hive at deploy time and says which hive an agent belongs to, never which agent. The client id rides with the secret for matrix.rs's stated reason — a credential has to be reconstructable from the store alone, and deriving `hive-<name>-agent` on the reading side is the split spelling the authelia module warns denies every agent as a timeout. policy.rs's render() takes the hive name now and emits a second, narrow stanza for that hive's own path. The agent stanza is untouched: an agent's path does not name its hive, so narrowing it still needs the enumeration docs/trust-boundary/security.md rejects. A hive path does name its principal, so scoping it costs nothing and drifts nowhere. every_hive_gets_a_byte_identical_document is replaced rather than deleted. Its surviving half is that the text is a function of the deploy-time name alone, so a re-emission cannot drift; the new arms are that one hive's document cannot reach another's path, and that a name which could close the stanza is refused — live again now that a name reaches the document text. Refs #3853
84 lines
3.3 KiB
Rust
84 lines
3.3 KiB
Rust
//! 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 rules every path obeys ([`path`]), the translation from this
|
|
//! deployment's environment into a logged-in client ([`client`]), and, per kind
|
|
//! of secret, the path it lives at together with the fields it holds
|
|
//! ([`matrix`], [`queue`]). Each of those is a thing the controller and a hive
|
|
//! must say identically, so it is said once here.
|
|
//!
|
|
//! [`policy`] is the same kind of agreement seen from the other side: which of
|
|
//! those paths a hive's own token may read. It belongs here rather than in the
|
|
//! controller because the grant and the path are one statement — spelled
|
|
//! differently they produce a 403 that names neither.
|
|
//!
|
|
//! [`client`] is deliberately ignorant of all of it: it moves whatever type a
|
|
//! caller names, so a second kind of secret is a new module beside [`matrix`]
|
|
//! and not another field on a struct shared with it.
|
|
|
|
pub mod client;
|
|
pub mod matrix;
|
|
pub mod path;
|
|
pub mod policy;
|
|
pub mod queue;
|
|
|
|
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. A principal's kind in the singular
|
|
/// (`agent`, `hive`, `service`, `controller`) when the name addresses
|
|
/// one, or what the name is to the secret otherwise — `account`, for
|
|
/// a matrix credential.
|
|
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))
|
|
}
|
|
}
|