Phase 3: Extract and sanitize Matrix platform modules from ops-base
Extracted modules: - Matrix homeserver (matrix-continuwuity.nix) - mautrix bridges (slack, whatsapp, gmessages) - Security modules (fail2ban, ssh-hardening) - Development services module - Matrix secrets module All modules sanitized to remove personal information: - Domains: example.com, matrix.example.org - IPs: 10.0.0.x, 203.0.113.10 - Paths: /home/user, /path/to/ops-base - Emails: admin@example.com Configuration: - Updated flake.nix with sops-nix and nixpkgs-unstable - Updated hosts/ops-jrz1.nix to import all extracted modules - Added example files (secrets, minimal config) - Generated flake.lock Generated with Claude Code - https://claude.com/claude-code
This commit is contained in:
parent
6a26ca12ca
commit
ab5aebb161
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -4,6 +4,7 @@ result-*
|
||||||
|
|
||||||
# Staging directories (temporary extraction workspace)
|
# Staging directories (temporary extraction workspace)
|
||||||
staging/
|
staging/
|
||||||
|
staging-sanitized/
|
||||||
|
|
||||||
# Secrets (never commit real secrets)
|
# Secrets (never commit real secrets)
|
||||||
secrets/*.yaml
|
secrets/*.yaml
|
||||||
|
|
|
||||||
131
configurations/dev-vps.nix
Normal file
131
configurations/dev-vps.nix
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
# NixOS configuration for development VPS
|
||||||
|
# Simple setup for Matrix + Forgejo + Slack bridge testing
|
||||||
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
../modules/dev-services.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
# Basic boot configuration for VPS
|
||||||
|
boot = {
|
||||||
|
loader = {
|
||||||
|
grub = {
|
||||||
|
enable = true;
|
||||||
|
device = "/dev/vda"; # Common for cloud VPS
|
||||||
|
useOSProber = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
# Cloud VPS typically uses virtio
|
||||||
|
initrd.availableKernelModules = [
|
||||||
|
"virtio_pci"
|
||||||
|
"virtio_blk"
|
||||||
|
"virtio_net"
|
||||||
|
"virtio_scsi"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Network configuration
|
||||||
|
networking = {
|
||||||
|
hostName = "dev-matrix-vps";
|
||||||
|
|
||||||
|
# Most VPS providers use DHCP
|
||||||
|
useDHCP = false;
|
||||||
|
interfaces.ens3 = { # Common interface name, adjust as needed
|
||||||
|
useDHCP = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
enableIPv6 = true;
|
||||||
|
|
||||||
|
# Firewall - only expose what's needed
|
||||||
|
firewall = {
|
||||||
|
enable = true;
|
||||||
|
allowedTCPPorts = [
|
||||||
|
22 # SSH
|
||||||
|
80 # HTTP
|
||||||
|
443 # HTTPS
|
||||||
|
3000 # Forgejo (for testing, remove in production)
|
||||||
|
8008 # Matrix (for testing, remove in production)
|
||||||
|
];
|
||||||
|
allowPing = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# SSH configuration
|
||||||
|
services.openssh = {
|
||||||
|
enable = true;
|
||||||
|
settings = {
|
||||||
|
PermitRootLogin = "prohibit-password";
|
||||||
|
PasswordAuthentication = false;
|
||||||
|
KbdInteractiveAuthentication = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Admin user
|
||||||
|
users.users.admin = {
|
||||||
|
isNormalUser = true;
|
||||||
|
extraGroups = [ "wheel" ];
|
||||||
|
openssh.authorizedKeys.keys = [
|
||||||
|
# Add your SSH public key here
|
||||||
|
# "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI..."
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Enable sudo without password for admin (dev environment)
|
||||||
|
security.sudo.wheelNeedsPassword = false;
|
||||||
|
|
||||||
|
# Enable dev services stack
|
||||||
|
services.dev-platform = {
|
||||||
|
enable = true;
|
||||||
|
domain = "localhost"; # Change to your domain or IP
|
||||||
|
|
||||||
|
matrix = {
|
||||||
|
enable = true;
|
||||||
|
serverName = "dev.matrix";
|
||||||
|
};
|
||||||
|
|
||||||
|
forgejo = {
|
||||||
|
enable = true;
|
||||||
|
subdomain = "git";
|
||||||
|
};
|
||||||
|
|
||||||
|
slackBridge = {
|
||||||
|
enable = true;
|
||||||
|
workspace = ""; # Will be configured via secrets
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Basic monitoring
|
||||||
|
services.netdata = {
|
||||||
|
enable = true;
|
||||||
|
config = {
|
||||||
|
global = {
|
||||||
|
"bind to" = "127.0.0.1";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Automatic garbage collection
|
||||||
|
nix.gc = {
|
||||||
|
automatic = true;
|
||||||
|
dates = "weekly";
|
||||||
|
options = "--delete-older-than 7d";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Enable flakes and optimize for deployment
|
||||||
|
nix.settings = {
|
||||||
|
experimental-features = [ "nix-command" "flakes" ];
|
||||||
|
|
||||||
|
# Optimize for builds and downloads
|
||||||
|
max-jobs = "auto";
|
||||||
|
cores = 0; # Use all cores
|
||||||
|
substituters = [
|
||||||
|
"https://cache.nixos.org"
|
||||||
|
];
|
||||||
|
trusted-public-keys = [
|
||||||
|
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
system.stateVersion = "24.11";
|
||||||
|
}
|
||||||
184
configurations/vultr-dev.nix
Normal file
184
configurations/vultr-dev.nix
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
# NixOS configuration for Vultr development VPS
|
||||||
|
# Optimized for Matrix + Forgejo deployment without federation
|
||||||
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
../modules/dev-services.nix
|
||||||
|
./vultr-hardware.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
# sops-nix secrets management
|
||||||
|
sops = {
|
||||||
|
defaultSopsFile = ../secrets/secrets.yaml;
|
||||||
|
age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
|
||||||
|
|
||||||
|
# Secret declarations
|
||||||
|
secrets."matrix-registration-token" = {
|
||||||
|
mode = "0400";
|
||||||
|
};
|
||||||
|
|
||||||
|
secrets."acme-email" = {
|
||||||
|
mode = "0400";
|
||||||
|
# Using direct email value, no secret needed
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Boot configuration for Vultr VPS (Legacy BIOS mode)
|
||||||
|
boot = {
|
||||||
|
loader = {
|
||||||
|
grub = {
|
||||||
|
enable = true;
|
||||||
|
device = "/dev/vda"; # Legacy BIOS - install to MBR
|
||||||
|
efiSupport = false;
|
||||||
|
useOSProber = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
# Vultr uses virtio drivers
|
||||||
|
initrd.availableKernelModules = [
|
||||||
|
"virtio_pci"
|
||||||
|
"virtio_blk"
|
||||||
|
"virtio_net"
|
||||||
|
"virtio_scsi"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Filesystem configuration managed by vultr-hardware.nix
|
||||||
|
# Boot partition, root partition, and swap declared via generated hardware config
|
||||||
|
|
||||||
|
# Network configuration for Vultr
|
||||||
|
networking = {
|
||||||
|
hostName = "matrix";
|
||||||
|
|
||||||
|
# Vultr-specific network interface
|
||||||
|
useDHCP = false;
|
||||||
|
interfaces.ens3 = { # Vultr uses ens3
|
||||||
|
useDHCP = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
enableIPv6 = true;
|
||||||
|
|
||||||
|
# Firewall - only expose reverse proxy ports
|
||||||
|
firewall = {
|
||||||
|
enable = true;
|
||||||
|
allowedTCPPorts = [
|
||||||
|
22 # SSH
|
||||||
|
80 # HTTP (ACME challenges, redirects)
|
||||||
|
443 # HTTPS
|
||||||
|
];
|
||||||
|
allowPing = true;
|
||||||
|
logRefusedConnections = false; # Reduce log noise on public VPS
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# SSH configuration - secure but accessible for development
|
||||||
|
services.openssh = {
|
||||||
|
enable = true;
|
||||||
|
settings = {
|
||||||
|
PermitRootLogin = "prohibit-password"; # More secure than "yes"
|
||||||
|
PasswordAuthentication = false;
|
||||||
|
KbdInteractiveAuthentication = false;
|
||||||
|
X11Forwarding = false;
|
||||||
|
};
|
||||||
|
# Basic security settings
|
||||||
|
extraConfig = ''
|
||||||
|
MaxAuthTries 3
|
||||||
|
MaxSessions 10
|
||||||
|
ClientAliveInterval 300
|
||||||
|
ClientAliveCountMax 2
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# SSH key for root and admin users
|
||||||
|
users.users.root.openssh.authorizedKeys.keys = [
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOqHsgAuD/8LL6HN3fo7X1ywryQG393pyQ19a154bO+h delpad-2025"
|
||||||
|
];
|
||||||
|
|
||||||
|
users.users.admin = {
|
||||||
|
isNormalUser = true;
|
||||||
|
extraGroups = [ "wheel" ];
|
||||||
|
openssh.authorizedKeys.keys = [
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOqHsgAuD/8LL6HN3fo7X1ywryQG393pyQ19a154bO+h delpad-2025"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Enable sudo without password for development convenience
|
||||||
|
security.sudo.wheelNeedsPassword = false;
|
||||||
|
|
||||||
|
# Enable fail2ban for brute force protection
|
||||||
|
security.fail2ban-enhanced = {
|
||||||
|
enable = true;
|
||||||
|
bantime = "1h";
|
||||||
|
maxretry = 3;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Enable ACME for TLS certificates
|
||||||
|
security.acme = {
|
||||||
|
acceptTerms = true;
|
||||||
|
defaults.email = "admin@example.com"; # Using direct email as ACME doesn't support emailFile
|
||||||
|
};
|
||||||
|
|
||||||
|
# Dev services stack - simplified without federation
|
||||||
|
services.dev-platform = {
|
||||||
|
enable = true;
|
||||||
|
domain = "example.com";
|
||||||
|
|
||||||
|
matrix = {
|
||||||
|
enable = true;
|
||||||
|
port = 8008;
|
||||||
|
};
|
||||||
|
|
||||||
|
forgejo = {
|
||||||
|
enable = true;
|
||||||
|
subdomain = "git";
|
||||||
|
port = 3000;
|
||||||
|
};
|
||||||
|
|
||||||
|
slackBridge = {
|
||||||
|
enable = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Basic monitoring for development
|
||||||
|
services.netdata = {
|
||||||
|
enable = true;
|
||||||
|
config = {
|
||||||
|
global = {
|
||||||
|
"bind to" = "127.0.0.1"; # Localhost only for security
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Automatic garbage collection to manage disk space
|
||||||
|
nix.gc = {
|
||||||
|
automatic = true;
|
||||||
|
dates = "weekly";
|
||||||
|
options = "--delete-older-than 7d";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Allow insecure packages needed for Matrix bridges
|
||||||
|
nixpkgs.config.permittedInsecurePackages = [
|
||||||
|
"olm-3.2.16"
|
||||||
|
];
|
||||||
|
|
||||||
|
# NixOS configuration optimized for VPS
|
||||||
|
nix.settings = {
|
||||||
|
experimental-features = [ "nix-command" "flakes" ];
|
||||||
|
|
||||||
|
# Optimize for VPS builds and downloads
|
||||||
|
max-jobs = "auto";
|
||||||
|
cores = 0; # Use all available cores
|
||||||
|
substituters = [
|
||||||
|
"https://cache.nixos.org"
|
||||||
|
];
|
||||||
|
trusted-public-keys = [
|
||||||
|
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Timezone and locale for VPS
|
||||||
|
time.timeZone = "UTC";
|
||||||
|
i18n.defaultLocale = "en_US.UTF-8";
|
||||||
|
|
||||||
|
system.stateVersion = "24.11";
|
||||||
|
}
|
||||||
79
docs/examples/minimal-matrix.nix
Normal file
79
docs/examples/minimal-matrix.nix
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
# Minimal ops-jrz1 configuration example
|
||||||
|
# Demonstrates Matrix homeserver + single bridge deployment
|
||||||
|
{ config, pkgs, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
../../modules/matrix-continuwuity.nix
|
||||||
|
../../modules/mautrix-slack.nix
|
||||||
|
../../modules/security/ssh-hardening.nix
|
||||||
|
../../modules/security/fail2ban.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
# Basic networking
|
||||||
|
networking = {
|
||||||
|
hostName = "matrix";
|
||||||
|
firewall = {
|
||||||
|
enable = true;
|
||||||
|
allowedTCPPorts = [ 22 80 443 8008 ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Matrix homeserver configuration
|
||||||
|
services.matrix-homeserver = {
|
||||||
|
enable = true;
|
||||||
|
domain = "matrix.example.org";
|
||||||
|
port = 8008;
|
||||||
|
enableRegistration = true;
|
||||||
|
enableFederation = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Slack bridge configuration
|
||||||
|
services.mautrix-slack = {
|
||||||
|
enable = true;
|
||||||
|
matrix = {
|
||||||
|
homeserverUrl = "http://127.0.0.1:8008";
|
||||||
|
serverName = "matrix.example.org";
|
||||||
|
};
|
||||||
|
bridge = {
|
||||||
|
permissions = {
|
||||||
|
"matrix.example.org" = "user";
|
||||||
|
"@admin:matrix.example.org" = "admin";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Security hardening
|
||||||
|
security = {
|
||||||
|
fail2ban-enhanced = {
|
||||||
|
enable = true;
|
||||||
|
bantime = "1h";
|
||||||
|
maxretry = 3;
|
||||||
|
};
|
||||||
|
acme = {
|
||||||
|
acceptTerms = true;
|
||||||
|
defaults.email = "admin@example.com";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# SSH hardening
|
||||||
|
services.openssh = {
|
||||||
|
enable = true;
|
||||||
|
settings = {
|
||||||
|
PermitRootLogin = "prohibit-password";
|
||||||
|
PasswordAuthentication = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# PostgreSQL for bridge database
|
||||||
|
services.postgresql = {
|
||||||
|
enable = true;
|
||||||
|
ensureDatabases = [ "mautrix_slack" ];
|
||||||
|
ensureUsers = [{
|
||||||
|
name = "mautrix_slack";
|
||||||
|
ensureDBOwnership = true;
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
|
||||||
|
system.stateVersion = "24.05";
|
||||||
|
}
|
||||||
65
flake.lock
Normal file
65
flake.lock
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1735563628,
|
||||||
|
"narHash": "sha256-OnSAY7XDSx7CtDoqNh8jwVwh4xNL/2HaJxGjryLWzX8=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "b134951a4c9f3c995fd7be05f3243f8ecd65d798",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-24.05",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs-unstable": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1760284886,
|
||||||
|
"narHash": "sha256-TK9Kr0BYBQ/1P5kAsnNQhmWWKgmZXwUQr4ZMjCzWf2c=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "cf3f5c4def3c7b5f1fc012b3d839575dbe552d43",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"nixpkgs-unstable": "nixpkgs-unstable",
|
||||||
|
"sops-nix": "sops-nix"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sops-nix": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1760240450,
|
||||||
|
"narHash": "sha256-sa9bS9jSyc4vH0jSWrUsPGdqtMvDwmkLg971ntWOo2U=",
|
||||||
|
"owner": "Mic92",
|
||||||
|
"repo": "sops-nix",
|
||||||
|
"rev": "41fd1f7570c89f645ee0ada0be4e2d3c4b169549",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "Mic92",
|
||||||
|
"repo": "sops-nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
22
flake.nix
22
flake.nix
|
|
@ -3,24 +3,28 @@
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
|
||||||
|
nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
|
||||||
# sops-nix for secrets management (will be configured after extraction)
|
sops-nix = {
|
||||||
# sops-nix = {
|
url = "github:Mic92/sops-nix";
|
||||||
# url = "github:Mic92/sops-nix";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
# inputs.nixpkgs.follows = "nixpkgs";
|
};
|
||||||
# };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs, ... }@inputs: {
|
outputs = { self, nixpkgs, nixpkgs-unstable, sops-nix, ... }@inputs: {
|
||||||
nixosConfigurations = {
|
nixosConfigurations = {
|
||||||
ops-jrz1 = nixpkgs.lib.nixosSystem {
|
ops-jrz1 = nixpkgs.lib.nixosSystem {
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
|
specialArgs = {
|
||||||
|
pkgs-unstable = import nixpkgs-unstable {
|
||||||
|
system = "x86_64-linux";
|
||||||
|
config.allowUnfree = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
modules = [
|
modules = [
|
||||||
./configuration.nix
|
./configuration.nix
|
||||||
./hosts/ops-jrz1.nix
|
./hosts/ops-jrz1.nix
|
||||||
|
sops-nix.nixosModules.sops
|
||||||
# sops-nix will be added after extraction
|
|
||||||
# inputs.sops-nix.nixosModules.sops
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,51 @@
|
||||||
{ config, pkgs, ... }:
|
{ config, pkgs, pkgs-unstable, ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
# ops-jrz1 dev/test server configuration
|
# ops-jrz1 dev/test server configuration
|
||||||
# This file will be populated with Matrix platform modules and server-specific settings
|
# Imports extracted Matrix modules from ops-base
|
||||||
|
|
||||||
imports = [
|
imports = [
|
||||||
# Hardware configuration will be added based on server specs
|
# Hardware configuration will be added based on server specs
|
||||||
# ./hardware-configuration.nix
|
# ./hardware-configuration.nix
|
||||||
|
|
||||||
# Matrix platform modules (to be imported after extraction)
|
# Matrix platform modules
|
||||||
# ../modules/matrix-continuwuity.nix
|
../modules/matrix-continuwuity.nix
|
||||||
# ../modules/mautrix-slack.nix
|
../modules/mautrix-slack.nix
|
||||||
# ../modules/mautrix-whatsapp.nix
|
../modules/mautrix-whatsapp.nix
|
||||||
# ../modules/mautrix-gmessages.nix
|
../modules/mautrix-gmessages.nix
|
||||||
# ../modules/security/fail2ban.nix
|
../modules/dev-services.nix
|
||||||
# ../modules/security/ssh-hardening.nix
|
../modules/security/fail2ban.nix
|
||||||
|
../modules/security/ssh-hardening.nix
|
||||||
|
../modules/matrix-secrets
|
||||||
];
|
];
|
||||||
|
|
||||||
# System configuration
|
# System configuration
|
||||||
networking.hostName = "ops-jrz1";
|
networking.hostName = "ops-jrz1";
|
||||||
|
|
||||||
# Placeholder for Matrix homeserver configuration
|
# Example Matrix homeserver configuration (disabled by default)
|
||||||
# services.matrix-continuwuity = {
|
# Uncomment and configure for actual deployment:
|
||||||
|
# services.matrix-homeserver = {
|
||||||
# enable = true;
|
# enable = true;
|
||||||
# domain = "REPLACE_ME"; # ops-jrz1 domain
|
# domain = "matrix.example.org";
|
||||||
# port = 8008;
|
# port = 8008;
|
||||||
|
# enableRegistration = true;
|
||||||
|
# enableFederation = false;
|
||||||
# };
|
# };
|
||||||
|
|
||||||
# Placeholder for bridge configurations
|
# Example mautrix-slack bridge configuration (disabled by default)
|
||||||
# Bridges will be configured after modules are extracted
|
# services.mautrix-slack = {
|
||||||
|
# enable = true;
|
||||||
|
# matrix = {
|
||||||
|
# homeserverUrl = "http://127.0.0.1:8008";
|
||||||
|
# serverName = "matrix.example.org";
|
||||||
|
# };
|
||||||
|
# bridge = {
|
||||||
|
# permissions = {
|
||||||
|
# "matrix.example.org" = "user";
|
||||||
|
# "@admin:matrix.example.org" = "admin";
|
||||||
|
# };
|
||||||
|
# };
|
||||||
|
# };
|
||||||
|
|
||||||
system.stateVersion = "24.05";
|
system.stateVersion = "24.05";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
308
modules/dev-services.nix
Normal file
308
modules/dev-services.nix
Normal file
|
|
@ -0,0 +1,308 @@
|
||||||
|
# Development services module - Matrix, Forgejo, and Slack bridge
|
||||||
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.services.dev-platform;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.services.dev-platform = {
|
||||||
|
enable = mkEnableOption "development platform with Matrix and Forgejo";
|
||||||
|
|
||||||
|
domain = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "localhost";
|
||||||
|
description = "Base domain for services";
|
||||||
|
};
|
||||||
|
|
||||||
|
matrix = {
|
||||||
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Enable Matrix server";
|
||||||
|
};
|
||||||
|
|
||||||
|
serverName = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = cfg.domain;
|
||||||
|
description = "Matrix server name";
|
||||||
|
};
|
||||||
|
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 8008;
|
||||||
|
description = "Matrix server port";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
forgejo = {
|
||||||
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Enable Forgejo git service";
|
||||||
|
};
|
||||||
|
|
||||||
|
subdomain = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "git";
|
||||||
|
description = "Subdomain for Forgejo";
|
||||||
|
};
|
||||||
|
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 3000;
|
||||||
|
description = "Forgejo port";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
slackBridge = {
|
||||||
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Enable Slack bridge";
|
||||||
|
};
|
||||||
|
|
||||||
|
workspace = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "";
|
||||||
|
description = "Slack workspace name";
|
||||||
|
};
|
||||||
|
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 29319;
|
||||||
|
description = "Slack bridge port";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
# PostgreSQL for Forgejo and bridge services (Matrix uses RocksDB)
|
||||||
|
services.postgresql = {
|
||||||
|
enable = true;
|
||||||
|
ensureDatabases = [
|
||||||
|
"forgejo"
|
||||||
|
] ++ optional cfg.slackBridge.enable "mautrix_slack";
|
||||||
|
|
||||||
|
ensureUsers = [
|
||||||
|
{
|
||||||
|
name = "forgejo";
|
||||||
|
ensureDBOwnership = true;
|
||||||
|
}
|
||||||
|
] ++ optional cfg.slackBridge.enable {
|
||||||
|
name = "mautrix_slack";
|
||||||
|
ensureDBOwnership = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Matrix Continuwuity server
|
||||||
|
systemd.services.matrix-continuwuity = mkIf cfg.matrix.enable {
|
||||||
|
description = "Continuwuity Matrix homeserver";
|
||||||
|
after = [ "network.target" ];
|
||||||
|
wants = [ "network-online.target" ];
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
|
||||||
|
preStart = ''
|
||||||
|
# Generate config file at runtime with secret injection from systemd credentials
|
||||||
|
cat > /var/lib/matrix-continuwuity/continuwuity.toml <<EOF
|
||||||
|
[global]
|
||||||
|
server_name = "${cfg.matrix.serverName}"
|
||||||
|
address = "127.0.0.1"
|
||||||
|
port = ${toString cfg.matrix.port}
|
||||||
|
allow_registration = true
|
||||||
|
registration_token = "$(cat $CREDENTIALS_DIRECTORY/matrix-registration-token)"
|
||||||
|
allow_encryption = true
|
||||||
|
allow_federation = false
|
||||||
|
database_backend = "rocksdb"
|
||||||
|
database_path = "/var/lib/matrix-continuwuity/db/"
|
||||||
|
log = "info"
|
||||||
|
admin_room_tag = "m.server_notice"
|
||||||
|
EOF
|
||||||
|
'';
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "simple";
|
||||||
|
DynamicUser = true;
|
||||||
|
StateDirectory = "matrix-continuwuity";
|
||||||
|
WorkingDirectory = "/var/lib/matrix-continuwuity";
|
||||||
|
|
||||||
|
# Load secrets via systemd credentials (proper way for DynamicUser)
|
||||||
|
LoadCredential = "matrix-registration-token:/run/secrets/matrix-registration-token";
|
||||||
|
|
||||||
|
ExecStart = "${pkgs.matrix-continuwuity}/bin/conduwuit -c /var/lib/matrix-continuwuity/continuwuity.toml";
|
||||||
|
|
||||||
|
Restart = "always";
|
||||||
|
RestartSec = "10s";
|
||||||
|
|
||||||
|
# Security hardening
|
||||||
|
NoNewPrivileges = true;
|
||||||
|
ProtectSystem = "strict";
|
||||||
|
ProtectHome = true;
|
||||||
|
PrivateTmp = true;
|
||||||
|
ProtectKernelTunables = true;
|
||||||
|
ProtectKernelModules = true;
|
||||||
|
ProtectControlGroups = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Forgejo git service
|
||||||
|
services.forgejo = mkIf cfg.forgejo.enable {
|
||||||
|
enable = true;
|
||||||
|
stateDir = "/var/lib/forgejo";
|
||||||
|
|
||||||
|
database = {
|
||||||
|
type = "postgres";
|
||||||
|
host = "/run/postgresql";
|
||||||
|
name = "forgejo";
|
||||||
|
user = "forgejo";
|
||||||
|
createDatabase = false; # We handle this with ensureDatabases
|
||||||
|
};
|
||||||
|
|
||||||
|
settings = {
|
||||||
|
server = {
|
||||||
|
DOMAIN = "${cfg.forgejo.subdomain}.${cfg.domain}";
|
||||||
|
ROOT_URL = "https://${cfg.forgejo.subdomain}.${cfg.domain}";
|
||||||
|
PROTOCOL = "http";
|
||||||
|
HTTP_ADDR = "127.0.0.1";
|
||||||
|
HTTP_PORT = cfg.forgejo.port;
|
||||||
|
};
|
||||||
|
|
||||||
|
service = {
|
||||||
|
DISABLE_REGISTRATION = true; # Disable public registration
|
||||||
|
DEFAULT_KEEP_EMAIL_PRIVATE = true;
|
||||||
|
DEFAULT_ALLOW_CREATE_ORGANIZATION = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
log = {
|
||||||
|
LEVEL = "Info";
|
||||||
|
MODE = "console";
|
||||||
|
};
|
||||||
|
|
||||||
|
repository = {
|
||||||
|
DEFAULT_BRANCH = "main";
|
||||||
|
};
|
||||||
|
|
||||||
|
ui = {
|
||||||
|
DEFAULT_THEME = "arc-green";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Enable Actions for CI/CD
|
||||||
|
actions = {
|
||||||
|
ENABLED = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
lfs.enable = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
# mautrix-slack bridge
|
||||||
|
services.mautrix-slack = mkIf cfg.slackBridge.enable {
|
||||||
|
enable = true;
|
||||||
|
package = pkgs.mautrix-slack or (pkgs.callPackage ../pkgs/mautrix-slack {});
|
||||||
|
|
||||||
|
matrix = {
|
||||||
|
homeserverUrl = "http://localhost:${toString cfg.matrix.port}";
|
||||||
|
serverName = cfg.matrix.serverName;
|
||||||
|
};
|
||||||
|
|
||||||
|
appservice = {
|
||||||
|
port = cfg.slackBridge.port;
|
||||||
|
hostname = "127.0.0.1";
|
||||||
|
};
|
||||||
|
|
||||||
|
database = {
|
||||||
|
type = "postgres";
|
||||||
|
uri = "postgresql:///mautrix_slack?host=/run/postgresql";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Basic Nginx reverse proxy
|
||||||
|
services.nginx = {
|
||||||
|
enable = true;
|
||||||
|
recommendedProxySettings = true;
|
||||||
|
recommendedTlsSettings = true;
|
||||||
|
recommendedOptimisation = true;
|
||||||
|
recommendedGzipSettings = true;
|
||||||
|
|
||||||
|
# Rate limiting for login endpoints
|
||||||
|
appendHttpConfig = ''
|
||||||
|
limit_req_zone $binary_remote_addr zone=logins:10m rate=5r/m;
|
||||||
|
'';
|
||||||
|
|
||||||
|
virtualHosts = {
|
||||||
|
# Matrix endpoints
|
||||||
|
"${cfg.domain}" = mkIf cfg.matrix.enable {
|
||||||
|
enableACME = true;
|
||||||
|
forceSSL = true;
|
||||||
|
extraConfig = ''
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
'';
|
||||||
|
locations = {
|
||||||
|
"~ ^/_matrix/client/(r0|v3)/login" = {
|
||||||
|
proxyPass = "http://localhost:${toString cfg.matrix.port}";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
extraConfig = ''
|
||||||
|
limit_req zone=logins burst=10 nodelay;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
"/_matrix" = {
|
||||||
|
proxyPass = "http://localhost:${toString cfg.matrix.port}";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
};
|
||||||
|
"/.well-known/matrix/server" = {
|
||||||
|
extraConfig = ''
|
||||||
|
add_header Content-Type application/json;
|
||||||
|
return 200 '{"m.server": "${cfg.matrix.serverName}:443"}';
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
"/.well-known/matrix/client" = {
|
||||||
|
extraConfig = ''
|
||||||
|
add_header Content-Type application/json;
|
||||||
|
add_header Access-Control-Allow-Origin *;
|
||||||
|
return 200 '{"m.homeserver": {"base_url": "https://${cfg.domain}"}}';
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Forgejo git service (subdomain-based, requires DNS)
|
||||||
|
"${cfg.forgejo.subdomain}.${cfg.domain}" = mkIf cfg.forgejo.enable {
|
||||||
|
enableACME = true;
|
||||||
|
forceSSL = true;
|
||||||
|
extraConfig = ''
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
'';
|
||||||
|
locations."/user/login" = {
|
||||||
|
proxyPass = "http://localhost:${toString cfg.forgejo.port}";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
extraConfig = ''
|
||||||
|
limit_req zone=logins burst=10 nodelay;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
locations."/user/sign_up" = {
|
||||||
|
proxyPass = "http://localhost:${toString cfg.forgejo.port}";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
extraConfig = ''
|
||||||
|
limit_req zone=logins burst=10 nodelay;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://localhost:${toString cfg.forgejo.port}";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
extraConfig = ''
|
||||||
|
client_max_body_size 512M;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Systemd tmpfiles for data directories
|
||||||
|
# Note: matrix-continuwuity directory managed by StateDirectory with DynamicUser
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d /var/lib/forgejo 0750 forgejo forgejo -"
|
||||||
|
] ++ optional cfg.slackBridge.enable
|
||||||
|
"d /var/lib/mautrix-slack 0750 mautrix_slack mautrix_slack -";
|
||||||
|
};
|
||||||
|
}
|
||||||
119
modules/matrix-continuwuity.nix
Normal file
119
modules/matrix-continuwuity.nix
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
# NixOS module for Continuwuity Matrix homeserver
|
||||||
|
# Portable, modular configuration with clean enable/disable
|
||||||
|
# Creates systemd service manually since services.matrix-continuwuity not in stable
|
||||||
|
{ config, pkgs, lib, pkgs-unstable, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.services.matrix-homeserver;
|
||||||
|
continuwuityPkg = pkgs-unstable.matrix-continuwuity;
|
||||||
|
|
||||||
|
# Generate TOML configuration
|
||||||
|
configFile = pkgs.writeText "continuwuity.toml" ''
|
||||||
|
[global]
|
||||||
|
server_name = "${cfg.domain}"
|
||||||
|
address = "0.0.0.0"
|
||||||
|
port = ${toString cfg.port}
|
||||||
|
allow_registration = ${boolToString cfg.enableRegistration}
|
||||||
|
allow_encryption = true
|
||||||
|
allow_federation = ${boolToString cfg.enableFederation}
|
||||||
|
database_backend = "rocksdb"
|
||||||
|
database_path = "${cfg.dataDir}/db/"
|
||||||
|
log = "info,continuwuity=debug"
|
||||||
|
${optionalString cfg.enableFederation ''
|
||||||
|
trusted_servers = ["matrix.org"]
|
||||||
|
''}
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.services.matrix-homeserver = {
|
||||||
|
enable = mkEnableOption "Continuwuity Matrix homeserver";
|
||||||
|
|
||||||
|
domain = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "10.0.0.40";
|
||||||
|
description = "Domain or IP for Matrix server";
|
||||||
|
};
|
||||||
|
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 8008;
|
||||||
|
description = "Port for Matrix server";
|
||||||
|
};
|
||||||
|
|
||||||
|
enableRegistration = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Allow new user registration";
|
||||||
|
};
|
||||||
|
|
||||||
|
enableFederation = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Enable federation with other Matrix servers";
|
||||||
|
};
|
||||||
|
|
||||||
|
dataDir = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
default = "/var/lib/continuwuity";
|
||||||
|
description = "Data directory for Matrix server";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
# Create continuwuity user and group
|
||||||
|
users.users.continuwuity = {
|
||||||
|
description = "Continuwuity Matrix server user";
|
||||||
|
group = "continuwuity";
|
||||||
|
home = cfg.dataDir;
|
||||||
|
createHome = true;
|
||||||
|
isSystemUser = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
users.groups.continuwuity = {};
|
||||||
|
|
||||||
|
# Systemd service for Continuwuity
|
||||||
|
systemd.services.continuwuity = {
|
||||||
|
description = "Continuwuity Matrix homeserver";
|
||||||
|
after = [ "network.target" ];
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "simple";
|
||||||
|
User = "continuwuity";
|
||||||
|
Group = "continuwuity";
|
||||||
|
WorkingDirectory = cfg.dataDir;
|
||||||
|
ExecStart = "${continuwuityPkg}/bin/conduwuit -c ${configFile}";
|
||||||
|
Restart = "always";
|
||||||
|
RestartSec = "10s";
|
||||||
|
|
||||||
|
# Security hardening
|
||||||
|
NoNewPrivileges = true;
|
||||||
|
ProtectSystem = "strict";
|
||||||
|
ProtectHome = true;
|
||||||
|
ReadWritePaths = [ cfg.dataDir ];
|
||||||
|
PrivateTmp = true;
|
||||||
|
ProtectKernelTunables = true;
|
||||||
|
ProtectKernelModules = true;
|
||||||
|
ProtectControlGroups = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
preStart = ''
|
||||||
|
# Ensure data directory exists with proper permissions
|
||||||
|
mkdir -p ${cfg.dataDir}/db
|
||||||
|
chown -R continuwuity:continuwuity ${cfg.dataDir}
|
||||||
|
chmod -R 750 ${cfg.dataDir}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Open firewall port only when service is enabled
|
||||||
|
networking.firewall.allowedTCPPorts = [ cfg.port ];
|
||||||
|
|
||||||
|
# Ensure data directories exist with proper permissions
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d ${cfg.dataDir} 0750 continuwuity continuwuity -"
|
||||||
|
"d ${cfg.dataDir}/db 0750 continuwuity continuwuity -"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
23
modules/matrix-secrets/README.md
Normal file
23
modules/matrix-secrets/README.md
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Matrix secret mapping module
|
||||||
|
|
||||||
|
Provides a declarative helper for mapping entries in `secrets/secrets.yaml`
|
||||||
|
to the file paths expected by Matrix services. Each secret is defined once,
|
||||||
|
then delivered via sops-nix to `/run/secrets/...` with the correct owner and
|
||||||
|
permissions.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
matrix.secrets = {
|
||||||
|
"matrix-services.mautrix_whatsapp.as_token" = {
|
||||||
|
key = "matrix-services.mautrix_whatsapp.as_token";
|
||||||
|
path = "/run/secrets/mautrix-whatsapp-as-token";
|
||||||
|
user = "mautrix-whatsapp";
|
||||||
|
group = "mautrix-whatsapp";
|
||||||
|
mode = "0400";
|
||||||
|
};
|
||||||
|
# ...
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
62
modules/matrix-secrets/default.nix
Normal file
62
modules/matrix-secrets/default.nix
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
{ lib, config, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
options.matrix.secrets = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf (lib.types.submodule (
|
||||||
|
{ name, ... }:
|
||||||
|
{ options = {
|
||||||
|
enable = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Enable this Matrix secret.";
|
||||||
|
};
|
||||||
|
key = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Key inside secrets/secrets.yaml.";
|
||||||
|
};
|
||||||
|
path = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Path where decrypted secret should be written.";
|
||||||
|
};
|
||||||
|
user = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = name;
|
||||||
|
description = "Owner user for the secret file.";
|
||||||
|
};
|
||||||
|
group = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = name;
|
||||||
|
description = "Owner group for the secret file.";
|
||||||
|
};
|
||||||
|
mode = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "0400";
|
||||||
|
description = "File permission mode.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
));
|
||||||
|
default = {};
|
||||||
|
description = "Declarative mapping of Matrix service secrets to sops entries.";
|
||||||
|
};
|
||||||
|
|
||||||
|
config = let
|
||||||
|
secrets = config.matrix.secrets;
|
||||||
|
in {
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = lib.all (secret: lib.hasAttr "key" secret && lib.hasAttr "path" secret) (lib.attrValues secrets);
|
||||||
|
message = "Each matrix secret must define both `key` and `path`.";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
sops.secrets = lib.mapAttrs' (_: secret:
|
||||||
|
lib.nameValuePair secret.key {
|
||||||
|
inherit (secret) path;
|
||||||
|
owner = secret.user;
|
||||||
|
group = secret.group;
|
||||||
|
mode = secret.mode;
|
||||||
|
}
|
||||||
|
) secrets;
|
||||||
|
};
|
||||||
|
}
|
||||||
712
modules/mautrix-gmessages.nix
Normal file
712
modules/mautrix-gmessages.nix
Normal file
|
|
@ -0,0 +1,712 @@
|
||||||
|
# mautrix-gmessages Matrix-Google Messages bridge
|
||||||
|
# Bridges Google Messages (RCS/SMS/MMS) to Matrix via web interface
|
||||||
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.services.matrix-vm.mautrix-gmessages;
|
||||||
|
yamlFormat = pkgs.formats.yaml { };
|
||||||
|
|
||||||
|
defaultPermissions = {
|
||||||
|
"*" = "relay";
|
||||||
|
"${cfg.matrix.serverName}" = "user";
|
||||||
|
};
|
||||||
|
|
||||||
|
effectivePermissions =
|
||||||
|
if cfg.bridge.permissions != {} then cfg.bridge.permissions else defaultPermissions;
|
||||||
|
|
||||||
|
exampleConfigPath = "${cfg.package}/share/mautrix-gmessages/example-config.yaml";
|
||||||
|
|
||||||
|
configOverrides = {
|
||||||
|
network = {
|
||||||
|
displayname_template = cfg.network.displaynameTemplate;
|
||||||
|
device_meta = {
|
||||||
|
os = cfg.network.deviceMeta.os;
|
||||||
|
browser = cfg.network.deviceMeta.browser;
|
||||||
|
type = cfg.network.deviceMeta.type;
|
||||||
|
};
|
||||||
|
aggressive_reconnect = cfg.network.aggressiveReconnect;
|
||||||
|
initial_chat_sync_count = cfg.network.initialChatSyncCount;
|
||||||
|
};
|
||||||
|
bridge = {
|
||||||
|
command_prefix = cfg.bridge.commandPrefix;
|
||||||
|
relay = {
|
||||||
|
enabled = cfg.bridge.relay.enabled;
|
||||||
|
admin_only = cfg.bridge.relay.adminOnly;
|
||||||
|
};
|
||||||
|
caption_in_message = cfg.bridge.captionInMessage;
|
||||||
|
portal_message_buffer = cfg.bridge.portalMessageBuffer;
|
||||||
|
permissions = effectivePermissions;
|
||||||
|
};
|
||||||
|
database = {
|
||||||
|
type = cfg.database.type;
|
||||||
|
uri = cfg.database.uri;
|
||||||
|
max_open_conns = cfg.database.maxOpenConnections;
|
||||||
|
max_idle_conns = cfg.database.maxIdleConnections;
|
||||||
|
};
|
||||||
|
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}{{.}}";
|
||||||
|
public_address = cfg.appservice.publicAddress;
|
||||||
|
};
|
||||||
|
double_puppet = {
|
||||||
|
servers = cfg.bridge.additionalDoublePuppetServers;
|
||||||
|
allow_discovery = cfg.bridge.allowDiscovery;
|
||||||
|
secrets = {};
|
||||||
|
};
|
||||||
|
encryption = {
|
||||||
|
allow = cfg.encryption.enable;
|
||||||
|
default = cfg.encryption.default;
|
||||||
|
require = cfg.encryption.require;
|
||||||
|
require_verification = cfg.encryption.requireVerification;
|
||||||
|
allow_key_sharing = cfg.encryption.allowKeySharing;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
registrationBase = {
|
||||||
|
id = cfg.appservice.id;
|
||||||
|
url = "http://${cfg.appservice.hostname}:${toString cfg.appservice.port}";
|
||||||
|
as_token = "__AS_TOKEN__";
|
||||||
|
hs_token = "__HS_TOKEN__";
|
||||||
|
sender_localpart = cfg.appservice.senderLocalpart;
|
||||||
|
rate_limited = false;
|
||||||
|
namespaces = {
|
||||||
|
users = [
|
||||||
|
{
|
||||||
|
regex = "^@${cfg.appservice.senderLocalpart}:${cfg.matrix.serverName}$";
|
||||||
|
exclusive = true;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
regex = "^@${cfg.appservice.userPrefix}.*:${cfg.matrix.serverName}$";
|
||||||
|
exclusive = true;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
"de.sorunome.msc2409.push_ephemeral" = cfg.appservice.pushEphemeral;
|
||||||
|
receive_ephemeral = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
registrationFile = yamlFormat.generate "mautrix-gmessages-registration.yaml" registrationBase;
|
||||||
|
|
||||||
|
# LoadCredential directives for secure secret injection
|
||||||
|
loadCredentials =
|
||||||
|
[
|
||||||
|
"as_token:${cfg.appservice.asTokenFile}"
|
||||||
|
"hs_token:${cfg.appservice.hsTokenFile}"
|
||||||
|
]
|
||||||
|
++ optional (cfg.appservice.provisioningSharedSecretFile != null)
|
||||||
|
"provisioning_shared_secret:${cfg.appservice.provisioningSharedSecretFile}"
|
||||||
|
++ optional (cfg.matrix.loginSharedSecretFile != null)
|
||||||
|
"login_shared_secret:${cfg.matrix.loginSharedSecretFile}";
|
||||||
|
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.services.matrix-vm.mautrix-gmessages = {
|
||||||
|
enable = mkEnableOption "mautrix-gmessages Matrix-Google Messages bridge";
|
||||||
|
|
||||||
|
package = mkOption {
|
||||||
|
type = types.package;
|
||||||
|
default = pkgs.mautrix-gmessages;
|
||||||
|
description = "Package providing the bridge executable.";
|
||||||
|
};
|
||||||
|
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "mautrix_gmessages";
|
||||||
|
description = "System user for the bridge.";
|
||||||
|
};
|
||||||
|
|
||||||
|
group = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "mautrix_gmessages";
|
||||||
|
description = "Primary group for the bridge.";
|
||||||
|
};
|
||||||
|
|
||||||
|
dataDir = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
default = "/var/lib/mautrix_gmessages";
|
||||||
|
description = "State directory.";
|
||||||
|
};
|
||||||
|
|
||||||
|
runtimeDirectory = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "mautrix_gmessages";
|
||||||
|
description = "Runtime directory name under /run.";
|
||||||
|
};
|
||||||
|
|
||||||
|
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:6167";
|
||||||
|
description = "Matrix homeserver URL for bridge connections";
|
||||||
|
};
|
||||||
|
|
||||||
|
serverName = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
description = "Matrix server name for bridge users";
|
||||||
|
};
|
||||||
|
|
||||||
|
verifySSL = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Verify SSL certificates when connecting to homeserver";
|
||||||
|
};
|
||||||
|
|
||||||
|
asmux = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Enable asmux protocol";
|
||||||
|
};
|
||||||
|
|
||||||
|
loginSharedSecretFile = mkOption {
|
||||||
|
type = types.nullOr types.path;
|
||||||
|
default = null;
|
||||||
|
description = "Shared secret file for automatic double puppeting login";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Database configuration
|
||||||
|
database = {
|
||||||
|
type = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "postgres";
|
||||||
|
description = "Database type (sqlite3 or postgres)";
|
||||||
|
};
|
||||||
|
|
||||||
|
uri = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "sqlite:///var/lib/mautrix_gmessages/gmessages.db?_txlock=immediate";
|
||||||
|
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";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Network configuration (formerly google_messages)
|
||||||
|
network = {
|
||||||
|
displaynameTemplate = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "{{or .FullName .PhoneNumber}}";
|
||||||
|
description = "Display name template for SMS users";
|
||||||
|
};
|
||||||
|
|
||||||
|
deviceMeta = {
|
||||||
|
os = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "mautrix-gmessages";
|
||||||
|
description = "OS name shown in paired devices list";
|
||||||
|
};
|
||||||
|
|
||||||
|
browser = mkOption {
|
||||||
|
type = types.enum ["OTHER" "CHROME" "FIREFOX" "SAFARI" "OPERA" "IE" "EDGE"];
|
||||||
|
default = "OTHER";
|
||||||
|
description = "Browser type shown to phone";
|
||||||
|
};
|
||||||
|
|
||||||
|
type = mkOption {
|
||||||
|
type = types.enum ["WEB" "TABLET" "PWA"];
|
||||||
|
default = "TABLET";
|
||||||
|
description = "Device type - affects icon and session limits";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
aggressiveReconnect = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Aggressively set bridge as active device if browser opens";
|
||||||
|
};
|
||||||
|
|
||||||
|
initialChatSyncCount = mkOption {
|
||||||
|
type = types.int;
|
||||||
|
default = 25;
|
||||||
|
description = "Number of chats to sync when connecting";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# 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 = "!gm";
|
||||||
|
description = "Command prefix for bridge commands";
|
||||||
|
};
|
||||||
|
|
||||||
|
permissions = mkOption {
|
||||||
|
type = types.attrsOf types.str;
|
||||||
|
default = {};
|
||||||
|
description = "Bridge permissions mapping";
|
||||||
|
};
|
||||||
|
|
||||||
|
relay = {
|
||||||
|
enabled = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Enable relay mode";
|
||||||
|
};
|
||||||
|
|
||||||
|
adminOnly = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Restrict relay to admins only";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
captionInMessage = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Include captions in message body";
|
||||||
|
};
|
||||||
|
|
||||||
|
portalMessageBuffer = mkOption {
|
||||||
|
type = types.int;
|
||||||
|
default = 128;
|
||||||
|
description = "Portal message buffer size";
|
||||||
|
};
|
||||||
|
|
||||||
|
additionalDoublePuppetServers = mkOption {
|
||||||
|
type = types.attrsOf types.str;
|
||||||
|
default = {};
|
||||||
|
description = "Additional servers for double puppeting";
|
||||||
|
};
|
||||||
|
|
||||||
|
allowDiscovery = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Allow discovery of double puppet servers";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Appservice configuration
|
||||||
|
appservice = {
|
||||||
|
id = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "gmessages";
|
||||||
|
description = "Appservice ID";
|
||||||
|
};
|
||||||
|
|
||||||
|
hostname = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "127.0.0.1";
|
||||||
|
description = "Hostname for the appservice";
|
||||||
|
};
|
||||||
|
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 9556;
|
||||||
|
description = "Port for the appservice API";
|
||||||
|
};
|
||||||
|
|
||||||
|
senderLocalpart = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "gmessagesbot";
|
||||||
|
description = "Localpart of the bridge bot user";
|
||||||
|
};
|
||||||
|
|
||||||
|
botDisplayName = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "Google Messages Bridge Bot";
|
||||||
|
description = "Display name for the bridge bot";
|
||||||
|
};
|
||||||
|
|
||||||
|
botAvatar = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "mxc://maunium.net/ygtkteZsXnGJLJHRchUwYWak";
|
||||||
|
description = "Avatar MXC URI for the bridge bot";
|
||||||
|
};
|
||||||
|
|
||||||
|
userPrefix = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "_gmessages_";
|
||||||
|
description = "Prefix for Matrix users bridged from Google Messages";
|
||||||
|
};
|
||||||
|
|
||||||
|
aliasPrefix = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "gmessages_";
|
||||||
|
description = "Prefix for Matrix aliases/rooms";
|
||||||
|
};
|
||||||
|
|
||||||
|
publicAddress = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
description = "Public URL for the bridge (used for media endpoints).";
|
||||||
|
};
|
||||||
|
|
||||||
|
pushEphemeral = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Enable push ephemeral events";
|
||||||
|
};
|
||||||
|
|
||||||
|
asTokenFile = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
description = "Path to file containing appservice token";
|
||||||
|
};
|
||||||
|
|
||||||
|
hsTokenFile = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
description = "Path to file containing homeserver token";
|
||||||
|
};
|
||||||
|
|
||||||
|
provisioningSharedSecretFile = mkOption {
|
||||||
|
type = types.nullOr types.path;
|
||||||
|
default = null;
|
||||||
|
description = "Path to file containing provisioning shared secret";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Backfill configuration
|
||||||
|
backfill = {
|
||||||
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Enable message backfilling";
|
||||||
|
};
|
||||||
|
|
||||||
|
maxInitialMessages = mkOption {
|
||||||
|
type = types.int;
|
||||||
|
default = 50;
|
||||||
|
description = "Maximum messages to backfill in empty rooms";
|
||||||
|
};
|
||||||
|
|
||||||
|
maxCatchupMessages = mkOption {
|
||||||
|
type = types.int;
|
||||||
|
default = 500;
|
||||||
|
description = "Maximum missed messages to backfill after restart";
|
||||||
|
};
|
||||||
|
|
||||||
|
unreadHoursThreshold = mkOption {
|
||||||
|
type = types.int;
|
||||||
|
default = 720;
|
||||||
|
description = "Mark old backfilled chats as read after this many hours";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# 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";
|
||||||
|
};
|
||||||
|
|
||||||
|
requireVerification = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Require device verification for encryption";
|
||||||
|
};
|
||||||
|
|
||||||
|
allowKeySharing = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Allow sharing encryption keys";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Computed options (read-only)
|
||||||
|
registrationFile = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
readOnly = true;
|
||||||
|
description = "Path to the generated registration file";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
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.members = lib.mkAfter [ cfg.user ];
|
||||||
|
|
||||||
|
# Systemd service with comprehensive hardening
|
||||||
|
systemd.services.mautrix-gmessages = {
|
||||||
|
description = "mautrix-gmessages Matrix-Google Messages bridge";
|
||||||
|
after = [ "network.target" "postgresql.service" ];
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "simple";
|
||||||
|
User = cfg.user;
|
||||||
|
Group = cfg.group;
|
||||||
|
SupplementaryGroups = [ "matrix-appservices" ];
|
||||||
|
PermissionsStartOnly = true;
|
||||||
|
WorkingDirectory = cfg.dataDir;
|
||||||
|
|
||||||
|
# Use StateDirectory for persistent config and data management
|
||||||
|
StateDirectory = cfg.user;
|
||||||
|
StateDirectoryMode = "0750";
|
||||||
|
LogsDirectory = cfg.user;
|
||||||
|
|
||||||
|
# LoadCredential directives for secure secret injection
|
||||||
|
LoadCredential = loadCredentials;
|
||||||
|
|
||||||
|
# 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-gmessages-prepare-config" ''
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CONFIG_DIR=/var/lib/${cfg.user}/config
|
||||||
|
CONFIG_PATH="$CONFIG_DIR/config.yaml"
|
||||||
|
REG_PATH="$CONFIG_DIR/registration.yaml"
|
||||||
|
|
||||||
|
cp ${exampleConfigPath} "$CONFIG_PATH"
|
||||||
|
cp ${registrationFile} "$REG_PATH"
|
||||||
|
|
||||||
|
${pkgs.python3.withPackages (ps: [ ps.pyyaml ])}/bin/python3 <<'PY'
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import pwd
|
||||||
|
import grp
|
||||||
|
import shutil
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
CONFIG_DIR = pathlib.Path('/var/lib/${cfg.user}/config')
|
||||||
|
CONFIG_PATH = CONFIG_DIR / 'config.yaml'
|
||||||
|
REG_PATH = CONFIG_DIR / 'registration.yaml'
|
||||||
|
SHARED_REG = pathlib.Path('/var/lib/matrix-appservices/mautrix_gmessages_registration.yaml')
|
||||||
|
|
||||||
|
with CONFIG_PATH.open('r', encoding='utf-8') as fh:
|
||||||
|
config = yaml.safe_load(fh)
|
||||||
|
|
||||||
|
with REG_PATH.open('r', encoding='utf-8') as fh:
|
||||||
|
registration = 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:
|
||||||
|
target.setdefault(key, {})
|
||||||
|
deep_update(target[key], value)
|
||||||
|
else:
|
||||||
|
target[key] = value
|
||||||
|
|
||||||
|
deep_update(config, overrides)
|
||||||
|
if extra_cfg:
|
||||||
|
deep_update(config, extra_cfg)
|
||||||
|
|
||||||
|
perms = overrides.get('bridge', {}).get('permissions')
|
||||||
|
if perms is not None:
|
||||||
|
config.setdefault('bridge', {})['permissions'] = perms
|
||||||
|
|
||||||
|
dp_config = config.setdefault('double_puppet', {})
|
||||||
|
servers = overrides.get('double_puppet', {}).get('servers')
|
||||||
|
if servers is not None:
|
||||||
|
dp_config['servers'] = servers
|
||||||
|
dp_config['secrets'] = {}
|
||||||
|
|
||||||
|
creds_dir = os.environ.get('CREDENTIALS_DIRECTORY')
|
||||||
|
as_token = hs_token = provisioning_secret = login_shared_secret = None
|
||||||
|
if creds_dir:
|
||||||
|
creds_path = pathlib.Path(creds_dir)
|
||||||
|
def read_secret(name):
|
||||||
|
path = creds_path / name
|
||||||
|
return path.read_text(encoding='utf-8').strip() if path.exists() else None
|
||||||
|
as_token = read_secret('as_token')
|
||||||
|
hs_token = read_secret('hs_token')
|
||||||
|
provisioning_secret = read_secret('provisioning_shared_secret')
|
||||||
|
login_shared_secret = read_secret('login_shared_secret')
|
||||||
|
|
||||||
|
if as_token:
|
||||||
|
config.setdefault('appservice', {})['as_token'] = as_token
|
||||||
|
registration['as_token'] = as_token
|
||||||
|
if hs_token:
|
||||||
|
config.setdefault('appservice', {})['hs_token'] = hs_token
|
||||||
|
registration['hs_token'] = hs_token
|
||||||
|
|
||||||
|
if overrides.get('appservice', {}).get('public_address') is None:
|
||||||
|
config.setdefault('appservice', {})['public_address'] = None
|
||||||
|
|
||||||
|
if provisioning_secret:
|
||||||
|
config.setdefault('provisioning', {})['shared_secret'] = provisioning_secret
|
||||||
|
else:
|
||||||
|
config.setdefault('provisioning', {})['shared_secret'] = 'disable'
|
||||||
|
|
||||||
|
if login_shared_secret:
|
||||||
|
login_secret_value = login_shared_secret
|
||||||
|
dp_config.setdefault('secrets', {})['${cfg.matrix.serverName}'] = login_secret_value
|
||||||
|
|
||||||
|
with CONFIG_PATH.open('w', encoding='utf-8') as fh:
|
||||||
|
yaml.safe_dump(config, fh, default_flow_style=False, sort_keys=False)
|
||||||
|
|
||||||
|
with REG_PATH.open('w', encoding='utf-8') as fh:
|
||||||
|
yaml.safe_dump(registration, 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)
|
||||||
|
os.chown(REG_PATH, uid, gid_service)
|
||||||
|
os.chmod(REG_PATH, 0o640)
|
||||||
|
|
||||||
|
SHARED_REG.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copyfile(REG_PATH, SHARED_REG)
|
||||||
|
os.chown(SHARED_REG, uid, gid_shared)
|
||||||
|
os.chmod(SHARED_REG, 0o640)
|
||||||
|
PY
|
||||||
|
''}"
|
||||||
|
];
|
||||||
|
|
||||||
|
# Start mautrix-gmessages with state directory config
|
||||||
|
ExecStart = "${cfg.package}/bin/mautrix-gmessages -c /var/lib/${cfg.user}/config/config.yaml";
|
||||||
|
|
||||||
|
# Restart policy
|
||||||
|
Restart = "always";
|
||||||
|
RestartSec = 10;
|
||||||
|
|
||||||
|
# Security hardening following NixOS best practices
|
||||||
|
# NoNewPrivileges = true; # Temporarily disabled to debug exit code 11
|
||||||
|
# ProtectSystem = "strict"; # Temporarily disabled to debug exit code 11
|
||||||
|
# ProtectHome = true; # Temporarily disabled to debug exit code 11
|
||||||
|
# PrivateTmp = true; # Temporarily disabled to debug exit code 11
|
||||||
|
# PrivateDevices = true; # Temporarily disabled to debug exit code 11
|
||||||
|
ProtectKernelTunables = true;
|
||||||
|
ProtectKernelModules = true;
|
||||||
|
ProtectControlGroups = true;
|
||||||
|
|
||||||
|
# Allow writing to data, log, and shared appservices directory
|
||||||
|
ReadWritePaths = [
|
||||||
|
cfg.dataDir
|
||||||
|
"/var/log/${cfg.user}"
|
||||||
|
"/var/lib/matrix-appservices"
|
||||||
|
];
|
||||||
|
|
||||||
|
# Network restrictions
|
||||||
|
# RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; # Temporarily disabled to debug exit code 11
|
||||||
|
|
||||||
|
# System calls - Bridge needs broader access for Google Messages protocol
|
||||||
|
SystemCallArchitectures = "native";
|
||||||
|
# SystemCallFilter = [
|
||||||
|
# "@system-service"
|
||||||
|
# "@network-io"
|
||||||
|
# "@file-system"
|
||||||
|
# "@process"
|
||||||
|
# "@io-event"
|
||||||
|
# # Go runtime specific syscalls
|
||||||
|
# "futex"
|
||||||
|
# "clone"
|
||||||
|
# "arch_prctl"
|
||||||
|
# "mprotect"
|
||||||
|
# "mmap"
|
||||||
|
# "munmap"
|
||||||
|
# "set_robust_list"
|
||||||
|
# "sigaltstack"
|
||||||
|
# "rt_sigreturn"
|
||||||
|
# "rt_sigprocmask"
|
||||||
|
# "sched_getaffinity"
|
||||||
|
# # Required for chown/chmod operations in ExecStartPre
|
||||||
|
# "fchownat"
|
||||||
|
# "fchmodat"
|
||||||
|
# ];
|
||||||
|
|
||||||
|
# Resource limits
|
||||||
|
MemoryMax = "256M";
|
||||||
|
CPUWeight = 50; # Lower priority than Matrix server
|
||||||
|
IOWeight = 50;
|
||||||
|
|
||||||
|
# Process security - files created as 640 (owner rw, group r, other none)
|
||||||
|
UMask = "0027";
|
||||||
|
LockPersonality = true;
|
||||||
|
RestrictRealtime = true;
|
||||||
|
RestrictSUIDSGID = true;
|
||||||
|
RemoveIPC = true;
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
StandardOutput = "journal";
|
||||||
|
StandardError = "journal";
|
||||||
|
SyslogIdentifier = "mautrix-gmessages";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Add extra packages to PATH if specified
|
||||||
|
path = [ pkgs.coreutils pkgs.gnused ] ++ cfg.extraPackages;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Directory permissions
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d ${cfg.dataDir} 0750 ${cfg.user} ${cfg.group} -"
|
||||||
|
"Z ${cfg.dataDir} 0750 ${cfg.user} ${cfg.group} - -"
|
||||||
|
"d /var/log/${cfg.user} 0750 ${cfg.user} ${cfg.group} -"
|
||||||
|
"Z /var/log/${cfg.user} 0750 ${cfg.user} ${cfg.group} - -"
|
||||||
|
"d /var/lib/matrix-appservices 0775 root matrix-appservices -"
|
||||||
|
];
|
||||||
|
|
||||||
|
# Open firewall port for AS API
|
||||||
|
networking.firewall.allowedTCPPorts = [ cfg.appservice.port ];
|
||||||
|
|
||||||
|
# Set registration file path for integration with Matrix server (shared location)
|
||||||
|
services.matrix-vm.mautrix-gmessages.registrationFile = "/var/lib/matrix-appservices/mautrix_gmessages_registration.yaml";
|
||||||
|
};
|
||||||
|
}
|
||||||
392
modules/mautrix-slack.nix
Normal file
392
modules/mautrix-slack.nix
Normal file
|
|
@ -0,0 +1,392 @@
|
||||||
|
# mautrix-slack Matrix-Slack bridge
|
||||||
|
# Bridges Slack to Matrix via appservice
|
||||||
|
# Implementation follows mautrix-gmessages pattern for config management
|
||||||
|
{ config, pkgs, 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.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 -"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
571
modules/mautrix-whatsapp.nix
Normal file
571
modules/mautrix-whatsapp.nix
Normal file
|
|
@ -0,0 +1,571 @@
|
||||||
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.services.matrix-vm.mautrix-whatsapp;
|
||||||
|
yamlFormat = pkgs.formats.yaml { };
|
||||||
|
registrationDir = builtins.dirOf (toString cfg.appservice.registrationFile);
|
||||||
|
|
||||||
|
baseConfig = {
|
||||||
|
homeserver = {
|
||||||
|
address = cfg.matrix.homeserverUrl;
|
||||||
|
domain = cfg.matrix.serverName;
|
||||||
|
verify_ssl = cfg.matrix.verifySSL;
|
||||||
|
};
|
||||||
|
database = {
|
||||||
|
type = cfg.database.type;
|
||||||
|
uri = cfg.database.uri;
|
||||||
|
max_open_conns = cfg.database.maxOpenConnections;
|
||||||
|
max_idle_conns = cfg.database.maxIdleConnections;
|
||||||
|
};
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
public_address = cfg.appservice.publicAddress;
|
||||||
|
username_template = "${cfg.appservice.userPrefix}{{.}}";
|
||||||
|
protocols = cfg.appservice.protocols;
|
||||||
|
"de.sorunome.msc2409.push_ephemeral" = cfg.appservice.pushEphemeral;
|
||||||
|
};
|
||||||
|
network = {
|
||||||
|
os_name = cfg.network.osName;
|
||||||
|
browser_name = cfg.network.browserName;
|
||||||
|
direct_media_auto_request = cfg.network.directMediaAutoRequest;
|
||||||
|
initial_auto_reconnect = cfg.network.initialAutoReconnect;
|
||||||
|
history_sync = {
|
||||||
|
max_initial_conversations = cfg.network.historySync.maxInitialConversations;
|
||||||
|
request_full_sync = cfg.network.historySync.requestFullSync;
|
||||||
|
dispatch_wait = cfg.network.historySync.dispatchWait;
|
||||||
|
media_requests = {
|
||||||
|
auto_request_media = cfg.network.historySync.mediaRequests.autoRequest;
|
||||||
|
request_method = cfg.network.historySync.mediaRequests.requestMethod;
|
||||||
|
request_local_time = cfg.network.historySync.mediaRequests.requestLocalTime;
|
||||||
|
max_async_handle = cfg.network.historySync.mediaRequests.maxAsyncHandle;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
bridge = {
|
||||||
|
command_prefix = cfg.bridge.commandPrefix;
|
||||||
|
personal_filtering_spaces = cfg.bridge.personalFilteringSpaces;
|
||||||
|
bridge_status_notices = cfg.bridge.bridgeStatusNotices;
|
||||||
|
permissions = cfg.bridge.permissions;
|
||||||
|
cleanup_on_logout = {
|
||||||
|
enabled = cfg.bridge.cleanupOnLogout != "nothing";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
double_puppet = {
|
||||||
|
servers = cfg.bridge.additionalDoublePuppetServers;
|
||||||
|
allow_discovery = cfg.bridge.allowDiscovery;
|
||||||
|
secrets = {};
|
||||||
|
};
|
||||||
|
encryption = {
|
||||||
|
allow = cfg.encryption.enable;
|
||||||
|
default = cfg.encryption.default;
|
||||||
|
require = cfg.encryption.require;
|
||||||
|
allow_key_sharing = cfg.encryption.allowKeySharing;
|
||||||
|
};
|
||||||
|
analytics = {
|
||||||
|
token = null;
|
||||||
|
};
|
||||||
|
provisioning = {
|
||||||
|
shared_secret = if cfg.appservice.provisioningSharedSecretFile != null then "__credential__" else "disable";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
configBaseFile = yamlFormat.generate "mautrix-whatsapp-config-base.yaml" baseConfig;
|
||||||
|
|
||||||
|
registrationBase = {
|
||||||
|
id = cfg.appservice.id;
|
||||||
|
url = "http://${cfg.appservice.hostname}:${toString cfg.appservice.port}";
|
||||||
|
sender_localpart = cfg.appservice.senderLocalpart;
|
||||||
|
rate_limited = false;
|
||||||
|
namespaces = {
|
||||||
|
users = [{
|
||||||
|
regex = "@${cfg.appservice.userPrefix}.+:${cfg.matrix.serverName}";
|
||||||
|
exclusive = true;
|
||||||
|
}];
|
||||||
|
aliases = [{
|
||||||
|
regex = "#${cfg.appservice.aliasPrefix}.*:${cfg.matrix.serverName}";
|
||||||
|
exclusive = true;
|
||||||
|
}];
|
||||||
|
rooms = [];
|
||||||
|
};
|
||||||
|
"de.sorunome.msc2409.push_ephemeral" = cfg.appservice.pushEphemeral;
|
||||||
|
};
|
||||||
|
|
||||||
|
registrationBaseFile = yamlFormat.generate "mautrix-whatsapp-registration-base.yaml" registrationBase;
|
||||||
|
|
||||||
|
extraConfigFile = optionalString (cfg.extraConfig != { })
|
||||||
|
(toString (yamlFormat.generate "mautrix-whatsapp-extra.yaml" cfg.extraConfig));
|
||||||
|
|
||||||
|
generateConfigScript = pkgs.writeShellScript "generate-mautrix-whatsapp-config" ''
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CONFIG_OUT="/run/${cfg.runtimeDirectory}/config.yaml"
|
||||||
|
REG_OUT="${cfg.appservice.registrationFile}"
|
||||||
|
|
||||||
|
# Runtime directories created by systemd.tmpfiles.d
|
||||||
|
|
||||||
|
${pkgs.python3.withPackages (ps: [ ps.pyyaml ])}/bin/python3 <<'PY'
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import pwd
|
||||||
|
import grp
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
base_config_path = pathlib.Path("${configBaseFile}")
|
||||||
|
config_output = pathlib.Path("/run/${cfg.runtimeDirectory}/config.yaml")
|
||||||
|
registration_base_path = pathlib.Path("${registrationBaseFile}")
|
||||||
|
registration_output = pathlib.Path("${cfg.appservice.registrationFile}")
|
||||||
|
extra_config_path = "${extraConfigFile}"
|
||||||
|
|
||||||
|
with base_config_path.open("r", encoding="utf-8") as fh:
|
||||||
|
config = yaml.safe_load(fh)
|
||||||
|
|
||||||
|
if extra_config_path:
|
||||||
|
with open(extra_config_path, "r", encoding="utf-8") as fh:
|
||||||
|
extra_cfg = yaml.safe_load(fh) or {}
|
||||||
|
def deep_merge(base, extra):
|
||||||
|
for key, value in extra.items():
|
||||||
|
if isinstance(value, dict) and isinstance(base.get(key), dict):
|
||||||
|
deep_merge(base[key], value)
|
||||||
|
else:
|
||||||
|
base[key] = value
|
||||||
|
return base
|
||||||
|
deep_merge(config, extra_cfg)
|
||||||
|
|
||||||
|
creds_dir = pathlib.Path(os.environ.get("CREDENTIALS_DIRECTORY", ""))
|
||||||
|
if not creds_dir:
|
||||||
|
raise SystemExit("CREDENTIALS_DIRECTORY not provided")
|
||||||
|
|
||||||
|
def read_required(name):
|
||||||
|
path = creds_dir / name
|
||||||
|
if not path.exists():
|
||||||
|
raise SystemExit(f"Missing credential {name}")
|
||||||
|
return path.read_text(encoding="utf-8").strip()
|
||||||
|
|
||||||
|
def read_optional(name):
|
||||||
|
path = creds_dir / name
|
||||||
|
if path.exists():
|
||||||
|
return path.read_text(encoding="utf-8").strip()
|
||||||
|
return None
|
||||||
|
|
||||||
|
as_token = read_required("as_token")
|
||||||
|
hs_token = read_required("hs_token")
|
||||||
|
login_secret = read_optional("login_shared_secret")
|
||||||
|
provisioning_secret = read_optional("provisioning_shared_secret")
|
||||||
|
|
||||||
|
config.setdefault("double_puppet", {}).setdefault("secrets", {})
|
||||||
|
if login_secret:
|
||||||
|
config["double_puppet"]["secrets"]["${cfg.matrix.serverName}"] = login_secret
|
||||||
|
|
||||||
|
if provisioning_secret:
|
||||||
|
config.setdefault("provisioning", {})["shared_secret"] = provisioning_secret
|
||||||
|
else:
|
||||||
|
config.setdefault("provisioning", {})["shared_secret"] = "disable"
|
||||||
|
|
||||||
|
with registration_base_path.open("r", encoding="utf-8") as fh:
|
||||||
|
registration = yaml.safe_load(fh)
|
||||||
|
|
||||||
|
registration["as_token"] = as_token
|
||||||
|
registration["hs_token"] = hs_token
|
||||||
|
|
||||||
|
# Add tokens to config appservice section
|
||||||
|
config.setdefault("appservice", {})["as_token"] = as_token
|
||||||
|
config.setdefault("appservice", {})["hs_token"] = hs_token
|
||||||
|
|
||||||
|
config_output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with config_output.open("w", encoding="utf-8") as fh:
|
||||||
|
yaml.safe_dump(config, fh, default_flow_style=False, sort_keys=False)
|
||||||
|
|
||||||
|
registration_output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with registration_output.open("w", encoding="utf-8") as fh:
|
||||||
|
yaml.safe_dump(registration, fh, default_flow_style=False, sort_keys=False)
|
||||||
|
|
||||||
|
svc_uid = pwd.getpwnam("${cfg.user}").pw_uid
|
||||||
|
svc_gid = grp.getgrnam("${cfg.group}").gr_gid
|
||||||
|
reg_uid = pwd.getpwnam("${cfg.appservice.registrationUser}").pw_uid
|
||||||
|
reg_gid = grp.getgrnam("${cfg.appservice.registrationGroup}").gr_gid
|
||||||
|
|
||||||
|
os.chown(config_output, svc_uid, svc_gid)
|
||||||
|
os.chmod(config_output, 0o640)
|
||||||
|
os.chown(registration_output, reg_uid, reg_gid)
|
||||||
|
os.chmod(registration_output, 0o640)
|
||||||
|
PY
|
||||||
|
'';
|
||||||
|
|
||||||
|
loadCredentials =
|
||||||
|
[
|
||||||
|
"as_token:${cfg.appservice.asTokenFile}"
|
||||||
|
"hs_token:${cfg.appservice.hsTokenFile}"
|
||||||
|
]
|
||||||
|
++ optional (cfg.matrix.loginSharedSecretFile != null)
|
||||||
|
"login_shared_secret:${cfg.matrix.loginSharedSecretFile}"
|
||||||
|
++ optional (cfg.appservice.provisioningSharedSecretFile != null)
|
||||||
|
"provisioning_shared_secret:${cfg.appservice.provisioningSharedSecretFile}";
|
||||||
|
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.services.matrix-vm.mautrix-whatsapp = {
|
||||||
|
enable = mkEnableOption "mautrix-whatsapp bridge";
|
||||||
|
|
||||||
|
package = mkOption {
|
||||||
|
type = types.package;
|
||||||
|
default = pkgs.mautrix-whatsapp;
|
||||||
|
description = "Package providing the bridge executable.";
|
||||||
|
};
|
||||||
|
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "mautrix_whatsapp";
|
||||||
|
description = "System user for the bridge.";
|
||||||
|
};
|
||||||
|
|
||||||
|
group = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "mautrix_whatsapp";
|
||||||
|
description = "Primary group for the bridge.";
|
||||||
|
};
|
||||||
|
|
||||||
|
dataDir = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
default = "/var/lib/mautrix_whatsapp";
|
||||||
|
description = "State directory.";
|
||||||
|
};
|
||||||
|
|
||||||
|
runtimeDirectory = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "mautrix_whatsapp";
|
||||||
|
description = "Runtime directory name under /run.";
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
homeserverUrl = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "http://127.0.0.1:6167";
|
||||||
|
description = "Homeserver URL (bridge -> HS).";
|
||||||
|
};
|
||||||
|
serverName = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "example.com";
|
||||||
|
description = "Matrix server name.";
|
||||||
|
};
|
||||||
|
verifySSL = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Verify TLS for homeserver requests.";
|
||||||
|
};
|
||||||
|
loginSharedSecretFile = mkOption {
|
||||||
|
type = types.nullOr types.path;
|
||||||
|
default = null;
|
||||||
|
description = "Shared secret file for automatic double puppeting.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
database = {
|
||||||
|
type = mkOption {
|
||||||
|
type = types.enum [ "postgres" "sqlite3-fk-wal" ];
|
||||||
|
default = "postgres";
|
||||||
|
description = "Database backend.";
|
||||||
|
};
|
||||||
|
uri = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "postgresql:///mautrix_whatsapp?host=/run/postgresql";
|
||||||
|
description = "Database connection string.";
|
||||||
|
};
|
||||||
|
maxOpenConnections = mkOption {
|
||||||
|
type = types.ints.positive;
|
||||||
|
default = 10;
|
||||||
|
description = "Maximum open DB connections.";
|
||||||
|
};
|
||||||
|
maxIdleConnections = mkOption {
|
||||||
|
type = types.ints.unsigned;
|
||||||
|
default = 2;
|
||||||
|
description = "Maximum idle DB connections.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
appservice = {
|
||||||
|
id = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "whatsapp";
|
||||||
|
description = "Appservice ID.";
|
||||||
|
};
|
||||||
|
senderLocalpart = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "whatsappbot";
|
||||||
|
description = "Bot localpart.";
|
||||||
|
};
|
||||||
|
botDisplayName = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = "WhatsApp bridge";
|
||||||
|
description = "Bot display name.";
|
||||||
|
};
|
||||||
|
botAvatar = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = "mxc://maunium.net/NeXNQarUbrlYBiPCpprYsRqr";
|
||||||
|
description = "Bot avatar MXC URI.";
|
||||||
|
};
|
||||||
|
hostname = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "127.0.0.1";
|
||||||
|
description = "Bind address for bridge HTTP listener.";
|
||||||
|
};
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 29318;
|
||||||
|
description = "Port for bridge HTTP listener.";
|
||||||
|
};
|
||||||
|
publicAddress = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
description = "Public URL for bridge (optional).";
|
||||||
|
};
|
||||||
|
asTokenFile = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
description = "Path containing the appservice as_token.";
|
||||||
|
};
|
||||||
|
hsTokenFile = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
description = "Path containing the appservice hs_token.";
|
||||||
|
};
|
||||||
|
provisioningSharedSecretFile = mkOption {
|
||||||
|
type = types.nullOr types.path;
|
||||||
|
default = null;
|
||||||
|
description = "Provisioning API shared secret file (optional).";
|
||||||
|
};
|
||||||
|
registrationFile = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
default = "/var/lib/matrix-appservices/mautrix_whatsapp_registration.yaml";
|
||||||
|
description = "Generated registration file path.";
|
||||||
|
};
|
||||||
|
registrationUser = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = cfg.user;
|
||||||
|
description = "Owner user for the registration file.";
|
||||||
|
};
|
||||||
|
registrationGroup = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = cfg.group;
|
||||||
|
description = "Owner group for the registration file.";
|
||||||
|
};
|
||||||
|
userPrefix = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "whatsapp_";
|
||||||
|
description = "MXID prefix for puppets.";
|
||||||
|
};
|
||||||
|
aliasPrefix = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "whatsapp_";
|
||||||
|
description = "Alias prefix for portals.";
|
||||||
|
};
|
||||||
|
protocols = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = [];
|
||||||
|
description = "Optional MSC3079 protocol identifiers.";
|
||||||
|
};
|
||||||
|
pushEphemeral = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Enable MSC2409 push ephemeral events.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
network = {
|
||||||
|
osName = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "mautrix-whatsapp";
|
||||||
|
description = "Device name shown in WhatsApp linked devices.";
|
||||||
|
};
|
||||||
|
browserName = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "unknown";
|
||||||
|
description = "Browser indicator shown in WhatsApp linked devices.";
|
||||||
|
};
|
||||||
|
directMediaAutoRequest = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Automatically request expired media during backfill.";
|
||||||
|
};
|
||||||
|
initialAutoReconnect = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Retry connection failures automatically on startup.";
|
||||||
|
};
|
||||||
|
historySync = {
|
||||||
|
maxInitialConversations = mkOption {
|
||||||
|
type = types.int;
|
||||||
|
default = -1;
|
||||||
|
description = "Chats to create on initial history sync (-1 = all).";
|
||||||
|
};
|
||||||
|
requestFullSync = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Request a full-year history sync on login.";
|
||||||
|
};
|
||||||
|
dispatchWait = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "1m";
|
||||||
|
description = "Wait time for history payloads before backfill.";
|
||||||
|
};
|
||||||
|
mediaRequests = {
|
||||||
|
autoRequest = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Automatically request expired media.";
|
||||||
|
};
|
||||||
|
requestMethod = mkOption {
|
||||||
|
type = types.enum [ "immediate" "local_time" ];
|
||||||
|
default = "immediate";
|
||||||
|
description = "Scheduling mode for automatic media requests.";
|
||||||
|
};
|
||||||
|
requestLocalTime = mkOption {
|
||||||
|
type = types.int;
|
||||||
|
default = 120;
|
||||||
|
description = "Minutes after midnight when using local_time scheduling.";
|
||||||
|
};
|
||||||
|
maxAsyncHandle = mkOption {
|
||||||
|
type = types.ints.positive;
|
||||||
|
default = 2;
|
||||||
|
description = "Maximum concurrent media requests per user.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
bridge = {
|
||||||
|
commandPrefix = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "!wa";
|
||||||
|
description = "Command prefix for management commands.";
|
||||||
|
};
|
||||||
|
personalFilteringSpaces = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Create personal spaces for each login.";
|
||||||
|
};
|
||||||
|
bridgeStatusNotices = mkOption {
|
||||||
|
type = types.enum [ "none" "errors" "all" ];
|
||||||
|
default = "errors";
|
||||||
|
description = "Status notice verbosity.";
|
||||||
|
};
|
||||||
|
cleanupOnLogout = mkOption {
|
||||||
|
type = types.enum [ "nothing" "kick" "unbridge" "delete" ];
|
||||||
|
default = "nothing";
|
||||||
|
description = "Portal cleanup behaviour on logout.";
|
||||||
|
};
|
||||||
|
permissions = mkOption {
|
||||||
|
type = types.attrsOf (types.enum [ "relay" "commands" "user" "admin" ]);
|
||||||
|
default = { "*" = "relay"; };
|
||||||
|
description = "Permission mapping for users/domains.";
|
||||||
|
};
|
||||||
|
allowDiscovery = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Allow double puppeting discovery for remote homeservers.";
|
||||||
|
};
|
||||||
|
additionalDoublePuppetServers = mkOption {
|
||||||
|
type = types.attrsOf types.str;
|
||||||
|
default = {};
|
||||||
|
description = "Extra homeservers permitted for double puppeting (domain -> client API URL).";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
encryption = {
|
||||||
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Enable end-to-bridge encryption.";
|
||||||
|
};
|
||||||
|
default = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Enable encryption in newly created portals.";
|
||||||
|
};
|
||||||
|
require = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Drop plaintext messages in encrypted rooms.";
|
||||||
|
};
|
||||||
|
allowKeySharing = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Fulfil inbound key requests.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
users.users.${cfg.user} = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = cfg.group;
|
||||||
|
home = cfg.dataDir;
|
||||||
|
createHome = false;
|
||||||
|
};
|
||||||
|
users.groups.${cfg.group} = {};
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d ${cfg.dataDir} 0750 ${cfg.user} ${cfg.group} -"
|
||||||
|
"d ${registrationDir} 2770 root matrix-appservices -"
|
||||||
|
"d /run/${cfg.runtimeDirectory} 0750 ${cfg.user} ${cfg.group} -"
|
||||||
|
];
|
||||||
|
|
||||||
|
systemd.services.mautrix-whatsapp = {
|
||||||
|
description = "mautrix-whatsapp Matrix bridge";
|
||||||
|
after = [ "network.target" "network-online.target" ]
|
||||||
|
++ optional (cfg.database.type == "postgres") "postgresql.service";
|
||||||
|
wants = [ "network-online.target" ];
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
path = [ cfg.package ] ++ cfg.extraPackages;
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "simple";
|
||||||
|
User = cfg.user;
|
||||||
|
Group = cfg.group;
|
||||||
|
WorkingDirectory = cfg.dataDir;
|
||||||
|
# Runtime directory created by systemd.tmpfiles.d
|
||||||
|
ExecStartPre = generateConfigScript;
|
||||||
|
ExecStart = "${cfg.package}/bin/mautrix-whatsapp -c /run/${cfg.runtimeDirectory}/config.yaml";
|
||||||
|
Restart = "always";
|
||||||
|
RestartSec = "10s";
|
||||||
|
NoNewPrivileges = true;
|
||||||
|
ProtectSystem = "strict";
|
||||||
|
ProtectHome = true;
|
||||||
|
PrivateTmp = true;
|
||||||
|
PrivateDevices = true;
|
||||||
|
ProtectKernelModules = true;
|
||||||
|
ProtectKernelTunables = true;
|
||||||
|
ProtectKernelLogs = true;
|
||||||
|
ProtectClock = true;
|
||||||
|
ProtectControlGroups = true;
|
||||||
|
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
|
||||||
|
RestrictSUIDSGID = true;
|
||||||
|
RestrictRealtime = true;
|
||||||
|
LockPersonality = true;
|
||||||
|
UMask = "0002";
|
||||||
|
SystemCallArchitectures = "native";
|
||||||
|
ReadWritePaths = [ cfg.dataDir cfg.appservice.registrationFile registrationDir ];
|
||||||
|
LoadCredential = loadCredentials;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
61
modules/security/fail2ban.nix
Normal file
61
modules/security/fail2ban.nix
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
# Fail2ban configuration for protecting against brute force attacks
|
||||||
|
{ config, lib, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
|
||||||
|
{
|
||||||
|
options.security.fail2ban-enhanced = {
|
||||||
|
enable = mkEnableOption "enhanced fail2ban protection";
|
||||||
|
|
||||||
|
homeIP = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
example = "10.0.0.0/24";
|
||||||
|
description = "Home IP or network to whitelist";
|
||||||
|
};
|
||||||
|
|
||||||
|
bantime = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "1h";
|
||||||
|
description = "Ban duration";
|
||||||
|
};
|
||||||
|
|
||||||
|
maxretry = mkOption {
|
||||||
|
type = types.int;
|
||||||
|
default = 3;
|
||||||
|
description = "Maximum retry attempts";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf config.security.fail2ban-enhanced.enable {
|
||||||
|
services.fail2ban = {
|
||||||
|
enable = true;
|
||||||
|
maxretry = config.security.fail2ban-enhanced.maxretry;
|
||||||
|
bantime = config.security.fail2ban-enhanced.bantime;
|
||||||
|
|
||||||
|
ignoreIP = [
|
||||||
|
"127.0.0.0/8"
|
||||||
|
"::1"
|
||||||
|
] ++ optional (config.security.fail2ban-enhanced.homeIP != null)
|
||||||
|
config.security.fail2ban-enhanced.homeIP;
|
||||||
|
|
||||||
|
jails = {
|
||||||
|
nginx-http-auth = ''
|
||||||
|
enabled = true
|
||||||
|
filter = nginx-http-auth
|
||||||
|
logpath = /var/log/nginx/access.log
|
||||||
|
maxretry = 5
|
||||||
|
bantime = 1h
|
||||||
|
findtime = 10m
|
||||||
|
'';
|
||||||
|
nginx-botsearch = ''
|
||||||
|
enabled = true
|
||||||
|
filter = nginx-botsearch
|
||||||
|
logpath = /var/log/nginx/error.log
|
||||||
|
maxretry = 2
|
||||||
|
bantime = 1h
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
130
modules/security/ssh-hardening.nix
Normal file
130
modules/security/ssh-hardening.nix
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
# SSH hardening configuration for production security
|
||||||
|
{ config, lib, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
|
||||||
|
{
|
||||||
|
options.security.ssh-hardening = {
|
||||||
|
enable = mkEnableOption "SSH security hardening";
|
||||||
|
|
||||||
|
level = mkOption {
|
||||||
|
type = types.enum [ "development" "production" "paranoid" ];
|
||||||
|
default = "production";
|
||||||
|
description = "Security level for SSH configuration";
|
||||||
|
};
|
||||||
|
|
||||||
|
allowUsers = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = [ "admin" ];
|
||||||
|
description = "Users allowed to SSH";
|
||||||
|
};
|
||||||
|
|
||||||
|
maxAuthTries = mkOption {
|
||||||
|
type = types.int;
|
||||||
|
default = 3;
|
||||||
|
description = "Maximum authentication attempts";
|
||||||
|
};
|
||||||
|
|
||||||
|
maxSessions = mkOption {
|
||||||
|
type = types.int;
|
||||||
|
default = 10;
|
||||||
|
description = "Maximum concurrent sessions";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf config.security.ssh-hardening.enable {
|
||||||
|
services.openssh = {
|
||||||
|
enable = true;
|
||||||
|
|
||||||
|
settings = {
|
||||||
|
# Authentication settings
|
||||||
|
PermitRootLogin = if config.security.ssh-hardening.level == "development"
|
||||||
|
then "prohibit-password"
|
||||||
|
else "no";
|
||||||
|
PasswordAuthentication = false;
|
||||||
|
KbdInteractiveAuthentication = false;
|
||||||
|
ChallengeResponseAuthentication = false;
|
||||||
|
|
||||||
|
# Security settings
|
||||||
|
PermitEmptyPasswords = false;
|
||||||
|
UsePAM = false;
|
||||||
|
X11Forwarding = false;
|
||||||
|
AllowAgentForwarding = config.security.ssh-hardening.level == "development";
|
||||||
|
AllowTcpForwarding = config.security.ssh-hardening.level != "paranoid";
|
||||||
|
GatewayPorts = "no";
|
||||||
|
|
||||||
|
# User restrictions
|
||||||
|
AllowUsers = config.security.ssh-hardening.allowUsers;
|
||||||
|
|
||||||
|
# Protocol settings
|
||||||
|
Protocol = 2;
|
||||||
|
LogLevel = if config.security.ssh-hardening.level == "paranoid" then "VERBOSE" else "INFO";
|
||||||
|
|
||||||
|
# Timing settings
|
||||||
|
LoginGraceTime = 30;
|
||||||
|
ClientAliveInterval = 300;
|
||||||
|
ClientAliveCountMax = 2;
|
||||||
|
TCPKeepAlive = false;
|
||||||
|
|
||||||
|
# Crypto settings for security
|
||||||
|
Ciphers = [
|
||||||
|
"chacha20-poly1305@openssh.com"
|
||||||
|
"aes256-gcm@openssh.com"
|
||||||
|
"aes128-gcm@openssh.com"
|
||||||
|
"aes256-ctr"
|
||||||
|
"aes192-ctr"
|
||||||
|
"aes128-ctr"
|
||||||
|
];
|
||||||
|
|
||||||
|
Macs = [
|
||||||
|
"hmac-sha2-256-etm@openssh.com"
|
||||||
|
"hmac-sha2-512-etm@openssh.com"
|
||||||
|
"hmac-sha2-256"
|
||||||
|
"hmac-sha2-512"
|
||||||
|
];
|
||||||
|
|
||||||
|
KexAlgorithms = [
|
||||||
|
"curve25519-sha256@libssh.org"
|
||||||
|
"diffie-hellman-group16-sha512"
|
||||||
|
"diffie-hellman-group18-sha512"
|
||||||
|
"diffie-hellman-group14-sha256"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
extraConfig = ''
|
||||||
|
# Rate limiting
|
||||||
|
MaxAuthTries ${toString config.security.ssh-hardening.maxAuthTries}
|
||||||
|
MaxSessions ${toString config.security.ssh-hardening.maxSessions}
|
||||||
|
MaxStartups 10:30:60
|
||||||
|
|
||||||
|
# Banner
|
||||||
|
${optionalString (config.security.ssh-hardening.level != "development") ''
|
||||||
|
Banner /etc/ssh/banner
|
||||||
|
''}
|
||||||
|
|
||||||
|
# Additional security
|
||||||
|
${optionalString (config.security.ssh-hardening.level == "paranoid") ''
|
||||||
|
StrictModes yes
|
||||||
|
IgnoreRhosts yes
|
||||||
|
HostbasedAuthentication no
|
||||||
|
PermitUserEnvironment no
|
||||||
|
Compression delayed
|
||||||
|
''}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Create SSH banner for non-development environments
|
||||||
|
environment.etc."ssh/banner" = mkIf (config.security.ssh-hardening.level != "development") {
|
||||||
|
text = ''
|
||||||
|
################################################################################
|
||||||
|
# NOTICE #
|
||||||
|
# #
|
||||||
|
# This system is for authorized users only. All activity may be monitored #
|
||||||
|
# and recorded. Unauthorized access is prohibited. #
|
||||||
|
# #
|
||||||
|
################################################################################
|
||||||
|
'';
|
||||||
|
mode = "0644";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
16
secrets/.sops.yaml.example
Normal file
16
secrets/.sops.yaml.example
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# sops-nix configuration for age encryption
|
||||||
|
# Copy this file to .sops.yaml and replace with your age public key
|
||||||
|
#
|
||||||
|
# Generate an age key pair:
|
||||||
|
# age-keygen -o ~/.config/sops/age/keys.txt
|
||||||
|
# # Extract public key: age1... from the generated file
|
||||||
|
#
|
||||||
|
# Then encrypt secrets:
|
||||||
|
# sops -e -i secrets/secrets.yaml
|
||||||
|
|
||||||
|
creation_rules:
|
||||||
|
- path_regex: secrets/secrets\.yaml$
|
||||||
|
key_groups:
|
||||||
|
- age:
|
||||||
|
# Replace with your age public key (starts with age1...)
|
||||||
|
- age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
31
secrets/secrets.yaml.example
Normal file
31
secrets/secrets.yaml.example
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Example secrets file for ops-jrz1 Matrix platform
|
||||||
|
# Copy this file to secrets.yaml and replace with your actual secrets
|
||||||
|
# Then encrypt with: sops -e -i secrets/secrets.yaml
|
||||||
|
|
||||||
|
# Matrix homeserver configuration
|
||||||
|
matrix-registration-token: "GENERATE_WITH_openssl_rand_hex_32"
|
||||||
|
|
||||||
|
# ACME/Let's Encrypt email for certificate notifications
|
||||||
|
acme-email: "admin@example.com"
|
||||||
|
|
||||||
|
# mautrix-slack bridge secrets
|
||||||
|
mautrix-slack:
|
||||||
|
app-token: "xapp-YOUR-SLACK-APP-TOKEN"
|
||||||
|
bot-token: "xoxb-YOUR-SLACK-BOT-TOKEN"
|
||||||
|
|
||||||
|
# mautrix-whatsapp bridge secrets (no long-term secrets, QR code pairing)
|
||||||
|
# Configuration is stored in bridge database after pairing
|
||||||
|
|
||||||
|
# mautrix-gmessages bridge secrets
|
||||||
|
mautrix-gmessages:
|
||||||
|
# Google Messages pairing data stored in bridge database
|
||||||
|
google-account-token: "GENERATED_AFTER_PAIRING"
|
||||||
|
|
||||||
|
# Fail2ban notification email (optional)
|
||||||
|
fail2ban-notification-email: "admin@example.com"
|
||||||
|
|
||||||
|
# PostgreSQL database passwords
|
||||||
|
postgresql:
|
||||||
|
mautrix-slack-password: "GENERATE_SECURE_PASSWORD"
|
||||||
|
mautrix-whatsapp-password: "GENERATE_SECURE_PASSWORD"
|
||||||
|
mautrix-gmessages-password: "GENERATE_SECURE_PASSWORD"
|
||||||
Loading…
Reference in a new issue