Watch
0
0
Fork
You've already forked hyperhive
0

mint per-agent proxy credentials and a per-hive squid self-check credential

This commit is contained in:
atlas 2026-10-28 13:02:25 +01:00 • committed by mara
commit ec06a91deb

View file

@ -0,0 +1,153 @@
//! 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 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<String, Error> {
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/<this hive>/*` 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<String, Error> {
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", &"<redacted>")
.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_user_is_not_a_legal_agent_name() {
assert!(crate::path::checked_segment("agent", SELFCHECK_USER).is_err());
}
#[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"}"#);
}
}