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
|
|
@ -73,10 +73,21 @@ let
|
|||
# anything, so the account exists on the very first boot of a fresh
|
||||
# homeserver without hive-c0re racing to register it.
|
||||
#
|
||||
# ⚠️ Must equal `matrix::HIVE_LOCALPART` in hive-c0re, which derives
|
||||
# it independently with nothing wiring an override across — same
|
||||
# agreement, and same reason for saying so, as the token path below.
|
||||
hiveLocalpart = "hive";
|
||||
# 🩸 **Derived from the hive name, so it is one account per hive.** It was
|
||||
# the bare `hive` and a swarm runs one homeserver: every hive on it acted
|
||||
# as the same `@hive:`, which is one identity with no attribution and no
|
||||
# way to revoke a single hive. The hives that do not run this container
|
||||
# register their own `hive-<name>` through the appservice namespace below,
|
||||
# which already admits it.
|
||||
#
|
||||
# ⚠️ Must equal `swarm_secret_client::matrix::hive_localpart`, which
|
||||
# hive-c0re and swarm-matrix-ctl both derive from independently with
|
||||
# nothing wiring an override across — same agreement, and same reason for
|
||||
# saying so, as the token path below.
|
||||
#
|
||||
# `hiveName` has no fallback here for the reason ./glue-matrix-bao-token.nix
|
||||
# gives at its own use of it: it is asserted set for every hyperhive host.
|
||||
hiveLocalpart = "hive-${toString config.services.hyperhive.hiveName}";
|
||||
|
||||
# The `as_token`, and the `hs_token` the spec requires alongside it. Both
|
||||
# minted by the render script below, mode 0600; the `as_token` is the one
|
||||
|
|
@ -168,7 +179,7 @@ let
|
|||
# knows about itself.
|
||||
ctlHomeserverUrl = if cfg.gatewayHost == null then "" else "https://${toString cfg.gatewayHost}";
|
||||
|
||||
# Every local user this hive may provision — agents, `@hive:` itself, and
|
||||
# Every local user this hive may provision — agents, `@hive-<hive>:` itself, and
|
||||
# the operator accounts `hivectl matrix create-user` makes, which is the
|
||||
# whole matrix localpart charset.
|
||||
#
|
||||
|
|
@ -1439,6 +1450,12 @@ in
|
|||
# never a value.
|
||||
MATRIX_MINT_REGISTRATION = appserviceRegistrationPath;
|
||||
MATRIX_MINT_LOCALPART = hiveLocalpart;
|
||||
# The hive segment of the store path the token is published
|
||||
# under, and so the thing that keeps this hive's token out of
|
||||
# every other hive's reach: the grant that reaches it is the
|
||||
# hive's own `swarm/hives/<name>/*` stanza. ./swarm-bao.nix
|
||||
# spells the same name into matrix-ctl's write grant.
|
||||
MATRIX_MINT_HIVE = toString config.services.hyperhive.hiveName;
|
||||
MATRIX_MINT_HOMESERVER = ctlHomeserverUrl;
|
||||
}
|
||||
// lib.optionalAttrs (deployCfg.bao.serverCaFile != null) {
|
||||
|
|
|
|||
|
|
@ -249,9 +249,17 @@ let
|
|||
# written by the caller — the same trap as the two grants above.
|
||||
#
|
||||
# Not `swarm/services/*` like the publisher's: this principal produces
|
||||
# exactly one secret, the appservice sender account's access token, and a
|
||||
# homeserver is not entitled to overwrite Grafana's OIDC client. The path is
|
||||
# spelled to the leaf for that reason, not for tidiness.
|
||||
# exactly one secret, its own hive's matrix sender account access token, and
|
||||
# a homeserver is not entitled 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/<name>/` 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.
|
||||
#
|
||||
# `read` as well as write, unlike either sibling, and it is what makes "and
|
||||
# only once" mechanical: matrix-ctl's first act is to read this path back and
|
||||
|
|
@ -260,11 +268,22 @@ let
|
|||
# here recovers one secret this principal itself wrote, which is a much
|
||||
# narrower grant than the publisher's would have been.
|
||||
matrixCtlPolicyText = ''
|
||||
path "${credentialMountPath}/data/swarm/services/matrix/sender-token" {
|
||||
path "${credentialMountPath}/data/swarm/hives/${matrixCtlHive}/matrix/sender-token" {
|
||||
capabilities = ["create", "update", "read"]
|
||||
}
|
||||
'';
|
||||
|
||||
# Which hive matrix-ctl mints for. This host's own by default, which is right
|
||||
# whenever the store and the homeserver are co-located and is the shape the
|
||||
# `mkDefault` deployments produce; an operator running them apart names the
|
||||
# homeserver's hive here, because the policy is written where the store is
|
||||
# and the container runs where the homeserver is.
|
||||
#
|
||||
# A wrong value is loud rather than silent: matrix-ctl's write comes back 403
|
||||
# with the store's own message and the hive falls back to minting its account
|
||||
# locally, which is the same degrade a store that was never deployed gives.
|
||||
matrixCtlHive = baoDeploy.matrixCtlHiveName;
|
||||
|
||||
# The KV v2 engine the controller writes agent credentials through. Named
|
||||
# once because the grant above and the `secrets enable` in the bootstrap unit
|
||||
# have to agree: a policy pointing at a mount nobody created is precisely the
|
||||
|
|
@ -711,6 +730,31 @@ 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.
|
||||
|
||||
The sender account's access token is **per hive**: it lives at
|
||||
`swarm/hives/<name>/matrix/sender-token`, and the only read grant that
|
||||
reaches it is that hive's own. So matrix-ctl's write grant names one
|
||||
hive too — the hive whose homeserver container it runs in.
|
||||
|
||||
Defaults to this host's own {option}`services.hyperhive.hiveName`,
|
||||
which is correct whenever the store and the homeserver are co-located.
|
||||
Set it when they are not: the policy is written where the store runs,
|
||||
and the oneshot runs where the homeserver does.
|
||||
|
||||
A wrong value degrades rather than breaks — matrix-ctl's write is
|
||||
refused with the store's own message and the hive mints its account
|
||||
locally instead, the same fallback a swarm that never deployed the
|
||||
store already uses.
|
||||
'';
|
||||
};
|
||||
|
||||
serverCaFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
|
|
|
|||
Loading…
Reference in a new issue