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:
parent
e4b170f8c6
commit
48067d07e8
31 changed files with 362 additions and 1233 deletions
|
|
@ -17,8 +17,8 @@ formatted at each call site is an agreement with no owner — it holds right up
|
|||
until one side is edited alone, and then it fails as a missing key rather than
|
||||
as a mismatch.
|
||||
|
||||
The third end is what settles it. `nix/host-modules/glue-matrix-bao-token.nix`
|
||||
reads the store with `bao kv get -field=value`. That reader is a shell line in
|
||||
The third end is what settles it. `nix/agent-modules/forge-token.nix` reads
|
||||
the store with `bao kv get -field=value`. That reader is a shell line in
|
||||
a nix module: it cannot be renamed by the same refactor as a Rust struct, and
|
||||
no Rust test reaches it. So the field name is pinned by a test against the
|
||||
serialised literal rather than left to the struct definition.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue