From 67a20d387fb7aa8862da2e30ed38339e269f38a2 Mon Sep 17 00:00:00 2001 From: atlas Date: Tue, 11 Aug 2026 22:32:26 +0200 Subject: [PATCH 1/2] feat(3083): the gateway serves authelia MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit authelia has listened on 127.0.0.1:9091 since it was stood up, with nothing proxying to it — so `auth.` resolved and then refused the connection. This is the vhost that was never written. Follows forge and matrix exactly: one `optionalAttrs` attrset merged into `virtualHosts`, TLS chosen by `vhostTlsFor` (the swarm-services leaf already names it, since `swarm.serviceDomains` includes `authelia.domain`), and the same four wiring sites those two occupy — vhost, dnsmasq address, local-dev `/etc/hosts`, and the arg lists that feed both files. Gated on this host running the container, not on authelia being configured: every hive knows the swarm's `authelia.url`, but only the one serving it may claim the name. A client hive declaring this vhost would answer for a service it does not run. Two things that are deliberate rather than incidental: `X-Forwarded-{Proto,Host,Uri,For}` are set because authelia decides by the *original* request — the login redirect and the session cookie's domain both derive from them. Without them every request looks like it arrived at 127.0.0.1 over plain http. And no `auth_basic`. Applying the gateway's basic-auth block to the SSO provider would put the login page behind the login mechanism it exists to replace. --- nix/host-modules/hive-gateway/default.nix | 4 +++ nix/host-modules/hive-gateway/dnsmasq.nix | 4 ++- nix/host-modules/hive-gateway/vhosts.nix | 38 +++++++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/nix/host-modules/hive-gateway/default.nix b/nix/host-modules/hive-gateway/default.nix index 084937a5..92d5f7c9 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; + autheliaCfg = config.services.hyperhive.swarm.authelia; forgeCfg = config.services.hyperhive.swarm.forge; networkCfg = config.services.hyperhive.network; @@ -70,6 +71,7 @@ let cfg forgeCfg matrixCfg + autheliaCfg hyperhiveDomain dashboardDist swaggerUiTheme @@ -309,6 +311,7 @@ in networkCfg forgeCfg matrixCfg + autheliaCfg hyperhiveDomain ; }; @@ -332,6 +335,7 @@ in ++ lib.optional (config.services.hyperhive.swarm.forge.behindGateway or false ) config.services.hyperhive.swarm.forge.domain ++ lib.optional (matrixCfg.enable && matrixCfg.gatewayHost != null) matrixCfg.gatewayHost + ++ lib.optional autheliaCfg.enable autheliaCfg.domain ); }; }; diff --git a/nix/host-modules/hive-gateway/dnsmasq.nix b/nix/host-modules/hive-gateway/dnsmasq.nix index b212309a..52eabb4d 100644 --- a/nix/host-modules/hive-gateway/dnsmasq.nix +++ b/nix/host-modules/hive-gateway/dnsmasq.nix @@ -10,6 +10,7 @@ networkCfg, forgeCfg, matrixCfg, + autheliaCfg, hyperhiveDomain, }: { @@ -58,7 +59,8 @@ ++ lib.optional ((forgeCfg.behindGateway or false)) "/${forgeCfg.domain}/${networkCfg.bridgeIp}" ++ lib.optional ( matrixCfg.enable && matrixCfg.gatewayHost != null - ) "/${matrixCfg.gatewayHost}/${networkCfg.bridgeIp}"; + ) "/${matrixCfg.gatewayHost}/${networkCfg.bridgeIp}" + ++ lib.optional autheliaCfg.enable "/${autheliaCfg.domain}/${networkCfg.bridgeIp}"; # DHCP pool covering all usable host addresses on the bridge # subnet — bounds computed by hive-network.nix from # bridgeIp/bridgePrefixLength. All containers (agents and service diff --git a/nix/host-modules/hive-gateway/vhosts.nix b/nix/host-modules/hive-gateway/vhosts.nix index 91d5f484..20d9c97d 100644 --- a/nix/host-modules/hive-gateway/vhosts.nix +++ b/nix/host-modules/hive-gateway/vhosts.nix @@ -9,6 +9,7 @@ cfg, # services.hyperhive.gateway forgeCfg, matrixCfg, + autheliaCfg, # services.hyperhive.swarm.authelia hyperhiveDomain, dashboardDist, swaggerUiTheme, # nix/packages/swagger-ui-theme.nix: has index.html + hyperhive-theme.css @@ -123,6 +124,42 @@ let }; }; + # Authelia sub-domain vhost. `server_name = authelia.domain`, all of + # `/` → authelia. Empty attrset unless THIS host runs the container: + # every hive knows the swarm's `authelia.url`, but only the one + # serving it may claim the name — a client hive declaring this vhost + # would answer for a service it does not run. + # + # ⚠️ The server name must be exactly `autheliaCfg.domain`, not a + # near-miss: authelia validates `authelia_url ⊂ session cookie domain` + # at STARTUP, so a mismatch is a container that refuses to boot rather + # than a login that misbehaves. + # + # ⚠️ And deliberately NO `dashboardAuth` here. That block is the + # gateway's `auth_basic`; applying it to the SSO provider would put + # the login page behind the login mechanism it exists to replace. + autheliaVhost = lib.optionalAttrs autheliaCfg.enable { + "${autheliaCfg.domain}" = (vhostTlsFor autheliaCfg.domain) // { + listen = vhostListen; + extraConfig = securityHeaders; + locations."/" = { + proxyPass = "http://127.0.0.1:${toString autheliaCfg.port}/"; + proxyWebsockets = true; + extraConfig = '' + proxy_buffering off; + # authelia decides by the ORIGINAL request, not by the hop it + # sees — the login redirect and the session cookie's domain + # both derive from these. Without them every request looks + # like it arrived at 127.0.0.1 over plain http. + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Uri $request_uri; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + ''; + }; + }; + }; + # Matrix sub-domain vhost. `server_name = matrixCfg.gatewayHost`. # `/_matrix/*` → tuwunel (CORS *, 50M body cap, 1h long-poll # timeout). `/` serves fluffychat or 404 if GUI off. nginx @@ -409,5 +446,6 @@ in }; } // forgeVhost + // autheliaVhost // matrixVhost; } From 660629a7c6b86c8b537e8e9dc0075811ee4537df Mon Sep 17 00:00:00 2001 From: atlas Date: Tue, 11 Aug 2026 22:41:41 +0200 Subject: [PATCH 2/2] docs(3083): getting into the SSO provider the first time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The vhost half of this change is only useful with an account behind it, and the provider is generated with an empty user set on purpose. Document the `swarmctl user add` step rather than automating it: bootstrapping an IdP non-interactively means a secret arriving from a file, an env var or a nix expression, all worse than one command typed once. The gateway and network pages gain the rows they would otherwise be missing — vhost map, local-dev hosts entry, and the resolver's authoritative-name list. --- docs/gateway.md | 4 ++++ docs/network.md | 3 ++- docs/swarm/sso.md | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/docs/gateway.md b/docs/gateway.md index 6bf2df77..e569b0fc 100644 --- a/docs/gateway.md +++ b/docs/gateway.md @@ -14,6 +14,9 @@ Single nginx in front of every hyperhive web surface. Runs on the **host**, next | `matrix./_matrix/*` | `matrix.` | tuwunel (`8008`) | `matrix.gatewayHost != null` | | `matrix./` | `matrix.` | fluffychat-web static | `matrix.gui.enable` | | `matrix./config.json` | `matrix.` | inline JSON (FluffyChat boot config) | `matrix.gui.enable && domain != null` | +| `auth./` | `auth.` | authelia (`9091`) | `swarm.authelia.enable` | + +The authelia vhost is declared only by the host that **runs** authelia, not by every hive that uses it — a client hive knows the swarm's `authelia.url` but must not answer for a name it doesn't serve. Its server name is exactly `swarm.authelia.domain`: authelia validates `authelia_url ⊂ session cookie domain` at startup, so a near-miss is a container that refuses to boot. It carries no `auth_basic` — the login page must not sit behind the login mechanism it replaces — and sets the four `X-Forwarded-{Proto,Host,Uri,For}` headers, since authelia decides by the *original* request rather than the hop it sees. Per-agent UIs stay sub-path because they're hyperhive-internal and base-path-aware. External standard apps (forge / matrix) get sub-domains because their defaults work cleanly at sub-domain root + per-origin cookies / storage isolation matters. @@ -57,6 +60,7 @@ Each location carries a duplicated `auth_basic` block (separate locations don't - `` → `127.0.0.1` - `forge.` → `127.0.0.1` (when forge.behindGateway) - `matrix.` → `127.0.0.1` (when matrix.gatewayHost set) +- `auth.` → `127.0.0.1` (when swarm.authelia.enable) `lib.unique` de-dupes if any sub-domain happens to equal another entry. Operators with real DNS leave it off. diff --git a/docs/network.md b/docs/network.md index 1d2edbd9..f538bf95 100644 --- a/docs/network.md +++ b/docs/network.md @@ -113,7 +113,8 @@ schemes pick their own. ## Resolver behaviour dnsmasq is **authoritative** for the hive's own zones — answers -``, `forge.`, `matrix.` +``, `forge.`, `matrix.` and — +on the host running it — the swarm's `auth.` queries with the bridge IP (where nginx is reachable). Everything else is forwarded to the host's own resolvers: dnsmasq runs on the host and reads the host's `/etc/resolv.conf` directly. Containers don't need diff --git a/docs/swarm/sso.md b/docs/swarm/sso.md index 0c1c5b61..627120c4 100644 --- a/docs/swarm/sso.md +++ b/docs/swarm/sso.md @@ -10,6 +10,38 @@ The second role is derived rather than switched: on. authelia refuses to start with a provider that has no clients, so a separate `enable` would be a second fact free to disagree with the first. +## Getting in the first time + +authelia binds loopback only. The **gateway** on the host running it +publishes it as `auth.` — vhost, dnsmasq record and TLS +name all follow `swarm.authelia.enable`, so there is nothing to turn on +separately. (Details, including why a client hive must not declare that +vhost: [`../gateway.md`](../gateway.md).) + +Reachable is not the same as usable: the provider is generated with an +empty user set, deliberately. A provider with nobody in it yet is the +correct state for a fresh swarm — it is not a half-finished install, and +seeding a default account would be a credential in a config file. + +Add the first subject on the host running authelia: + +```console +# swarmctl user add mara --display-name Mara --email mara@example.com --group admins +added mara to /var/lib/authelia-swarm/users.yml +password: +this password is stored nowhere — record it now +``` + +The password is generated, hashed, and printed once; only the hash is +kept. `swarmctl` writes its canonical `users.json`, re-renders authelia's +`users.yml` from it, and restarts authelia. Full reference: +[`../tools/swarmctl-cli.md`](../tools/swarmctl-cli.md). + +This step stays manual on purpose. Bootstrapping an identity provider +non-interactively means a secret arriving from somewhere — a file, an +env var, a nix expression — and every one of those is worse than an +operator typing one command once. + ## What secrets exist, and where each one lives | secret | generated by | rests in | read by |