nixos/hosts/fw/modules/web/matrix.nix

375 lines
11 KiB
Nix

{ 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";
};
};
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}
'';
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";
};
sops.secrets.mautrix-whatsapp-env = { };
sops.secrets.mautrix-signal-env = { };
sops.secrets.mautrix-discord-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 <<MASEOF
http:
public_base: ${baseUrl}/
listeners:
- name: web
resources:
- name: discovery
- name: human
- name: oauth
- name: compat
- name: graphql
- name: assets
binds:
- address: "127.0.0.1:8081"
database:
uri: postgresql:///mas?host=/run/postgresql
matrix:
homeserver: cloonar.com
endpoint: "http://[::1]:8008"
secret: "$MATRIX_SECRET"
upstream_oauth2:
providers:
- id: ${masUpstreamId}
synapse_idp_id: oidc-authelia
human_name: Authelia
issuer: https://auth.cloonar.com
client_id: synapse
client_secret: "$CLIENT_SECRET"
token_endpoint_auth_method: client_secret_post
scope: "openid email profile"
claims_imports:
localpart:
action: force
template: "{{ user.email | split('@') | first }}"
displayname:
action: suggest
template: "{{ user.name }}"
email:
action: force
template: "{{ user.email }}"
set_email_verification: always
clients:
- client_id: ${elementWebClientId}
client_auth_method: none
redirect_uris:
- https://element.cloonar.com/
- https://element.cloonar.com/?no_universal_links=true
passwords:
enabled: true
schemes:
- version: 1
algorithm: bcrypt
secrets:
encryption: "$ENCRYPTION_KEY"
keys:
- kid: mas-rsa-key
key_file: ${config.sops.secrets.mas-rsa-key.path}
telemetry:
tracing:
exporter: none
metrics:
exporter: none
MASEOF
'';
};
# Synapse homeserver
services.matrix-synapse = {
enable = true;
extraConfigFiles = [ "${synapseMasConfig}" ];
settings = {
server_name = "cloonar.com";
public_baseurl = baseUrl;
listeners = [
{
port = 8008;
bind_addresses = [ "::1" ];
type = "http";
tls = false;
x_forwarded = true;
resources = [
{
compress = true;
names = [ "client" "federation" ];
}
];
}
];
database = {
name = "psycopg2";
args = {
host = "/run/postgresql";
database = "matrix-synapse";
user = "matrix-synapse";
};
};
allow_guest_access = false;
};
};
# Synapse runs inside an isolated microVM, so PrivateUsers provides minimal
# additional security. Disabling it allows Synapse to read bridge registration
# files via SupplementaryGroups (user namespace blocks mapped GIDs otherwise).
# Synapse depends on MAS for auth delegation
systemd.services.matrix-synapse.after = [ "matrix-authentication-service.service" ];
systemd.services.matrix-synapse.wants = [ "matrix-authentication-service.service" ];
systemd.services.matrix-synapse.serviceConfig.PrivateUsers = lib.mkForce false;
# 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";
};
};
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";
# Synapse endpoints
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://[::1]:8008";
domain = "cloonar.com";
};
bridge = {
command_prefix = "!wa";
permissions."*" = "relay";
permissions."cloonar.com" = "user";
relay.enabled = true;
};
encryption = {
allow = true;
default = true;
require = true;
pickle_key = "$MAUTRIX_WHATSAPP_PICKLE_KEY";
};
};
};
# Signal bridge
services.mautrix-signal = {
enable = true;
registerToSynapse = true;
environmentFile = config.sops.secrets.mautrix-signal-env.path;
settings = {
homeserver = {
address = "http://[::1]:8008";
domain = "cloonar.com";
};
bridge = {
command_prefix = "!signal";
permissions."*" = "relay";
permissions."cloonar.com" = "user";
relay.enabled = true;
};
encryption = {
allow = true;
default = true;
require = true;
pickle_key = "$MAUTRIX_SIGNAL_PICKLE_KEY";
};
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://[::1]:8008";
domain = "cloonar.com";
};
bridge = {
command_prefix = "!discord";
permissions."*" = "relay";
permissions."cloonar.com" = "user";
relay.enabled = true;
};
# Override dummy token defaults so env var substitution writes real tokens
# into the config and registration file (module defaults are placeholder strings)
appservice = {
as_token = "$MAUTRIX_DISCORD_AS_TOKEN";
hs_token = "$MAUTRIX_DISCORD_HS_TOKEN";
};
encryption = {
allow = true;
default = true;
require = true;
pickle_key = "$MAUTRIX_DISCORD_PICKLE_KEY";
};
};
};
}