diff --git a/stacks/synapse/README.md b/stacks/synapse/README.md new file mode 100644 index 0000000..53c12c7 --- /dev/null +++ b/stacks/synapse/README.md @@ -0,0 +1,49 @@ +# synapse — Matrix homeserver (ana-docker) + +Canonical copy of the stack running at `/opt/docker/compose/synapse/` on +**ana-docker** (`10.250.50.70`). Serves `matrix.phasefinal.com` and +`chat.phasefinal.com` through Traefik. + +Deep documentation — current versions, the upgrade procedure, the appservice +namespace decision, conventions, and the push-notification findings — lives in +[`docs/pfi/vm-102-matrix-synapse.md`](../../docs/pfi/vm-102-matrix-synapse.md). +This file covers only what is specific to deploying the stack. + +## ⚠ The password is NOT in this copy + +The live compose carries `POSTGRES_PASSWORD` **inline in plaintext**. This +tracked copy replaces it with `${POSTGRES_PASSWORD:?set in .env}` so the secret +is not committed. + +**They have therefore diverged**, and `deploy-stack.sh` would push the +placeholder over a working literal and break the database on next recreate. +Do not deploy this stack from the repo until the live file has been converted +to read from a `.env` — which is the right fix and is not done yet. + +Until then: **edit the live file on the host, then re-mirror to here.** + +## /_synapse/admin is LAN-only + +Synapse mounts its admin API on the same vhost as the client API, so publishing +`matrix.phasefinal.com` published the admin surface with it — it answered 200 +from the open internet until 2026-09-01. + +A higher-priority router now scopes `PathPrefix(/_synapse/admin)` behind an +`ipallowlist`. Verified from a genuinely external vantage (the NH3 residential +egress proxy, not a fleet host): admin **403**, client API **200**, Element +unaffected. + +⚠ The `10.0.0.0/8` entry currently matches nothing, deliberately. The hostname +resolves publicly, so fleet hosts hairpin out their own WAN and arrive as a +public address — a request from nh3-dev measured as `70.230.226.88`. The rule is +effectively **deny-all through Traefik**, which is intended: admin work is done +with `docker exec synapse` against `localhost:8008`, which never touches +Traefik. Allow-listing the sites' WAN addresses was rejected — they are dynamic, +and a stale entry either locks us out or hands admin to whoever inherits the +address. + +## Upgrades + +**Take a verified `pg_dump` first.** Schema migrations are one-way; an older +Synapse will not start against a newer database, so rollback is restore, not +revert. Full procedure in the docs link above. diff --git a/stacks/synapse/compose.yaml b/stacks/synapse/compose.yaml new file mode 100644 index 0000000..422b649 --- /dev/null +++ b/stacks/synapse/compose.yaml @@ -0,0 +1,142 @@ +# ============================================================================== +# Matrix Synapse Stack — VM-102 (10.250.50.70) +# Domain: matrix.phasefinal.com +# +# Deploy: /opt/docker/compose/synapse/compose.yaml +# Config: /opt/docker/conf/synapse/homeserver.yaml +# +# Services: +# 1. synapse-db — Postgres 16 (internal only) +# 2. synapse — Matrix Synapse homeserver (port 8008) +# 3. element-web — Element Web client (port 8080) +# +# Stack uses traefik-net (tnet) for reverse proxy / TLS termination. +# Cert resolver: anaprod (matches existing VM-102 convention) +# ============================================================================== + +services: + # --------------------------------------------------------------------------- + # Postgres database for Synapse + # --------------------------------------------------------------------------- + synapse-db: + image: postgres:16-alpine + container_name: synapse-db + restart: unless-stopped + environment: + POSTGRES_DB: synapse + POSTGRES_USER: synapse + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set in .env} + POSTGRES_INITDB_ARGS: --lc-collate=C --lc-ctype=C --encoding=UTF8 + volumes: + - synapse-db-data:/var/lib/postgresql/data + networks: + - synapse-internal + healthcheck: + test: + - CMD-SHELL + - pg_isready -U synapse -d synapse + interval: 10s + timeout: 5s + retries: 5 + # --------------------------------------------------------------------------- + # Matrix Synapse homeserver + # --------------------------------------------------------------------------- + synapse: + image: matrixdotorg/synapse:v1.159.0 + container_name: synapse + restart: unless-stopped + depends_on: + synapse-db: + condition: service_healthy + volumes: + - /opt/docker/conf/synapse/homeserver.yaml:/data/homeserver.yaml:ro + - synapse-data:/data + - /opt/docker/conf/synapse/aipa_appservice.yaml:/conf/aipa_appservice.yaml:ro + networks: + - tnet + - synapse-internal + labels: + - traefik.enable=true + - traefik.http.routers.synapse.rule=Host(`matrix.phasefinal.com`) + - traefik.http.routers.synapse.tls=true + - traefik.http.routers.synapse.tls.certresolver=anaprod + - traefik.http.services.synapse.loadbalancer.server.port=8008 + # -- /_synapse/admin is LAN-only ------------------------------------ + # Synapse mounts its admin API on the same vhost as the client API, so + # publishing matrix.phasefinal.com published the admin surface too. It + # answered 200 from the open internet. HMAC-protected, so not an open + # door -- but Synapse's own guidance is to keep it off the public + # listener, and a shared secret is a poor last line. + # + # Longer rule than the parent router; priority set EXPLICITLY rather than + # relying on Traefik's rule-length tie-break. + # + # This host is NOT Cloudflare-proxied (plain A record to 38.120.12.44), + # so the address Traefik sees is the real client and there is no + # forwarded-header to spoof past. + # + # WARNING: if this record is ever put behind Cloudflare, every request + # arrives from a Cloudflare address and matches nothing here. That is + # deny-everyone, and it tells you nothing about why. Revisit if proxied. + # + # ⚠ AS OF 2026-09-01 THE 10.0.0.0/8 ENTRY MATCHES NOTHING, AND THAT IS + # EXPECTED. matrix.phasefinal.com resolves PUBLICLY, so fleet hosts + # reaching it go out their own WAN and arrive here as a public address -- + # measured: a request from nh3-dev (10.100.10.50) landed as 70.230.226.88. + # So this rule is EFFECTIVELY DENY-ALL through Traefik, which is the + # intended posture, not an accident: + # + # - Admin work is done via `docker exec synapse` against localhost:8008, + # which never traverses Traefik and is unaffected by any of this. + # - Allow-listing the sites' WAN addresses was considered and REJECTED. + # They are dynamic; a stale entry either locks us out or, worse, hands + # admin to whoever inherits the address next. + # + # The private ranges are kept anyway because they are free and become live + # the moment an internal DNS rewrite points this name at 10.250.50.70 -- + # at which point fleet traffic stops hairpinning and starts matching. + - "traefik.http.routers.synapse-admin.rule=Host(`matrix.phasefinal.com`) && PathPrefix(`/_synapse/admin`)" + - traefik.http.routers.synapse-admin.priority=100 + - traefik.http.routers.synapse-admin.entrypoints=websecure + - traefik.http.routers.synapse-admin.tls=true + - traefik.http.routers.synapse-admin.tls.certresolver=anaprod + - traefik.http.routers.synapse-admin.service=synapse + - traefik.http.routers.synapse-admin.middlewares=synapse-admin-lan + # Traefik v3 spelling: ipallowlist. (v2 called it ipwhitelist.) + - traefik.http.middlewares.synapse-admin-lan.ipallowlist.sourcerange=10.0.0.0/8,127.0.0.1/32 + # --------------------------------------------------------------------------- + # Element Web client + # --------------------------------------------------------------------------- + element-web: + image: vectorim/element-web:v1.12.27 + container_name: element-web + restart: unless-stopped + depends_on: + - synapse + volumes: + - /opt/docker/conf/synapse/element-config.json:/app/config.json:ro + networks: + - tnet + labels: + - traefik.enable=true + - traefik.http.routers.element.rule=Host(`chat.phasefinal.com`) + - traefik.http.routers.element.tls=true + - traefik.http.routers.element.tls.certresolver=anaprod + - traefik.http.services.element.loadbalancer.server.port=80 + - homepage.group=Apps + - homepage.name=Matrix (Element) + - homepage.icon=si-element + - homepage.description=Matrix homeserver + Element Web client + - homepage.href=https://chat.phasefinal.com +networks: + synapse-internal: + driver: bridge + tnet: + name: traefik-net + external: true +# ============================================================================= +# Volumes +# ============================================================================= +volumes: + synapse-db-data: null + synapse-data: null