- 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>
348 lines
15 KiB
Markdown
348 lines
15 KiB
Markdown
# 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.
|
|
|
|
```sql
|
|
-- 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:
|
|
|
|
```sql
|
|
-- 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:
|
|
|
|
```yaml
|
|
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`)
|
|
|
|
```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:
|
|
|
|
```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:
|
|
|
|
```yaml
|
|
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:
|
|
```bash
|
|
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`:
|
|
```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
|
|
|
|
```yaml
|
|
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`:
|
|
```yaml
|
|
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:
|
|
```yaml
|
|
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`:
|
|
```bash
|
|
# 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:
|
|
|
|
```bash
|
|
# 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)
|