ops-jrz1/modules/dev-services.nix
Dan ab5aebb161 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
2025-10-13 14:51:14 -07:00

308 lines
8.6 KiB
Nix

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