Watch
0
0
Fork
You've already forked hyperhive
0

matrix: drop the per-hive appservice and its unused token

Every hive's homeserver loaded a per-hive `hyperhive` appservice
registration, and the swarm minted and delivered an as_token for it
through bao. No client presented that token: swarm-controller creates
every `@hive-<hive>` sender and every agent account with the swarm
appservice token (hive_sender.rs, agent_token.rs), and no Rust code read
the per-hive one.

Removed, with everything that existed only to serve it:
- hive-matrix.nix: the `hyperhive` registration, its activation-time
  render, the host bind mount of /var/lib/hyperhive/matrix-appservice,
  its LoadCredential entry, and the internal options
  `deploy.matrix.appserviceTokenFile` / `appserviceRegistrationScript`.
  tuwunel now loads only the swarm registration.
- glue-matrix-bao-token.nix and its `swarm-bao-matrix-token` unit.
- glue-bao-tls.nix: the matrix-token leaf and its defaults.
- swarm-bao.nix: the `swarm-matrix-token-<hive>` roles and policies
  (`swarm-bao-matrix-token-policy`), and the options
  `deploy.bao.matrixTokenClient{Cert,Key}File` and
  `matrixTokenCommonNamePrefix`, now mkRemovedOptionModule.
- swarm-secret-publisher.nix: the per-hive token mint and its state dir.
- swarm.nix: the reserved `<matrixTokenCommonNamePrefix>-<hive>` CN.
- swarm-secret-client: `matrix::appservice_token_path`.

The swarm registration (swarm-matrix-ctl, swarm-controller) is untouched.
Comments and docs that pointed at the removed glue file now point at
glue-queue-agent-credential.nix, which has the same reader shape.

Refs #4428
This commit is contained in:
atlas 2026-10-04 22:52:22 +02:00 • committed by mara
commit 48067d07e8
31 changed files with 362 additions and 1233 deletions

View file

@ -35,8 +35,9 @@ pub fn accounts_dir(agent: &str) -> Result<String, Error> {
Ok(format!("{prefix}/matrix"))
}
/// The localpart `hive` acts as on the homeserver, and the `sender_localpart`
/// of that hive's appservice registration.
/// The localpart `hive` acts as on the homeserver: the account
/// `swarm-controller` creates for it with the swarm's appservice token, and
/// the one hive-c0re's sender token belongs to.
///
/// **Derived from the hive's name, which is what makes it one account per
/// hive.** It used to be the bare constant `hive`: one swarm runs one
@ -45,9 +46,8 @@ pub fn accounts_dir(agent: &str) -> Result<String, Error> {
/// one hive without revoking all of them.
///
/// The prefix is kept so the account still reads as a hive's rather than an
/// agent's; `nix/host-modules/hive-matrix.nix` renders the same string into
/// the registration's `sender_localpart`, and **the two must match** — nothing
/// wires an override across.
/// agent's. Both `swarm-controller` and hive-c0re derive it here, so the two
/// cannot disagree on which account a hive is.
///
/// Infallible on purpose: a hive name is `[a-z0-9-]` (`hive_types::Ident`) and
/// the appservice namespace regex admits that charset, so there is no
@ -60,7 +60,7 @@ pub fn hive_localpart(hive: &str) -> String {
/// The path holding `hive`'s matrix sender account's homeserver access token.
///
/// Keyed per **hive**, like [`appservice_token_path`] below. Under
/// Keyed per **hive**, like [`crate::queue::agent_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.
@ -73,27 +73,6 @@ pub fn sender_token_path(hive: &str) -> Result<String, Error> {
Ok(format!("{prefix}/matrix/sender-token"))
}
/// The path holding `hive`'s matrix appservice token (`as_token`).
///
/// Keyed per **hive**, not per agent, like [`crate::queue::agent_client_path`]
/// and unlike [`account_path`] above: one homeserver admits one hive's
/// accounts, so the identity that creates them is the hive's.
///
/// ⚠️ Renamed from `registration-token` along with what it holds: the
/// homeserver no longer accepts a shared registration secret at all, so a
/// value still stored under the old path would be read by nothing. A hive
/// whose store has only the old path falls back to its locally minted
/// token — see `glue-matrix-bao-token.nix` — so the rename degrades rather
/// than breaks, but the store needs a fresh `put` to take effect again.
///
/// # 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 appservice_token_path(hive: &str) -> Result<String, Error> {
let prefix = principal_prefix(Kind::Hive, hive)?;
Ok(format!("{prefix}/matrix/appservice-token"))
}
/// The name segment of the controller's own subtree, and the cert-auth role it
/// logs in under (`swarm-controller`'s `store::CERT_ROLE`).
pub(crate) const CONTROLLER: &str = "swarm-controller";
@ -122,9 +101,9 @@ pub fn swarm_appservice_token_path() -> Result<String, Error> {
/// no other place its homeserver is written down.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Credential {
/// `glue-matrix-bao-token.nix` reads the store with
/// `bao kv get -field=value`, so this name is load-bearing for a reader
/// this crate does not control. Renaming it silently breaks that unit.
/// Named `value` like [`crate::forge::Credential::value`] and
/// [`crate::queue::Credential::value`], so a nix-side reader spells
/// `bao kv get -field=value` for every kind.
pub value: String,
/// Absent on every object written before this field existed, and KV2 keeps
@ -134,8 +113,8 @@ pub struct Credential {
/// would break every stored credential at once.
///
/// `skip_serializing_if` is doing separate work: without it a token-only
/// credential serialises `"homeserver":null`, and this object is read by
/// `glue-matrix-bao-token.nix` with `bao kv get -field=value`.
/// credential serialises `"homeserver":null` rather than the
/// `{"value":…}` shape the objects written before this field have.
#[serde(skip_serializing_if = "Option::is_none")]
pub homeserver: Option<String>,
}
@ -183,10 +162,8 @@ mod tests {
#[test]
fn the_localpart_is_derived_from_the_hive_name() {
// The literal is the point: `nix/host-modules/hive-matrix.nix` renders
// the same string into the registration's `sender_localpart`, and
// nothing wires an override across — so a change here is a change
// there.
// The literal is the point: an existing hive's account on the
// homeserver carries this localpart, so a change here strands it.
assert_eq!(hive_localpart("pr1ma"), "hive-pr1ma");
assert_ne!(
hive_localpart("pr1ma"),
@ -197,33 +174,6 @@ mod tests {
assert_ne!(hive_localpart("pr1ma"), "hive");
}
#[test]
fn the_appservice_token_lands_under_the_hive_prefix_the_grant_covers() {
// Spelled out for the same reason as above, and with a second job here:
// the read policy grants `secret/data/swarm/hives/<hive>/*`, so this
// string is what makes the path reachable at all.
assert_eq!(
appservice_token_path("pr1ma").expect("a plain name is legal"),
"swarm/hives/pr1ma/matrix/appservice-token"
);
}
#[test]
fn the_appservice_token_is_not_a_top_level_namespace() {
// The path its predecessor used to hold. `Kind` is a closed set and
// `matrix` is not one of its members, so a path with `matrix` as the
// second segment is outside every grant — which is how it came to 403
// on every read.
let p = appservice_token_path("pr1ma").expect("legal");
assert!(!p.starts_with("swarm/matrix/"), "{p}");
}
#[test]
fn a_traversal_in_the_hive_name_is_refused() {
let e = appservice_token_path("../beta").expect_err("a traversal is not");
assert!(matches!(e, Error::PathSegment { kind: "hive", .. }), "{e}");
}
#[test]
fn the_swarm_appservice_token_lands_under_the_controller() {
assert_eq!(
@ -310,14 +260,14 @@ mod tests {
#[test]
fn the_value_field_matches_what_the_nix_reader_asks_for() {
// The literal is the point: `bao kv get -field=value` is the other end
// of this agreement and lives in a file no Rust test can reach, so the
// The literal is the point: a nix-side reader spells the field as
// `bao kv get -field=value` in a file no Rust test can reach, so the
// name is pinned here rather than derived from the struct.
//
// It doubles as the compatibility control for `homeserver`: a
// token-only credential must still serialise to exactly these bytes,
// with no `homeserver` key at all, so adding the field cannot change
// what that unit reads.
// what such a reader gets.
let json = serde_json::to_string(&Credential {
value: "t".to_owned(),
homeserver: None,

View file

@ -10,9 +10,9 @@ use crate::Error;
/// The KV v2 mount every swarm secret lives under.
///
/// A literal because the nix-side reader spells the same one in
/// `glue-matrix-bao-token.nix`; an option nobody sets would be two ways to say
/// one thing.
/// 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
/// to say one thing.
pub const MOUNT: &str = "secret";
/// The root under [`MOUNT`] that every swarm secret lives beneath.