Compare commits

...
Author SHA1 Message Date
atlas
a5210311bf feat(nix): the matrix server_name follows the swarm domain too
A swarm runs one homeserver, so its identity belongs to the swarm and
not to whichever hive happens to host it -- otherwise relocating the
container between hives reads as a different homeserver rather than a
move. `gatewayHost` moved for that reason a commit ago; `serverName`
was left behind, which made the identity hive-shaped and the routing
swarm-shaped.

⚠️ This is the one value on the page that a rebuild cannot undo.
`gatewayHost` is a routing detail clients rediscover through
`.well-known`; `server_name` is embedded in every user and room id, so
adopting a new one strands the existing accounts and rooms rather than
renaming them -- their ids still name a homeserver that stops
answering. Moving the DEFAULT is safe; moving a RUNNING deployment is
not, and existing hives must pin the value they already minted ids
under before rebuilding.

The fallback stays total on a null swarm domain for the same reason as
its neighbours: the required-domain assertion in hive-network.nix is
what should fire, not a coercion error from an unrelated option
interpolating null.

The legacy-pin eval probe now covers `serverName` as well. It existed
to answer "what do I set so old deployments don't change", and was
proving that only for the two values that are cheap to change -- the
irreversible one was the one it did not assert.
2026-08-09 17:32:44 +02:00
atlas
a1243fe04a docs: follow the swarm service names to the swarm domain
The three moves above falsify prose in five files, and none of it is
caught by a gate: clippy, cargo test and nix flake check read exactly
zero markdown.

Corrected where a doc stated a DEFAULT or enumerated the vhosts --
gateway (ACME requirements + the header table + the two Forgejo
reachability lines), matrix (gatewayHost, firewall rationale, agent
reachability), network (container table + the HTTP resolution bullet),
turn-loop config, dashboard.

Two mentions are deliberately left alone. `docs/tools/hivectl.md` says
the URL is read from the daemon "instead of assuming forge.<domain>" --
that sentence is about not assuming a shape and stays true. And
matrix.md's breaking-change note describes what `serverName` USED to
default to; it is history, correctly labelled, and rewriting it would
erase the record of an earlier migration.

The ACME line gained a requirement rather than a rename: with the
service names under the swarm domain, every one of those names has to
resolve to this host, not just the hive's own.
2026-08-09 17:32:44 +02:00
atlas
9a406c2046 feat(nix): authelia moves to auth.<swarm.domain>, cookie domain with it
Third of the three service moves, and the one that fails closed rather
than loudly. The vhost name and the session cookie's domain move in the
SAME commit because authelia validates `authelia_url` is a sub-domain of
the cookie `domain` at STARTUP -- move only the vhost and it does not
misbehave at first login, it refuses to boot.

No migration pin, unlike the forge and matrix: nothing depends on the
old name yet, so it moves outright.

Also gives the container the authelia binary, so an operator with a
shell can run `authelia crypto hash generate` for the users file. That
fix was written for a PR that was closed and is tracked nowhere else.

The accepted scope limit is recorded where the cookie is configured: a
hive keeping a domain outside the swarm's tree does not receive this
cookie, so SSO covers the swarm's services and not that hive's own
dashboard until its domain moves too.

⚠️ Nothing resolves `auth.<swarm.domain>` yet. The hive gateway is
deliberately NOT taught about it: swarm services get their vhost and
their name on the HOST, not inside a per-hive proxy container. Both
halves are the swarm-nginx work, and authelia has no vhost there either
way today.
2026-08-09 17:32:44 +02:00
atlas
ff84ca947d feat(nix): the matrix gateway host moves to chat.<swarm.domain>
Both halves change: the parent, because a swarm runs one homeserver and
every hive reaches it; and the label, because `chat` names the service
people use rather than the protocol it speaks.

⚠️ `serverName` is deliberately NOT touched, and the two are now
documented as the different things they are. `gatewayHost` is a routing
detail -- the API listener nginx proxies `/_matrix/*` to, which clients
rediscover through `.well-known`. `serverName` is the matrix identifier
baked into every user and room id: changing it is a different
homeserver, not a rename, so it still falls back to the bare hive
domain. A note at the fallback says so, since that binding is where a
future edit would most plausibly "fix" the inconsistency.

Old deployments pin `matrix.<hive domain>` -- exactly what the old
default rendered -- and dnsmasq already lists `gatewayHost` explicitly,
so a pinned or moved name stays routed either way.
2026-08-09 17:32:44 +02:00
atlas
727743507c feat(nix): the forge's default hostname moves under the swarm domain
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: `forge.<swarm.domain>` instead of `forge.<hive domain>`.

A deployment that was running before this keeps its current name with
one line -- `swarm.forge.domain = "forge.<hive domain>"` -- which is
exactly what the old default rendered, so pinning it is a no-op today
and freezes it against the new default.

Certificates follow either way: the swarm-services sub-CA is
name-constrained to the CONFIGURED names rather than to a fixed tree, so
a pinned legacy name is as issuable as the new default. DNS follows too
-- dnsmasq already lists `forge.domain` explicitly, precisely so a
cross-domain override stays routed.

The default stays total on a null swarm domain (`forge.invalid`) so the
required-domain assertion in hive-network.nix is what an operator sees,
rather than a coercion error naming this option.
2026-08-09 17:32:44 +02:00
8 changed files with 180 additions and 71 deletions

View file

@ -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`). 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.<domain>`, `matrix.<domain>`) gets its own cert via separate ACME challenges. **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.<swarm-domain>`, `chat.<swarm-domain>`) 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.
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). 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 host-side service — nixos-container is here for state + systemd-unit
isolation, not network isolation. Note this is the FORGE container; isolation, not network isolation. Note this is the FORGE container;
agent containers are network-isolated and reach the forge through the agent containers are network-isolated and reach the forge through the
gateway by `forge.<domain>` (see `HIVE_FORGE_URL` above), not via the gateway by `forge.<swarm-domain>` (see `HIVE_FORGE_URL` above), not via
host's loopback. the host's loopback.
State lives at `/var/lib/nixos-containers/hive-forge/var/lib/forgejo/` State lives at `/var/lib/nixos-containers/hive-forge/var/lib/forgejo/`
and survives container restart / host reboot. To wipe, destroy the 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 `openFirewall` (default **false**) controls whether `httpPort` and
`sshPort` are opened in the host firewall. Off by default (secure by `sshPort` are opened in the host firewall. Off by default (secure by
default): agents reach Forgejo through the gateway (`forge.<domain>` on default): agents reach Forgejo through the gateway (`forge.<swarm-domain>` on
the bridge), not the raw port, so no firewall hole is needed. Flip to the bridge), not the raw port, so no firewall hole is needed. Flip to
`true` when you need: `true` when you need:
- The operator's browser to reach `http://<host>:<httpPort>/` directly - The operator's browser to reach `http://<host>:<httpPort>/` directly
@ -542,7 +542,7 @@ ordering, preventing the subrequest from looping back through
## Security headers ## Security headers
The following headers are emitted at server scope on every gateway The following headers are emitted at server scope on every gateway
vhost (`_`, `forge.<domain>`, `matrix.<domain>`): vhost (`_`, `forge.<swarm-domain>`, `chat.<swarm-domain>`):
| Header | Value | | Header | Value |
|--------|-------| |--------|-------|

View file

@ -18,7 +18,7 @@ Same shape as [`gateway.md::hive-forge container shape`](gateway.md):
separate state dir. separate state dir.
- Container shares the host network namespace - Container shares the host network namespace
(`privateNetwork = false`) for state + systemd-unit isolation. Agents (`privateNetwork = false`) for state + systemd-unit isolation. Agents
reach the homeserver at `matrix.<domain>` via the gateway (agents reach the homeserver at `chat.<swarm-domain>` via the gateway (agents
run in private netns and can't access host loopback directly). run in private netns and can't access host loopback directly).
- Persistent state at - Persistent state at
`/var/lib/nixos-containers/hive-matrix/var/lib/matrix-tuwunel/` `/var/lib/nixos-containers/hive-matrix/var/lib/matrix-tuwunel/`
@ -33,27 +33,45 @@ Two distinct hostnames:
*irrevocably* in every `@user:<server_name>` and `!room:<server_name>` *irrevocably* in every `@user:<server_name>` and `!room:<server_name>`
identifier minted on this homeserver. Cannot be changed later identifier minted on this homeserver. Cannot be changed later
without abandoning every account and chat history. Defaults to the without abandoning every account and chat history. Defaults to the
bare `services.hyperhive.domain`; clients auto-discover the actual bare `services.hyperhive.swarm.domain`; clients auto-discover the
API endpoint via the `.well-known/matrix/{client,server}` routes actual API endpoint via the `.well-known/matrix/{client,server}`
the hive-gateway serves at that domain. routes the gateway serves at that domain.
- **`gatewayHost`** — the API listener hostname, where the gateway's - **`gatewayHost`** — the API listener hostname, where the gateway's
matrix vhost proxies `/_matrix/*` to tuwunel. Defaults to matrix vhost proxies `/_matrix/*` to tuwunel. Defaults to
`matrix.<services.hyperhive.domain>` (sub-domain shape). Set to `chat.<services.hyperhive.swarm.domain>`. Set to `null` to skip the
`null` to skip the gateway vhost (tuwunel stays direct on gateway vhost (tuwunel stays direct on `httpPort`).
`httpPort`).
**Breaking change**: `serverName` used to default to Both now default under the **swarm** domain, because a swarm runs one
`matrix.${services.hyperhive.domain}`. Existing homeservers must set homeserver: tying its identity to a single hive's domain would make
the option explicitly to preserve their existing user / room IDs relocating the container between hives look like a different
before rebuilding. The default flipped because the bare hive-domain homeserver.
makes for cleaner matrix IDs and `.well-known` delegation hides the
sub-domain from the user-facing identifier. ⚠️ **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}";
};
```
## Default-closed firewall ## Default-closed firewall
`openFirewall` defaults to `false` (secure-by-default): the host `openFirewall` defaults to `false` (secure-by-default): the host
reaches the homeserver on loopback, and agent containers reach it reaches the homeserver on loopback, and agent containers reach it
at `matrix.<domain>` via the gateway — so the firewall hole only at `chat.<swarm-domain>` via the gateway — so the firewall hole only
matters for access from *outside* the host. Flip to `true` when matters for access from *outside* the host. Flip to `true` when
announcing the homeserver to other hives or when an external matrix announcing the homeserver to other hives or when an external matrix
client needs to reach the client-server API directly. client needs to reach the client-server API directly.

View file

@ -46,7 +46,7 @@ untouched by any of it.
| container | netns | IPv4 | listens / reached via | | 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-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.<domain>` vhost | | `hive-forge` | host (shared) | host addresses | forgejo `:3000` http, `:2222` git-ssh; fronted by the `forge.<swarm-domain>` vhost |
| `hive-matrix` | host (shared) | host addresses | tuwunel `:8008` (+ optional federation port); fronted by the matrix 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 | | `hive-ci` | private, veth on bridge | DHCP pool | outbound only (runner → forge); no inbound surface |
| `h-<agent>` | private, veth on bridge | DHCP pool | web UI via UDS `/run/hive-agent/<name>` → nginx sub-path; in-container UI port hashed 81008999 | | `h-<agent>` | private, veth on bridge | DHCP pool | web UI via UDS `/run/hive-agent/<name>` → nginx sub-path; in-container UI port hashed 81008999 |
@ -58,8 +58,9 @@ The flows, end to end:
- **DNS** — agents query `bridgeIp:53`; hive zones are answered - **DNS** — agents query `bridgeIp:53`; hive zones are answered
authoritatively with the bridge IP, everything else forwards to the authoritatively with the bridge IP, everything else forwards to the
host's resolvers (see *Resolver behaviour* below). host's resolvers (see *Resolver behaviour* below).
- **HTTP**`forge.<domain>` / matrix / dashboard names all resolve - **HTTP**`forge.` and `chat.` (under `swarm.domain`) plus the hive's
to the bridge IP, land on nginx `:80`/`:443`, and proxy to forgejo own dashboard name 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 `:3000`, tuwunel `:8008`, hive-c0re `127.0.0.1:7000`, or a per-agent
UI unix socket. UI unix socket.
- **Internet egress** — agent default route points at the bridge IP; - **Internet egress** — agent default route points at the bridge IP;

View file

@ -157,7 +157,7 @@ evaluated outside a hive.
**`hyperhive.matrix.url`** — homeserver URL used by **`hyperhive.matrix.url`** — homeserver URL used by
`hive-matrix-daemon` when connecting via the matrix-sdk. hive-c0re `hive-matrix-daemon` when connecting via the matrix-sdk. hive-c0re
writes it into every agent at deploy time as the gateway-routed writes it into every agent at deploy time as the gateway-routed
`matrix.<domain>` URL, so isolated agents can reach the homeserver. `chat.<swarm-domain>` URL, so isolated agents can reach the homeserver.
Override per-agent when an agent should talk to a different 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 — for example a remote hive's tuwunel reached over a VPN, or an
external Matrix server for a federation-only agent. external Matrix server for a federation-only agent.

View file

@ -692,7 +692,7 @@ in once with the in-host tuwunel homeserver URL
(`http://localhost:8008` or whatever the matrix module exposes). (`http://localhost:8008` or whatever the matrix module exposes).
The unified nginx-front re-root to The unified nginx-front re-root to
`https://matrix.${hyperhive.domain}` + `.well-known/matrix/client` `https://chat.${hyperhive.swarm.domain}` + `.well-known/matrix/client`
auto-discovery lives in `docs/gateway.md` (atlas's lane). auto-discovery lives in `docs/gateway.md` (atlas's lane).
## FL0W page (`/flow.html`) ## FL0W page (`/flow.html`)

View file

@ -8,6 +8,7 @@ let
cfg = config.services.hyperhive.swarm.forge; cfg = config.services.hyperhive.swarm.forge;
gatewayCfg = config.services.hyperhive.gateway; gatewayCfg = config.services.hyperhive.gateway;
hyperhiveDomain = config.services.hyperhive.domain; hyperhiveDomain = config.services.hyperhive.domain;
swarmDomain = config.services.hyperhive.swarm.domain;
tlsCfg = config.services.hyperhive.tls; tlsCfg = config.services.hyperhive.tls;
# Self-signed gateway TLS: forgejo (Go) validates outbound webhook # Self-signed gateway TLS: forgejo (Go) validates outbound webhook
@ -120,10 +121,14 @@ in
domain = lib.mkOption { domain = lib.mkOption {
type = lib.types.str; type = lib.types.str;
# Total on a null hive domain so the required-domain assertion in # 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
# hive-network.nix is the thing that fires; see the comment there. # hive-network.nix is the thing that fires; see the comment there.
default = if hyperhiveDomain == null then "forge.invalid" else "forge.${hyperhiveDomain}"; default = if swarmDomain == null then "forge.invalid" else "forge.${swarmDomain}";
defaultText = lib.literalExpression ''"forge.''${services.hyperhive.domain}"''; defaultText = lib.literalExpression ''"forge.''${services.hyperhive.swarm.domain}"'';
example = "git.example.com"; example = "git.example.com";
description = '' description = ''
Public hostname for the forge. Doubles as both the forgejo Public hostname for the forge. Doubles as both the forgejo
@ -131,10 +136,16 @@ in
gateway vhost server-name when `behindGateway = true` gateway vhost server-name when `behindGateway = true`
(sub-domain routing see `docs/gateway.md`). (sub-domain routing see `docs/gateway.md`).
Defaults to `forge.''${services.hyperhive.domain}` (idiomatic Defaults to `forge.''${services.hyperhive.swarm.domain}` the
sub-domain shape `forge` labelled under the hive's bare swarm's domain, not this hive's, because a swarm runs **one**
domain). `services.hyperhive.domain` is required, so there's forge that every hive in it talks to.
always a domain to derive from.
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.
Set to a full hostname (`git.example.com`, Set to a full hostname (`git.example.com`,
`forge.internal.lan`, etc.) for a bespoke vhost shape the `forge.internal.lan`, etc.) for a bespoke vhost shape the

View file

@ -7,8 +7,29 @@
let let
cfg = config.services.hyperhive.swarm.matrix; cfg = config.services.hyperhive.swarm.matrix;
networkCfg = config.services.hyperhive.network; networkCfg = config.services.hyperhive.network;
hyperhiveDomain = config.services.hyperhive.domain; swarmDomain = config.services.hyperhive.swarm.domain;
effectiveServerName = if cfg.serverName != null then cfg.serverName else hyperhiveDomain; # 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";
# fluffychat-web build fixes: nixpkgs's `flutter341.buildFlutterApplication` # fluffychat-web build fixes: nixpkgs's `flutter341.buildFlutterApplication`
# skips the dart web-worker compile + the emscripten native_imaging # skips the dart web-worker compile + the emscripten native_imaging
@ -131,21 +152,32 @@ in
Matrix `server_name` the host part of every user ID Matrix `server_name` the host part of every user ID
(`@argus:<server_name>`) and room ID minted on this (`@argus:<server_name>`) and room ID minted on this
homeserver. CRITICAL: must be stable from day one because homeserver. CRITICAL: must be stable from day one because
it's embedded irrevocably in the identifiers. Defaults to it's embedded irrevocably in the identifiers.
`services.hyperhive.domain` (the bare hive domain). Combined
with the `.well-known/matrix/{client,server}` routes the Defaults to `services.hyperhive.swarm.domain` (the bare swarm
hive-gateway serves at that domain, clients auto-discover the 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
actual matrix endpoint without needing a subdomain. Override actual matrix endpoint without needing a subdomain. Override
here only if you need a different server_name shape (e.g. here only if you need a different server_name shape (e.g.
`matrix.<domain>` if you want the subdomain split, or
`chat.example.org` for a bespoke hostname). `chat.example.org` for a bespoke hostname).
**Breaking change**: this used to default to **Breaking change, and the one on this page that cannot be
`matrix.''${services.hyperhive.domain}`. matrix IDs embed undone by rebuilding.** This default has now moved twice from
the server_name irrevocably, so existing homeservers must `matrix.''${services.hyperhive.domain}`, then to the bare hive
set `services.hyperhive.swarm.matrix.serverName = "matrix.''${services.hyperhive.domain}";` domain, and now to the swarm domain. Every existing homeserver
explicitly to preserve their existing user / room IDs must pin whichever value it already minted ids under, e.g.
before rebuilding.
```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.
''; '';
}; };
@ -194,23 +226,38 @@ in
gatewayHost = lib.mkOption { gatewayHost = lib.mkOption {
type = lib.types.nullOr lib.types.str; type = lib.types.nullOr lib.types.str;
# Total on a null hive domain so the required-domain assertion in # `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
# hive-network.nix is the thing that fires; see the comment there. # hive-network.nix is the thing that fires; see the comment there.
default = if hyperhiveDomain == null then "matrix.invalid" else "matrix.${hyperhiveDomain}"; default = if swarmDomain == null then "chat.invalid" else "chat.${swarmDomain}";
defaultText = lib.literalExpression ''"matrix.''${services.hyperhive.domain}"''; defaultText = lib.literalExpression ''"chat.''${services.hyperhive.swarm.domain}"'';
example = "matrix.example.com"; example = "matrix.example.com";
description = '' description = ''
Public hostname for the matrix homeserver behind the gateway. Public hostname for the matrix homeserver behind the gateway.
Defaults to `matrix.''${services.hyperhive.domain}` (sub-domain Defaults to `chat.''${services.hyperhive.swarm.domain}` the
shape see `docs/gateway.md`). Set to `null` to skip the swarm's domain, because a swarm runs **one** homeserver. Set to
gateway vhost (tuwunel stays direct on `httpPort`). See `null` to skip the gateway vhost (tuwunel stays direct on
`docs/gateway.md` for the vhost map + matrix discovery flow, `httpPort`). See `docs/gateway.md` for the vhost map + matrix
and the federation port-8448 caveat at the bottom of that doc. discovery flow, and the federation port-8448 caveat at the
bottom of that doc.
Note: `gatewayHost` is the API listener hostname (where nginx **`gatewayHost` and `serverName` are different things, and
proxies `/_matrix/*`); `serverName` is the matrix-identifier they carry very different costs.** `gatewayHost` is the API
domain embedded irrevocably in user/room IDs (default = bare listener hostname (where nginx proxies `/_matrix/*`) and is
hive-domain). The two are distinct. 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.
''; '';
}; };

View file

@ -32,6 +32,7 @@ let
cfg = config.services.hyperhive.swarm.authelia; cfg = config.services.hyperhive.swarm.authelia;
hyperhiveCfg = config.services.hyperhive; hyperhiveCfg = config.services.hyperhive;
hyperhiveDomain = hyperhiveCfg.domain; hyperhiveDomain = hyperhiveCfg.domain;
swarmDomain = hyperhiveCfg.swarm.domain;
# Upstream's `services.authelia.instances.<name>` derives the unit, # Upstream's `services.authelia.instances.<name>` derives the unit,
# user, group and StateDirectory from the instance name # user, group and StateDirectory from the instance name
@ -41,10 +42,16 @@ let
unitName = "authelia-${instance}"; unitName = "authelia-${instance}";
stateDir = "/var/lib/${unitName}"; stateDir = "/var/lib/${unitName}";
# Total on a null hive domain for the same reason the option defaults # The SWARM's domain, because that is where the protected apps now live
# (`forge.<swarm>`, `chat.<swarm>`, `auth.<swarm>`). 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
# below are: the required-domain assertion in hive-network.nix should # below are: the required-domain assertion in hive-network.nix should
# be what an operator sees, not a coercion error from here. # be what an operator sees, not a coercion error from here.
cookieDomain = if hyperhiveDomain == null then "invalid" else hyperhiveDomain; cookieDomain = if swarmDomain == null then "invalid" else swarmDomain;
in in
{ {
options.services.hyperhive.swarm.authelia = { options.services.hyperhive.swarm.authelia = {
@ -85,15 +92,25 @@ in
domain = lib.mkOption { domain = lib.mkOption {
type = lib.types.str; type = lib.types.str;
# Total on a null hive domain so the required-domain assertion in # 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
# hive-network.nix is the thing that fires; see the comment there. # hive-network.nix is the thing that fires; see the comment there.
default = if hyperhiveDomain == null then "auth.invalid" else "auth.${hyperhiveDomain}"; default = if swarmDomain == null then "auth.invalid" else "auth.${swarmDomain}";
defaultText = lib.literalExpression ''"auth.''${services.hyperhive.domain}"''; defaultText = lib.literalExpression ''"auth.''${services.hyperhive.swarm.domain}"'';
example = "login.example.com"; example = "login.example.com";
description = '' description = ''
Public hostname for the SSO provider the sub-domain shape the Public hostname for the SSO provider the sub-domain shape the
forge and matrix already use. Doubles as the cookie domain's forge and matrix already use, under the swarm's domain because a
host, so it must be the name browsers actually visit. 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.
''; '';
}; };
@ -148,6 +165,13 @@ in
{ {
system.stateVersion = "26.05"; 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 # This container shares the host netns, so its own
# firewall.service would rewrite the HOST ruleset at every # firewall.service would rewrite the HOST ruleset at every
# boot. The host firewall owns all filtering. # boot. The host firewall owns all filtering.
@ -241,14 +265,22 @@ in
access_control.default_policy = "one_factor"; access_control.default_policy = "one_factor";
# The cookie domain is the hive's domain, NOT authelia's # The cookie domain is the SWARM's domain, NOT authelia's
# own host: the session cookie has to be sent to the apps # own host: the session cookie has to be sent to the apps
# being protected (`<hive>`, `forge.<hive>`, # being protected (`forge.<swarm>`, `chat.<swarm>`), and a
# `matrix.<hive>`), and a cookie scoped to `auth.<hive>` # cookie scoped to `auth.<swarm>` reaches none of them.
# reaches none of them. authelia enforces the relationship # authelia enforces the relationship from the other side
# from the other side too — `authelia_url` must be a # too — `authelia_url` must be a sub-domain of `domain`, so
# sub-domain of `domain`, so setting both to the same host # setting both to the same host fails validation at startup
# fails validation at startup rather than at first login. # 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.
session.cookies = [ session.cookies = [
{ {
domain = cookieDomain; domain = cookieDomain;