ops-jrz1/modules/mautrix-slack.nix
Dan 4c38331e17 Fix Matrix package references to use nixpkgs-unstable
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.
2025-10-21 00:06:43 -07:00

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 -"
];
};
}