matrix: one sender account and one sender token per hive
A swarm runs one homeserver and every hive on it logged in as the same `@hive:` localpart, holding the same access token out of one swarm-wide store path. That is one matrix identity for N hives: the homeserver cannot attribute an action to the hive that took it, and revoking one hive's standing revokes every hive's. Three changes, and the third is the one that makes the other two real: - **The localpart carries the hive's name** (`hive-<hive>`), derived in one place, `swarm_secret_client::matrix::hive_localpart`. `hive-matrix.nix` renders the same string as the appservice registration's `sender_localpart`, so the shared account stops being created rather than merely stops being used. - **The store path is templated by hive**, not a constant. The "a swarm runs one homeserver, so this is a constant rather than a parameter" rationale went with it; it stopped holding the moment two hives shared the homeserver it describes. - **The path moved out from under the grant every hive has.** It sat at `swarm/services/matrix/sender-token`, inside the `secret/data/swarm/services/*` read stanza `policy::render` gives every hive. It now sits under that hive's own stanza, `secret/data/swarm/hives/<hive>/*`, which interpolates the reader's name — so a hive reads its own token and is refused another's. The policy renderer itself is unchanged: narrowing the `services/*` grant would break the OIDC-secret read it exists for, and moving the credential is what this needed instead. A policy test walks the rendered stanzas and asserts none of hive alpha's covers hive beta's sender token, so a later stanza that widened it fails here. `swarm-matrix-ctl` takes a new required `MATRIX_MINT_HIVE` and writes that hive's path; its store grant in `swarm-bao.nix` follows, scoped to one hive's leaf via the new `deploy.bao.matrixCtlHiveName` (defaulting to this host's `hiveName`) rather than a `hives/*` wildcard, which would hand the matrix container every hive's token back. Migration: no outage at deploy. `ensure_hive_user` short-circuits on the local token file, so a hive keeps running on what it has; with no such file it reads the new per-hive path, finds nothing, and falls through to the existing register-or-appservice-login ladder against its own localpart — which needs only the per-hive `as_token` on local disk. The old shared object is read by nothing afterwards. Rooms do not follow the identity, and that is the one operator step; both ways out are written into `docs/integrations/matrix.md`. No admin standing is granted to the per-hive accounts: `admin_execute` stays empty and the assertion pinning it is untouched.
This commit is contained in:
parent
637b308d4d
commit
1261b525d6
14 changed files with 461 additions and 133 deletions
|
|
@ -147,20 +147,26 @@ let
|
|||
# the `services/` prefix the publisher holds would be a real loss even
|
||||
# though it would read as tidier.
|
||||
#
|
||||
# ⚠️ `services` is PLURAL, because the path segment comes from
|
||||
# `Kind::Service`'s `#[strum(serialize = "services")]` and not from
|
||||
# `Kind::label`, which renders the singular for error text. The singular
|
||||
# spelling evaluates, deploys, and 403s every read with "permission
|
||||
# denied" and nothing else.
|
||||
name = "matrix-ctl's grant is the sender token's path and nothing else";
|
||||
# ⚠️ `hives` is PLURAL, because the path segment comes from
|
||||
# `Kind::Hive`'s strum serialisation and not from `Kind::label`, which
|
||||
# renders the singular for error text. The singular spelling evaluates,
|
||||
# deploys, and 403s every read with "permission denied" and nothing else.
|
||||
#
|
||||
# 🩸 The hive NAME in the middle is the per-hive half of this credential:
|
||||
# the token used to be one swarm-wide value under `services/matrix/`,
|
||||
# which every hive's own policy granted read on. The negative arms below
|
||||
# are what keep it from drifting back — neither the `services/*` tree nor
|
||||
# a `hives/*` wildcard may appear, since either one hands matrix-ctl (or
|
||||
# a hive) reach beyond the single leaf it owns.
|
||||
name = "matrix-ctl's grant is one hive's sender token path and nothing else";
|
||||
ok =
|
||||
let
|
||||
s = baoGrantHere.systemd.services.swarm-bao-matrix-ctl-policy.script;
|
||||
in
|
||||
lib.hasInfix "path \"secret/data/swarm/services/matrix/sender-token\" {" s
|
||||
&& !(lib.hasInfix "secret/data/swarm/services/*" s)
|
||||
lib.hasInfix "path \"secret/data/swarm/hives/h1/matrix/sender-token\" {" s
|
||||
&& !(lib.hasInfix "secret/data/swarm/services" s)
|
||||
&& !(lib.hasInfix "secret/data/swarm/agents" s)
|
||||
&& !(lib.hasInfix "secret/data/swarm/hives" s)
|
||||
&& !(lib.hasInfix "secret/data/swarm/hives/*" s)
|
||||
&& !(lib.hasInfix "sys/policies/acl" s);
|
||||
}
|
||||
{
|
||||
|
|
|
|||
|
|
@ -289,6 +289,26 @@ let
|
|||
&& u.environment.MATRIX_MINT_REGISTRATION == "/var/lib/hyperhive/matrix-appservice/hyperhive.yaml"
|
||||
&& u.serviceConfig.Type == "oneshot";
|
||||
}
|
||||
{
|
||||
# 🩸 The per-hive minting identity, read off the rendered unit rather than
|
||||
# off the option: the binary builds its store path out of
|
||||
# `MATRIX_MINT_HIVE` and logs in as `MATRIX_MINT_LOCALPART`, so a unit
|
||||
# that passed the old bare `hive` would publish one identity for the
|
||||
# whole swarm again and nothing in the Rust tests could see it. Both
|
||||
# spellings are pinned, and the localpart is pinned as *derived from* the
|
||||
# hive name rather than as a literal, which is the agreement
|
||||
# `swarm_secret_client::matrix::hive_localpart` owns.
|
||||
name = "matrix-ctl is told which hive it mints for, and acts as that hive's account";
|
||||
ok =
|
||||
let
|
||||
m = baoWithMatrix;
|
||||
u = m.containers.hive-matrix.config.systemd.services.swarm-matrix-ctl;
|
||||
hive = m.services.hyperhive.hiveName;
|
||||
in
|
||||
u.environment.MATRIX_MINT_HIVE == hive
|
||||
&& u.environment.MATRIX_MINT_LOCALPART == "hive-${hive}"
|
||||
&& u.environment.MATRIX_MINT_LOCALPART != "hive";
|
||||
}
|
||||
{
|
||||
# 🩸 The crate is a `*ctl` with subcommands, so the unit has to name a
|
||||
# VERB. This is the one end of that contract nix owns: the binary's own
|
||||
|
|
|
|||
Loading…
Reference in a new issue