{ pkgs, lib, config, ... }: let hostname = "matrix"; fqdn = "${hostname}.cloonar.com"; baseUrl = "https://${fqdn}"; clientConfig = { "m.homeserver".base_url = baseUrl; # MAS auth issuer discovery (MSC2965) "org.matrix.msc2965.authentication" = { issuer = baseUrl + "/"; account = baseUrl + "/account"; }; # MatrixRTC LiveKit focus for Element Call "org.matrix.msc4143.rtc_foci" = [ { type = "livekit"; livekit_service_url = "${baseUrl}/livekit/jwt"; } ]; }; serverConfig."m.server" = "${fqdn}:443"; mkWellKnown = data: '' default_type application/json; add_header Access-Control-Allow-Origin *; return 200 '${builtins.toJSON data}'; ''; masUpstreamId = "01KJPRKN397E5N8D0CA2Z3TJ7Y"; elementWebClientId = "01KJPVT5D54NRAY7AJY6PZEN0D"; masPackage = pkgs.matrix-authentication-service; synapseMasConfig = pkgs.writeText "synapse-mas-config.yaml" '' matrix_authentication_service: enabled: true endpoint: "http://127.0.0.1:8081" secret_path: ${config.sops.secrets.mas-matrix-secret-synapse.path} ''; synapseVoipConfig = "/run/matrix-synapse/voip-config.yaml"; doublePuppetRegistration = "/run/matrix-synapse/double-puppet-registration.yaml"; in { # Secrets for MAS sops.secrets.mas-encryption-key = { owner = "mas"; }; sops.secrets.mas-matrix-secret = { owner = "mas"; }; sops.secrets.mas-authelia-client-secret = { owner = "mas"; }; sops.secrets.mas-rsa-key = { owner = "mas"; }; # Synapse also needs the shared secret sops.secrets.mas-matrix-secret-synapse = { owner = "matrix-synapse"; key = "mas-matrix-secret"; }; # Double puppet appservice tokens (for bridge double puppeting via MAS) sops.secrets.double-puppet-as-token = { owner = "matrix-synapse"; }; sops.secrets.double-puppet-hs-token = { owner = "matrix-synapse"; }; # TURN shared secret (for Synapse VoIP config) sops.secrets.coturn-static-secret = { sopsFile = ./secrets.yaml; owner = "matrix-synapse"; }; sops.secrets.mautrix-whatsapp-env = { }; sops.secrets.mautrix-signal-env = { }; sops.secrets.mautrix-discord-env = { }; sops.secrets.mautrix-mattermost-env = { }; # MAS system user users.users.mas = { isSystemUser = true; group = "mas"; home = "/var/lib/mas"; }; users.groups.mas = { }; # PostgreSQL databases for Synapse and MAS services.postgresql = { enable = true; # Synapse requires C locale for correct collation behavior initdbArgs = [ "--lc-collate=C" "--lc-ctype=C" ]; ensureDatabases = [ "matrix-synapse" "mas" ]; ensureUsers = [ { name = "matrix-synapse"; ensureDBOwnership = true; } { name = "mas"; ensureDBOwnership = true; } ]; }; services.postgresqlBackup.enable = true; services.postgresqlBackup.databases = [ "matrix-synapse" "mas" ]; # Matrix Authentication Service (MAS) systemd.services.matrix-authentication-service = { description = "Matrix Authentication Service"; after = [ "postgresql.service" "network.target" ]; before = [ "matrix-synapse.service" ]; wantedBy = [ "multi-user.target" ]; serviceConfig = { Type = "simple"; User = "mas"; Group = "mas"; RuntimeDirectory = "mas"; RuntimeDirectoryMode = "0755"; StateDirectory = "mas"; StateDirectoryMode = "0750"; ExecStart = "${masPackage}/bin/mas-cli server --config /run/mas/config.yaml"; Restart = "on-failure"; RestartSec = "5s"; }; preStart = '' # Read secrets from SOPS-managed files ENCRYPTION_KEY=$(cat ${config.sops.secrets.mas-encryption-key.path}) MATRIX_SECRET=$(cat ${config.sops.secrets.mas-matrix-secret.path}) CLIENT_SECRET=$(cat ${config.sops.secrets.mas-authelia-client-secret.path}) # Write MAS config with secrets interpolated cat > /run/mas/config.yaml < ${synapseVoipConfig} < ${doublePuppetRegistration} < /run/double-puppet-env chmod 0444 /run/double-puppet-env ''; }; # Append double puppet env file to each bridge's EnvironmentFile # WhatsApp & Signal: envsubst runs in main service preStart systemd.services.mautrix-whatsapp.serviceConfig.EnvironmentFile = [ "/run/double-puppet-env" ]; systemd.services.mautrix-signal.serviceConfig.EnvironmentFile = [ "/run/double-puppet-env" ]; # Discord & Mattermost: envsubst runs in registration service script systemd.services.mautrix-discord-registration.serviceConfig.EnvironmentFile = [ "/run/double-puppet-env" ]; systemd.services.mautrix-mattermost-registration.serviceConfig.EnvironmentFile = [ "/run/double-puppet-env" ]; # Element Web client services.nginx.virtualHosts."element.cloonar.com" = { forceSSL = true; enableACME = true; acmeRoot = null; root = pkgs.element-web.override { conf = { default_theme = "dark"; default_server_config = { "m.homeserver" = { base_url = "https://matrix.cloonar.com"; server_name = "cloonar.com"; }; "org.matrix.msc2965.authentication" = { issuer = "https://matrix.cloonar.com/"; account = "https://matrix.cloonar.com/account"; }; "org.matrix.msc4143.rtc_foci" = [ { type = "livekit"; livekit_service_url = "https://matrix.cloonar.com/livekit/jwt"; } ]; }; oidc_static_clients = { "https://matrix.cloonar.com/" = { client_id = elementWebClientId; }; }; disable_custom_urls = true; disable_3pid_login = true; default_country_code = "AT"; }; }; }; # Synapse + MAS nginx reverse proxy services.nginx.virtualHosts."${fqdn}" = { forceSSL = true; enableACME = true; acmeRoot = null; locations."/".extraConfig = '' return 404; ''; locations."= /.well-known/matrix/server".extraConfig = mkWellKnown serverConfig; locations."= /.well-known/matrix/client".extraConfig = mkWellKnown clientConfig; # MAS compatibility endpoints (must be before /_matrix catch-all) locations."~ ^/_matrix/client/(r0|v3)/login".proxyPass = "http://127.0.0.1:8081"; locations."~ ^/_matrix/client/(r0|v3)/logout".proxyPass = "http://127.0.0.1:8081"; locations."~ ^/_matrix/client/(r0|v3)/refresh$".proxyPass = "http://127.0.0.1:8081"; # MAS own endpoints locations."/authorize".proxyPass = "http://127.0.0.1:8081"; locations."/oauth2".proxyPass = "http://127.0.0.1:8081"; locations."/.well-known/openid-configuration".proxyPass = "http://127.0.0.1:8081"; locations."/.well-known/webfinger".proxyPass = "http://127.0.0.1:8081"; locations."/assets".proxyPass = "http://127.0.0.1:8081"; locations."/graphql".proxyPass = "http://127.0.0.1:8081"; locations."/account".proxyPass = "http://127.0.0.1:8081"; locations."/upstream".proxyPass = "http://127.0.0.1:8081"; locations."/register".proxyPass = "http://127.0.0.1:8081"; locations."/consent".proxyPass = "http://127.0.0.1:8081"; locations."/recovery".proxyPass = "http://127.0.0.1:8081"; locations."/login".proxyPass = "http://127.0.0.1:8081"; locations."/change-password".proxyPass = "http://127.0.0.1:8081"; locations."/complete-compat-sso".proxyPass = "http://127.0.0.1:8081"; locations."/logout".proxyPass = "http://127.0.0.1:8081"; # LiveKit JWT service for MatrixRTC locations."^~ /livekit/jwt/" = { proxyPass = "http://127.0.0.1:8082/"; }; # LiveKit SFU WebSocket locations."^~ /livekit/sfu/" = { proxyPass = "http://127.0.0.1:7880/"; proxyWebsockets = true; extraConfig = '' proxy_send_timeout 120; proxy_read_timeout 120; proxy_buffering off; ''; }; # Synapse endpoints locations."/_matrix".proxyPass = "http://[::1]:8008"; locations."/_synapse/client" = { proxyPass = "http://[::1]:8008"; extraConfig = '' # MSC4108 rendezvous relies on strong ETag comparison; # gzip can break it, so disable compression here. gzip off; ''; }; locations."/_synapse/mas".proxyPass = "http://[::1]:8008"; }; # Internal proxy for bridges: routes login/auth to MAS, everything else to Synapse. # Bridges connect here instead of directly to Synapse, which no longer serves # /_matrix/client/v3/login when MAS is enabled. services.nginx.virtualHosts."matrix-internal" = { listen = [{ addr = "127.0.0.1"; port = 8009; }]; locations."~ ^/_matrix/client/(r0|v3)/login".proxyPass = "http://127.0.0.1:8081"; locations."~ ^/_matrix/client/(r0|v3)/logout".proxyPass = "http://127.0.0.1:8081"; locations."~ ^/_matrix/client/(r0|v3)/refresh$".proxyPass = "http://127.0.0.1:8081"; locations."/_matrix".proxyPass = "http://[::1]:8008"; locations."/_synapse/client".proxyPass = "http://[::1]:8008"; }; # # Mautrix bridges (using NixOS modules) # Modules handle users, groups, registration files, Synapse integration, # and service ordering automatically via registerToSynapse. # # WhatsApp bridge services.mautrix-whatsapp = { enable = true; registerToSynapse = true; environmentFile = config.sops.secrets.mautrix-whatsapp-env.path; settings = { homeserver = { address = "http://127.0.0.1:8009"; domain = "cloonar.com"; }; bridge = { command_prefix = "!wa"; permissions."*" = "relay"; permissions."cloonar.com" = "user"; relay.enabled = true; }; double_puppet = { servers."cloonar.com" = "http://127.0.0.1:8009"; secrets."cloonar.com" = "as_token:$DOUBLE_PUPPET_AS_TOKEN"; }; encryption = { allow = true; default = true; require = true; self_sign = true; pickle_key = "$MAUTRIX_WHATSAPP_PICKLE_KEY"; msc4190 = true; }; }; }; # Signal bridge services.mautrix-signal = { enable = true; registerToSynapse = true; environmentFile = config.sops.secrets.mautrix-signal-env.path; settings = { homeserver = { address = "http://127.0.0.1:8009"; domain = "cloonar.com"; }; bridge = { command_prefix = "!signal"; permissions."*" = "relay"; permissions."cloonar.com" = "user"; relay.enabled = true; }; double_puppet = { servers."cloonar.com" = "http://127.0.0.1:8009"; secrets."cloonar.com" = "as_token:$DOUBLE_PUPPET_AS_TOKEN"; }; encryption = { allow = true; default = true; require = true; self_sign = true; pickle_key = "$MAUTRIX_SIGNAL_PICKLE_KEY"; msc4190 = true; }; matrix.sync_direct_chat_list = true; }; }; # Discord bridge services.mautrix-discord = { enable = true; registerToSynapse = true; environmentFile = config.sops.secrets.mautrix-discord-env.path; settings = { homeserver = { address = "http://127.0.0.1:8009"; domain = "cloonar.com"; }; bridge = { command_prefix = "!discord"; permissions."*" = "relay"; permissions."cloonar.com" = "user"; relay.enabled = true; }; double_puppet = { servers."cloonar.com" = "http://127.0.0.1:8009"; secrets."cloonar.com" = "as_token:$DOUBLE_PUPPET_AS_TOKEN"; }; # Override token defaults so env var substitution writes real tokens. # Must include database/address/port since setting appservice replaces the whole default. appservice = { address = "http://localhost:29334"; hostname = "0.0.0.0"; port = 29334; database = { type = "sqlite3"; uri = "file:/var/lib/mautrix-discord/mautrix-discord.db?_txlock=immediate"; }; id = "discord"; bot.username = "discordbot"; as_token = "$MAUTRIX_DISCORD_AS_TOKEN"; hs_token = "$MAUTRIX_DISCORD_HS_TOKEN"; }; encryption = { allow = true; default = true; require = true; self_sign = true; pickle_key = "$MAUTRIX_DISCORD_PICKLE_KEY"; msc4190 = true; }; }; }; # # LiveKit SFU + JWT service for MatrixRTC video/voice calls # # LiveKit SFU — handles WebRTC media relay services.livekit = { enable = true; openFirewall = true; keyFile = "/run/livekit/key"; settings = { rtc = { port_range_start = 50000; port_range_end = 50200; use_external_ip = true; }; room.auto_create = false; }; }; # JWT service — validates Matrix OpenID tokens and issues LiveKit JWTs services.lk-jwt-service = { enable = true; livekitUrl = "wss://${fqdn}/livekit/sfu"; port = 8082; keyFile = "/run/livekit/key"; }; # Only allow cloonar.com users to create LiveKit rooms systemd.services.lk-jwt-service.environment.LIVEKIT_FULL_ACCESS_HOMESERVERS = "cloonar.com"; # Generate LiveKit API key on boot systemd.services.livekit-key-generate = { description = "Generate LiveKit API key"; before = [ "livekit.service" "lk-jwt-service.service" ]; requiredBy = [ "livekit.service" "lk-jwt-service.service" ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; script = '' if [ ! -f /run/livekit/key ]; then mkdir -p /run/livekit SECRET=$(${pkgs.openssl}/bin/openssl rand -hex 32) echo "lk-jwt-service: $SECRET" > /run/livekit/key chmod 0644 /run/livekit/key fi ''; }; # Mattermost bridge (bridgev2 — attrs replace entirely, so include all needed fields) services.mautrix-mattermost = { enable = true; registerToSynapse = true; environmentFile = config.sops.secrets.mautrix-mattermost-env.path; settings = { homeserver = { address = "http://127.0.0.1:8009"; domain = "cloonar.com"; }; bridge = { command_prefix = "!mm"; permissions."*" = "relay"; permissions."cloonar.com" = "user"; relay.enabled = true; }; double_puppet = { servers."cloonar.com" = "http://127.0.0.1:8009"; secrets."cloonar.com" = "as_token:$DOUBLE_PUPPET_AS_TOKEN"; }; appservice = { address = "http://localhost:29335"; hostname = "0.0.0.0"; port = 29335; id = "mattermost"; bot.username = "mattermostbot"; ephemeral_events = true; username_template = "mattermost_{{.}}"; as_token = "$MAUTRIX_MATTERMOST_AS_TOKEN"; hs_token = "$MAUTRIX_MATTERMOST_HS_TOKEN"; }; encryption = { allow = true; default = true; require = true; self_sign = true; pickle_key = "$MAUTRIX_MATTERMOST_PICKLE_KEY"; msc4190 = true; }; }; }; }