
Installation et configuration d'une infrastructure Matrix complète avec authentification SSO via Keycloak et Matrix Authentication Service (MAS)
🎯 Objectifs
Installer un service de messagerie interne sécurisé en s'appuyant sur Matrix, pas de fédération dans ce cas précis.
Les utilisateurs pourront communiquer via l'application mobile Element X ou via leur navigateur sur poste de travail quelle que soit leur position géographique.
Infrastructure Matrix sécurisée et modulaire, comprenant :
-
Synapse : Le serveur Matrix (homeserver).
-
Element : Le client web.
-
Keycloak : Le fournisseur d'identité (SSO).
-
Matrix Authentication Service (MAS) : Le service d'authentification OAuth2/OIDC.
📋 Prérequis
L'architecture est conçue pour être déployée sur un ou plusieurs serveurs avec Docker, derrière un reverse proxy (HAProxy).
Dans ce lab, 2 VMs, une pour la stack synapse, l'autre pour les services d'authentification Keycloak et MAS.
Domaines publics :
-
matrix.domaine.com → Synapse / Element
-
auth.domaine.com → Keycloak
-
mas.domaine.com → Matrix Authentication Service
Accès aux entrées DNS pour l'ajout des enregistrement de type A.
Certificats TLS : Gérés par HAProxy (Let's Encrypt).
Docker et Docker Compose : Installés sur chaque VM.
Réseau : Les VMs doivent pouvoir communiquer entre elles sur les ports internes.
| VM | IP | Services |
|---|---|---|
| VM1 | 192.168.10.210 | Synapse, Element, Nginx |
| VM2 | 192.168.10.118 | Keycloak, MAS |
🔧 Étape 1 : Installation de Synapse et Element (VM Synapse)
1.1 Structure des dossiers
/srv/docker/matrix/
├── docker-compose.yml
├── data/
│ └── homeserver.yaml
├── element/
│ └── config.json
├── nginx/
│ └── nginx.conf
└── .env
1.2 docker-compose.yml
services:
postgres:
image: postgres:17-alpine
container_name: synapse_db
restart: unless-stopped
environment:
POSTGRES_USER: ${SYN_POSTGRES_USER}
POSTGRES_PASSWORD: ${SYN_POSTGRES_PASSWORD}
POSTGRES_DB: ${SYN_POSTGRES_DB}
POSTGRES_INITDB_ARGS: --encoding=UTF-8 --lc-collate=C --lc-ctype=C
volumes:
- ./postgres_data:/var/lib/postgresql/data
networks:
- matrix_net
synapse:
image: matrixdotorg/synapse:1.98.0
container_name: synapse
restart: unless-stopped
depends_on:
- postgres
ports:
- "8008:8008"
extra_hosts:
- "auth.domaine.com:XX.XX.XX.XX" # IP publique
volumes:
- ./data:/data
environment:
- SYNAPSE_SERVER_NAME=matrix.domaine.com
- SYNAPSE_REPORT_STATS=no
logging:
driver: journald
options:
tag: "synapse"
labels: "synapse,production"
networks:
- matrix_net
element:
image: vectorim/element-web:v1.12.22
container_name: element
restart: unless-stopped
volumes:
- ./element/config.json:/app/config.json:ro
networks:
- matrix_net
nginx:
image: nginx:stable
container_name: nginx
restart: unless-stopped
volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
networks:
- matrix_net
depends_on:
- synapse
- element
networks:
matrix_net:
driver: bridge
Renseigner le fichier .env pour les variables.
1.3 homeserver.yaml (data/homeserver.yaml)
Générer un secret pour MAS
openssl rand -hex 32
server_name: "matrix.domaine.com"
public_baseurl: https://matrix.domaine.com/
database:
name: psycopg2
args:
user: utilisateur_postgres # identique à ${SYN_POSTGRES_USER}
password: motdepasse_postgres # identique à ${SYN_POSTGRES_PASSWORD}
database: nomdelabase_postgres # identique à ${SYN_POSTGRES_DB}
host: postgres
port: 5432
# Délégation OAuth2 vers MAS
matrix_authentication_service:
enabled: true
endpoint: http://192.168.10.118:8888 # IP de la VM MAS
secret: "secret_généré_avec_openssl"
suppress_key_server_warning: true
1.4 nginx/nginx.conf
server {
listen 80;
server_name matrix.domaine.com;
location ~ ^/(_matrix|_synapse) {
proxy_pass http://synapse:8008;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
location / {
proxy_pass http://element:80;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
}
1.5 element/config.json
{
"default_server_config": {
"m.homeserver": {
"base_url": "https://matrix.domaine.com",
"server_name": "matrix.domaine.com"
}
}
}
🔑 Étape 2 : Installation de Keycloak (VM auth)
2.1 Structure des dossiers
/srv/docker/keycloak/
├── docker-compose.yml
├── .env
└── keycloak_data/
2.2 docker-compose.yml
services:
postgres:
env_file:
- ./.env
image: postgres:17-alpine
container_name: keycloak-db
environment:
POSTGRES_DB: ${KC_POSTGRES_DB}
POSTGRES_USER: ${KC_POSTGRES_USER}
POSTGRES_PASSWORD: ${KC_POSTGRES_PASSWORD}
volumes:
- ./postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${KC_POSTGRES_USER} -d ${KC_POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- keycloak_net
keycloak:
env_file:
- ./.env
image: quay.io/keycloak/keycloak:26.6.4
container_name: keycloak
environment:
KC_DB: ${KC_DB}
KC_DB_URL: ${KC_DB_URL}
KC_DB_USERNAME: ${KC_DB_USERNAME}
KC_DB_PASSWORD: ${KC_DB_PASSWORD}
KC_HOSTNAME: ${KC_HOSTNAME}
KC_HTTP_PORT: 80
KC_BOOTSTRAP_ADMIN_USERNAME: ${KC_BOOTSTRAP_ADMIN_USERNAME}
KC_BOOTSTRAP_ADMIN_PASSWORD: ${KC_BOOTSTRAP_ADMIN_PASSWORD}
KC_PROXY_HEADERS: xforwarded
KC_HOSTNAME_STRICT: false
KC_LOG_LEVEL: info
logging:
driver: journald
options:
tag: "keycloak-auth"
labels: "keycloak,production"
volumes:
- ./keycloak_data:/opt/keycloak/data
depends_on:
- postgres
command: start --http-enabled=true
networks:
- keycloak_net
ports:
- "80:80"
networks:
keycloak_net:
driver: bridge
Renseigner le fichier .env pour les variables.
2.3 Configuration du client OIDC dans Keycloak
Sur le site https://it-tools.tech/ulid-generator générer un ULID
Se connecter à l'interface d'administration : https://auth.domaine.com
Créer un realm (ex: domaine.com).
Aller dans Clients → Create client.
Paramétrer le client mas :
Client ID : mas
Client authentication : On (confidential)
Authentication flow : Standard flow (activé)
Direct access grants : Off
Implicit flow : Off
Valid redirect URIs : https://mas.domaine.com/upstream/callback/ULID
Web origins : https://mas.domaine.com
Dans l'onglet Credentials, noter le Client secret.
2.4 Configuration fail2ban
sudo vim /etc/fail2ban/filter.d/keycloak-journald.conf
[Definition]
failregex = \s+WARN\s+\[org\.keycloak\.events\]\s+.*type="LOGIN_ERROR".*ipAddress="<HOST>".*$
ignoreregex =
sudo vim /etc/fail2ban/jail.local
enabled = true
port = 80
filter = keycloak-journald
backend = systemd
journalmatch = CONTAINER_TAG=keycloak-auth
maxretry = 3
findtime = 300
bantime = 86400
action = iptables-multiport[name=keycloak, port="80", protocol=tcp]
sudo systemctl restart fail2ban
sudo fail2ban-client status keycloak
sudo iptables -L -n | grep keycloak
🔐 Étape 3 : Installation de Matrix Authentication Service (VM auth)
3.1 Structure des dossiers
/srv/docker/mas/
├── docker-compose.yml
├── config.yaml
├── .env
└── db_data/
3.2 docker-compose.yml
services:
db:
image: postgres:17-alpine
container_name: mas-db
restart: unless-stopped
environment:
POSTGRES_USER: ${MAS_POSTGRES_USER}
POSTGRES_PASSWORD: ${MAS_POSTGRES_PASSWORD}
POSTGRES_DB: ${MAS_POSTGRES_DB}
volumes:
- ./db_data:/var/lib/postgresql/data
networks:
- mas_net
matrix-auth-service:
image: ghcr.io/element-hq/matrix-authentication-service:v1.19.0
container_name: matrix-auth-service
restart: unless-stopped
depends_on:
- db
ports:
- "8888:8080" # Port exposé pour Synapse et HAProxy
- "8081:8081" # Healthcheck
volumes:
- ./config.yaml:/app/config/config.yaml:ro
networks:
- mas_net
networks:
mas_net:
driver: bridge
3.3 Génération et configuration de config.yaml
Génération :
docker run ghcr.io/element-hq/matrix-authentication-service config generate > config.yaml
Configuration minimale config.yaml :
http:
listeners:
- name: web
resources:
- name: discovery
- name: human
- name: oauth
- name: compat
- name: graphql
- name: assets
binds:
- address: '[::]:8080'
proxy_protocol: false
- name: internal
resources:
- name: health
binds:
- host: localhost
port: 8081
proxy_protocol: false
trusted_proxies:
- 192.168.10.0/24
- 172.16.0.0/12
- 10.0.0.0/10
- 127.0.0.1/8
- fd00::/8
- ::1/128
public_base: https://mas.domaine.com
issuer: https://mas.domaine.com
database:
uri: postgres://mas:833ea31192d18d8029ebaad5a2e6fa0904fc4bbc4c7c51e1842f52b7cdc4876d@db:5432/mas
max_connections: 10
min_connections: 0
connect_timeout: 30
idle_timeout: 600
max_lifetime: 1800
email:
from: '"domaine Chat" <noreply@domaine.com>'
reply_to: '"domaine Support" <noreply@domaine.com>'
transport: smtp
smtp:
host: smtp.domaine.com
port: 587
tls: starttls
username: "user@domaine.com"
password: "motdepasse"
mode: starttls
hostname: domaine.com
secrets:
données générées automatiquement
passwords:
enabled: true
schemes:
- version: 1
algorithm: argon2id
minimum_complexity: 3
matrix:
kind: synapse
homeserver: matrix.domaine.com
secret: généré_automatiquement
endpoint: http://192.168.10.210:8008
upstream_oauth2:
providers:
- id: "ULID généré précédemment"
human_name: "Keycloak"
brand_name: "keycloak"
issuer: "https://auth.domaine.com/realms/mon_realms"
client_id: "mas"
client_secret: "secret_généré_par_keycloak"
token_endpoint_auth_method: "client_secret_basic"
scope: "openid profile email"
claims_imports:
localpart:
action: require
template: "{{ user.preferred_username }}"
displayname:
action: suggest
template: "{{ user.name }}"
email:
action: suggest
template: "{{ user.email }}"
3.4 Validation de la configuration
docker run -v /srv/docker/mas:/config ghcr.io/element-hq/matrix-authentication-service --config /config/config.yaml config check
🌐 Étape 4 : Configuration du Reverse Proxy HAProxy
Configuration pour HAProxy :
# Backend Matrix (matrix.domaine.com)
frontend public
bind *:443 ssl crt /etc/ssl/private/domaine.com.pem
bind *:80
acl is_chat hdr(host) -i matrix.domaine.com
use_backend matrix_nginx if is_chat
backend matrix_nginx
server synapse-vm 192.168.10.210:80 # IP de la VM Synapse (port Nginx)
# Backend Keycloak (auth.domaine.com)
frontend keycloak
bind *:443 ssl crt /etc/ssl/private/domaine.com.pem
acl is_keycloak hdr(host) -i auth.domaine.com
use_backend keycloak_backend if is_keycloak
backend keycloak_backend
server keycloak-vm 192.168.10.118:80 # IP de la VM Keycloak
# Backend MAS (mas.domaine.com)
frontend mas
bind *:443 ssl crt /etc/ssl/private/domaine.com.pem
acl is_mas hdr(host) -i mas.domaine.com
use_backend mas_backend if is_mas
backend mas_backend
server mas-vm 192.168.10.118:8888 # IP de la VM MAS (port exposé)
🔄 Étape 5 : Synchronisation et démarrage
5.1 Ordre de démarrage
-
PostgreSQL (Synapse, Keycloak, MAS)
-
Keycloak
-
MAS
-
Synapse
-
Element et Nginx
5.2 Commandes de démarrage
# VM Synapse
docker compose up -d
# VM Keycloak
docker compose up -d
# VM MAS
docker compose up -d
5.3 Vérification des services
Connectivité Synapse → MAS :
# Depuis la VM Synapse
curl -v http://192.168.10.118:8888/health
Connectivité MAS → Synapse :
# Depuis la VM MAS
curl -v http://192.168.10.210:8008/_matrix/client/versions
🧪 Étape 6 : Tests d'authentification
https://mas.domaine.com/login.
"Se connecter avec Keycloak".
Saisir les identifiants.
Après authentification, redirection vers Element.
Si l'utilisateur n'existe pas : Il est créé automatiquement dans Synapse.
Si erreur de type Localpart not available : utilisateur déjà existant dans synapse, pour le supprimer:
docker compose exec synapse psql -U synapse -d synapse -c "DELETE FROM users WHERE name = '@test:matrix.domaine.com';"
📌 Résumé des paramètres importants
| Service | Domaine / URL | Port interne | Port exposé | Rôle |
|---|---|---|---|---|
| Synapse | matrix.domaine.com | 8008 | 8008 | Homeserver Matrix |
| Element | matrix.domaine.com | 80 | - | Client web |
| Keycloak | auth.domaine.com | 80 | 80 | Fournisseur d'identité |
| MAS | mas.domaine.com | 8080 | 8888 | Service d'authentification OAuth2 |
| HAProxy | - | - | 443 | Reverse proxy TLS |
🔧 Dépannage
| Erreur | Cause | Solution |
|---|---|---|
| Connection refused sur 8008 | Port Synapse non exposé | Ajouter ports: - "8008:8008" dans le service Synapse |
| invalid_redirect_uri | Mauvaise URI dans Keycloak | Vérifier Valid Redirect URIs pour le client mas |
| Localpart not available | Compte existant dans Synapse | Supprimer l'ancien compte ou configurer un préfixe dans MAS |
| 504 Gateway Time-out | Timeout entre MAS et Synapse | Vérifier la connectivité et les timeouts du proxy |
| 401 Unauthorized (introspection) | Secret MAS/Synapse différent | Synchroniser le secret dans les deux configurations |
📚 Commandes utiles
Gestion des utilisateurs MAS :
# Créer un utilisateur
docker compose exec matrix-auth-service mas-cli manage register-user --username nom
# Créer un administrateur
docker compose exec matrix-auth-service mas-cli manage register-user --username admin --admin
# Lister les utilisateurs
docker compose exec matrix-auth-service mas-cli manage list-users
Vérification réseau :
# Tester l'écoute d'un port sur l'hôte
ss -pantu | grep <port>
# Tester la connexion depuis un conteneur
docker compose exec <service> curl -v <url>
🔒 Sécurité
Keycloak : Protéger avec fail2ban.
Certificats : Utiliser Let's Encrypt pour les domaines publics.
Mises à jour : Planifier des mises à jour régulières de Synapse, MAS et Keycloak.
✨ Bonus
📞 Implémentation des fonctionnalités d'appels vocaux et vidéo
Ajout des fonctionnalités d'appels vocaux et vidéo à l'infrastructure Matrix existante, en utilisant LiveKit et le service lk-jwt-service.
📋 Prérequis spécifiques
Créer un sous-domaine dédié, ici: live.domaine.com
Ports ouverts sur le pare-feu (NAT) :
-
443/TCP : Signalisation WebSocket (via HAProxy).
-
7881/TCP : Fallback TCP pour les médias.
-
7882/UDP et une plage 50100-50200/UDP : Flux média principaux.
-
8448/TCP : Fédération Matrix (si activé pour la validation OpenID).
Étape 1 : Configuration de HAProxy
Ajout des règles suivantes à la configuration HAProxy pour rediriger le trafic vers LiveKit et le service JWT.
Ordre des règles important : les règles spécifiques (comme /sfu/get) doivent être placées avant la règle générique.
frontend livekit_https
bind *:443 ssl crt /etc/ssl/private/domaine.com.pem
mode http
# ACL pour les endpoints du service JWT (prioritaires)
acl is_jwt path_beg /sfu/get /healthz /get_token
use_backend jwt_backend if is_jwt
# Par défaut, tout le reste va à LiveKit (signalisation)
default_backend livekit_backend
backend jwt_backend
mode http
# Redirige vers le service JWT sur le port 8081
server jwt-vm 192.168.10.210:8081
backend livekit_backend
mode http
# Redirige vers LiveKit sur le port 7880
server livekit-vm 192.168.10.210:7880
Étape 2 : Configuration des services Docker
Nouveau dossier dédié /srv/docker/live
Générer des clés sécurisées pour LIVEKIT_KEY et LIVEKIT_SECRET.
docker run --rm livekit/livekit-server:latest generate-keys
Créer un nouveau fichier docker-compose.yml (ou ajoutez ces services à la stack synapse existante) pour LiveKit et le service JWT.
Variables d'environnement importantes :
-
LIVEKIT_URL=wss://live.domaine.com : URL WebSocket du serveur LiveKit.
-
LIVEKIT_FULL_ACCESS_HOMESERVERS=matrix.domaine.com : Le server_name de Synapse.
-
LIVEKIT_CS_API_URL_OVERRIDES=matrix.domaine.com=http://192.168.10.210:8008 : Force le service JWT à utiliser l'URL interne de Synapse (port 8008) pour la validation OpenID, au lieu du port de fédération (8448).
services:
lk-jwt-service:
image: ghcr.io/element-hq/lk-jwt-service:latest
container_name: lk-jwt-service
restart: unless-stopped
environment:
LIVEKIT_JWT_BIND: ":8081"
LIVEKIT_URL: "wss://live.domaine.com"
LIVEKIT_KEY: "${LIVEKIT_KEY}"
LIVEKIT_SECRET: "${LIVEKIT_SECRET}"
LIVEKIT_FULL_ACCESS_HOMESERVERS: "matrix.domaine.com"
LIVEKIT_CS_API_URL_OVERRIDES: "matrix.domaine.com=http://192.168.10.210:8008"
ports:
- "8081:8081"
networks:
- matrix_net
livekit:
image: livekit/livekit-server:latest
container_name: livekit
restart: unless-stopped
command: --config /etc/livekit.yaml
volumes:
- ./livekit.yaml:/etc/livekit.yaml:ro
ports:
- "7880:7880/tcp"
- "7881:7881/tcp"
- "7882:7882/udp"
- "50100-50200:50100-50200/udp"
networks:
- matrix_net
Étape 3 : Configuration de LiveKit
Créer un fichier livekit.yaml pour configurer le serveur LiveKit.
port: 7880
bind_addresses:
- "0.0.0.0"
rtc:
tcp_port: 7881
port_range_start: 50100
port_range_end: 50200
node_ip: XX.XX.XX.XX # IP publique
keys:
devkey: "identique_à_LIVEKIT_KEY"
room:
auto_create: false
turn:
enabled: false
domain: localhost
cert_file: ""
key_file: ""
tls_port: 5349
udp_port: 443
external_tls: true
logging:
level: info
Étape 4 : Configuration de Synapse
Dans homeserver.yaml, activer les fonctionnalités expérimentales pour le support des appels.
experimental_features:
msc3266_enabled: true
msc4222_enabled: true
msc4140_enabled: true
max_event_delay_duration: 24h
rc_message:
per_second: 0.5
burst_count: 30
rc_delayed_event_mgmt:
per_second: 1
burst_count: 20
Si utilisation de la fédération, configurer un écouteur HTTPS sur le port 8448 :
listeners:
- port: 8448
tls: true
type: http
x_forwarded: true
resources:
- names: [federation]
compress: false
Étape 5 : Annonce du service aux clients (.well-known)
Mettre à jour le fichier /.well-known/matrix/client servi par le serveur web (Nginx) pour annoncer la présence du backend LiveKit.
{
"m.homeserver": {
"base_url": "https://matrix.domaine.com"
},
"org.matrix.msc4143.rtc_foci": [
{
"type": "livekit",
"livekit_service_url": "https://live.domaine.com"
}
]
}
Étape 6 : Validation
Démarrer les services
docker compose up -d
Surveiller les logs
docker compose logs -f lk-jwt-service livekit
Tester un appel depuis Element X (mobile) ou Element Web. Vérifier que les logs du service JWT et de LiveKit s'affichent.
🔧 Dépannages
| Erreur | Cause probable | Solution |
|---|---|---|
| OPEN_ID_ERROR sur mobile | Le service JWT n'est pas accessible ou ne peut pas valider le token OpenID. | Vérifier la configuration HAProxy, les variables LIVEKIT_CS_API_URL_OVERRIDES et la connectivité entre JWT et Synapse. |
| Failed to look up user info (timeout) | Le service JWT ne parvient pas à joindre Synapse (port 8448) ou utilise le mauvais protocole. | Configurer Synapse en HTTPS sur le port 8448 ou forcer l'URL de l'API Client-Server avec LIVEKIT_CS_API_URL_OVERRIDES. |
| 404 page not found sur /sfu/get | HAProxy ne redirige pas correctement le trafic vers le service JWT. | Vérifier l'ordre des règles HAProxy (les chemins JWT doivent être prioritaires). |
| Pas de logs dans LiveKit | Le client n'atteint pas LiveKit. | Vérifier la résolution DNS de live.domaine.com, les règles HAProxy et le pare-feu (ports UDP). |
💡 Visualisation des logs de LiveKit
Pour surveiller et visualiser les logs de LiveKit en temps réel :
# Afficher les logs en direct
docker logs -f livekit
# Extraire les logs dans un fichier
docker logs livekit --since 1h > livekit_logs.txt
Option : Configurer le logging Docker vers journald et utiliser un outil comme logspout ou un serveur Python minimal pour les visualiser dans un navigateur.
💎 Conclusion
On dispose d'une infrastructure Matrix complète et sécurisée, avec une authentification SSO via Keycloak et une intégration native avec les clients mobiles Element X grâce à Matrix Authentication Service. Cette architecture est modulaire, scalable et prête à être adaptée à d'autres environnements.