Skip to content

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.

  1. Declare the services on the target host in etc/config.yaml :

    etc/config.yaml
    services:
    matrix:
    title: "Matrix"
    description: "Messagerie instantanée"
    element: # client web
    turn: # relais audio/vidéo des clients historiques
  2. Declare the Matrix administrator (global key, outside hosts) :

    etc/config.yaml
    matrix:
    admin: "alice" # local part du compte administrateur

    This account manages the bridges and receives the supervision alerts.

  3. 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.

To fill in in usr/secrets/secrets.yaml :

Clé sopsRôleGénération
matrix-rss-passwordRegistration shared secret Synapseopenssl rand -hex 32
oidc-secret-matrixSecret du client OIDC Kanidmcf. SSO
turn-secretSecret partagé coturn (si turn activé)openssl rand -hex 32
livekit-secretSecret partagé MatrixRTC (si matrixRtc activé)just passwd-livekit
mautrix-doublepuppet-as-tokenToken appservice double puppeting (commun aux ponts)openssl rand -hex 32
mautrix-doublepuppet-hs-tokenIdem, requis par Synapse, jamais utiliséopenssl rand -hex 32

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é)
Optionmas.enable = falsemas.enable = true
Connexion SSOSynapse est client OIDC de KanidmMAS est client OIDC de Kanidm
Element Web / Desktopouioui
Element X (Android, iOS)nonoui
Connexion par QR codenonoui
Application proposée sur mobileElement ClassicElement X
Portail de compteaucunhttps://matrix.domain.tld/account/
Jetons d’inscriptionAPI admin Synapsednf-mas sur l’hôte
Promotion administrateurSQL dans la base Synapsednf-mas manage promote-admin

The Matrix Authentication Service 🡕 replaces Synapse’s internal authentication. For what it changes and how it fits in, see the architecture.

  1. Generate the three dedicated secrets (idempotent, never overwrites existing) :

    Fenêtre de terminal
    just passwd-mas
    sops keyRole
    mas-encryption-secretMAS internal encryption
    mas-synapse-secretShared secret MAS ↔ Synapse
    mas-rsa-private-keyToken signing key
  2. Enable the option in the host’s nix config :

    # usr/machines/<host>/default.nix
    darkone.service.matrix.mas.enable = true;
  3. 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.

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.

  1. Back up, in this order (the dump must make it into the snapshot) :

    Fenêtre de terminal
    sudo systemctl start postgresqlBackup.service
    sudo systemctl start restic-backups-system-main.service
  2. Deploy the configuration with mas.enable = true (see above). The maintenance window starts here.

  3. Stop both services : syn2mas requires 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
  4. Check, then run a dry run, then migrate :

    Fenêtre de terminal
    sudo dnf-mas syn2mas check
    sudo dnf-mas syn2mas migrate --dry-run
    sudo dnf-mas syn2mas migrate

    The report announces the recovered accounts, SSO links, access tokens and devices. The hundreds of “ignored” users are the bridges’ ghosts : that’s normal.

  5. Restart, MAS first :

    Fenêtre de terminal
    sudo systemctl start matrix-authentication-service
    sudo systemctl start matrix-synapse

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.nix
darkone.service.matrix.friendRegistration.enable = true;
  1. 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 604800

    The response displays the token, for example Created user registration token: 9SwPTGIFGQPa. Send it to the friend.

  2. The friend opens https://matrix.domain.tld/register and chooses password-based creation.

  3. He enters his username and password, then the token, requested at the next step.

  4. He then signs in with this username / password pair from any client, Element X included.

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 to
that upstream account. Your homeserver does not allow linking an upstream
account to an existing account

Check before declaring a user, on the Matrix host :

Fenêtre de terminal
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.

  1. First declare bob in etc/config.yaml and deploy, so that they get Kanidm credentials.

  2. Bob signs in on https://matrix.domain.tld/account/ with their friend password.

  3. In the same browser, they open the link account of the IDM provider :

    https://matrix.domain.tld/upstream/authorize/01JDNF0000000000000KAN1DM0
  4. 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.

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.nix
darkone.service.matrix.bridges = {
discord.enable = true; # opt-in
messenger.enable = false; # disable a default bridge
};

Each bridge has its own secrets :

BridgeSops keysNote
WhatsAppmautrix-whatsapp-as-token, mautrix-whatsapp-hs-token, mautrix-whatsapp-encryption-pickle-keyopenssl rand -hex 32 each
Signalmautrix-signal-as-token, mautrix-signal-hs-token, mautrix-signal-encryption-pickle-keyopenssl rand -hex 32 each
Messengermautrix-meta-as-token, mautrix-meta-hs-token, mautrix-meta-encryption-pickle-keyopenssl rand -hex 32 each
Telegrammautrix-telegram-api-id, mautrix-telegram-api-hash, mautrix-telegram-as-token, mautrix-telegram-hs-tokenAPI id/hash to create on my.telegram.org 🡕
Discordmautrix-discord-as-token, mautrix-discord-hs-tokenopenssl rand -hex 32 each

The as/hs tokens make each bridge’s appservice registration deterministic (see appservice tokens).

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).

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.

Two stacks coexist, because neither covers all clients. They activate separately and do not replace each other :

WebRTC historiqueMatrixRTC
Optiondarkone.service.turn.enabledarkone.service.matrix.matrixRtc.enable
ComposantscoturnLiveKit (SFU) + service d’autorisation
Appels à deuxouioui
Appels de groupenonoui
Element Classic (Android)ouinon
Element Web / Desktopouioui
Element X (Android, iOS)nonoui

Keep both enabled : Element Classic only speaks the first, Element X only the second.

  1. Enable the MatrixRTC stack in the host’s nix config :

    # usr/machines/<host>/default.nix
    darkone.service.matrix = {
    mas.enable = true; # requis par Element X pour se connecter
    matrixRtc.enable = true; # requis par Element X pour appeler
    };
  2. Generate the shared secret, then deploy :

    Fenêtre de terminal
    just passwd-livekit
    just apply <host> switch
  3. 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 true
    curl -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 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 :

PortUsage
40000-40100/udpWebRTC media, one port per participant
7881/tcpFallback for networks that block UDP

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.nix
darkone.service.matrix.federation = {
enable = true; # false = federation fully blocked
whitelist = [ ]; # empty = open ; otherwise whitelist
};
GoalSetting
Isolated network (no federation)enable = false;
Open to the entire Matrix ecosystemenable = true; whitelist = [ ];
Restricted to trusted serversenable = true; whitelist = [ "ami.org" "matrix.org" ];

Verify after deployment:

  1. Test incoming federation with federationtester.matrix.org 🡕 on domain.tld → all green lights.

  2. With a whitelist, an unlisted server is denied (Synapse log: Federation denied), a listed domain works.

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.

  1. In Element: room → Room SettingsSecurity & Privacy.

  2. Room access → “Ask to join” (knock).

  3. A user (local or federated remote) requests access; a moderator approves or denies.

ActionCommande
État des servicessystemctl status matrix-synapse mautrix-whatsapp mautrix-signal mautrix-telegram mautrix-meta-messenger
Journaux d’un pontjournalctl -u mautrix-whatsapp -f
Journaux Synapsejournalctl -u matrix-synapse -f
Journaux MASjournalctl -u matrix-authentication-service -f
Journaux MatrixRTCjournalctl -u livekit -u lk-jwt-service -f

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.

ActionCommand
Create a registration tokensudo dnf-mas manage issue-user-registration-token
Promote an administratorsudo dnf-mas manage promote-admin alice
List administratorssudo dnf-mas manage list-admin-users
Create an account (bot, service)sudo dnf-mas manage register-user
Change a passwordsudo dnf-mas manage set-password alice
Close an account’s sessionssudo dnf-mas manage kill-sessions alice
Lock an accountsudo dnf-mas manage lock-user alice
Full diagnosticsudo dnf-mas doctor

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 :

Fenêtre de terminal
journalctl -u matrix-authentication-service -f
MessageCauseFix
wrong signature alg on /upstream/callback/…MAS expects an RS256 id_token, Kanidm signs with ES256id_token_signed_response_alg of the upstream provider : set to ES256 by the module
client registration denied by the policy: invalid redirect_uriThe client_uri advertised by the client does not match its redirect schemeDo not force oidc_metadata in the Element config ; its default values pass
M_UNRECOGNIZED on /login/sso/redirectThe path goes to Synapse instead of MASCheck 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 identifierMerge or rename (cf. identifier collision)
” The QR code is not supported ” on the client sideThe MSC4108 rendezvous channel is missingexperimental_features.msc4108_enabled : enabled by the module with MAS
” The call is not supported ”, MISSING_MATRIX_RTC_TRANSPORTNo MatrixRTC backend announcedEnable matrixRtc.enable (cf. Audio and video calls)
Call rings but stays silent, or drops after a few secondsThe media ports do not reach the hostOpen 40000-40100/udp and 7881/tcp up to it

Quick checks, without signing in :

Fenêtre de terminal
# org.matrix.msc4108 must be true
curl -A Mozilla/5.0 https://matrix.domain.tld/_matrix/client/versions
# must answer 201 and a public url
curl -A Mozilla/5.0 -X POST --data probe \
https://matrix.domain.tld/_matrix/client/unstable/org.matrix.msc4108/rendezvous

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 :

  1. Is the service actually running ?

    Fenêtre de terminal
    systemctl status mautrix-<bridge>
    journalctl -u mautrix-<bridge> -n 50

    If 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.

  2. Does the message reach the bridge ? Follow Synapse while sending a help to the bot :

    Fenêtre de terminal
    journalctl -u matrix-synapse -f | grep transactions

    A PUT .../_matrix/app/v1/transactions/... 200 line should appear. If nothing goes out, the appservice is not loaded : check for the registration file in /var/lib/mautrix-<bridge>/ and restart Synapse.

  3. 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> -f

    Look 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).

  4. 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.

  5. Last resort : reset the bridge state (box below), then restart Synapse to reload the registration.