drop the shared hive agent login
Every agent now connects to the queue and reads logs with its own secret from the store (#5181), so the per-hive `hive-<h>-agent` OIDC client has no reader left. Remove it end to end: - hive-c0re no longer forwards `hive-queue-agent-{secret,client-id}` into containers, and no longer forwards HIVE_AGENT_OIDC_TOKEN_ENDPOINT; the agent module that loaded them (nix/agent-modules/queue.nix) is gone, its options refused with a removal message. - The host reader (glue-queue-agent-credential.nix, swarm-bao-queue-agent), its store role and policy unit (swarm-bao-queue-agent-policy), its PKI leaf and the deploy.bao.queueAgent* / queue.agentCredentialDir options are gone; the options are refused with a removal message. - authelia mints no `hive-<h>-agent` client, and swarm-secret-publish no longer writes swarm/hives/<h>/queue/agent. - swarm-secret-client drops queue::agent_client_path / queue::Credential. - HIVE_AGENT_NATS_URL is now gated on the agents' queue address alone. The log store's authelia check stays for operators. swarm-nats-auth still refuses a `-agent` client id; that refusal and `agentClientSuffix` are left for a follow-up. Refs #4459 Refs #4765
This commit is contained in:
parent
d19bd83aa5
commit
868c6cf96c
41 changed files with 286 additions and 1410 deletions
|
|
@ -95,7 +95,7 @@ pub fn hive_localpart(hive: &str) -> String {
|
|||
|
||||
/// The path holding `hive`'s matrix sender account's homeserver access token.
|
||||
///
|
||||
/// Keyed per **hive**, like [`crate::queue::agent_client_path`]. Under
|
||||
/// Keyed per **hive**, like [`crate::queue::hive_client_path`]. Under
|
||||
/// [`Kind::Hive`] the grant that reaches it is the hive's **own** stanza,
|
||||
/// `secret/data/swarm/hives/<this hive>/*`, which interpolates the name: hive
|
||||
/// `alpha` reads its own token and gets a 403 on `beta`'s.
|
||||
|
|
@ -137,7 +137,7 @@ pub fn swarm_appservice_token_path() -> Result<String, Error> {
|
|||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Credential {
|
||||
/// Named `value` like [`crate::forge::Credential::value`] and
|
||||
/// [`crate::queue::Credential::value`], so a nix-side reader spells
|
||||
/// [`crate::queue::AgentCredential::value`], so a nix-side reader spells
|
||||
/// `bao kv get -field=value` for every kind.
|
||||
pub value: String,
|
||||
|
||||
|
|
|
|||
|
|
@ -40,10 +40,9 @@ pub fn identity_path(agent: &str) -> Result<String, Error> {
|
|||
/// was issued from.
|
||||
///
|
||||
/// All three, because a reader has to reconstruct a usable identity from the
|
||||
/// store alone — the same requirement [`crate::queue::Credential`] states for
|
||||
/// carrying its client id. The authority rides along so the cert-auth role
|
||||
/// and the certificate cannot be delivered from two different sources and
|
||||
/// silently disagree; it is public material, unlike the other two fields.
|
||||
/// store alone. The authority rides along so the cert-auth role and the
|
||||
/// certificate cannot be delivered from two different sources and silently
|
||||
/// disagree; it is public material, unlike the other two fields.
|
||||
///
|
||||
/// ⚠️ **No `Debug` derive.** See the hand-written impl below: one of these
|
||||
/// fields is a private key, and a derived `Debug` would put it in any log line
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use crate::Error;
|
|||
/// The KV v2 mount every swarm secret lives under.
|
||||
///
|
||||
/// A literal because the nix-side readers spell the same one, as in
|
||||
/// `glue-queue-agent-credential.nix`; an option nobody sets would be two ways
|
||||
/// `nix/agent-modules/bao.nix`; an option nobody sets would be two ways
|
||||
/// to say one thing.
|
||||
pub const MOUNT: &str = "secret";
|
||||
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@ mod tests {
|
|||
assert!(
|
||||
stanza_paths(&p)
|
||||
.iter()
|
||||
.any(|g| covers(g, "swarm/hives/pr1ma/queue/agent"))
|
||||
.any(|g| covers(g, "swarm/hives/pr1ma/oidc/client"))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,10 @@
|
|||
//! The queue agreements: where a queue credential lives in the store, and what
|
||||
//! the object at that path holds.
|
||||
//!
|
||||
//! **Two kinds live here, and which is which is the thing to get right.**
|
||||
//!
|
||||
//! - [`agent_client_path`] / [`Credential`] are keyed per **hive**: one OIDC
|
||||
//! client minted per hive at deploy time, shared by every agent container on
|
||||
//! it. It says which hive a caller belongs to, never which agent.
|
||||
//! - [`agent_queue_path`] / [`AgentCredential`] are keyed per **agent**: a
|
||||
//! secret the swarm mints for one agent, at swarm level, with no hive in the
|
||||
//! chain. It is what makes one agent distinguishable from its co-hived
|
||||
//! neighbours.
|
||||
//!
|
||||
//! They are separate objects rather than one with a nullable field because they
|
||||
//! are minted by different principals on different events — the hive-scoped one
|
||||
//! by authelia at deploy time, the per-agent one by `swarm-controller` when an
|
||||
//! agent is created — and an object whose shape depends on who wrote it is a
|
||||
//! reader that has to guess.
|
||||
//! An agent's credential, [`agent_queue_path`] / [`AgentCredential`], is keyed
|
||||
//! per **agent**: a secret the swarm mints for one agent, at swarm level, with
|
||||
//! no hive in the chain. It is what makes one agent distinguishable from its
|
||||
//! co-hived neighbours.
|
||||
//!
|
||||
//! The per-agent secret is deliberately **not** derived from the agent's mTLS
|
||||
//! identity (the leaf at [`crate::mtls::identity_path`]). That leaf is for
|
||||
|
|
@ -33,17 +22,6 @@ use crate::{
|
|||
path::{Kind, principal_prefix},
|
||||
};
|
||||
|
||||
/// The path holding the client secret that agent containers on `hive` present
|
||||
/// to the swarm queue.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::PathSegment`] when `hive` contains anything but `[A-Za-z0-9_-]`,
|
||||
/// which is what keeps one hive's name from addressing another hive's secret.
|
||||
pub fn agent_client_path(hive: &str) -> Result<String, Error> {
|
||||
let prefix = principal_prefix(Kind::Hive, hive)?;
|
||||
Ok(format!("{prefix}/queue/agent"))
|
||||
}
|
||||
|
||||
/// The path holding the secret **one agent** presents to the swarm queue.
|
||||
///
|
||||
/// Under the agent's own principal prefix, and that is the whole reason for
|
||||
|
|
@ -89,8 +67,7 @@ pub struct ControllerCredential {
|
|||
/// The path holding `hive`'s own OIDC client secret: the `hive-<hive>`
|
||||
/// identity hive-c0re publishes the hive's status to the swarm queue with.
|
||||
///
|
||||
/// Not [`agent_client_path`], which is the agents' client on the same hive.
|
||||
/// Both sit under the hive's own prefix, which the hive's policy reads
|
||||
/// Under the hive's own prefix, which the hive's policy reads
|
||||
/// ([`crate::policy::render`]).
|
||||
///
|
||||
/// The nix half is `swarm-secret-publisher.nix`, which writes it.
|
||||
|
|
@ -117,8 +94,8 @@ pub struct HiveClientCredential {
|
|||
/// field still decode, because unknown fields are ignored.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct AgentCredential {
|
||||
/// The secret itself. Named to match [`Credential::value`] and
|
||||
/// [`crate::matrix::Credential::value`] so a nix-side reader spells
|
||||
/// The secret itself. Named to match [`crate::matrix::Credential::value`]
|
||||
/// so a nix-side reader spells
|
||||
/// `bao kv get -field=value` for every kind.
|
||||
pub value: String,
|
||||
|
||||
|
|
@ -136,79 +113,10 @@ pub struct AgentCredential {
|
|||
pub minted_at: Option<i64>,
|
||||
}
|
||||
|
||||
/// What the path holds: the client secret, plus the client id it belongs to.
|
||||
///
|
||||
/// The id rides with the secret for the same reason the homeserver rides with
|
||||
/// a matrix token — a credential has to be reconstructable from the store
|
||||
/// alone. Deriving it on the reading side instead would mean spelling
|
||||
/// `hive-<name>-agent` in a second place, and the authelia module's own option
|
||||
/// says what a split spelling costs: every agent is denied as a timeout.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Credential {
|
||||
/// The secret itself. Named to match [`crate::matrix::Credential::value`]
|
||||
/// so a nix-side reader spells `bao kv get -field=value` for either kind.
|
||||
pub value: String,
|
||||
|
||||
/// The OIDC client id the secret authenticates.
|
||||
///
|
||||
/// Required, unlike [`crate::matrix::Credential::homeserver`]: that field
|
||||
/// is optional because it was added to objects already in the store, and
|
||||
/// this path is new. An object without an id is not a usable credential,
|
||||
/// so failing to decode is the wanted behaviour — the alternative is a
|
||||
/// reader holding `None` whose only recourse is to spell the id itself.
|
||||
pub client_id: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_hive_name_lands_under_its_own_principal_prefix() {
|
||||
assert_eq!(
|
||||
agent_client_path("alpha").expect("a plain name is legal"),
|
||||
"swarm/hives/alpha/queue/agent"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_traversal_in_the_hive_name_is_refused() {
|
||||
let e = agent_client_path("../beta").expect_err("a traversal is not");
|
||||
assert!(matches!(e, Error::PathSegment { kind: "hive", .. }), "{e}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_hives_never_share_a_path() {
|
||||
assert_ne!(
|
||||
agent_client_path("alpha").expect("legal"),
|
||||
agent_client_path("beta").expect("legal")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_object_round_trips_through_the_store_representation() {
|
||||
let c = Credential {
|
||||
value: "s3cr3t".to_owned(),
|
||||
client_id: "hive-alpha-agent".to_owned(),
|
||||
};
|
||||
let json = serde_json::to_string(&c).expect("serialises");
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Credential>(&json).expect("deserialises"),
|
||||
c
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_field_names_the_nix_reader_asks_for_are_the_ones_written() {
|
||||
let json = serde_json::to_value(Credential {
|
||||
value: "s3cr3t".to_owned(),
|
||||
client_id: "hive-alpha-agent".to_owned(),
|
||||
})
|
||||
.expect("serialises");
|
||||
assert_eq!(json["value"], "s3cr3t");
|
||||
assert_eq!(json["client_id"], "hive-alpha-agent");
|
||||
}
|
||||
|
||||
/// Spelled out in full because `swarm-secret-publisher.nix` and
|
||||
/// `swarm-bao.nix` write the same string by hand.
|
||||
#[test]
|
||||
|
|
@ -231,9 +139,9 @@ mod tests {
|
|||
let reads = |path: &str| granted.iter().any(|g| path.starts_with(g));
|
||||
let path = controller_client_path().expect("a constant segment");
|
||||
assert!(!reads(&path), "{path} is readable under {granted:?}");
|
||||
// The control: the hive's own agent client IS under a hive stanza, so
|
||||
// the assertion above can fail at all.
|
||||
let own = agent_client_path("alpha").expect("legal");
|
||||
// The control: the hive's own client IS under a hive stanza, so the
|
||||
// assertion above can fail at all.
|
||||
let own = hive_client_path("alpha").expect("legal");
|
||||
assert!(reads(&own), "{own} should be readable under {granted:?}");
|
||||
}
|
||||
|
||||
|
|
@ -381,29 +289,4 @@ mod tests {
|
|||
serde_json::from_str::<AgentCredential>(r#"{"value":"s","hive":"alpha"}"#).is_err()
|
||||
);
|
||||
}
|
||||
|
||||
/// decodes as the other — the property that keeps a reader from picking up
|
||||
/// a hive-shared credential where a per-agent one was meant.
|
||||
#[test]
|
||||
fn the_hive_credential_and_the_agent_credential_are_not_interchangeable() {
|
||||
let hive_json = serde_json::to_string(&Credential {
|
||||
value: "s".to_owned(),
|
||||
client_id: "hive-alpha-agent".to_owned(),
|
||||
})
|
||||
.expect("serialises");
|
||||
assert!(serde_json::from_str::<AgentCredential>(&hive_json).is_err());
|
||||
|
||||
let agent_json = serde_json::to_string(&AgentCredential {
|
||||
value: "s".to_owned(),
|
||||
agent: "atlas".to_owned(),
|
||||
minted_at: None,
|
||||
})
|
||||
.expect("serialises");
|
||||
assert!(serde_json::from_str::<Credential>(&agent_json).is_err());
|
||||
|
||||
assert_ne!(
|
||||
agent_queue_path("atlas").expect("legal"),
|
||||
agent_client_path("atlas").expect("legal"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue