Aller au contenu

Matrix et les ponts de messagerie

Activer le serveur Matrix (Synapse), son client web Element et les ponts mautrix (WhatsApp, Signal, Telegram, Messenger, Discord). Pour l’architecture, voir Matrix dans DNF.

  1. Déclarer les services sur l’hôte cible dans 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. Déclarer les administrateurs Matrix dans la section network (hors hôtes) :

    etc/config.yaml
    network:
    matrix:
    admins: ["alice", "bob"] # local parts des administrateurs

    Une seule liste pour les deux administrations, appliquée au rebuild : les ponts et le serveur. Ces comptes reçoivent aussi les alertes de supervision.

  3. Créer les secrets requis (voir tableau ci-dessous), puis déployer.

L’authentification passe par le SSO Kanidm (OIDC) : tout utilisateur déclaré se connecte avec son compte réseau, aucune création de compte Matrix n’est nécessaire.

Tous internes : just configure-admin-host les crée dans usr/secrets/secrets.yaml, sans jamais écraser l’existant.

Clé sopsRôle
oidc-secret-matrixSecret du client OIDC Kanidm (cf. SSO)
turn-secretSecret partagé coturn (si turn activé)
livekit-secretSecret partagé MatrixRTC (si matrixRtc activé)
mautrix-doublepuppet-as-tokenToken appservice double puppeting (commun aux ponts)
mautrix-doublepuppet-hs-tokenIdem, requis par Synapse, jamais utilisé

Toute l’authentification est déléguée au Matrix Authentication Service 🡕 (MAS), sans option : c’est le composant qui manque à Synapse pour Element X 🡕, la connexion par QR code et le portail de compte, et l’authentification interne de Synapse est abandonnée en amont (MSC3861). Kanidm reste la source d’identité : MAS devient son client OIDC à la place de Synapse.

Pour la place de MAS dans l’architecture, voir Matrix dans DNF.

SujetOù ça se passe
Connexion SSOMAS, client OIDC de Kanidm
Portail de comptehttps://matrix.domain.tld/account/
Jetons d’inscriptiondnf-mas sur l’hôte
Administrateurs serveurnetwork.matrix.admins, déclaratif
Comptes, sessions, salonsinterface Ketesa
  1. Générer les trois secrets dédiés (idempotent, n’écrase jamais l’existant) :

    Fenêtre de terminal
    just configure-admin-host
    Clé sopsRôle
    mas-encryption-secretChiffrement interne MAS
    mas-synapse-secretSecret partagé MAS ↔ Synapse
    mas-rsa-private-keyClé de signature des tokens
  2. Déployer. Sur un serveur neuf, c’est terminé. Sur un serveur qui a déjà des comptes, enchaîner sur la migration sans laisser personne se connecter entre les deux.

MAS devient propriétaire des comptes et des sessions : ils doivent être importés avec l’outil officiel syn2mas, servi par la commande dnf-mas de l’hôte. Sans cet import, tous les comptes sont invisibles et tout le monde est déconnecté.

  1. Sauvegarder, dans cet ordre (le dump doit entrer dans l’instantané) :

    Fenêtre de terminal
    sudo systemctl start postgresqlBackup.service
    sudo systemctl start restic-backups-system-main.service
  2. Déployer la configuration (cf. ci-dessus). La fenêtre de maintenance commence ici.

  3. Arrêter les deux services : syn2mas exige Synapse hors ligne, et MAS ne doit pas provisionner de compte entre-temps.

    Fenêtre de terminal
    sudo systemctl stop matrix-synapse matrix-authentication-service
  4. Vérifier, puis répéter à blanc, puis migrer :

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

    Le compte-rendu annonce les comptes, liens SSO, jetons d’accès et devices repris. Les centaines d’utilisateurs « ignorés » sont les fantômes des ponts : c’est normal.

  5. Redémarrer, MAS d’abord :

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

En plus des utilisateurs du SSO, on peut ouvrir une inscription sur jeton pour des comptes locaux à mot de passe (les amis). Sans jeton, aucune inscription n’est possible.

# usr/machines/<host>/configuration.nix
darkone.service.matrix.friendRegistration.enable = true;
  1. Créer un jeton sur l’hôte Matrix. Ici valable une fois et 7 jours (sans option, le jeton est à usage unique et sans expiration) :

    Fenêtre de terminal
    sudo dnf-mas manage issue-user-registration-token --usage-limit 1 --expires-in 604800

    La réponse affiche le jeton, par exemple Created user registration token: 9SwPTGIFGQPa. Le transmettre à l’ami.

  2. L’ami ouvre https://matrix.domain.tld/register et choisit la création par mot de passe.

  3. Il saisit son identifiant et son mot de passe, puis le jeton, réclamé à l’étape suivante.

  4. Il se connecte ensuite avec ce couple identifiant / mot de passe depuis n’importe quel client, Element X compris.

Collision d’identifiant avec un utilisateur déclaré

Section intitulée « Collision d’identifiant avec un utilisateur déclaré »

Amis et utilisateurs SSO partagent un seul espace de noms : @bob:domain.tld n’appartient qu’à un compte, quelle que soit son origine. Et un identifiant Matrix est définitif — la table users de MAS impose son unicité, et désactiver un compte ne libère pas son identifiant. On ne renomme pas, on ne recycle pas.

Le cas typique : un ami s’inscrit comme bob, puis bob est déclaré plus tard comme utilisateur DNF dans etc/config.yaml. Sa première connexion SSO s’arrête alors sur une page d’erreur User exists :

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

Vérifier avant de déclarer un utilisateur, sur l’hôte Matrix :

Fenêtre de terminal
sudo -u postgres psql matrix-authentication-service \
-c "SELECT username FROM users WHERE lower(username) = lower('bob');"

Deux issues, selon ce qu’on veut garder :

Rattache l’identité Kanidm au compte ami existant, qui conserve tout : salons, historique, clés de chiffrement, ponts. C’est presque toujours ce qu’on veut, et l’intéressé le fait lui-même.

  1. Déclarer d’abord bob dans etc/config.yaml et déployer, pour qu’il ait des identifiants Kanidm.

  2. Bob se connecte sur https://matrix.domain.tld/account/ avec son mot de passe d’ami.

  3. Dans le même navigateur, il ouvre le lien de rattachement du fournisseur IDM :

    https://matrix.domain.tld/upstream/authorize/01JDNF0000000000000KAN1DM0
  4. Il s’identifie sur Kanidm ; MAS propose alors « Lier à votre compte existant », qu’il valide.

Bob se connecte ensuite par le SSO comme n’importe quel utilisateur déclaré. Son mot de passe d’ami reste valable : le supprimer se fait depuis le portail de compte.

Les ponts WhatsApp, Signal, Telegram et Messenger sont activés par défaut avec Matrix ; Discord est désactivé par défaut. Réglage par hôte dans la configuration nix de la machine :

# usr/machines/<host>/configuration.nix
darkone.service.matrix.bridges = {
discord.enable = true; # opt-in
messenger.enable = false; # désactiver un pont par défaut
};

Chaque pont a ses secrets propres :

PontClés sopsRemarque
WhatsAppmautrix-whatsapp-as-token, mautrix-whatsapp-hs-token, mautrix-whatsapp-encryption-pickle-keyopenssl rand -hex 32 chacun
Signalmautrix-signal-as-token, mautrix-signal-hs-token, mautrix-signal-encryption-pickle-keyopenssl rand -hex 32 chacun
Messengermautrix-meta-as-token, mautrix-meta-hs-token, mautrix-meta-encryption-pickle-keyopenssl rand -hex 32 chacun
Telegrammautrix-telegram-api-id, mautrix-telegram-api-hash, mautrix-telegram-as-token, mautrix-telegram-hs-tokenAPI id/hash à créer sur my.telegram.org 🡕
Discordmautrix-discord-as-token, mautrix-discord-hs-tokenopenssl rand -hex 32 chacun

Les tokens as/hs rendent la registration appservice de chaque pont déterministe (voir les tokens d’appservice).

Rien à gérer : tout compte local (@*:domain.tld) peut utiliser chaque pont avec son propre compte distant, sans interférence entre utilisateurs. Les comptes listés dans network.matrix.admins disposent en plus des commandes d’administration des bots (help en conversation directe avec le bot pour la liste).

Les ponts partagent un appservice doublepuppet (token mautrix-doublepuppet-as-token) : les messages envoyés depuis le téléphone apparaissent dans Matrix comme venant du vrai compte de l’utilisateur. C’est automatique pour tous les comptes locaux, aucune action par utilisateur.

Deux piles cohabitent, parce qu’aucune ne couvre tous les clients. Elles s’activent séparément et ne se remplacent pas :

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

Garder les deux activées : Element Classic ne parle que la première, Element X que la seconde.

  1. Activer la pile MatrixRTC dans la config nix de l’hôte :

    # usr/machines/<host>/configuration.nix
    darkone.service.matrix.matrixRtc.enable = true; # requis par Element X
  2. Générer le secret partagé, puis déployer :

    Fenêtre de terminal
    just configure-admin-host
    just apply <host> switch
  3. Vérifier que le serveur annonce bien un 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

Aucun sous-domaine ni certificat supplémentaire : le SFU et son service d’autorisation sont servis sur le vhost Matrix, sous /livekit/sfu et /livekit/jwt. Côté client, rien à régler non plus — Element X et Element Web embarquent Element Call et découvrent le transport tout seuls.

Le média ne passe pas par le proxy inverse : les clients joignent l’hôte directement. Ces ports doivent donc être ouverts jusqu’à lui, et redirigés s’il est derrière un NAT :

PortPileUsage
30000-30100/udpMatrixRTCMédia WebRTC, un port par participant
7881/tcpMatrixRTCRepli pour les réseaux qui bloquent l’UDP
3478/udp, 3478/tcp, 5349/tcpcoturnSTUN et négociation TURN
61000-65535/udpcoturnMédia relayé

Un client WebRTC découvre son adresse publique avec une requête STUN Binding, qu’il envoie toujours sans authentification, y compris vers le serveur TURN. Si coturn la refuse (option secure-stun), le client n’obtient aucun candidat server-reflexive : il ne lui reste que ses adresses locales, inutilisables hors du réseau local, et son adresse de relais. Aucun chemin direct n’est alors jamais tenté : tous les appels traversent coturn, plus longs à s’établir et sans repli quand le relais sature.

DNF laisse donc secure-stun désactivé. Les allocations TURN, elles, restent authentifiées par le secret partagé (use-auth-secret) : seul le Binding est anonyme.

Contrôler depuis une machine extérieure :

Fenêtre de terminal
nix shell nixpkgs#stuntman --command stunclient turn.domain.tld 3478

Binding test: success suivi de l’adresse vue par le serveur : correct. Binding test: fail : les requêtes anonymes sont refusées, tous les appels seront relayés.

Pendant un appel, coturn journalise des erreurs de cette forme :

ERROR session 000000000000000138: A peer IP 192.168.0.42 denied in the range:
192.168.0.0-192.168.255.255 in server 0

Ce n’est pas une panne. denied-peer-ip filtre la destination d’une allocation (CreatePermission et ChannelBind), pas les clients autorisés à se connecter. Le client a proposé un candidat en adresse privée, coturn a refusé de relayer vers là, ICE écarte la paire et poursuit avec les candidats publics.

DNF refuse donc toutes les plages privées et n’autorise que l’adresse publique de l’hôte, parce que coturn est un relais public uniquement : les clients le joignent sur cette adresse, et relay-ip est la même. Deux raisons de ne pas rouvrir 10.0.0.0/8 ni 100.64.0.0/10.

Chemin asymétrique. Le média relayé sort par le relay-ip public alors qu’un pair du tailnet ou du réseau local n’est joignable que par l’autre interface. L’appel s’établit, puis meurt quand l’entrée de suivi de connexion expire : c’est la signature des appels longs qui coupent sans raison. L’option mobility aggrave le cas, un téléphone qui change de réseau conserve une permission pointant l’adresse qu’il vient de quitter.

Pivot vers le réseau privé. Un relais qui retransmet vers du RFC1918 ou du CGNAT devient un point d’entrée pour quiconque détient des identifiants TURN, c’est-à-dire tout compte du serveur Matrix puisque Synapse les distribue (use-auth-secret).

Par défaut, le serveur fédère avec tous les serveurs Matrix, tout en restant invisible dans les recherches (annuaires non exposés). Pour contrôler la portée, régler darkone.service.matrix.federation dans la config nix de l’hôte :

# usr/machines/<host>/configuration.nix
darkone.service.matrix.federation = {
enable = true; # false = fédération totalement bloquée
whitelist = [ ]; # vide = ouverte ; sinon liste blanche
};
ObjectifRéglage
Réseau isolé (aucune fédération)enable = false;
Ouvert à tout l’écosystème Matrixenable = true; whitelist = [ ];
Restreint à des serveurs de confianceenable = true; whitelist = [ "ami.org" "matrix.org" ];

Vérifier après déploiement :

  1. Tester la fédération entrante avec federationtester.matrix.org 🡕 sur domain.tld → tous les voyants verts.

  2. En liste blanche, un serveur non listé est refusé (journal Synapse : Federation denied), un domaine listé fonctionne.

Un salon public au sens Matrix se rejoint librement : pas de validation. Pour un salon « joignable mais sur approbation », utiliser la règle d’accès knock (toquer) — réglage par salon, côté client, sans configuration serveur.

  1. Dans Element : salon → Paramètres du salon → Sécurité & confidentialité.

  2. Accès au salon → « Demander à rejoindre » (knock).

  3. Un utilisateur (local ou distant fédéré) demande l’accès ; un modérateur approuve ou refuse.

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 enveloppe l’outil officiel mas-cli sur l’hôte Matrix : il reconstitue la configuration et les secrets du service, et fonctionne donc service arrêté comme démarré. Toujours en sudo.

ActionCommande
Créer un jeton d’inscriptionsudo dnf-mas manage issue-user-registration-token
Lister les administrateurssudo dnf-mas manage list-admin-users
Retirer une promotion héritéesudo dnf-mas manage demote-admin alice
Créer un compte (bot, service)sudo dnf-mas manage register-user
Changer un mot de passesudo dnf-mas manage set-password alice
Fermer les sessions d’un comptesudo dnf-mas manage kill-sessions alice
Verrouiller un comptesudo dnf-mas manage lock-user alice
Diagnostic completsudo dnf-mas doctor

mas-cli n’expose aucune commande de lecture : pour voir les comptes, leurs appareils et leurs sessions, activer le service matrix-admin, l’interface web Ketesa 🡕 (ex-synapse-admin), qui parle aux API d’administration de MAS et de Synapse.

# etc/config.yaml, sur l'hôte du serveur Matrix
services:
matrix-admin:

Elle répond sur https://matrix-admin.domain.tld, depuis le LAN et le tailnet uniquement. Le sous-domaine doit tout de même résoudre publiquement vers le HCS : Caddy en a besoin pour le certificat ACME, avant de refuser les appelants extérieurs. Connexion par le bouton OIDC, avec un compte listé dans network.matrix.admins : la politique MAS lui accorde les portées urn:mas:admin et urn:synapse:admin:* dont l’interface a besoin sur les deux API.

CôtéCe que l’interface permet
MASSessions (compat, OAuth2, personnelles), e-mails, liens OIDC amont, jetons d’inscription, mot de passe, verrouillage, désactivation
SynapseComptes et appareils, salons, médias, signalements, limites de débit

Après une migration, les sessions déjà ouvertes continuent de fonctionner : une connexion cassée ne se voit donc qu’au premier login neuf. Suivre MAS pendant une tentative :

Fenêtre de terminal
journalctl -u matrix-authentication-service -f
MessageCauseCorrectif
wrong signature alg sur /upstream/callback/…MAS attend un id_token RS256, Kanidm signe en ES256id_token_signed_response_alg du fournisseur amont : réglé sur ES256 par le module
client registration denied by the policy: invalid redirect_uriLe client_uri annoncé par le client ne correspond pas à son schéma de redirectionNe pas forcer oidc_metadata dans la config Element ; ses valeurs par défaut passent
M_UNRECOGNIZED sur /login/sso/redirectLe chemin part vers Synapse au lieu de MASVérifier le matcher @masCompat du vhost Caddy
Page User exists : « returned “bob” as username, which is not linked »Un compte ami occupe déjà l’identifiant de l’utilisateur déclaréFusionner ou renommer (cf. collision d’identifiant)
« Le code QR n’est pas pris en charge » côté clientLe canal de rendez-vous MSC4108 est absentexperimental_features.msc4108_enabled : activé par le module avec MAS
« L’appel n’est pas pris en charge », MISSING_MATRIX_RTC_TRANSPORTAucun backend MatrixRTC annoncéActiver matrixRtc.enable (cf. Appels audio et vidéo)
Appel qui sonne mais reste muet, ou coupe au bout de quelques secondesLes ports média n’atteignent pas l’hôteOuvrir 30000-30100/udp et 7881/tcp jusqu’à lui
Appel long à s’établir, voire qui échoue, et tout le trafic passe par coturnLes clients n’obtiennent aucun candidat server-reflexive, donc aucun chemin directcoturn doit répondre aux requêtes STUN Binding anonymes (cf. Appels relayés en permanence)

Contrôles rapides, sans se connecter :

Fenêtre de terminal
# org.matrix.msc4108 doit valoir true
curl -A Mozilla/5.0 https://matrix.domain.tld/_matrix/client/versions
# doit répondre 201 et une url publique
curl -A Mozilla/5.0 -X POST --data probe \
https://matrix.domain.tld/_matrix/client/unstable/org.matrix.msc4108/rendezvous

Un bot qui ne répond pas à help alors que son service tourne tombe presque toujours dans l’un des deux cas : ses tokens ne correspondent plus à la registration chargée par Synapse, ou il reçoit les messages mais ne peut pas les déchiffrer. Diagnostic dans l’ordre :

  1. Le service tourne-t-il vraiment ?

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

    S’il redémarre en boucle avec « The as_token was not accepted », l’enregistrement en mémoire de Synapse ne correspond plus aux tokens du pont (typique après un reset ou une migration, voir l’encart plus haut) : systemctl restart matrix-synapse, puis redémarrer le pont.

  2. Le message arrive-t-il au pont ? Suivre Synapse pendant l’envoi d’un help au bot :

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

    Une ligne PUT .../_matrix/app/v1/transactions/... 200 doit apparaître. Si rien ne part, l’appservice n’est pas chargé : vérifier la présence de l’enregistrement dans /var/lib/mautrix-<pont>/ et redémarrer Synapse.

  3. Le pont peut-il le déchiffrer ? Si la transaction passe (200) mais que le bot reste muet, suivre ses logs pendant un nouvel envoi :

    Fenêtre de terminal
    journalctl -u mautrix-<pont> -f

    Chercher decrypt, session, verification, dropping. Un échec de déchiffrement signifie que la clé de session du salon n’a pas été partagée avec le device actuel du bot (cas classique après un reset du pont : son identité e2ee a changé).

  4. Recréer la conversation. Quitter le message direct avec le bot et en démarrer un nouveau : le client établit une session de chiffrement neuve et partage la clé avec le device actuel du bot. Vérifier au passage que sa propre session Element est vérifiée.

  5. Dernier recours : réinitialiser l’état du pont (encart ci-dessous), puis redémarrer Synapse pour recharger l’enregistrement.