matrix: remove the registration token
Nothing reads it any more: hive-c0re creates accounts as the hive's appservice, so the mint, the host file, the bind mount, the `LoadCredential` entry and tuwunel's `registration_token_file` all go. ⚠️ `allow_registration` has to go to `false` in the same change, and not as hardening. tuwunel refuses to START when registration is allowed with no token configured — it demands `yes_i_am_very_very_sure_…_open_registration_…` instead — so dropping the token and leaving the flag true is not a lax homeserver, it is one that does not boot. The flag is checked only for requests arriving without an appservice token, so hive-c0re provisions exactly as before and everyone else is refused outright. The swarm secret store keeps its role, repointed at the credential that replaced the token (`swarm/hives/<hive>/matrix/appservice-token`). Its unit now also re-runs hive-matrix's own registration renderer after writing the file: the token is half an agreement, and a registration still naming the previous value authenticates nobody. The renderer is shared through an internal option rather than copied, so the registration's shape has one home. Both spellings of `registrationTokenFile` become `mkRemovedOptionModule` with a message naming what replaced them. A hive that never set the option — the default — is unaffected; one that pinned it fails to evaluate with instructions instead of a silent no-op. An upgraded hive needs no intervention: the activation script has both halves in place before the homeserver restarts, existing agents keep the tokens their devices already hold, and the old token file is left on disk read by nothing. docs/integrations/matrix.md spells the path out. Refs #4402
This commit is contained in:
parent
43cd8607ba
commit
7ee7080b21
11 changed files with 410 additions and 242 deletions
|
|
@ -90,39 +90,54 @@ by default. Reaching it on 8448 needs either an explicit tuwunel
|
|||
bind to that port OR a reverse-proxy + `.well-known/matrix/server`
|
||||
delegation (the latter lives in `gateway.md::Discovery flow`).
|
||||
|
||||
## Provisioning flow (registration token)
|
||||
## Provisioning flow (appservice)
|
||||
|
||||
Token-gated registration: hive-c0re holds the token, agents never
|
||||
see it. The agent only receives the resulting `access_token`.
|
||||
Registration is closed. Accounts are created by the hive's own
|
||||
**appservice**: hive-c0re holds the appservice token, agents never see
|
||||
it, and an agent only ever receives its own `access_token`.
|
||||
|
||||
1. **System activation** writes a 32-byte random hex token (64
|
||||
chars) to `services.hyperhive.deploy.matrix.registrationTokenFile`
|
||||
(`/var/lib/hyperhive/matrix-register-token` by default), mode
|
||||
`0600 root:root`, before any container start. Idempotent — only
|
||||
writes when the file is missing or empty; always re-applies 0600
|
||||
(normalises any 0640 / world-readable carry-over from
|
||||
pre-LoadCredential deployments). This runs at activation time
|
||||
(not first container start) to dodge a race where nspawn creates
|
||||
an empty file when the bind-mount target is missing and tuwunel
|
||||
reads `registration_token_file=""`, rejecting every registration
|
||||
until next restart.
|
||||
2. **Read-only bind-mount** maps the host file into the tuwunel
|
||||
The appservice has no URL (`url: null` in its registration), so the
|
||||
homeserver never calls out to it and there is no service to run. What the
|
||||
registration buys is an identity the homeserver recognises — which is why
|
||||
no secret has to be equal on both sides of the wire, and why account
|
||||
creation doesn't depend on registration being open to anyone who learns
|
||||
a token.
|
||||
|
||||
1. **System activation** mints a 32-byte random hex appservice token (64
|
||||
chars) at `/var/lib/hyperhive/matrix-appservice-token` and its
|
||||
spec-required `hs_token` sibling, mode `0600 root:root`, then renders
|
||||
the registration to
|
||||
`/var/lib/hyperhive/matrix-appservice/hyperhive.yaml` (also `0600`).
|
||||
The tokens are minted only when missing; the registration is
|
||||
re-rendered every time, because the token file can be overwritten in
|
||||
place by the swarm secret store and a registration naming a stale
|
||||
token authenticates nobody. Runs at activation time, before any
|
||||
container start, because the directory is bind-mounted and
|
||||
nixos-container refuses to start when a bind source is missing.
|
||||
2. **Read-only bind-mount** maps that directory into the tuwunel
|
||||
container at the same path.
|
||||
3. **systemd `LoadCredential=`** inside the container copies the
|
||||
bind-mounted file into
|
||||
`/run/credentials/tuwunel.service/registration_token`, owned by
|
||||
tuwunel's dynamic user with mode `0400`, at service start. The
|
||||
host file stays `root:root 0600` — no `chown :tuwunel` /
|
||||
`chmod 0640` / GID-pin gymnastics required. Keeps
|
||||
`DynamicUser = true` + `PrivateUsers = true` intact.
|
||||
4. tuwunel's `registration_token_file` points at the credentials
|
||||
path, not the original bind-mount path.
|
||||
5. **hive-c0re** uses the token to register each agent account via
|
||||
the matrix-spec UIAA registration flow, persists the returned
|
||||
`access_token` to `<agent-state>/matrix-token`. The agent's
|
||||
matrix MCP client authenticates with that access_token and
|
||||
never touches the shared registration token.
|
||||
6. **hive-c0re restarts `hive-matrix-daemon`** for the agent
|
||||
registration into
|
||||
`/run/credentials/tuwunel.service/hyperhive-appservice.yaml`, owned by
|
||||
tuwunel's dynamic user with mode `0400`, at service start. The host
|
||||
file stays `root:root 0600` — no `chown :tuwunel` / `chmod 0640` /
|
||||
GID-pin gymnastics required. Keeps `DynamicUser = true` +
|
||||
`PrivateUsers = true` intact.
|
||||
4. tuwunel's `appservice_dir` points at the credentials directory, not at
|
||||
the bind-mount path. It reads only `.yaml`/`.yml` entries from there,
|
||||
so the sibling credentials are invisible to it. The `.yaml` suffix on
|
||||
the credential id is what makes this work.
|
||||
5. **hive-c0re** reads the appservice token and creates each account with
|
||||
one `POST /register` typed `m.login.application_service`, persisting
|
||||
the returned `access_token` to `<agent-state>/matrix-token`. It never
|
||||
mints the token itself: the value has to be the one the rendered
|
||||
registration names, and only the nix side writes that.
|
||||
6. **An account that exists but has lost its token file** is re-tokened
|
||||
by an appservice `POST /login` — no password and no admin rights
|
||||
involved. A stored-password login and an admin-room password reset
|
||||
remain behind that, for accounts created before the appservice existed
|
||||
or named outside its namespace.
|
||||
7. **hive-c0re restarts `hive-matrix-daemon`** for the agent
|
||||
immediately after writing the token so the daemon picks up the
|
||||
new credential without waiting for a full container restart. If
|
||||
the restart fails (for example daemon not yet running on first boot)
|
||||
|
|
@ -130,16 +145,70 @@ see it. The agent only receives the resulting `access_token`.
|
|||
(`hive-matrix-daemon.path` watching for `matrix-token` appearance)
|
||||
brings the daemon up on the same boot cycle anyway.
|
||||
|
||||
### The admin account, and why it needs no first-user luck
|
||||
|
||||
`@hive:<server_name>` is the appservice's own `sender_localpart`, which
|
||||
the homeserver creates itself when it loads the registration — on a
|
||||
zero-user database, inside startup, before the HTTP listener accepts
|
||||
anything. Its **admin rights** then come from an explicit
|
||||
`make_user_admin`, run by tuwunel's `admin_execute` in the same startup
|
||||
and likewise before the listener — so a fresh hive has a joined,
|
||||
power-level-100 admin on its first boot.
|
||||
|
||||
This replaces a dependency on being the first account ever registered,
|
||||
which was fragile in both directions: an appservice-created account is
|
||||
excluded from that automatic grant by design, and on a homeserver that
|
||||
already had users the rule never fired at all.
|
||||
|
||||
Promotion can't be bootstrapped over the API, and that's upstream's
|
||||
design rather than a gap: tuwunel only treats an admin-room message as a
|
||||
command when its sender is already an admin. `admin_execute` is the one
|
||||
lever with no sender to check. hive-c0re re-checks the result on every
|
||||
sweep by reading the admin account's own joined-rooms list; if the rights
|
||||
are missing it says so, names
|
||||
`systemctl restart container@hive-matrix` as the fix, and carries on —
|
||||
agent accounts, the hive Space and the chat room need no admin.
|
||||
|
||||
<details><summary>Upgrading a hive that used the registration token</summary>
|
||||
|
||||
Nothing to do, and nothing to time. The activation script mints the
|
||||
appservice token and renders the registration before the homeserver
|
||||
restarts, so the first boot after the switch already has both halves.
|
||||
|
||||
- **Existing accounts keep working.** An access token lives on the
|
||||
device that minted it; removing the registration token touches no
|
||||
device, no account and no session. `login_with_password` stays on, so
|
||||
the password fallback is still there too.
|
||||
- **Existing token files are honoured.** The per-agent sweep skips any
|
||||
agent that already has a `matrix-token`, so no account is re-registered
|
||||
and no session is displaced.
|
||||
- **The admin account is already admin** on such a hive (it won the
|
||||
first-user grant when the hive was new), so the startup promotion is a
|
||||
no-op — upstream's `make_user_admin` short-circuits when the user is
|
||||
already joined at power level 100.
|
||||
- **`/var/lib/hyperhive/matrix-register-token` is left on disk**, read by
|
||||
nothing. Delete it or leave it; neither does any harm.
|
||||
- **`registrationTokenFile` is a removed option.** A config that still
|
||||
sets it fails to evaluate with a message naming the appservice — a hive
|
||||
that never set it (the default) is unaffected.
|
||||
- **A swarm store holding the old `matrix/registration-token` path** is
|
||||
no longer read at all; the hive uses its locally minted appservice
|
||||
token until someone `put`s a value at `matrix/appservice-token`.
|
||||
|
||||
</details>
|
||||
|
||||
Initial rollout settings:
|
||||
|
||||
- `allow_federation = true` at the protocol level so swarms can be
|
||||
wired up later by extending `trustedServers` without a homeserver
|
||||
restart. `trusted_servers = []` keeps it effectively closed
|
||||
until you list peers.
|
||||
- `allow_registration = true` (required for the token flow to
|
||||
engage). The absent
|
||||
`yes_i_am_very_very_sure_…_open_registration_…` flag keeps the
|
||||
server closed to anyone without the token.
|
||||
- `allow_registration = false`. tuwunel checks this flag only for
|
||||
requests that arrive **without** an appservice token, so hive-c0re
|
||||
provisions exactly as before and everyone else is refused. It's not a
|
||||
hardening afterthought: with no registration token configured,
|
||||
`allow_registration = true` makes tuwunel refuse to start unless
|
||||
`yes_i_am_very_very_sure_…_open_registration_…` is also set.
|
||||
- `allow_encryption` — server-side E2EE switch, sourced from
|
||||
`services.hyperhive.swarm.matrix.allowEncryption` (**default `false`**, opt-in).
|
||||
Off by default because on the hive-internal homeserver the operator
|
||||
|
|
|
|||
Loading…
Reference in a new issue