diff --git a/swarm-secret-client/src/policy.rs b/swarm-secret-client/src/policy.rs index c314ce27..4144bcc9 100644 --- a/swarm-secret-client/src/policy.rs +++ b/swarm-secret-client/src/policy.rs @@ -186,43 +186,6 @@ pub fn render_agent(agent: &str) -> Result { ))) } -/// Render `agent`'s policy document: read on that one agent's credentials and -/// on the hive's shared queue credential. -/// -/// Extends [`render_agent`] with a second stanza granting read on -/// `swarm/hives//queue/agent`. The queue credential is **hive-shared, -/// not per-agent** — every agent in a hive authenticates to the queue with the -/// same client secret (`queue.rs:1-8`), so a policy scoped strictly to -/// `agents//*` cannot read it and an in-container pull would fail. That -/// hive-shared credential is already handed to every agent container on that -/// hive by the host today, so this grant adds no new authority — it merely -/// makes the existing capability reachable through the agent's own token -/// instead of requiring the credential to be delivered out of band. -/// -/// ⚠️ **Every agent in a hive can read that hive's queue credential.** This is -/// not new authority (the host already provides this exact value to all agents -/// on the hive), but it is a documented property: an agent policy grants read -/// on a path shared across every agent on its hive, not on a path unique to -/// that agent alone. -/// -/// Read-only, for the same reason [`render_agent`]'s is: an agent that could -/// write credentials could hand itself an identity it was never issued. -/// -/// # Errors -/// [`Error::PathSegment`] when `agent` or `hive` holds anything but -/// `[A-Za-z0-9_-]` — both are interpolated into policy paths, so a name that -/// could close a stanza could grant itself anything. -pub fn render_agent_with_queue(agent: &str, hive: &str) -> Result { - checked_segment("agent", agent)?; - let agent_stanza = read_stanza(&format!( - "{MOUNT}/data/{ROOT}/{}/{agent}/*", - <&str>::from(Kind::Agent) - )); - let queue_path = crate::queue::agent_client_path(hive)?; - let queue_stanza = read_stanza(&format!("{MOUNT}/data/{queue_path}")); - Ok(format!("{agent_stanza}{queue_stanza}")) -} - #[cfg(test)] mod tests { use super::*; @@ -445,100 +408,4 @@ mod tests { // consistent with it. assert!(hive.contains("path \"secret/data/swarm/agents/*\"")); } - - #[test] - fn an_agents_document_with_queue_grants_both_paths() { - // The happy path: the document grants read on the agent's own namespace - // and on the hive's queue credential. - let p = render_agent_with_queue("atlas", "pr1ma").expect("legal"); - assert!( - p.contains("path \"secret/data/swarm/agents/atlas/*\""), - "must grant the agent's own path: {p}" - ); - let expected_queue_path = format!( - "path \"{MOUNT}/data/{}\"", - crate::queue::agent_client_path("pr1ma").expect("legal") - ); - assert!( - p.contains(&expected_queue_path), - "must grant the hive's queue credential: {p}" - ); - assert_eq!( - p.matches("path \"").count(), - 2, - "two stanzas, one for the agent and one for the queue: {p}" - ); - } - - #[test] - fn an_agents_document_with_queue_is_read_only() { - // An agent that could write the queue credential could hand every agent - // on its hive an identity they were never issued. - let p = render_agent_with_queue("atlas", "pr1ma").expect("legal"); - for capability in ["create", "update", "delete", "list", "sudo", "patch"] { - assert!(!p.contains(capability), "must not grant {capability}: {p}"); - } - assert!(p.contains("capabilities = [\"read\"]")); - } - - #[test] - fn an_agent_name_with_traversal_in_the_queue_variant_is_refused() { - // The agent parameter is an injection surface in both renderers, so - // refusing a traversal here proves the new one validates it. - assert!( - render_agent_with_queue("atlas/*\" { capabilities = [\"root\"] }", "pr1ma").is_err() - ); - assert!(render_agent_with_queue("", "pr1ma").is_err()); - // The control: legal names still work. - assert!(render_agent_with_queue("a-b_C9", "pr1ma").is_ok()); - } - - #[test] - fn a_hive_name_with_traversal_in_the_queue_variant_is_refused() { - // The hive parameter is a second injection surface that only the queue - // variant introduces, so this test proves that new parameter is - // validated. A name that could close the stanza could grant the agent - // anything. - assert!( - render_agent_with_queue("atlas", "pr1ma/*\" { capabilities = [\"root\"] }").is_err() - ); - assert!(render_agent_with_queue("atlas", "").is_err()); - assert!( - render_agent_with_queue("atlas", "../services/swarm-grafana").is_err(), - "a path traversal that could reach a different kind" - ); - // The control: legal names still work. - assert!(render_agent_with_queue("atlas", "a-b_C9").is_ok()); - } - - #[test] - fn the_queue_variant_does_not_widen_the_agent_stanza() { - // The queue grant must not cause the agent stanza to widen from - // `agents//*` to `agents/*` — that would give every agent every - // other agent's credentials. - let p = render_agent_with_queue("atlas", "pr1ma").expect("legal"); - assert!( - !p.contains("swarm/agents/*"), - "must not grant the whole agent prefix: {p}" - ); - assert!(p.contains("swarm/agents/atlas/*")); - } - - #[test] - fn the_queue_variant_does_not_grant_the_whole_hive_prefix() { - // The queue stanza must grant only the queue credential path, not - // `hives//*` — the latter would give the agent read on every - // secret of the hive that hosts it. - let p = render_agent_with_queue("atlas", "pr1ma").expect("legal"); - assert!( - !p.contains("swarm/hives/*"), - "must not grant the whole hive prefix: {p}" - ); - assert!( - !p.contains("swarm/hives/pr1ma/*"), - "must not grant the hive's whole path: {p}" - ); - let expected_queue_path = crate::queue::agent_client_path("pr1ma").expect("legal"); - assert!(p.contains(&expected_queue_path)); - } }