Files
hobbs-iac/docs/superpowers/plans/2026-08-20-mas-qr-login.md
T
2026-08-20 14:57:55 -05:00

669 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# QR Sign-In via Matrix Authentication Service — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Enable "sign in with QR code" (MSC4108) for Matrix accounts on hobbs.farm, which requires deploying Matrix Authentication Service (MAS) as the homeserver's auth provider.
**Architecture:** QR sign-in is an OAuth 2.0 device grant, not a password login. Synapse 1.159.0 refuses to start with `msc4108_enabled: true` unless `matrix_authentication_service.enabled` is also true. MAS requires PostgreSQL, and — critically — **`syn2mas` can only read a Synapse whose own database is PostgreSQL**. Synapse here is on SQLite, so this is a three-stage chain: port Synapse to PostgreSQL, deploy MAS and migrate the accounts into it, then turn on the QR MSCs.
**Tech Stack:** Synapse 1.159.0 (SQLite → PostgreSQL 16), Matrix Authentication Service (Docker, ghcr.io/element-hq), PostgreSQL 16-alpine ×2, Traefik, Cloudflare DNS, Docker Compose on hf-pdocker-01.
## Why this is three migrations, not a config flag
Verified in the running image's own source (`synapse/config/experimental.py`):
```python
if (self.msc4108_enabled or self.msc4108_delegation_endpoint is not None) and not auth_delegated:
raise ConfigError("MSC4108 requires matrix_authentication_service to be enabled", ...)
```
The same guard exists for MSC4388 (the QR out-of-band channel). And from the MAS docs:
> "It is worth noting that MAS currently only supports PostgreSQL as a database backend."
> "The migration tool only supports reading from PostgreSQL for the Synapse database as well."
Synapse on this node currently runs `database: {name: sqlite3, database: /data/homeserver.db}`.
## Global Constraints
- Synapse image stays pinned at `ghcr.io/element-hq/synapse:v1.159.0` (**`v` prefix required**; `:1.159.0` 404s). Do not bump during this work.
- Synapse ≥1.143 requires **PostgreSQL 14+**. Use `postgres:16-alpine`.
- Synapse's PostgreSQL database MUST be created with `ENCODING 'UTF8' LC_COLLATE 'C' LC_CTYPE 'C'` — Synapse refuses any other collation.
- MAS requires its **own separate, blank** PostgreSQL database. Do not share Synapse's.
- All lock-prone databases go on **LOCAL disk**, never NFS (CLAUDE.md Known Issue #16). Use `/var/lib/synapse-db` and `/var/lib/mas-db`.
- On hf-pdocker-01, **`memswap_limit` must equal `mem_limit`** on every container (Known Issue #12 — a drift here crashed the whole node on 2026-07-12).
- Node headroom: ~8GB available of 23GB, Frigate is the main consumer. Budget ≤600MB total for the new containers.
- **hobbs.farm has no wildcard DNS record** (memory `hobbs-farm-dns`). Traefik uses DNS-01, so a certificate will be issued for a hostname with **no A record** — a valid cert does NOT mean the name resolves. Any new public hostname needs an explicit Cloudflare A record → `98.160.103.126`, DNS-only (grey cloud).
- Watchtower does not run on .192; pin every image tag explicitly.
- Synapse data lives at `/var/lib/synapse` on local NVMe. The NFS copy at `/mnt/docker-data/configs/matrix/data` is a stale backup — **never remount it**.
- The mandatory post-change checklist in CLAUDE.md applies (CLAUDE.md, MEMORY.md, Prometheus probes, wiki, Farm Manager links).
## Blast Radius
Authentication for **all 19 accounts** moves to MAS. Known consumers that must keep working:
| Account | How it authenticates | Migrates via |
|---|---|---|
| `@jeremy` | password `bigdog01` + 5 Element devices (2× Element X Android, 2× Element Desktop) | password hash + devices |
| `@frigate` | long-lived access token `syt_ZnJpZ2F0…` in `frigate-notify/.env` | access token |
| `@alertbot` | long-lived access token `syt_YWxlcnRib3Q…` in `matrix-receiver-config.yml` | access token |
| `@margaret` | openclaw.json contains **both** `accessToken` and `password` | both |
| `@spirit` | openclaw config (path differs from margaret's — resolve in Task 1) | TBD in Task 1 |
| `@clippyauto` | n8n credential on .100, polls `/sync` every ~2s | TBD in Task 1 |
| `@fry`, `@hermes`, `@julie`, `@pluto`, `@testbot`, `@admin1`, `@adminuser` | unknown | TBD in Task 1 |
MAS exposes a compatibility layer proxying `/_matrix/client/*/login`, so password-based bots are expected to keep working — **this is the single biggest assumption in the plan and Task 1 exists to test it, not trust it.**
## File Structure
- Create `compose/hf-pdocker-01/mas/docker-compose.yml` — MAS + `mas-db` PostgreSQL. One file, one responsibility: authentication.
- Modify `compose/hf-pdocker-01/matrix/docker-compose.yml` — re-add a `synapse-db` service (PostgreSQL 16, local disk). Note this partly reverses the 2026-08-20 removal of the *empty* postgres; this one will actually hold data.
- Create `/var/lib/synapse/homeserver-postgres.yaml` on the node — port-target config for `synapse_port_db` (node-only, not in repo — contains DB credentials).
- Create `/mnt/docker-data/configs/mas/config.yaml` on the node — MAS config (node-only, contains signing keys and secrets; back up to Vaultwarden).
- Modify `/mnt/docker-data/traefik/dynamic/temp-routes.yml` — add the `auth.hobbs.farm` router + service. (Yes, the Matrix, Home Assistant and Guacamole routes all live in a file called `temp-routes.yml`. It is load-bearing despite the name.)
- Modify `CLAUDE.md`, `MEMORY.md` — post-change checklist.
---
### Task 1: Auth inventory and full backup
**Files:**
- Create: `/var/lib/synapse.bak-premas/` (node), `docs/superpowers/plans/mas-auth-inventory.md` (repo)
**Interfaces:**
- Produces: a complete table of every account → auth method → config file location, used by Tasks 7 and 10 to verify nothing broke.
- [ ] **Step 1: Enumerate every device and its last-seen IP, to know what "still working" means later**
```bash
ssh glyptic@192.168.86.192 'docker exec synapse python3 -c "
import sqlite3
c=sqlite3.connect(\"file:/var/lib/synapse/homeserver.db?mode=ro\",uri=True)
for u,d,n,ip in c.execute(\"select user_id,device_id,display_name,last_seen_ip from devices order by user_id\"):
print(u,d,n,ip)
"'
```
Expected: rows for `@jeremy` (5 devices), plus bot devices for margaret/spirit/clippyauto/frigate/alertbot/fry.
- [ ] **Step 2: Locate spirit's and clippyauto's credentials (unresolved in planning)**
```bash
ssh glyptic@192.168.86.192 'docker inspect spirit --format "{{range .Mounts}}{{.Source}} -> {{.Destination}}{{println}}{{end}}"; docker exec spirit sh -c "ls -la /home/glyptic/.openclaw/ 2>/dev/null || find / -name openclaw.json -maxdepth 6 2>/dev/null | head"'
ssh glyptic@192.168.86.100 'docker exec n8n sh -c "printenv | grep -i matrix" 2>/dev/null; echo "---"; docker exec n8n-db psql -U n8n -tc "select name, type from credentials_entity;" 2>/dev/null | head'
```
Expected: a config path for spirit and a credential type for n8n. Record whether each uses an **access token** or a **password** — access tokens migrate untouched; passwords depend on the MAS compat layer.
- [ ] **Step 3: Write the inventory file**
Record one row per account: user id, auth method, exact file path holding the credential, and how to verify it post-migration. Save as `docs/superpowers/plans/mas-auth-inventory.md`.
- [ ] **Step 4: Take a cold, complete backup**
```bash
ssh glyptic@192.168.86.192 '
cd /mnt/docker-data/compose/hf-pdocker-01/matrix && docker compose stop synapse
sudo rsync -a /var/lib/synapse/ /var/lib/synapse.bak-premas/
sudo python3 -c "
import sqlite3;c=sqlite3.connect(\"/var/lib/synapse.bak-premas/homeserver.db\")
print(\"integrity:\", c.execute(\"pragma integrity_check\").fetchone()[0])
print(\"users:\", c.execute(\"select count(*) from users\").fetchone()[0])
print(\"events:\", c.execute(\"select count(*) from events\").fetchone()[0])
"
docker compose start synapse'
```
Expected: `integrity: ok`, 19 users, ~93,884 events. **Do not proceed if integrity is anything but `ok`.**
- [ ] **Step 5: Commit the inventory**
```bash
git add docs/superpowers/plans/mas-auth-inventory.md
git commit -m "docs: matrix auth inventory ahead of MAS migration"
```
---
### Task 2: PostgreSQL for Synapse (blank, correct collation)
**Files:**
- Modify: `compose/hf-pdocker-01/matrix/docker-compose.yml`
**Interfaces:**
- Produces: `synapse-db` reachable at `synapse-db:5432` on the `matrix-internal` network, database `synapse`, user `synapse`, C collation. Task 3 ports data into it.
- [ ] **Step 1: Add the service to the compose file**
```yaml
synapse-db:
image: postgres:16-alpine
container_name: synapse-db
restart: unless-stopped
environment:
- POSTGRES_USER=synapse
- POSTGRES_PASSWORD=<generate: openssl rand -base64 24>
- POSTGRES_DB=synapse
- POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C
volumes:
- /var/lib/synapse-db:/var/lib/postgresql/data # LOCAL disk - Known Issue #16
networks:
- matrix-internal
mem_limit: 512M
memswap_limit: 512M # MUST equal mem_limit - Known Issue #12
labels:
- "com.centurylinklabs.watchtower.enable=false"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U synapse -d synapse"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
logging:
driver: "json-file"
options: {max-size: "50m", max-file: "3"}
```
Note: **no `user: "1024:100"`** — that workaround is only for NFS root_squash. This volume is local.
- [ ] **Step 2: Deploy and verify collation**
```bash
scp compose/hf-pdocker-01/matrix/docker-compose.yml glyptic@192.168.86.192:/mnt/docker-data/compose/hf-pdocker-01/matrix/
ssh glyptic@192.168.86.192 'cd /mnt/docker-data/compose/hf-pdocker-01/matrix && docker compose up -d synapse-db && sleep 20 && docker exec synapse-db psql -U synapse -d synapse -c "show lc_collate;"'
```
Expected: `lc_collate` = `C`. **If it says anything else, delete `/var/lib/synapse-db` and redo** — Synapse will refuse to start otherwise, and fixing it later means re-porting.
- [ ] **Step 3: Commit**
```bash
git add compose/hf-pdocker-01/matrix/docker-compose.yml
git commit -m "feat: add postgres for synapse (local disk, C collation) ahead of MAS"
```
---
### Task 3: Port Synapse SQLite → PostgreSQL
**Files:**
- Create: `/var/lib/synapse/homeserver-postgres.yaml` (node only)
- Modify: `/var/lib/synapse/homeserver.yaml` (node only)
**Interfaces:**
- Consumes: `synapse-db` from Task 2.
- Produces: a Synapse running on PostgreSQL — the precondition `syn2mas` requires in Task 7.
- [ ] **Step 1: Write the port-target config**
Copy `/var/lib/synapse/homeserver.yaml` to `homeserver-postgres.yaml` and replace the `database:` block with:
```yaml
database:
name: psycopg2
args:
user: synapse
password: <the password from Task 2>
dbname: synapse
host: synapse-db
cp_min: 5
cp_max: 10
```
- [ ] **Step 2: Stop Synapse (downtime starts)**
```bash
ssh glyptic@192.168.86.192 'cd /mnt/docker-data/compose/hf-pdocker-01/matrix && docker compose stop synapse'
```
- [ ] **Step 3: Run the port**
```bash
ssh glyptic@192.168.86.192 'docker run --rm --network matrix_matrix-internal \
-v /var/lib/synapse:/data ghcr.io/element-hq/synapse:v1.159.0 \
synapse_port_db --sqlite-database /data/homeserver.db --postgres-config /data/homeserver-postgres.yaml'
```
Expected: a progress table per-table, ending in `Success!`. Budget 10–40 minutes for ~94k events on this hardware. If it fails partway, drop and recreate the database before retrying — **the port tool is not idempotent against a partially-filled database.**
- [ ] **Step 4: Verify row counts match the source**
```bash
ssh glyptic@192.168.86.192 'docker exec synapse-db psql -U synapse -d synapse -tc "select (select count(*) from users) as users, (select count(*) from events) as events;"'
```
Expected: `19 | 93884` (or higher for events if the server ran between backup and port). **Must not be lower.**
- [ ] **Step 5: Cut Synapse over**
```bash
ssh glyptic@192.168.86.192 'sudo cp /var/lib/synapse/homeserver.yaml /var/lib/synapse/homeserver.yaml.bak-presql
sudo cp /var/lib/synapse/homeserver-postgres.yaml /var/lib/synapse/homeserver.yaml
cd /mnt/docker-data/compose/hf-pdocker-01/matrix && docker compose start synapse'
```
- [ ] **Step 6: Verify the service on PostgreSQL**
```bash
ssh glyptic@192.168.86.192 '
for i in $(seq 1 30); do [ "$(docker inspect synapse --format "{{.State.Health.Status}}")" = healthy ] && echo healthy && break; sleep 10; done
curl -s http://localhost:8008/_synapse/admin/v1/server_version; echo
curl -s -X POST http://localhost:8008/_matrix/client/v3/login -H "Content-Type: application/json" \
-d "{\"type\":\"m.login.password\",\"identifier\":{\"type\":\"m.id.user\",\"user\":\"jeremy\"},\"password\":\"bigdog01\"}" | head -c 80
docker logs synapse --since 5m 2>&1 | grep -cE " ERROR "'
```
Expected: `healthy`, version `1.159.0`, an `access_token` in the login response, `0` errors. Log out that verification session afterwards (`POST /_matrix/client/v3/logout`).
- [ ] **Step 7: Confirm bots reconnected before declaring the task done**
```bash
ssh glyptic@192.168.86.192 'sleep 60; docker logs synapse --since 60s 2>&1 | grep -oE "\{@[a-z0-9]+:hobbs.farm\}" | sort | uniq -c'
```
Expected: clippyauto, margaret, spirit, fry all present. **Rollback if not:** restore `homeserver.yaml.bak-presql` and restart — SQLite file is untouched and still authoritative until Task 7.
- [ ] **Step 8: Commit**
```bash
git commit --allow-empty -m "chore: synapse ported from sqlite to postgres (node config, not in repo)"
```
---
### Task 4: PostgreSQL for MAS
**Files:**
- Create: `compose/hf-pdocker-01/mas/docker-compose.yml`
**Interfaces:**
- Produces: `mas-db:5432`, database `mas`, user `mas`, on a new `mas` bridge network that also carries the MAS container.
- [ ] **Step 1: Create the compose file with the database service only**
Same shape as Task 2 Step 1, but: `container_name: mas-db`, `POSTGRES_USER=mas`, `POSTGRES_DB=mas`, volume `/var/lib/mas-db:/var/lib/postgresql/data`, `mem_limit: 256M`, `memswap_limit: 256M`, network `mas`. Default collation is fine — the C-collation requirement is Synapse's, not MAS's.
- [ ] **Step 2: Deploy and verify**
```bash
ssh glyptic@192.168.86.192 'cd /mnt/docker-data/compose/hf-pdocker-01/mas && docker compose up -d mas-db && sleep 20 && docker exec mas-db psql -U mas -d mas -tc "select 1;"'
```
Expected: `1`.
- [ ] **Step 3: Commit**
```bash
git add compose/hf-pdocker-01/mas/docker-compose.yml
git commit -m "feat: add mas-db postgres for matrix authentication service"
```
---
### Task 5: Generate MAS config and deploy the service
**Files:**
- Create: `/mnt/docker-data/configs/mas/config.yaml` (node only — contains secrets)
- Modify: `compose/hf-pdocker-01/mas/docker-compose.yml`
**Interfaces:**
- Consumes: `mas-db` from Task 4.
- Produces: MAS listening on host port **8020** (verified free on .192: 8008/8010/8080/8082/8085/8086/8090/8123/8181/8199/8300/8310 are taken).
- [ ] **Step 1: Generate a config seeded from the Synapse config**
```bash
ssh glyptic@192.168.86.192 'sudo mkdir -p /mnt/docker-data/configs/mas && docker run --rm \
-v /var/lib/synapse:/synapse:ro -v /mnt/docker-data/configs/mas:/out \
ghcr.io/element-hq/matrix-authentication-service:latest \
config generate --synapse-config /synapse/homeserver.yaml --output /out/config.yaml'
```
Expected: `config.yaml` written, containing generated signing keys and a `matrix.secret` shared secret.
- [ ] **Step 2: Set the database URI, public base, and password hashing scheme**
Edit `/mnt/docker-data/configs/mas/config.yaml`:
```yaml
database:
uri: postgresql://mas:<password>@mas-db/mas
http:
public_base: https://auth.hobbs.farm
listeners:
- name: web
resources: [{name: discovery}, {name: human}, {name: oauth}, {name: compat}, {name: graphql}, {name: assets}]
binds: [{address: "[::]:8080"}]
passwords:
enabled: true
schemes:
- version: 1
algorithm: bcrypt
unicode_normalization: true # REQUIRED for imported Synapse hashes
- version: 2
algorithm: argon2id
```
The `unicode_normalization: true` on the bcrypt v1 scheme is what makes migrated Synapse password hashes verify. Without it, **every migrated password silently fails**.
- [ ] **Step 3: Add the MAS service to compose**
```yaml
mas:
image: ghcr.io/element-hq/matrix-authentication-service:latest # pin to the digest after first pull
container_name: mas
restart: unless-stopped
command: ["server", "--config", "/config/config.yaml"]
volumes:
- /mnt/docker-data/configs/mas:/config:ro
ports:
- "8020:8080"
networks: [mas, matrix-internal]
depends_on:
mas-db: {condition: service_healthy}
mem_limit: 256M
memswap_limit: 256M
labels:
- "com.centurylinklabs.watchtower.enable=false"
```
MAS needs to reach Synapse's admin API and Synapse needs to reach MAS, hence both networks.
- [ ] **Step 4: Run migrations and start**
```bash
ssh glyptic@192.168.86.192 'cd /mnt/docker-data/compose/hf-pdocker-01/mas && docker compose run --rm mas database migrate --config /config/config.yaml && docker compose up -d mas && sleep 15 && curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8020/health'
```
Expected: migrations apply cleanly, health endpoint returns `200`.
- [ ] **Step 5: Store the MAS secrets in Vaultwarden and commit**
The generated `config.yaml` holds signing keys that cannot be regenerated without invalidating sessions. Store a copy in Vaultwarden (note: the vault master password is currently unknown — see memory `vault-master-password-stale`; resolve that first or keep an encrypted copy on the NAS).
```bash
git add compose/hf-pdocker-01/mas/docker-compose.yml
git commit -m "feat: deploy matrix authentication service on :8020"
```
---
### Task 6: syn2mas check and dry-run
**Interfaces:**
- Consumes: Synapse-on-PostgreSQL (Task 3), MAS + DB (Tasks 4–5).
- Produces: a clean dry-run, the go/no-go gate for Task 7.
- [ ] **Step 1: Run the checker**
```bash
ssh glyptic@192.168.86.192 'docker exec mas mas-cli syn2mas check --config /config/config.yaml --synapse-config /synapse/homeserver.yaml'
```
(Mount `/var/lib/synapse` read-only into the MAS container as `/synapse` for this — add it in Task 5 Step 3 if not already present.)
Expected: no blocking findings. Known blockers it looks for: custom password provider modules, SAML/LDAP providers. This deployment has neither.
- [ ] **Step 2: Dry run**
```bash
ssh glyptic@192.168.86.192 'docker exec mas mas-cli syn2mas migrate --config /config/config.yaml --synapse-config /synapse/homeserver.yaml --dry-run'
```
Expected: a summary of users, password hashes, devices, and access tokens it *would* import. Cross-check the user count against **19** and the device count against Task 1 Step 1. **Do not proceed to Task 7 if the counts disagree.**
- [ ] **Step 3: Record the dry-run output in the inventory doc and commit**
```bash
git add docs/superpowers/plans/mas-auth-inventory.md
git commit -m "docs: syn2mas dry-run results"
```
---
### Task 7: Migrate accounts into MAS and delegate Synapse auth
**Files:**
- Modify: `/var/lib/synapse/homeserver.yaml` (node only)
**Interfaces:**
- Produces: Synapse delegating all authentication to MAS — the precondition MSC4108 requires.
**This is the point of no return.** The MAS docs state migration "is not easily reversible and requires careful backup procedures before proceeding." Rollback means restoring both databases from backup.
- [ ] **Step 1: Fresh backup of both databases immediately before migrating**
```bash
ssh glyptic@192.168.86.192 '
cd /mnt/docker-data/compose/hf-pdocker-01/matrix && docker compose stop synapse
docker exec synapse-db pg_dump -U synapse synapse | gzip > /tmp/synapse-premas.sql.gz
docker exec mas-db pg_dump -U mas mas | gzip > /tmp/mas-premigrate.sql.gz
sudo mv /tmp/*.gz /var/lib/synapse.bak-premas/ && ls -la /var/lib/synapse.bak-premas/*.gz'
```
Expected: two non-empty `.gz` files.
- [ ] **Step 2: Run the migration (Synapse stays stopped)**
```bash
ssh glyptic@192.168.86.192 'docker exec mas mas-cli syn2mas migrate --config /config/config.yaml --synapse-config /synapse/homeserver.yaml'
```
Expected: completes with an import summary matching the dry-run.
- [ ] **Step 3: Point Synapse at MAS**
Add to `/var/lib/synapse/homeserver.yaml`:
```yaml
matrix_authentication_service:
enabled: true
endpoint: http://mas:8080/
secret: <the matrix.secret value from /mnt/docker-data/configs/mas/config.yaml>
```
- [ ] **Step 4: Start Synapse and verify delegation is live**
```bash
ssh glyptic@192.168.86.192 '
cd /mnt/docker-data/compose/hf-pdocker-01/matrix && docker compose start synapse
for i in $(seq 1 30); do [ "$(docker inspect synapse --format "{{.State.Health.Status}}")" = healthy ] && echo healthy && break; sleep 10; done
curl -s -o /dev/null -w "auth_metadata: %{http_code}\n" http://localhost:8008/_matrix/client/unstable/org.matrix.msc2965/auth_metadata'
```
Expected: `healthy`, and auth_metadata now returns **200** (it returns 404 today).
- [ ] **Step 5: Verify every account from the Task 1 inventory still authenticates**
```bash
ssh glyptic@192.168.86.192 '
echo "--- password login via MAS compat layer:"
curl -s -X POST http://localhost:8008/_matrix/client/v3/login -H "Content-Type: application/json" \
-d "{\"type\":\"m.login.password\",\"identifier\":{\"type\":\"m.id.user\",\"user\":\"jeremy\"},\"password\":\"bigdog01\"}" | head -c 100; echo
echo "--- migrated access token (frigate):"
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8008/_matrix/client/v3/account/whoami -H "Authorization: Bearer syt_ZnJpZ2F0<rest-from-.env>"
echo "--- bots syncing:"
sleep 90; docker logs synapse --since 90s 2>&1 | grep -oE "\{@[a-z0-9]+:hobbs.farm\}" | sort | uniq -c'
```
Expected: an `access_token` for jeremy, `200` for the frigate token, and margaret/spirit/clippyauto/fry all syncing. **If password login fails, the bcrypt `unicode_normalization: true` setting from Task 5 Step 2 is the first thing to check.**
- [ ] **Step 6: Commit**
```bash
git commit --allow-empty -m "feat: synapse auth delegated to MAS (node config)"
```
---
### Task 8: Publish MAS and add DNS
**Files:**
- Modify: `/mnt/docker-data/traefik/dynamic/temp-routes.yml` (confirmed: this is the file holding the `mess`, `wellknown`, `yoda` and `solo` routers)
- External: Cloudflare DNS
- [ ] **Step 1: Add the Cloudflare A record FIRST**
`auth.hobbs.farm` → `98.160.103.126`, **DNS-only (grey cloud)**. There is no wildcard record on this zone, and Traefik's DNS-01 challenge will happily issue a valid certificate for a hostname that does not resolve — so a working cert proves nothing here (memory `hobbs-farm-dns`).
Verify from outside the LAN before continuing:
```bash
getent hosts auth.hobbs.farm
```
Expected: `98.160.103.126`.
- [ ] **Step 2: Add the Traefik router and service**
```yaml
auth:
rule: "Host(`auth.hobbs.farm`)"
service: auth
entryPoints: [websecure]
middlewares: [security-headers]
tls: {certResolver: letsencrypt}
```
```yaml
auth:
loadBalancer:
servers:
- url: "http://192.168.86.192:8020"
```
Do **not** add basicAuth — clients must reach this unauthenticated to log in.
- [ ] **Step 3: Verify publicly**
```bash
curl -s -o /dev/null -w "%{http_code}\n" https://auth.hobbs.farm/health
curl -s https://mess.hobbs.farm/_matrix/client/unstable/org.matrix.msc2965/auth_metadata | head -c 300
```
Expected: `200`, and auth metadata advertising `https://auth.hobbs.farm` endpoints.
- [ ] **Step 4: Confirm `.well-known` needs no change (verify, do not assume)**
Current contents, served from `/mnt/docker-data/configs/matrix/wellknown/` via `matrix-wellknown` on :8010:
```
client -> {"m.homeserver": {"base_url": "https://mess.hobbs.farm"}}
server -> {"m.server": "mess.hobbs.farm:443"}
```
Under MAS, clients discover the auth server from Synapse's `auth_metadata` endpoint, not from `.well-known`, so **both files should stay as they are**. Verify a client can complete discovery end to end:
```bash
curl -s https://hobbs.farm/.well-known/matrix/client
curl -s https://mess.hobbs.farm/_matrix/client/unstable/org.matrix.msc2965/auth_metadata | head -c 200
```
Expected: `.well-known` unchanged and still pointing at `mess.hobbs.farm`; auth_metadata returning the `auth.hobbs.farm` issuer. If a client cannot find the auth server, revisit `http.public_base` in the MAS config (Task 5 Step 2) before touching `.well-known`.
- [ ] **Step 5: Add a Prometheus blackbox probe for the new endpoint**
Add `https://auth.hobbs.farm/health` to the blackbox target list in `/mnt/docker-data/configs/prometheus/prometheus.yml`, then hot-reload:
```bash
ssh glyptic@192.168.86.192 'curl -s -X POST http://localhost:9095/-/reload && sleep 5 && curl -s "http://localhost:9095/api/v1/query?query=probe_success" | grep -o "auth.hobbs.farm"'
```
Expected: the target appears and probes `1`. **Auth being down now means nobody can log in — this probe is not optional.**
- [ ] **Step 6: Commit**
```bash
git add compose/ && git commit -m "feat: publish MAS at auth.hobbs.farm with blackbox probe"
```
---
### Task 9: Enable the QR MSCs
**Files:**
- Modify: `/var/lib/synapse/homeserver.yaml` (node only)
- [ ] **Step 1: Enable both MSCs**
```yaml
experimental_features:
msc4108_enabled: true
msc4388_mode: authenticated
```
`msc4388_mode: authenticated` requires the rendezvous channel user to be logged in — prefer it over `open`, which would let unauthenticated parties open rendezvous sessions on an internet-facing server.
- [ ] **Step 2: Restart and confirm Synapse actually starts**
```bash
ssh glyptic@192.168.86.192 'docker restart synapse && sleep 45 && docker logs synapse --since 2m 2>&1 | grep -iE "ConfigError|MSC4108|MSC4388" | head'
```
Expected: **no `ConfigError`**. If you see `MSC4108 requires matrix_authentication_service to be enabled`, Task 7 Step 3 did not take effect.
- [ ] **Step 3: Verify the feature is advertised**
```bash
ssh glyptic@192.168.86.192 'curl -s http://localhost:8008/_matrix/client/versions | python3 -c "
import sys,json; uf=json.load(sys.stdin)[\"unstable_features\"]
print(\"msc4108:\", uf.get(\"org.matrix.msc4108\"))
"'
```
Expected: `msc4108: True` (it is `False` today).
- [ ] **Step 4: Commit**
```bash
git commit --allow-empty -m "feat: enable MSC4108/MSC4388 QR sign-in"
```
---
### Task 10: End-to-end QR sign-in test and documentation
- [ ] **Step 1: Test the actual feature**
On an already-signed-in Element X Android device: Settings → Sessions → "Sign in with QR code" / "Link new device". On a second client, choose "Sign in with QR code" and scan.
Expected: the new device signs in and receives E2EE keys **without** the password being typed. This is the deliverable — if it does not work here, the feature is not done regardless of what the config says.
- [ ] **Step 2: Re-verify the full account inventory one more time**
Re-run Task 7 Step 5. Expected: unchanged — all bots syncing, all tokens valid.
- [ ] **Step 3: Complete the mandatory post-change checklist (CLAUDE.md)**
- CLAUDE.md: MAS + two PostgreSQL containers on .192, port 8020, `auth.hobbs.farm`, Synapse now on PostgreSQL (supersedes Known Issues #24/#25 wording), new known issue for the MAS dependency — **auth is now a hard dependency for every login, including bots**
- MEMORY.md: new entry for the MAS migration; update `matrix-account` (the reset recipe changes — user management moves to MAS)
- Prometheus: done in Task 8 Step 4
- Farm Manager links page: add `auth.hobbs.farm` to `serverLinks` in `/opt/farm-manager/server/static/app.js`, then **rebuild the image** (links are baked in, not mounted) and redeploy
- Wiki: run the documentation-keeper agent
- [ ] **Step 4: Retire the backups once stable**
Leave `/var/lib/synapse.bak-premas/`, `/var/lib/synapse.bak-20260820-pre1159/`, and the SQLite file in place for **at least a week** of normal use. Only then delete.
- [ ] **Step 5: Final commit**
```bash
git add -A && git commit -m "docs: MAS QR sign-in deployment complete"
```
---
## Rollback Summary
| Failed at | Recovery |
|---|---|
| Task 3 (port to PG) | Restore `homeserver.yaml.bak-presql`; SQLite file untouched and still authoritative |
| Task 7 (syn2mas) | Restore `synapse-premas.sql.gz` + `mas-premigrate.sql.gz`; remove `matrix_authentication_service` block |
| Task 9 (MSCs) | Remove `experimental_features` block; restart — no data implications |
## Open Questions for the Operator
1. **Element Desktop compatibility.** Your two Element Desktop sessions are on the legacy auth stack. OIDC-native support in Element Web/Desktop has historically lagged Element X. Worth confirming before migrating, since a broken desktop client is a worse day than no QR code.
2. **Is `auth.hobbs.farm` the hostname you want?** It becomes permanent — it is baked into session state, and changing `public_base` later invalidates sessions.
3. **Vaultwarden is currently locked out** (memory `vault-master-password-stale`). MAS generates signing keys that must be backed up somewhere durable. Resolve the vault access first, or agree on an alternative.
4. **Node memory.** .192 sits at ~14/23GB with Frigate as the main consumer. This adds ~1GB across three containers (Synapse's PG, MAS's PG, MAS). If that is too tight, the MAS stack could go on .100 instead — it only needs network reachability to Synapse, not co-location.