//! The egress-proxy agreement: the credentials squid authenticates its clients //! with, where they live in the store, and what the objects at those paths hold. //! //! Each agent has one ([`agent_credential_path`]), minted by `swarm-controller` //! and read by the agent's hive, which turns it into the hash squid's users file //! holds. Each hive has one more ([`selfcheck_path`]) for its own squid //! self-check, so the self-check can prove an authenticated request is let //! through. Neither end is senior, so both halves are stated once, here, beside //! [`crate::forge`]. use serde::{Deserialize, Serialize}; use crate::{ Error, path::{Kind, principal_prefix}, }; /// The name of an agent's leaf. pub const AGENT_LEAF: &str = "proxy-credential"; /// The name of a hive's self-check leaf. pub const SELFCHECK_LEAF: &str = "squid-selfcheck"; /// The squid user the self-check logs in as. /// /// An agent's squid user is its name, and agent names cannot hold a `.` /// ([`crate::path::checked_segment`]), so this one cannot be an agent's. /// `nix/host-modules/hive-squid.nix` spells the same string. pub const SELFCHECK_USER: &str = "selfcheck.squid"; /// The squid user the self-check logs in as to prove an unrestricted agent /// reaches an unlisted name. It authenticates with the same secret as /// [`SELFCHECK_USER`] ([`selfcheck_path`]), and is always on squid's /// unrestricted list. It cannot be an agent's name for the same reason as /// [`SELFCHECK_USER`]. `nix/host-modules/hive-squid.nix` spells the same /// string. pub const SELFCHECK_UNRESTRICTED_USER: &str = "selfcheck-unrestricted.squid"; /// The path holding the secret `agent` authenticates to the egress proxy with. /// /// A flat leaf under the agent's prefix, like /// [`crate::forge::agent_token_path`], so the agent's own read stanza /// ([`crate::policy::render_shared_agent`]) already covers it, and so does the /// hive's. /// /// # Errors /// [`Error::PathSegment`] when `agent` contains anything but `[A-Za-z0-9_-]`, /// which is what keeps one agent's name from addressing another agent's secret. pub fn agent_credential_path(agent: &str) -> Result { let prefix = principal_prefix(Kind::Agent, agent)?; Ok(format!("{prefix}/{AGENT_LEAF}")) } /// The path holding the secret `hive`'s own squid self-check authenticates with. /// /// Under [`Kind::Hive`], so the grant that reaches it is the hive's own /// `swarm/hives//*` stanza: hive `alpha` reads its own and gets a 403 /// on `beta`'s. /// /// # Errors /// [`Error::PathSegment`] when `hive` contains anything but `[A-Za-z0-9_-]`. pub fn selfcheck_path(hive: &str) -> Result { let prefix = principal_prefix(Kind::Hive, hive)?; Ok(format!("{prefix}/{SELFCHECK_LEAF}")) } /// What [`agent_credential_path`] and [`selfcheck_path`] hold. /// /// No `Debug` derive: `value` is a live credential, and a derived `Debug` is /// one `{:?}` away from a log line. #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Credential { /// The secret: 32 random bytes, base64url without padding, so it can sit in /// a proxy URL's userinfo unescaped. pub value: String, } impl std::fmt::Debug for Credential { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Credential") .field("value", &"") .finish() } } #[cfg(test)] mod tests { use super::*; fn covered_by(policy: &str, path: &str) -> bool { policy.lines().any(|line| { line.strip_prefix("path \"") .and_then(|rest| rest.split_once("\" {")) .and_then(|(p, _)| p.strip_suffix('*')) .is_some_and(|prefix| format!("secret/data/{path}").starts_with(prefix)) }) } #[test] fn the_paths_are_the_leaves_the_nix_side_spells() { assert_eq!( agent_credential_path("atlas").expect("legal"), "swarm/agents/atlas/proxy-credential" ); assert_eq!( selfcheck_path("h1").expect("legal"), "swarm/hives/h1/squid-selfcheck" ); } #[test] fn a_traversal_in_either_name_is_refused() { for bad in ["../argus", "a/b", "a.b", ""] { assert!(agent_credential_path(bad).is_err(), "{bad:?}"); assert!(selfcheck_path(bad).is_err(), "{bad:?}"); } assert!(agent_credential_path("a-b_C9").is_ok()); assert!(selfcheck_path("a-b_C9").is_ok()); } #[test] fn the_agent_reads_its_own_credential_and_a_hive_reads_every_one() { let path = agent_credential_path("atlas").expect("legal"); assert!(covered_by(&crate::policy::effective_agent("atlas"), &path)); assert!(!covered_by(&crate::policy::effective_agent("argus"), &path)); assert!(covered_by(&crate::policy::effective_hive("h1"), &path)); } #[test] fn a_hive_reads_its_own_selfcheck_credential_and_no_other_hives() { let path = selfcheck_path("h1").expect("legal"); assert!(covered_by(&crate::policy::effective_hive("h1"), &path)); assert!(!covered_by(&crate::policy::effective_hive("h2"), &path)); assert!(!covered_by(&crate::policy::effective_agent("atlas"), &path)); } #[test] fn the_selfcheck_users_are_not_legal_agent_names() { for user in [SELFCHECK_USER, SELFCHECK_UNRESTRICTED_USER] { assert!( crate::path::checked_segment("agent", user).is_err(), "{user}" ); } } #[test] fn debug_hides_the_value() { let shown = format!( "{:?}", Credential { value: "s3cret".to_owned() } ); assert!(!shown.contains("s3cret"), "{shown}"); } #[test] fn the_value_field_is_named_value() { let json = serde_json::to_string(&Credential { value: "t".to_owned(), }) .expect("a String serialises"); assert_eq!(json, r#"{"value":"t"}"#); } }