From 792d7f503f2d84b105e217b3eaaef7f76e432d75 Mon Sep 17 00:00:00 2001 From: atlas Date: Mon, 24 Aug 2026 22:12:43 +0200 Subject: [PATCH] forge, matrix: SSO is not optional Both services carried an `sso.enable` defaulting to false, so a swarm's own forge and homeserver shipped with their identity provider switched off unless an operator remembered two lines. Grafana never had the toggle and is the shape the other two now match. Behaves as if the setting were true: `ssoLocal` loses one conjunct, the three assertions become unconditional, and the login source and identity_provider render always. The option is removed rather than defaulted, so a config that turned SSO OFF fails where that line is instead of silently gaining a login provider on the next rebuild. --- docs/matrix.md | 20 +++-- docs/swarm/sso.md | 3 +- nix/host-modules/hive-forge/default.nix | 57 +++++++------- nix/host-modules/hive-matrix.nix | 98 ++++++++++++------------- 4 files changed, 94 insertions(+), 84 deletions(-) diff --git a/docs/matrix.md b/docs/matrix.md index dc361844..17f59636 100644 --- a/docs/matrix.md +++ b/docs/matrix.md @@ -220,14 +220,18 @@ surprising behaviour: `server_name` (treated as wildcard catch-all, surprising) and `/etc/hosts` (invalid entry). `null` is the right opt-out shape; empty string is rejected explicitly. -- **`sso.enable` requires `sso.clientSecretFile`** — fails at eval, - not at boot: tuwunel reads its identity providers from the config - file, so a half-configured one can stop the homeserver from - starting outright rather than merely hiding a login button. -- **`sso.enable` requires `swarm.authelia.url`** — without a - provider URL there is nothing to discover against. -- **`sso.enable` requires `gatewayHost != null`** — the SSO callback - URL is format-locked to `/_matrix/client/unstable/login/sso/callback/`, +SSO is unconditional, so the three below are requirements of running a +homeserver at all rather than of a setting: + +- **`sso.clientSecretFile` is required** — fails at eval, not at boot: + tuwunel reads its identity providers from the config file, so a + half-configured one can stop the homeserver from starting outright + rather than merely hiding a login button. On a host that also runs + the swarm's authelia it is wired up for you. +- **`swarm.authelia.url` is required** — without a provider URL there + is nothing to discover against. +- **`gatewayHost != null` is required** — the SSO callback URL is + format-locked to `/_matrix/client/unstable/login/sso/callback/`, and the identity provider needs a public name to redirect the browser to. diff --git a/docs/swarm/sso.md b/docs/swarm/sso.md index c78dab5e..53aad373 100644 --- a/docs/swarm/sso.md +++ b/docs/swarm/sso.md @@ -110,8 +110,7 @@ mechanism with flags. ### 1. All-local — one host runs both -Nothing to configure beyond `swarm.forge.sso.enable = true` or -`swarm.matrix.sso.enable = true`. Per service, a host-side unit waits for +Nothing to configure at all. Per service, a host-side unit waits for authelia's first boot to mint that client's secret and copies it into the service's container, and the service's own module contributes its client entry — callback URL included — to authelia's client list. diff --git a/nix/host-modules/hive-forge/default.nix b/nix/host-modules/hive-forge/default.nix index fc0499dd..edb3b94a 100644 --- a/nix/host-modules/hive-forge/default.nix +++ b/nix/host-modules/hive-forge/default.nix @@ -29,7 +29,7 @@ let # authelia, so the secret can be moved without an operator. The other # two cases (swarm side via swarmctl, remote hive) leave # `clientSecretFile` to be set explicitly — see docs/swarm/. - ssoLocal = cfg.sso.enable && autheliaCfg.enable; + ssoLocal = autheliaCfg.enable; # Where the plaintext lands inside the forge container. Under # /var/lib rather than /run: the forge may start before the delivery @@ -124,6 +124,16 @@ in [ "services" "hyperhive" "forge" ] [ "services" "hyperhive" "swarm" "forge" ] ) + (lib.mkRemovedOptionModule [ "services" "hyperhive" "swarm" "forge" "sso" "enable" ] '' + SSO is no longer optional: the forge always registers the swarm's + authelia as a login source. + + 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 provider on the next rebuild. Drop the line; if it was + false, set services.hyperhive.swarm.forge.sso.clientSecretFile and + services.hyperhive.swarm.authelia.url as the assertions describe. + '') ]; # The internal forge is mandatory — it's the canonical store for the @@ -348,23 +358,16 @@ in ''; }; + # The swarm's authelia is always registered as an OpenID Connect + # login source here — there is no toggle, for the same reason the + # forge itself has none. + # + # **Additive, never exclusive.** Forgejo keeps its local password + # database and gains an extra "sign in with" button; this does not + # disable local login. Deliberate: an identity provider that can take + # the forge offline when it hiccups is a worse forge than one with + # two ways in — which is also what makes always-on safe. sso = { - enable = lib.mkOption { - type = lib.types.bool; - default = false; - example = true; - description = '' - Register the swarm's authelia as an OpenID Connect login - source on this forge. - - **Additive, never exclusive.** Forgejo keeps its local - password database and gains an extra "sign in with" button; - this does not disable local login. Deliberate: an identity - provider that can take the forge offline when it hiccups is a - worse forge than one with two ways in. - ''; - }; - clientId = lib.mkOption { type = lib.types.str; default = "forgejo"; @@ -529,11 +532,12 @@ in { # Fail at EVAL, not at boot. The alternative failure is a login # button that always 401s, three layers from the missing file. - assertion = !cfg.sso.enable || cfg.sso.clientSecretFile != null; + assertion = cfg.sso.clientSecretFile != null; message = '' - services.hyperhive.swarm.forge.sso.enable requires - sso.clientSecretFile — the path (inside the forge container) - holding the OIDC client secret's plaintext. + The forge's SSO login source requires + services.hyperhive.swarm.forge.sso.clientSecretFile — the path + (inside the forge container) holding the OIDC client secret's + plaintext. On a hive that also runs the swarm's authelia this is wired up for you. Set it explicitly when authelia lives on another @@ -543,9 +547,9 @@ in { # Without a provider URL there is nothing to discover against, # and the rendered unit would ask `null/.well-known/…`. - assertion = !cfg.sso.enable || autheliaUrl != null; + assertion = autheliaUrl != null; message = '' - services.hyperhive.swarm.forge.sso.enable requires + The forge's SSO login source requires services.hyperhive.swarm.authelia.url — the base URL of the swarm's SSO provider. @@ -670,7 +674,10 @@ in (caTrust.trustBundle { inherit pkgs; name = "hive-forge"; - consumers = [ "forgejo" ] ++ lib.optional cfg.sso.enable "forgejo-sso-source"; + consumers = [ + "forgejo" + "forgejo-sso-source" + ]; }) ]; @@ -941,7 +948,7 @@ in # spelled out for the GPG key above: a stamp survives a state # wipe that took the thing it claims exists, and then suppresses # the repair. - systemd.services.forgejo-sso-source = lib.mkIf cfg.sso.enable { + systemd.services.forgejo-sso-source = { description = "register authelia as Forgejo's OIDC login source"; after = [ "forgejo.service" ]; requires = [ "forgejo.service" ]; diff --git a/nix/host-modules/hive-matrix.nix b/nix/host-modules/hive-matrix.nix index b2089c34..3b7aa588 100644 --- a/nix/host-modules/hive-matrix.nix +++ b/nix/host-modules/hive-matrix.nix @@ -31,7 +31,7 @@ let # authelia, so the secret can be moved without an operator. The other # two cases (swarm side, remote hive) leave `clientSecretFile` to be set # explicitly — same split the forge module documents. - ssoLocal = cfg.sso.enable && autheliaCfg.enable; + ssoLocal = autheliaCfg.enable; # Where the plaintext lands inside the matrix container. Under /var/lib # rather than /run: the homeserver may start before the delivery unit on @@ -163,6 +163,16 @@ in [ "services" "hyperhive" "matrix" ] [ "services" "hyperhive" "swarm" "matrix" ] ) + (lib.mkRemovedOptionModule [ "services" "hyperhive" "swarm" "matrix" "sso" "enable" ] '' + SSO is no longer optional: a homeserver that runs at all delegates + login to the swarm's authelia. + + 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 + services.hyperhive.swarm.authelia.url as the assertions describe. + '') ]; options.services.hyperhive.swarm.matrix = { @@ -429,37 +439,30 @@ in }; }; + # 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 = { - enable = lib.mkOption { - type = lib.types.bool; - default = false; - description = '' - Let this homeserver delegate login to the swarm's authelia, - as an OIDC relying party — matrix SSO (`m.login.sso`), an - extra flow offered alongside password login. - - ⚠️ 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 option makes - it a *client* of an external identity provider. The two - families 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. - 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. - ''; - }; - clientId = lib.mkOption { type = lib.types.str; default = "tuwunel"; @@ -610,11 +613,12 @@ 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.enable || cfg.sso.clientSecretFile != null; + assertion = cfg.sso.clientSecretFile != null; message = '' - services.hyperhive.swarm.matrix.sso.enable requires - sso.clientSecretFile — the path (inside the matrix container) - holding the OIDC client secret's plaintext. + This homeserver's SSO login flow requires + services.hyperhive.swarm.matrix.sso.clientSecretFile — the path + (inside the matrix container) holding the OIDC client secret's + plaintext. On a hive that also runs the swarm's authelia this is wired up for you. Set it explicitly when authelia lives on another @@ -624,9 +628,9 @@ in { # Without a provider URL there is nothing to discover against, and # the rendered config would name `null` as its issuer. - assertion = !cfg.sso.enable || autheliaUrl != null; + assertion = autheliaUrl != null; message = '' - services.hyperhive.swarm.matrix.sso.enable requires + This homeserver's SSO login flow requires services.hyperhive.swarm.authelia.url — the base URL of the swarm's SSO provider. @@ -638,9 +642,9 @@ in { # The callback URL must name the homeserver itself, and with no # gateway vhost there is no public name for it to be built from. - assertion = !cfg.sso.enable || cfg.gatewayHost != null; + assertion = cfg.gatewayHost != null; message = '' - services.hyperhive.swarm.matrix.sso.enable requires + This homeserver's SSO login flow requires services.hyperhive.swarm.matrix.gatewayHost. tuwunel's SSO callback URL is format-locked to @@ -931,11 +935,7 @@ in # Tuwunel's default suffix is " 💕" — suppress it so agent # display names are clean (just the agent name, no emoji). new_user_displayname_suffix = ""; - } - # `optionalAttrs`, not a key set to `[]`: with SSO off the - # rendered settings must be *exactly* what they were before - # this option existed, and an empty list is still a key. - // lib.optionalAttrs cfg.sso.enable { + # tuwunel's OIDC server and this list are the two ends of one # pipe: `oidc_native_auth` stays false (its default), which # upstream defines as "the OIDC server runs only to broker @@ -990,11 +990,11 @@ in # See `man systemd.exec` → LoadCredential. systemd.services.tuwunel.serviceConfig.LoadCredential = [ "registration_token:${toString cfg.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. - ++ lib.optional cfg.sso.enable "oidc_client_secret:${toString cfg.sso.clientSecretFile}"; + # 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}" + ]; environment.systemPackages = [ cfg.package ]; };