Matrix Homeserver with MAS, Keycloak & Tailscale
Matrix Homeserver with MAS, Keycloak & Tailscale
A deep-dive into setting up a self-hosted Matrix homeserver with modern authentication, SSO via Keycloak, and QR code login support—all exposed securely through Tailscale Funnel.
The Goal
Get a Matrix homeserver running on a Raspberry Pi that:
- Is publicly accessible via Tailscale Funnel
- Uses Matrix Authentication Service (MAS) for modern OIDC-based auth
- Integrates with Keycloak for SSO
- Supports QR code login from Element iOS/Android
- Federates with other Matrix servers
Architecture Overview
Internet
│
▼
Tailscale Funnel (HTTPS :443)
│
▼
┌─────────────────────────────────────────────────────────┐
│ Raspberry Pi (thinkmeshmatrix.tail16ecc2.ts.net) │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ nginx-proxy │────▶│ Synapse │ │
│ │ (:8090) │ │ (:8008) │ │
│ └──────┬──────┘ └─────────────┘ │
│ │ │
│ ├────────────▶┌─────────────┐ │
│ │ │ MAS │ │
│ │ │ (:8080) │ │
│ │ └──────┬──────┘ │
│ │ │ │
│ │ ┌──────▼──────┐ │
│ │ │ mas-postgres │ │
│ │ └─────────────┘ │
│ │ │
│ └────────────▶┌─────────────┐ │
│ │ Keycloak │ │
│ │ (:8080) │ │
│ └─────────────┘ │
└─────────────────────────────────────────────────────────┘
The Journey (and the Hiccups)
1. Initial Tailscale Setup
Started by installing Tailscale directly on the Raspberry Pi running Synapse and enabling Funnel for public HTTPS access:
tailscale up --ssh
tailscale funnel 8090
Changed the Pi’s hostname to thinkmeshmatrix to reflect its purpose.
2. Federation Woes
Initial server was configured with server_name: matrix.local—which obviously can’t federate since remote servers can’t verify it.
Hiccup #1: Empty federation_domain_whitelist: [] was blocking ALL outbound federation. The fix was to comment out that line entirely.
Hiccup #2: Matrix federation uses port 8448 by default, but Tailscale Funnel serves on 443. Fixed by adding serve_server_wellknown: true to Synapse config, which tells remote servers to use port 443.
Had to reset the server with a new server_name:
server_name: "thinkmeshmatrix.tail16ecc2.ts.net"
3. Setting Up MAS (Matrix Authentication Service)
MAS provides modern OIDC-based authentication for Matrix, enabling features like:
- Delegated authentication to external providers
- Fine-grained session management
- QR code login (MSC4108)
Hiccup #3: MAS requires PostgreSQL, not SQLite. Deployed a postgres:16-alpine container:
docker run -d --name mas-postgres \
--network matrix-net \
-e POSTGRES_USER=mas \
-e POSTGRES_PASSWORD=maspassword \
-e POSTGRES_DB=mas \
-v mas-postgres-data:/var/lib/postgresql/data \
--restart unless-stopped \
postgres:16-alpine
4. Nginx Reverse Proxy Routing
The trickiest part was getting nginx to route requests correctly between Synapse, MAS, and Keycloak. Key insight: certain Matrix endpoints need to go to MAS, not Synapse:
# MAS endpoints
location /.well-known/openid-configuration { proxy_pass http://mas; }
location /oauth2/ { proxy_pass http://mas; }
location /authorize { proxy_pass http://mas; }
location /_matrix/client/v3/login { proxy_pass http://mas; }
location /complete-compat-sso/ { proxy_pass http://mas; }
# Keycloak endpoints
location /realms/ { proxy_pass http://keycloak; }
# Everything else to Synapse
location / { proxy_pass http://synapse; }
Hiccup #4: Kept getting “No Such Resource” errors during SSO flow. Turned out several MAS endpoints were missing from nginx config: /complete-compat-sso/, /link, /consent, /device.
5. Keycloak Integration
Set up Keycloak as the upstream identity provider for MAS. This allows using Keycloak’s user management and potentially federating with other identity providers later.
Hiccup #5: MAS was redirecting users to http://keycloak:8080/... (internal Docker hostname) instead of the public URL. Fixed by:
- Adding Keycloak routes to nginx
- Configuring Keycloak with
KC_HOSTNAME:
docker run -d --name keycloak \
-e KC_HOSTNAME=thinkmeshmatrix.tail16ecc2.ts.net \
-e KC_HTTP_ENABLED=true \
-e KC_PROXY_HEADERS=xforwarded \
...
- Updating MAS config to use public URL for issuer
Hiccup #6: MAS cached the old Keycloak discovery document. Had to change the provider ID to force a refresh.
6. User Provisioning Issues
Hiccup #7: “Localpart not available” error when trying to register via SSO. The user existed in Synapse’s database from a failed previous attempt, but not in MAS. Had to manually clean up:
# Stop Synapse
docker stop synapse
# Clean up orphaned user data
sudo sqlite3 /home/pi/matrix-synapse/homeserver.db \
"DELETE FROM users WHERE name = '@username:server';"
sudo sqlite3 /home/pi/matrix-synapse/homeserver.db \
"DELETE FROM profiles WHERE full_user_id LIKE '%username%';"
# Clean up MAS
docker exec mas-postgres psql -U mas -d mas -c \
"DELETE FROM upstream_oauth_authorization_sessions; DELETE FROM upstream_oauth_links;"
docker start synapse
7. Synapse Configuration Evolution
Hiccup #8: Synapse 1.136+ deprecated experimental_features.msc3861 in favor of a stable matrix_authentication_service config block:
# Old (deprecated)
experimental_features:
msc3861:
enabled: true
issuer: https://...
# New (stable)
matrix_authentication_service:
enabled: true
endpoint: http://mas:8080
secret: "shared-secret"
8. QR Code Login (MSC4108)
The final boss: getting QR code login working for Element iOS.
Hiccup #9: MSC4108 wasn’t being advertised. Had to add to Synapse config:
experimental_features:
msc4108_enabled: true
Hiccup #10: The rendezvous endpoint was incorrectly routed to MAS instead of Synapse. Synapse has a built-in rendezvous server at /_synapse/client/rendezvous. Removed the incorrect nginx route and let it fall through to Synapse.
Testing the rendezvous endpoint:
curl -X POST "https://server/_matrix/client/unstable/org.matrix.msc4108/rendezvous" \
-H "Content-Type: text/plain" -d 'test'
# Returns: {"url":"https://server/_synapse/client/rendezvous/SESSION_ID"}
Final Configuration
Synapse (homeserver.yaml)
server_name: "thinkmeshmatrix.tail16ecc2.ts.net"
public_baseurl: "https://thinkmeshmatrix.tail16ecc2.ts.net/"
serve_server_wellknown: true
allow_public_rooms_over_federation: true
matrix_authentication_service:
enabled: true
endpoint: http://mas:8080
secret: "shared-secret"
account_management_url: "https://thinkmeshmatrix.tail16ecc2.ts.net/account"
experimental_features:
msc4108_enabled: true
MAS (config.yaml)
http:
public_base: https://thinkmeshmatrix.tail16ecc2.ts.net/
issuer: https://thinkmeshmatrix.tail16ecc2.ts.net/
matrix:
homeserver: thinkmeshmatrix.tail16ecc2.ts.net
endpoint: http://synapse:8008
secret: "shared-secret"
upstream_oauth2:
providers:
- id: keycloak-provider
issuer: https://thinkmeshmatrix.tail16ecc2.ts.net/realms/matrix
client_id: mas
client_secret: "keycloak-client-secret"
claims_imports:
localpart:
action: require
template: "{{ user.preferred_username }}"
experimental:
qr_login:
enabled: true
Data Persistence & Recovery
All containers configured with:
--restart unless-stoppedfor automatic recovery after power outages- Proper volume mounts for data persistence:
| Service | Volume |
|---|---|
| Synapse | /home/pi/matrix-synapse → /data |
| MAS | /home/pi/mas → /config |
| MAS-Postgres | Docker volume mas-postgres-data |
| Keycloak | /home/pi/keycloak-data → /opt/keycloak/data |
| nginx-proxy | /home/pi/nginx-proxy → /etc/nginx/conf.d |
Lessons Learned
Docker networking is tricky - Internal hostnames (
keycloak:8080) work for server-to-server communication but not for browser redirects. Always use public URLs in OAuth flows.OIDC discovery caching - MAS caches discovery documents. When you change upstream provider URLs, you may need to force a refresh by changing provider IDs or restarting.
Database cleanup matters - Failed registration attempts can leave orphaned records in Synapse’s database that block future attempts. Clean up both the
usersandprofilestables.nginx routing order - More specific routes must come before catch-all routes. The MSC4108 rendezvous endpoint needs to go to Synapse, not MAS.
Synapse evolves fast - Configuration that works in one version may be deprecated in the next. The move from
experimental_features.msc3861tomatrix_authentication_servicecaught me off guard.
Current Status
- ✅ Matrix homeserver running at
thinkmeshmatrix.tail16ecc2.ts.net - ✅ Federation working with matrix.org and other servers
- ✅ SSO via Keycloak operational
- ✅ QR code login (MSC4108) functional
- ✅ Automatic restart after power outages
- ✅ Data persistence across container restarts