The operator admin sync now also takes homeserver admin away. It records each user id it sends `make-user-admin` for in `operator-matrix-admins.json` under the controller's state directory (written to a temporary file and renamed over it; a missing file is an empty set). On each pass, a recorded account that is no longer in authelia's `admins` group but is still joined to `#admins:<server>` is sent `!admin users force-leave-room <user> <admin room id>` as `@swarm`, over the same `/_matrix` path the grant uses. Leaving that room is what ends tuwunel admin. It never revokes an admin it didn't grant, `@swarm`, tuwunel's server user (`@conduit:<server>`), an agent's account (the store's `hive-agent-*` cert roles, the same roster the agent account mint uses), or anyone in the new `services.hyperhive.swarm.matrix.adminSync.ignoreUsers` list. That list reaches the daemon as `SWARM_CONTROLLER_MATRIX_ADMIN_SYNC_IGNORE_USERS`. It also never sends a revoke that would leave `#admins` with no member besides `@swarm` and the server user. A revoke is sent at most once per account per controller process, like a grant. A recorded account that is out of both the group and the room is dropped from the file. Closes #4799
220 lines
10 KiB
Nix
220 lines
10 KiB
Nix
# The swarm's matrix homeserver as every hive sees it: the name it mints ids
|
||
# under, the ports and 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.
|
||
'';
|
||
};
|
||
|
||
httpPort = lib.mkOption {
|
||
type = lib.types.port;
|
||
default = 8008;
|
||
description = ''
|
||
TCP port tuwunel serves the matrix client-server API on.
|
||
Default 8008 is the matrix-spec well-known port. Sits
|
||
outside hyperhive's claimed ranges (dashboard 7000, every
|
||
agent in 8100..8999 via FNV-1a hash). Federation listens on
|
||
`federationPort` separately.
|
||
'';
|
||
};
|
||
|
||
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 (tuwunel stays direct on
|
||
`httpPort`). 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.
|
||
'';
|
||
};
|
||
};
|
||
};
|
||
}
|