diff --git a/docs/gateway.md b/docs/gateway.md index c0c582ba..1d2c26e9 100644 --- a/docs/gateway.md +++ b/docs/gateway.md @@ -184,7 +184,7 @@ services.hyperhive.gateway = { nginx inside the gateway container obtains and auto-renews certs via the ACME HTTP-01 challenge on `port` (default 80). The gateway container shares the host network namespace (`privateNetwork = false`) so outbound ACME requests work without any extra routing. Certs are stored inside the container's persistent state dir (`/var/lib/acme/` inside `hive-gateway`; survives restarts because `ephemeral = false`). -**Requirements**: `services.hyperhive.domain` must be publicly DNS-resolvable to this host, and `openFirewall = true` so Let's Encrypt can reach `/.well-known/acme-challenge/`. Each active vhost (main domain, `forge.`, `chat.`) gets its own cert via separate ACME challenges — the swarm services default to names under `services.hyperhive.swarm.domain`, so **every one of those names must resolve to this host too**, not just the hive's own. +**Requirements**: `services.hyperhive.domain` must be publicly DNS-resolvable to this host, and `openFirewall = true` so Let's Encrypt can reach `/.well-known/acme-challenge/`. Each active vhost (main domain, `forge.`, `matrix.`) gets its own cert via separate ACME challenges. Mutual exclusion: `tls.certDir` set together with `tls.acme.enable = true` fails an assertion — pick one external TLS source (or neither, for the self-signed default). @@ -308,8 +308,8 @@ The forge container shares the host network namespace host-side service — nixos-container is here for state + systemd-unit isolation, not network isolation. Note this is the FORGE container; agent containers are network-isolated and reach the forge through the -gateway by `forge.` (see `HIVE_FORGE_URL` above), not via -the host's loopback. +gateway by `forge.` (see `HIVE_FORGE_URL` above), not via the +host's loopback. State lives at `/var/lib/nixos-containers/hive-forge/var/lib/forgejo/` and survives container restart / host reboot. To wipe, destroy the @@ -336,7 +336,7 @@ via `-p 2222`). Port 22 is left alone on the host for openssh. `openFirewall` (default **false**) controls whether `httpPort` and `sshPort` are opened in the host firewall. Off by default (secure by -default): agents reach Forgejo through the gateway (`forge.` on +default): agents reach Forgejo through the gateway (`forge.` on the bridge), not the raw port, so no firewall hole is needed. Flip to `true` when you need: - The operator's browser to reach `http://:/` directly @@ -542,7 +542,7 @@ ordering, preventing the subrequest from looping back through ## Security headers The following headers are emitted at server scope on every gateway -vhost (`_`, `forge.`, `chat.`): +vhost (`_`, `forge.`, `matrix.`): | Header | Value | |--------|-------| diff --git a/docs/matrix.md b/docs/matrix.md index 5c27216f..9e1efcd1 100644 --- a/docs/matrix.md +++ b/docs/matrix.md @@ -18,7 +18,7 @@ Same shape as [`gateway.md::hive-forge container shape`](gateway.md): separate state dir. - Container shares the host network namespace (`privateNetwork = false`) for state + systemd-unit isolation. Agents - reach the homeserver at `chat.` via the gateway (agents + reach the homeserver at `matrix.` via the gateway (agents run in private netns and can't access host loopback directly). - Persistent state at `/var/lib/nixos-containers/hive-matrix/var/lib/matrix-tuwunel/` @@ -33,45 +33,27 @@ Two distinct hostnames: *irrevocably* in every `@user:` and `!room:` identifier minted on this homeserver. Cannot be changed later without abandoning every account and chat history. Defaults to the - bare `services.hyperhive.swarm.domain`; clients auto-discover the - actual API endpoint via the `.well-known/matrix/{client,server}` - routes the gateway serves at that domain. + bare `services.hyperhive.domain`; clients auto-discover the actual + API endpoint via the `.well-known/matrix/{client,server}` routes + the hive-gateway serves at that domain. - **`gatewayHost`** — the API listener hostname, where the gateway's matrix vhost proxies `/_matrix/*` to tuwunel. Defaults to - `chat.`. Set to `null` to skip the - gateway vhost (tuwunel stays direct on `httpPort`). + `matrix.` (sub-domain shape). Set to + `null` to skip the gateway vhost (tuwunel stays direct on + `httpPort`). -Both now default under the **swarm** domain, because a swarm runs one -homeserver: tying its identity to a single hive's domain would make -relocating the container between hives look like a different -homeserver. - -⚠️ **They are still not interchangeable, and the difference is the -cost of changing one.** `gatewayHost` is a routing detail clients -rediscover through `.well-known`, so it is safe to move on a running -deployment. `serverName` is baked into every user and room id, so -adopting a new one does **not** rename the existing users and rooms — -it strands them, because their ids still name a homeserver that no -longer answers. - -**Breaking change — pin `serverName` before rebuilding.** Its default -has now moved twice: from `matrix.${services.hyperhive.domain}`, to -the bare hive domain, and now to the swarm domain. Any homeserver that -has already minted ids must name the value it minted them under: - -```nix -services.hyperhive.swarm.matrix = { - # whichever this deployment already uses - serverName = config.services.hyperhive.domain; - gatewayHost = "matrix.${config.services.hyperhive.domain}"; -}; -``` +**Breaking change**: `serverName` used to default to +`matrix.${services.hyperhive.domain}`. Existing homeservers must set +the option explicitly to preserve their existing user / room IDs +before rebuilding. The default flipped because the bare hive-domain +makes for cleaner matrix IDs and `.well-known` delegation hides the +sub-domain from the user-facing identifier. ## Default-closed firewall `openFirewall` defaults to `false` (secure-by-default): the host reaches the homeserver on loopback, and agent containers reach it -at `chat.` via the gateway — so the firewall hole only +at `matrix.` via the gateway — so the firewall hole only matters for access from *outside* the host. Flip to `true` when announcing the homeserver to other hives or when an external matrix client needs to reach the client-server API directly. diff --git a/docs/network.md b/docs/network.md index 54a15428..36aecaf2 100644 --- a/docs/network.md +++ b/docs/network.md @@ -46,7 +46,7 @@ untouched by any of it. | container | netns | IPv4 | listens / reached via | | -------------- | ----------------------- | -------------------- | -------------------------------------------------------------------------------------------- | | `hive-gateway` | host (shared) | host addresses | nginx `:80`/`:443` (every vhost); dnsmasq `bridgeIp:53` + DHCP `:67` on the bridge | -| `hive-forge` | host (shared) | host addresses | forgejo `:3000` http, `:2222` git-ssh; fronted by the `forge.` vhost | +| `hive-forge` | host (shared) | host addresses | forgejo `:3000` http, `:2222` git-ssh; fronted by the `forge.` vhost | | `hive-matrix` | host (shared) | host addresses | tuwunel `:8008` (+ optional federation port); fronted by the matrix vhost | | `hive-ci` | private, veth on bridge | DHCP pool | outbound only (runner → forge); no inbound surface | | `h-` | private, veth on bridge | DHCP pool | web UI via UDS `/run/hive-agent/` → nginx sub-path; in-container UI port hashed 8100–8999 | @@ -58,9 +58,8 @@ The flows, end to end: - **DNS** — agents query `bridgeIp:53`; hive zones are answered authoritatively with the bridge IP, everything else forwards to the host's resolvers (see *Resolver behaviour* below). -- **HTTP** — `forge.` and `chat.` (under `swarm.domain`) plus the hive's - own dashboard name resolve to the bridge IP, land on nginx - `:80`/`:443`, and proxy to forgejo +- **HTTP** — `forge.` / matrix / dashboard names all resolve + to the bridge IP, land on nginx `:80`/`:443`, and proxy to forgejo `:3000`, tuwunel `:8008`, hive-c0re `127.0.0.1:7000`, or a per-agent UI unix socket. - **Internet egress** — agent default route points at the bridge IP; diff --git a/docs/turn-loop/config.md b/docs/turn-loop/config.md index e02c5a45..0cfba598 100644 --- a/docs/turn-loop/config.md +++ b/docs/turn-loop/config.md @@ -157,7 +157,7 @@ evaluated outside a hive. **`hyperhive.matrix.url`** — homeserver URL used by `hive-matrix-daemon` when connecting via the matrix-sdk. hive-c0re writes it into every agent at deploy time as the gateway-routed -`chat.` URL, so isolated agents can reach the homeserver. +`matrix.` URL, so isolated agents can reach the homeserver. Override per-agent when an agent should talk to a different homeserver — for example a remote hive's tuwunel reached over a VPN, or an external Matrix server for a federation-only agent. diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index 3cfc0968..bc38230b 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -692,7 +692,7 @@ in once with the in-host tuwunel homeserver URL (`http://localhost:8008` or whatever the matrix module exposes). The unified nginx-front re-root to -`https://chat.${hyperhive.swarm.domain}` + `.well-known/matrix/client` +`https://matrix.${hyperhive.domain}` + `.well-known/matrix/client` auto-discovery lives in `docs/gateway.md` (atlas's lane). ## FL0W page (`/flow.html`) diff --git a/nix/host-modules/hive-forge/default.nix b/nix/host-modules/hive-forge/default.nix index 7a59a3ce..c50532ef 100644 --- a/nix/host-modules/hive-forge/default.nix +++ b/nix/host-modules/hive-forge/default.nix @@ -8,7 +8,6 @@ let cfg = config.services.hyperhive.swarm.forge; gatewayCfg = config.services.hyperhive.gateway; hyperhiveDomain = config.services.hyperhive.domain; - swarmDomain = config.services.hyperhive.swarm.domain; tlsCfg = config.services.hyperhive.tls; # Self-signed gateway TLS: forgejo (Go) validates outbound webhook @@ -121,14 +120,10 @@ in domain = lib.mkOption { type = lib.types.str; - # Under the SWARM domain, not this hive's: a swarm runs one forge - # and every hive in it reaches the same host, so the name belongs - # to the swarm rather than to whichever hive happens to run it. - # - # Total on a null swarm domain so the required-domain assertion in + # Total on a null hive domain so the required-domain assertion in # hive-network.nix is the thing that fires; see the comment there. - default = if swarmDomain == null then "forge.invalid" else "forge.${swarmDomain}"; - defaultText = lib.literalExpression ''"forge.''${services.hyperhive.swarm.domain}"''; + default = if hyperhiveDomain == null then "forge.invalid" else "forge.${hyperhiveDomain}"; + defaultText = lib.literalExpression ''"forge.''${services.hyperhive.domain}"''; example = "git.example.com"; description = '' Public hostname for the forge. Doubles as both the forgejo @@ -136,16 +131,10 @@ in gateway vhost server-name when `behindGateway = true` (sub-domain routing — see `docs/gateway.md`). - Defaults to `forge.''${services.hyperhive.swarm.domain}` — the - swarm's domain, not this hive's, because a swarm runs **one** - forge that every hive in it talks to. - - ⚠️ A deployment that was running before this moved keeps its - current name by pinning it here: - `forge.''${services.hyperhive.domain}`, which is exactly what - the old default rendered. Certificates follow either way: the - swarm-services sub-CA is name-constrained to the configured - names (see `./swarm-ca.nix`), not to a fixed tree. + Defaults to `forge.''${services.hyperhive.domain}` (idiomatic + sub-domain shape — `forge` labelled under the hive's bare + domain). `services.hyperhive.domain` is required, so there's + always a domain to derive from. Set to a full hostname (`git.example.com`, `forge.internal.lan`, etc.) for a bespoke vhost shape — the diff --git a/nix/host-modules/hive-matrix.nix b/nix/host-modules/hive-matrix.nix index f3b62abf..96aca15e 100644 --- a/nix/host-modules/hive-matrix.nix +++ b/nix/host-modules/hive-matrix.nix @@ -7,29 +7,8 @@ let cfg = config.services.hyperhive.swarm.matrix; networkCfg = config.services.hyperhive.network; - swarmDomain = config.services.hyperhive.swarm.domain; - # Falls back to the SWARM domain: a swarm runs one homeserver, so its - # identifier belongs to the swarm rather than to whichever hive happens - # to host it — otherwise moving the container between hives would look - # like a different homeserver. - # - # ⚠️ Changing this default is a BREAKING change in a way that moving - # `gatewayHost` was not: `serverName` is baked irrevocably into every - # user and room id, so a deployment that rebuilds onto a new one is a - # *different homeserver*, not a renamed one. Existing hives pin the old - # value explicitly (see the option's description); the default is what - # a fresh swarm gets. - # - # Total on a null swarm domain, deliberately: the required-domain - # assertion in hive-network.nix is what should fire, not a coercion - # error from an unrelated option interpolating null. - effectiveServerName = - if cfg.serverName != null then - cfg.serverName - else if swarmDomain != null then - swarmDomain - else - "invalid"; + hyperhiveDomain = config.services.hyperhive.domain; + effectiveServerName = if cfg.serverName != null then cfg.serverName else hyperhiveDomain; # fluffychat-web build fixes: nixpkgs's `flutter341.buildFlutterApplication` # skips the dart web-worker compile + the emscripten native_imaging @@ -152,32 +131,21 @@ in Matrix `server_name` — the host part of every user ID (`@argus:`) and room ID minted on this homeserver. CRITICAL: must be stable from day one because - it's embedded irrevocably in the identifiers. - - Defaults to `services.hyperhive.swarm.domain` (the bare swarm - domain), because **a swarm runs one homeserver** — tying its - identity to a single hive's domain would make relocating the - container between hives look like a different homeserver. - Combined with the `.well-known/matrix/{client,server}` routes - the gateway serves at that domain, clients auto-discover the + it's embedded irrevocably in the identifiers. Defaults to + `services.hyperhive.domain` (the bare hive domain). Combined + with the `.well-known/matrix/{client,server}` routes the + hive-gateway serves at that domain, clients auto-discover the actual matrix endpoint without needing a subdomain. Override here only if you need a different server_name shape (e.g. + `matrix.` if you want the subdomain split, or `chat.example.org` for a bespoke hostname). - **Breaking change, and the one on this page that cannot be - undone by rebuilding.** This default has now moved twice — from - `matrix.''${services.hyperhive.domain}`, then to the bare hive - domain, and now to the swarm domain. Every existing homeserver - must pin whichever value it already minted ids under, e.g. - - ```nix - services.hyperhive.swarm.matrix.serverName = - config.services.hyperhive.domain; # or "matrix.''${…domain}" - ``` - - before rebuilding. Adopting a new `server_name` does not rename - the old users and rooms — it strands them, because their ids - still name a homeserver that no longer answers. + **Breaking change**: this used to default to + `matrix.''${services.hyperhive.domain}`. matrix IDs embed + the server_name irrevocably, so existing homeservers must + set `services.hyperhive.swarm.matrix.serverName = "matrix.''${services.hyperhive.domain}";` + explicitly to preserve their existing user / room IDs + before rebuilding. ''; }; @@ -226,38 +194,23 @@ in gatewayHost = lib.mkOption { type = lib.types.nullOr lib.types.str; - # `chat.` under the SWARM domain — both halves change: a swarm runs - # one homeserver, and the label follows the service rather than the - # protocol. - # - # Total on a null swarm domain so the required-domain assertion in + # Total on a null hive domain so the required-domain assertion in # hive-network.nix is the thing that fires; see the comment there. - default = if swarmDomain == null then "chat.invalid" else "chat.${swarmDomain}"; - defaultText = lib.literalExpression ''"chat.''${services.hyperhive.swarm.domain}"''; + default = if hyperhiveDomain == null then "matrix.invalid" else "matrix.${hyperhiveDomain}"; + defaultText = lib.literalExpression ''"matrix.''${services.hyperhive.domain}"''; example = "matrix.example.com"; description = '' Public hostname for the matrix homeserver behind the gateway. - Defaults to `chat.''${services.hyperhive.swarm.domain}` — the - swarm's domain, because a swarm runs **one** homeserver. Set to - `null` to skip the gateway vhost (tuwunel stays direct on - `httpPort`). See `docs/gateway.md` for the vhost map + matrix - discovery flow, and the federation port-8448 caveat at the - bottom of that doc. + Defaults to `matrix.''${services.hyperhive.domain}` (sub-domain + shape — see `docs/gateway.md`). Set to `null` to skip the + gateway vhost (tuwunel stays direct on `httpPort`). See + `docs/gateway.md` for the vhost map + matrix discovery flow, + and the federation port-8448 caveat at the bottom of that doc. - ⚠️ **`gatewayHost` and `serverName` are different things, and - they carry very different costs.** `gatewayHost` is the API - listener hostname (where nginx proxies `/_matrix/*`) and is - free to change: it is a routing detail clients rediscover - through `.well-known`. `serverName` is the matrix-identifier - domain embedded **irrevocably** in every user and room id — - adopting a new one is a different homeserver, not a rename. - Both defaults now sit under the swarm domain, but only this - one is safe to move on a running deployment. - - A deployment that was running before this moved keeps its - current name by pinning - `matrix.''${services.hyperhive.domain}` here — exactly what the - old default rendered. + Note: `gatewayHost` is the API listener hostname (where nginx + proxies `/_matrix/*`); `serverName` is the matrix-identifier + domain embedded irrevocably in user/room IDs (default = bare + hive-domain). The two are distinct. ''; }; diff --git a/nix/host-modules/swarm-authelia.nix b/nix/host-modules/swarm-authelia.nix index 2522d9c3..f4f2ec87 100644 --- a/nix/host-modules/swarm-authelia.nix +++ b/nix/host-modules/swarm-authelia.nix @@ -32,7 +32,6 @@ let cfg = config.services.hyperhive.swarm.authelia; hyperhiveCfg = config.services.hyperhive; hyperhiveDomain = hyperhiveCfg.domain; - swarmDomain = hyperhiveCfg.swarm.domain; # Upstream's `services.authelia.instances.` derives the unit, # user, group and StateDirectory from the instance name @@ -42,16 +41,10 @@ let unitName = "authelia-${instance}"; stateDir = "/var/lib/${unitName}"; - # The SWARM's domain, because that is where the protected apps now live - # (`forge.`, `chat.`, `auth.`). It moves in the - # same commit as `domain` below and cannot lag it: authelia validates - # `authelia_url ⊂ cookie domain` at STARTUP, so a half-move does not - # misbehave at login — it refuses to boot. - # - # Total on a null swarm domain for the same reason the option defaults + # Total on a null hive domain for the same reason the option defaults # below are: the required-domain assertion in hive-network.nix should # be what an operator sees, not a coercion error from here. - cookieDomain = if swarmDomain == null then "invalid" else swarmDomain; + cookieDomain = if hyperhiveDomain == null then "invalid" else hyperhiveDomain; in { options.services.hyperhive.swarm.authelia = { @@ -92,25 +85,15 @@ in domain = lib.mkOption { type = lib.types.str; - # Under the SWARM domain, like the forge and matrix: a swarm has one - # SSO provider, and the session cookie has to reach the swarm's - # services. - # - # Total on a null swarm domain so the required-domain assertion in + # Total on a null hive domain so the required-domain assertion in # hive-network.nix is the thing that fires; see the comment there. - default = if swarmDomain == null then "auth.invalid" else "auth.${swarmDomain}"; - defaultText = lib.literalExpression ''"auth.''${services.hyperhive.swarm.domain}"''; + default = if hyperhiveDomain == null then "auth.invalid" else "auth.${hyperhiveDomain}"; + defaultText = lib.literalExpression ''"auth.''${services.hyperhive.domain}"''; example = "login.example.com"; description = '' Public hostname for the SSO provider — the sub-domain shape the - forge and matrix already use, under the swarm's domain because a - swarm has **one** SSO provider. Must be the name browsers - actually visit: it is the `authelia_url` the session cookie is - validated against. - - ⚠️ Unlike the forge and matrix names, this one carries **no - migration pin**: nothing depends on the previous - `auth.''${services.hyperhive.domain}` yet, so it moves outright. + forge and matrix already use. Doubles as the cookie domain's + host, so it must be the name browsers actually visit. ''; }; @@ -165,13 +148,6 @@ in { system.stateVersion = "26.05"; - # The authelia binary itself, so an operator who gets a shell - # in here can run `authelia crypto hash generate` to make a - # password for the users file. Without it the container runs - # authelia and cannot invoke it: the unit's ExecStart resolves - # through the store path, and nothing puts the CLI on PATH. - environment.systemPackages = [ cfg.package ]; - # This container shares the host netns, so its own # firewall.service would rewrite the HOST ruleset at every # boot. The host firewall owns all filtering. @@ -265,22 +241,14 @@ in access_control.default_policy = "one_factor"; - # The cookie domain is the SWARM's domain, NOT authelia's + # The cookie domain is the hive's domain, NOT authelia's # own host: the session cookie has to be sent to the apps - # being protected (`forge.`, `chat.`), and a - # cookie scoped to `auth.` reaches none of them. - # authelia enforces the relationship from the other side - # too — `authelia_url` must be a sub-domain of `domain`, so - # setting both to the same host fails validation at startup - # rather than at first login. - # - # ⚠️ Known and accepted consequence while a hive keeps a - # domain outside the swarm's tree: this cookie is NOT sent - # to that hive's own surfaces (its dashboard), so SSO - # covers the swarm's services and not the hive's. It - # resolves when the hive domain moves under the swarm - # domain; until then it is a scope limit, not a bug to - # chase. + # being protected (``, `forge.`, + # `matrix.`), and a cookie scoped to `auth.` + # reaches none of them. authelia enforces the relationship + # from the other side too — `authelia_url` must be a + # sub-domain of `domain`, so setting both to the same host + # fails validation at startup rather than at first login. session.cookies = [ { domain = cookieDomain;