6 Commits

Author SHA1 Message Date
wmair 9dc60194c7 Sync quickstart.sh with deploy.sh security fixes; add production + quickstart test scenarios
quickstart.sh:
- admin localhost:2019 (was 0.0.0.0:2019 — exposed admin API publicly)
- Add /_synapse/admin block returning 403
- Forward Host + X-Forwarded-Host headers to MAS on all proxy blocks
- handle /account/* (was handle_path — stripped prefix, broke MAS SPA routing)
- Add allow_guest_access/allow_public_rooms_* false to Synapse config
- sudo chown synapse/data/ after docker run generate (sed -i needs ownership)

deploy.sh:
- Add SKIP_START=true env var to skip docker compose up (enables config-only CI testing)

test_deploy.sh:
- Scenario P: production Caddyfile assertions (caddy/Caddyfile.production)
- Scenario Q: quickstart.sh config assertions
- assert_quickstart_configs(): 15 assertions covering all previously-missed security properties

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 09:08:50 +02:00
wmair 6d9b156738 Expand test coverage and fix /_synapse/admin in local mode
test_deploy.sh:
- Add curl_local_status helper (returns HTTP status code)
- Config assertions: MAS public_base/issuer/endpoint, Synapse MAS endpoint,
  registration disabled, allow_public_rooms_* false, Caddyfile admin binding,
  X-Forwarded-Host header, /_synapse/admin block, m.authentication in well-known
- Endpoint assertions: OIDC issuer + authorization_endpoint domain (issue #16
  regression), well-known m.authentication.issuer, Synapse /versions,
  login compat proxy to MAS, /_synapse/admin 403

deploy.sh:
- Add /_synapse/admin block to local Caddyfile generation (was production-only)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 20:34:14 +01:00
wmair 7353cae399 Fix MAS authorization: forward Host and X-Forwarded-Host headers
Caddy does not set X-Forwarded-Host by default. Without it, MAS cannot
verify the request host when building OAuth2 redirect URIs, causing the
login "Continue" button to do nothing.

Added header_up Host and X-Forwarded-Host to all MAS reverse_proxy
blocks in both local and production Caddyfile generation.

Fixes #16

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 20:23:02 +01:00
wmair 88806d12fb Security hardening: Caddy admin API, Synapse admin endpoint, public room settings
- Caddy admin API: bind to localhost:2019 instead of 0.0.0.0:2019 (local + production)
- Production Caddyfile: block /_synapse/admin* with 403 (not needed publicly)
- homeserver.yaml: explicitly set allow_public_rooms_without_auth/over_federation to false

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 09:10:36 +01:00
wmair 74b0a3ecb9 Fix well-known CORS header missing on production Caddy config 2026-03-24 08:55:38 +01:00
wmair 0799cd3e38 Fix two deploy.sh bugs: Caddy handle_path strips /account/, cert copy needs sudo
- Replace handle_path /account/* with handle /account/* in both local and
  production Caddyfile templates. handle_path was stripping the /account/
  prefix before proxying to MAS, causing MAS to serve the root landing page
  instead of the account management SPA — making it impossible to edit
  account data.

- Fix Caddy CA cert extraction: use sudo cp/chmod (caddy/data/ is root-owned
  via Docker) and replace the one-shot sleep check with a retry loop (24×5s)
  that triggers curl on each iteration to prompt Caddy to generate the cert.
  Previously the cert copy silently failed, leaving MAS in a crash loop.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 19:24:05 +01:00
4 changed files with 298 additions and 39 deletions
+97 -32
View File
@@ -980,6 +980,10 @@ 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
@@ -1006,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:
@@ -1081,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
@@ -1183,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
@@ -1205,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
@@ -1223,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
@@ -1244,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
@@ -1266,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
@@ -1275,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)
@@ -1290,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
@@ -1429,6 +1461,11 @@ 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 ""
@@ -1497,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
@@ -1518,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
# ============================================================================
@@ -1555,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
}
# =========================
@@ -1567,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
}
@@ -1609,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
@@ -1628,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
}
@@ -1641,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 {
@@ -1711,6 +1775,7 @@ ${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
}
+36 -6
View File
@@ -270,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
@@ -297,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
@@ -337,7 +341,7 @@ fi
cat > caddy/Caddyfile << EOF
{
email ${LETSENCRYPT_EMAIL}
admin 0.0.0.0:2019
admin localhost:2019
}
${MATRIX_DOMAIN} {
@@ -370,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
}
}
@@ -382,6 +388,11 @@ ${MATRIX_DOMAIN} {
}
}
# Block public access to Synapse admin API
handle /_synapse/admin* {
respond "Forbidden" 403
}
handle {
reverse_proxy synapse:8008
}
@@ -391,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}
}
}
}
@@ -451,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
@@ -469,6 +497,8 @@ info "Starting all services..."
sudo docker compose --profile single-machine up -d ${CORE_SERVICES}
echo ""
fi
# ── Summary ───────────────────────────────────────────────────────────────────
ok "Stack is up."
+4
View File
@@ -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:
+161 -1
View File
@@ -209,6 +209,39 @@ assert_configs() {
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 ─────────────
@@ -223,6 +256,18 @@ curl_local() {
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.
@@ -293,10 +338,45 @@ assert_endpoints() {
fi
# MAS OIDC discovery
local oidc; oidc=$(curl_local "auth.example.test" "/.well-known/openid-configuration")
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" "/")
@@ -305,6 +385,40 @@ assert_endpoints() {
|| 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"
@@ -379,6 +493,52 @@ run_scenario \
"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