fix: synapse

This commit is contained in:
Dominik Polakovics Polakovics 2026-03-02 08:25:58 +01:00
parent 7ecb772efd
commit 61801639fd
5 changed files with 112 additions and 425 deletions

View file

@ -10,18 +10,20 @@ let
add_header Access-Control-Allow-Origin *;
return 200 '${builtins.toJSON data}';
'';
# Shared settings format for bridges
settingsFormat = pkgs.formats.json {};
in {
# Secrets for Synapse
sops.secrets.synapse-oidc-client-secret = {
owner = "matrix-synapse";
};
sops.secrets.mautrix-whatsapp-env = { };
sops.secrets.mautrix-signal-env = { };
sops.secrets.mautrix-discord-env = { };
# PostgreSQL database for Synapse
services.postgresql = {
enable = true;
# Synapse requires C locale for correct collation behavior
initdbArgs = [ "--lc-collate=C" "--lc-ctype=C" ];
ensureDatabases = [ "matrix-synapse" ];
ensureUsers = [
{
@ -84,28 +86,20 @@ in {
allow_existing_users = true;
user_mapping_provider.config = {
subject_claim = "sub";
localpart_template = "{{ user.preferred_username }}";
localpart_template = "{{ user.email | localpart_from_email }}";
display_name_template = "{{ user.name }}";
email_template = "{{ user.email }}";
};
}
];
# Appservice registrations for bridges
app_service_config_files = [
"/var/lib/mautrix-whatsapp/whatsapp-registration.yaml"
"/var/lib/mautrix-signal/signal-registration.yaml"
"/var/lib/mautrix-discord/discord-registration.yaml"
];
};
};
# Allow bridge users to read registration files
systemd.services.matrix-synapse.serviceConfig.SupplementaryGroups = [
"mautrix-whatsapp"
"mautrix-signal"
"mautrix-discord"
];
# 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).
systemd.services.matrix-synapse.serviceConfig.PrivateUsers = lib.mkForce false;
# Element Web client
services.nginx.virtualHosts."element.cloonar.com" = {
@ -136,414 +130,98 @@ in {
locations."/".extraConfig = ''
return 404;
'';
locations."= /.well-known/matrix/server".extraConfig = mkWellKnown serverConfig;
locations."= /.well-known/matrix/client".extraConfig = mkWellKnown clientConfig;
locations."/_matrix".proxyPass = "http://[::1]:8008";
locations."/_synapse/client".proxyPass = "http://[::1]:8008";
};
#
# Mautrix bridges
# Mautrix bridges (using NixOS modules)
# Modules handle users, groups, registration files, Synapse integration,
# and service ordering automatically via registerToSynapse.
#
# WhatsApp bridge
users.users.mautrix-whatsapp = {
isSystemUser = true;
group = "mautrix-whatsapp";
home = "/var/lib/mautrix-whatsapp";
description = "Mautrix-WhatsApp bridge user";
};
users.groups.mautrix-whatsapp = {};
systemd.services.mautrix-whatsapp = let
dataDir = "/var/lib/mautrix-whatsapp";
registrationFile = "${dataDir}/whatsapp-registration.yaml";
settingsFile = "${dataDir}/config.json";
settingsFileUnsubstituted = settingsFormat.generate "mautrix-whatsapp-config-unsubstituted.json" defaultConfig;
appservicePort = 29318;
defaultConfig = {
services.mautrix-whatsapp = {
enable = true;
registerToSynapse = true;
environmentFile = config.sops.secrets.mautrix-whatsapp-env.path;
settings = {
homeserver = {
address = "http://[::1]:8008";
domain = "cloonar.com";
};
appservice = {
hostname = "[::]";
port = appservicePort;
database.type = "sqlite3";
database.uri = "${dataDir}/mautrix-whatsapp.db";
id = "whatsapp";
bot.username = "whatsappbot";
bot.displayname = "WhatsApp Bridge Bot";
as_token = "";
hs_token = "";
};
bridge = {
username_template = "whatsapp_{{.}}";
displayname_template = "{{if .BusinessName}}{{.BusinessName}}{{else if .PushName}}{{.PushName}}{{else}}{{.JID}}{{end}} (WA)";
double_puppet_server_map = {};
login_shared_secret_map = {};
command_prefix = "!wa";
permissions."*" = "relay";
permissions."cloonar.com" = "user";
relay.enabled = true;
history_sync.request_full_sync = false;
encryption = {
allow = true;
default = true;
require = true;
};
};
logging = {
min_level = "info";
writers = lib.singleton {
type = "stdout";
format = "pretty-colored";
time_format = " ";
};
encryption = {
allow = true;
default = true;
require = true;
pickle_key = "$MAUTRIX_WHATSAPP_PICKLE_KEY";
};
};
in {
description = "Mautrix-WhatsApp Service - A WhatsApp bridge for Matrix";
wantedBy = ["multi-user.target"];
wants = ["network-online.target" "matrix-synapse.service"];
after = ["network-online.target" "matrix-synapse.service"];
preStart = ''
test -f '${settingsFile}' && rm -f '${settingsFile}'
old_umask=$(umask)
umask 0177
${pkgs.envsubst}/bin/envsubst \
-o '${settingsFile}' \
-i '${settingsFileUnsubstituted}'
umask $old_umask
# generate the appservice's registration file if absent
if [ ! -f '${registrationFile}' ]; then
${pkgs.mautrix-whatsapp}/bin/mautrix-whatsapp \
--generate-registration \
--config='${settingsFile}' \
--registration='${registrationFile}'
fi
chmod 640 ${registrationFile}
umask 0177
${pkgs.yq}/bin/yq -s '.[0].appservice.as_token = .[1].as_token
| .[0].appservice.hs_token = .[1].hs_token
| .[0]' '${settingsFile}' '${registrationFile}' \
> '${settingsFile}.tmp'
mv '${settingsFile}.tmp' '${settingsFile}'
umask $old_umask
'';
serviceConfig = {
User = "mautrix-whatsapp";
Group = "mautrix-whatsapp";
StateDirectory = baseNameOf dataDir;
WorkingDirectory = dataDir;
ExecStart = ''
${pkgs.mautrix-whatsapp}/bin/mautrix-whatsapp \
--config='${settingsFile}' \
--registration='${registrationFile}' \
--ignore-unsupported-server
'';
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = "strict";
Restart = "on-failure";
RestartSec = "30s";
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallErrorNumber = "EPERM";
SystemCallFilter = ["@system-service"];
Type = "simple";
UMask = 0027;
};
restartTriggers = [settingsFileUnsubstituted];
};
# Signal bridge
users.users.mautrix-signal = {
isSystemUser = true;
group = "mautrix-signal";
home = "/var/lib/mautrix-signal";
description = "Mautrix-Signal bridge user";
};
users.groups.mautrix-signal = {};
systemd.services.mautrix-signal = let
pkgswithsignal = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/fd698a4ab779fb7fb95425f1b56974ba9c2fa16c.tar.gz") {
config = {
permittedInsecurePackages = [
"olm-3.2.16"
];
};
};
dataDir = "/var/lib/mautrix-signal";
registrationFile = "${dataDir}/signal-registration.yaml";
settingsFile = "${dataDir}/config.json";
settingsFileUnsubstituted = settingsFormat.generate "mautrix-signal-config-unsubstituted.json" defaultConfig;
appservicePort = 29328;
defaultConfig = {
services.mautrix-signal = {
enable = true;
registerToSynapse = true;
environmentFile = config.sops.secrets.mautrix-signal-env.path;
settings = {
homeserver = {
address = "http://[::1]:8008";
domain = "cloonar.com";
};
appservice = {
hostname = "[::]";
port = appservicePort;
database.type = "sqlite3";
database.uri = "file:${dataDir}/mautrix-signal.db?_txlock=immediate";
id = "signal";
bot = {
username = "signalbot";
displayname = "Signal Bridge Bot";
};
as_token = "";
hs_token = "";
};
bridge = {
username_template = "signal_{{.}}";
displayname_template = "{{or .ProfileName .PhoneNumber \"Unknown user\"}} (Signal)";
double_puppet_server_map = { };
login_shared_secret_map = { };
command_prefix = "!signal";
permissions."*" = "relay";
permissions."cloonar.com" = "user";
relay.enabled = true;
encryption = {
allow = true;
default = true;
require = true;
};
};
matrix = {
sync_direct_chat_list = true;
};
logging = {
min_level = "info";
writers = lib.singleton {
type = "stdout";
format = "pretty-colored";
time_format = " ";
};
encryption = {
allow = true;
default = true;
require = true;
pickle_key = "$MAUTRIX_SIGNAL_PICKLE_KEY";
};
matrix.sync_direct_chat_list = true;
};
in {
description = "Mautrix-Signal Service - A Signal bridge for Matrix";
wantedBy = ["multi-user.target"];
wants = ["network-online.target" "matrix-synapse.service"];
after = ["network-online.target" "matrix-synapse.service"];
preStart = ''
test -f '${settingsFile}' && rm -f '${settingsFile}'
old_umask=$(umask)
umask 0177
${pkgs.envsubst}/bin/envsubst \
-o '${settingsFile}' \
-i '${settingsFileUnsubstituted}'
umask $old_umask
# generate the appservice's registration file if absent
if [ ! -f '${registrationFile}' ]; then
${pkgswithsignal.mautrix-signal}/bin/mautrix-signal \
--generate-registration \
--config='${settingsFile}' \
--registration='${registrationFile}'
fi
chmod 640 ${registrationFile}
umask 0177
${pkgs.yq}/bin/yq -s '.[0].appservice.as_token = .[1].as_token
| .[0].appservice.hs_token = .[1].hs_token
| .[0]
| if env.MAUTRIX_SIGNAL_BRIDGE_LOGIN_SHARED_SECRET then .bridge.login_shared_secret_map.[.homeserver.domain] = env.MAUTRIX_SIGNAL_BRIDGE_LOGIN_SHARED_SECRET else . end' \
'${settingsFile}' '${registrationFile}' > '${settingsFile}.tmp'
mv '${settingsFile}.tmp' '${settingsFile}'
umask $old_umask
'';
serviceConfig = {
User = "mautrix-signal";
Group = "mautrix-signal";
StateDirectory = baseNameOf dataDir;
WorkingDirectory = dataDir;
ExecStart = ''
${pkgswithsignal.mautrix-signal}/bin/mautrix-signal \
--config='${settingsFile}' \
--registration='${registrationFile}' \
--ignore-unsupported-server
'';
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = "strict";
Restart = "on-failure";
RestartSec = "30s";
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallErrorNumber = "EPERM";
SystemCallFilter = ["@system-service"];
Type = "simple";
UMask = 0027;
};
restartTriggers = [settingsFileUnsubstituted];
};
# Discord bridge
users.users.mautrix-discord = {
isSystemUser = true;
group = "mautrix-discord";
home = "/var/lib/mautrix-discord";
description = "Mautrix-Discord bridge user";
};
users.groups.mautrix-discord = {};
systemd.services.mautrix-discord = let
pkgswithdiscord = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/5ed627539ac84809c78b2dd6d26a5cebeb5ae269.tar.gz") {
config = {
permittedInsecurePackages = [
"olm-3.2.16"
];
};
};
dataDir = "/var/lib/mautrix-discord";
registrationFile = "${dataDir}/discord-registration.yaml";
settingsFile = "${dataDir}/config.json";
settingsFileUnsubstituted = settingsFormat.generate "mautrix-discord-config-unsubstituted.json" defaultConfig;
appservicePort = 29329;
defaultConfig = {
services.mautrix-discord = {
enable = true;
registerToSynapse = true;
environmentFile = config.sops.secrets.mautrix-discord-env.path;
settings = {
homeserver = {
address = "http://[::1]:8008";
domain = "cloonar.com";
};
appservice = {
hostname = "[::]";
port = appservicePort;
database.type = "sqlite3";
database.uri = "file:${dataDir}/mautrix-discord.db?_txlock=immediate";
id = "discord";
bot = {
username = "discordbot";
displayname = "Discord Bridge Bot";
};
as_token = "";
hs_token = "";
};
bridge = {
username_template = "discord_{{.}}";
displayname_template = "{{or .GlobalName .Username}} (Discord{{if .Bot}} bot{{end}})";
double_puppet_server_map = { };
login_shared_secret_map = { };
command_prefix = "!discord";
permissions."*" = "relay";
permissions."cloonar.com" = "user";
relay.enabled = true;
restricted_rooms = false;
encryption = {
allow = true;
default = true;
require = true;
};
};
logging = {
min_level = "info";
writers = lib.singleton {
type = "stdout";
format = "pretty-colored";
time_format = " ";
};
# 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";
};
};
in {
description = "Mautrix-Discord Service - A Discord bridge for Matrix";
wantedBy = ["multi-user.target"];
wants = ["network-online.target" "matrix-synapse.service"];
after = ["network-online.target" "matrix-synapse.service"];
preStart = ''
test -f '${settingsFile}' && rm -f '${settingsFile}'
old_umask=$(umask)
umask 0177
${pkgs.envsubst}/bin/envsubst \
-o '${settingsFile}' \
-i '${settingsFileUnsubstituted}'
umask $old_umask
# generate the appservice's registration file if absent
if [ ! -f '${registrationFile}' ]; then
${pkgswithdiscord.mautrix-discord}/bin/mautrix-discord \
--generate-registration \
--config='${settingsFile}' \
--registration='${registrationFile}'
fi
chmod 640 ${registrationFile}
umask 0177
${pkgs.yq}/bin/yq -s '.[0].appservice.as_token = .[1].as_token
| .[0].appservice.hs_token = .[1].hs_token
| .[0]
| if env.MAUTRIX_DISCORD_BRIDGE_LOGIN_SHARED_SECRET then .bridge.login_shared_secret_map.[.homeserver.domain] = env.MAUTRIX_DISCORD_BRIDGE_LOGIN_SHARED_SECRET else . end' \
'${settingsFile}' '${registrationFile}' > '${settingsFile}.tmp'
mv '${settingsFile}.tmp' '${settingsFile}'
umask $old_umask
'';
serviceConfig = {
User = "mautrix-discord";
Group = "mautrix-discord";
StateDirectory = baseNameOf dataDir;
WorkingDirectory = dataDir;
ExecStart = ''
${pkgswithdiscord.mautrix-discord}/bin/mautrix-discord \
--config='${settingsFile}' \
--registration='${registrationFile}'
'';
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = "strict";
Restart = "on-failure";
RestartSec = "30s";
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallErrorNumber = "EPERM";
SystemCallFilter = ["@system-service"];
Type = "simple";
UMask = 0027;
};
restartTriggers = [settingsFileUnsubstituted];
};
}