//! 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`], [`mtls`], [`forge`], [`github`], [`grafana`], //! [`acp`], [`ci`], [`review_classifier`], [`builder`], [`ci_metrics`], //! [`claude_login`]). //! 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 given principal's own token may read — a hive's, and an //! agent's, which are two documents because they are two shapes of grant rather //! than one with a name in it. 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. //! //! [`mtls`] is the one module about reaching the store rather than about a //! value inside it, and its doc explains why that is not circular. //! //! [`install_crypto_provider`] is process setup, not an agreement. It lives //! here because this crate depends on no other swarm crate, so a binary that //! needs only the provider and the store links nothing else. pub mod acp; pub mod builder; pub mod ci; pub mod ci_metrics; pub mod claude_login; pub mod client; pub mod forge; pub mod github; pub mod grafana; pub mod matrix; pub mod mtls; pub mod path; pub mod policy; pub mod queue; pub mod review_classifier; pub mod startup_read; pub mod stored_secret; pub use client::SecretStore; pub use startup_read::read_with_startup_retry; /// 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, did not answer within the /// client's request timeout, or answered something we could not parse. #[error(transparent)] Vault(#[from] Box), /// 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), /// The store answered an issue request with a field empty that a usable /// identity needs. Names the field, never its value. #[error("the store issued a certificate whose {0} is empty")] IncompleteIssue(&'static str), } impl From 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)) } } /// Install `aws-lc-rs` as the process-wide rustls crypto provider. Call it /// first thing in `main` of every binary that dials the queue or reaches the /// secret store over TLS. /// /// rustls is built with both `ring` and `aws-lc-rs`; without an installed /// default, the first TLS handshake panics. On the queue side async-nats /// builds its config with `ClientConfig::builder()`, so the client never /// connects. On the store side the client is reqwest, via vaultrs. /// `aws-lc-rs` because it is what reqwest uses when nothing is installed. pub fn install_crypto_provider() { // `Err` only means a provider is already installed, which is all this needs. let _already_installed = rustls::crypto::aws_lc_rs::default_provider().install_default(); } #[cfg(test)] mod tests { use super::*; #[test] fn a_tls_client_config_builds_once_the_provider_is_installed() { install_crypto_provider(); let _config = rustls::ClientConfig::builder() .with_root_certificates(rustls::RootCertStore::empty()) .with_no_client_auth(); } }