- Documents recent infrastructure cleanup (8 CTs destroyed, 35 DNS records removed, Headscale cleanup) - Adds 24 new runbooks covering Authentik, PeerTube, Meshtastic, RECON, Proxmox, Mailcow, Internet Archive, GPU routing - Adds project documentation for headscale, vaultwarden, peertube, matrix, mmud, advbbs, arr stack - Updates services.md, environment.md, caddy.md, authentik.md to match live infrastructure - Removes 4 deprecated runbook duplicates (canonical versions live in projects/) - Adds .gitignore for binary archives and editor temp files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
15 KiB
mautrix-signal Bridge Deployment Plan
1. Deployment Target
Same Contabo host, same Docker Compose stack at /opt/matrix/docker-compose.yml.
Rationale: Synapse runs as Docker on Contabo (ref: synapse.ref — "Docker Compose at /opt/matrix/docker-compose.yml"). The bridge container joins the existing matrix-net network so it can reach both matrix-synapse and matrix-postgres by container name without exposing any new ports externally.
2. Bridge Version
- Image:
dock.mau.dev/mautrix/signal:v0.2603.0 - Released: 2026-03-16 (latest stable as of 2026-04-09)
- Type: Go rewrite (NOT the deprecated Python mautrix-signal)
Pin to the exact tag v0.2603.0, not :latest, so upgrades are intentional.
3. Database Plan
Create a new Postgres database and role inside the existing matrix-postgres container. The role gets minimal grants — LOGIN only, no SUPERUSER, no CREATEDB, no CREATEROLE. Ownership of the new database is the sole privilege.
-- Connect as the synapse superuser to create the role and database
CREATE ROLE mautrix_signal WITH LOGIN PASSWORD '<generated-64-char-password>'
NOSUPERUSER NOCREATEDB NOCREATEROLE;
CREATE DATABASE mautrix_signal
OWNER mautrix_signal
ENCODING 'UTF8'
LC_COLLATE 'C'
LC_CTYPE 'C';
-- No additional GRANT needed — OWNER on the database gives full DDL/DML
-- within mautrix_signal only. The role has zero access to synapse or mas databases.
Verification after creation:
-- Confirm no superuser, no createdb
SELECT rolname, rolsuper, rolcreatedb, rolcreaterole FROM pg_roles WHERE rolname = 'mautrix_signal';
-- Expected: rolsuper=f, rolcreatedb=f, rolcreaterole=f
-- Confirm cannot access synapse DB
SET ROLE mautrix_signal;
SELECT 1 FROM synapse.public.users LIMIT 1; -- should fail with permission denied
RESET ROLE;
- Collation matches Synapse's DB settings (ref:
synapse.ref— POSTGRES_INITDB_ARGS uses--lc-collate=C --lc-ctype=C) - No shared schema with Synapse or MAS
- The
synapseuser has Superuser privileges so can create the role/DB (ref:synapse.ref— "synapse (Superuser, Create role, Create DB)") - Bridge config URI:
postgres://mautrix_signal:<password>@matrix-postgres:5432/mautrix_signal?sslmode=disable
4. Networking
- Appservice port: 29328 (mautrix-signal default)
- Bind: 0.0.0.0:29328 inside container (Docker internal only, NOT exposed to host)
- Appservice address in config:
http://mautrix-signal:29328(container name on matrix-net) - Verified unused: No ports in 29xxx range are in use (ref:
appservices.ref— "Full 29000-29999 range — AVAILABLE") - No Caddy changes needed — bridge communicates with Synapse over the internal Docker network
- No firewall changes needed — no host port mapping
The bridge container joins matrix-net in docker-compose.yml:
mautrix-signal:
image: dock.mau.dev/mautrix/signal:v0.2603.0
container_name: mautrix-signal
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
volumes:
- ./mautrix-signal:/data
networks:
- matrix-net
No ports: section — the container is only reachable from within matrix-net.
5. Encryption Config
Enable end-to-bridge encryption with MSC4190 (required for MAS compatibility).
5a. Bridge config (config.yaml)
encryption:
allow: true
default: true
require: true
appservice: false
msc4190: true # REQUIRED when MAS is in use
allow_key_sharing: true
pickle_key: generate
self_sign: true
MSC4190 requirement: The mautrix docs state: "The encryption -> msc4190 config option must be set to true for encryption to work if you use MAS."
Source: https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html
5b. Synapse homeserver.yaml diff
The exact flag names are confirmed from Synapse 1.147.1 source code at synapse/config/experimental.py:
msc3202_transaction_extensions— parsed at line ~167, defaultFalsemsc2409_to_device_messages_enabled— parsed at line ~163, defaultFalse
Source: synapse.config.experimental.ExperimentalConfig in Synapse v1.147.1 (docker exec matrix-synapse python3 -c "import synapse.config.appservice" — inspected live).
The current homeserver.yaml has no experimental_features block. The exact diff:
--- a/homeserver.yaml
+++ b/homeserver.yaml
@@ -end of file
+
+experimental_features:
+ msc3202_transaction_extensions: true
+ msc2409_to_device_messages_enabled: true
These flags enable:
msc3202_transaction_extensions: Allows Synapse to send to-device messages, device list changes, and OTK counts in appservice transaction pushes (required for E2BE)msc2409_to_device_messages_enabled: Allows appservices to receive to-device messages (required for encryption key exchange)
Note: MSC4190 is NOT an experimental_features flag. It is parsed from the appservice registration YAML file as io.element.msc4190: true (confirmed from Synapse 1.147.1 source: synapse/config/appservice.py line 190: msc4190_enabled = as_info.get("io.element.msc4190", False)).
5c. Appservice registration (registration.yaml)
The bridge auto-generates registration.yaml when encryption.msc4190: true is set in config.yaml. The generated file will include these flags:
io.element.msc4190: true
de.sorunome.msc2409.push_ephemeral: true
push_ephemeral: true
Synapse 1.147.1 supports MSC4190 (merged in Synapse 1.121.0 via PR #17705). Source: https://github.com/element-hq/synapse/pull/17705
5d. Trust boundary
The bridge process holds plaintext message content in memory after decryption. The trust boundary extends from the Matrix client to the bridge container. Signal transport encryption remains intact on the Signal side.
6. MAS Interaction
Claim: Appservice registration bypasses MAS
Status: ASSUMPTION — verify during Phase 3 with stop-and-check.
The evidence supporting this claim:
- Appservices authenticate to Synapse via
as_token/hs_tokenin the registration YAML, which is a Synapse-native mechanism predating MAS. - GitHub issue element-hq/matrix-authentication-service#3206 shows a user successfully registering and running a mautrix-signal appservice alongside MAS — basic appservice connectivity (GET
/versions, GET/account/whoami, appservice ping) all returned 200 before encryption was attempted. - A commenter on that issue confirmed: "Can confirm it works with mautrix-signal and mautrix-whatsapp."
- The Matrix Application Service spec defines its own auth mechanism independent of the C-S API auth layer.
Source: https://github.com/element-hq/matrix-authentication-service/issues/3206
However, no official MAS documentation explicitly states "appservices bypass MAS." The MAS docs (element-hq.github.io/matrix-authentication-service/) have no dedicated appservice compatibility page.
Phase 3 stop-and-check procedure:
- After adding the registration to
homeserver.yamland restarting Synapse, check Synapse logs for appservice registration errors - Before starting the bridge container, run a manual appservice ping test:
curl -sv http://127.0.0.1:8008/_matrix/client/v3/account/whoami \ -H "Authorization: Bearer <as_token>" \ -H "Content-Type: application/json" 2>&1
PASS criteria (ALL must be true):
- HTTP status code is
200 - Response body contains
"user_id":"@signalbot:echo6.co" - Response body contains
"appservice_id":"signal"(confirms Synapse recognized the as_token as appservice auth)
FAIL criteria (ANY triggers STOP):
- HTTP status
401or403→ MAS or Synapse rejected the as_token - HTTP
3xxredirect to MAS (Location:header pointing tomatrix-mas:8080ormatrix.echo6.coauth endpoints) - Response contains MAS-specific indicators: HTML login page,
matrix-authentication-servicein headers/body, orerrcode: M_UNKNOWN_TOKENwith MAS introspection trace in Synapse logs user_idin response does NOT match@signalbot:echo6.co
On FAIL: Do NOT start the bridge container. Do NOT proceed to step 16. Capture the full curl -sv output (headers + body) and the last 50 lines of Synapse logs (docker logs matrix-synapse --tail 50). Report both for triage.
Registration steps
- Generate the registration file by running the bridge container once with config in place
- Copy
registration.yamlinto the Synapse data volume (/opt/matrix/synapse/) - Add to
homeserver.yaml:
Both files are listed — Synapse reads all entries on startup. A single restart covers both registrations.app_service_config_files: - /data/registration.yaml - /data/doublepuppet.yaml - Restart Synapse (
docker compose restart synapse) to pick up both appservice registrations
7. Permissions
bridge:
permissions:
"*": relay
"echo6.co": user
"@matt:echo6.co": admin
*: relay— external users can interact via relay (relay disabled by default, so effectively no access)echo6.co: user— all echo6.co users can use the bridge@matt:echo6.co: admin— full admin access for matt
Single-user deployment — only matt will link a Signal account.
8. Double Puppeting
Use the appservice-based automatic double puppeting method:
- Generate a dedicated double-puppet appservice registration (
doublepuppet.yaml) with a null URL and anas_token:id: doublepuppet url: as_token: <generated-token> hs_token: <generated-token> sender_localpart: _doublepuppet rate_limited: false namespaces: users: - regex: '@.*:echo6\.co' exclusive: false - Register it with Synapse alongside the bridge registration in
app_service_config_files(see section 6) - Configure the bridge:
double_puppet: secrets: echo6.co: "as_token:<the-as-token-from-doublepuppet.yaml>"
This ensures messages matt sends from Signal Desktop appear as @matt:echo6.co in Matrix rooms rather than as the Signal ghost user.
MAS compatibility: The appservice-based double puppeting method uses the appservice as_token to impersonate the user, which works independently of MAS. MAS handles human user auth; appservice tokens are Synapse-native. (Same assumption as section 6 — covered by the stop-and-check.)
9. Backup Impact
The existing backup script (/opt/matrix/scripts/pg_backup.sh) only backs up the synapse database (ref: synapse.ref — "Backs up synapse DB only (NOT mas DB)").
Action required — BEFORE bridge goes live:
-
Update
pg_backup.shto dumpmautrix_signalandmas:# Add to pg_backup.sh after the synapse dump: # mautrix-signal bridge database SIGNAL_BACKUP="${BACKUP_DIR}/mautrix_signal_${TIMESTAMP}.sql.gz" docker exec matrix-postgres pg_dump -U mautrix_signal -d mautrix_signal | gzip > "${SIGNAL_BACKUP}" if [ $? -eq 0 ] && [ -s "${SIGNAL_BACKUP}" ]; then echo "$(date): Backup created: ${SIGNAL_BACKUP} ($(du -h "${SIGNAL_BACKUP}" | cut -f1))" else echo "$(date): WARNING: mautrix_signal backup failed" fi # MAS database (was missing from backups) MAS_BACKUP="${BACKUP_DIR}/mas_${TIMESTAMP}.sql.gz" docker exec matrix-postgres pg_dump -U mas -d mas | gzip > "${MAS_BACKUP}" if [ $? -eq 0 ] && [ -s "${MAS_BACKUP}" ]; then echo "$(date): Backup created: ${MAS_BACKUP} ($(du -h "${MAS_BACKUP}" | cut -f1))" else echo "$(date): WARNING: mas backup failed" fi -
Update the cleanup
findto also covermautrix_signal_*.sql.gzandmas_*.sql.gz -
Test the backup by running
pg_backup.shmanually after DB creation but before starting the bridge. Verify:mautrix_signaldump succeeds (even if empty, it should produce a valid .sql.gz)masdump succeeds- Retention cleanup patterns match the new filenames
-
Apply the same 14-day retention policy.
10. Rollback Plan
If the bridge needs to be removed:
# 1. Stop and remove the bridge container
cd /opt/matrix
docker compose stop mautrix-signal
docker compose rm -f mautrix-signal
# 2. Remove mautrix-signal service block from docker-compose.yml
# 3. Remove appservice registrations from Synapse homeserver.yaml:
# - Remove /data/registration.yaml from app_service_config_files
# - Remove /data/doublepuppet.yaml from app_service_config_files
# - If app_service_config_files is now empty, remove the key entirely
# 4. Revert experimental_features from homeserver.yaml:
# - Remove the entire experimental_features block:
# experimental_features:
# msc3202_transaction_extensions: true
# msc2409_to_device_messages_enabled: true
# - Only safe to remove if no other bridges depend on these flags.
# As of this plan, no other appservices exist (ref: appservices.ref),
# so removal is safe.
# 5. Remove registration files from Synapse volume
rm /opt/matrix/synapse/registration.yaml
rm /opt/matrix/synapse/doublepuppet.yaml
# 6. Restart Synapse to apply config changes
docker compose restart synapse
# 7. Drop the database and role
docker exec matrix-postgres psql -U synapse -c "DROP DATABASE mautrix_signal;"
docker exec matrix-postgres psql -U synapse -c "DROP ROLE mautrix_signal;"
# 8. Remove bridge data directory
rm -rf /opt/matrix/mautrix-signal
# 9. Revert backup script
# Edit pg_backup.sh: remove the mautrix_signal and mas dump sections
# (Keep mas dump if desired — it was missing before this plan anyway)
# 10. Clean up docker image
docker rmi dock.mau.dev/mautrix/signal:v0.2603.0
Implementation Order (Phase 3 — requires approval)
- Generate password for
mautrix_signalDB role - Create DB role (with
NOSUPERUSER NOCREATEDB NOCREATEROLE) and database in matrix-postgres - Verify role privileges are minimal (query
pg_roles) - Update backup script to include
mautrix_signalandmasdumps - Test backup — run
pg_backup.shmanually, verify all three dumps succeed - Create
/opt/matrix/mautrix-signal/directory - Generate initial config with
docker run --rm - Edit
config.yamlwith all settings from this plan - Run container again to generate
registration.yaml - Create
doublepuppet.yamlregistration - Copy both registration files to Synapse volume (
/opt/matrix/synapse/) - Add
experimental_featuresblock tohomeserver.yaml - Add
app_service_config_fileswith both registration paths tohomeserver.yaml - Restart Synapse
- STOP-AND-CHECK: Verify appservice auth works alongside MAS (see section 6 procedure)
- Add mautrix-signal service to
docker-compose.yml docker compose up -d mautrix-signal- Verify bridge bot appears in Matrix
- Link Signal account via
!signal linkin bridge bot DM - Update docs (services.md, MEMORY.md)