diff --git a/docs/tools/hivectl.md b/docs/tools/hivectl.md index d985d582..9c9fb8df 100644 --- a/docs/tools/hivectl.md +++ b/docs/tools/hivectl.md @@ -315,5 +315,5 @@ an error. A surface has no URL when it isn't browser-reachable: `home` needs `services.hyperhive.domain`; `forge` needs `services.hyperhive.deploy.forgejo.behindGateway = true`; `matrix` needs -`services.hyperhive.swarm.matrix.gui.enable = true`. In those cases the command +`services.hyperhive.deploy.matrix.gui.enable = true`. In those cases the command exits with a hint naming the option to set. diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index 116ae44a..77fc50a2 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -627,7 +627,7 @@ A static matrix web client (default `pkgs.fluffychat-web` rebuilt with `--base-href /matrix/`, swappable via `services.hyperhive.swarm.matrix.gui.package`) served by the hive-gateway nginx container at `/matrix/` when -`services.hyperhive.swarm.matrix.gui.enable` is on (defaults to +`services.hyperhive.deploy.matrix.gui.enable` is on (defaults to `matrix.enable`). c0re signals availability via the `HIVE_MATRIX_GUI_ENABLED` env var → `state.matrix_gui_enabled` in `/api/state`; the gateway does the actual static serving. diff --git a/hive-c0re/src/dashboard/state_snapshot.rs b/hive-c0re/src/dashboard/state_snapshot.rs index be12d782..32556ae3 100644 --- a/hive-c0re/src/dashboard/state_snapshot.rs +++ b/hive-c0re/src/dashboard/state_snapshot.rs @@ -75,7 +75,7 @@ pub(super) struct StateSnapshot { forge_present: bool, /// Whether the matrix GUI is reachable at `/matrix/`. Sourced from /// `HIVE_MATRIX_GUI_ENABLED` env var (set by the c0re NixOS module - /// when `services.hyperhive.swarm.matrix.gui.enable` is on). The gateway + /// when `services.hyperhive.deploy.matrix.gui.enable` is on). The gateway /// (`nix/host-modules/hive-gateway/vhosts.nix`) does the actual `/matrix/` static serving; /// this flag is just an availability signal for iris's dashboard /// chrome so the `M4TR1X →` tab doesn't flash when the GUI is off. diff --git a/hivectl/src/open.rs b/hivectl/src/open.rs index 16e366dd..4ebf7a56 100644 --- a/hivectl/src/open.rs +++ b/hivectl/src/open.rs @@ -30,7 +30,7 @@ pub(crate) async fn open_url(socket: &Path, target: OpenTarget) -> Result<()> { ), OpenTarget::Matrix => ( urls.matrix, - "the matrix GUI URL needs `services.hyperhive.swarm.matrix.gui.enable = true`", + "the matrix GUI URL needs `services.hyperhive.deploy.matrix.gui.enable = true`", ), }; let url = url.with_context(|| format!("no URL available for this surface — {hint}"))?; diff --git a/nix/host-modules/deploy.nix b/nix/host-modules/deploy.nix index 78d74f71..2ad3738f 100644 --- a/nix/host-modules/deploy.nix +++ b/nix/host-modules/deploy.nix @@ -130,6 +130,37 @@ in [ "services" "hyperhive" "deploy" "forgejo" "hostSwarmControllerTokenFile" ] ) + # The homeserver, split the same way. `swarm.matrix` keeps what it IS to + # every hive — its package, the name it answers to, the ports and URLs it + # is reached on, the client id it is registered under. These six are what + # the host running it decides. ⚠️ `enable` is NOT here: it was renamed + # earlier and its shim lives in ./hive-matrix.nix, so that file is the one + # to check before assuming this table is the whole matrix story. + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "swarm" "matrix" "openFirewall" ] + [ "services" "hyperhive" "deploy" "matrix" "openFirewall" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "swarm" "matrix" "trustedServers" ] + [ "services" "hyperhive" "deploy" "matrix" "trustedServers" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "swarm" "matrix" "maxRequestSize" ] + [ "services" "hyperhive" "deploy" "matrix" "maxRequestSize" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "swarm" "matrix" "registrationTokenFile" ] + [ "services" "hyperhive" "deploy" "matrix" "registrationTokenFile" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "swarm" "matrix" "gui" "enable" ] + [ "services" "hyperhive" "deploy" "matrix" "gui" "enable" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "swarm" "matrix" "sso" "clientSecretFile" ] + [ "services" "hyperhive" "deploy" "matrix" "sso" "clientSecretFile" ] + ) + # Retention is read only where the container is defined, so it is a # decision of the host running the store rather than something the swarm # agrees on. The two stores keep everything else — package, domain, port diff --git a/nix/host-modules/glue-matrix-bao-token.nix b/nix/host-modules/glue-matrix-bao-token.nix index fabdba5a..89f77cc2 100644 --- a/nix/host-modules/glue-matrix-bao-token.nix +++ b/nix/host-modules/glue-matrix-bao-token.nix @@ -31,7 +31,6 @@ let hyperhiveCfg = config.services.hyperhive; deployCfg = hyperhiveCfg.deploy; baoCfg = hyperhiveCfg.swarm.bao; - matrixCfg = hyperhiveCfg.swarm.matrix; baoDeploy = deployCfg.bao; @@ -48,8 +47,9 @@ let # A literal, not an option — ./hive-matrix.nix names its container # `containers.hive-matrix` directly and declares no `machine` to derive it # from, which the trust-bundle call in that file already says out loud. - # ⚠️ `matrixCfg.machine` parses fine and fails at module-system resolution, - # so this is the kind of mistake only reading the target module catches. + # ⚠️ A `swarm.matrix.machine` read parses fine and fails at module-system + # resolution, so this is the kind of mistake only reading the target module + # catches. matrixMachine = "hive-matrix"; in { @@ -115,8 +115,8 @@ in fi umask 077 - printf '%s\n' "$token" > ${lib.escapeShellArg (toString matrixCfg.registrationTokenFile)} - chmod 0600 ${lib.escapeShellArg (toString matrixCfg.registrationTokenFile)} + printf '%s\n' "$token" > ${lib.escapeShellArg (toString deployCfg.matrix.registrationTokenFile)} + chmod 0600 ${lib.escapeShellArg (toString deployCfg.matrix.registrationTokenFile)} ''; }; }; diff --git a/nix/host-modules/hive-c0re/environment.nix b/nix/host-modules/hive-c0re/environment.nix index 9882bead..5a56cf7b 100644 --- a/nix/host-modules/hive-c0re/environment.nix +++ b/nix/host-modules/hive-c0re/environment.nix @@ -200,7 +200,7 @@ in # one. HIVE_MATRIX_API_URL = config.services.hyperhive.swarm.matrix.apiUrl; } -// lib.optionalAttrs config.services.hyperhive.swarm.matrix.gui.enable { +// lib.optionalAttrs config.services.hyperhive.deploy.matrix.gui.enable { # Availability flags read by the dashboard's `/api/state`. # Matrix GUI lives entirely on the gateway nginx (matrix tab # only shows when both flags are on). Gateway routing detail: @@ -229,7 +229,7 @@ in // lib.optionalAttrs ( - config.services.hyperhive.swarm.matrix.gui.enable + config.services.hyperhive.deploy.matrix.gui.enable && config.services.hyperhive.swarm.matrix.gatewayHost != null ) { diff --git a/nix/host-modules/hive-gateway/default.nix b/nix/host-modules/hive-gateway/default.nix index c3130905..fe71e7f7 100644 --- a/nix/host-modules/hive-gateway/default.nix +++ b/nix/host-modules/hive-gateway/default.nix @@ -22,6 +22,7 @@ let # same list rather than each deciding what "a swarm service" means. swarmServiceDomains = config.services.hyperhive.swarm.serviceDomains; matrixCfg = config.services.hyperhive.swarm.matrix; + matrixDeployCfg = config.services.hyperhive.deploy.matrix; networkCfg = config.services.hyperhive.network; # Every vhost claiming `default_server`, ours and the operator's @@ -104,6 +105,7 @@ let cfg errorPages matrixCfg + matrixDeployCfg hyperhiveDomain dashboardDist swaggerUiTheme diff --git a/nix/host-modules/hive-gateway/vhosts.nix b/nix/host-modules/hive-gateway/vhosts.nix index 0a9d2752..7c4b1ce9 100644 --- a/nix/host-modules/hive-gateway/vhosts.nix +++ b/nix/host-modules/hive-gateway/vhosts.nix @@ -15,7 +15,8 @@ { lib, cfg, # services.hyperhive.gateway - matrixCfg, + matrixCfg, # services.hyperhive.swarm.matrix — what the homeserver IS + matrixDeployCfg, # services.hyperhive.deploy.matrix — what THIS host runs hyperhiveDomain, dashboardDist, swaggerUiTheme, # nix/packages/swagger-ui-theme.nix: has index.html + hyperhive-theme.css @@ -44,7 +45,8 @@ let # `/matrix/*` → 301 → `matrix./$1` (legacy deep-link # shim during the fluffychat sub-domain move). See `docs/networking/gateway.md`. matrixRedirectLocations = - lib.optionalAttrs (matrixCfg.enable && matrixCfg.gui.enable && matrixCfg.gatewayHost != null) + lib.optionalAttrs + (matrixDeployCfg.enable && matrixDeployCfg.gui.enable && matrixCfg.gatewayHost != null) ( let target = "${publicScheme}://${matrixCfg.gatewayHost}${publicPortSuffix}"; @@ -62,7 +64,7 @@ let # clients at `matrixCfg.gatewayHost` when set; falls back to direct # `:`. CORS `*` per matrix spec. The `m.server` # port-8448 carve-out is documented inline. See `docs/networking/gateway.md`. - wellKnownLocations = lib.optionalAttrs matrixCfg.enable ( + wellKnownLocations = lib.optionalAttrs matrixDeployCfg.enable ( let clientBaseUrl = if matrixCfg.gatewayHost != null then diff --git a/nix/host-modules/hive-matrix.nix b/nix/host-modules/hive-matrix.nix index cdeaf9ef..fe6e39e0 100644 --- a/nix/host-modules/hive-matrix.nix +++ b/nix/host-modules/hive-matrix.nix @@ -178,7 +178,7 @@ in Removed rather than defaulted to true so a config that turned it OFF fails here, where the line is, instead of silently gaining a login flow on the next rebuild. Drop the line; if it was false, - set services.hyperhive.swarm.matrix.sso.clientSecretFile and + set services.hyperhive.deploy.matrix.sso.clientSecretFile and services.hyperhive.swarm.authelia.url as the assertions describe. '') ]; @@ -312,6 +312,91 @@ in ''; }; + allowEncryption = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Server-side switch for matrix end-to-end encryption — sets + tuwunel's `allow_encryption`. Off by default: on the hive-internal + homeserver the operator already controls the transport, so server + E2EE adds key-management overhead (cross-signing, device + verification, undecryptable-message recovery) without a clear + threat-model win for the common single-hive case. Turn on when + agents join encrypted rooms on external / federated homeservers, + or when the operator wants message contents opaque to the + homeserver admin. Independent of the agent matrix client, which + always supports decryption so it can read encrypted rooms it is + invited to regardless of this flag; this option only governs + whether THIS homeserver permits room encryption. + ''; + }; + + gui = { + # Whether this host serves it is `deploy.matrix.gui.enable`. + package = lib.mkOption { + type = lib.types.package; + default = fluffychat-web-fixed; + defaultText = lib.literalMD '' + `pkgs.fluffychat-web` with a `postInstall` patch that adds + the three files `flutter341.buildFlutterApplication` skips. + ''; + description = '' + Static web client dist served at `matrix./`. Override + to swap fluffychat for hydrogen-web, cinny, element-web, or + an out-of-tree dist — any replacement is mounted at the + sub-domain root with the upstream-default ``, + no sub-path gymnastics needed. + ''; + }; + }; + + # This homeserver always delegates login to the swarm's authelia, as + # an OIDC relying party — matrix SSO (`m.login.sso`), offered + # alongside password login. No toggle: a homeserver in a swarm is a + # client of that swarm's identity provider. + # + # ⚠️ Not to be confused with tuwunel's `oidc_*` settings, which point + # the other way: those make this homeserver an *authorization server* + # for matrix clients. This family makes it a *client* of an external + # identity provider. The two share the protocol's name and answer + # opposite questions. + # + # This **adds** a way in. Password login keeps working: an identity + # provider that can take the homeserver offline when it hiccups is a + # worse homeserver than one with two ways in — which is also what + # makes always-on safe. Making authelia the *only* path is a + # separate, reversible switch (tuwunel's `login_with_password`), + # deliberately not folded in here. + # + # ⚠️ Matrix SSO lives **inside** the homeserver, never behind a + # forward-auth proxy: the client-server API is spoken by non-browser + # clients holding matrix access tokens — every agent's own + # `hive-matrix-daemon` — plus federation, and a proxy in front of + # `/_matrix/` breaks all of it. + sso = { + clientId = lib.mkOption { + type = lib.types.str; + default = "tuwunel"; + description = '' + OAuth2 client id this homeserver identifies itself with. Must + match the `id` of the corresponding entry in + `services.hyperhive.swarm.authelia.oidc.clients`. + + The secret it pairs with is a host path, so it lives at + `services.hyperhive.deploy.matrix.sso.clientSecretFile`. + ''; + }; + }; + }; + + # What stays above is what the homeserver IS from any hive's point of view: + # its package, the name it answers to, the ports and URLs it is reached on, + # and the client id it is registered under. What lives here is what the host + # running it decides — whether it is exposed, which peers it trusts, how large + # a request it accepts, and where its host-local secrets sit. Same rule as + # ./swarm-victorialogs.nix; `enable` already lives in ./deploy.nix, which also + # carries the renames. + options.services.hyperhive.deploy.matrix = { openFirewall = lib.mkOption { type = lib.types.bool; default = false; @@ -327,7 +412,7 @@ in **Breaking change**: this used to default to `true`. If you relied on the old default for external reach, add - `services.hyperhive.swarm.matrix.openFirewall = true;` to your host + `services.hyperhive.deploy.matrix.openFirewall = true;` to your host config before rebuilding. Note: federation (the matrix-spec well-known port 8448) is @@ -381,112 +466,43 @@ in ''; }; - allowEncryption = lib.mkOption { + gui.enable = lib.mkOption { type = lib.types.bool; - default = false; + default = deployCfg.matrix.enable; + defaultText = lib.literalExpression "config.services.hyperhive.deploy.matrix.enable"; description = '' - Server-side switch for matrix end-to-end encryption — sets - tuwunel's `allow_encryption`. Off by default: on the hive-internal - homeserver the operator already controls the transport, so server - E2EE adds key-management overhead (cross-signing, device - verification, undecryptable-message recovery) without a clear - threat-model win for the common single-hive case. Turn on when - agents join encrypted rooms on external / federated homeservers, - or when the operator wants message contents opaque to the - homeserver admin. Independent of the agent matrix client, which - always supports decryption so it can read encrypted rooms it is - invited to regardless of this flag; this option only governs - whether THIS homeserver permits room encryption. + Serve a matrix web client at `matrix.''${services.hyperhive.domain}/`. + Requires `swarm.matrix.gatewayHost != null` (default `matrix.` + when hive-domain set); the gateway itself always runs. When + off, the dashboard's `M4TR1X →` tab is hidden. See + `docs/networking/gateway.md` for the discovery flow that lets clients + auto-find the sub-domain. The client build itself is + `swarm.matrix.gui.package` — which client, as opposed to whether + this host serves it. ''; }; - gui = { - enable = lib.mkOption { - type = lib.types.bool; - default = deployCfg.matrix.enable; - defaultText = lib.literalExpression "config.services.hyperhive.deploy.matrix.enable"; - description = '' - Serve a matrix web client at `matrix.''${services.hyperhive.domain}/`. - Requires `matrix.gatewayHost != null` (default `matrix.` - when hive-domain set); the gateway itself always runs. When - off, the dashboard's `M4TR1X →` tab is hidden. See - `docs/networking/gateway.md` for the discovery flow that lets clients - auto-find the sub-domain. - ''; - }; + sso.clientSecretFile = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "/var/lib/tuwunel-oidc/tuwunel.secret"; + description = '' + Path **inside the matrix container** holding the client + secret's plaintext. - package = lib.mkOption { - type = lib.types.package; - default = fluffychat-web-fixed; - defaultText = lib.literalMD '' - `pkgs.fluffychat-web` with a `postInstall` patch that adds - the three files `flutter341.buildFlutterApplication` skips. - ''; - description = '' - Static web client dist served at `matrix./`. Override - to swap fluffychat for hydrogen-web, cinny, element-web, or - an out-of-tree dist — any replacement is mounted at the - sub-domain root with the upstream-default ``, - no sub-path gymnastics needed. - ''; - }; - }; + A path, never a value: an OIDC client secret has two holders + in two containers (authelia keeps a hash, this homeserver + needs the plaintext), and a literal written here would be + rendered into the world-readable nix store. - # This homeserver always delegates login to the swarm's authelia, as - # an OIDC relying party — matrix SSO (`m.login.sso`), offered - # alongside password login. No toggle: a homeserver in a swarm is a - # client of that swarm's identity provider. - # - # ⚠️ Not to be confused with tuwunel's `oidc_*` settings, which point - # the other way: those make this homeserver an *authorization server* - # for matrix clients. This family makes it a *client* of an external - # identity provider. The two share the protocol's name and answer - # opposite questions. - # - # This **adds** a way in. Password login keeps working: an identity - # provider that can take the homeserver offline when it hiccups is a - # worse homeserver than one with two ways in — which is also what - # makes always-on safe. Making authelia the *only* path is a - # separate, reversible switch (tuwunel's `login_with_password`), - # deliberately not folded in here. - # - # ⚠️ Matrix SSO lives **inside** the homeserver, never behind a - # forward-auth proxy: the client-server API is spoken by non-browser - # clients holding matrix access tokens — every agent's own - # `hive-matrix-daemon` — plus federation, and a proxy in front of - # `/_matrix/` breaks all of it. - sso = { - clientId = lib.mkOption { - type = lib.types.str; - default = "tuwunel"; - description = '' - OAuth2 client id this homeserver identifies itself with. Must - match the `id` of the corresponding entry in - `services.hyperhive.swarm.authelia.oidc.clients`. - ''; - }; - - clientSecretFile = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; - example = "/var/lib/tuwunel-oidc/tuwunel.secret"; - description = '' - Path **inside the matrix container** holding the client - secret's plaintext. - - A path, never a value: an OIDC client secret has two holders - in two containers (authelia keeps a hash, this homeserver - needs the plaintext), and a literal written here would be - rendered into the world-readable nix store. - - Required when `enable` is set — deliberately no fallback. A - homeserver that boots with SSO half-configured is worse than - one that fails to evaluate: tuwunel reads OIDC from its - config file rather than a database row, so a malformed block - can stop the server outright instead of merely hiding a - button. - ''; - }; + Deliberately no fallback — a homeserver that boots with SSO + half-configured is worse than one that fails to evaluate: + tuwunel reads OIDC from its config file rather than a database + row, so a malformed block can stop the server outright instead + of merely hiding a button. The id this pairs with is + `swarm.matrix.sso.clientId`, which is swarm-wide because it must + match authelia's register. + ''; }; }; @@ -520,7 +536,7 @@ in # page). See `services.hyperhive.swarm.controller.links`'s # description. services.hyperhive.swarm.controller.links = - lib.optional (cfg.gatewayHost != null && cfg.gui.enable) + lib.optional (cfg.gatewayHost != null && deployCfg.matrix.gui.enable) { label = "Matrix"; icon = "💬"; @@ -534,7 +550,7 @@ in # contributes instead of replacing it. # # The dashboard needs no equivalent — it routes by path. - services.nginx.appendHttpConfig = lib.optionalString cfg.gui.enable '' + services.nginx.appendHttpConfig = lib.optionalString deployCfg.matrix.gui.enable '' map $http_accept $matrix_spa_target { default "/__matrix_spa_no_html_fallback"; "~*text/html" "/index.html"; @@ -569,7 +585,7 @@ in ''; }; } - // lib.optionalAttrs cfg.gui.enable { + // lib.optionalAttrs deployCfg.matrix.gui.enable { # fluffychat at sub-domain root, SPA-fallback via the # Accept-header `$matrix_spa_target` map above. "/" = { @@ -588,7 +604,7 @@ in ''; }; } - // lib.optionalAttrs (!cfg.gui.enable) { + // lib.optionalAttrs (!deployCfg.matrix.gui.enable) { "/" = { return = "404"; }; @@ -618,10 +634,10 @@ in # Fail at EVAL, not at boot. tuwunel reads its identity providers # from the config file, so a half-configured one does not hide a # login button — it can stop the homeserver from starting at all. - assertion = cfg.sso.clientSecretFile != null; + assertion = deployCfg.matrix.sso.clientSecretFile != null; message = '' This homeserver's SSO login flow requires - services.hyperhive.swarm.matrix.sso.clientSecretFile — the path + services.hyperhive.deploy.matrix.sso.clientSecretFile — the path (inside the matrix container) holding the OIDC client secret's plaintext. @@ -683,7 +699,7 @@ in # Same case, same reasoning: this host minted the secret, so it can say # where the homeserver will find it. - services.hyperhive.swarm.matrix.sso.clientSecretFile = lib.mkIf ssoLocal ( + services.hyperhive.deploy.matrix.sso.clientSecretFile = lib.mkIf ssoLocal ( lib.mkDefault matrixSecretPath ); @@ -729,7 +745,7 @@ in set -euo pipefail src=${lib.escapeShellArg "${autheliaCfg.hostClientSecretDir}/${cfg.sso.clientId}.secret"} - dst=${lib.escapeShellArg "/var/lib/nixos-containers/hive-matrix${toString cfg.sso.clientSecretFile}"} + dst=${lib.escapeShellArg "/var/lib/nixos-containers/hive-matrix${toString deployCfg.matrix.sso.clientSecretFile}"} # authelia's container is up, but its first-boot generator may # still be minting. Bounded wait, then fail: a silent skip here @@ -811,7 +827,7 @@ in ); system.activationScripts.hive-matrix-register-token = lib.stringAfter [ "var" ] '' - tokenFile=${lib.escapeShellArg (toString cfg.registrationTokenFile)} + tokenFile=${lib.escapeShellArg (toString deployCfg.matrix.registrationTokenFile)} if [ ! -s "$tokenFile" ]; then mkdir -p "$(dirname "$tokenFile")" head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n' > "$tokenFile" @@ -833,8 +849,8 @@ in # Read-only bind of the host-managed registration token; tuwunel # reads it via systemd LoadCredential below (not directly). bindMounts = { - ${cfg.registrationTokenFile} = { - hostPath = cfg.registrationTokenFile; + ${deployCfg.matrix.registrationTokenFile} = { + hostPath = deployCfg.matrix.registrationTokenFile; isReadOnly = true; }; } @@ -923,11 +939,11 @@ in # `address` + `port` are upstream `listOf` — wrap singles. address = [ "0.0.0.0" ]; port = [ cfg.httpPort ]; - max_request_size = cfg.maxRequestSize; + max_request_size = deployCfg.matrix.maxRequestSize; # Federation enabled at the protocol level; empty # trustedServers keeps it effectively closed. allow_federation = true; - trusted_servers = cfg.trustedServers; + trusted_servers = deployCfg.matrix.trustedServers; # Token-gated registration. The absent # `yes_i_am_very_very_sure_…_open_registration_…` flag # keeps the server closed to anyone without the token. @@ -995,18 +1011,18 @@ in # host-side chown :tuwunel / GID-pin gymnastics needed. # See `man systemd.exec` → LoadCredential. systemd.services.tuwunel.serviceConfig.LoadCredential = [ - "registration_token:${toString cfg.registrationTokenFile}" + "registration_token:${toString deployCfg.matrix.registrationTokenFile}" # Same mechanism, second secret. tuwunel re-reads this file on # every OAuth exchange, not just at startup, so it has to # outlive the unit's start — a credentials path does. - "oidc_client_secret:${toString cfg.sso.clientSecretFile}" + "oidc_client_secret:${toString deployCfg.matrix.sso.clientSecretFile}" ]; environment.systemPackages = [ cfg.package ]; }; }; - networking.firewall = lib.mkIf cfg.openFirewall { + networking.firewall = lib.mkIf deployCfg.matrix.openFirewall { allowedTCPPorts = [ cfg.httpPort ]; diff --git a/nix/module-eval.nix b/nix/module-eval.nix index 9af2b083..301ce64c 100644 --- a/nix/module-eval.nix +++ b/nix/module-eval.nix @@ -89,6 +89,21 @@ let ]; }; + # The homeserver's turn to split. All six host-side options are set through + # their pre-rename paths — including `gui.enable`, whose value is deliberately + # the opposite of its default so the definition has to actually land. All six + # so that dropping any single shim entry fails the eval, not just the two the + # assertion reads. + matrixOldPath = hive { + deploy.matrix.enable = true; + swarm.matrix.openFirewall = true; + swarm.matrix.trustedServers = [ "matrix.example.invalid" ]; + swarm.matrix.maxRequestSize = 31457280; + swarm.matrix.registrationTokenFile = "/etc/matrix/register.token"; + swarm.matrix.gui.enable = false; + swarm.matrix.sso.clientSecretFile = "/etc/matrix/oidc.secret"; + }; + baoPkcs11 = hive { deploy.bao.enable = true; deploy.bao.seal = "pkcs11"; @@ -236,6 +251,21 @@ let in builtins.elem 3000 ports && builtins.any (m: m.dest == "mirrors/tool") seeded; } + { + # Third split, and the one whose readers were hardest to see: the + # registration token is read only through a `let` alias in another + # module, so no full path names it anywhere. Both arms read a rendered + # effect — the host firewall and the container's bind-mount table — so a + # rename that resolves but stops reaching the module still fails. + name = "a config written against the pre-rename matrix paths still opens the port and mounts the token"; + ok = + let + ports = matrixOldPath.networking.firewall.allowedTCPPorts; + httpPort = matrixOldPath.services.hyperhive.swarm.matrix.httpPort; + in + builtins.elem httpPort ports + && matrixOldPath.containers.hive-matrix.bindMounts ? "/etc/matrix/register.token"; + } { # The gateway's per-name issuer choice. If this ever collapses to a # constant, every swarm-service vhost serves a certificate its CA