501 lines
15 KiB
Nix
501 lines
15 KiB
Nix
{
|
|
config,
|
|
pkgs,
|
|
lib,
|
|
inputs,
|
|
...
|
|
}:
|
|
let
|
|
name = "matrix";
|
|
hostDataDir = "/var/lib/${name}";
|
|
hostAddress = "10.0.0.1";
|
|
containerAddress = "10.0.0.6";
|
|
|
|
# Use unstable nixpkgs for the container (mautrix-gmessages module not in stable)
|
|
matrixNixpkgs = inputs.matrix-nixpkgs;
|
|
|
|
# Matrix server configuration
|
|
serverName = "matrix.joshuabell.xyz";
|
|
elementDomain = "element.joshuabell.xyz";
|
|
|
|
# Bind mount definitions following forgejo.nix pattern
|
|
binds = [
|
|
{
|
|
host = "${hostDataDir}/postgres";
|
|
container = "/var/lib/postgresql/17";
|
|
user = "postgres";
|
|
uid = 71;
|
|
gid = 71;
|
|
}
|
|
{
|
|
host = "${hostDataDir}/backups";
|
|
container = "/var/backup/postgresql";
|
|
user = "postgres";
|
|
uid = 71;
|
|
gid = 71;
|
|
}
|
|
{
|
|
host = "${hostDataDir}/dendrite";
|
|
container = "/var/lib/dendrite";
|
|
user = "dendrite";
|
|
uid = 993;
|
|
gid = 993;
|
|
}
|
|
{
|
|
host = "${hostDataDir}/gmessages";
|
|
container = "/var/lib/mautrix_gmessages";
|
|
user = "mautrix_gmessages";
|
|
uid = 992;
|
|
gid = 992;
|
|
}
|
|
];
|
|
|
|
uniqueUsers = lib.unique (map (b: { inherit (b) user uid gid; }) binds);
|
|
|
|
# Element Web configuration - points to our server, registration disabled
|
|
elementConfig = {
|
|
default_server_config = {
|
|
"m.homeserver" = {
|
|
base_url = "https://${serverName}";
|
|
server_name = serverName;
|
|
};
|
|
};
|
|
disable_guests = true;
|
|
disable_login_language_selector = false;
|
|
disable_3pid_login = true;
|
|
brand = "Element";
|
|
integrations_ui_url = "";
|
|
integrations_rest_url = "";
|
|
integrations_widgets_urls = [ ];
|
|
show_labs_settings = false;
|
|
room_directory = {
|
|
servers = [ serverName ];
|
|
};
|
|
# Security: disable features that could leak data
|
|
enable_presence_by_hs_url = { };
|
|
permalink_prefix = "https://${elementDomain}";
|
|
};
|
|
|
|
elementConfigFile = pkgs.writeText "element-config.json" (builtins.toJSON elementConfig);
|
|
|
|
# Custom Element Web with our config
|
|
elementWebCustom = pkgs.runCommand "element-web-custom" { } ''
|
|
cp -r ${pkgs.element-web} $out
|
|
chmod -R u+w $out
|
|
rm $out/config.json
|
|
cp ${elementConfigFile} $out/config.json
|
|
'';
|
|
|
|
in
|
|
{
|
|
# Create host directories and users
|
|
system.activationScripts."${name}-dirs" = lib.stringAfter [ "users" "groups" ] ''
|
|
${lib.concatMapStringsSep "\n" (b: ''
|
|
mkdir -p ${b.host}
|
|
chown ${toString b.uid}:${toString b.gid} ${b.host}
|
|
'') binds}
|
|
'';
|
|
|
|
# Create users/groups on host for bind mount permissions
|
|
users.users = lib.listToAttrs (
|
|
map (u: {
|
|
name = u.user;
|
|
value = {
|
|
isSystemUser = true;
|
|
uid = u.uid;
|
|
group = u.user;
|
|
};
|
|
}) (lib.filter (u: u.user != "postgres") uniqueUsers)
|
|
);
|
|
|
|
users.groups = lib.listToAttrs (
|
|
map (u: {
|
|
name = u.user;
|
|
value = {
|
|
gid = u.gid;
|
|
};
|
|
}) (lib.filter (u: u.user != "postgres") uniqueUsers)
|
|
);
|
|
|
|
# nginx reverse proxy on host
|
|
services.nginx.virtualHosts = {
|
|
# Matrix server - handles client and federation API
|
|
"${serverName}" = {
|
|
forceSSL = true;
|
|
useACMEHost = "joshuabell.xyz";
|
|
|
|
# .well-known for Matrix federation discovery
|
|
locations."= /.well-known/matrix/server" = {
|
|
return = ''200 '{"m.server": "${serverName}:443"}' '';
|
|
extraConfig = ''
|
|
default_type application/json;
|
|
add_header Access-Control-Allow-Origin *;
|
|
'';
|
|
};
|
|
|
|
locations."= /.well-known/matrix/client" = {
|
|
return = ''200 '{"m.homeserver": {"base_url": "https://${serverName}"}}' '';
|
|
extraConfig = ''
|
|
default_type application/json;
|
|
add_header Access-Control-Allow-Origin *;
|
|
'';
|
|
};
|
|
|
|
# Matrix client and federation API
|
|
locations."/_matrix" = {
|
|
proxyPass = "http://${containerAddress}:8008";
|
|
proxyWebsockets = true;
|
|
extraConfig = ''
|
|
proxy_read_timeout 600s;
|
|
client_max_body_size 50M;
|
|
'';
|
|
};
|
|
|
|
# Dendrite admin API (only accessible internally)
|
|
locations."/_dendrite" = {
|
|
proxyPass = "http://${containerAddress}:8008";
|
|
proxyWebsockets = true;
|
|
};
|
|
|
|
# Default location - redirect to Element
|
|
locations."/" = {
|
|
return = "301 https://${elementDomain}";
|
|
};
|
|
};
|
|
|
|
# Element Web client
|
|
"${elementDomain}" = {
|
|
forceSSL = true;
|
|
useACMEHost = "joshuabell.xyz";
|
|
|
|
locations."/" = {
|
|
proxyPass = "http://${containerAddress}:80";
|
|
proxyWebsockets = true;
|
|
};
|
|
};
|
|
};
|
|
|
|
# The container
|
|
containers.${name} = {
|
|
ephemeral = true;
|
|
autoStart = true;
|
|
privateNetwork = true;
|
|
hostAddress = hostAddress;
|
|
localAddress = containerAddress;
|
|
nixpkgs = matrixNixpkgs;
|
|
|
|
bindMounts = lib.listToAttrs (
|
|
map (b: {
|
|
name = b.container;
|
|
value = {
|
|
hostPath = b.host;
|
|
isReadOnly = false;
|
|
};
|
|
}) binds
|
|
);
|
|
|
|
config =
|
|
{ config, pkgs, ... }:
|
|
{
|
|
system.stateVersion = "24.11";
|
|
|
|
# Allow olm - required by mautrix-gmessages. The security issues are
|
|
# side-channel attacks on E2EE crypto, but SMS/RCS isn't E2EE through
|
|
# the bridge anyway (RCS encryption is handled by Google Messages).
|
|
nixpkgs.config.permittedInsecurePackages = [
|
|
"olm-3.2.16"
|
|
];
|
|
|
|
networking = {
|
|
firewall = {
|
|
enable = true;
|
|
allowedTCPPorts = [
|
|
8008 # Dendrite Matrix API
|
|
80 # Element Web (nginx)
|
|
];
|
|
};
|
|
useHostResolvConf = lib.mkForce false;
|
|
};
|
|
|
|
services.resolved.enable = true;
|
|
|
|
# PostgreSQL for Dendrite and mautrix-gmessages
|
|
services.postgresql = {
|
|
enable = true;
|
|
package = pkgs.postgresql_17;
|
|
ensureDatabases = [
|
|
"dendrite"
|
|
"mautrix_gmessages"
|
|
];
|
|
ensureUsers = [
|
|
{
|
|
name = "dendrite";
|
|
ensureDBOwnership = true;
|
|
}
|
|
{
|
|
name = "mautrix_gmessages";
|
|
ensureDBOwnership = true;
|
|
}
|
|
];
|
|
# Only allow local connections - no network access
|
|
enableTCPIP = false;
|
|
authentication = ''
|
|
local all all peer
|
|
'';
|
|
};
|
|
|
|
# PostgreSQL backup
|
|
services.postgresqlBackup = {
|
|
enable = true;
|
|
databases = [
|
|
"dendrite"
|
|
"mautrix_gmessages"
|
|
];
|
|
};
|
|
|
|
# Dendrite Matrix homeserver
|
|
services.dendrite = {
|
|
enable = true;
|
|
httpPort = 8008;
|
|
|
|
# Load signing key from file (generated on first boot)
|
|
loadCredential = [ "signing_key:/var/lib/dendrite/matrix_key.pem" ];
|
|
|
|
settings = {
|
|
global = {
|
|
server_name = serverName;
|
|
private_key = "$CREDENTIALS_DIRECTORY/signing_key";
|
|
|
|
# Security: Disable federation to keep messages private
|
|
# Set to true if you want to chat with users on other Matrix servers
|
|
disable_federation = true;
|
|
|
|
database = {
|
|
connection_string = "postgresql:///dendrite?host=/run/postgresql";
|
|
max_open_conns = 50;
|
|
max_idle_conns = 5;
|
|
conn_max_lifetime = -1;
|
|
};
|
|
|
|
# Security: strict DNS caching
|
|
dns_cache = {
|
|
enabled = true;
|
|
cache_size = 256;
|
|
cache_lifetime = "5m";
|
|
};
|
|
};
|
|
|
|
# Client API configuration
|
|
client_api = {
|
|
# Security: Disable registration - only admin can create accounts
|
|
registration_disabled = true;
|
|
|
|
# Security: Disable guest access
|
|
guests_disabled = true;
|
|
|
|
# Rate limiting
|
|
rate_limiting = {
|
|
enabled = true;
|
|
threshold = 20;
|
|
cooloff_ms = 500;
|
|
exempt_user_ids = [ ];
|
|
};
|
|
};
|
|
|
|
# Federation API - disabled for privacy
|
|
federation_api = {
|
|
# Security: No federation means messages stay on your server only
|
|
disable_tls_validation = false;
|
|
disable_http_keepalives = false;
|
|
send_max_retries = 16;
|
|
key_perspectives = [ ];
|
|
};
|
|
|
|
# Media API
|
|
media_api = {
|
|
base_path = "/var/lib/dendrite/media";
|
|
max_file_size_bytes = 52428800; # 50MB
|
|
dynamic_thumbnails = true;
|
|
};
|
|
|
|
# Sync API
|
|
sync_api = {
|
|
real_ip_header = "X-Forwarded-For";
|
|
search = {
|
|
enabled = true;
|
|
index_path = "/var/lib/dendrite/searchindex";
|
|
};
|
|
};
|
|
|
|
# User API
|
|
user_api = {
|
|
bcrypt_cost = 12; # Security: higher bcrypt cost
|
|
};
|
|
|
|
# MSCs (Matrix Spec Changes) - enable useful ones
|
|
mscs = {
|
|
mscs = [
|
|
"msc2836" # Threading
|
|
"msc2946" # Spaces
|
|
];
|
|
};
|
|
|
|
# Logging
|
|
logging = [
|
|
{
|
|
type = "std";
|
|
level = "warn";
|
|
}
|
|
];
|
|
|
|
# App services (bridges) - will be configured below
|
|
app_service_api = {
|
|
database = {
|
|
connection_string = "postgresql:///dendrite?host=/run/postgresql";
|
|
max_open_conns = 10;
|
|
max_idle_conns = 2;
|
|
conn_max_lifetime = -1;
|
|
};
|
|
config_files = [
|
|
"/var/lib/mautrix_gmessages/registration.yaml"
|
|
];
|
|
};
|
|
};
|
|
};
|
|
|
|
# Generate Dendrite signing key if it doesn't exist
|
|
systemd.services.dendrite-keygen = {
|
|
description = "Generate Dendrite signing key";
|
|
wantedBy = [ "dendrite.service" ];
|
|
before = [ "dendrite.service" ];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
User = "dendrite";
|
|
Group = "dendrite";
|
|
RemainAfterExit = true;
|
|
};
|
|
script = ''
|
|
if [ ! -f /var/lib/dendrite/matrix_key.pem ]; then
|
|
${pkgs.dendrite}/bin/generate-keys --private-key /var/lib/dendrite/matrix_key.pem
|
|
chmod 600 /var/lib/dendrite/matrix_key.pem
|
|
fi
|
|
'';
|
|
};
|
|
|
|
# mautrix-gmessages bridge (manual service - no NixOS module exists)
|
|
systemd.services.mautrix-gmessages = {
|
|
description = "mautrix-gmessages Matrix-Google Messages bridge";
|
|
after = [
|
|
"network.target"
|
|
"dendrite.service"
|
|
"postgresql.service"
|
|
"mautrix-gmessages-init.service"
|
|
];
|
|
requires = [ "postgresql.service" ];
|
|
wantedBy = [ "multi-user.target" ];
|
|
|
|
serviceConfig = {
|
|
Type = "simple";
|
|
User = "mautrix_gmessages";
|
|
Group = "mautrix_gmessages";
|
|
ExecStart = "${pkgs.mautrix-gmessages}/bin/mautrix-gmessages -c /var/lib/mautrix_gmessages/config.yaml";
|
|
Restart = "on-failure";
|
|
RestartSec = "10s";
|
|
|
|
# Security hardening
|
|
NoNewPrivileges = true;
|
|
PrivateTmp = true;
|
|
ProtectSystem = "strict";
|
|
ProtectHome = true;
|
|
ReadWritePaths = [ "/var/lib/mautrix_gmessages" ];
|
|
StateDirectory = "mautrix_gmessages";
|
|
};
|
|
};
|
|
|
|
# Generate mautrix-gmessages config if it doesn't exist
|
|
systemd.services.mautrix-gmessages-init = {
|
|
description = "Initialize mautrix-gmessages configuration";
|
|
wantedBy = [ "mautrix-gmessages.service" ];
|
|
before = [ "mautrix-gmessages.service" ];
|
|
after = [ "postgresql.service" ];
|
|
requires = [ "postgresql.service" ];
|
|
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
User = "mautrix_gmessages";
|
|
Group = "mautrix_gmessages";
|
|
RemainAfterExit = true;
|
|
};
|
|
|
|
script = ''
|
|
CONFIG_DIR="/var/lib/mautrix_gmessages"
|
|
CONFIG_FILE="$CONFIG_DIR/config.yaml"
|
|
REG_FILE="$CONFIG_DIR/registration.yaml"
|
|
|
|
# Generate example config if none exists
|
|
if [ ! -f "$CONFIG_FILE" ]; then
|
|
${pkgs.mautrix-gmessages}/bin/mautrix-gmessages -c "$CONFIG_FILE" -g
|
|
|
|
# Patch the generated config with our settings
|
|
${pkgs.yq-go}/bin/yq -i '
|
|
.homeserver.address = "http://localhost:8008" |
|
|
.homeserver.domain = "${serverName}" |
|
|
.appservice.database.type = "postgres" |
|
|
.appservice.database.uri = "postgresql:///mautrix_gmessages?host=/run/postgresql" |
|
|
.appservice.hostname = "127.0.0.1" |
|
|
.appservice.port = 29336 |
|
|
.appservice.id = "gmessages" |
|
|
.appservice.bot.username = "gmessagesbot" |
|
|
.appservice.bot.displayname = "Google Messages Bridge" |
|
|
.bridge.permissions."${serverName}" = "user" |
|
|
.bridge.permissions."@josh:${serverName}" = "admin" |
|
|
.bridge.delivery_receipts = true |
|
|
.bridge.sync_direct_chat_list = true |
|
|
.logging.min_level = "warn"
|
|
' "$CONFIG_FILE"
|
|
fi
|
|
|
|
# Generate registration file if none exists
|
|
if [ ! -f "$REG_FILE" ]; then
|
|
${pkgs.mautrix-gmessages}/bin/mautrix-gmessages -c "$CONFIG_FILE" -r "$REG_FILE"
|
|
chmod 640 "$REG_FILE"
|
|
fi
|
|
'';
|
|
};
|
|
|
|
# Create user/group for mautrix_gmessages
|
|
users.users.mautrix_gmessages = {
|
|
isSystemUser = true;
|
|
group = "mautrix_gmessages";
|
|
home = "/var/lib/mautrix_gmessages";
|
|
};
|
|
|
|
users.groups.mautrix_gmessages = { };
|
|
|
|
# nginx inside container for Element Web
|
|
services.nginx = {
|
|
enable = true;
|
|
virtualHosts."element" = {
|
|
listen = [
|
|
{
|
|
addr = "0.0.0.0";
|
|
port = 80;
|
|
}
|
|
];
|
|
root = elementWebCustom;
|
|
locations."/" = {
|
|
tryFiles = "$uri $uri/ /index.html";
|
|
};
|
|
};
|
|
};
|
|
|
|
# Ensure directories exist with proper permissions
|
|
systemd.tmpfiles.rules = [
|
|
"d /var/lib/dendrite 0750 dendrite dendrite -"
|
|
"d /var/lib/dendrite/media 0750 dendrite dendrite -"
|
|
"d /var/lib/dendrite/searchindex 0750 dendrite dendrite -"
|
|
"d /var/lib/mautrix_gmessages 0750 mautrix_gmessages mautrix_gmessages -"
|
|
];
|
|
};
|
|
};
|
|
}
|