Matrix packages (mautrix-*, matrix-continuwuity) only exist in nixpkgs-unstable, not in nixpkgs 24.05 stable. This commit updates all module defaults and references to use pkgs-unstable. Changes: - Add pkgs-unstable to module function signatures (4 modules) - Update package option defaults from pkgs.* to pkgs-unstable.* - Configure pkgs-unstable in flake.nix to permit olm-3.2.16 - Add VM config permittedInsecurePackages for olm (mautrix dependency) The olm library is deprecated with known CVEs but required by mautrix bridges. This is acceptable for testing; production should migrate to newer cryptography implementations when available. This maintains our stable base system (NixOS 24.05) while using unstable only for Matrix ecosystem packages under active development.
393 lines
11 KiB
Nix
393 lines
11 KiB
Nix
# mautrix-slack Matrix-Slack bridge
|
|
# Bridges Slack to Matrix via appservice
|
|
# Implementation follows mautrix-gmessages pattern for config management
|
|
{ config, pkgs, pkgs-unstable, lib, ... }:
|
|
|
|
with lib;
|
|
|
|
let
|
|
cfg = config.services.mautrix-slack;
|
|
yamlFormat = pkgs.formats.yaml { };
|
|
|
|
exampleConfigPath = "${cfg.package}/example-config.yaml";
|
|
|
|
configOverrides = {
|
|
homeserver = {
|
|
address = cfg.matrix.homeserverUrl;
|
|
domain = cfg.matrix.serverName;
|
|
};
|
|
appservice = {
|
|
address = "http://${cfg.appservice.hostname}:${toString cfg.appservice.port}";
|
|
hostname = cfg.appservice.hostname;
|
|
port = cfg.appservice.port;
|
|
id = cfg.appservice.id;
|
|
bot = {
|
|
username = cfg.appservice.senderLocalpart;
|
|
displayname = cfg.appservice.botDisplayName;
|
|
avatar = cfg.appservice.botAvatar;
|
|
};
|
|
username_template = "${cfg.appservice.userPrefix}{{.}}";
|
|
};
|
|
database = {
|
|
type = cfg.database.type;
|
|
uri = cfg.database.uri;
|
|
max_open_conns = cfg.database.maxOpenConnections;
|
|
max_idle_conns = cfg.database.maxIdleConnections;
|
|
};
|
|
bridge = {
|
|
command_prefix = cfg.bridge.commandPrefix;
|
|
permissions = cfg.bridge.permissions;
|
|
};
|
|
encryption = {
|
|
allow = cfg.encryption.enable;
|
|
default = cfg.encryption.default;
|
|
require = cfg.encryption.require;
|
|
};
|
|
logging = {
|
|
min_level = cfg.logging.level;
|
|
};
|
|
};
|
|
|
|
in
|
|
{
|
|
options.services.mautrix-slack = {
|
|
enable = mkEnableOption "mautrix-slack Matrix-Slack bridge";
|
|
|
|
package = mkOption {
|
|
type = types.package;
|
|
default = pkgs-unstable.mautrix-slack;
|
|
description = "Package providing the bridge executable.";
|
|
};
|
|
|
|
user = mkOption {
|
|
type = types.str;
|
|
default = "mautrix_slack";
|
|
description = "System user for the bridge.";
|
|
};
|
|
|
|
group = mkOption {
|
|
type = types.str;
|
|
default = "mautrix_slack";
|
|
description = "Primary group for the bridge.";
|
|
};
|
|
|
|
dataDir = mkOption {
|
|
type = types.path;
|
|
default = "/var/lib/mautrix_slack";
|
|
description = "State directory.";
|
|
};
|
|
|
|
extraPackages = mkOption {
|
|
type = types.listOf types.package;
|
|
default = [];
|
|
description = "Additional packages exposed to the service PATH.";
|
|
};
|
|
|
|
extraConfig = mkOption {
|
|
type = yamlFormat.type;
|
|
default = {};
|
|
description = "Optional YAML fragment merged into the config.";
|
|
};
|
|
|
|
# Matrix homeserver configuration
|
|
matrix = {
|
|
homeserverUrl = mkOption {
|
|
type = types.str;
|
|
default = "http://127.0.0.1:8008";
|
|
description = "Matrix homeserver URL for bridge connections";
|
|
};
|
|
|
|
serverName = mkOption {
|
|
type = types.str;
|
|
description = "Matrix server name for bridge users";
|
|
};
|
|
};
|
|
|
|
# Database configuration
|
|
database = {
|
|
type = mkOption {
|
|
type = types.str;
|
|
default = "postgres";
|
|
description = "Database type (sqlite3 or postgres)";
|
|
};
|
|
|
|
uri = mkOption {
|
|
type = types.str;
|
|
default = "postgresql:///mautrix_slack?host=/run/postgresql";
|
|
description = "Database connection string";
|
|
};
|
|
|
|
maxOpenConnections = mkOption {
|
|
type = types.int;
|
|
default = 32;
|
|
description = "Maximum number of open database connections";
|
|
};
|
|
|
|
maxIdleConnections = mkOption {
|
|
type = types.int;
|
|
default = 4;
|
|
description = "Maximum number of idle database connections";
|
|
};
|
|
};
|
|
|
|
# Logging configuration
|
|
logging = {
|
|
level = mkOption {
|
|
type = types.str;
|
|
default = "info";
|
|
description = "Log level (debug, info, warn, error)";
|
|
};
|
|
};
|
|
|
|
# Bridge behavior configuration
|
|
bridge = {
|
|
commandPrefix = mkOption {
|
|
type = types.str;
|
|
default = "!slack";
|
|
description = "Command prefix for bridge commands";
|
|
};
|
|
|
|
permissions = mkOption {
|
|
type = types.attrsOf types.str;
|
|
default = {
|
|
"example.com" = "user";
|
|
};
|
|
description = "Bridge permissions mapping";
|
|
};
|
|
};
|
|
|
|
# Appservice configuration
|
|
appservice = {
|
|
id = mkOption {
|
|
type = types.str;
|
|
default = "slack";
|
|
description = "Appservice ID";
|
|
};
|
|
|
|
hostname = mkOption {
|
|
type = types.str;
|
|
default = "127.0.0.1";
|
|
description = "Hostname for the appservice";
|
|
};
|
|
|
|
port = mkOption {
|
|
type = types.port;
|
|
default = 29319;
|
|
description = "Port for the appservice API";
|
|
};
|
|
|
|
senderLocalpart = mkOption {
|
|
type = types.str;
|
|
default = "slackbot";
|
|
description = "Localpart of the bridge bot user";
|
|
};
|
|
|
|
botDisplayName = mkOption {
|
|
type = types.str;
|
|
default = "Slack Bridge Bot";
|
|
description = "Display name for the bridge bot";
|
|
};
|
|
|
|
botAvatar = mkOption {
|
|
type = types.str;
|
|
default = "";
|
|
description = "Avatar MXC URI for the bridge bot";
|
|
};
|
|
|
|
userPrefix = mkOption {
|
|
type = types.str;
|
|
default = "slack_";
|
|
description = "Prefix for Matrix users bridged from Slack";
|
|
};
|
|
};
|
|
|
|
# Encryption configuration
|
|
encryption = {
|
|
enable = mkOption {
|
|
type = types.bool;
|
|
default = true;
|
|
description = "Enable Matrix end-to-end encryption";
|
|
};
|
|
|
|
default = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = "Enable encryption by default in new portals";
|
|
};
|
|
|
|
require = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = "Require encryption in all portals";
|
|
};
|
|
};
|
|
};
|
|
|
|
config = mkIf cfg.enable {
|
|
# User and group
|
|
users.users.${cfg.user} = {
|
|
isSystemUser = true;
|
|
group = cfg.group;
|
|
extraGroups = [ "matrix-appservices" ];
|
|
home = cfg.dataDir;
|
|
createHome = true;
|
|
};
|
|
users.groups.${cfg.group} = {};
|
|
users.groups.matrix-appservices = {};
|
|
|
|
# Systemd service with gmessages-style configuration
|
|
systemd.services.mautrix-slack = {
|
|
description = "mautrix-slack Matrix-Slack bridge";
|
|
after = [ "network.target" ]
|
|
++ optional (cfg.database.type == "postgres") "postgresql.service";
|
|
wants = [ "network-online.target" ];
|
|
wantedBy = [ "multi-user.target" ];
|
|
|
|
serviceConfig = {
|
|
Type = "simple";
|
|
User = cfg.user;
|
|
Group = cfg.group;
|
|
SupplementaryGroups = [ "matrix-appservices" ];
|
|
WorkingDirectory = cfg.dataDir;
|
|
|
|
# Use StateDirectory for persistent config and data management
|
|
StateDirectory = cfg.user;
|
|
StateDirectoryMode = "0750";
|
|
|
|
# Two-stage pre-start: directory setup as root, then config generation as service user
|
|
ExecStartPre = [
|
|
# Stage 1: Create config directory as root (+ prefix)
|
|
"+${pkgs.coreutils}/bin/mkdir -p /var/lib/${cfg.user}/config"
|
|
"+${pkgs.coreutils}/bin/chown -R ${cfg.user}:${cfg.group} /var/lib/${cfg.user}"
|
|
"+${pkgs.coreutils}/bin/chmod 750 /var/lib/${cfg.user}"
|
|
"+${pkgs.coreutils}/bin/chmod 750 /var/lib/${cfg.user}/config"
|
|
|
|
# Stage 2: Generate configuration using example template
|
|
"${pkgs.writeShellScript "mautrix-slack-prepare-config" ''
|
|
set -euo pipefail
|
|
|
|
CONFIG_DIR=/var/lib/${cfg.user}/config
|
|
CONFIG_PATH="$CONFIG_DIR/config.yaml"
|
|
REG_PATH="/var/lib/matrix-appservices/mautrix_slack_registration.yaml"
|
|
|
|
# Always regenerate config from example
|
|
cd "$CONFIG_DIR"
|
|
rm -f config.yaml
|
|
${cfg.package}/bin/mautrix-slack -c config.yaml -e
|
|
|
|
${pkgs.python3.withPackages (ps: [ ps.pyyaml ])}/bin/python3 <<'PY'
|
|
import json
|
|
import os
|
|
import pathlib
|
|
import pwd
|
|
import grp
|
|
import yaml
|
|
|
|
CONFIG_PATH = pathlib.Path('config.yaml')
|
|
REG_PATH = pathlib.Path('/var/lib/matrix-appservices/mautrix_slack_registration.yaml')
|
|
|
|
with CONFIG_PATH.open('r', encoding='utf-8') as fh:
|
|
config = yaml.safe_load(fh)
|
|
|
|
overrides = json.loads(${lib.escapeShellArg (builtins.toJSON configOverrides)})
|
|
extra_cfg = json.loads(${lib.escapeShellArg (builtins.toJSON cfg.extraConfig)})
|
|
|
|
def deep_update(target, data):
|
|
for key, value in data.items():
|
|
if isinstance(value, dict) and value is not None:
|
|
if key not in target or not isinstance(target.get(key), dict):
|
|
target[key] = {}
|
|
deep_update(target[key], value)
|
|
else:
|
|
target[key] = value
|
|
|
|
deep_update(config, overrides)
|
|
if extra_cfg:
|
|
deep_update(config, extra_cfg)
|
|
|
|
# Handle permissions explicitly to ensure example values are replaced
|
|
perms = overrides.get('bridge', {}).get('permissions')
|
|
if perms is not None:
|
|
config.setdefault('bridge', {})['permissions'] = perms
|
|
|
|
# Read tokens from registration file if it exists
|
|
if REG_PATH.exists():
|
|
with REG_PATH.open('r', encoding='utf-8') as fh:
|
|
registration = yaml.safe_load(fh)
|
|
|
|
if 'as_token' in registration:
|
|
config.setdefault('appservice', {})['as_token'] = registration['as_token']
|
|
if 'hs_token' in registration:
|
|
config.setdefault('appservice', {})['hs_token'] = registration['hs_token']
|
|
|
|
with CONFIG_PATH.open('w', encoding='utf-8') as fh:
|
|
yaml.safe_dump(config, fh, default_flow_style=False, sort_keys=False)
|
|
|
|
uid = pwd.getpwnam('${cfg.user}').pw_uid
|
|
gid_service = grp.getgrnam('${cfg.group}').gr_gid
|
|
gid_shared = grp.getgrnam('matrix-appservices').gr_gid
|
|
|
|
os.chown(CONFIG_PATH, uid, gid_service)
|
|
os.chmod(CONFIG_PATH, 0o640)
|
|
PY
|
|
|
|
# Generate registration file if it doesn't exist
|
|
if [ ! -f "$REG_PATH" ]; then
|
|
mkdir -p $(dirname "$REG_PATH")
|
|
${cfg.package}/bin/mautrix-slack -c config.yaml -g -r "$REG_PATH"
|
|
chown ${cfg.user}:matrix-appservices "$REG_PATH"
|
|
chmod 640 "$REG_PATH"
|
|
fi
|
|
''}"
|
|
];
|
|
|
|
# Start mautrix-slack with state directory config
|
|
ExecStart = "${cfg.package}/bin/mautrix-slack -c /var/lib/${cfg.user}/config/config.yaml";
|
|
|
|
# Restart policy
|
|
Restart = "always";
|
|
RestartSec = "10s";
|
|
|
|
# Security hardening
|
|
NoNewPrivileges = true;
|
|
ProtectSystem = "strict";
|
|
ProtectHome = true;
|
|
PrivateTmp = true;
|
|
PrivateDevices = true;
|
|
ProtectKernelTunables = true;
|
|
ProtectKernelModules = true;
|
|
ProtectControlGroups = true;
|
|
|
|
# Allow writing to data and shared appservices directory
|
|
ReadWritePaths = [
|
|
cfg.dataDir
|
|
"/var/lib/matrix-appservices"
|
|
];
|
|
|
|
# Resource limits
|
|
MemoryMax = "256M";
|
|
|
|
# Process security
|
|
UMask = "0027";
|
|
LockPersonality = true;
|
|
RestrictRealtime = true;
|
|
RestrictSUIDSGID = true;
|
|
|
|
# Logging
|
|
StandardOutput = "journal";
|
|
StandardError = "journal";
|
|
SyslogIdentifier = "mautrix-slack";
|
|
};
|
|
|
|
# Add extra packages to PATH if specified
|
|
path = [ pkgs.coreutils ] ++ cfg.extraPackages;
|
|
};
|
|
|
|
# Directory permissions
|
|
systemd.tmpfiles.rules = [
|
|
"d ${cfg.dataDir} 0750 ${cfg.user} ${cfg.group} -"
|
|
"d /var/lib/matrix-appservices 0770 root matrix-appservices -"
|
|
];
|
|
};
|
|
}
|