Each container now runs the journal forwarder the CI runner's, Grafana's and the log store's containers run, pushing to a receiver of its own on the swarm collector (serviceProducers hive-forge and hive-matrix). Each authenticates as a new machine OIDC client of its own (swarm.forge.otel.clientId, swarm.matrix.otel.clientId), registered by a glue file wherever authelia runs, minted by authelia and published to the store by the secret publisher. The forge's and the homeserver's browser login clients are unchanged. The client ids are swarm-forge-collector and swarm-matrix-collector, not hive-<x>-collector, which is the client id a hive named <x>-collector gets. The fetch unit inside each container reads the client secret with the store identity the host already holds for that service. The forge publisher's leaf is handed into the forge container as an nspawn system credential, as the grafana and log store containers do. matrix-ctl's leaf is already bound into the homeserver's container, so its forwarder reads it at those paths, as swarm-matrix-appservice-publish does; with no matrix-ctl identity the homeserver's forwarder is off. Each of the two policies gains one read stanza on its forwarder's client path. The host units that use those identities are unchanged. The two containers drop --link-journal=host, and the host collector stops reading them.
228 lines
10 KiB
Nix
228 lines
10 KiB
Nix
# The swarm's matrix homeserver as every hive sees it: the name it mints ids
|
||
# under, the URLs it is reached on, its encryption policy and the
|
||
# OIDC client id it is registered under, identical on every host. What the
|
||
# host running it decides, and the container itself, are in ./hive-matrix.nix.
|
||
{
|
||
lib,
|
||
config,
|
||
...
|
||
}:
|
||
let
|
||
cfg = config.services.hyperhive.swarm.matrix;
|
||
swarmDomain = config.services.hyperhive.swarm.domain;
|
||
in
|
||
{
|
||
options.services.hyperhive.swarm.matrix = {
|
||
serverName = lib.mkOption {
|
||
type = lib.types.nullOr lib.types.str;
|
||
default = null;
|
||
example = "chat.example.org";
|
||
description = ''
|
||
Matrix `server_name` — the host part of every user ID
|
||
(`@argus:<server_name>`) 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
|
||
actual matrix endpoint without needing a subdomain. Override
|
||
here only if you need a different server_name shape (e.g.
|
||
`chat.example.org` for a bespoke hostname).
|
||
|
||
**Changing this cannot be undone by rebuilding.** The default
|
||
is the swarm domain, so a homeserver that minted ids under
|
||
another value (`matrix.''${services.hyperhive.domain}` or the
|
||
bare hive domain) must pin that value, 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.
|
||
'';
|
||
};
|
||
|
||
apiUrl = lib.mkOption {
|
||
type = lib.types.nullOr lib.types.str;
|
||
default = if cfg.gatewayHost == null then null else "https://${cfg.gatewayHost}";
|
||
defaultText = lib.literalExpression ''
|
||
if services.hyperhive.swarm.matrix.gatewayHost == null then null
|
||
else "https://''${services.hyperhive.swarm.matrix.gatewayHost}"
|
||
'';
|
||
example = "https://matrix.example.com";
|
||
description = ''
|
||
Client-server API base URL **hive-c0re itself** uses to
|
||
provision matrix (register agent users, create the hive space
|
||
and chat room, invite members). Distinct from the agent-facing
|
||
`hyperhive.matrix.url`, which is the gateway vhost handed to
|
||
each agent's `hive-matrix-daemon`.
|
||
|
||
Defaults to `https://` on the homeserver's gateway vhost
|
||
(`gatewayHost`), the same name on every hive whichever host runs
|
||
tuwunel, and `null` when there is no vhost. hive-c0re logs in
|
||
with the hive's matrix sender token over this URL, so it crosses
|
||
the network whenever hive-c0re is not on the homeserver's host:
|
||
an `http://` value sends that token in cleartext. hive-c0re
|
||
verifies the gateway's certificate against the hive trust bundle
|
||
(system CAs plus the swarm and services roots).
|
||
|
||
`null` means hive-c0re has no homeserver to provision against
|
||
and matrix provisioning no-ops. There is deliberately no
|
||
fallback compiled into the daemon: an address baked into the
|
||
binary is one that builds fine and then talks to the wrong
|
||
machine.
|
||
'';
|
||
};
|
||
|
||
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
|
||
# 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}"'';
|
||
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, which leaves nothing able to
|
||
reach the homeserver. See `docs/networking/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 sit under the swarm domain, but only this
|
||
one is safe to change on a running deployment.
|
||
|
||
To use the per-hive name `matrix.''${services.hyperhive.domain}`
|
||
instead, pin it here.
|
||
'';
|
||
};
|
||
|
||
allowEncryption = lib.mkOption {
|
||
type = lib.types.bool;
|
||
default = false;
|
||
description = ''
|
||
Server-side switch for matrix end-to-end encryption — sets
|
||
tuwunel's `allow_encryption`. Off by default: on the hive-internal
|
||
homeserver the operator already controls the transport, so server
|
||
E2EE adds key-management overhead (cross-signing, device
|
||
verification, undecryptable-message recovery) without a clear
|
||
threat-model win for the common single-hive case. Turn on when
|
||
agents join encrypted rooms on external / federated homeservers,
|
||
or when the operator wants message contents opaque to the
|
||
homeserver admin. Independent of the agent matrix client, which
|
||
always supports decryption so it can read encrypted rooms it is
|
||
invited to regardless of this flag; this option only governs
|
||
whether THIS homeserver permits room encryption.
|
||
'';
|
||
};
|
||
|
||
adminSync.ignoreUsers = lib.mkOption {
|
||
type = lib.types.listOf (lib.types.strMatching "@[^:[:space:]]+:[^[:space:]]+");
|
||
default = [ ];
|
||
example = [ "@mara:example.org" ];
|
||
description = ''
|
||
Full matrix user ids `swarm-controller`'s operator admin sync never
|
||
revokes homeserver admin from. The sync revokes admin only from
|
||
accounts it made admin itself, once their owner leaves authelia's
|
||
`admins` group; an account listed here keeps admin regardless.
|
||
'';
|
||
};
|
||
|
||
# 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 = {
|
||
clientId = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "tuwunel";
|
||
description = ''
|
||
OAuth2 client id this homeserver identifies itself with. The host
|
||
running authelia registers it, whichever host runs the homeserver.
|
||
|
||
The homeserver's host reads the secret it pairs with out of the
|
||
swarm secret store, into
|
||
`services.hyperhive.deploy.matrix.sso.clientSecretFile`.
|
||
'';
|
||
};
|
||
|
||
redirectUri = lib.mkOption {
|
||
type = lib.types.nullOr lib.types.str;
|
||
readOnly = true;
|
||
default =
|
||
if cfg.gatewayHost == null then
|
||
null
|
||
else
|
||
"https://${cfg.gatewayHost}/_matrix/client/unstable/login/sso/callback/${cfg.sso.clientId}";
|
||
defaultText = lib.literalExpression ''"https://''${services.hyperhive.swarm.matrix.gatewayHost}/_matrix/client/unstable/login/sso/callback/''${services.hyperhive.swarm.matrix.sso.clientId}"'';
|
||
description = ''
|
||
SSO callback authelia sends the browser back to, and the URI it
|
||
matches **exactly**. Format-locked by tuwunel: the callback host
|
||
must point directly at the homeserver, and the path is fixed.
|
||
|
||
Read-only: the homeserver's config and its authelia client entry
|
||
both read it from here. `null` when `gatewayHost` is, because the
|
||
homeserver then has no name a browser can reach.
|
||
'';
|
||
};
|
||
};
|
||
|
||
otel.clientId = lib.mkOption {
|
||
type = lib.types.str;
|
||
readOnly = true;
|
||
# Not `hive-matrix-collector`: `hive-<name>` is a hive's own client id,
|
||
# and `matrix-collector` is a name a hive may take.
|
||
default = "swarm-matrix-collector";
|
||
description = ''
|
||
OAuth2 client id the journal forwarder inside the homeserver's
|
||
container authenticates as, and the audience it asks its token for.
|
||
Its own, not {option}`services.hyperhive.swarm.matrix.sso.clientId`,
|
||
which is the browser login's.
|
||
|
||
Read-only because three hosts have to agree on it: authelia
|
||
registers the client (`glue-matrix-otel-oidc-client.nix`), the
|
||
swarm collector admits it on the container's receiver
|
||
(`swarm.otel.serviceProducers`), and the secret publisher carries
|
||
its secret to the store.
|
||
'';
|
||
};
|
||
};
|
||
}
|