Matrix — matrix.poneyworld.net
Homeserver Synapse et ses satellites. Huit conteneurs :
| Service | Rôle | Exposé sur |
|---|---|---|
synapse |
le homeserver | synapse.poneyworld.net (fédération) |
db |
PostgreSQL 18 | — |
redis |
Valkey, cache interne de Synapse (bloc redis: du homeserver) |
— |
nginx |
sert /_matrix (vers synapse:8008) et /.well-known/matrix/ |
matrix.poneyworld.net (API client) |
web |
Element Web | chat.poneyworld.net |
livekit |
SFU pour Element Call | livekit.opytex.org |
lk-jwt |
délivre les jetons LiveKit à Element | livekit.opytex.org/sfu/get, /get_token, /healthz |
coturn |
serveur TURN (appels 1-à-1) | ports 3478 / 5349 / 49160-49200 |
Deux entrées différentes, à ne pas confondre : matrix.poneyworld.net est le
server_name et l'API client, synapse.poneyworld.net est la cible de
délégation annoncée par /.well-known/matrix/server, empruntée par les
serveurs fédérés. Les deux sont vivantes.
Déploiement
Depuis ce dossier, sur chioggia :
sops exec-env secrets.env.yaml 'docker compose up -d'
Les secrets sont déclarés ${VAR:?} : toute commande docker compose
échoue sans l'enveloppe sops exec-env, y compris stop, logs, ps et
exec. Pour en enchaîner plusieurs, une seule enveloppe suffit :
sops exec-env secrets.env.yaml '
docker compose stop synapse &&
docker compose exec -T db psql -U synapse synapse -c "..." &&
docker compose up -d
'
Après une rotation de secret, docker compose up -d répond « up to date » et
garde l'ancienne valeur. Il faut --force-recreate sur le service concerné.
Le nom de projet
name: matrix_poneyworld, en tête du compose, ne suit pas le nom du dossier.
C'est délibéré et c'est porteur : les volumes s'appellent
matrix_poneyworld_matrix_data et matrix_poneyworld_matrix_db.
Retirer cette ligne, ou la modifier, rendrait invisibles les médias, les
clés de signature du serveur, homeserver.yaml et toute la base — la stack
repartirait sur un homeserver vide, sans la moindre erreur.
Où vit vraiment la configuration
Presque rien de la configuration de Synapse n'est dans ce dépôt : elle est
dans /data/homeserver.yaml, à l'intérieur du volume matrix_data.
Les variables SYNAPSE_* de l'env_file ne sont lues que par le mode
generate de l'image, au tout premier démarrage. Elles sont inertes au
runtime : SYNAPSE_ENABLE_REGISTRATION=yes y cohabite sans conséquence avec
un enable_registration: False effectif dans homeserver.yaml. Ne pas s'y
fier pour lire l'état du serveur.
Corollaire pour les secrets : turn_shared_secret, macaroon_secret_key,
registration_shared_secret et le mot de passe de la base sont dans
homeserver.yaml, donc hors SOPS et hors dépôt. Une rotation se fait des
deux côtés (voir plus bas).
Secrets
Cinq valeurs dans secrets.env.yaml, injectées par sops exec-env :
| Variable | Consommé par |
|---|---|
POSTGRES_PASSWORD |
db |
LIVEKIT_KEY, LIVEKIT_SECRET |
livekit (via LIVEKIT_KEYS) et lk-jwt |
TURN_STATIC_AUTH_SECRET, TURN_CLI_PASSWORD |
coturn |
LIVEKIT_KEY tient plus de l'identifiant que du secret. Il est chiffré avec
son secret parce que les deux se tournent toujours ensemble : les séparer sur
deux mécanismes rendrait la rotation facile à ne faire qu'à moitié.
sops edit secrets.env.yaml
Deux mécanismes différents pour deux logiciels
Ni coturn ni LiveKit ne lisent une variable d'environnement depuis leur fichier de configuration — un fichier monté n'est pas interpolé par Compose. C'est le même problème que la clé LAPI de CrowdSec sur Traefik, mais les deux n'offrent pas la même sortie.
LiveKit accepte la paire par variable d'environnement : LIVEKIT_KEYS, au
format clé: secret. livekit.yaml n'a donc pas de bloc keys:, et la
valeur est composée dans environment: du compose, où l'interpolation
${...} s'applique. lk-jwt consomme les deux mêmes variables.
coturn n'offre rien d'équivalent : pas d'interpolation dans
turnserver.conf, pas d'option ...File, pas de lecture d'environnement. La
seule voie sans bricolage est la ligne de commande :
command:
- -c
- /etc/coturn/turnserver.conf
- --static-auth-secret=${TURN_STATIC_AUTH_SECRET:?}
- --cli-password=${TURN_CLI_PASSWORD:?}
Le compromis est là : les deux valeurs sont visibles dans docker inspect coturn et dans la table des processus de l'hôte. Un secret Compose ferait
mieux, coturn ne sait pas le lire.
turnserver.conf ne doit jamais porter de ligne static-auth-secret= ou
cli-password= : ce serait un secret en clair dans le dépôt, et une seconde
source concurrente de celle du command:.
Rotation
turn_shared_secret ne se change pas seul : la même valeur doit être
identique dans homeserver.yaml (Synapse la signe) et côté coturn (il la
vérifie). Deux valeurs différentes ne produisent aucune erreur au démarrage —
les appels échouent simplement à établir un relais.
sops edit secrets.env.yaml # TURN_STATIC_AUTH_SECRET
# homeserver.yaml s'édite depuis l'hôte : l'image Synapse n'a pas d'éditeur.
sudo vi "$(docker volume inspect matrix_poneyworld_matrix_data \
--format '{{.Mountpoint}}')/homeserver.yaml" # turn_shared_secret
sops exec-env secrets.env.yaml '
docker compose up -d --force-recreate coturn &&
docker compose restart synapse
'
macaroon_secret_key (dans homeserver.yaml) signe les jetons d'accès : le
changer déconnecte tous les utilisateurs, sur tous leurs appareils. Ce
n'est pas une opération anodine, c'est une opération annoncée.
registration_shared_secret (dans homeserver.yaml) n'est utilisé que par
register_new_matrix_user. Il se change librement.
POSTGRES_PASSWORD ne se change pas seul non plus : PostgreSQL ignore la
variable sur un PGDATA déjà initialisé. Il faut un ALTER USER dans la base
et la même valeur dans le bloc database: de homeserver.yaml.
Volumes et sauvegarde
| Volume | Contenu | Sauvegarde |
|---|---|---|
matrix_poneyworld_matrix_data |
médias, clé de signature du serveur, homeserver.yaml |
label backup.enable=true + MANUAL_VOLUMES |
matrix_poneyworld_matrix_db |
base PostgreSQL | dump SQL (backup.type=postgres sur db) |
Les labels de volume ne s'appliquent qu'à la création : en ajouter ou en
retirer un ne changerait rien au volume réel, mais ferait diverger sa
déclaration, et Compose proposerait alors de le recréer. Ne pas toucher au
bloc volumes: du compose.
POSTGRES_DB conditionne le dump
backup_docker.sh lit la base à dumper dans l'environnement du conteneur
db :
db_name=$(docker exec "$container" printenv POSTGRES_DB 2>/dev/null || echo "postgres")
Sans POSTGRES_DB, le repli s'applique : le script dumpe la base système
postgres, vide, et journalise un succès. POSTGRES_DB=synapse dans
l'env_file n'a pas d'autre fonction que d'être visible par ce printenv —
PostgreSQL, lui, ne lit cette variable qu'à l'initialisation du cluster.
Vérification, après tout changement touchant db :
zcat /other/backups/db-dumps/matrix_poneyworld.sql.gz | grep -c "CREATE TABLE"
Un 0 signifie que la sauvegarde est vide.
L'IP publique est écrite en dur, à deux endroits
coturn/turnserver.conf (external-ip) et livekit/livekit.yaml (node_ip)
portent tous deux l'adresse publique de chioggia, 188.165.240.182. Les deux
services tournent en bridge : ils ne voient qu'une IP privée et ne peuvent pas
deviner celle qu'il faut annoncer aux clients.
Si la machine change d'adresse, ces deux fichiers doivent changer avec elle. Aucun des deux ne le signalera : coturn et LiveKit démarrent normalement en annonçant une adresse erronée, et seuls les appels échouent — sans erreur dans les logs, puisque du point de vue des serveurs tout va bien.
Le chemin des requêtes clientes
matrix.poneyworld.net passe par Traefik, puis nginx, puis Synapse. Deux
conséquences à ne pas défaire :
- nginx doit propager la chaîne avec
$proxy_add_x_forwarded_for, jamais$remote_addr: ce dernier vaut l'adresse de Traefik, et l'utiliser écrase l'en-tête posé en amont. Synapse voit alors tous les clients sous une seule adresse — la limitation de débit à la connexion devient un compteur partagé par tous les comptes, et les journaux d'authentification perdent leur intérêt. Côté Synapse, le listener 8008 doit porterx_forwarded: true. crowdsec@fileest posé sur les routeursmatrix_nginx(API client) etelement(interface web), délibérément pas sursynapse: ce dernier sert la fédération, qui arrive de milliers de serveurs distants. Une décision de bannissement à cet endroit couperait les correspondants.
Contrôle de la chaîne, une seule adresse en 172.x signalant le problème :
SELECT ip, count(*) FROM user_ips GROUP BY ip ORDER BY count DESC LIMIT 10;
Versions d'images
Toutes les images sont épinglées au patch. Les tags d'amont ne suivent pas la
même forme : livekit-server préfixe d'un v, lk-jwt-service non — bien
que son tag git le porte. Le label OCI
org.opencontainers.image.version du conteneur donne la forme du tag d'image,
c'est lui qui fait foi.
livekit et lk-jwt se montent ensemble : ils s'authentifient mutuellement et
suivent le même protocole MatrixRTC.
Références :