73866f6a7e
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. A higher-priority router (explicit priority 100, not relying on Traefik's rule-length tie-break) scopes PathPrefix(/_synapse/admin) behind an ipallowlist. Verified from a genuinely external vantage rather than from a fleet host, since nh3-dev sits inside the allowed range and would have proved nothing: via the NH3 residential egress proxy the admin path returns 403 while the client API returns 200 and Element is unaffected. The 10.0.0.0/8 entry matches nothing today and the comment says so rather than implying fleet access exists. matrix.phasefinal.com resolves publicly, so fleet hosts hairpin out their own WAN -- a request from nh3-dev arrived as 70.230.226.88. The rule is effectively deny-all through Traefik, which is the intended posture: admin work goes through docker exec to localhost:8008 and never traverses Traefik. Allow-listing the sites' WAN addresses was considered and rejected as a maintenance trap on dynamic addresses. Also brings the stack under stacks/ with the Postgres password replaced by a required .env variable. The tracked copy and the live file have therefore DIVERGED and deploy-stack.sh must not be used until the live file reads from a .env; the README says so.
143 lines
6.2 KiB
YAML
143 lines
6.2 KiB
YAML
# ==============================================================================
|
|
# 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
|