Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9dc60194c7 | |||
| 6d9b156738 | |||
| 7353cae399 | |||
| 88806d12fb | |||
| 74b0a3ecb9 | |||
| 0799cd3e38 | |||
| e2f578fde5 | |||
| ff3143cd1f | |||
| 9318a69eae | |||
| 26993e8a6f | |||
| 4e43a6f713 |
@@ -19,6 +19,9 @@ synapse/data/homeserver.yaml.bak
|
||||
synapse/data/*.signing.key
|
||||
synapse/data/*.log.config
|
||||
|
||||
# Appservice registrations (contain tokens, created manually per deployment)
|
||||
appservices/
|
||||
|
||||
# Data directories
|
||||
postgres/data/
|
||||
synapse/data/
|
||||
@@ -43,6 +46,7 @@ backups/
|
||||
# Production deployment configs (contain server IPs and secrets)
|
||||
caddy-server/
|
||||
authelia-server/
|
||||
caddy/Caddyfile
|
||||
caddy/Caddyfile.production
|
||||
|
||||
# macOS
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# =============================================================================
|
||||
# GitLab CI — matrix-2 deploy.sh integration tests
|
||||
#
|
||||
# Runs test_deploy.sh inside a Docker-in-Docker environment.
|
||||
# Both test scenarios execute sequentially in a single job (~15-20 min).
|
||||
#
|
||||
# Requirements on the GitLab runner:
|
||||
# - Docker executor with privileged mode enabled (for dind)
|
||||
# - OR shell executor with Docker + docker compose v2 already installed
|
||||
# =============================================================================
|
||||
|
||||
stages:
|
||||
- test
|
||||
|
||||
variables:
|
||||
# Docker-in-Docker TLS settings
|
||||
DOCKER_HOST: tcp://docker:2376
|
||||
DOCKER_TLS_CERTDIR: "/certs"
|
||||
DOCKER_TLS_VERIFY: "1"
|
||||
DOCKER_CERT_PATH: "/certs/client"
|
||||
|
||||
# ── Full integration test (config generation + live endpoint checks) ──────────
|
||||
deploy-integration:
|
||||
stage: test
|
||||
image: docker:25-cli
|
||||
services:
|
||||
- name: docker:25-dind
|
||||
alias: docker
|
||||
variables:
|
||||
DOCKER_TLS_CERTDIR: "/certs"
|
||||
before_script:
|
||||
- apk add --no-cache bash openssl curl
|
||||
- docker info # smoke-test dind connection
|
||||
script:
|
||||
- chmod +x test_deploy.sh
|
||||
- bash test_deploy.sh
|
||||
after_script:
|
||||
# Capture container logs on failure for easier debugging
|
||||
- >
|
||||
docker compose --project-directory .
|
||||
-f compose-variants/docker-compose.local.yml
|
||||
logs --no-color 2>&1 | tail -300 > ci-container-logs.txt || true
|
||||
timeout: 25 minutes
|
||||
artifacts:
|
||||
when: on_failure
|
||||
paths:
|
||||
- ci-container-logs.txt
|
||||
expire_in: 1 week
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH' # all branch pushes
|
||||
- if: '$CI_MERGE_REQUEST_IID' # all merge requests
|
||||
|
||||
# ── Config-only test (fast path — no endpoint checks, still needs Docker) ─────
|
||||
#
|
||||
# Useful for quick feedback on config-generation changes without waiting for
|
||||
# full service startup. Docker is still required because deploy.sh runs
|
||||
# `docker run matrixdotorg/synapse:latest generate` to create homeserver.yaml.
|
||||
deploy-config-only:
|
||||
stage: test
|
||||
image: docker:25-cli
|
||||
services:
|
||||
- name: docker:25-dind
|
||||
alias: docker
|
||||
variables:
|
||||
DOCKER_TLS_CERTDIR: "/certs"
|
||||
variables:
|
||||
SKIP_INTEGRATION: "true"
|
||||
before_script:
|
||||
- apk add --no-cache bash openssl curl
|
||||
- docker info
|
||||
script:
|
||||
- chmod +x test_deploy.sh
|
||||
- bash test_deploy.sh
|
||||
timeout: 12 minutes
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH'
|
||||
- if: '$CI_MERGE_REQUEST_IID'
|
||||
@@ -78,6 +78,7 @@ http:
|
||||
- name: graphql
|
||||
playground: true
|
||||
- name: assets # ← Critical: This is required!
|
||||
- name: adminapi # ← Required for Element Admin panel
|
||||
binds:
|
||||
- address: '[::]:8080'
|
||||
```
|
||||
|
||||
@@ -198,5 +198,6 @@ docker compose restart mas
|
||||
| `homeserver.domain not configured` (bridge) | Run `setup-bridges.sh` |
|
||||
| `as_token was not accepted` | Registration not loaded in Synapse — check `homeserver.yaml` |
|
||||
| MAS CSS missing | Add `- name: assets` to MAS listener resources |
|
||||
| Element Admin: `TypeError: Failed to fetch` | Add `- name: adminapi` to MAS listener resources |
|
||||
| `Template rendered to empty string` | Set `fetch_userinfo: true` in MAS upstream provider |
|
||||
| Bridge: `Connection refused` | Bridge hostname is 127.0.0.1 — must be 0.0.0.0 in config |
|
||||
|
||||
@@ -109,6 +109,12 @@ TELEGRAM_API_HASH=your_hash
|
||||
|
||||
Bridges use double puppet support (messages appear from your actual Matrix user, not a bridge bot) and have encryption disabled for compatibility with MAS. See [BRIDGE_SETUP_GUIDE.md](BRIDGE_SETUP_GUIDE.md) for details.
|
||||
|
||||
## Air-gapped / Custom Registry
|
||||
|
||||
`deploy.sh` optionally prefixes all image references with a custom registry URL (for internal mirrors or air-gapped environments) and optionally switches Redis, PostgreSQL, and Caddy to hardened variants from [dhi.io](https://dhi.io). Both settings are written to `.env` and picked up automatically by Docker Compose.
|
||||
|
||||
See [SETUP.md — Custom Docker Registry](SETUP.md#custom-docker-registry) for details, including a note on pull-through cache registries (Harbor, Artifactory, Nexus) that require the full registry path in image names.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Docker and Docker Compose v2
|
||||
|
||||
@@ -23,6 +23,22 @@ Key variables:
|
||||
| `POSTGRES_PASSWORD` | Shared database password (same across all services) |
|
||||
| `MAS_SECRET_KEY` | 64-char hex encryption key for MAS |
|
||||
| `LIVEKIT_SECRET` | LiveKit API secret (only if Element Call enabled) |
|
||||
| `POSTGRES_IMAGE` | PostgreSQL image (default: `postgres:16-alpine`) |
|
||||
| `SYNAPSE_IMAGE` | Synapse image (default: `matrixdotorg/synapse:latest`) |
|
||||
| `ELEMENT_IMAGE` | Element Web image (default: `vectorim/element-web:latest`) |
|
||||
| `ELEMENT_ADMIN_IMAGE` | Element Admin image (default: `oci.element.io/element-admin:latest`) |
|
||||
| `REDIS_IMAGE` | Redis image (default: `redis:7-alpine`) |
|
||||
| `MAS_IMAGE` | MAS image (default: `ghcr.io/element-hq/matrix-authentication-service:latest`) |
|
||||
| `TELEGRAM_IMAGE` | mautrix-telegram image (default: `dock.mau.dev/mautrix/telegram:latest`) |
|
||||
| `WHATSAPP_IMAGE` | mautrix-whatsapp image (default: `dock.mau.dev/mautrix/whatsapp:latest`) |
|
||||
| `SIGNAL_IMAGE` | mautrix-signal image (default: `dock.mau.dev/mautrix/signal:latest`) |
|
||||
| `LIVEKIT_IMAGE` | LiveKit image (default: `livekit/livekit-server:latest`) |
|
||||
| `LK_JWT_IMAGE` | lk-jwt-service image (default: `ghcr.io/element-hq/lk-jwt-service:latest`) |
|
||||
| `ELEMENT_CALL_IMAGE` | Element Call image (default: `ghcr.io/element-hq/element-call:latest`) |
|
||||
| `AUTHELIA_IMAGE` | Authelia image (default: `authelia/authelia:latest`) |
|
||||
| `CADDY_IMAGE` | Caddy image (default: `caddy:2-alpine`) |
|
||||
|
||||
All image variables are optional. If absent, each compose service falls back to the default tag via `${VAR:-default}` syntax, so `docker compose` works without a `.env` file.
|
||||
|
||||
### `synapse/data/homeserver.yaml`
|
||||
|
||||
@@ -206,6 +222,44 @@ The key name `livekit-key` must match the `LIVEKIT_KEY` value in `docker-compose
|
||||
|
||||
---
|
||||
|
||||
## Custom Docker Registry
|
||||
|
||||
`deploy.sh` prompts for two optional image settings:
|
||||
|
||||
**Custom registry prefix** — prepends a registry URL to every image. Useful for air-gapped environments or internal mirrors:
|
||||
|
||||
```
|
||||
Custom Docker registry prefix: myregistry.example.com
|
||||
```
|
||||
|
||||
Results in `.env` entries like:
|
||||
```
|
||||
SYNAPSE_IMAGE=myregistry.example.com/matrixdotorg/synapse:latest
|
||||
REDIS_IMAGE=myregistry.example.com/redis:7-alpine
|
||||
```
|
||||
|
||||
**Hardened images** — uses [dhi.io](https://dhi.io) hardened variants for Redis, PostgreSQL, and Caddy. Takes priority over the custom registry prefix for those three images:
|
||||
|
||||
```
|
||||
REDIS_IMAGE=dhi.io/redis:7
|
||||
POSTGRES_IMAGE=dhi.io/postgres:16
|
||||
CADDY_IMAGE=dhi.io/caddy:2
|
||||
```
|
||||
|
||||
### Pull-through cache registries (Harbor, Artifactory, Nexus)
|
||||
|
||||
Many enterprise registries act as pull-through caches that mirror images under the **full original registry path**. For example, Harbor's proxy cache serves Docker Hub images as:
|
||||
|
||||
```
|
||||
myregistry.example.com/docker.io/library/redis:7-alpine
|
||||
myregistry.example.com/docker.io/matrixdotorg/synapse:latest
|
||||
myregistry.example.com/ghcr.io/element-hq/matrix-authentication-service:latest
|
||||
```
|
||||
|
||||
The custom registry prefix in `deploy.sh` does **not** add the `docker.io/` or other intermediate path components — it produces `myregistry.example.com/redis:7-alpine`. If your mirror requires the full path structure, set the image variables manually in `.env` after running `deploy.sh`, or configure your registry to serve images without the intermediate path (most registries support this as an alias/rewrite).
|
||||
|
||||
---
|
||||
|
||||
## Docker Compose Profiles
|
||||
|
||||
The `docker-compose.yml` uses Docker Compose profiles to make services optional:
|
||||
|
||||
-289
@@ -1,289 +0,0 @@
|
||||
# Local Development Caddyfile for Matrix Stack
|
||||
# Uses self-signed certificates for local HTTPS testing
|
||||
# Auto-generated by deploy.sh — do not edit manually
|
||||
|
||||
{
|
||||
# Use local CA for self-signed certificates
|
||||
local_certs
|
||||
# Enable admin API
|
||||
admin 0.0.0.0:2019
|
||||
}
|
||||
|
||||
# =========================
|
||||
# Matrix Homeserver (Synapse)
|
||||
# =========================
|
||||
matrix.example.test:443 {
|
||||
# TLS with self-signed cert
|
||||
tls internal
|
||||
|
||||
# Well-known client endpoint
|
||||
# IMPORTANT: The respond body must stay on a single line. If you edit this file manually
|
||||
# and your editor wraps the JSON, Caddy will refuse to start with "invalid control character".
|
||||
@wk path /.well-known/matrix/client
|
||||
handle @wk {
|
||||
header Content-Type application/json
|
||||
header Access-Control-Allow-Origin "*"
|
||||
respond `{"m.homeserver":{"base_url":"https://matrix.example.test"},"m.authentication":{"issuer":"https://auth.example.test/"},"org.matrix.msc4143.rtc_foci":[{"type":"livekit","livekit_service_url":"https://rtc.example.test/livekit/jwt"}]}` 200
|
||||
}
|
||||
|
||||
# Well-known server endpoint (federation)
|
||||
@wk_server path /.well-known/matrix/server
|
||||
handle @wk_server {
|
||||
header Content-Type application/json
|
||||
respond `{"m.server":"matrix.example.test:443"}` 200
|
||||
}
|
||||
|
||||
# Rendezvous endpoints for QR code login (MSC4108)
|
||||
@rendezvous path_regexp rendezvous ^/_matrix/client/(unstable|v1)/org\.matrix\.(msc3886|msc4108)/rendezvous.*$
|
||||
handle @rendezvous {
|
||||
header Access-Control-Allow-Origin "*"
|
||||
header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
|
||||
header Access-Control-Allow-Headers "Authorization, Content-Type, Accept, If-Match, If-None-Match"
|
||||
header Vary "Origin, Access-Control-Request-Method, Access-Control-Request-Headers"
|
||||
encode {
|
||||
}
|
||||
reverse_proxy synapse:8008 {
|
||||
header_down -Access-Control-Allow-Origin
|
||||
header_down -Access-Control-Allow-Methods
|
||||
header_down -Access-Control-Allow-Headers
|
||||
header_down -Vary
|
||||
}
|
||||
}
|
||||
|
||||
# Client versions endpoint with CORS
|
||||
@versions path /_matrix/client/versions
|
||||
handle @versions {
|
||||
header Access-Control-Allow-Origin "*"
|
||||
header Access-Control-Allow-Methods "GET, OPTIONS"
|
||||
header Access-Control-Allow-Headers "Authorization, Content-Type, Accept"
|
||||
header Vary "Origin, Access-Control-Request-Method, Access-Control-Request-Headers"
|
||||
reverse_proxy synapse:8008 {
|
||||
header_down -Access-Control-Allow-Origin
|
||||
header_down -Access-Control-Allow-Methods
|
||||
header_down -Access-Control-Allow-Headers
|
||||
header_down -Vary
|
||||
}
|
||||
}
|
||||
|
||||
# CORS preflight for auth metadata
|
||||
@auth_preflight {
|
||||
method OPTIONS
|
||||
path /_matrix/client/unstable/org.matrix.msc2965/auth_metadata
|
||||
}
|
||||
handle @auth_preflight {
|
||||
header Access-Control-Allow-Origin "*"
|
||||
header Access-Control-Allow-Methods "GET, OPTIONS"
|
||||
header Access-Control-Allow-Headers "Authorization, Content-Type, Accept"
|
||||
header Access-Control-Max-Age "86400"
|
||||
respond 204
|
||||
}
|
||||
|
||||
# CORS preflight for all Matrix API
|
||||
@preflight {
|
||||
method OPTIONS
|
||||
path_regexp matrix ^/_matrix/.*$
|
||||
}
|
||||
handle @preflight {
|
||||
header Access-Control-Allow-Origin "*"
|
||||
header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
|
||||
header Access-Control-Allow-Headers "Authorization, Content-Type, Accept"
|
||||
header Access-Control-Max-Age "86400"
|
||||
respond 204
|
||||
}
|
||||
|
||||
# MAS compat endpoints (login/logout/refresh) with CORS
|
||||
@compat path \
|
||||
/_matrix/client/v3/login* \
|
||||
/_matrix/client/v3/logout* \
|
||||
/_matrix/client/v3/refresh* \
|
||||
/_matrix/client/r0/login* \
|
||||
/_matrix/client/r0/logout* \
|
||||
/_matrix/client/r0/refresh*
|
||||
handle @compat {
|
||||
header Access-Control-Allow-Origin "*"
|
||||
header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
|
||||
header Access-Control-Allow-Headers "Authorization, Content-Type, Accept"
|
||||
header Vary "Origin, Access-Control-Request-Method, Access-Control-Request-Headers"
|
||||
reverse_proxy mas:8080 {
|
||||
header_down -Access-Control-Allow-Origin
|
||||
header_down -Access-Control-Allow-Methods
|
||||
header_down -Access-Control-Allow-Headers
|
||||
header_down -Vary
|
||||
}
|
||||
}
|
||||
|
||||
# Everything else under /_matrix → Synapse with CORS
|
||||
@matrix_rest path_regexp matrix ^/_matrix/.*$
|
||||
handle @matrix_rest {
|
||||
header Access-Control-Allow-Origin "*"
|
||||
header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
|
||||
header Access-Control-Allow-Headers "Authorization, Content-Type, Accept"
|
||||
header Vary "Origin, Access-Control-Request-Method, Access-Control-Request-Headers"
|
||||
reverse_proxy synapse:8008 {
|
||||
header_down -Access-Control-Allow-Origin
|
||||
header_down -Access-Control-Allow-Methods
|
||||
header_down -Access-Control-Allow-Headers
|
||||
header_down -Vary
|
||||
}
|
||||
}
|
||||
|
||||
# Default: everything else → Synapse
|
||||
handle {
|
||||
reverse_proxy synapse:8008
|
||||
}
|
||||
}
|
||||
|
||||
# =========================
|
||||
# Matrix Authentication Service (MAS)
|
||||
# =========================
|
||||
auth.example.test:443 {
|
||||
tls internal
|
||||
|
||||
# OIDC Discovery
|
||||
@disco path /.well-known/openid-configuration
|
||||
handle @disco {
|
||||
header ?Access-Control-Allow-Origin "*"
|
||||
header ?Access-Control-Allow-Methods "GET, OPTIONS"
|
||||
header ?Access-Control-Allow-Headers "*"
|
||||
reverse_proxy mas:8080
|
||||
}
|
||||
|
||||
# Dynamic Client Registration: CORS preflight
|
||||
@reg_opts {
|
||||
method OPTIONS
|
||||
path /oauth2/registration
|
||||
}
|
||||
handle @reg_opts {
|
||||
header ?Access-Control-Allow-Origin "*"
|
||||
header ?Access-Control-Allow-Methods "POST, OPTIONS"
|
||||
header ?Access-Control-Allow-Headers "*"
|
||||
respond 204
|
||||
}
|
||||
|
||||
# Dynamic Client Registration (POST)
|
||||
@reg path /oauth2/registration
|
||||
route @reg {
|
||||
header ?Access-Control-Allow-Origin "*"
|
||||
header ?Access-Control-Allow-Methods "POST, OPTIONS"
|
||||
header ?Access-Control-Allow-Headers "*"
|
||||
reverse_proxy mas:8080
|
||||
}
|
||||
|
||||
# JWKS preflight
|
||||
@jwks_opts {
|
||||
method OPTIONS
|
||||
path /oauth2/keys.json
|
||||
}
|
||||
handle @jwks_opts {
|
||||
header ?Access-Control-Allow-Origin "*"
|
||||
header ?Access-Control-Allow-Methods "GET, OPTIONS"
|
||||
header ?Access-Control-Allow-Headers "*"
|
||||
respond 204
|
||||
}
|
||||
|
||||
# Map keys.json → /oauth2/jwks (MAS naming)
|
||||
@jwksjson path /oauth2/keys.json
|
||||
route @jwksjson {
|
||||
header ?Access-Control-Allow-Origin "*"
|
||||
header ?Access-Control-Allow-Methods "GET, OPTIONS"
|
||||
header ?Access-Control-Allow-Headers "*"
|
||||
uri replace /oauth2/keys.json /oauth2/jwks
|
||||
reverse_proxy mas:8080
|
||||
}
|
||||
|
||||
# Generic OAuth2 endpoints
|
||||
@oauth path /oauth2/*
|
||||
route @oauth {
|
||||
header ?Access-Control-Allow-Origin "*"
|
||||
header ?Access-Control-Allow-Methods "GET, OPTIONS, POST"
|
||||
header ?Access-Control-Allow-Headers "*"
|
||||
reverse_proxy mas:8080
|
||||
}
|
||||
|
||||
# Account portal
|
||||
handle_path /account/* {
|
||||
reverse_proxy mas:8080
|
||||
}
|
||||
|
||||
# Authelia endpoints (proxy to authelia)
|
||||
handle_path /authelia/* {
|
||||
reverse_proxy authelia:9091
|
||||
}
|
||||
|
||||
# Fallback: everything else to MAS
|
||||
handle {
|
||||
reverse_proxy mas:8080
|
||||
}
|
||||
|
||||
# Add CORS on error responses
|
||||
handle_errors {
|
||||
header ?Access-Control-Allow-Origin "*"
|
||||
header ?Access-Control-Allow-Headers "*"
|
||||
header ?Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
|
||||
}
|
||||
}
|
||||
|
||||
# =========================
|
||||
# Authelia SSO
|
||||
# =========================
|
||||
authelia.example.test:443 {
|
||||
tls internal
|
||||
|
||||
reverse_proxy authelia:9091
|
||||
}
|
||||
|
||||
# =========================
|
||||
# Element Web Client
|
||||
# =========================
|
||||
element.example.test:443 {
|
||||
tls internal
|
||||
|
||||
# Serve config.json with proper settings
|
||||
# IMPORTANT: respond body must stay on a single line — see well-known note above.
|
||||
@cfg path /config.json
|
||||
handle @cfg {
|
||||
header Content-Type application/json
|
||||
header Cache-Control no-store
|
||||
respond `{"default_server_config":{"m.homeserver":{"base_url":"https://matrix.example.test","server_name":"matrix.example.test"}},"default_server_name":"matrix.example.test","disable_custom_urls":false,"disable_guests":true,"features":{"feature_oidc_aware_navigation":true,"feature_element_call_video_rooms":true},"element_call":{"url":"https://call.element.io","participant_limit":8,"brand":"Element Call"}}` 200
|
||||
}
|
||||
|
||||
# Everything else to Element container
|
||||
handle {
|
||||
reverse_proxy element:80
|
||||
}
|
||||
}
|
||||
|
||||
# =========================
|
||||
# Element Admin
|
||||
# =========================
|
||||
admin.example.test:443 {
|
||||
tls internal
|
||||
|
||||
handle {
|
||||
reverse_proxy element-admin:8080
|
||||
}
|
||||
}
|
||||
|
||||
# =========================
|
||||
# Element Call (LiveKit)
|
||||
# =========================
|
||||
rtc.example.test:443 {
|
||||
tls internal
|
||||
|
||||
handle_path /livekit/jwt* {
|
||||
reverse_proxy lk-jwt-service:8080
|
||||
}
|
||||
|
||||
handle_path /livekit/sfu* {
|
||||
reverse_proxy livekit:7880
|
||||
}
|
||||
}
|
||||
|
||||
# =========================
|
||||
# Element Call Frontend
|
||||
# =========================
|
||||
call.example.test:443 {
|
||||
tls internal
|
||||
|
||||
reverse_proxy element-call:8080
|
||||
}
|
||||
@@ -1,267 +0,0 @@
|
||||
# Production Caddyfile for Matrix Stack
|
||||
# Uses Let's Encrypt for automatic HTTPS certificates
|
||||
# IMPORTANT: Replace {$DOMAIN} with your actual domain during deployment
|
||||
{
|
||||
# Production: automatic HTTPS with Let's Encrypt
|
||||
# local_certs is NOT used in production
|
||||
|
||||
# Enable admin API for troubleshooting
|
||||
admin 0.0.0.0:2019
|
||||
|
||||
# Email for Let's Encrypt notifications (set via environment)
|
||||
email {$ACME_EMAIL}
|
||||
}
|
||||
|
||||
# =========================
|
||||
# Matrix Homeserver (Synapse)
|
||||
# =========================
|
||||
matrix.{$DOMAIN}:443 {
|
||||
# Automatic HTTPS with Let's Encrypt
|
||||
# No tls directive needed - Caddy handles it automatically
|
||||
|
||||
# Well-known client endpoint
|
||||
@wk path /.well-known/matrix/client
|
||||
handle @wk {
|
||||
header Content-Type application/json
|
||||
respond `{"m.homeserver":{"base_url":"https://matrix.{$DOMAIN}"},"m.authentication":{"issuer":"https://auth.{$DOMAIN}/"}}` 200
|
||||
}
|
||||
|
||||
# Well-known server endpoint (federation)
|
||||
@wk_server path /.well-known/matrix/server
|
||||
handle @wk_server {
|
||||
header Content-Type application/json
|
||||
respond `{"m.server":"matrix.{$DOMAIN}:443"}` 200
|
||||
}
|
||||
|
||||
# Client versions endpoint with CORS - strip backend headers to prevent duplicates
|
||||
@versions path /_matrix/client/versions
|
||||
handle @versions {
|
||||
header Access-Control-Allow-Origin "*"
|
||||
header Access-Control-Allow-Methods "GET, OPTIONS"
|
||||
header Access-Control-Allow-Headers "Authorization, Content-Type, Accept"
|
||||
header Vary "Origin, Access-Control-Request-Method, Access-Control-Request-Headers"
|
||||
reverse_proxy synapse:8008 {
|
||||
header_down -Access-Control-Allow-Origin
|
||||
header_down -Access-Control-Allow-Methods
|
||||
header_down -Access-Control-Allow-Headers
|
||||
header_down -Vary
|
||||
}
|
||||
}
|
||||
|
||||
# CORS preflight for auth metadata
|
||||
@auth_preflight {
|
||||
method OPTIONS
|
||||
path /_matrix/client/unstable/org.matrix.msc2965/auth_metadata
|
||||
}
|
||||
handle @auth_preflight {
|
||||
header Access-Control-Allow-Origin "*"
|
||||
header Access-Control-Allow-Methods "GET, OPTIONS"
|
||||
header Access-Control-Allow-Headers "Authorization, Content-Type, Accept"
|
||||
header Access-Control-Max-Age "86400"
|
||||
respond 204
|
||||
}
|
||||
|
||||
# CORS preflight for all Matrix API
|
||||
@preflight {
|
||||
method OPTIONS
|
||||
path_regexp matrix ^/_matrix/.*$
|
||||
}
|
||||
handle @preflight {
|
||||
header Access-Control-Allow-Origin "*"
|
||||
header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
|
||||
header Access-Control-Allow-Headers "Authorization, Content-Type, Accept"
|
||||
header Access-Control-Max-Age "86400"
|
||||
respond 204
|
||||
}
|
||||
|
||||
# MAS compat endpoints (login/logout/refresh) with CORS
|
||||
@compat path \
|
||||
/_matrix/client/v3/login* \
|
||||
/_matrix/client/v3/logout* \
|
||||
/_matrix/client/v3/refresh* \
|
||||
/_matrix/client/r0/login* \
|
||||
/_matrix/client/r0/logout* \
|
||||
/_matrix/client/r0/refresh*
|
||||
handle @compat {
|
||||
header Access-Control-Allow-Origin "*"
|
||||
header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
|
||||
header Access-Control-Allow-Headers "Authorization, Content-Type, Accept"
|
||||
header Vary "Origin, Access-Control-Request-Method, Access-Control-Request-Headers"
|
||||
reverse_proxy mas:8080 {
|
||||
header_down -Access-Control-Allow-Origin
|
||||
header_down -Access-Control-Allow-Methods
|
||||
header_down -Access-Control-Allow-Headers
|
||||
header_down -Vary
|
||||
}
|
||||
}
|
||||
|
||||
# Everything else under /_matrix → Synapse with CORS
|
||||
@matrix_rest path_regexp matrix ^/_matrix/.*$
|
||||
handle @matrix_rest {
|
||||
header Access-Control-Allow-Origin "*"
|
||||
header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
|
||||
header Access-Control-Allow-Headers "Authorization, Content-Type, Accept"
|
||||
header Vary "Origin, Access-Control-Request-Method, Access-Control-Request-Headers"
|
||||
reverse_proxy synapse:8008 {
|
||||
header_down -Access-Control-Allow-Origin
|
||||
header_down -Access-Control-Allow-Methods
|
||||
header_down -Access-Control-Allow-Headers
|
||||
header_down -Vary
|
||||
}
|
||||
}
|
||||
|
||||
# Federation endpoint (port 8448)
|
||||
# In production, this should be accessible on port 8448 OR via .well-known
|
||||
handle /_matrix/federation/* {
|
||||
reverse_proxy synapse:8008
|
||||
}
|
||||
|
||||
# Default: everything else → Synapse
|
||||
handle {
|
||||
reverse_proxy synapse:8008
|
||||
}
|
||||
}
|
||||
|
||||
# =========================
|
||||
# Matrix Authentication Service (MAS)
|
||||
# =========================
|
||||
auth.{$DOMAIN}:443 {
|
||||
# Automatic HTTPS with Let's Encrypt
|
||||
|
||||
# OIDC Discovery
|
||||
@disco path /.well-known/openid-configuration
|
||||
handle @disco {
|
||||
header ?Access-Control-Allow-Origin "*"
|
||||
header ?Access-Control-Allow-Methods "GET, OPTIONS"
|
||||
header ?Access-Control-Allow-Headers "*"
|
||||
reverse_proxy mas:8080
|
||||
}
|
||||
|
||||
# Dynamic Client Registration: CORS preflight
|
||||
@reg_opts {
|
||||
method OPTIONS
|
||||
path /oauth2/registration
|
||||
}
|
||||
handle @reg_opts {
|
||||
header ?Access-Control-Allow-Origin "*"
|
||||
header ?Access-Control-Allow-Methods "POST, OPTIONS"
|
||||
header ?Access-Control-Allow-Headers "*"
|
||||
respond 204
|
||||
}
|
||||
|
||||
# Dynamic Client Registration (POST)
|
||||
@reg path /oauth2/registration
|
||||
route @reg {
|
||||
header ?Access-Control-Allow-Origin "*"
|
||||
header ?Access-Control-Allow-Methods "POST, OPTIONS"
|
||||
header ?Access-Control-Allow-Headers "*"
|
||||
reverse_proxy mas:8080
|
||||
}
|
||||
|
||||
# JWKS preflight
|
||||
@jwks_opts {
|
||||
method OPTIONS
|
||||
path /oauth2/keys.json
|
||||
}
|
||||
handle @jwks_opts {
|
||||
header ?Access-Control-Allow-Origin "*"
|
||||
header ?Access-Control-Allow-Methods "GET, OPTIONS"
|
||||
header ?Access-Control-Allow-Headers "*"
|
||||
respond 204
|
||||
}
|
||||
|
||||
# Map keys.json → /oauth2/jwks (MAS naming)
|
||||
@jwksjson path /oauth2/keys.json
|
||||
route @jwksjson {
|
||||
header ?Access-Control-Allow-Origin "*"
|
||||
header ?Access-Control-Allow-Methods "GET, OPTIONS"
|
||||
header ?Access-Control-Allow-Headers "*"
|
||||
uri replace /oauth2/keys.json /oauth2/jwks
|
||||
reverse_proxy mas:8080
|
||||
}
|
||||
|
||||
# Generic OAuth2 endpoints
|
||||
@oauth path /oauth2/*
|
||||
route @oauth {
|
||||
header ?Access-Control-Allow-Origin "*"
|
||||
header ?Access-Control-Allow-Methods "GET, OPTIONS, POST"
|
||||
header ?Access-Control-Allow-Headers "*"
|
||||
reverse_proxy mas:8080
|
||||
}
|
||||
|
||||
# Account portal
|
||||
handle_path /account/* {
|
||||
reverse_proxy mas:8080
|
||||
}
|
||||
|
||||
# Authelia endpoints (proxy to authelia if using Authelia profile)
|
||||
handle_path /authelia/* {
|
||||
reverse_proxy authelia:9091
|
||||
}
|
||||
|
||||
# Fallback: everything else to MAS
|
||||
handle {
|
||||
reverse_proxy mas:8080
|
||||
}
|
||||
|
||||
# Add CORS on error responses
|
||||
handle_errors {
|
||||
header ?Access-Control-Allow-Origin "*"
|
||||
header ?Access-Control-Allow-Headers "*"
|
||||
header ?Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
|
||||
}
|
||||
}
|
||||
|
||||
# =========================
|
||||
# Authelia SSO (Optional)
|
||||
# =========================
|
||||
authelia.{$DOMAIN}:443 {
|
||||
# Automatic HTTPS with Let's Encrypt
|
||||
reverse_proxy authelia:9091
|
||||
}
|
||||
|
||||
# =========================
|
||||
# Element Web Client
|
||||
# =========================
|
||||
element.{$DOMAIN}:443 {
|
||||
# Automatic HTTPS with Let's Encrypt
|
||||
|
||||
# Serve config.json with proper settings
|
||||
@cfg path /config.json
|
||||
handle @cfg {
|
||||
header Content-Type application/json
|
||||
header Cache-Control no-store
|
||||
respond `{
|
||||
"default_server_config": {
|
||||
"m.homeserver": {
|
||||
"base_url": "https://matrix.{$DOMAIN}",
|
||||
"server_name": "matrix.{$DOMAIN}"
|
||||
}
|
||||
},
|
||||
"default_server_name": "matrix.{$DOMAIN}",
|
||||
"disable_custom_urls": false,
|
||||
"disable_guests": true,
|
||||
"features": {
|
||||
"feature_oidc_aware_navigation": true
|
||||
}
|
||||
}` 200
|
||||
}
|
||||
|
||||
# Everything else to Element container
|
||||
handle {
|
||||
reverse_proxy element:80
|
||||
}
|
||||
}
|
||||
|
||||
# =========================
|
||||
# Element Admin
|
||||
# =========================
|
||||
admin.{$DOMAIN}:443 {
|
||||
# Automatic HTTPS with Let's Encrypt
|
||||
|
||||
# Proxy to Element Admin container
|
||||
handle {
|
||||
reverse_proxy element-admin:80
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
# PostgreSQL Database
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
image: ${POSTGRES_IMAGE:-postgres:16-alpine}
|
||||
container_name: matrix-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@@ -22,7 +22,7 @@ services:
|
||||
|
||||
# Matrix Synapse Server
|
||||
synapse:
|
||||
image: matrixdotorg/synapse:latest
|
||||
image: ${SYNAPSE_IMAGE:-matrixdotorg/synapse:latest}
|
||||
container_name: matrix-synapse
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@@ -52,7 +52,7 @@ services:
|
||||
|
||||
# Element Web Client
|
||||
element:
|
||||
image: vectorim/element-web:latest
|
||||
image: ${ELEMENT_IMAGE:-vectorim/element-web:latest}
|
||||
container_name: matrix-element
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
@@ -69,7 +69,7 @@ services:
|
||||
livekit:
|
||||
profiles:
|
||||
- element-call
|
||||
image: livekit/livekit-server:latest
|
||||
image: ${LIVEKIT_IMAGE:-livekit/livekit-server:latest}
|
||||
container_name: matrix-livekit
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
@@ -87,7 +87,7 @@ services:
|
||||
lk-jwt-service:
|
||||
profiles:
|
||||
- element-call
|
||||
image: ghcr.io/element-hq/lk-jwt-service:latest
|
||||
image: ${LK_JWT_IMAGE:-ghcr.io/element-hq/lk-jwt-service:latest}
|
||||
container_name: matrix-lk-jwt
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
@@ -106,7 +106,7 @@ services:
|
||||
redis:
|
||||
profiles:
|
||||
- authelia # Only started when Authelia profile is active
|
||||
image: redis:7-alpine
|
||||
image: ${REDIS_IMAGE:-redis:7-alpine}
|
||||
container_name: matrix-redis
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
@@ -121,7 +121,7 @@ services:
|
||||
authelia:
|
||||
profiles:
|
||||
- authelia # Only started when Authelia profile is active
|
||||
image: authelia/authelia:latest
|
||||
image: ${AUTHELIA_IMAGE:-authelia/authelia:latest}
|
||||
container_name: matrix-authelia
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@@ -145,7 +145,7 @@ services:
|
||||
|
||||
# Matrix Authentication Service (MAS)
|
||||
mas:
|
||||
image: ghcr.io/element-hq/matrix-authentication-service:latest
|
||||
image: ${MAS_IMAGE:-ghcr.io/element-hq/matrix-authentication-service:latest}
|
||||
container_name: matrix-mas
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@@ -176,7 +176,7 @@ services:
|
||||
|
||||
# Caddy Reverse Proxy (HTTPS termination)
|
||||
caddy:
|
||||
image: caddy:2-alpine
|
||||
image: ${CADDY_IMAGE:-caddy:2-alpine}
|
||||
container_name: matrix-caddy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
@@ -197,7 +197,7 @@ services:
|
||||
|
||||
# mautrix-telegram Bridge
|
||||
mautrix-telegram:
|
||||
image: dock.mau.dev/mautrix/telegram:latest
|
||||
image: ${TELEGRAM_IMAGE:-dock.mau.dev/mautrix/telegram:latest}
|
||||
container_name: matrix-bridge-telegram
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
@@ -210,7 +210,7 @@ services:
|
||||
|
||||
# mautrix-whatsapp Bridge
|
||||
mautrix-whatsapp:
|
||||
image: dock.mau.dev/mautrix/whatsapp:latest
|
||||
image: ${WHATSAPP_IMAGE:-dock.mau.dev/mautrix/whatsapp:latest}
|
||||
container_name: matrix-bridge-whatsapp
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
@@ -223,7 +223,7 @@ services:
|
||||
|
||||
# mautrix-signal Bridge
|
||||
mautrix-signal:
|
||||
image: dock.mau.dev/mautrix/signal:latest
|
||||
image: ${SIGNAL_IMAGE:-dock.mau.dev/mautrix/signal:latest}
|
||||
container_name: matrix-bridge-signal
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
|
||||
@@ -180,6 +180,65 @@ else
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ============================================================================
|
||||
# DOCKER REGISTRY AND HARDENED IMAGES
|
||||
# ============================================================================
|
||||
echo -e "${CYAN}Docker Image Configuration:${NC}"
|
||||
echo ""
|
||||
read -p "Custom Docker registry prefix (leave blank for default): " DOCKER_REGISTRY_INPUT
|
||||
DOCKER_REGISTRY="${DOCKER_REGISTRY_INPUT%/}" # strip trailing slash
|
||||
[ -n "$DOCKER_REGISTRY" ] && DOCKER_REGISTRY="${DOCKER_REGISTRY}/"
|
||||
|
||||
if [ -n "$DOCKER_REGISTRY" ]; then
|
||||
echo -e "${GREEN}✓${NC} Custom registry: ${DOCKER_REGISTRY}"
|
||||
else
|
||||
echo -e "${GREEN}✓${NC} Using default registries"
|
||||
fi
|
||||
|
||||
USE_HARDENED_IMAGES=false
|
||||
read -p "Use hardened images from dhi.io for Redis/PostgreSQL/Caddy? [y/N]: " yn
|
||||
[[ "$yn" =~ ^[Yy] ]] && USE_HARDENED_IMAGES=true
|
||||
if [ "$USE_HARDENED_IMAGES" = true ]; then
|
||||
echo -e "${GREEN}✓${NC} Hardened images (dhi.io) enabled for Redis/PostgreSQL/Caddy"
|
||||
else
|
||||
echo -e "${GREEN}✓${NC} Using standard images"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Build image reference helper
|
||||
build_image() {
|
||||
local image="$1"
|
||||
if [ -n "$DOCKER_REGISTRY" ]; then
|
||||
echo "${DOCKER_REGISTRY}${image}"
|
||||
else
|
||||
echo "${image}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Standard images (respect custom registry)
|
||||
POSTGRES_IMAGE=$(build_image "postgres:16-alpine")
|
||||
SYNAPSE_IMAGE=$(build_image "matrixdotorg/synapse:latest")
|
||||
ELEMENT_IMAGE=$(build_image "vectorim/element-web:latest")
|
||||
ELEMENT_ADMIN_IMAGE=$(build_image "oci.element.io/element-admin:latest")
|
||||
MAS_IMAGE=$(build_image "ghcr.io/element-hq/matrix-authentication-service:latest")
|
||||
TELEGRAM_IMAGE=$(build_image "dock.mau.dev/mautrix/telegram:latest")
|
||||
WHATSAPP_IMAGE=$(build_image "dock.mau.dev/mautrix/whatsapp:latest")
|
||||
SIGNAL_IMAGE=$(build_image "dock.mau.dev/mautrix/signal:latest")
|
||||
LIVEKIT_IMAGE=$(build_image "livekit/livekit-server:latest")
|
||||
LK_JWT_IMAGE=$(build_image "ghcr.io/element-hq/lk-jwt-service:latest")
|
||||
ELEMENT_CALL_IMAGE=$(build_image "ghcr.io/element-hq/element-call:latest")
|
||||
AUTHELIA_IMAGE=$(build_image "authelia/authelia:latest")
|
||||
|
||||
# Hardened images take priority for redis/postgres/caddy
|
||||
if [ "$USE_HARDENED_IMAGES" = true ]; then
|
||||
REDIS_IMAGE="dhi.io/redis:7"
|
||||
POSTGRES_IMAGE="dhi.io/postgres:16"
|
||||
CADDY_IMAGE="dhi.io/caddy:2"
|
||||
else
|
||||
REDIS_IMAGE=$(build_image "redis:7-alpine")
|
||||
CADDY_IMAGE=$(build_image "caddy:2-alpine")
|
||||
fi
|
||||
|
||||
# Function to generate secure random string (32 bytes base64)
|
||||
generate_secret() {
|
||||
openssl rand -base64 32 | tr -d "=+/" | cut -c1-32
|
||||
@@ -222,6 +281,18 @@ if [[ "$DEPLOYMENT_MODE" == "local" ]]; then
|
||||
RTC_DOMAIN="rtc.example.test"
|
||||
CALL_DOMAIN="call.example.test"
|
||||
|
||||
# Matrix server name (MXID identity domain)
|
||||
echo ""
|
||||
echo -e "${CYAN}Matrix User ID format:${NC}"
|
||||
echo -e " [1] Short: @user:${DOMAIN_BASE} ← recommended"
|
||||
echo -e " [2] Subdomain: @user:${MATRIX_DOMAIN}"
|
||||
read -p "Choose [1/2, default: 1]: " _sn_choice
|
||||
if [[ "$_sn_choice" == "2" ]]; then
|
||||
SERVER_NAME="${MATRIX_DOMAIN}"
|
||||
else
|
||||
SERVER_NAME="${DOMAIN_BASE}"
|
||||
fi
|
||||
|
||||
echo -e "${CYAN}Local Testing Configuration:${NC}"
|
||||
echo -e " Matrix API: https://${MATRIX_DOMAIN}"
|
||||
echo -e " Element Web: https://${ELEMENT_DOMAIN}"
|
||||
@@ -233,6 +304,9 @@ if [[ "$DEPLOYMENT_MODE" == "local" ]]; then
|
||||
fi
|
||||
echo ""
|
||||
HOSTS_DOMAINS="${MATRIX_DOMAIN} ${ELEMENT_DOMAIN} ${AUTH_DOMAIN} ${AUTHELIA_DOMAIN}"
|
||||
if [[ "$SERVER_NAME" != "$MATRIX_DOMAIN" ]]; then
|
||||
HOSTS_DOMAINS="${SERVER_NAME} ${HOSTS_DOMAINS}"
|
||||
fi
|
||||
if [[ "$USE_ELEMENT_CALL" == true ]]; then
|
||||
HOSTS_DOMAINS="${HOSTS_DOMAINS} ${RTC_DOMAIN} ${CALL_DOMAIN}"
|
||||
fi
|
||||
@@ -289,6 +363,18 @@ else
|
||||
CALL_DOMAIN="${CALL_SUBDOMAIN}.${DOMAIN_BASE}"
|
||||
fi
|
||||
|
||||
# Matrix server name (MXID identity domain)
|
||||
echo ""
|
||||
echo -e "${CYAN}Matrix User ID format:${NC}"
|
||||
echo -e " [1] Short: @user:${DOMAIN_BASE} ← recommended"
|
||||
echo -e " [2] Subdomain: @user:${MATRIX_DOMAIN}"
|
||||
read -p "Choose [1/2, default: 1]: " _sn_choice
|
||||
if [[ "$_sn_choice" == "2" ]]; then
|
||||
SERVER_NAME="${MATRIX_DOMAIN}"
|
||||
else
|
||||
SERVER_NAME="${DOMAIN_BASE}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}Backend Server Addresses (for Caddyfile):${NC}"
|
||||
echo -e " ${YELLOW}Enter IP addresses or hostnames${NC}"
|
||||
@@ -309,6 +395,7 @@ else
|
||||
echo ""
|
||||
echo -e "${GREEN}✓${NC} Configuration Summary:"
|
||||
echo -e " Base Domain: ${DOMAIN_BASE}"
|
||||
echo -e " Server Name: ${SERVER_NAME} (@user:${SERVER_NAME})"
|
||||
echo -e " Matrix: https://${MATRIX_DOMAIN}"
|
||||
echo -e " Element: https://${ELEMENT_DOMAIN}"
|
||||
echo -e " MAS: https://${AUTH_DOMAIN}"
|
||||
@@ -346,6 +433,7 @@ mkdir -p synapse/data
|
||||
mkdir -p postgres/data
|
||||
mkdir -p caddy/data caddy/config
|
||||
mkdir -p bridges/{telegram,whatsapp,signal}/config
|
||||
mkdir -p appservices
|
||||
print_status "Directory structure created"
|
||||
echo ""
|
||||
|
||||
@@ -362,6 +450,8 @@ AUTHELIA_SESSION_SECRET=$(generate_secret)
|
||||
AUTHELIA_STORAGE_ENCRYPTION_KEY=$(generate_secret)
|
||||
MAS_SECRET_KEY=$(generate_hex_secret) # MAS requires hex format
|
||||
SYNAPSE_SHARED_SECRET=$(generate_secret)
|
||||
DOUBLEPUPPET_AS_TOKEN=$(generate_hex_secret)
|
||||
DOUBLEPUPPET_HS_TOKEN=$(generate_hex_secret)
|
||||
if [[ "$USE_ELEMENT_CALL" == true ]]; then
|
||||
LIVEKIT_SECRET=$(generate_secret)
|
||||
fi
|
||||
@@ -382,7 +472,7 @@ ELEMENT_DOMAIN=${ELEMENT_DOMAIN}
|
||||
ADMIN_DOMAIN=${ADMIN_DOMAIN}
|
||||
AUTH_DOMAIN=${AUTH_DOMAIN}
|
||||
AUTHELIA_DOMAIN=${AUTHELIA_DOMAIN}
|
||||
SERVER_NAME=${MATRIX_DOMAIN}
|
||||
SERVER_NAME=${SERVER_NAME}
|
||||
|
||||
# PostgreSQL
|
||||
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
@@ -403,6 +493,22 @@ MAS_SECRET_KEY=${MAS_SECRET_KEY}
|
||||
|
||||
# Timezone
|
||||
TZ=${TZ:-Europe/Berlin}
|
||||
|
||||
# Docker images
|
||||
POSTGRES_IMAGE=${POSTGRES_IMAGE}
|
||||
SYNAPSE_IMAGE=${SYNAPSE_IMAGE}
|
||||
ELEMENT_IMAGE=${ELEMENT_IMAGE}
|
||||
ELEMENT_ADMIN_IMAGE=${ELEMENT_ADMIN_IMAGE}
|
||||
REDIS_IMAGE=${REDIS_IMAGE}
|
||||
MAS_IMAGE=${MAS_IMAGE}
|
||||
TELEGRAM_IMAGE=${TELEGRAM_IMAGE}
|
||||
WHATSAPP_IMAGE=${WHATSAPP_IMAGE}
|
||||
SIGNAL_IMAGE=${SIGNAL_IMAGE}
|
||||
LIVEKIT_IMAGE=${LIVEKIT_IMAGE}
|
||||
LK_JWT_IMAGE=${LK_JWT_IMAGE}
|
||||
ELEMENT_CALL_IMAGE=${ELEMENT_CALL_IMAGE}
|
||||
AUTHELIA_IMAGE=${AUTHELIA_IMAGE}
|
||||
CADDY_IMAGE=${CADDY_IMAGE}
|
||||
EOF
|
||||
|
||||
# Add production-specific variables
|
||||
@@ -628,6 +734,7 @@ http:
|
||||
- name: graphql
|
||||
playground: true
|
||||
- name: assets # Required for CSS/JS files
|
||||
- name: adminapi
|
||||
binds:
|
||||
- address: '[::]:8080'
|
||||
- name: internal
|
||||
@@ -690,7 +797,7 @@ upstream_oauth2:
|
||||
set_email_verification: always
|
||||
|
||||
matrix:
|
||||
homeserver: '${MATRIX_DOMAIN}'
|
||||
homeserver: '${SERVER_NAME}'
|
||||
endpoint: 'http://synapse:8008'
|
||||
secret: '${SYNAPSE_SHARED_SECRET}'
|
||||
|
||||
@@ -702,7 +809,7 @@ else
|
||||
cat >> mas/config/config.yaml << EOF
|
||||
|
||||
matrix:
|
||||
homeserver: '${MATRIX_DOMAIN}'
|
||||
homeserver: '${SERVER_NAME}'
|
||||
endpoint: 'http://synapse:8008'
|
||||
secret: '${SYNAPSE_SHARED_SECRET}'
|
||||
|
||||
@@ -784,7 +891,7 @@ cat > element/config/config.json << EOF
|
||||
"default_server_config": {
|
||||
"m.homeserver": {
|
||||
"base_url": "https://${MATRIX_DOMAIN}",
|
||||
"server_name": "${MATRIX_DOMAIN}"
|
||||
"server_name": "${SERVER_NAME}"
|
||||
}
|
||||
},
|
||||
"brand": "Element",
|
||||
@@ -805,7 +912,7 @@ cat > element/config/config.json << EOF
|
||||
"features": {
|
||||
"feature_oidc_aware_navigation": true${ELEMENT_CALL_FEATURES}
|
||||
},
|
||||
"default_server_name": "${MATRIX_DOMAIN}",
|
||||
"default_server_name": "${SERVER_NAME}",
|
||||
"disable_custom_urls": false,
|
||||
"disable_guests": true${ELEMENT_CALL_BLOCK}
|
||||
}
|
||||
@@ -838,11 +945,13 @@ echo -e "${BLUE}[12/13] Generating Synapse configuration...${NC}"
|
||||
# Generate homeserver.yaml if it doesn't exist
|
||||
if [ ! -f "synapse/data/homeserver.yaml" ]; then
|
||||
print_info "Generating new homeserver.yaml..."
|
||||
$DOCKER_CMD run -it --rm \
|
||||
$DOCKER_CMD run --rm \
|
||||
-v $(pwd)/synapse/data:/data \
|
||||
-e SYNAPSE_SERVER_NAME=${MATRIX_DOMAIN} \
|
||||
-e SYNAPSE_SERVER_NAME=${SERVER_NAME} \
|
||||
-e SYNAPSE_REPORT_STATS=no \
|
||||
matrixdotorg/synapse:latest generate
|
||||
# Fix ownership: synapse runs as uid 991 inside Docker; reclaim files for current user
|
||||
sudo chown -R "$(id -u):$(id -g)" synapse/data/
|
||||
print_status "Synapse configuration generated"
|
||||
else
|
||||
print_info "Existing homeserver.yaml found - preserving custom configurations"
|
||||
@@ -871,6 +980,14 @@ sed -i '/^# Experimental features$/d' synapse/data/homeserver.yaml
|
||||
sed -i '/^# Enable registration/d' synapse/data/homeserver.yaml
|
||||
sed -i '/^enable_registration:/d' synapse/data/homeserver.yaml
|
||||
|
||||
# Remove old public room settings if present (re-added explicitly below)
|
||||
sed -i '/^allow_public_rooms_without_auth:/d' synapse/data/homeserver.yaml
|
||||
sed -i '/^allow_public_rooms_over_federation:/d' synapse/data/homeserver.yaml
|
||||
|
||||
# Remove old double-puppeting appservice registration if present (prevents duplication on re-run)
|
||||
sed -i '/^# Double-puppeting appservice/d' synapse/data/homeserver.yaml
|
||||
sed -i '/^app_service_config_files:/,/^[^ ]/{ /^app_service_config_files:/d; /^[^ ]/!d }' synapse/data/homeserver.yaml
|
||||
|
||||
# Remove old Element Call rate limit config if present (prevents duplication on re-run)
|
||||
sed -i '/^# Element Call: delayed event rate limiting/d' synapse/data/homeserver.yaml
|
||||
sed -i '/^max_event_delay_duration:/d' synapse/data/homeserver.yaml
|
||||
@@ -893,6 +1010,9 @@ database:
|
||||
|
||||
# Enable registration (disabled when using MAS/OAuth delegation)
|
||||
enable_registration: false
|
||||
allow_guest_access: false
|
||||
allow_public_rooms_without_auth: false
|
||||
allow_public_rooms_over_federation: false
|
||||
|
||||
# MAS Integration (Synapse 1.136+ stable config — replaces deprecated experimental_features.msc3861)
|
||||
matrix_authentication_service:
|
||||
@@ -920,6 +1040,30 @@ rc_delayed_event_mgmt:
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Generate double-puppeting appservice registration
|
||||
cat > appservices/doublepuppet.yaml << EOF
|
||||
id: doublepuppet
|
||||
url: null
|
||||
as_token: "${DOUBLEPUPPET_AS_TOKEN}"
|
||||
hs_token: "${DOUBLEPUPPET_HS_TOKEN}"
|
||||
sender_localpart: doublepuppet
|
||||
rate_limited: false
|
||||
|
||||
namespaces:
|
||||
users:
|
||||
- regex: "@.*:${SERVER_NAME}"
|
||||
exclusive: false
|
||||
EOF
|
||||
|
||||
cat >> synapse/data/homeserver.yaml << EOF
|
||||
|
||||
# Double-puppeting appservice
|
||||
app_service_config_files:
|
||||
- /appservices/doublepuppet.yaml
|
||||
EOF
|
||||
|
||||
# Restore ownership to Synapse uid so the container can read/write its own data
|
||||
sudo chown -R 991:991 synapse/data/
|
||||
print_status "Database configuration updated with current credentials"
|
||||
echo ""
|
||||
|
||||
@@ -930,10 +1074,10 @@ if [[ "$DEPLOYMENT_MODE" == "local" ]]; then
|
||||
# Pre-build JSON blobs for the local Caddyfile (single-line, no literal \n)
|
||||
if [[ "$USE_ELEMENT_CALL" == true ]]; then
|
||||
LOCAL_WELLKNOWN_JSON="{\"m.homeserver\":{\"base_url\":\"https://${MATRIX_DOMAIN}\"},\"m.authentication\":{\"issuer\":\"https://${AUTH_DOMAIN}/\"},\"org.matrix.msc4143.rtc_foci\":[{\"type\":\"livekit\",\"livekit_service_url\":\"https://${RTC_DOMAIN}/livekit/jwt\"}]}"
|
||||
LOCAL_ELEMENT_CFG_JSON="{\"default_server_config\":{\"m.homeserver\":{\"base_url\":\"https://${MATRIX_DOMAIN}\",\"server_name\":\"${MATRIX_DOMAIN}\"}},\"default_server_name\":\"${MATRIX_DOMAIN}\",\"disable_custom_urls\":false,\"disable_guests\":true,\"features\":{\"feature_oidc_aware_navigation\":true,\"feature_element_call_video_rooms\":true},\"element_call\":{\"url\":\"https://${CALL_DOMAIN}\",\"participant_limit\":8,\"brand\":\"Element Call\"}}"
|
||||
LOCAL_ELEMENT_CFG_JSON="{\"default_server_config\":{\"m.homeserver\":{\"base_url\":\"https://${MATRIX_DOMAIN}\",\"server_name\":\"${SERVER_NAME}\"}},\"default_server_name\":\"${SERVER_NAME}\",\"disable_custom_urls\":false,\"disable_guests\":true,\"features\":{\"feature_oidc_aware_navigation\":true,\"feature_element_call_video_rooms\":true},\"element_call\":{\"url\":\"https://${CALL_DOMAIN}\",\"participant_limit\":8,\"brand\":\"Element Call\"}}"
|
||||
else
|
||||
LOCAL_WELLKNOWN_JSON="{\"m.homeserver\":{\"base_url\":\"https://${MATRIX_DOMAIN}\"},\"m.authentication\":{\"issuer\":\"https://${AUTH_DOMAIN}/\"}}"
|
||||
LOCAL_ELEMENT_CFG_JSON="{\"default_server_config\":{\"m.homeserver\":{\"base_url\":\"https://${MATRIX_DOMAIN}\",\"server_name\":\"${MATRIX_DOMAIN}\"}},\"default_server_name\":\"${MATRIX_DOMAIN}\",\"disable_custom_urls\":false,\"disable_guests\":true,\"features\":{\"feature_oidc_aware_navigation\":true}}"
|
||||
LOCAL_ELEMENT_CFG_JSON="{\"default_server_config\":{\"m.homeserver\":{\"base_url\":\"https://${MATRIX_DOMAIN}\",\"server_name\":\"${SERVER_NAME}\"}},\"default_server_name\":\"${SERVER_NAME}\",\"disable_custom_urls\":false,\"disable_guests\":true,\"features\":{\"feature_oidc_aware_navigation\":true}}"
|
||||
fi
|
||||
|
||||
cat > caddy/Caddyfile << 'CADDYEOF'
|
||||
@@ -944,8 +1088,8 @@ if [[ "$DEPLOYMENT_MODE" == "local" ]]; then
|
||||
{
|
||||
# Use local CA for self-signed certificates
|
||||
local_certs
|
||||
# Enable admin API
|
||||
admin 0.0.0.0:2019
|
||||
# Enable admin API (localhost only)
|
||||
admin localhost:2019
|
||||
}
|
||||
CADDYEOF
|
||||
|
||||
@@ -1046,6 +1190,8 @@ ${MATRIX_DOMAIN}:443 {
|
||||
header Access-Control-Allow-Headers "Authorization, Content-Type, Accept"
|
||||
header Vary "Origin, Access-Control-Request-Method, Access-Control-Request-Headers"
|
||||
reverse_proxy mas:8080 {
|
||||
header_up Host {http.request.host}
|
||||
header_up X-Forwarded-Host {http.request.host}
|
||||
header_down -Access-Control-Allow-Origin
|
||||
header_down -Access-Control-Allow-Methods
|
||||
header_down -Access-Control-Allow-Headers
|
||||
@@ -1068,6 +1214,11 @@ ${MATRIX_DOMAIN}:443 {
|
||||
}
|
||||
}
|
||||
|
||||
# Block public access to Synapse admin API
|
||||
handle /_synapse/admin* {
|
||||
respond "Forbidden" 403
|
||||
}
|
||||
|
||||
# Default: everything else → Synapse
|
||||
handle {
|
||||
reverse_proxy synapse:8008
|
||||
@@ -1086,7 +1237,10 @@ ${AUTH_DOMAIN}:443 {
|
||||
header ?Access-Control-Allow-Origin "*"
|
||||
header ?Access-Control-Allow-Methods "GET, OPTIONS"
|
||||
header ?Access-Control-Allow-Headers "*"
|
||||
reverse_proxy mas:8080
|
||||
reverse_proxy mas:8080 {
|
||||
header_up Host {http.request.host}
|
||||
header_up X-Forwarded-Host {http.request.host}
|
||||
}
|
||||
}
|
||||
|
||||
# Dynamic Client Registration: CORS preflight
|
||||
@@ -1107,7 +1261,10 @@ ${AUTH_DOMAIN}:443 {
|
||||
header ?Access-Control-Allow-Origin "*"
|
||||
header ?Access-Control-Allow-Methods "POST, OPTIONS"
|
||||
header ?Access-Control-Allow-Headers "*"
|
||||
reverse_proxy mas:8080
|
||||
reverse_proxy mas:8080 {
|
||||
header_up Host {http.request.host}
|
||||
header_up X-Forwarded-Host {http.request.host}
|
||||
}
|
||||
}
|
||||
|
||||
# JWKS preflight
|
||||
@@ -1129,7 +1286,10 @@ ${AUTH_DOMAIN}:443 {
|
||||
header ?Access-Control-Allow-Methods "GET, OPTIONS"
|
||||
header ?Access-Control-Allow-Headers "*"
|
||||
uri replace /oauth2/keys.json /oauth2/jwks
|
||||
reverse_proxy mas:8080
|
||||
reverse_proxy mas:8080 {
|
||||
header_up Host {http.request.host}
|
||||
header_up X-Forwarded-Host {http.request.host}
|
||||
}
|
||||
}
|
||||
|
||||
# Generic OAuth2 endpoints
|
||||
@@ -1138,12 +1298,18 @@ ${AUTH_DOMAIN}:443 {
|
||||
header ?Access-Control-Allow-Origin "*"
|
||||
header ?Access-Control-Allow-Methods "GET, OPTIONS, POST"
|
||||
header ?Access-Control-Allow-Headers "*"
|
||||
reverse_proxy mas:8080
|
||||
reverse_proxy mas:8080 {
|
||||
header_up Host {http.request.host}
|
||||
header_up X-Forwarded-Host {http.request.host}
|
||||
}
|
||||
}
|
||||
|
||||
# Account portal
|
||||
handle_path /account/* {
|
||||
reverse_proxy mas:8080
|
||||
# Account portal (handle, not handle_path — preserves /account/ prefix for MAS SPA routing)
|
||||
handle /account/* {
|
||||
reverse_proxy mas:8080 {
|
||||
header_up Host {http.request.host}
|
||||
header_up X-Forwarded-Host {http.request.host}
|
||||
}
|
||||
}
|
||||
|
||||
# Authelia endpoints (proxy to authelia)
|
||||
@@ -1153,7 +1319,10 @@ ${AUTH_DOMAIN}:443 {
|
||||
|
||||
# Fallback: everything else to MAS
|
||||
handle {
|
||||
reverse_proxy mas:8080
|
||||
reverse_proxy mas:8080 {
|
||||
header_up Host {http.request.host}
|
||||
header_up X-Forwarded-Host {http.request.host}
|
||||
}
|
||||
}
|
||||
|
||||
# Add CORS on error responses
|
||||
@@ -1205,6 +1374,32 @@ ${ADMIN_DOMAIN}:443 {
|
||||
}
|
||||
EOF
|
||||
|
||||
# Append identity domain well-known block if SERVER_NAME differs from MATRIX_DOMAIN
|
||||
if [[ "$SERVER_NAME" != "$MATRIX_DOMAIN" ]]; then
|
||||
cat >> caddy/Caddyfile << EOF
|
||||
|
||||
# =========================
|
||||
# Identity Domain (well-known delegation)
|
||||
# =========================
|
||||
${SERVER_NAME}:443 {
|
||||
tls internal
|
||||
|
||||
@wk path /.well-known/matrix/client
|
||||
handle @wk {
|
||||
header Content-Type application/json
|
||||
header Access-Control-Allow-Origin "*"
|
||||
respond \`${LOCAL_WELLKNOWN_JSON}\` 200
|
||||
}
|
||||
|
||||
@wk_server path /.well-known/matrix/server
|
||||
handle @wk_server {
|
||||
header Content-Type application/json
|
||||
respond \`{"m.server":"${MATRIX_DOMAIN}:443"}\` 200
|
||||
}
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Append Element Call blocks if enabled
|
||||
if [[ "$USE_ELEMENT_CALL" == true ]]; then
|
||||
cat >> caddy/Caddyfile << EOF
|
||||
@@ -1247,8 +1442,30 @@ chmod 755 authelia/config mas/config element/config 2>/dev/null || true
|
||||
print_status "Permissions fixed"
|
||||
echo ""
|
||||
|
||||
# Image summary
|
||||
echo -e "${CYAN}Docker images configured:${NC}"
|
||||
echo -e " Synapse: $SYNAPSE_IMAGE"
|
||||
echo -e " Postgres: $POSTGRES_IMAGE"
|
||||
echo -e " Redis: $REDIS_IMAGE"
|
||||
echo -e " MAS: $MAS_IMAGE"
|
||||
echo -e " Element: $ELEMENT_IMAGE"
|
||||
echo -e " Element Admin: $ELEMENT_ADMIN_IMAGE"
|
||||
echo -e " Authelia: $AUTHELIA_IMAGE"
|
||||
echo -e " Caddy: $CADDY_IMAGE"
|
||||
if [[ "$USE_ELEMENT_CALL" == true ]]; then
|
||||
echo -e " LiveKit: $LIVEKIT_IMAGE"
|
||||
echo -e " LK JWT: $LK_JWT_IMAGE"
|
||||
echo -e " Element Call: $ELEMENT_CALL_IMAGE"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Step 14: Start the stack
|
||||
echo -e "${BLUE}[14/14] Starting the Matrix stack...${NC}"
|
||||
|
||||
if [[ "${SKIP_START:-false}" == "true" ]]; then
|
||||
print_info "Skipping stack start (SKIP_START=true)"
|
||||
else
|
||||
|
||||
print_info "Using compose file: ${COMPOSE_FILE}"
|
||||
print_info "This may take a few minutes on first run..."
|
||||
echo ""
|
||||
@@ -1317,19 +1534,24 @@ if [[ "$DEPLOYMENT_MODE" == "local" ]]; then
|
||||
mkdir -p mas/certs
|
||||
mkdir -p caddy/data/caddy # Required for Caddy to save PKI certificates
|
||||
|
||||
# Wait for Caddy to generate CA
|
||||
print_info "Waiting for Caddy to generate local CA..."
|
||||
sleep 5
|
||||
# Wait for Caddy to generate CA (retry loop — cert is created lazily on first HTTPS request)
|
||||
print_info "Waiting for Caddy to generate local CA certificate..."
|
||||
CADDY_CA_SRC="caddy/data/caddy/pki/authorities/local/root.crt"
|
||||
CA_READY=false
|
||||
for i in {1..24}; do
|
||||
# Trigger HTTPS request each iteration to prompt Caddy to generate certs
|
||||
curl -k https://${AUTH_DOMAIN} > /dev/null 2>&1 || true
|
||||
if sudo test -f "${CADDY_CA_SRC}"; then
|
||||
CA_READY=true
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
# Trigger HTTPS requests to force Caddy to generate certificates
|
||||
print_info "Triggering certificate generation..."
|
||||
curl -k https://${AUTH_DOMAIN} > /dev/null 2>&1 || true
|
||||
sleep 3
|
||||
|
||||
# Copy CA certificate from host path (Caddy saves to volume)
|
||||
if [ -f "caddy/data/caddy/pki/authorities/local/root.crt" ]; then
|
||||
cp caddy/data/caddy/pki/authorities/local/root.crt mas/certs/caddy-ca.crt
|
||||
chmod 644 mas/certs/caddy-ca.crt
|
||||
# Copy CA certificate from host path (Caddy data dir is root-owned via Docker)
|
||||
if [[ "$CA_READY" == true ]]; then
|
||||
sudo cp "${CADDY_CA_SRC}" mas/certs/caddy-ca.crt
|
||||
sudo chmod 644 mas/certs/caddy-ca.crt
|
||||
print_status "Caddy CA certificate copied to mas/certs/caddy-ca.crt"
|
||||
|
||||
# Restart MAS to pick up the certificate
|
||||
@@ -1338,14 +1560,16 @@ if [[ "$DEPLOYMENT_MODE" == "local" ]]; then
|
||||
sleep 5
|
||||
print_status "MAS restarted with trusted CA certificate"
|
||||
else
|
||||
print_warning "Could not find Caddy CA certificate at caddy/data/caddy/pki/authorities/local/root.crt"
|
||||
print_warning "Could not find Caddy CA certificate after 2 minutes"
|
||||
print_info "You may need to manually copy it after Caddy generates it"
|
||||
print_info "Run: cp caddy/data/caddy/pki/authorities/local/root.crt mas/certs/caddy-ca.crt"
|
||||
print_info "Run: sudo cp caddy/data/caddy/pki/authorities/local/root.crt mas/certs/caddy-ca.crt"
|
||||
print_info "Then restart MAS: $DOCKER_COMPOSE_CMD -f ${COMPOSE_FILE} restart mas"
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
fi # end SKIP_START
|
||||
|
||||
# ============================================================================
|
||||
# PRODUCTION: Generate Caddy and Authelia configs for separate machines
|
||||
# ============================================================================
|
||||
@@ -1362,10 +1586,10 @@ if [[ "$DEPLOYMENT_MODE" == "production" ]]; then
|
||||
# Pre-build conditional JSON blobs for the Caddyfile
|
||||
if [[ "$USE_ELEMENT_CALL" == true ]]; then
|
||||
PROD_WELLKNOWN_JSON="{\"m.homeserver\":{\"base_url\":\"https://${MATRIX_DOMAIN}\"},\"m.authentication\":{\"issuer\":\"https://${AUTH_DOMAIN}/\"},\"org.matrix.msc4143.rtc_foci\":[{\"type\":\"livekit\",\"livekit_service_url\":\"https://${RTC_DOMAIN}/livekit/jwt\"}]}"
|
||||
PROD_ELEMENT_CFG_JSON="{\"default_server_config\":{\"m.homeserver\":{\"base_url\":\"https://${MATRIX_DOMAIN}\",\"server_name\":\"${MATRIX_DOMAIN}\"}},\"default_server_name\":\"${MATRIX_DOMAIN}\",\"disable_custom_urls\":false,\"disable_guests\":true,\"features\":{\"feature_oidc_aware_navigation\":true,\"feature_element_call_video_rooms\":true},\"element_call\":{\"url\":\"https://${CALL_DOMAIN}\",\"participant_limit\":8,\"brand\":\"Element Call\"}}"
|
||||
PROD_ELEMENT_CFG_JSON="{\"default_server_config\":{\"m.homeserver\":{\"base_url\":\"https://${MATRIX_DOMAIN}\",\"server_name\":\"${SERVER_NAME}\"}},\"default_server_name\":\"${SERVER_NAME}\",\"disable_custom_urls\":false,\"disable_guests\":true,\"features\":{\"feature_oidc_aware_navigation\":true,\"feature_element_call_video_rooms\":true},\"element_call\":{\"url\":\"https://${CALL_DOMAIN}\",\"participant_limit\":8,\"brand\":\"Element Call\"}}"
|
||||
else
|
||||
PROD_WELLKNOWN_JSON="{\"m.homeserver\":{\"base_url\":\"https://${MATRIX_DOMAIN}\"},\"m.authentication\":{\"issuer\":\"https://${AUTH_DOMAIN}/\"}}"
|
||||
PROD_ELEMENT_CFG_JSON="{\"default_server_config\":{\"m.homeserver\":{\"base_url\":\"https://${MATRIX_DOMAIN}\",\"server_name\":\"${MATRIX_DOMAIN}\"}},\"default_server_name\":\"${MATRIX_DOMAIN}\",\"disable_custom_urls\":false,\"disable_guests\":true,\"features\":{\"feature_oidc_aware_navigation\":true}}"
|
||||
PROD_ELEMENT_CFG_JSON="{\"default_server_config\":{\"m.homeserver\":{\"base_url\":\"https://${MATRIX_DOMAIN}\",\"server_name\":\"${SERVER_NAME}\"}},\"default_server_name\":\"${SERVER_NAME}\",\"disable_custom_urls\":false,\"disable_guests\":true,\"features\":{\"feature_oidc_aware_navigation\":true}}"
|
||||
fi
|
||||
|
||||
cat > caddy/Caddyfile.production << EOF
|
||||
@@ -1375,8 +1599,8 @@ if [[ "$DEPLOYMENT_MODE" == "production" ]]; then
|
||||
|
||||
{
|
||||
email ${LETSENCRYPT_EMAIL}
|
||||
# Enable admin API (restrict access in firewall)
|
||||
admin 0.0.0.0:2019
|
||||
# Enable admin API (localhost only)
|
||||
admin localhost:2019
|
||||
}
|
||||
|
||||
# =========================
|
||||
@@ -1387,6 +1611,7 @@ ${MATRIX_DOMAIN} {
|
||||
@wk path /.well-known/matrix/client
|
||||
handle @wk {
|
||||
header Content-Type application/json
|
||||
header Access-Control-Allow-Origin "*"
|
||||
respond \`${PROD_WELLKNOWN_JSON}\` 200
|
||||
}
|
||||
|
||||
@@ -1429,6 +1654,8 @@ ${MATRIX_DOMAIN} {
|
||||
handle @compat {
|
||||
header Access-Control-Allow-Origin "*"
|
||||
reverse_proxy ${MATRIX_SERVER_IP}:8080 {
|
||||
header_up Host {http.request.host}
|
||||
header_up X-Forwarded-Host {http.request.host}
|
||||
header_down -Access-Control-Allow-Origin
|
||||
header_down -Access-Control-Allow-Methods
|
||||
header_down -Access-Control-Allow-Headers
|
||||
@@ -1448,6 +1675,11 @@ ${MATRIX_DOMAIN} {
|
||||
}
|
||||
}
|
||||
|
||||
# Block public access to Synapse admin API
|
||||
handle /_synapse/admin* {
|
||||
respond "Forbidden" 403
|
||||
}
|
||||
|
||||
handle {
|
||||
reverse_proxy ${MATRIX_SERVER_IP}:8008
|
||||
}
|
||||
@@ -1461,23 +1693,35 @@ ${AUTH_DOMAIN} {
|
||||
@disco path /.well-known/openid-configuration
|
||||
handle @disco {
|
||||
header ?Access-Control-Allow-Origin "*"
|
||||
reverse_proxy ${MATRIX_SERVER_IP}:8080
|
||||
reverse_proxy ${MATRIX_SERVER_IP}:8080 {
|
||||
header_up Host {http.request.host}
|
||||
header_up X-Forwarded-Host {http.request.host}
|
||||
}
|
||||
}
|
||||
|
||||
# OAuth2 endpoints
|
||||
@oauth path /oauth2/*
|
||||
route @oauth {
|
||||
header ?Access-Control-Allow-Origin "*"
|
||||
reverse_proxy ${MATRIX_SERVER_IP}:8080
|
||||
reverse_proxy ${MATRIX_SERVER_IP}:8080 {
|
||||
header_up Host {http.request.host}
|
||||
header_up X-Forwarded-Host {http.request.host}
|
||||
}
|
||||
}
|
||||
|
||||
# Account portal
|
||||
handle_path /account/* {
|
||||
reverse_proxy ${MATRIX_SERVER_IP}:8080
|
||||
# Account portal (handle, not handle_path — preserves /account/ prefix for MAS SPA routing)
|
||||
handle /account/* {
|
||||
reverse_proxy ${MATRIX_SERVER_IP}:8080 {
|
||||
header_up Host {http.request.host}
|
||||
header_up X-Forwarded-Host {http.request.host}
|
||||
}
|
||||
}
|
||||
|
||||
handle {
|
||||
reverse_proxy ${MATRIX_SERVER_IP}:8080
|
||||
reverse_proxy ${MATRIX_SERVER_IP}:8080 {
|
||||
header_up Host {http.request.host}
|
||||
header_up X-Forwarded-Host {http.request.host}
|
||||
}
|
||||
}
|
||||
|
||||
handle_errors {
|
||||
@@ -1520,6 +1764,30 @@ ${ADMIN_DOMAIN} {
|
||||
}
|
||||
EOF
|
||||
|
||||
# Append identity domain well-known block if SERVER_NAME differs from MATRIX_DOMAIN
|
||||
if [[ "$SERVER_NAME" != "$MATRIX_DOMAIN" ]]; then
|
||||
cat >> caddy/Caddyfile.production << EOF
|
||||
|
||||
# =========================
|
||||
# Identity Domain (well-known delegation)
|
||||
# =========================
|
||||
${SERVER_NAME} {
|
||||
@wk path /.well-known/matrix/client
|
||||
handle @wk {
|
||||
header Content-Type application/json
|
||||
header Access-Control-Allow-Origin "*"
|
||||
respond \`${PROD_WELLKNOWN_JSON}\` 200
|
||||
}
|
||||
|
||||
@wk_server path /.well-known/matrix/server
|
||||
handle @wk_server {
|
||||
header Content-Type application/json
|
||||
respond \`{"m.server":"${MATRIX_DOMAIN}:443"}\` 200
|
||||
}
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Append Element Call blocks to production Caddyfile if enabled
|
||||
if [[ "$USE_ELEMENT_CALL" == true ]]; then
|
||||
cat >> caddy/Caddyfile.production << EOF
|
||||
|
||||
+14
-14
@@ -17,7 +17,7 @@
|
||||
services:
|
||||
# PostgreSQL Database
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
image: ${POSTGRES_IMAGE:-postgres:16-alpine}
|
||||
container_name: matrix-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@@ -38,7 +38,7 @@ services:
|
||||
|
||||
# Matrix Synapse Server
|
||||
synapse:
|
||||
image: matrixdotorg/synapse:latest
|
||||
image: ${SYNAPSE_IMAGE:-matrixdotorg/synapse:latest}
|
||||
container_name: matrix-synapse
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@@ -64,7 +64,7 @@ services:
|
||||
|
||||
# Element Web Client
|
||||
element:
|
||||
image: vectorim/element-web:latest
|
||||
image: ${ELEMENT_IMAGE:-vectorim/element-web:latest}
|
||||
container_name: matrix-element
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
@@ -79,7 +79,7 @@ services:
|
||||
|
||||
# Element Admin - Web UI for managing users/rooms via MAS
|
||||
element-admin:
|
||||
image: oci.element.io/element-admin:latest
|
||||
image: ${ELEMENT_ADMIN_IMAGE:-oci.element.io/element-admin:latest}
|
||||
container_name: matrix-element-admin
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@@ -99,7 +99,7 @@ services:
|
||||
redis:
|
||||
profiles:
|
||||
- authelia # Only started when Authelia profile is active
|
||||
image: redis:7-alpine
|
||||
image: ${REDIS_IMAGE:-redis:7-alpine}
|
||||
container_name: matrix-redis
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
@@ -112,7 +112,7 @@ services:
|
||||
|
||||
# Matrix Authentication Service (MAS)
|
||||
mas:
|
||||
image: ghcr.io/element-hq/matrix-authentication-service:latest
|
||||
image: ${MAS_IMAGE:-ghcr.io/element-hq/matrix-authentication-service:latest}
|
||||
container_name: matrix-mas
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@@ -139,7 +139,7 @@ services:
|
||||
|
||||
# mautrix-telegram Bridge
|
||||
mautrix-telegram:
|
||||
image: dock.mau.dev/mautrix/telegram:latest
|
||||
image: ${TELEGRAM_IMAGE:-dock.mau.dev/mautrix/telegram:latest}
|
||||
container_name: matrix-bridge-telegram
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
@@ -152,7 +152,7 @@ services:
|
||||
|
||||
# mautrix-whatsapp Bridge
|
||||
mautrix-whatsapp:
|
||||
image: dock.mau.dev/mautrix/whatsapp:latest
|
||||
image: ${WHATSAPP_IMAGE:-dock.mau.dev/mautrix/whatsapp:latest}
|
||||
container_name: matrix-bridge-whatsapp
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
@@ -165,7 +165,7 @@ services:
|
||||
|
||||
# mautrix-signal Bridge
|
||||
mautrix-signal:
|
||||
image: dock.mau.dev/mautrix/signal:latest
|
||||
image: ${SIGNAL_IMAGE:-dock.mau.dev/mautrix/signal:latest}
|
||||
container_name: matrix-bridge-signal
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
@@ -180,7 +180,7 @@ services:
|
||||
livekit:
|
||||
profiles:
|
||||
- element-call
|
||||
image: livekit/livekit-server:latest
|
||||
image: ${LIVEKIT_IMAGE:-livekit/livekit-server:latest}
|
||||
container_name: matrix-livekit
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
@@ -197,7 +197,7 @@ services:
|
||||
lk-jwt-service:
|
||||
profiles:
|
||||
- element-call
|
||||
image: ghcr.io/element-hq/lk-jwt-service:latest
|
||||
image: ${LK_JWT_IMAGE:-ghcr.io/element-hq/lk-jwt-service:latest}
|
||||
container_name: matrix-lk-jwt
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
@@ -218,7 +218,7 @@ services:
|
||||
element-call:
|
||||
profiles:
|
||||
- element-call
|
||||
image: ghcr.io/element-hq/element-call:latest
|
||||
image: ${ELEMENT_CALL_IMAGE:-ghcr.io/element-hq/element-call:latest}
|
||||
container_name: matrix-element-call
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
@@ -230,7 +230,7 @@ services:
|
||||
authelia:
|
||||
profiles:
|
||||
- authelia # Only started when Authelia profile is active
|
||||
image: authelia/authelia:latest
|
||||
image: ${AUTHELIA_IMAGE:-authelia/authelia:latest}
|
||||
container_name: matrix-authelia
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@@ -258,7 +258,7 @@ services:
|
||||
caddy:
|
||||
profiles:
|
||||
- single-machine # Only started for single-machine deployments
|
||||
image: caddy:2-alpine
|
||||
image: ${CADDY_IMAGE:-caddy:2-alpine}
|
||||
container_name: matrix-caddy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
|
||||
+37
-6
@@ -134,6 +134,7 @@ http:
|
||||
- name: graphql
|
||||
playground: true
|
||||
- name: assets
|
||||
- name: adminapi
|
||||
binds:
|
||||
- address: '[::]:8080'
|
||||
- name: internal
|
||||
@@ -269,6 +270,7 @@ if [[ ! -f "synapse/data/homeserver.yaml" ]]; then
|
||||
-e SYNAPSE_SERVER_NAME="${MATRIX_DOMAIN}" \
|
||||
-e SYNAPSE_REPORT_STATS=no \
|
||||
matrixdotorg/synapse:latest generate 2>/dev/null
|
||||
sudo chown -R "$(id -u):$(id -g)" synapse/data/
|
||||
ok "Synapse config generated"
|
||||
fi
|
||||
|
||||
@@ -296,6 +298,9 @@ database:
|
||||
cp_max: 10
|
||||
|
||||
enable_registration: false
|
||||
allow_guest_access: false
|
||||
allow_public_rooms_without_auth: false
|
||||
allow_public_rooms_over_federation: false
|
||||
|
||||
matrix_authentication_service:
|
||||
enabled: true
|
||||
@@ -336,7 +341,7 @@ fi
|
||||
cat > caddy/Caddyfile << EOF
|
||||
{
|
||||
email ${LETSENCRYPT_EMAIL}
|
||||
admin 0.0.0.0:2019
|
||||
admin localhost:2019
|
||||
}
|
||||
|
||||
${MATRIX_DOMAIN} {
|
||||
@@ -369,6 +374,8 @@ ${MATRIX_DOMAIN} {
|
||||
handle @compat {
|
||||
header Access-Control-Allow-Origin "*"
|
||||
reverse_proxy mas:8080 {
|
||||
header_up Host {http.request.host}
|
||||
header_up X-Forwarded-Host {http.request.host}
|
||||
header_down -Access-Control-Allow-Origin
|
||||
}
|
||||
}
|
||||
@@ -381,6 +388,11 @@ ${MATRIX_DOMAIN} {
|
||||
}
|
||||
}
|
||||
|
||||
# Block public access to Synapse admin API
|
||||
handle /_synapse/admin* {
|
||||
respond "Forbidden" 403
|
||||
}
|
||||
|
||||
handle {
|
||||
reverse_proxy synapse:8008
|
||||
}
|
||||
@@ -390,21 +402,33 @@ ${AUTH_DOMAIN} {
|
||||
@disco path /.well-known/openid-configuration
|
||||
handle @disco {
|
||||
header Access-Control-Allow-Origin "*"
|
||||
reverse_proxy mas:8080
|
||||
reverse_proxy mas:8080 {
|
||||
header_up Host {http.request.host}
|
||||
header_up X-Forwarded-Host {http.request.host}
|
||||
}
|
||||
}
|
||||
|
||||
@oauth path /oauth2/*
|
||||
route @oauth {
|
||||
header Access-Control-Allow-Origin "*"
|
||||
reverse_proxy mas:8080
|
||||
reverse_proxy mas:8080 {
|
||||
header_up Host {http.request.host}
|
||||
header_up X-Forwarded-Host {http.request.host}
|
||||
}
|
||||
}
|
||||
|
||||
handle_path /account/* {
|
||||
reverse_proxy mas:8080
|
||||
handle /account/* {
|
||||
reverse_proxy mas:8080 {
|
||||
header_up Host {http.request.host}
|
||||
header_up X-Forwarded-Host {http.request.host}
|
||||
}
|
||||
}
|
||||
|
||||
handle {
|
||||
reverse_proxy mas:8080
|
||||
reverse_proxy mas:8080 {
|
||||
header_up Host {http.request.host}
|
||||
header_up X-Forwarded-Host {http.request.host}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,6 +474,11 @@ echo ""
|
||||
|
||||
# ── Start the stack ───────────────────────────────────────────────────────────
|
||||
|
||||
if [[ "${SKIP_START:-false}" == "true" ]]; then
|
||||
ok "Skipping stack start (SKIP_START=true)"
|
||||
echo ""
|
||||
else
|
||||
|
||||
info "Starting PostgreSQL..."
|
||||
sudo docker compose up -d postgres
|
||||
|
||||
@@ -468,6 +497,8 @@ info "Starting all services..."
|
||||
sudo docker compose --profile single-machine up -d ${CORE_SERVICES}
|
||||
echo ""
|
||||
|
||||
fi
|
||||
|
||||
# ── Summary ───────────────────────────────────────────────────────────────────
|
||||
|
||||
ok "Stack is up."
|
||||
|
||||
@@ -45,6 +45,10 @@ registration_shared_secret: "{{SYNAPSE_REGISTRATION_SHARED_SECRET}}"
|
||||
# Allow guest access
|
||||
allow_guest_access: false
|
||||
|
||||
# Public room directory visibility
|
||||
allow_public_rooms_without_auth: false
|
||||
allow_public_rooms_over_federation: false
|
||||
|
||||
# Matrix Authentication Service (MAS) integration (Synapse 1.136+)
|
||||
# Replaces deprecated experimental_features.msc3861
|
||||
matrix_authentication_service:
|
||||
|
||||
Executable
+544
@@ -0,0 +1,544 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# test_deploy.sh — Integration test suite for deploy.sh
|
||||
#
|
||||
# Scenarios:
|
||||
# A) TLD identity: SERVER_NAME=example.test (@user:example.test)
|
||||
# B) Subdomain identity: SERVER_NAME=matrix.example.test (@user:matrix.example.test)
|
||||
#
|
||||
# Each scenario:
|
||||
# 1. Runs deploy.sh with pre-set stdin
|
||||
# 2. Validates all generated config files
|
||||
# 3. Hits live endpoints via curl (Caddy:443 → 127.0.0.1)
|
||||
# 4. Tears down the stack and cleans up
|
||||
#
|
||||
# Usage:
|
||||
# ./test_deploy.sh # full suite (config + endpoints)
|
||||
# SKIP_INTEGRATION=true ./test_deploy.sh # config-file checks only (no endpoint tests)
|
||||
#
|
||||
# Requires: docker, docker compose v2, bash ≥ 4, openssl, curl
|
||||
# =============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ─── Colors ──────────────────────────────────────────────────────────────────
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'; CYAN='\033[0;36m'; MAGENTA='\033[0;35m'
|
||||
BOLD='\033[1m'; NC='\033[0m'
|
||||
|
||||
# ─── Config ───────────────────────────────────────────────────────────────────
|
||||
SKIP_INTEGRATION="${SKIP_INTEGRATION:-false}"
|
||||
COMPOSE_FILE="compose-variants/docker-compose.local.yml"
|
||||
COMPOSE_CMD="sudo docker compose --project-directory ."
|
||||
|
||||
# ─── Counters ─────────────────────────────────────────────────────────────────
|
||||
TESTS_PASSED=0
|
||||
TESTS_FAILED=0
|
||||
|
||||
# ─── Output helpers ───────────────────────────────────────────────────────────
|
||||
pass() { echo -e " ${GREEN}✓${NC} $1"; TESTS_PASSED=$((TESTS_PASSED + 1)); }
|
||||
fail() { echo -e " ${RED}✗${NC} $1"; TESTS_FAILED=$((TESTS_FAILED + 1)); }
|
||||
info() { echo -e " ${BLUE}ℹ${NC} $1"; }
|
||||
warn() { echo -e " ${YELLOW}⚠${NC} $1"; }
|
||||
header() { echo -e "\n${BOLD}${CYAN}── $1 ──${NC}"; }
|
||||
section(){ echo -e "\n${BOLD}${MAGENTA}════ $1 ════${NC}"; }
|
||||
|
||||
# ─── Sudo shim for root CI environments that lack sudo ───────────────────────
|
||||
setup_sudo_shim() {
|
||||
if ! command -v sudo &>/dev/null; then
|
||||
local d; d=$(mktemp -d)
|
||||
printf '#!/bin/sh\nexec "$@"\n' > "$d/sudo"
|
||||
chmod +x "$d/sudo"
|
||||
export PATH="$d:$PATH"
|
||||
info "Created sudo passthrough shim (running as root)"
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Prerequisites ────────────────────────────────────────────────────────────
|
||||
check_prereqs() {
|
||||
header "Prerequisites"
|
||||
local ok=true
|
||||
|
||||
for cmd in bash openssl curl; do
|
||||
command -v "$cmd" &>/dev/null \
|
||||
&& pass "$cmd available" \
|
||||
|| { fail "$cmd not found"; ok=false; }
|
||||
done
|
||||
|
||||
sudo docker ps &>/dev/null \
|
||||
&& pass "Docker daemon reachable" \
|
||||
|| { fail "Docker not accessible (sudo docker ps failed)"; ok=false; }
|
||||
|
||||
sudo docker compose version &>/dev/null \
|
||||
&& pass "docker compose v2 available" \
|
||||
|| { fail "docker compose not available"; ok=false; }
|
||||
|
||||
[[ -f deploy.sh ]] \
|
||||
|| { fail "deploy.sh not found — run from repo root"; ok=false; }
|
||||
|
||||
[[ "$ok" == "true" ]] || { echo -e "\n${RED}Prerequisites failed. Aborting.${NC}"; exit 1; }
|
||||
}
|
||||
|
||||
# ─── Stop stack and wipe all data volumes ─────────────────────────────────────
|
||||
teardown_stack() {
|
||||
info "Stopping Docker stack and removing volumes..."
|
||||
$COMPOSE_CMD -f "$COMPOSE_FILE" down -v --remove-orphans 2>/dev/null || true
|
||||
sudo rm -rf postgres/data mas/data mas/certs caddy/data caddy/config 2>/dev/null || true
|
||||
# Wipe synapse/data fully so no leftover signing keys or log configs
|
||||
# confuse the next scenario's `docker run ... generate` step
|
||||
sudo rm -rf synapse/data 2>/dev/null || true
|
||||
mkdir -p synapse/data
|
||||
}
|
||||
|
||||
# ─── Remove all generated config files ────────────────────────────────────────
|
||||
cleanup_configs() {
|
||||
info "Removing generated configs..."
|
||||
rm -f .env mas-signing.key authelia_private.pem
|
||||
rm -f caddy/Caddyfile caddy/Caddyfile.production
|
||||
rm -f livekit/livekit.yaml
|
||||
rm -f appservices/doublepuppet.yaml
|
||||
# These may be root-owned from docker run or previous deploys
|
||||
sudo rm -f mas/config/config.yaml 2>/dev/null || true
|
||||
sudo rm -f element/config/config.json 2>/dev/null || true
|
||||
sudo rm -f authelia/config/configuration.yml authelia/config/users_database.yml 2>/dev/null || true
|
||||
sudo rm -f synapse/data/homeserver.yaml synapse/data/homeserver.yaml.bak 2>/dev/null || true
|
||||
}
|
||||
|
||||
# ─── Assertions ───────────────────────────────────────────────────────────────
|
||||
assert_file() {
|
||||
local file="$1" label="$2"
|
||||
[[ -f "$file" ]] && pass "$label" || fail "$label (missing: $file)"
|
||||
}
|
||||
|
||||
assert_contains() {
|
||||
local file="$1" pattern="$2" label="$3"
|
||||
if grep -qF "$pattern" "$file" 2>/dev/null; then
|
||||
pass "$label"
|
||||
else
|
||||
fail "$label [expected '${pattern}' in ${file}]"
|
||||
fi
|
||||
}
|
||||
|
||||
assert_not_contains() {
|
||||
local file="$1" pattern="$2" label="$3"
|
||||
if grep -qF "$pattern" "$file" 2>/dev/null; then
|
||||
fail "$label [unexpected '${pattern}' found in ${file}]"
|
||||
else
|
||||
pass "$label"
|
||||
fi
|
||||
}
|
||||
|
||||
# Regex variant — use when the value may be quoted/unquoted (grep -E)
|
||||
assert_matches() {
|
||||
local file="$1" pattern="$2" label="$3"
|
||||
if grep -qE "$pattern" "$file" 2>/dev/null; then
|
||||
pass "$label"
|
||||
else
|
||||
fail "$label [expected /$pattern/ in ${file}]"
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Config-file assertions (no Docker needed) ────────────────────────────────
|
||||
assert_configs() {
|
||||
local server_name="$1"
|
||||
local matrix_domain="matrix.example.test"
|
||||
|
||||
header "Config assertions (SERVER_NAME=${server_name})"
|
||||
|
||||
# .env
|
||||
assert_file ".env" ".env generated"
|
||||
assert_contains ".env" "SERVER_NAME=${server_name}" ".env → SERVER_NAME"
|
||||
assert_contains ".env" "MATRIX_DOMAIN=${matrix_domain}" ".env → MATRIX_DOMAIN"
|
||||
|
||||
# MAS config
|
||||
assert_file "mas/config/config.yaml" "mas/config/config.yaml generated"
|
||||
assert_contains "mas/config/config.yaml" \
|
||||
"homeserver: '${server_name}'" "MAS → homeserver"
|
||||
assert_contains "mas/config/config.yaml" \
|
||||
"name: adminapi" "MAS → adminapi listener present"
|
||||
|
||||
# Element Web config (heredoc format has spaces: `"key": "value"`)
|
||||
assert_file "element/config/config.json" "element/config/config.json generated"
|
||||
assert_contains "element/config/config.json" \
|
||||
"\"server_name\": \"${server_name}\"" "Element → server_name"
|
||||
assert_contains "element/config/config.json" \
|
||||
"\"default_server_name\": \"${server_name}\"" "Element → default_server_name"
|
||||
assert_contains "element/config/config.json" \
|
||||
"\"base_url\": \"https://${matrix_domain}\"" "Element → base_url stays matrix domain"
|
||||
|
||||
# Synapse homeserver.yaml
|
||||
assert_file "synapse/data/homeserver.yaml" "synapse/data/homeserver.yaml generated"
|
||||
# Synapse may quote the value: `server_name: "example.test"` or `server_name: example.test`
|
||||
assert_matches "synapse/data/homeserver.yaml" \
|
||||
"^server_name: \"?${server_name//./\\.}\"?" "Synapse → server_name"
|
||||
assert_contains "synapse/data/homeserver.yaml" \
|
||||
"app_service_config_files:" "Synapse → app_service_config_files present"
|
||||
assert_contains "synapse/data/homeserver.yaml" \
|
||||
"/appservices/doublepuppet.yaml" "Synapse → doublepuppet.yaml registered"
|
||||
|
||||
# Double-puppeting appservice
|
||||
assert_file "appservices/doublepuppet.yaml" "appservices/doublepuppet.yaml generated"
|
||||
assert_contains "appservices/doublepuppet.yaml" \
|
||||
"id: doublepuppet" "doublepuppet.yaml → id"
|
||||
assert_contains "appservices/doublepuppet.yaml" \
|
||||
"url: null" "doublepuppet.yaml → url null"
|
||||
assert_contains "appservices/doublepuppet.yaml" \
|
||||
"as_token:" "doublepuppet.yaml → as_token present"
|
||||
assert_contains "appservices/doublepuppet.yaml" \
|
||||
"@.*:${server_name}" "doublepuppet.yaml → user regex matches server_name"
|
||||
|
||||
# Caddyfile (JSON blobs are compact, no spaces around ':')
|
||||
assert_file "caddy/Caddyfile" "caddy/Caddyfile generated"
|
||||
assert_contains "caddy/Caddyfile" \
|
||||
"\"server_name\":\"${server_name}\"" "Caddyfile JSON → server_name"
|
||||
assert_contains "caddy/Caddyfile" \
|
||||
"\"default_server_name\":\"${server_name}\"" "Caddyfile JSON → default_server_name"
|
||||
assert_contains "caddy/Caddyfile" \
|
||||
"\"base_url\":\"https://${matrix_domain}\"" "Caddyfile JSON → base_url stays matrix domain"
|
||||
|
||||
if [[ "$server_name" != "$matrix_domain" ]]; then
|
||||
# TLD mode: identity domain block must be present
|
||||
assert_contains "caddy/Caddyfile" \
|
||||
"# Identity Domain (well-known delegation)" "Caddyfile → identity domain block present"
|
||||
assert_contains "caddy/Caddyfile" \
|
||||
"${server_name}:443 {" "Caddyfile → ${server_name}:443 block"
|
||||
assert_contains "caddy/Caddyfile" \
|
||||
"\"m.server\":\"${matrix_domain}:443\"" "Caddyfile → m.server delegates to matrix domain"
|
||||
else
|
||||
# Subdomain mode: no identity domain block
|
||||
assert_not_contains "caddy/Caddyfile" \
|
||||
"# Identity Domain (well-known delegation)" "Caddyfile → no identity block in subdomain mode"
|
||||
fi
|
||||
|
||||
# ── MAS config correctness ───────────────────────────────────────────────
|
||||
local auth_domain="auth.example.test"
|
||||
assert_contains "mas/config/config.yaml" \
|
||||
"public_base: 'https://${auth_domain}/'" "MAS → public_base uses auth domain"
|
||||
assert_contains "mas/config/config.yaml" \
|
||||
"issuer: 'https://${auth_domain}/'" "MAS → issuer uses auth domain"
|
||||
assert_contains "mas/config/config.yaml" \
|
||||
"endpoint: 'http://synapse:8008'" "MAS → synapse endpoint correct"
|
||||
|
||||
# ── Synapse config correctness ───────────────────────────────────────────
|
||||
assert_contains "synapse/data/homeserver.yaml" \
|
||||
"endpoint: 'http://mas:8080'" "Synapse → MAS endpoint correct"
|
||||
assert_contains "synapse/data/homeserver.yaml" \
|
||||
"enable_registration: false" "Synapse → registration disabled"
|
||||
assert_contains "synapse/data/homeserver.yaml" \
|
||||
"allow_public_rooms_without_auth: false" "Synapse → public rooms not public"
|
||||
assert_contains "synapse/data/homeserver.yaml" \
|
||||
"allow_public_rooms_over_federation: false" "Synapse → public rooms not over federation"
|
||||
|
||||
# ── Caddyfile security ───────────────────────────────────────────────────
|
||||
assert_contains "caddy/Caddyfile" \
|
||||
"admin localhost:2019" "Caddyfile → admin API localhost only"
|
||||
assert_contains "caddy/Caddyfile" \
|
||||
"header_up X-Forwarded-Host" "Caddyfile → MAS proxy forwards X-Forwarded-Host"
|
||||
assert_contains "caddy/Caddyfile" \
|
||||
"/_synapse/admin" "Caddyfile → synapse admin route present"
|
||||
|
||||
# ── Well-known completeness ──────────────────────────────────────────────
|
||||
assert_contains "caddy/Caddyfile" \
|
||||
'"m.authentication"' "Caddyfile → well-known includes m.authentication"
|
||||
assert_contains "caddy/Caddyfile" \
|
||||
"\"issuer\":\"https://${auth_domain}/\"" "Caddyfile → well-known m.authentication.issuer correct"
|
||||
}
|
||||
|
||||
# ─── Curl an HTTPS endpoint, routing *.example.test → 127.0.0.1 ─────────────
|
||||
curl_local() {
|
||||
local domain="$1" path="$2"
|
||||
local -a args=(-sf --max-time 15 --resolve "${domain}:443:127.0.0.1")
|
||||
if [[ -f mas/certs/caddy-ca.crt ]]; then
|
||||
args+=(--cacert mas/certs/caddy-ca.crt)
|
||||
else
|
||||
args+=(-k)
|
||||
fi
|
||||
curl "${args[@]}" "https://${domain}${path}" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Returns only the HTTP status code (does not fail on non-2xx)
|
||||
curl_local_status() {
|
||||
local domain="$1" path="$2"
|
||||
local -a args=(-s --max-time 15 --resolve "${domain}:443:127.0.0.1" -o /dev/null -w "%{http_code}")
|
||||
if [[ -f mas/certs/caddy-ca.crt ]]; then
|
||||
args+=(--cacert mas/certs/caddy-ca.crt)
|
||||
else
|
||||
args+=(-k)
|
||||
fi
|
||||
curl "${args[@]}" "https://${domain}${path}" 2>/dev/null || echo "000"
|
||||
}
|
||||
|
||||
# ─── Copy Caddy CA to MAS and restart MAS ────────────────────────────────────
|
||||
# deploy.sh only waits 5s for the PKI file — not reliable. Use Caddy's admin
|
||||
# API (localhost:2019) instead, which returns the CA cert as soon as Caddy is up.
|
||||
setup_mas_ca() {
|
||||
local ca_dst="mas/certs/caddy-ca.crt"
|
||||
local waited=0
|
||||
info "Fetching Caddy CA via admin API (up to 60s)..."
|
||||
while (( waited < 60 )); do
|
||||
local resp; resp=$(curl -sf --max-time 5 http://localhost:2019/pki/ca/local 2>/dev/null || echo "")
|
||||
if echo "$resp" | grep -q '"root_certificate"'; then
|
||||
# Extract PEM from JSON: value uses literal \n; strip key/quotes, unescape newlines
|
||||
echo "$resp" \
|
||||
| grep -o '"root_certificate":"[^"]*"' \
|
||||
| sed 's/"root_certificate":"//; s/"$//' \
|
||||
| sed 's/\\n/\n/g' \
|
||||
| sudo tee "$ca_dst" > /dev/null
|
||||
info "Caddy CA fetched → restarting MAS..."
|
||||
$COMPOSE_CMD -f "$COMPOSE_FILE" restart mas 2>/dev/null || true
|
||||
sleep 10
|
||||
return 0
|
||||
fi
|
||||
sleep 3; waited=$((waited + 3))
|
||||
done
|
||||
warn "Caddy admin API did not return CA after 60s — MAS OIDC test will fail"
|
||||
}
|
||||
|
||||
# ─── Live endpoint assertions ─────────────────────────────────────────────────
|
||||
assert_endpoints() {
|
||||
local server_name="$1"
|
||||
local matrix_domain="matrix.example.test"
|
||||
|
||||
header "Endpoint tests (SERVER_NAME=${server_name})"
|
||||
info "Allowing 20s for full service initialization..."
|
||||
sleep 20
|
||||
|
||||
setup_mas_ca
|
||||
|
||||
# Synapse /health
|
||||
local health; health=$(curl_local "$matrix_domain" "/health")
|
||||
[[ "$health" == "OK" ]] \
|
||||
&& pass "Synapse /health → OK" \
|
||||
|| fail "Synapse /health (got: '${health:-no response}')"
|
||||
|
||||
# .well-known/matrix/client on matrix domain
|
||||
local wk; wk=$(curl_local "$matrix_domain" "/.well-known/matrix/client")
|
||||
echo "$wk" | grep -q '"m.homeserver"' \
|
||||
&& pass "${matrix_domain} → .well-known/matrix/client responds" \
|
||||
|| fail "${matrix_domain} → .well-known/matrix/client (got: '${wk:-no response}')"
|
||||
echo "$wk" | grep -q "https://${matrix_domain}" \
|
||||
&& pass ".well-known base_url = https://${matrix_domain}" \
|
||||
|| fail ".well-known base_url wrong (got: '${wk:-}')"
|
||||
|
||||
if [[ "$server_name" != "$matrix_domain" ]]; then
|
||||
# TLD mode: identity domain must serve well-known
|
||||
|
||||
local wk_id; wk_id=$(curl_local "$server_name" "/.well-known/matrix/client")
|
||||
echo "$wk_id" | grep -q '"m.homeserver"' \
|
||||
&& pass "${server_name} → .well-known/matrix/client responds" \
|
||||
|| fail "${server_name} → .well-known/matrix/client (got: '${wk_id:-no response}')"
|
||||
|
||||
local wk_srv; wk_srv=$(curl_local "$server_name" "/.well-known/matrix/server")
|
||||
echo "$wk_srv" | grep -q '"m.server"' \
|
||||
&& pass "${server_name} → .well-known/matrix/server responds" \
|
||||
|| fail "${server_name} → .well-known/matrix/server (got: '${wk_srv:-no response}')"
|
||||
echo "$wk_srv" | grep -q "$matrix_domain" \
|
||||
&& pass ".well-known/matrix/server delegates to ${matrix_domain}" \
|
||||
|| fail ".well-known/matrix/server missing ${matrix_domain} (got: '${wk_srv:-}')"
|
||||
fi
|
||||
|
||||
# MAS OIDC discovery
|
||||
local auth_domain="auth.example.test"
|
||||
local oidc; oidc=$(curl_local "$auth_domain" "/.well-known/openid-configuration")
|
||||
echo "$oidc" | grep -q '"issuer"' \
|
||||
&& pass "MAS OIDC discovery responds" \
|
||||
|| fail "MAS OIDC discovery (got: '${oidc:-no response}')"
|
||||
# issuer and authorization_endpoint must use the public auth domain, not an internal hostname
|
||||
# (regression test for issue #16 — missing X-Forwarded-Host caused silent OAuth2 breakage)
|
||||
echo "$oidc" | grep -q "\"issuer\":\"https://${auth_domain}/\"" \
|
||||
&& pass "MAS OIDC issuer = https://${auth_domain}/" \
|
||||
|| fail "MAS OIDC issuer wrong — check X-Forwarded-Host forwarding (got: '${oidc:-}')"
|
||||
echo "$oidc" | grep -q "\"authorization_endpoint\":\"https://${auth_domain}/" \
|
||||
&& pass "MAS OIDC authorization_endpoint on ${auth_domain}" \
|
||||
|| fail "MAS OIDC authorization_endpoint wrong — login button will silently fail (got: '${oidc:-}')"
|
||||
|
||||
# .well-known must include m.authentication so clients find MAS
|
||||
echo "$wk" | grep -q '"m.authentication"' \
|
||||
&& pass ".well-known includes m.authentication" \
|
||||
|| fail ".well-known missing m.authentication (got: '${wk:-}')"
|
||||
echo "$wk" | grep -q "\"issuer\":\"https://${auth_domain}/\"" \
|
||||
&& pass ".well-known m.authentication.issuer = https://${auth_domain}/" \
|
||||
|| fail ".well-known m.authentication.issuer wrong (got: '${wk:-}')"
|
||||
|
||||
# Synapse /_matrix/client/versions (confirms Synapse is up and routing works)
|
||||
local versions; versions=$(curl_local "$matrix_domain" "/_matrix/client/versions")
|
||||
echo "$versions" | grep -q '"versions"' \
|
||||
&& pass "Synapse /_matrix/client/versions responds" \
|
||||
|| fail "Synapse /_matrix/client/versions (got: '${versions:-no response}')"
|
||||
|
||||
# Login compat endpoint must be proxied to MAS (not return Synapse 404)
|
||||
local login; login=$(curl_local "$matrix_domain" "/_matrix/client/v3/login")
|
||||
echo "$login" | grep -qE '"flows"|"type"' \
|
||||
&& pass "/_matrix/client/v3/login proxied to MAS" \
|
||||
|| fail "/_matrix/client/v3/login not proxied to MAS (got: '${login:-no response}')"
|
||||
|
||||
# Synapse admin API must be blocked at Caddy (403)
|
||||
local admin_code; admin_code=$(curl_local_status "$matrix_domain" "/_synapse/admin/v1/server_version")
|
||||
[[ "$admin_code" == "403" ]] \
|
||||
&& pass "/_synapse/admin blocked (403)" \
|
||||
|| fail "/_synapse/admin not blocked (HTTP ${admin_code})"
|
||||
|
||||
# Element Web
|
||||
local elem; elem=$(curl_local "element.example.test" "/")
|
||||
echo "$elem" | grep -qi "element" \
|
||||
&& pass "Element Web root serves HTML" \
|
||||
|| fail "Element Web root (no Element content in response)"
|
||||
}
|
||||
|
||||
# ─── Quickstart config assertions ────────────────────────────────────────────
|
||||
assert_quickstart_configs() {
|
||||
local domain="$1"
|
||||
local matrix_domain="matrix.${domain}"
|
||||
local auth_domain="auth.${domain}"
|
||||
|
||||
header "Quickstart config assertions (domain=${domain})"
|
||||
|
||||
assert_file ".env" ".env generated"
|
||||
assert_contains ".env" "DOMAIN=${domain}" ".env → DOMAIN"
|
||||
assert_contains ".env" "MATRIX_DOMAIN=${matrix_domain}" ".env → MATRIX_DOMAIN"
|
||||
|
||||
assert_file "mas/config/config.yaml" "mas/config/config.yaml generated"
|
||||
assert_contains "mas/config/config.yaml" "homeserver: '${matrix_domain}'" "MAS → homeserver"
|
||||
assert_contains "mas/config/config.yaml" "name: adminapi" "MAS → adminapi listener"
|
||||
assert_contains "mas/config/config.yaml" "public_base: 'https://${auth_domain}/'" "MAS → public_base"
|
||||
assert_contains "mas/config/config.yaml" "issuer: 'https://${auth_domain}/'" "MAS → issuer"
|
||||
|
||||
assert_file "element/config/config.json" "element/config/config.json generated"
|
||||
|
||||
assert_file "synapse/data/homeserver.yaml" "synapse/data/homeserver.yaml generated"
|
||||
assert_contains "synapse/data/homeserver.yaml" "enable_registration: false" "Synapse → registration disabled"
|
||||
assert_contains "synapse/data/homeserver.yaml" "allow_guest_access: false" "Synapse → guest access disabled"
|
||||
assert_contains "synapse/data/homeserver.yaml" "allow_public_rooms_without_auth: false" "Synapse → public rooms blocked"
|
||||
assert_contains "synapse/data/homeserver.yaml" "allow_public_rooms_over_federation: false" "Synapse → public rooms over federation blocked"
|
||||
|
||||
assert_file "caddy/Caddyfile" "caddy/Caddyfile generated"
|
||||
assert_contains "caddy/Caddyfile" "admin localhost:2019" "Caddyfile → admin API localhost only"
|
||||
assert_contains "caddy/Caddyfile" "/_synapse/admin" "Caddyfile → synapse admin block present"
|
||||
assert_contains "caddy/Caddyfile" "header_up X-Forwarded-Host" "Caddyfile → MAS proxy forwards X-Forwarded-Host"
|
||||
assert_contains "caddy/Caddyfile" "handle /account/" "Caddyfile → /account/ uses handle (preserves prefix)"
|
||||
assert_not_contains "caddy/Caddyfile" "handle_path /account/" "Caddyfile → /account/ not handle_path"
|
||||
}
|
||||
|
||||
# ─── Run one full scenario ────────────────────────────────────────────────────
|
||||
run_scenario() {
|
||||
local name="$1"
|
||||
local sn_choice="$2" # "1" = TLD, "2" = subdomain
|
||||
local expected_sn="$3"
|
||||
|
||||
section "$name"
|
||||
teardown_stack
|
||||
cleanup_configs
|
||||
|
||||
info "Running deploy.sh (piped stdin)"
|
||||
|
||||
# Stdin answers in prompt order:
|
||||
# [1] Deployment type: 1 (local)
|
||||
# [2] Include Authelia? n
|
||||
# [3] Enable Element Call? n
|
||||
# [4] Custom Docker registry prefix: (empty → default)
|
||||
# [5] Use hardened images? n
|
||||
# [6] SERVER_NAME choice: $sn_choice (1=TLD, 2=subdomain)
|
||||
# [7] Press Enter to continue: (empty)
|
||||
printf '%s\n' "1" "n" "n" "" "n" "$sn_choice" "" \
|
||||
| bash deploy.sh
|
||||
|
||||
assert_configs "$expected_sn"
|
||||
|
||||
if [[ "$SKIP_INTEGRATION" == "true" ]]; then
|
||||
warn "Skipping endpoint tests (SKIP_INTEGRATION=true)"
|
||||
else
|
||||
assert_endpoints "$expected_sn"
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Summary ──────────────────────────────────────────────────────────────────
|
||||
print_summary() {
|
||||
echo ""
|
||||
echo -e "${BOLD}${CYAN}══════════════════════════════════════${NC}"
|
||||
echo -e "${BOLD} Results: ${GREEN}${TESTS_PASSED} passed${NC}${BOLD}, ${RED}${TESTS_FAILED} failed${NC}"
|
||||
echo -e "${BOLD}${CYAN}══════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
if (( TESTS_FAILED > 0 )); then
|
||||
echo -e "${RED}✗ Test suite FAILED${NC}"
|
||||
exit 1
|
||||
else
|
||||
echo -e "${GREEN}✓ All tests PASSED${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Cleanup on exit (INT, TERM, or normal exit) ──────────────────────────────
|
||||
cleanup_on_exit() {
|
||||
echo ""
|
||||
section "Cleanup"
|
||||
teardown_stack
|
||||
cleanup_configs
|
||||
info "Done."
|
||||
}
|
||||
trap cleanup_on_exit EXIT
|
||||
|
||||
# ─── Main ─────────────────────────────────────────────────────────────────────
|
||||
cd "$(dirname "$(realpath "$0")")" # ensure we're in the repo root
|
||||
setup_sudo_shim
|
||||
check_prereqs
|
||||
|
||||
# Scenario A — TLD identity: @user:example.test
|
||||
run_scenario \
|
||||
"A · TLD identity (@user:example.test)" \
|
||||
"1" \
|
||||
"example.test"
|
||||
|
||||
# Scenario B — Subdomain identity: @user:matrix.example.test
|
||||
run_scenario \
|
||||
"B · Subdomain identity (@user:matrix.example.test)" \
|
||||
"2" \
|
||||
"matrix.example.test"
|
||||
|
||||
# Scenario P — production Caddyfile generation (config only, no Let's Encrypt)
|
||||
section "P · Production Caddyfile (config only)"
|
||||
teardown_stack
|
||||
cleanup_configs
|
||||
info "Running deploy.sh production mode (piped stdin, SKIP_START=true)"
|
||||
# Stdin answers in prompt order:
|
||||
# [1] Deployment type: 2 (production)
|
||||
# [2] Include Authelia? n
|
||||
# [3] Enable Element Call? n
|
||||
# [4] Custom Docker registry prefix: (empty)
|
||||
# [5] Use hardened images? n
|
||||
# [6] Base domain: example.com
|
||||
# [7] Matrix subdomain: (empty → matrix)
|
||||
# [8] Element subdomain: (empty → element)
|
||||
# [9] Admin subdomain: (empty → admin)
|
||||
# [10] Auth subdomain: (empty → auth)
|
||||
# [11] Authelia subdomain: (empty → authelia)
|
||||
# [12] SERVER_NAME choice: 1 (TLD: @user:example.com)
|
||||
# [13] Matrix server address: (empty → 10.0.1.10)
|
||||
# [14] Authelia server address: (empty → 10.0.1.20)
|
||||
# [15] Let's Encrypt email: (empty → admin@example.com)
|
||||
printf '%s\n' "2" "n" "n" "" "n" "example.com" "" "" "" "" "" "1" "" "" "" \
|
||||
| SKIP_START=true bash deploy.sh
|
||||
|
||||
header "Production Caddyfile assertions"
|
||||
assert_file "caddy/Caddyfile.production" "caddy/Caddyfile.production generated"
|
||||
assert_contains "caddy/Caddyfile.production" "admin localhost:2019" "Caddyfile.production → admin API localhost only"
|
||||
assert_contains "caddy/Caddyfile.production" "/_synapse/admin" "Caddyfile.production → synapse admin block present"
|
||||
assert_contains "caddy/Caddyfile.production" "header_up X-Forwarded-Host" "Caddyfile.production → MAS proxy forwards X-Forwarded-Host"
|
||||
assert_contains "caddy/Caddyfile.production" "handle /account/" "Caddyfile.production → /account/ uses handle (preserves prefix)"
|
||||
assert_not_contains "caddy/Caddyfile.production" "handle_path /account/" "Caddyfile.production → /account/ not handle_path"
|
||||
assert_contains "caddy/Caddyfile.production" '"m.authentication"' "Caddyfile.production → well-known includes m.authentication"
|
||||
assert_contains "caddy/Caddyfile.production" "Access-Control-Allow-Origin" "Caddyfile.production → well-known has CORS header"
|
||||
|
||||
# Scenario Q — quickstart.sh config generation
|
||||
section "Q · quickstart.sh (single-machine, config only)"
|
||||
teardown_stack
|
||||
cleanup_configs
|
||||
info "Running quickstart.sh (piped stdin, SKIP_START=true)"
|
||||
printf '%s\n' "example.test" "test@example.test" "n" \
|
||||
| SKIP_START=true bash quickstart.sh
|
||||
assert_quickstart_configs "example.test"
|
||||
if [[ "$SKIP_INTEGRATION" != "true" ]]; then
|
||||
warn "Quickstart endpoint tests skipped (stack not started in SKIP_START mode)"
|
||||
fi
|
||||
|
||||
trap - EXIT
|
||||
cleanup_on_exit
|
||||
print_summary
|
||||
Reference in New Issue
Block a user