Matrix: Server Synapse
Scritto il: 2026-04-13
Preambolo
Bene, ci siamo: è giunto il momento in cui si rende necessario parlare di un mastodonte: SYNAPSE, il server per sfruttare il protocollo Matrix per messaggistica sicura (E2EE) federata. Faccio presente che sicura non significa senza metadata. Matrix è una fonte ifinita di metadata, proprio perché è federato: tuttavia se self-hostate il vostro server, potete stare certi che quei metadati non finiranno in mano a chi non vorrete, salvo contattare utenze che "abitano" altri server.
DISCLAIMER
Ragazzi 'sta roba è fatta girare su un mini PC per onore della cronaca. Non mi sono inerpicato in load balancing dei postgres, worker push, media worker, kubernetes. È per far vedere l'architettura come è e come farla scalare. Per l'alta affidabilità c'è da mettere mano alla duplicazione dei servizi e bilanciamento su cluster.
Tassonomia
Il tutto sarà così fatto
Cloudflare Tunnel
`
|
OIDC Provider
|
`-- Server Nginx
` .- worker
| |
+-- Matrix server +-- worker
| | |
MAS | `-- worker
|
+--- Database Postgres
|
|
`--- Bridges- Provider OIDC: gestisce la registrazione degli utenti
- MAS, aka Matrix Authentication Service: è il connettore tra OIDC e Matrix
- Worker: servono per bilanciare i carichi per scalabilità
- Server Nginx: è il bilanciatore ed è importantissimo configurarlo per smistare il carico tra i vari worker
- Bridges: sono per l'interoperabilità con altri protocolli (come Whatsapp, Signal, Telegram, ...)
NOTA: utilizzeremo lo SPLIT domain. Significa che
- il server Synapse sarà in ascolto su : matrix.dreadful.work
- l'handle sarà: user:dreadful.work e non user:matrix.dreadful.work
- Quando si trova qualcosa di [HIDDEN], significa che o è stato configurato dall'applicazione o è stato generato con
openssl rand -hex 32.
ATTENZIONE: il provider OIDC dovete averlo già installato! Come fare è già stato affrontato, quindi non coprirò l'argomento in questa pagina. In alternativa potete utilizzare il MAS come OIDC provider a tutti gli effetti per il vostro server Synapse abilitando le registrazioni.
Installazione di Synapse: la base da cui tutto parte
Iniziamo con il creare la directory di lavoro
sudo mkdir -p /opt/dreadful.work/vector && cd opt/dreadful.work/vectorGenerazione del file di configurazione
La prima configurazione richiede il comando generate:
docker run -it --rm \
-v $(pwd)/data:/data \
-e SYNAPSE_SERVER_NAME=dreadful.work \
-e SYNAPSE_REPORT_STATS=no \
ghcr.io/element-hq/synapse:develop generateQuesto comando genererà il file di configurazione sotto data/homeserver.yaml, e sarà il file che dovrà essere modificato per aggiungere la public_baseurl.
server_name: "dreadful.work"
public_baseurl: "matrix.dreadful.work" # <-- Da aggiungere
pid_file: /data/homeserver.pid
listeners:
- port: 8008
resources:
- compress: false
names:
- client
- federation
tls: false
type: http
x_forwarded: true
database:
name: sqlite3
args:
database: /data/homeserver.db
log_config: "/data/dreadful.work.log.config"
media_store_path: /data/media_store
registration_shared_secret: "[HIDDEN]"
report_stats: false
macaroon_secret_key: "[HIDDEN]"
form_secret: "[HIDDEN]"
signing_key_path: "/data/dreadful.work.signing.key"
trusted_key_servers:
- server_name: "matrix.org"Creazione docker network
Utilizzeremo una docker network appositamente per Synapse:
docker network create synapse
be636df73d51c5275918e9f8f07fbc748d0da6b4ddd250a2cba8c408a1f609ab
Il Docker Compose File
Vista la complessità del dispiegamento che stiamo intentando, è il caso di creare un docker compose file. Sarà poi modificato nel tempo per costruire l'implementazione finale.
Partiamo con l'environment: .env (password del DB generata con openssl);
# Configurazione Synapse
SYNAPSE_CONFIG_PATH=/data/homeserver.yaml
SYNAPSE_SERVER_NAME=dreadful.work
SYNAPSE_REPORT_STATS=yes
VIRTUAL_HOST=dreadful.work
VIRTUAL_PORT=8008
# Configurazione Database
POSTGRES_USER=synapse
POSTGRES_DB=synapse
POSTGRES_PASSWORD=[HIDDEN]Il docker-compose.yml invece sarà
services:
synapse:
image: ghcr.io/element-hq/synapse:develop
restart: unless-stopped
networks:
- synapse
environment:
- SYNAPSE_CONFIG_PATH=${SYNAPSE_CONFIG_PATH}
- VIRTUAL_HOST=${VIRTUAL_HOST}
- VIRTUAL_PORT=${VIRTUAL_PORT}
- SYNAPSE_SERVER_NAME=${SYNAPSE_SERVER_NAME}
- SYNAPSE_REPORT_STATS=${SYNAPSE_REPORT_STATS}
volumes:
- ./data:/data
ports:
- 8008:8008/tcp
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
redis:
image: redis:7-alpine
restart: unless-stopped
networks:
- synapse
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
volumes:
- ./redis:/data
db:
image: docker.io/postgres:16-alpine
restart: unless-stopped
networks:
- synapse
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
volumes:
- ./postgres:/var/lib/postgresql/data
networks:
synapse:
external: trueNiente docker compose up ancora: dobbiamo dire a synapse che deve utilizzare postgres e non sqlite. Dobbiamo editare data/homeserver.yml, sostituendo
database:
name: sqlite3
args:
database: /data/homeserver.dbcon
database:
name: psycopg2
args:
user: synapse
password: [HIDDEN]
host: db
database: synapse
cp_min: 5
cp_max: 10Ovviamente la password del DB è quella generata con openssl, e che è stata scritta nel file .env. Solo a questo punto si può procedere con il prossimo:
docker compose up -d; docker compose logs -fÈ quindi utilizzabile? No per niente, ma almeno vediamo che non ci siano errori.
Reverse proxy con nginx (e i balletti con cloudflare)
Ora c'è un distinguo:
- matrix.dreadful.work
- dreadful.work Mentre il primo si occupa solo di Synapse, il secondo deve contenere i well-known per permettere la gestione degli handle. Quindi mentre il primo può essere containerizzato qui, il secondo dovrà essere modificato a mano perché si presuppone che sia altrove, a servire direttamente il dominio, per esempio. Di conseguenza dentro questo container andremo a gestire unicamente
matrix.dreadful.work.
Essendo dietro Cloudflare, è necessario andare a mettere le mani direttamente sul pannell di amministrazione del dominio.
Nella gestione del dominio, a destra: SSL/TLS -> Server di Origine. Io utilizzo la chiave ECC e lascio *.dreadful.work e dreadful.work.
Torniamo sulla nostra macchina lasciando la pagina coi certificati visibile:
mkdir -p nginx/conf.d
mkdir nginx/certsSalviamo quindi la chiave privata e quella pubblica in:
- nginx/certs/origin.key
- nginx/certs/origin.crt
Nuovamente sulla pagina di Cloudflare, andiamo nella sezione Zero Trust e creiamo un connettore docker: segnamoci il TUNNEL_TOKEN
Popoliamo quindi nginx/conf.d/matrix.dreadful.work.conf
server {
listen 443 ssl http2;
server_name matrix.dreadful.work;
# Usiamo i certificati Origin CA di Cloudflare
ssl_certificate /etc/nginx/certs/origin.crt;
ssl_certificate_key /etc/nginx/certs/origin.key;
# Ottimizzazioni per la sicurezza
ssl_protocols TLSv1.2 TLSv1.3;
client_max_body_size 100M;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
location / {
proxy_pass http://synapse:8008;
proxy_read_timeout 600s;
}
# Federazione Matrix
location /.well-known/matrix/server {
return 200 '{"m.server": "matrix.dreadful.work:443"}';
add_header Content-Type application/json;
add_header Access-Control-Allow-Origin *;
}
location /.well-known/matrix/client {
return 200 '{"m.homeserver": {"base_url": "https://matrix.dreadful.work"}}';
add_header Content-Type application/json;
add_header Access-Control-Allow-Origin *;
}
}Modifichiamo il docker compose file per contenere anche nginx aggiungendo:
nginx:
image: nginx:alpine
restart: unless-stopped
networks:
- synapse
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./nginx/certs:/etc/nginx/certs:ro # Montiamo la cartella con origin.crt e origin.key
tunnel:
image: cloudflare/cloudflared:latest
restart: unless-stopped
networks:
- synapse
command: tunnel run
environment:
- TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN}
- TUNNEL_ORIGIN_CERT_CHECK=falseAggiungiamo anche al file .env la parte necessaria per nginx:
CLOUDFLARE_TUNNEL_TOKEN=TUNNEL_TOKEN_SEGNATO_PRIMAFacciamo lo startup sia di nginx che del tunnel
docker compose up -d nginx tunnelE torniamo nuovamente su cloudflare per configurare il connettore appena creato aggiungendo l'applicazione pubblicata:
- Subdomain: matrix
- Dominio: dreadful.work
- Servizio: https
- Url: nginx:443
- Altre impostazioni di applicazione:
- TLS
- Nessuna verifica TLS: on
- Connessione HTTP2: on
- TLS
A questo punto visitando https://matrix.dreadful.work comparirà la pagina base di Synapse su protocollo Matrix.
Siccome vogliamo gli handle diversi dall'effettivo nome di dominio, è necessario modificare anche nginx che serve il dominio principale, ovvero dreadful.work. Nello specifico è necessario aggiungere:
location = /.well-known/matrix/server {
default_type application/json;
add_header Access-Control-Allow-Origin *;
return 200 '{"m.server": "matrix.dreadful.work:443"}';
}
location = /.well-known/matrix/client {
default_type application/json;
add_header Access-Control-Allow-Origin *;
return 200 '{"m.homeserver": {"base_url": "https://matrix.dreadful.work"}, "m.identity_server": {"base_url": "https://vector.im"}}';
}Ora possiamo vedere se è tutto correttamente configurato per lo split domain andando su https://federationtester.matrix.org/ e inserendo dreadful.work nel test.
I worker
Un problema abbastanza devastante è il fatto che se il processo principale fa tutto (sincronizzazione, federazione, scambi, etc) il tutto va in overload ed è lento. È necessario scalare. Ecco perché creiamo più workers ognuno dei quali è specializzato in qualcosa e fa solo quello.
Modifichiamo docker-compose.yml in modo da contenerli, aggiungendo:
generic-federation-presence:
image: "ghcr.io/element-hq/synapse:develop"
container_name: generic-federation-presence
restart: unless-stopped
entrypoint: ["/start.py", "run", "--config-path=/data/homeserver.yaml", "--config-path=/data/workers/generic-federation-presence.yaml"]
volumes:
- ./data:/data
environment:
SYNAPSE_WORKER: synapse.app.generic_worker
depends_on:
- db
- redis
- synapse
networks:
- synapse
generic-sync-1:
image: "ghcr.io/element-hq/synapse:develop"
container_name: generic-sync-1
restart: unless-stopped
entrypoint: ["/start.py", "run", "--config-path=/data/homeserver.yaml", "--config-path=/data/workers/generic-sync-1.yaml"]
volumes:
- ./data:/data
environment:
SYNAPSE_WORKER: synapse.app.generic_worker
depends_on:
- db
- redis
- synapse
networks:
- synapse
generic-sync-2:
image: "ghcr.io/element-hq/synapse:develop"
container_name: generic-sync-2
restart: unless-stopped
entrypoint: ["/start.py", "run", "--config-path=/data/homeserver.yaml", "--config-path=/data/workers/generic-sync-2.yaml"]
volumes:
- ./data:/data
environment:
SYNAPSE_WORKER: synapse.app.generic_worker
depends_on:
- db
- redis
- synapse
networks:
- synapse
generic-sync-initial:
image: "ghcr.io/element-hq/synapse:develop"
container_name: generic-sync-initial
restart: unless-stopped
entrypoint: ["/start.py", "run", "--config-path=/data/homeserver.yaml", "--config-path=/data/workers/generic-sync-initial.yaml"]
volumes:
- ./data:/data
environment:
SYNAPSE_WORKER: synapse.app.generic_worker
depends_on:
- db
- redis
- synapse
networks:
- synapse
event-persister:
image: "ghcr.io/element-hq/synapse:develop"
container_name: event-persister
restart: unless-stopped
entrypoint: ["/start.py", "run", "--config-path=/data/homeserver.yaml", "--config-path=/data/workers/event-persister.yaml"]
volumes:
- ./data:/data
environment:
SYNAPSE_WORKER: synapse.app.generic_worker
depends_on:
- synapse
- db
- redis
networks:
- synapse
Per ognuno di questi deve essere creato un worker file all'interno di data/workers/ quindi
mkdir data/workersAl suo interno avremo: event-persister.yaml
worker_app: synapse.app.generic_worker
worker_name: event-persister
# Listener per la comunicazione interna (Replication)
worker_listeners:
- port: 8086
type: http
resources:
- names: [replication]
# Configurazione Redis (deve puntare al nome del servizio nel compose)
redis:
enabled: true
host: redis
port: 6379poi generic-federation-presence.yaml
worker_app: synapse.app.generic_worker
worker_name: generic-federation-presence
worker_types:
- federation_sender
- federation_reader
- federation_event_auth
- presence
- typing
- to_device
worker_listeners:
- port: 8083
type: http
tls: false
resources:
- names: [federation]
compress: true
read_only: true
redis:
enabled: true
host: redispoi generic-sync-1.yaml
worker_app: synapse.app.generic_worker
worker_name: generic-sync-1
worker_types:
- sync
worker_listeners:
- port: 8082
type: http
tls: false
resources:
- names: [client]
compress: true
read_only: true
redis:
enabled: true
host: redis
e ancora generic-sync-2.yaml
worker_app: synapse.app.generic_worker
worker_name: generic-sync-2
worker_types:
- sync
worker_listeners:
- port: 8084
type: http
tls: false
resources:
- names: [client]
compress: true
read_only: true
redis:
enabled: true
host: redis
e per finire (per ora) generic-sync-initial.yaml
worker_app: synapse.app.generic_worker
worker_name: generic-sync-initial
worker_types:
- sync
worker_listeners:
- port: 8085
type: http
tls: false
resources:
- names: [client]
compress: true
read_only: true
redis:
enabled: true
host: redis
mettiamo a posto i permessi
chown 991:991 data/workers -Re modifichiamo il mapping di data/homeserver.yml
server_name: "dreadful.work"
public_baseurl: "https://matrix.dreadful.work"
pid_file: /data/homeserver.pid
listeners:
- port: 8008
tls: false
type: http
x_forwarded: true
resources:
- names: [client, federation, replication]
compress: true
worker_types:
- frontend_proxy
- federation_sender
instance_map:
main:
host: synapse
port: 8008
generic-sync-1:
host: generic-sync-1
port: 8082
generic-federation-presence:
host: generic-federation-presence
port: 8083
generic-sync-2:
host: generic-sync-2
port: 8084
generic-sync-initial:
host: generic-sync-initial
port: 8085
event-persister:
host: event-persister
port: 8086
stream_writers:
events:
- event-persister
federation_rr_transactions_per_second: 100
federation_rr_concurrent_transmissions: 50
redis:
enabled: true
host: redis
port: 6379
database:
name: psycopg2
[...resto della configurazione ...]
Tecnicamente adesso possiamo anche proseguire con la configurazione di nginx per bilanciare sui worker, ma non lo faremo ancora. Dobbiamo prima installare il Matrix Authentication Service o MAS.
Matrix Authentication Service
Il Matrix Authentication Service (MAS) è un'implementazione di riferimento di un Authorization Server conforme agli standard OAuth 2.0 e OpenID Connect (OIDC), progettato specificamente per fungere da identity provider (IdP) nell'ecosistema del protocollo Matrix.
Caratteristiche Formali dell'Architettura
- Disaccoppiamento dell'Autenticazione (Separation of Concerns)
Tradizionalmente, l'autenticazione in Matrix era integrata direttamente nel Homeserver (ad esempio in Synapse). Il MAS sposta questa responsabilità all'esterno. Formalmente, questo trasforma il Homeserver in un Resource Server (RS) secondo il framework OAuth 2.0, delegando la validazione delle identità e l'emissione dei token al MAS (Authorization Server).
- Standardizzazione dei Protocolli
Il MAS implementa le specifiche standard del settore per eliminare le API di autenticazione custom di Matrix (definite "Legacy"):
RFC 6749 (OAuth 2.0): Per l'autorizzazione dell'accesso alle risorse.
OpenID Connect Core 1.0: Per lo strato di identità sopra OAuth 2.0.
Protocolli di backend: Supporta l'interfacciamento con database relazionali (PostgreSQL) per la persistenza dei dati e protocolli come LDAP o altri IdP upstream per l'autenticazione federata.
Gestione del Ciclo di Vita delle Sessioni Il MAS non si limita al login, ma gestisce formalmente:
- Emissione e Revoca dei Token: Gestione di Access Tokens a breve scadenza e Refresh Tokens.
- Device Management: Associazione univoca tra sessioni OIDC e device_id del protocollo Matrix.
- Compatibilità (Compatibility Layer): Fornisce un'interfaccia di traduzione per i client che non supportano ancora nativamente OIDC, simulando le vecchie API di login di Matrix.
Installazione del MAS
Siamo ancora sotto /opt/dreadful.work/vector. Creiamo il file di configurazione "vergine"
mkdir mas-config
docker run ghcr.io/element-hq/matrix-authentication-service config generate > mas-config/config.yamlVediamo un po' come è fatto questo file incredibile: cat mas-config/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.0.0/16
- 172.16.0.0/12
- 10.0.0.0/10
- 127.0.0.1/8
- fd00::/8
- ::1/128
public_base: http://[::]:8080/
issuer: http://[::]:8080/
database:
uri: postgresql://
max_connections: 10
min_connections: 0
connect_timeout: 30
idle_timeout: 600
max_lifetime: 1800
email:
from: '"Authentication Service" <root@localhost>'
reply_to: '"Authentication Service" <root@localhost>'
transport: blackhole
secrets:
encryption: [HIDDEN]
keys:
- key: |
-----BEGIN RSA PRIVATE KEY-----
[HIDDEN]
-----END RSA PRIVATE KEY-----
- key: |
-----BEGIN EC PRIVATE KEY-----
[HIDDEN]
-----END EC PRIVATE KEY-----
- key: |
-----BEGIN EC PRIVATE KEY-----
[HIDDEN]
-----END EC PRIVATE KEY-----
- key: |
-----BEGIN EC PRIVATE KEY-----
[HIDDEN]
-----END EC PRIVATE KEY-----
passwords:
enabled: true
schemes:
- version: 1
algorithm: argon2id
minimum_complexity: 3
matrix:
kind: synapse
homeserver: localhost:8008
secret: [HIDDEN]
endpoint: http://localhost:8008/Qui ci sono un po' di modifiche da fare, come è facile immaginare.
ATTENZIONE: secondo le specifiche il MAS deve stare su un dominio a sé stante rispetto a Synapse. Di conseguenza utilizzeremo auth.dreadful.work.
Il file che vediamo è da personalizzare pesantemente. Ecco come deve risultare:
http:
public_base: https://auth.dreadful.work
issuer: https://auth.dreadful.work
listeners:
- name: web
resources:
- name: discovery
- name: human
- name: oauth
- name: compat
- name: graphql
- name: assets
binds:
- host: 0.0.0.0
port: 8080
proxy_protocol: false
database:
uri: postgres://synapse:[HIDDEN]@mas-db/synapse
max_connections: 10
min_connections: 0
connect_timeout: 30
idle_timeout: 600
max_lifetime: 1800
email:
from: '"Authentication Service" <[email protected]>'
reply_to: '"Authentication Service" <[email protected]>'
transport: blackhole
secrets:
encryption: [HIDDEN]
keys:
- key: |
-----BEGIN RSA PRIVATE KEY-----
[HIDDEN]
-----END RSA PRIVATE KEY-----
- key: |
[HIDDEN]
- key: |
[HIDDEN]
-----END EC PRIVATE KEY-----
- key: |
[HIDDEN]
passwords:
enabled: true
schemes:
- version: 1
algorithm: argon2id
minimum_complexity: 3
matrix:
kind: synapse
homeserver: dreadful.work
secret: [HIDDEN]
endpoint: http://synapse:8008
upstream_oauth2:
providers:
- id: 01HFRQFT5QFMJFGF01P7JAV2ME
human_name: Authentik
issuer: "https://idp.multihop.network/application/o/dreadful-synapse/" #
client_id: "[HIDDEN]"
client_secret: "[HIDDEN]"
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 }}"
avatar:
action: suggest
template: "{{ user.picture }}"Ok questa sarà lunga da digerire, analizziamo le righe salienti
- 2:
public_base: https://auth.dreadful.work - 3:
issuer: https://auth.dreadful.work: queste due devono puntare al MAS perché indicano chi farà da oidc provider per Synapse - 18: la password deve essere generata con
openssl rand -hex 32e deve essere corrispondente a quella che metteremo per il DB di MAS nel docker compose (ora lo abbiamo girato con docker run) - [54-75]: questa è la configurazione dell'OIDC provider. Esatto. Dovete aver configurato un Provider + App in modalità confidential su Authentik: issuer, client_id, client_secret sono forniti da Authentik. I campi hanno suggest invece di require per permettere all'utente di valorizzare o meno i campi. Se ci sono, li suggerisce, ma sono modificabili.
- 56: Questo ID deve tassativamente essere un ULID valido generabile anche online https://ulidgenerator.com/
Se volete abilitare le registrazioni e/o non utilizzare l'OIDC provider esterno dovete aggiungere/sostituire il blocco upstream_oauth2 con:
account:
password_registration_enabled: true
email_change_allowed: true
displayname_change_allowed: true
password_change_allowed: true
password_recovery_enabled: trueIn questo caso vi conviene abilitare anche le mail in uscita (per esempio con Brevo), cancellando completamente il blocco:
email:
from: '"Authentication Service" <[email protected]>'
reply_to: '"Authentication Service" <[email protected]>'
transport: blackholee posizionando:
email:
smtp_host: "smtp-relay.brevo.com"
smtp_port: 587
smtp_user: "[HIDDEN]"
smtp_pass: "[HIDDEN]"
require_transport_security: true
notif_from: "[email protected]"
app_name: multihopnetworkmatrix
client_base_url: https://matrix.dreadful.woek
invite_client_location: https://dreadful.work
Modifica del docker compose file
Aggiungiamo quindi il servizio MAS e relativo DB al docker-compose.yml:
mas-db:
image: docker.io/postgres:16-alpine
restart: unless-stopped
networks:
- synapse
environment:
- POSTGRES_USER=${MAS_POSTGRES_USER}
- POSTGRES_DB=${MAS_POSTGRES_DB}
- POSTGRES_PASSWORD=${MAS_POSTGRES_PASSWORD}
- POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${MAS_POSTGRES_USER} -d ${MAS_POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
volumes:
- ./postgres-mas:/var/lib/postgresql/data
mas:
image: ghcr.io/element-hq/matrix-authentication-service:latest
restart: unless-stopped
networks:
- synapse
command: ["server"]
volumes:
- "./mas-config/config.yaml:/config.yaml:ro"
depends_on:
mas-db:
condition: service_healthy
synapse:
condition: service_startedVa da sé che dentro .env devono essere aggiunte:
MAS_POSTGRES_USER=synapse
MAS_POSTGRES_DB=synapse
MAS_POSTGRES_PASSWORD=[HIDDEN]Modifica dell'homeserver
Anche data/homeserver.yml deve essere aggiustato. Dobbiamo aggiungere:
matrix_authentication_service:
enabled: true
endpoint: http://mas:8080
secret: "[HIDDEN]"Al posto del secret ci dobbiamo scrivere il secret trovato nel file mas-config/config.yml alla riga 52.
Nginx per auth
Avendo adesso anche questo dominio in più da gestire, ovvero auth.dreadful.work, è necessario servirlo proprio come abbiamo fatto con matrix.dreadful.work. Creiamo quindi il file nginx/conf.d/auth.dreadful.work.conf:
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name auth.dreadful.work;
ssl_certificate /etc/nginx/certs/origin.crt;
ssl_certificate_key /etc/nginx/certs/origin.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_http_version 1.1;
# Nome del servizio Docker per il MAS
proxy_pass http://mas:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_redirect off;
}
}
server {
listen 80;
server_name auth.dreadful.work;
return 301 https://$host$request_uri;
}Anche in questo caso dobbiamo andare a pubblicare il connettore, sempre quello di prima, su Cloudflare in modo che punti a https://nginx:443 per il dominio auth.dreadful.work
Modifica di nginx per supportare i worker
Ok siamo quasi in dirittura di arrivo. Dobbiamo modificare il file nginx/conf.d/matrix.dreadful.work.conf in modo che supporti tutti quei begli endpoint che abbiamo creato con i worker.
Vi avviso è un bagno di sangue perché da quelle poche righe messe in croce diventa un trattato peggio del De Rerum Natura:
upstream synapse_mas {
server mas:8080;
}
upstream synapse_master {
server synapse:8008;
}
upstream synapse_federation {
server generic-federation-presence:8083;
}
upstream synapse_client {
server generic-sync-1:8082;
}
map $request_uri $synapse_backend {
default synapse_master;
~*^/_matrix/client/v3/(login|logout|refresh) synapse_mas;
~*^/_matrix/client/r0/(login|logout|refresh) synapse_mas;
~*^/_matrix/client/(r0|v3)/sync$ synapse_client;
~*^/_matrix/client/(api/v1|r0|v3)/events$ synapse_client;
~*^/_matrix/client/(api/v1|r0|v3)/initialSync$ synapse_client;
~*^/_matrix/client/(api/v1|r0|v3)/rooms/[^/]+/initialSync$ synapse_client;
~*^/_matrix/client/(api/v1|r0|v3|unstable)/createRoom$ synapse_client;
~*^/_matrix/client/(api/v1|r0|v3|unstable)/publicRooms$ synapse_client;
~*^/_matrix/client/(api/v1|r0|v3|unstable)/rooms/.*/joined_members$ synapse_client;
~*^/_matrix/client/(api/v1|r0|v3|unstable)/rooms/.*/context/.*$ synapse_client;
~*^/_matrix/client/(api/v1|r0|v3|unstable)/rooms/.*/members$ synapse_client;
~*^/_matrix/client/(api/v1|r0|v3|unstable)/rooms/.*/state$ synapse_client;
~*^/_matrix/client/v1/rooms/.*/hierarchy$ synapse_client;
~*^/_matrix/client/unstable/org.matrix.msc2716/rooms/.*/batch_send$ synapse_client;
~*^/_matrix/client/unstable/im.nheko.summary/rooms/.*/summary$ synapse_client;
~*^/_matrix/client/(r0|v3|unstable)/account/3pid$ synapse_client;
~*^/_matrix/client/(r0|v3|unstable)/account/whoami$ synapse_client;
~*^/_matrix/client/(r0|v3|unstable)/devices$ synapse_client;
~*^/_matrix/client/versions$ synapse_client;
~*^/_matrix/client/(api/v1|r0|v3|unstable)/rooms/.*/event/ synapse_client;
~*^/_matrix/client/(api/v1|r0|v3|unstable)/joined_rooms$ synapse_client;
#~*^/_matrix/client/(api/v1|r0|v3|unstable)/search$ synapse_client;
~*^/_matrix/client/(r0|v3|unstable)/keys/query$ synapse_client;
~*^/_matrix/client/(r0|v3|unstable)/keys/changes$ synapse_client;
~*^/_matrix/client/(r0|v3|unstable)/keys/claim$ synapse_client;
~*^/_matrix/client/(r0|v3|unstable)/room_keys/ synapse_client;
~*^/_matrix/client/(api/v1|r0|v3|unstable)/rooms/.*/redact synapse_client;
~*^/_matrix/client/(api/v1|r0|v3|unstable)/rooms/.*/send synapse_client;
~*^/_matrix/client/(api/v1|r0|v3|unstable)/rooms/.*/state/ synapse_client;
~*^/_matrix/client/(api/v1|r0|v3|unstable)/rooms/.*/(join|invite|leave|ban|unban|kick)$ synapse_master;
~*^/_matrix/client/(api/v1|r0|v3|unstable)/join/ synapse_client;
~*^/_matrix/client/(api/v1|r0|v3|unstable)/profile/ synapse_client;
~*^/_matrix/client/(r0|v3|unstable)/.*/tags synapse_client;
~*^/_matrix/client/(r0|v3|unstable)/user/[^/]+/account_data/ synapse_master;
~*^/_matrix/client/(r0|v3|unstable)/.*/account_data synapse_client;
#~*^/_matrix/client/(r0|v3|unstable)/rooms/.*/receipt synapse_client;
#~*^/_matrix/client/(r0|v3|unstable)/rooms/.*/read_markers synapse_client;
~*^/_matrix/client/(api/v1|r0|v3|unstable)/presence/ synapse_client;
~*^/_matrix/client/(r0|v3|unstable)/user_directory/search$ synapse_client;
~*^/_matrix/federation/v1/version$ synapse_federation;
~*^/_matrix/federation/v1/event/ synapse_federation;
~*^/_matrix/federation/v1/state/ synapse_federation;
~*^/_matrix/federation/v1/state_ids/ synapse_federation;
~*^/_matrix/federation/v1/backfill/ synapse_federation;
~*^/_matrix/federation/v1/get_missing_events/ synapse_federation;
~*^/_matrix/federation/v1/publicRooms synapse_federation;
~*^/_matrix/federation/v1/query/ synapse_federation;
~*^/_matrix/federation/v1/make_join/ synapse_federation;
~*^/_matrix/federation/v1/make_leave/ synapse_federation;
~*^/_matrix/federation/(v1|v2)/send_join/ synapse_federation;
~*^/_matrix/federation/(v1|v2)/send_leave/ synapse_federation;
~*^/_matrix/federation/v1/make_knock/ synapse_federation;
~*^/_matrix/federation/v1/send_knock/ synapse_federation;
~*^/_matrix/federation/(v1|v2)/invite/ synapse_federation;
~*^/_matrix/federation/v1/event_auth/ synapse_federation;
~*^/_matrix/federation/v1/timestamp_to_event/ synapse_federation;
~*^/_matrix/federation/v1/exchange_third_party_invite/ synapse_federation;
~*^/_matrix/federation/v1/user/devices/ synapse_federation;
~*^/_matrix/key/v2/query synapse_federation;
~*^/_matrix/federation/v1/hierarchy/ synapse_federation;
~*^/_matrix/federation/v1/send/ synapse_federation;
}
# Choose sync worker based on the existence of "since" query parameter
map $arg_since $sync {
default synapse_sync;
'' synapse_initial_sync;
}
# Extract username from access token passed as URL parameter
map $arg_access_token $accesstoken_from_urlparam {
# Defaults to just passing back the whole accesstoken
default $arg_access_token;
# Try to extract username part from accesstoken URL parameter
"~syt_(?<username>.*?)_.*" $username;
}
# Extract username from access token passed as authorization header
map $http_authorization $mxid_localpart {
# Defaults to just passing back the whole accesstoken
default $http_authorization;
# Try to extract username part from accesstoken header
"~Bearer syt_(?<username>.*?)_.*" $username;
# if no authorization-header exist, try mapper for URL parameter "access_token"
"" $accesstoken_from_urlparam;
}
upstream synapse_initial_sync {
# Use the username mapper result for hash key
hash $mxid_localpart consistent;
server generic-sync-initial:8085;
}
upstream synapse_sync {
# Use the username mapper result for hash key
hash $mxid_localpart consistent;
server generic-sync-2:8084;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name matrix.dreadful.work;
ssl_certificate /etc/nginx/certs/origin.crt;
ssl_certificate_key /etc/nginx/certs/origin.key;
# Ottimizzazioni SSL
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 1d;
add_header Strict-Transport-Security "max-age=15768000" always;
access_log /var/log/nginx/matrix.access.log;
error_log /var/log/nginx/matrix.error.log;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
client_max_body_size 50M;
# Well-known
location ^~ /.well-known {
root /var/www/html/matrix;
}
# Sync (Dinamico: initial vs normal)
location ~ ^/_matrix/client/(r0|v3)/sync$ {
proxy_pass http://$sync;
}
# Normal sync
location ~ ^/_matrix/client/(api/v1|r0|v3)/events$ {
proxy_pass http://synapse_sync;
}
# Initial_sync
location ~ ^/_matrix/client/(api/v1|r0|v3)/initialSync$ {
proxy_pass http://synapse_initial_sync;
}
location ~ ^/_matrix/client/(api/v1|r0|v3)/rooms/[^/]+/initialSync$ {
proxy_pass http://synapse_initial_sync;
}
location ~ ^/_matrix/client/v3/(login|logout|refresh) {
proxy_http_version 1.1;
proxy_pass http://$synapse_backend;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location ~ ^(/_matrix|/_synapse/client|/_synapse/admin) {
proxy_pass http://$synapse_backend;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
client_max_body_size 50M;
proxy_http_version 1.1;
}
location / {
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Content-Security-Policy "frame-ancestors 'none'";
proxy_pass http://$synapse_backend;
#proxy_pass http://127.0.0.1:39050;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
client_max_body_size 50M;
proxy_http_version 1.1;
}
}
# Redirect HTTP -> HTTPS
server {
listen 80;
server_name matrix.dreadful.work;
return 301 https://$server_name$request_uri;
}
BACKUP iniziale
Adesso: un bel backup di tutto. E teniamolo.
cd /opt/dreadful.work
sudo tar cvf vector.iniziale.tar vector/
cd vectorCreazione dell'admin
Per manipolare il MAS la questione non è proprio user-friendly. Tanto per cominciare tutta l'autenticazione è delegata a lui e quindi non è possibile creare utenti direttamente su Synapse. Ho creato questo script per evitare di dover scrivere sempre tutto il comando docker a mano. È da mettere dentro la directory che ha il docker compose e l'ho chiamato mas-cli.sh:
#!/usr/bin/env bash
docker_command="docker run --network synapse -p 59062:8080 -v \"./mas-config/config.yaml:/config.yaml\" ghcr.io/element-hq/matrix-authentication-service"
for param in "$@"; do
docker_command="$docker_command $param"
done
# Esegui il comando docker completo
echo "Esecuzione del comando: $docker_command" # Utile per il debug
eval "$docker_command"Creiamo quindi l'admin:
./mas-cli.sh manage register-user admin \
--password 'SuperPass1234!' \
--admin \
--email [email protected] \
--ignore-password-complexity \
--yes
Esecuzione del comando: docker run --network synapse -p 59062:8080 -v "./mas-config/config.yaml:/config.yaml" ghcr.io/element-hq/matrix-authentication-service manage register-user admin --password SuperPass1234! --admin --email [email protected] --ignore-password-complexity --yes
User attributes
Username: admin
Matrix ID: @admin:localhost:8008
Password: ********
Email: [email protected]
Can request admin: true
2026-04-13T20:50:42.782269Z INFO mas_cli::commands::manage:936 User registered user.id=01KP49RQYDZJHQC31KF8RKGSN6Element
Perfetto. A questo punto possiamo tranquillamente avviare Element e gustarci il nuovo homeserver puntandolo a matrix.dreadful.work
Ketesa ((Synapse Admin))
Ketesa è il tool de facto per l'amministrazione via web di Synapse. Aggiungiamolo al docker-compose-yml (pubblicatelo pure con un altro dominio Cloudflare):
Ketesa:
hostname: ketesa
build:
context: https://github.com/etkecc/ketesa.git
dockerfile: docker/Dockerfile.build
args:
- BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
- REACT_APP_SERVER=https://matrix.dreadful.work
ports:
- "9000:8080"
restart: unless-stopped
networks:
- synapseTuning?
Redis
Iniziamo con il migliorare un po' la questione di redis, sostituendo il suo servizio in nel docker-compose.yml con
redis:
image: redis:7-alpine
restart: unless-stopped
networks:
- synapse
mem_limit: 1024
mem_reservation: 256m
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
volumes:
- ./redis:/data
command: >
redis-server
--appendonly yes
--appendfsync everysec
--maxmemory 768mb
--maxmemory-policy noeviction
--auto-aof-rewrite-percentage 100
--auto-aof-rewrite-min-size 64mb
Pgbouncer
Utilizziamolo per limitare le connessioni verso Postrgresql. Aggiungiamo il servizio a docker-compose.yml:
pgbouncer:
image: edoburu/pgbouncer:latest
restart: unless-stopped
networks:
- synapse
environment:
- DB_HOST=db
- DB_USER=${POSTGRES_USER}
- DB_PASSWORD=${POSTGRES_PASSWORD}
- POOL_MODE=transaction
- DEFAULT_POOL_SIZE=10
- MAX_CLIENT_CONN=50
- AUTH_TYPE=scram-sha-256
- RESERVE_POOL_SIZE=5
- SERVER_RESET_QUERY=DISCARD ALL
- IGNORE_STARTUP_PARAMETERS=extra_float_digitse modifichiamo il file data/homeserver.yaml in modo che usi la nuova configurazione.
database:
name: psycopg2
args:
user: synapse
password: [HIDDEN]
#host: db
host: pgbouncer
port: 5432
database: synapse
cp_min: 1
cp_max: 5La password rimane invariata, cambiano host, cp_min e cp_max
Postgresql
Siamo su un N150, quindi ottimizziamo anche Postgres:
mkdir postgres-confige poi
cat > /opt/dreadful.work/vector/postgres-config/postgresql.conf << 'EOF'
shared_buffers = '1.5GB'
effective_cache_size = '3GB'
work_mem = '4MB'
maintenance_work_mem = '128MB'
max_connections = 50
wal_buffers = '4MB'
checkpoint_completion_target = 0.9
checkpoint_timeout = '15min'
max_wal_size = '2GB'
min_wal_size = '512MB'
autovacuum = on
autovacuum_max_workers = 2
autovacuum_naptime = '30s'
autovacuum_vacuum_scale_factor = 0.02
autovacuum_analyze_scale_factor = 0.01
autovacuum_vacuum_cost_limit = 1500
autovacuum_vacuum_cost_delay = '5ms'
max_worker_processes = 2
max_parallel_workers = 1
max_parallel_workers_per_gather = 1
log_min_duration_statement = '1000ms'
log_checkpoints = off
log_autovacuum_min_duration = '1000ms'Questo file deve essere montato nel container di Postgres in docker-compose.yml quindi modifichiamo la sezione:
db:
image: docker.io/postgres:16-alpine
restart: unless-stopped
networks:
- synapse
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
volumes:
- ./postgres:/var/lib/postgresql/data
- ./postgres-config/postgresql.conf:/etc/postgresql/postgresql.conf:ro
command: postgres -c config_file=/etc/postgresql/postgresql.confLiveKit aka Videoconferenze native
[ To Be Continued]