Files

15 KiB

Déployer Matrix sur a5l.fr avec SSO Keycloak et Element Call

Vue d'ensemble de l'architecture

Voici la pile complète à monter, et les sous-domaines associés :

a5l.fr                     → délégation .well-known vers matrix.a5l.fr
matrix.a5l.fr              → Synapse (homeserver)
auth.a5l.fr (ou matrix-auth.a5l.fr) → MAS (Matrix Authentication Service)
chat.a5l.fr                → Element Web
mrtc.a5l.fr                → MatrixRTC backend (lk-jwt-service + LiveKit SFU)
idp.a5l.fr                 → Keycloak (déjà en place)
turn.a5l.fr                → coturn (optionnel mais recommandé)

Et les briques applicatives :

Brique Rôle
Synapse Homeserver Matrix : messages, rooms, fédération
PostgreSQL Base de données de Synapse
MAS (Matrix Authentication Service) Délègue l'auth à Keycloak en OIDC, sert d'OP aux clients Matrix (obligatoire pour Element X)
Element Web Client web (les desktop et mobiles l'embarquent)
LiveKit SFU Serveur de média WebRTC pour les appels audio/vidéo
lk-jwt-service Émet les JWT permettant aux utilisateurs Matrix d'accéder à LiveKit
coturn Serveur TURN pour les NAT difficiles (utile derrière FAI)
Keycloak Votre IdP existant sur idp.a5l.fr

Warning

Pourquoi MAS et pas directement OIDC dans Synapse ? Synapse supporte historiquement OIDC nativement, mais Element X (les nouvelles apps mobiles iOS et Android) refuse de s'authentifier sans MAS. Element X mise sur le mécanisme "next-gen auth" de Matrix, qui passe par MAS. Comme votre cible est "tous appareils", MAS est non-négociable. Les clients legacy (Element Web/Desktop) acceptent aussi MAS.

Prérequis matériels et réseau

Serveur :

  • 4 Go RAM minimum, 8 Go recommandé pour confort
  • 50 Go SSD pour démarrer (PostgreSQL grossit avec l'historique fédéré)
  • Docker + Docker Compose installés
  • Un reverse proxy (Nginx ou Caddy) sur le serveur

Réseau / FAI :

  • IP publique non-CGNAT (vérifiez)
  • Ports à ouvrir et rediriger vers le serveur :
Port Protocole Usage
443 TCP HTTPS pour tous les services web
8448 TCP Fédération Matrix
3478 UDP/TCP coturn STUN/TURN
5349 UDP/TCP coturn STUN/TURN sur TLS
50000-50100 UDP LiveKit (plage WebRTC)

DNS chez votre registrar :

matrix       IN A   <IP_PUBLIQUE>
auth         IN A   <IP_PUBLIQUE>
chat         IN A   <IP_PUBLIQUE>
mrtc         IN A   <IP_PUBLIQUE>
turn         IN A   <IP_PUBLIQUE>

; Découverte fédération Matrix
_matrix._tcp.a5l.fr  IN SRV  10 5 8448 matrix.a5l.fr.

Étape 1 — Déléguer Matrix depuis a5l.fr

Pour que @user:a5l.fr fonctionne (alors que Synapse tourne sur matrix.a5l.fr), le domaine racine doit servir deux fichiers de découverte.

Sur le serveur qui répond à https://a5l.fr/, exposez :

/.well-known/matrix/server :

{ "m.server": "matrix.a5l.fr:443" }

/.well-known/matrix/client :

{
  "m.homeserver": {
    "base_url": "https://matrix.a5l.fr"
  },
  "org.matrix.msc4143.rtc_foci": [
    {
      "type": "livekit",
      "livekit_service_url": "https://mrtc.a5l.fr/livekit/jwt"
    }
  ]
}

Le bloc rtc_foci (MSC4143) est ce qui dit aux clients Element X d'utiliser votre backend MatrixRTC plutôt que celui d'Element.

Servez ces deux fichiers avec Content-Type: application/json et Access-Control-Allow-Origin: *.

Étape 2 — Configurer Keycloak

Dans votre realm Keycloak (disons homelab), créez deux clients OIDC :

Client matrix-auth-service (pour MAS)

  • Client ID : matrix-auth-service
  • Client authentication : ON (confidentiel)
  • Standard flow : activé
  • Valid redirect URIs : https://auth.a5l.fr/upstream/callback/01HXXXXXX (l'UUID sera donné par MAS au premier démarrage, ajustez)
  • Web origins : https://auth.a5l.fr
  • Récupérez le Client Secret

Vérifiez que les mappers exposent bien preferred_username, email, name dans l'access token.

Restriction d'accès

Créez un rôle client matrix-user et assignez-le aux comptes Keycloak autorisés à utiliser Matrix. Configurez un Client Scope pour n'autoriser que ces rôles à obtenir un token.

Étape 3 — Déployer Synapse, MAS, et PostgreSQL

Voici un docker-compose.yml de base. Adaptez les chemins de volumes à votre arborescence.

services:
  postgres:
    image: postgres:16
    restart: unless-stopped
    environment:
      POSTGRES_USER: synapse
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: synapse
      POSTGRES_INITDB_ARGS: "--lc-collate=C --lc-ctype=C --encoding=UTF8"
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
    networks:
      - matrix

  synapse:
    image: ghcr.io/element-hq/synapse:latest
    restart: unless-stopped
    depends_on:
      - postgres
    environment:
      SYNAPSE_SERVER_NAME: a5l.fr
      SYNAPSE_REPORT_STATS: "no"
    volumes:
      - ./data/synapse:/data
    ports:
      - "127.0.0.1:8008:8008"
    networks:
      - matrix

  mas:
    image: ghcr.io/element-hq/matrix-authentication-service:latest
    restart: unless-stopped
    depends_on:
      - postgres
    command: ["server", "--config=/config/config.yaml"]
    volumes:
      - ./data/mas:/config
    ports:
      - "127.0.0.1:8080:8080"
    networks:
      - matrix

networks:
  matrix:
    driver: bridge

Configuration MAS (./data/mas/config.yaml)

MAS est l'élément le plus subtil de la pile. Il joue deux rôles : client OIDC vers Keycloak (en upstream) et serveur OAuth/OIDC pour Synapse et les clients Matrix (en downstream).

http:
  listeners:
    - name: web
      resources:
        - name: discovery
        - name: human
        - name: oauth
        - name: compat
        - name: graphql
        - name: assets
      binds:
        - address: '[::]:8080'
  public_base: https://auth.a5l.fr/
  issuer: https://auth.a5l.fr/

database:
  uri: postgresql://synapse:${POSTGRES_PASSWORD}@postgres/mas

matrix:
  homeserver: a5l.fr
  endpoint: http://synapse:8008/
  secret: ${MATRIX_SHARED_SECRET}

passwords:
  enabled: false  # On délègue entièrement à Keycloak

upstream_oauth2:
  providers:
    - id: 01HXXXXXXKEYCLOAK
      issuer: https://idp.a5l.fr/realms/homelab
      human_name: a5l SSO
      client_id: matrix-auth-service
      client_secret: ${KEYCLOAK_CLIENT_SECRET}
      token_endpoint_auth_method: client_secret_basic
      scope: "openid profile email"
      claims_imports:
        subject:
          template: "{{ user.sub }}"
        localpart:
          action: require
          template: "{{ user.preferred_username }}"
        displayname:
          action: suggest
          template: "{{ user.name }}"
        email:
          action: suggest
          template: "{{ user.email }}"

clients:
  - client_id: 0000000000000000000SYNAPSE
    client_auth_method: client_secret_basic
    client_secret: ${SYNAPSE_MAS_SECRET}

Générez l'UUID 01HXXXXXXKEYCLOAK avec mas-cli config generate (ou un ULID), et mettez la même valeur dans la Valid redirect URI côté Keycloak : https://auth.a5l.fr/upstream/callback/01HXXXXXXKEYCLOAK.

Configuration Synapse (./data/synapse/homeserver.yaml)

Points clés à ajouter ou modifier après l'initialisation :

server_name: "a5l.fr"
public_baseurl: "https://matrix.a5l.fr/"

listeners:
  - port: 8008
    type: http
    tls: false
    bind_addresses: ['0.0.0.0']
    x_forwarded: true
    resources:
      - names: [client, federation, openid]
        compress: false

database:
  name: psycopg2
  args:
    user: synapse
    password: ${POSTGRES_PASSWORD}
    database: synapse
    host: postgres
    cp_min: 5
    cp_max: 10

# Délégation à MAS (remplace oidc_providers + password_config)
experimental_features:
  msc3861:
    enabled: true
    issuer: https://auth.a5l.fr/
    client_id: 0000000000000000000SYNAPSE
    client_auth_method: client_secret_basic
    client_secret: ${SYNAPSE_MAS_SECRET}
    admin_token: ${MAS_ADMIN_TOKEN}
    account_management_url: https://auth.a5l.fr/account/
  # Requis par Element Call
  msc3266_enabled: true
  msc4222_enabled: true

max_event_delay_duration: 24h

# Désactiver l'auth locale puisque MAS gère tout
password_config:
  enabled: false

enable_registration: false

Étape 4 — Déployer le backend MatrixRTC (Element Call)

Ajoutez au docker-compose.yml :

  livekit:
    image: livekit/livekit-server:latest
    restart: unless-stopped
    command: --config /etc/livekit.yaml
    volumes:
      - ./data/livekit/livekit.yaml:/etc/livekit.yaml:ro
    network_mode: host  # nécessaire pour la plage UDP WebRTC

  lk-jwt-service:
    image: ghcr.io/element-hq/lk-jwt-service:latest
    restart: unless-stopped
    environment:
      LK_JWT_PORT: 8081
      LIVEKIT_URL: https://mrtc.a5l.fr/livekit/sfu
      LIVEKIT_KEY: ${LIVEKIT_KEY}
      LIVEKIT_SECRET: ${LIVEKIT_SECRET}
      LIVEKIT_FULL_ACCESS_HOMESERVERS: a5l.fr
    ports:
      - "127.0.0.1:8081:8081"
    networks:
      - matrix

./data/livekit/livekit.yaml :

port: 7880
bind_addresses:
  - "127.0.0.1"

rtc:
  tcp_port: 7881
  port_range_start: 50000
  port_range_end: 50100
  use_external_ip: true

keys:
  ${LIVEKIT_KEY}: ${LIVEKIT_SECRET}

turn:
  enabled: false  # On utilise coturn dédié

Étape 5 — Element Web

  element:
    image: vectorim/element-web:latest
    restart: unless-stopped
    volumes:
      - ./data/element/config.json:/app/config.json:ro
    ports:
      - "127.0.0.1:8082:80"
    networks:
      - matrix

./data/element/config.json (minimal) :

{
  "default_server_config": {
    "m.homeserver": {
      "base_url": "https://matrix.a5l.fr",
      "server_name": "a5l.fr"
    }
  },
  "features": {
    "feature_video_rooms": true,
    "feature_element_call_video_rooms": true,
    "feature_group_calls": true
  },
  "element_call": {
    "url": "https://chat.a5l.fr",
    "use_exclusively": true,
    "participant_limit": 8
  }
}

Étape 6 — Reverse proxy

Voici un squelette Nginx. La partie sensible est le routage /livekit/jwt/ et /livekit/sfu/ sur mrtc.a5l.fr.

# matrix.a5l.fr
server {
    listen 443 ssl http2;
    listen 8448 ssl http2;  # Fédération
    server_name matrix.a5l.fr;
    
    ssl_certificate     /etc/letsencrypt/live/matrix.a5l.fr/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/matrix.a5l.fr/privkey.pem;

    client_max_body_size 100M;

    location ~ ^(/_matrix|/_synapse/client) {
        proxy_pass http://127.0.0.1:8008;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host $host;
    }
}

# auth.a5l.fr (MAS)
server {
    listen 443 ssl http2;
    server_name auth.a5l.fr;
    
    ssl_certificate     /etc/letsencrypt/live/auth.a5l.fr/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/auth.a5l.fr/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host $host;
    }
}

# mrtc.a5l.fr (Element Call backend)
server {
    listen 443 ssl http2;
    server_name mrtc.a5l.fr;
    
    ssl_certificate     /etc/letsencrypt/live/mrtc.a5l.fr/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mrtc.a5l.fr/privkey.pem;

    location ^~ /livekit/jwt/ {
        proxy_pass http://127.0.0.1:8081/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location ^~ /livekit/sfu/ {
        proxy_pass http://127.0.0.1:7880/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_send_timeout 120;
        proxy_read_timeout 120;
        proxy_buffering off;
    }
}

# chat.a5l.fr (Element Web)
server {
    listen 443 ssl http2;
    server_name chat.a5l.fr;
    
    ssl_certificate     /etc/letsencrypt/live/chat.a5l.fr/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/chat.a5l.fr/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8082;
    }
}

Étape 7 — Premier démarrage et test

# Génération des secrets une fois pour toutes
openssl rand -hex 32  # pour chaque secret du .env

docker compose up -d postgres
sleep 5
docker compose exec postgres psql -U synapse -c "CREATE DATABASE mas;"

# Initialiser Synapse
docker compose run --rm synapse generate
# Éditez data/synapse/homeserver.yaml avec la conf ci-dessus

# Démarrer
docker compose up -d

Tests :

  1. https://a5l.fr/.well-known/matrix/client doit renvoyer le JSON correct
  2. https://federationtester.matrix.org/#a5l.fr doit passer au vert
  3. Sur Element Web (https://chat.a5l.fr), cliquez "Continuer avec a5l SSO" → vous devez être redirigé vers Keycloak → après login, retour sur Element connecté
  4. Créez un salon, lancez un appel vidéo → vérifiez les logs LiveKit

Pièges spécifiques au homelab derrière FAI

NAT et WebRTC. Vos appels passeront mal sans coturn si vos correspondants sont derrière des NAT symétriques (4G/5G notamment). Déployez coturn sur le port 3478/5349 et déclarez-le dans la config LiveKit (section turn_servers) ou laissez le client utiliser le TURN annoncé par Synapse via turn_uris.

IP dynamique. Configurez un client DDNS qui met à jour vos enregistrements A. Pour la plupart des registrars, un cron toutes les 5 minutes suffit. Les enregistrements SRV n'ont pas besoin d'être touchés.

Fédération bavarde. Si vous rejoignez #matrix:matrix.org ou un autre gros salon, votre base PostgreSQL va grossir vite. Activez mjolnir ou configurez state_compressor après quelques semaines.

Sauvegarde. Critique :

docker compose exec postgres pg_dumpall -U synapse | gzip > backup-$(date +%F).sql.gz
tar czf data-$(date +%F).tar.gz data/synapse/media_store data/mas data/livekit

Pour aller plus vite

Sincèrement, si vous n'êtes pas attaché à comprendre chaque brique, utilisez matrix-docker-ansible-deploy de spantaleev. Le playbook gère MAS, Synapse, Element Web, Element Call, LiveKit, coturn, le SSO Keycloak, les .well-known, certbot — tout, avec les bonnes versions ensemble. Vous éditez un vars.yml, vous lancez ansible-playbook setup.yml --tags=setup-all,start, et c'est en ligne. Le tutoriel ci-dessus vous donne la compréhension de ce que le playbook fait sous le capot, ce qui reste précieux pour le débogage.

Et après ?

Une fois que ça tourne, des extensions utiles :

  • synapse-admin pour gérer les utilisateurs en interface web
  • mautrix-signal / mautrix-whatsapp : ponts vers Signal, WhatsApp, Telegram, etc., pour centraliser vos messageries dans Element
  • hookshot pour les notifications GitHub/GitLab dans des salons
  • maubot pour des bots utilitaires