Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d9b156738 | |||
| 7353cae399 | |||
| 88806d12fb | |||
| 74b0a3ecb9 | |||
| 0799cd3e38 |
@@ -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
|
||||
@@ -1497,19 +1529,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,9 +1555,9 @@ 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 ""
|
||||
@@ -1555,8 +1592,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 +1604,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 +1647,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 +1668,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 +1686,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 +1768,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
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
+81
-1
@@ -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" "/")
|
||||
|
||||
Reference in New Issue
Block a user