Matrix and messaging bridges
Enable the Matrix server (Synapse), its Element web client and mautrix bridges (WhatsApp, Signal, Telegram, Messenger, Discord). For the architecture, see Matrix in DNF.
Enable Matrix
Section titled “Enable Matrix”-
Declare the services on the target host in
etc/config.yaml:etc/config.yaml services:matrix:title: "Matrix"description: "Messagerie instantanée"element: # client webturn: # relais audio/vidéo des clients historiques -
Declare the Matrix administrator (global key, outside hosts) :
etc/config.yaml matrix:admin: "alice" # local part du compte administrateurThis account manages the bridges and receives the supervision alerts.
-
Create the required secrets (see table below), then deploy.
Authentication goes through Kanidm SSO (OIDC) : every declared user signs in with their network account, no Matrix account creation is needed.
Required secrets
Section titled “Required secrets”To fill in in usr/secrets/secrets.yaml :
| Clé sops | Rôle | Génération |
|---|---|---|
matrix-rss-password | Registration shared secret Synapse | openssl rand -hex 32 |
oidc-secret-matrix | Secret du client OIDC Kanidm | cf. SSO |
turn-secret | Secret partagé coturn (si turn activé) | openssl rand -hex 32 |
livekit-secret | Secret partagé MatrixRTC (si matrixRtc activé) | just passwd-livekit |
mautrix-doublepuppet-as-token | Token appservice double puppeting (commun aux ponts) | openssl rand -hex 32 |
mautrix-doublepuppet-hs-token | Idem, requis par Synapse, jamais utilisé | openssl rand -hex 32 |
Two authentication modes
Section titled “Two authentication modes”The server runs in one of these two modes, set by
darkone.service.matrix.mas.enable. The chosen mode changes the compatible
clients, how to invite friends and the administration tools.
| Synapse historique (défaut) | MAS activé (recommandé) | |
|---|---|---|
| Option | mas.enable = false | mas.enable = true |
| Connexion SSO | Synapse est client OIDC de Kanidm | MAS est client OIDC de Kanidm |
| Element Web / Desktop | oui | oui |
| Element X (Android, iOS) | non | oui |
| Connexion par QR code | non | oui |
| Application proposée sur mobile | Element Classic | Element X |
| Portail de compte | aucun | https://matrix.domain.tld/account/ |
| Jetons d’inscription | API admin Synapse | dnf-mas sur l’hôte |
| Promotion administrateur | SQL dans la base Synapse | dnf-mas manage promote-admin |
Enabling MAS
Section titled “Enabling MAS”The Matrix Authentication Service 🡕 replaces Synapse’s internal authentication. For what it changes and how it fits in, see the architecture.
-
Generate the three dedicated secrets (idempotent, never overwrites existing) :
Fenêtre de terminal just passwd-massops key Role mas-encryption-secretMAS internal encryption mas-synapse-secretShared secret MAS ↔ Synapse mas-rsa-private-keyToken signing key -
Enable the option in the host’s nix config :
# usr/machines/<host>/default.nixdarkone.service.matrix.mas.enable = true; -
Deploy. On a new server, that’s done. On a server that already has accounts, continue with the migration without letting anyone sign in in between.
Migrating a server already in production
Section titled “Migrating a server already in production”MAS becomes the owner of accounts and sessions : they must be imported
with the official syn2mas tool, served by the host’s dnf-mas command.
Without this import, all accounts are invisible and everyone is signed out.
-
Back up, in this order (the dump must make it into the snapshot) :
Fenêtre de terminal sudo systemctl start postgresqlBackup.servicesudo systemctl start restic-backups-system-main.service -
Deploy the configuration with
mas.enable = true(see above). The maintenance window starts here. -
Stop both services :
syn2masrequires Synapse to be offline, and MAS must not provision accounts in the meantime.Fenêtre de terminal sudo systemctl stop matrix-synapse matrix-authentication-service -
Check, then run a dry run, then migrate :
Fenêtre de terminal sudo dnf-mas syn2mas checksudo dnf-mas syn2mas migrate --dry-runsudo dnf-mas syn2mas migrateThe report announces the recovered accounts, SSO links, access tokens and devices. The hundreds of “ignored” users are the bridges’ ghosts : that’s normal.
-
Restart, MAS first :
Fenêtre de terminal sudo systemctl start matrix-authentication-servicesudo systemctl start matrix-synapse
Registering friends
Section titled “Registering friends”In addition to SSO users, you can open token-based registration for local password accounts (the friends). Without a token, no registration is possible. The switch is the same in both modes, only how the token is produced differs.
# usr/machines/<host>/default.nixdarkone.service.matrix.friendRegistration.enable = true;-
Create a token on the Matrix host. Here valid once and 7 days (without options, the token is single-use and without expiry) :
Fenêtre de terminal sudo dnf-mas manage issue-user-registration-token --usage-limit 1 --expires-in 604800The response displays the token, for example
Created user registration token: 9SwPTGIFGQPa. Send it to the friend. -
The friend opens
https://matrix.domain.tld/registerand chooses password-based creation. -
He enters his username and password, then the token, requested at the next step.
-
He then signs in with this username / password pair from any client, Element X included.
-
Promote the
matrix.adminaccount to server admin (only once), on the Matrix host :Fenêtre de terminal sudo -u matrix-synapse psql matrix-synapse \-c "UPDATE users SET admin = 1 WHERE name = '@alice:domain.tld';" -
Retrieve an access token for the promoted account : in Element, Settings → Help & About → Advanced.
-
Create a registration token via the administration API :
Fenêtre de terminal curl -X POST \-A "Mozilla/5.0" \-H "Authorization: Bearer <ACCESS_TOKEN>" \-H "Content-Type: application/json" \-d '{"uses_allowed": 1, "expiry_time": null}' \https://matrix.domain.tld/_synapse/admin/v1/registration_tokens/newThe response contains the
tokento send to the friend. -
The friend registers in Element : Create account → server
domain.tld→ he pastes the token when it is requested.
Identifier collision with a declared user
Section titled “Identifier collision with a declared user”Friends and SSO users share a single namespace :
@bob:domain.tld belongs to only one account, regardless of its origin. And a
Matrix identifier is permanent — the users table of MAS enforces its
uniqueness, and deactivating an account does not free its identifier. No
renaming, no recycling.
The typical case : a friend registers as bob, then bob is declared
later as a DNF user in etc/config.yaml. Their first SSO connection then stops
on a User exists error page :
Upstream account provider returned "bob" as username, which is not linked tothat upstream account. Your homeserver does not allow linking an upstreamaccount to an existing accountCheck before declaring a user, on the Matrix host :
sudo -u postgres psql matrix-authentication-service \ -c "SELECT username FROM users WHERE lower(username) = lower('bob');"Two outcomes, depending on what you want to keep :
Links the Kanidm identity to the existing friend account, which keeps everything : rooms, history, encryption keys, bridges. This is almost always what you want, and the person concerned does it themselves.
-
First declare
bobinetc/config.yamland deploy, so that they get Kanidm credentials. -
Bob signs in on
https://matrix.domain.tld/account/with their friend password. -
In the same browser, they open the link account of the IDM provider :
https://matrix.domain.tld/upstream/authorize/01JDNF0000000000000KAN1DM0 -
They authenticate on Kanidm ; MAS then offers ” Link to your existing account ”, which they confirm.
Bob then signs in through SSO like any declared user. Their friend password stays valid : deleting it is done from the account portal.
If the friend account has nothing to save, it is simpler to declare the DNF
user under another login : the bob identifier is lost for good,
deactivating the friend account will not make it available.
Lock the old account to avoid confusion :
sudo dnf-mas manage lock-user bobManaging bridges
Section titled “Managing bridges”WhatsApp, Signal, Telegram and Messenger bridges are enabled by default with Matrix ; Discord is disabled by default. Per-host setting in the machine’s nix configuration :
# usr/machines/<host>/default.nixdarkone.service.matrix.bridges = { discord.enable = true; # opt-in messenger.enable = false; # disable a default bridge};Each bridge has its own secrets :
| Bridge | Sops keys | Note |
|---|---|---|
mautrix-whatsapp-as-token, mautrix-whatsapp-hs-token, mautrix-whatsapp-encryption-pickle-key | openssl rand -hex 32 each | |
| Signal | mautrix-signal-as-token, mautrix-signal-hs-token, mautrix-signal-encryption-pickle-key | openssl rand -hex 32 each |
| Messenger | mautrix-meta-as-token, mautrix-meta-hs-token, mautrix-meta-encryption-pickle-key | openssl rand -hex 32 each |
| Telegram | mautrix-telegram-api-id, mautrix-telegram-api-hash, mautrix-telegram-as-token, mautrix-telegram-hs-token | API id/hash to create on my.telegram.org 🡕 |
| Discord | mautrix-discord-as-token, mautrix-discord-hs-token | openssl rand -hex 32 each |
The as/hs tokens make each bridge’s appservice registration
deterministic (see
appservice tokens).
Permissions
Section titled “Permissions”Nothing to manage : any local account (@*:domain.tld) can use each
bridge with their own remote account, without interference between
users. The matrix.admin account has the bot administration
commands (help in direct conversation with the bot for the
list).
Double puppeting
Section titled “Double puppeting”Bridges share a doublepuppet appservice (token
mautrix-doublepuppet-as-token) : messages sent from the
phone appear in Matrix as coming from the user’s real account.
This is automatic for all local accounts, no action per
user.
Audio and video calls
Section titled “Audio and video calls”Two stacks coexist, because neither covers all clients. They activate separately and do not replace each other :
| WebRTC historique | MatrixRTC | |
|---|---|---|
| Option | darkone.service.turn.enable | darkone.service.matrix.matrixRtc.enable |
| Composants | coturn | LiveKit (SFU) + service d’autorisation |
| Appels à deux | oui | oui |
| Appels de groupe | non | oui |
| Element Classic (Android) | oui | non |
| Element Web / Desktop | oui | oui |
| Element X (Android, iOS) | non | oui |
Keep both enabled : Element Classic only speaks the first, Element X only the second.
-
Enable the MatrixRTC stack in the host’s nix config :
# usr/machines/<host>/default.nixdarkone.service.matrix = {mas.enable = true; # requis par Element X pour se connectermatrixRtc.enable = true; # requis par Element X pour appeler}; -
Generate the shared secret, then deploy :
Fenêtre de terminal just passwd-livekitjust apply <host> switch -
Check that the server announces a transport :
Fenêtre de terminal # doit contenir "org.matrix.msc4143.rtc_foci"curl -A Mozilla/5.0 https://domain.tld/.well-known/matrix/client# org.matrix.msc4143 doit valoir truecurl -A Mozilla/5.0 https://matrix.domain.tld/_matrix/client/versions
No extra subdomain or certificate : the SFU and its authorization service
are served on the Matrix vhost, under /livekit/sfu and
/livekit/jwt. Nothing to configure client-side either — Element X and
Element Web embed Element Call and discover the transport by themselves.
Media ports
Section titled “Media ports”Media does not go through the reverse proxy : clients connect to the host directly. These ports must therefore be open up to it, and forwarded if it sits behind NAT :
| Port | Usage |
|---|---|
| 40000-40100/udp | WebRTC media, one port per participant |
| 7881/tcp | Fallback for networks that block UDP |
Opening the network to federation
Section titled “Opening the network to federation”By default, the server federates with all Matrix servers, while remaining
invisible in searches (directories not exposed). To control the scope, set
darkone.service.matrix.federation in the host’s nix config:
# usr/machines/<host>/default.nixdarkone.service.matrix.federation = { enable = true; # false = federation fully blocked whitelist = [ ]; # empty = open ; otherwise whitelist};| Goal | Setting |
|---|---|
| Isolated network (no federation) | enable = false; |
| Open to the entire Matrix ecosystem | enable = true; whitelist = [ ]; |
| Restricted to trusted servers | enable = true; whitelist = [ "ami.org" "matrix.org" ]; |
Verify after deployment:
-
Test incoming federation with federationtester.matrix.org 🡕 on
domain.tld→ all green lights. -
With a whitelist, an unlisted server is denied (Synapse log:
Federation denied), a listed domain works.
Public rooms with approval (knock)
Section titled “Public rooms with approval (knock)”A public room in the Matrix sense can be joined freely: no validation. For a room that is “joinable but on approval”, use the knock access rule — a per-room setting, on the client side, no server configuration needed.
-
In Element: room → Room Settings → Security & Privacy.
-
Room access → “Ask to join” (knock).
-
A user (local or federated remote) requests access; a moderator approves or denies.
Operations
Section titled “Operations”| Action | Commande |
|---|---|
| État des services | systemctl status matrix-synapse mautrix-whatsapp mautrix-signal mautrix-telegram mautrix-meta-messenger |
| Journaux d’un pont | journalctl -u mautrix-whatsapp -f |
| Journaux Synapse | journalctl -u matrix-synapse -f |
| Journaux MAS | journalctl -u matrix-authentication-service -f |
| Journaux MatrixRTC | journalctl -u livekit -u lk-jwt-service -f |
Administering accounts with MAS
Section titled “Administering accounts with MAS”dnf-mas wraps the official mas-cli tool on the Matrix host : it
reconstructs the service’s configuration and secrets, and therefore works
whether the service is stopped or started. Always with sudo.
| Action | Command |
|---|---|
| Create a registration token | sudo dnf-mas manage issue-user-registration-token |
| Promote an administrator | sudo dnf-mas manage promote-admin alice |
| List administrators | sudo dnf-mas manage list-admin-users |
| Create an account (bot, service) | sudo dnf-mas manage register-user |
| Change a password | sudo dnf-mas manage set-password alice |
| Close an account’s sessions | sudo dnf-mas manage kill-sessions alice |
| Lock an account | sudo dnf-mas manage lock-user alice |
| Full diagnostic | sudo dnf-mas doctor |
Diagnosing a refused SSO connection
Section titled “Diagnosing a refused SSO connection”After a migration, already open sessions keep working : a broken connection is therefore only visible on the first fresh login. Follow MAS during an attempt :
journalctl -u matrix-authentication-service -f| Message | Cause | Fix |
|---|---|---|
wrong signature alg on /upstream/callback/… | MAS expects an RS256 id_token, Kanidm signs with ES256 | id_token_signed_response_alg of the upstream provider : set to ES256 by the module |
client registration denied by the policy: invalid redirect_uri | The client_uri advertised by the client does not match its redirect scheme | Do not force oidc_metadata in the Element config ; its default values pass |
M_UNRECOGNIZED on /login/sso/redirect | The path goes to Synapse instead of MAS | Check the @masCompat matcher of the Caddy vhost |
User exists page : ” returned “bob” as username, which is not linked “ | A friend account already holds the declared user’s identifier | Merge or rename (cf. identifier collision) |
| ” The QR code is not supported ” on the client side | The MSC4108 rendezvous channel is missing | experimental_features.msc4108_enabled : enabled by the module with MAS |
” The call is not supported ”, MISSING_MATRIX_RTC_TRANSPORT | No MatrixRTC backend announced | Enable matrixRtc.enable (cf. Audio and video calls) |
| Call rings but stays silent, or drops after a few seconds | The media ports do not reach the host | Open 40000-40100/udp and 7881/tcp up to it |
Quick checks, without signing in :
# org.matrix.msc4108 must be truecurl -A Mozilla/5.0 https://matrix.domain.tld/_matrix/client/versions
# must answer 201 and a public urlcurl -A Mozilla/5.0 -X POST --data probe \ https://matrix.domain.tld/_matrix/client/unstable/org.matrix.msc4108/rendezvousDiagnosing a silent bot
Section titled “Diagnosing a silent bot”A bot that does not reply to help while its service is running almost
always falls into one of two cases : its tokens no longer match the
registration loaded by Synapse, or it receives messages but cannot
decrypt them. Diagnose in order :
-
Is the service actually running ?
Fenêtre de terminal systemctl status mautrix-<bridge>journalctl -u mautrix-<bridge> -n 50If it keeps restarting with « The as_token was not accepted », Synapse’s in-memory registration no longer matches the bridge’s tokens (typical after a reset or migration, see the box above) :
systemctl restart matrix-synapse, then restart the bridge. -
Does the message reach the bridge ? Follow Synapse while sending a
helpto the bot :Fenêtre de terminal journalctl -u matrix-synapse -f | grep transactionsA
PUT .../_matrix/app/v1/transactions/... 200line should appear. If nothing goes out, the appservice is not loaded : check for the registration file in/var/lib/mautrix-<bridge>/and restart Synapse. -
Can the bridge decrypt it ? If the transaction goes through (200) but the bot stays silent, follow its logs while sending a new message :
Fenêtre de terminal journalctl -u mautrix-<bridge> -fLook for
decrypt,session,verification,dropping. A decryption failure means the room’s session key has not been shared with the bot’s current device (typical after a bridge reset : its e2ee identity has changed). -
Recreate the conversation. Leave the direct message with the bot and start a new one : the client establishes a fresh encryption session and shares the key with the bot’s current device. Also check that your own Element session is verified.
-
Last resort : reset the bridge state (box below), then restart Synapse to reload the registration.
See also
Section titled “See also”- Matrix messaging: user guide : bridge connection for users
- Matrix in DNF : architecture and implementation