From b58a0d8ba94f53b43d5fafa7ed662bccff579c37 Mon Sep 17 00:00:00 2001 From: atlas Date: Wed, 30 Sep 2026 00:50:39 +0200 Subject: [PATCH] bao: drop matrix-ctl's per-hive sender-token grant swarm-controller is the only minter of swarm/hives//matrix/sender-token since #4820, so the swarm-matrix-ctl policy's first stanza granted a write no code performs. The policy keeps its one used stanza, the swarm appservice token that `swarm-matrix-ctl appservice publish` writes. deploy.bao.matrixCtlHiveName only named the hive in the dropped stanza and goes with it. hive-matrix.nix no longer calls the per-hive appservice's as_token hive-c0re's authority: tuwunel loads the registration and creates the sender account, and no client presents that token. --- nix/host-modules/hive-matrix.nix | 12 ++++----- nix/host-modules/swarm-bao.nix | 44 +++++--------------------------- nix/module-eval/bao-grants.nix | 27 +++++--------------- 3 files changed, 19 insertions(+), 64 deletions(-) diff --git a/nix/host-modules/hive-matrix.nix b/nix/host-modules/hive-matrix.nix index b69a2b74..11ee555e 100644 --- a/nix/host-modules/hive-matrix.nix +++ b/nix/host-modules/hive-matrix.nix @@ -55,13 +55,13 @@ let # with no host-side chown or GID pinning. matrixSecretCredential = "/run/credentials/tuwunel.service/oidc_client_secret"; - # How this hive creates matrix accounts: an appservice registration whose - # `url` is null. Null is a legal `url` (ruma's `Registration` types it - # `Option`), and it is the whole point — with no URL the homeserver + # The hive's appservice registration, whose `url` is null. Null is a legal + # `url` (ruma's `Registration` types it `Option`), and it is the + # whole point — with no URL the homeserver # never calls out, so there is no HTTP service to run and no daemon to - # operate. What the registration delivers is the `as_token`: hive-c0re's - # standing authority to create, and log in as, the accounts named by the - # namespace below, with no shared registration secret in the picture. + # operate. tuwunel loads the registration and creates its sender account + # (below). No client presents its `as_token`: `swarm-controller` mints the + # sender's token with the swarm registration's. appserviceId = "hyperhive"; # The appservice's own user, and the account the hive acts as. An diff --git a/nix/host-modules/swarm-bao.nix b/nix/host-modules/swarm-bao.nix index a9c48a94..a12d1c79 100644 --- a/nix/host-modules/swarm-bao.nix +++ b/nix/host-modules/swarm-bao.nix @@ -472,36 +472,18 @@ let # written by the caller — the same trap as the two grants above. # # Not `swarm/services/*` like the publisher's: a homeserver is not entitled - # to overwrite Grafana's OIDC client. Each path is spelled to the leaf for + # to overwrite Grafana's OIDC client. The path is spelled to the leaf for # that reason, not for tidiness. # - # 🩸 And the leaf now carries a HIVE segment, which is the narrowing that - # matters: the credential used to live at `swarm/services/matrix/sender-token` - # — one value for the whole swarm, under the `services/*` tree every hive's - # own policy grants read on. Under `swarm/hives//` the only read grant - # that reaches it is that hive's own stanza, so one hive cannot fetch - # another's. `matrixCtlHive` below is the name this principal may write, and - # it is one hive rather than a `hives/*` wildcard for the same reason. - # - # ⚠️ Nothing presenting this identity writes the first stanza's path: - # `swarm-controller` is the sender token's only minter. The stanza is unused. - # - # The second stanza is the swarm appservice's token, which matrix-ctl mints - # inside the container and publishes here for the controller. `read` as well - # as write, unlike either sibling, because publish compares before it writes. + # It is the swarm appservice's token, which matrix-ctl mints inside the + # container and publishes here for the controller. `read` as well as write, + # unlike either sibling, because publish compares before it writes. matrixCtlPolicyText = '' - path "${credentialMountPath}/data/swarm/hives/${matrixCtlHive}/matrix/sender-token" { - capabilities = ["create", "update", "read"] - } - path "${credentialMountPath}/data/${swarmAppserviceTokenLeaf}" { capabilities = ["create", "update", "read"] } ''; - # The hive the unused first stanza above names. - matrixCtlHive = baoDeploy.matrixCtlHiveName; - # The swarm appservice token's leaf, the nix half of # `swarm_secret_client::matrix::swarm_appservice_token_path`. Under # `controller/`, the one kind no hive's policy reads: its sender is the @@ -780,9 +762,8 @@ let # One reader per hive in the swarm directory. Written from the directory # rather than from this host's own name because the policy is written where - # the STORE is and the reader runs where its hive is — the same split - # `matrixCtlHiveName` above exists to paper over, answered here by naming - # every hive instead of asking the operator which one. + # the STORE is and the reader runs where its hive is, so this host's name is + # not the reader's. perHiveReaders = { rolePrefix, cnPrefix, ... }@spec: lib.mapAttrsToList (hiveName: _: { @@ -1611,19 +1592,6 @@ in ''; }; - matrixCtlHiveName = lib.mkOption { - type = lib.types.str; - default = toString hyperhiveCfg.hiveName; - defaultText = lib.literalExpression "services.hyperhive.hiveName"; - example = "pr1ma"; - description = '' - Hive whose matrix sender token the store's matrix-ctl role may write. - - Unused: nothing presenting that identity writes the sender token; - `swarm-controller` is its only minter. - ''; - }; - matrixTokenCommonNamePrefix = lib.mkOption { type = lib.types.str; default = "swarm-bao-matrix-token"; diff --git a/nix/module-eval/bao-grants.nix b/nix/module-eval/bao-grants.nix index 21449f51..b99b3d3c 100644 --- a/nix/module-eval/bao-grants.nix +++ b/nix/module-eval/bao-grants.nix @@ -422,32 +422,19 @@ let # the `services/` prefix the publisher holds would be a real loss even # though it would read as tidier. # - # ⚠️ `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. - # - # The one other leaf is the swarm appservice token, which matrix-ctl - # mints and publishes for the controller. Counted, so a third stanza - # fails rather than riding along beside two correct ones. - name = "matrix-ctl's grant is one hive's sender token and the swarm appservice token, nothing else"; + # The leaf is the swarm appservice token, which matrix-ctl mints and + # publishes for the controller. Counted, so a second stanza fails rather + # than riding along beside a correct one. + name = "matrix-ctl's grant is the swarm appservice token, nothing else"; ok = let s = baoGrantHere.systemd.services.swarm-bao-matrix-ctl-policy.script; in - lib.hasInfix "path \"secret/data/swarm/hives/h1/matrix/sender-token\" {" s - && lib.hasInfix "path \"secret/data/swarm/controller/swarm-controller/matrix/appservice-token\" {\n capabilities = [\"create\", \"update\", \"read\"]\n}" s - && lib.length (lib.splitString "path \"" s) == 3 + lib.hasInfix "path \"secret/data/swarm/controller/swarm-controller/matrix/appservice-token\" {\n capabilities = [\"create\", \"update\", \"read\"]\n}" s + && lib.length (lib.splitString "path \"" s) == 2 && !(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); } {