hive-matrix: reject an override of registrationTokenFile instead of desyncing hive-c0re

This commit is contained in:
damocles 2026-09-07 15:19:57 +02:00 committed by mara
commit 9d55cba786
3 changed files with 76 additions and 3 deletions

View file

@ -255,6 +255,9 @@ pub const FORGE_CORE_TOKEN: &str = "/var/lib/hyperhive/forge-core-token";
/// `matrix-register-token` — shared matrix registration token.
// nix: bind-mounted into the tuwunel/matrix container (hive-matrix.nix) — must match.
// Not operator-option-driven: `registrationTokenFile` is `internal` on the nix
// side, and an `assertions` entry there rejects any attempt to move it, so this
// literal can never diverge from it.
#[must_use]
pub fn matrix_register_token() -> PathBuf {
state_root().join("matrix-register-token")

View file

@ -40,6 +40,12 @@ let
# login into an intermittent one.
matrixSecretPath = "/var/lib/tuwunel-oidc/${cfg.sso.clientId}.secret";
# Single source for the registration-token path: the option's `default`,
# its `config`-level `mkDefault` self-definition, and the assertion that
# rejects a moved path all read this same binding rather than repeating
# the string literal. See `registrationTokenFile`'s own comment below.
registrationTokenPath = "/var/lib/hyperhive/matrix-register-token";
# ⚠️ tuwunel does NOT read the path above directly, and this indirection
# is not ceremony. Upstream's own words: "under systemd the path must be
# visible to the service after sandboxing (ReadWritePaths / ProtectHome),
@ -452,7 +458,8 @@ in
registrationTokenFile = lib.mkOption {
type = lib.types.path;
default = "/var/lib/hyperhive/matrix-register-token";
internal = true;
default = registrationTokenPath;
description = ''
Host path to a file containing the matrix registration token
tuwunel reads to authorise new-account creation. The token is
@ -461,8 +468,28 @@ in
tuwunel container at the same path. Agents never see this
token hive-c0re uses it to provision per-agent accounts
and the agent only receives the resulting `access_token`.
Override only when integrating with externally-managed
registration tokens.
Not operator-settable `hive-c0re`'s Rust side derives this
same path independently (`paths::matrix_register_token()`) with
nothing wiring an override across, so a moved path used to
desync the two silently. An externally-managed token is
delivered by writing into *this* fixed path instead of moving it
see `glue-matrix-bao-token.nix`, which fetches from the swarm
secret store and overwrites this file in place.
Enforced by an `assertions` entry below rather than `readOnly`:
the `config` block gives this option its own `mkDefault`
definition (lowest priority) so any real override an operator's,
or the pre-rename `swarm.matrix.registrationTokenFile` shim's
still resolves cleanly instead of crashing eval with nixpkgs'
generic "read-only, set multiple times" message; the assertion
then names the actual problem. (`readOnly` was the first attempt
atlas caught that it only rejects a *second* definition,
so with nothing else defining the option a lone override sailed
through silently, same desync as before with a lock that wasn't
locked. Fixed by defining the value here instead of leaving it on
`default` alone, but a proper `assertions` message beat re-adding
`readOnly` on top once the option had a real definition either way.)
'';
};
@ -507,6 +534,13 @@ in
};
config = lib.mkIf deployCfg.matrix.enable {
# The option's own value, defined explicitly rather than left on its
# bare `default` — `mkDefault` so a real override (an operator's, or
# the pre-rename shim's) still resolves cleanly rather than crashing
# eval; the `assertions` entry below is what actually rejects it, with
# a message naming the reason. See that option's own comment above.
services.hyperhive.deploy.matrix.registrationTokenFile = lib.mkDefault registrationTokenPath;
# Matrix's own gateway surface: the sub-domain vhost, the name the
# hive resolver answers for, and the Accept-header map that vhost's
# SPA fallback reads. All three are matrix knowledge and none of
@ -676,6 +710,27 @@ in
httpPort and has no such name.
'';
}
{
# mkDefault above lets an override resolve instead of crashing
# eval — this is the actual rejection, with a message that names
# the real fix instead of nixpkgs' generic conflicting-definition
# text. Covers both the current path and the pre-rename
# `swarm.matrix.registrationTokenFile` shim in one check, since
# both land on the same merged option.
assertion = deployCfg.matrix.registrationTokenFile == registrationTokenPath;
message = ''
services.hyperhive.deploy.matrix.registrationTokenFile is fixed at
${registrationTokenPath} and cannot be moved hive-c0re's Rust
side derives this same path independently and has no way to learn
an override, so moving it desyncs the two silently instead of
loudly.
Integrating an externally-managed registration token? Deliver it
by writing into ${registrationTokenPath} instead of pointing this
option elsewhere see glue-matrix-bao-token.nix, which does
exactly that from the swarm secret store.
'';
}
];
# One declaration, two readers. The homeserver knows its own callback

View file

@ -296,6 +296,21 @@ let
builtins.elem httpPort ports
&& matrixOldPath.containers.hive-matrix.bindMounts ? "/etc/matrix/register.token";
}
{
# registrationTokenFile lost its override capability entirely
# (bao-delivered secrets don't need one — glue-matrix-bao-token.nix
# already writes into the fixed path instead of moving it), unlike its
# five siblings in the same rename. `matrixOldPath` above proves the
# override still *resolves* (mkDefault, not a crash) — this proves it
# also gets *rejected*, by a named assertion rather than nixpkgs' generic
# conflicting-definition text. Reads `.assertions` directly (cheap: a
# list of `{assertion; message;}`, not `system.build.toplevel`) rather
# than forcing a real build just to observe a boolean.
name = "overriding registrationTokenFile (even via the pre-rename shim) trips a named assertion, not a silent desync";
ok = lib.any (
a: !a.assertion && lib.hasInfix "registrationTokenFile" a.message
) matrixOldPath.assertions;
}
{
# Reads the DELIVERY UNIT, not the options: `responderConfigured` gates
# whether it exists at all, and the seed path is interpolated into its