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 :
https://a5l.fr/.well-known/matrix/clientdoit renvoyer le JSON correcthttps://federationtester.matrix.org/#a5l.frdoit passer au vert- 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é - 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