Files
esh-pfi-infrastructure/stacks/synapse/compose.yaml
T
vh 73866f6a7e feat(synapse): restrict /_synapse/admin to LAN, track the stack
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.
2026-09-01 16:09:42 -07:00

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