7
Matrix
Alvis edited this page 2026-03-26 14:04:37 +00:00

Matrix

Self-hosted Matrix homeserver (Synapse) with an E2EE bot adapter serving as the bridge between Adolf/Zabbix and Matrix rooms.

Synapse

Homeserver: mtx.alogins.net (Caddy → Synapse container port 8008)

Compose directory: agap_git/matrix/

Creating New Users

Registration is disabled by default. Use register_new_matrix_user with the shared secret from homeserver.yaml.

docker exec -it synapse register_new_matrix_user \
  -u <username> \
  -p <password> \
  -c /data/homeserver.yaml \
  http://localhost:8008

Add --admin flag to create an admin user. The shared secret is in ~/agap_git/matrix/data/synapse/homeserver.yaml under registration_shared_secret.

To create a user non-interactively:

docker exec synapse register_new_matrix_user \
  -u <username> \
  -p <password> \
  --no-admin \
  -c /data/homeserver.yaml \
  http://localhost:8008

Matrix Bot

Repo: ~/matrixbot/http://localhost:3000/alvis/matrixbot (if pushed)

FastAPI service (port 3002) running two matrix-nio E2EE clients:

Account Tag Device ID Purpose
@bot:mtx.alogins.net adolf ADOLFDEVICE Adolf channel adapter — inbound (forwards to deepagents) and outbound
@zabbix:mtx.alogins.net zabbix ZABBIXDEVICE Zabbix notifications — outbound only

API Endpoints

Method Path Description
POST /send Send message as adolf (@bot) — body: {"room_id": "...", "text": "..."}
POST /zabbix/send Send message as @zabbix — body: {"room_id": "...", "text": "..."}
GET /health Health check

E2EE and Cross-Signing

Both bots bootstrap cross-signing keys on first startup:

  1. Upload device keys to homeserver (keys_upload)
  2. Generate master, self-signing, and user-signing olm key pairs
  3. Upload via keys/device_signing/upload with UIAA password auth
  4. Self-sign the device key via keys/signatures/upload
  5. Persist key material to /data/{adolf,zabbix}/cross_signing.json

Device keys must be uploaded before cross-signing — otherwise the server doesn't know the device and self-signing fails.

On subsequent starts, keys are loaded from the persisted file — no regeneration.

In-Room SAS Verification

Both bots support interactive emoji verification initiated from Element X. The full flow:

Element X                       Bot
    ├─ m.key.verification.request ─→
    ←─ m.key.verification.ready ───┤
    ├─ m.key.verification.start ──→
    ←─ m.key.verification.accept ──┤
    ├─ m.key.verification.key ────→
    ←─ m.key.verification.key ─────┤
    ←─ m.key.verification.mac ─────┤
    ├─ m.key.verification.mac ────→
    ←─ m.key.verification.done ────┤
    ├─ m.key.verification.done ───→

The bot auto-accepts emoji matches. Master cross-signing key is included in the MAC so Element X can establish the cross-signing trust chain (green verified star).

Outgoing verification events must NOT contain transaction_id (that field is for to-device only) — only m.relates_to with rel_type: m.reference.

To-device verification is also handled as a fallback.

Crypto Store

E2EE state (olm sessions, megolm group sessions, device keys) is persisted in SQLite databases:

~/matrixbot/data/
├── adolf/@bot:mtx.alogins.net_ADOLFDEVICE.db
├── adolf/cross_signing.json
├── zabbix/@zabbix:mtx.alogins.net_ZABBIXDEVICE.db
└── zabbix/cross_signing.json
Store Pickle passphrase
SQLite databases (olm/megolm sessions) DEFAULT_KEY (matrix-nio default)
cross_signing.json files matrixbot-cs-keys (CS_PICKLE_PASS in bot.py)

To decrypt E2EE messages, run inside the matrixbot container (host python-olm links against a different libolm, causing BAD_ACCOUNT_KEY):

# docker exec matrixbot python3 -c "..."
import olm, sqlite3
conn = sqlite3.connect('/data/zabbix/@zabbix:mtx.alogins.net_ZABBIXDEVICE.db')
cur = conn.cursor()
cur.execute('SELECT session_id, session FROM megolminboundsessions WHERE room_id = ?', (ROOM,))
for sid, blob in cur.fetchall():
    session = olm.InboundGroupSession.from_pickle(blob, 'DEFAULT_KEY')
    plaintext, idx = session.decrypt(ciphertext)

Rooms

Room ID Name
!kNQXdXrjSAjoAMdosG:mtx.alogins.net Agap Notifications (Zabbix)
!vYXGUTRHUIIrrZXTFE:mtx.alogins.net Adolf chat

Gotchas

  • Device key upload before cross-signing: keys_upload() must run before bootstrap_cross_signing(), otherwise the server can't find the device for self-signing.
  • Store corruption on copy: Never copy olm store directories while the bot is running — the olm identity keys will diverge from what the server has. If you need to rename the store directory, stop the container first.
  • Changing device ID: If you change the device ID, you must: get a new access token (login with new device_id), clear the store, delete the old device from the server, and re-bootstrap cross-signing. Element X caches device keys aggressively — a new device ID forces a fresh key fetch.
  • transaction_id in room events: nio's accept_verification(), share_key(), get_mac() return ToDeviceMessage objects whose .content includes transaction_id. Strip it before sending as a room event — Element X may ignore events with this field.

Environment

Variables in ~/matrixbot/.env, passed through docker-compose.yml:

Variable Description
MATRIX_HOMESERVER Synapse URL (internal: http://synapse:8008)
MATRIX_ADOLF_TOKEN @bot access token
MATRIX_ADOLF_PASSWORD @bot password (for UIAA during cross-signing)
MATRIX_ADOLF_DEVICE_ID ADOLFDEVICE
MATRIX_ZABBIX_TOKEN @zabbix access token
MATRIX_ZABBIX_PASSWORD @zabbix password
MATRIX_ZABBIX_DEVICE_ID ZABBIXDEVICE
DEEPAGENTS_URL Adolf deepagents endpoint (http://host.docker.internal:8000)

Tokens and passwords stored in Vaultwarden: MATRIX_ADOLF_TOKEN, MATRIX_ADOLF_PASSWORD, MATRIX_ZABBIX_TOKEN, MATRIX_ZABBIX_PASSWORD.

Stack

~/matrixbot/
├── bot.py                 Single-file bot (FastAPI + matrix-nio)
├── docker-compose.yml     Service definition, networks: matrix_frontend, zabbix_frontend
├── Dockerfile             python:3.12-slim + libolm-dev
├── requirements.txt       matrix-nio[e2e]==0.25.2, fastapi, uvicorn, httpx, pydantic
├── .env                   Tokens and passwords
└── data/                  Persisted state (olm sessions, cross-signing keys)
    ├── adolf/
    └── zabbix/

Start

cd ~/matrixbot
docker compose up -d --build

Networks

The container joins two external Docker networks:

  • matrix_frontend — access to Synapse container
  • zabbix_frontend — allows Zabbix media type to reach /zabbix/send