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 ]; };