echo6-docs/PLAN.md
Matt Johnson e9231ac24a Migration: consolidate Echo6 docs to cortex with full infrastructure cleanup sync
- 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>
2026-04-13 06:02:16 +00:00

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 grantsLOGIN 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 synapse user 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, default False
  • msc2409_to_device_messages_enabled — parsed at line ~163, default False

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:

  1. Appservices authenticate to Synapse via as_token/hs_token in the registration YAML, which is a Synapse-native mechanism predating MAS.
  2. 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.
  3. A commenter on that issue confirmed: "Can confirm it works with mautrix-signal and mautrix-whatsapp."
  4. 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:

  1. After adding the registration to homeserver.yaml and restarting Synapse, check Synapse logs for appservice registration errors
  2. 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 401 or 403 → MAS or Synapse rejected the as_token
  • HTTP 3xx redirect to MAS (Location: header pointing to matrix-mas:8080 or matrix.echo6.co auth endpoints)
  • Response contains MAS-specific indicators: HTML login page, matrix-authentication-service in headers/body, or errcode: M_UNKNOWN_TOKEN with MAS introspection trace in Synapse logs
  • user_id in 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

  1. Generate the registration file by running the bridge container once with config in place
  2. Copy registration.yaml into the Synapse data volume (/opt/matrix/synapse/)
  3. Add to homeserver.yaml:
    app_service_config_files:
      - /data/registration.yaml
      - /data/doublepuppet.yaml
    
    Both files are listed — Synapse reads all entries on startup. A single restart covers both registrations.
  4. 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:

  1. Generate a dedicated double-puppet appservice registration (doublepuppet.yaml) with a null URL and an as_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
    
  2. Register it with Synapse alongside the bridge registration in app_service_config_files (see section 6)
  3. 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:

  1. Update pg_backup.sh to dump mautrix_signal and mas:

    # 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
    
  2. Update the cleanup find to also cover mautrix_signal_*.sql.gz and mas_*.sql.gz

  3. Test the backup by running pg_backup.sh manually after DB creation but before starting the bridge. Verify:

    • mautrix_signal dump succeeds (even if empty, it should produce a valid .sql.gz)
    • mas dump succeeds
    • Retention cleanup patterns match the new filenames
  4. 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)

  1. Generate password for mautrix_signal DB role
  2. Create DB role (with NOSUPERUSER NOCREATEDB NOCREATEROLE) and database in matrix-postgres
  3. Verify role privileges are minimal (query pg_roles)
  4. Update backup script to include mautrix_signal and mas dumps
  5. Test backup — run pg_backup.sh manually, verify all three dumps succeed
  6. Create /opt/matrix/mautrix-signal/ directory
  7. Generate initial config with docker run --rm
  8. Edit config.yaml with all settings from this plan
  9. Run container again to generate registration.yaml
  10. Create doublepuppet.yaml registration
  11. Copy both registration files to Synapse volume (/opt/matrix/synapse/)
  12. Add experimental_features block to homeserver.yaml
  13. Add app_service_config_files with both registration paths to homeserver.yaml
  14. Restart Synapse
  15. STOP-AND-CHECK: Verify appservice auth works alongside MAS (see section 6 procedure)
  16. Add mautrix-signal service to docker-compose.yml
  17. docker compose up -d mautrix-signal
  18. Verify bridge bot appears in Matrix
  19. Link Signal account via !signal link in bridge bot DM
  20. Update docs (services.md, MEMORY.md)