add server config

This commit is contained in:
chayleaf 2023-05-11 05:33:08 +07:00
parent 8655459e46
commit 33b4fe95c2
18 changed files with 1150 additions and 55 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
private.nix
private/

View file

@ -1,5 +1,37 @@
{
"nodes": {
"blobs": {
"flake": false,
"locked": {
"lastModified": 1604995301,
"narHash": "sha256-wcLzgLec6SGJA8fx1OEN1yV/Py5b+U5iyYpksUY/yLw=",
"owner": "simple-nixos-mailserver",
"repo": "blobs",
"rev": "2cccdf1ca48316f2cfd1c9a0017e8de5a7156265",
"type": "gitlab"
},
"original": {
"owner": "simple-nixos-mailserver",
"repo": "blobs",
"type": "gitlab"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1668681692,
"narHash": "sha256-Ht91NGdewz8IQLtWZ9LCeNXMSXHUss+9COoqu6JLmXU=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "009399224d5e398d03b22badca40a37ac85412a1",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
@ -83,6 +115,32 @@
"type": "github"
}
},
"nixos-mailserver": {
"inputs": {
"blobs": "blobs",
"flake-compat": "flake-compat",
"nixpkgs": [
"nixpkgs"
],
"nixpkgs-22_11": [
"nixpkgs"
],
"utils": "utils"
},
"locked": {
"lastModified": 1671738303,
"narHash": "sha256-PRgqtaWf2kMSYqVmcnmhTh+UsC0RmvXRTr+EOw5VZUA=",
"owner": "simple-nixos-mailserver",
"repo": "nixos-mailserver",
"rev": "6d0d9fb966cc565a3df74d3b686f924c7615118c",
"type": "gitlab"
},
"original": {
"owner": "simple-nixos-mailserver",
"repo": "nixos-mailserver",
"type": "gitlab"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1683408522,
@ -104,9 +162,10 @@
"impermanence": "impermanence",
"nix-gaming": "nix-gaming",
"nixos-hardware": "nixos-hardware",
"nixos-mailserver": "nixos-mailserver",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay",
"utils": "utils"
"utils": "utils_2"
}
},
"rust-overlay": {
@ -117,11 +176,11 @@
]
},
"locked": {
"lastModified": 1683685144,
"narHash": "sha256-HZBieMZj9Rp+WtbGiK5JaUciZwJ/2YX6LRe5z8jkfow=",
"lastModified": 1683708507,
"narHash": "sha256-i5zgWcuyZcNBnlrzVjhpAFQdJWr4OyhvQ1owAEHFFKw=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "74e44edb87aeed50798f5b81795ebad250d4a0c0",
"rev": "ed55dc022aa23ed3c42f383cf1782290b3b939d5",
"type": "github"
},
"original": {
@ -146,6 +205,21 @@
}
},
"utils": {
"locked": {
"lastModified": 1605370193,
"narHash": "sha256-YyMTf3URDL/otKdKgtoMChu4vfVL3vCMkRqpGifhUn0=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5021eac20303a61fafe17224c087f5519baed54d",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"utils_2": {
"inputs": {
"flake-utils": "flake-utils_2"
},

View file

@ -15,14 +15,25 @@
url = "github:fufexan/nix-gaming";
inputs.nixpkgs.follows = "nixpkgs";
};
nixos-mailserver = {
url = "gitlab:simple-nixos-mailserver/nixos-mailserver";
inputs.nixpkgs.follows = "nixpkgs";
inputs.nixpkgs-22_11.follows = "nixpkgs";
};
};
outputs = inputs@{ self, nixpkgs, utils, nixos-hardware, impermanence, nix-gaming, ... }:
outputs = inputs@{ self, nixpkgs, utils, nixos-hardware, impermanence, nix-gaming, nixos-mailserver, ... }:
let
hw = nixos-hardware.nixosModules;
# IRL-related stuff I'd rather not put into git
priv = if builtins.pathExists ./private.nix then (import ./private.nix) else { };
getPriv = (hostname: with builtins; if hasAttr hostname priv then getAttr hostname priv else { });
priv = if builtins.pathExists ./private/default.nix then (import ./private)
else if builtins.pathExists ./private.nix then (import ./private.nix)
else { };
getPriv = hostname: with builtins; if hasAttr hostname priv then getAttr hostname priv else { };
common = hostname: [ (getPriv hostname) impermanence.nixosModule ];
extraArgs = {
inherit nixpkgs;
};
in utils.lib.mkFlake {
inherit self inputs;
hostDefaults.modules = [
@ -44,18 +55,24 @@
system = "x86_64-linux";
modules = [
./hosts/nixmsi.nix
impermanence.nixosModule
nix-gaming.nixosModules.pipewireLowLatency
hw.common-pc-ssd # enables fstrim
hw.common-cpu-amd # microcode
hw.common-cpu-amd-pstate # amd-pstate
hw.common-gpu-amd # configures drivers
hw.common-pc-laptop # enables tlp
(getPriv "nixmsi")
];
extraArgs = {
inherit nixpkgs;
};
] ++ common "nixmsi";
inherit extraArgs;
};
nixserver = {
system = "x86_64-linux";
modules = [
./hosts/nixserver
nixos-mailserver.nixosModules.default
hw.common-pc-hdd
hw.common-cpu-intel
] ++ common "nixserver";
inherit extraArgs;
};
};
};

View file

@ -108,6 +108,11 @@ in {
opengl.extraPackages = with pkgs; [ vulkan-validation-layers ];
};
services.openssh = {
enable = true;
settings.PasswordAuthentication = false;
};
services.tlp.enable = true;
services.tlp.settings = {
USB_EXCLUDE_PHONE = 1;
@ -207,7 +212,6 @@ in {
++ (lib.range 1714 1764);
networking.firewall.allowedUDPPorts = lib.range 1714 1764;
# networking.hostName = "nixmsi";
networking.wireless.iwd.enable = true;
#networking.networkmanager.enable = true;
@ -327,17 +331,6 @@ in {
documentation.dev.enable = true;
### RANDOM PATCHES ###
# I've had some weird issues with the entire system breaking after
# suspend because of /dev/shm getting nuked, maybe this'll help
services.logind.extraConfig = ''
RemoveIPC=no
'';
# why is this not part of base NixOS?
systemd.tmpfiles.rules = [ "d /var/lib/systemd/pstore 0755 root root 14d" ];
# autologin once after boot
# --skip-login means directly call login instead of first asking for username
# (normally login asks for username too, but getty prefers to do it by itself for whatever reason)

View file

@ -0,0 +1,464 @@
{ lib
, pkgs
, config
, ... }:
let
cfg = config.server;
# TODO: move to lib
quotePotentialIpV6 = addr:
if lib.hasInfix ":" addr then "[${addr}]" else addr;
efiPart = "/dev/disk/by-uuid/3E2A-A5CB";
rootUuid = "6aace237-9b48-4294-8e96-196759a5305b";
rootPart = "/dev/disk/by-uuid/${rootUuid}";
hosted-domains =
map
(prefix: if prefix == null then cfg.domainName else "${prefix}.${cfg.domainName}")
[
null
"dns"
"mumble"
"mail"
"music"
"www"
"matrix"
"search"
"git"
"cloud"
"ns1"
"ns2"
];
in {
imports = [
./options.nix
./matrix.nix
./fdroid.nix
./mumble.nix
];
system.stateVersion = "22.11";
boot = {
initrd = {
availableKernelModules = [ "xhci_pci" "ehci_pci" "ahci" "usb_storage" "sd_mod" "sr_mod" "rtsx_pci_sdmmc" ];
};
kernelModules = [ "kvm-intel" ];
kernelParams = [
"consoleblank=60"
];
loader = {
grub = {
enable = true;
device = "nodev";
version = 2;
efiSupport = true;
efiInstallAsRemovable = true;
gfxmodeEfi = "1920x1080";
gfxmodeBios = "1920x1080";
};
efi.efiSysMountPoint = "/boot/efi";
};
};
hardware.enableRedistributableFirmware = true;
fileSystems = {
"/" = { device = "none"; fsType = "tmpfs"; neededForBoot = true;
options = [ "defaults" "size=2G" "mode=755" ]; };
"/persist" =
{ device = rootPart; fsType = "btrfs"; neededForBoot = true;
options = [ "compress=zstd:15" ]; };
"/boot" =
{ device = rootPart; fsType = "btrfs"; neededForBoot = true;
options = [ "compress=zstd:15" "subvol=boot" ]; };
"/boot/efi" =
{ device = efiPart; fsType = "vfat"; };
};
zramSwap.enable = true;
swapDevices = [ ];
impermanence = {
enable = true;
path = /persist;
directories = [
{ directory = /var/www/${cfg.domainName}; }
{ directory = /var/lib/maubot; }
{ directory = /var/lib/fdroid; }
{ directory = config.mailserver.dkimKeyDirectory; }
{ directory = config.mailserver.mailDirectory; }
{ directory = /home/user; }
{ directory = /root; }
{ directory = /nix; }
];
};
services.beesd = {
filesystems.root = {
spec = "UUID=${rootUuid}";
hashTableSizeMB = 128;
extraOptions = [ "--loadavg-target" "8.0" ];
};
};
i18n.defaultLocale = lib.mkDefault "en_US.UTF-8";
i18n.supportedLocales = lib.mkDefault [
"C.UTF-8/UTF-8"
"en_US.UTF-8/UTF-8"
"en_DK.UTF-8/UTF-8"
];
# ISO-8601
i18n.extraLocaleSettings.LC_TIME = "en_DK.UTF-8";
console.font = "${pkgs.terminus_font}/share/consolefonts/ter-v24n.psf.gz";
networking.useDHCP = true;
networking.resolvconf.extraConfig = ''
name_servers="127.0.0.1 ::1"
'';
networking.firewall = {
enable = true;
allowedTCPPorts = [
# ssh
22
# dns
53 853
# http/s
80 443
];
allowedUDPPorts = [
# dns
53 853
# wireguard
# 5553
];
};
# UNBOUND
services.unbound = {
enable = true;
package = pkgs.unbound-with-systemd.override {
withPythonModule = true;
python = pkgs.python3.withPackages (pkgs: with pkgs; [ pydbus dnspython ]);
};
localControlSocketPath = "/run/unbound/unbound.ctl";
resolveLocalQueries = false;
settings = {
server = {
interface = [ "0.0.0.0" "::" ];
access-control = [ "${cfg.lanCidrV4} allow" "${cfg.lanCidrV6} allow" ];
aggressive-nsec = true;
do-ip6 = true;
module-config = ''"validator iterator"'';
local-zone = [
''"local." static''
] ++ (lib.optionals (cfg.localIpV4 != null || cfg.localIpV6 != null) [
''"${cfg.domainName}." typetransparent''
]);
local-data = builtins.concatLists (map (domain:
lib.optionals (cfg.localIpV4 != null) [
''"${domain}. A ${cfg.localIpV4}"''
] ++ (lib.optionals (cfg.localIpV6 != null) [
''"${domain}. A ${cfg.localIpV6}"''
])) hosted-domains);
};
python.python-script = toString (pkgs.fetchurl {
url = "https://raw.githubusercontent.com/NLnetLabs/unbound/a912786ca9e72dc1ccde98d5af7d23595640043b/pythonmod/examples/avahi-resolver.py";
sha256 = "0r1iqjf08wrkpzvj6pql1jqa884hbbfy9ix5gxdrkrva09msiqgi";
});
remote-control.control-enable = true;
};
};
systemd.services.unbound.environment.MDNS_ACCEPT_NAMES = "^.*\\.local\\.$";
# just in case
networking.hosts."127.0.0.1" = [ "localhost" ] ++ hosted-domains;
# CUPS
services.printing = {
enable = true;
allowFrom = [ cfg.lanCidrV4 cfg.lanCidrV6 ];
browsing = true;
clientConf = ''
ServerName ${cfg.domainName}
'';
defaultShared = true;
drivers = [ pkgs.hplip ];
startWhenNeeded = false;
};
programs.fish.enable = true;
users.defaultUserShell = pkgs.fish;
users.users.user = {
isNormalUser = true;
extraGroups = [ "wheel" config.services.unbound.group ];
};
environment.systemPackages = with pkgs; [
comma
git
vim
wget
# rxvt-unicode-unwrapped.terminfo
kitty.terminfo
tmux
];
services.postgresql.enable = true;
services.postgresql.package = pkgs.postgresql_13;
nix = {
settings = {
allowed-users = [ "user" ];
auto-optimise-store = true;
};
gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than 30d";
};
package = pkgs.nixFlakes;
extraOptions = ''
experimental-features = nix-command flakes
'';
};
systemd.services.nix-daemon.serviceConfig.LimitSTACKSoft = "infinity";
# SSH
services.openssh = {
enable = true;
settings.PermitRootLogin = "no";
settings.PasswordAuthentication = false;
listenAddresses = [{
addr = "0.0.0.0";
} {
addr = "::";
}];
};
services.fail2ban.enable = true;
# SEARXNG
services.searx.enable = true;
services.searx.package = pkgs.searxng.overrideAttrs (_: {
src = pkgs.fetchFromGitHub {
owner = "searxng";
repo = "searxng";
rev = "cb1c3741d7de1354b524589114617f183009f6a8";
sha256 = "sha256-7erY5Bd1ZoTpAIDbhIupu64Xd1PQspaW6vBqu7knzNI=";
};
});
services.searx.runInUwsgi = true;
services.searx.uwsgiConfig = let inherit (config.services.searx) settings; in {
socket = "${quotePotentialIpV6 settings.server.bind_address}:${toString settings.server.port}";
};
users.groups.searx.members = [ "nginx" ];
services.searx.environmentFile = "/etc/nixos/private/searx.env";
services.searx.settings = {
use_default_settings = true;
search = {
safe_search = 0; # Filter results. 0: None, 1: Moderate, 2: Strict
autocomplete = "duckduckgo"; # Existing autocomplete backends: "dbpedia", "duckduckgo", "google", "startpage", "swisscows", "qwant", "wikipedia" - leave blank to turn it off by default
default_lang = ""; # Default search language - leave blank to detect from browser information or use codes from 'languages.py'
};
server = {
port = 8888;
bind_address = "::1";
secret_key = "@SEARX_SECRET_KEY@";
base_url = "https://search.${cfg.domainName}/";
image_proxy = true;
default_http_headers = {
X-Content-Type-Options = "nosniff";
X-XSS-Protection = "1; mode=block";
X-Download-Options = "noopen";
X-Robots-Tag = "noindex, nofollow";
Referrer-Policy = "no-referrer";
};
};
outgoing = {
request_timeout = 5.0; # default timeout in seconds, can be override by engine
max_request_timeout = 15.0; # the maximum timeout in seconds
pool_connections = 100; # Maximum number of allowable connections, or null
pool_maxsize = 10; # Number of allowable keep-alive connections, or null
enable_http2 = true; # See https://www.python-httpx.org/http2/
};
/* = {
name = "soundcloud";
disabled = true;
};*/
};
services.nginx.virtualHosts."search.${cfg.domainName}" = let inherit (config.services.searx) settings; in {
enableACME = true;
forceSSL = true;
# locations."/".proxyPass = "http://${quotePotentialIpV6 settings.server.bind_address}:${toString settings.server.port}";
locations."/".extraConfig = ''
uwsgi_pass "${quotePotentialIpV6 settings.server.bind_address}:${toString settings.server.port}";
include ${config.services.nginx.package}/conf/uwsgi_params;
'';
};
# NGINX
services.nginx.enable = true;
services.nginx.streamConfig =
let
cert = config.security.acme.certs."${cfg.domainName}".directory + "/fullchain.pem";
certKey = config.security.acme.certs."${cfg.domainName}".directory + "/key.pem";
trustedCert = config.security.acme.certs."${cfg.domainName}".directory + "/chain.pem";
in ''
upstream dns {
zone dns 64k;
server 127.0.0.1:53;
}
server {
listen 853 ssl;
ssl_certificate ${cert};
ssl_certificate_key ${certKey};
ssl_trusted_certificate ${trustedCert};
proxy_pass dns;
}
'';
services.nginx.commonHttpConfig = "log_format postdata '{\"ip\":\"$remote_addr\",\"time\":\"$time_iso8601\",\"referer\":\"$http_referer\",\"body\":\"$request_body\",\"ua\":\"$http_user_agent\"}';";
services.nginx.recommendedTlsSettings = true;
services.nginx.recommendedOptimisation = true;
services.nginx.recommendedGzipSettings = true;
services.nginx.recommendedProxySettings = true;
# BLOG
services.nginx.virtualHosts."${cfg.domainName}" = {
enableACME = true;
forceSSL = true;
extraConfig = "autoindex on;";
locations."/".root = "/var/www/${cfg.domainName}/";
locations."/src".root = "/var/www/${cfg.domainName}/";
locations."/src".extraConfig = "index force_dirlisting;";
locations."/submit_comment".extraConfig = ''
access_log /var/log/nginx/comments.log postdata;
proxy_pass https://${cfg.domainName}/submit.htm;
break;
'';
locations."/submit.htm" = {
extraConfig = ''
return 200 '<!doctype html><html><head><base href="/"/><link rel="preload" href="style.css" as="style"><title>Success!</title><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><link rel="icon" type="image/jpeg" href="pfp.jpg"><link rel="alternate" type="application/rss+xml" title="RSS" href="https://${cfg.domainName}/blog/index.xml"><link href="style.css" rel="stylesheet" /><script src="main.js"></script><meta http-equiv="refresh" content="10; url=$http_referer" /></head><body onload="documentLoaded()"><hr/><div class="main-body"><p>Success! It may take a while for your comment to get moderated.</p><p>Please wait for 10 seconds until you get redirected back...</p><p>Or just go there <a href="$http_referer">manually</a>.</p></div><hr/></body></html>';
'';
};
};
services.nginx.virtualHosts."www.${cfg.domainName}" = {
enableACME = true;
globalRedirect = cfg.domainName;
};
# MAILSERVER
# roundcube
services.nginx.virtualHosts."mail.${cfg.domainName}" = {
enableACME = true;
};
services.roundcube = {
enable = true;
package = pkgs.roundcube.withPlugins (plugins: [ plugins.persistent_login ]);
dicts = with pkgs.aspellDicts; [ en ru ];
hostName = "mail.${cfg.domainName}";
maxAttachmentSize = 100;
plugins = [ "persistent_login" ];
};
mailserver = {
enable = true;
fqdn = "mail.${cfg.domainName}";
domains = [ cfg.domainName ];
certificateScheme = 1;
certificateFile = config.security.acme.certs."mail.${cfg.domainName}".directory + "/fullchain.pem";
keyFile = config.security.acme.certs."mail.${cfg.domainName}".directory + "/key.pem";
localDnsResolver = false;
recipientDelimiter = "-";
lmtpSaveToDetailMailbox = "no";
hierarchySeparator = "/";
};
# Only allow local connections to noreply account
mailserver.loginAccounts."noreply@${cfg.domainName}" = {
# password is set in private.nix
hashedPassword = cfg.hashedNoreplyPassword;
sendOnly = true;
};
services.dovecot2.extraConfig =
let passwd = builtins.toFile "dovecot2-local-passwd" ''
noreply@${cfg.domainName}:{plain}${cfg.unhashedNoreplyPassword}::::::allow_nets=local,127.0.0.0/8,::1
'';
in ''
passdb {
driver = passwd-file
args = ${passwd}
}
'';
# GITEA
services.nginx.virtualHosts."git.${cfg.domainName}" = let inherit (config.services.gitea) settings; in {
enableACME = true;
forceSSL = true;
locations."/".proxyPass = "http://${quotePotentialIpV6 settings.server.HTTP_ADDR}:${toString settings.server.HTTP_PORT}";
};
services.gitea = {
enable = true;
database = {
createDatabase = false;
passwordFile = "/var/lib/gitea/db_password";
type = "postgres";
};
settings = {
mailer = {
ENABLED = true;
FROM = "Gitea <noreply@${cfg.domainName}>";
MAILER_TYPE = "smtp";
HOST = "mail.${cfg.domainName}:587";
USER = "noreply@${cfg.domainName}";
PASSWD = cfg.unhashedNoreplyPassword;
SKIP_VERIFY = true;
};
session = {
COOKIE_SECURE = true;
};
server = {
ROOT_URL = "https://git.${cfg.domainName}";
HTTP_ADDR = "::1";
HTTP_PORT = 3310;
DOMAIN = "git.${cfg.domainName}";
# START_SSH_SERVER = true;
# SSH_PORT = 2222;
};
service = {
DISABLE_REGISTRATION = true;
REGISTER_EMAIL_CONFIRM = true;
};
};
};
# NEXTCLOUD
services.nginx.virtualHosts."cloud.${cfg.domainName}" = {
enableACME = true;
forceSSL = true;
};
services.nextcloud = {
enable = true;
enableBrokenCiphersForSSE = false;
package = pkgs.nextcloud26;
autoUpdateApps.enable = true;
# TODO: use socket auth and remove the next line
database.createLocally = false;
config = {
adminpassFile = "/var/lib/nextcloud/admin_password";
dbpassFile = "/var/lib/nextcloud/db_password";
dbtype = "pgsql";
dbhost = "/run/postgresql";
overwriteProtocol = "https";
};
hostName = "cloud.${cfg.domainName}";
https = true;
};
systemd.services.pleroma.path = [ pkgs.exiftool pkgs.gawk ];
services.nginx.virtualHosts."pleroma.${cfg.domainName}" = {
enableACME = true;
forceSSL = true;
locations."/".proxyPass = "http://127.0.0.1:9970";
};
/*locations."/dns-query".extraConfig = ''
grpc_pass grpc://127.0.0.1:53453;
'';*/
# TODO: firefox sync?
}

View file

@ -0,0 +1,51 @@
{ config
, pkgs
, lib
, ... }:
let
cfg = config.server;
quotePotentialIpV6 = addr:
if lib.hasInfix ":" addr then "[${addr}]" else addr;
in {
services.nginx.virtualHosts."${cfg.domainName}" = {
locations."/fdroid/".alias = "/var/lib/fdroid/repo/";
};
users.users.fdroid = {
home = "/var/lib/fdroid";
group = "fdroid";
isSystemUser = true;
};
users.groups.fdroid = { };
systemd.timers.update-fdroid = {
wantedBy = [ "timers.target" ];
partOf = [ "update-fdroid.service" ];
# slightly unusual time to reduce server load
timerConfig.OnCalendar = [ "*-*-* 00:40:00" ]; # every day
};
systemd.services.update-fdroid = {
serviceConfig = let
inherit (pkgs) fdroidserver;
fdroidScript = pkgs.writeText "update-froid.py" ''
import requests, subprocess, os, sys
x = requests.get('https://api.github.com/repos/ppy/osu/releases').json()
for q in x:
for w in q.get('assets', []):
if w.get('name', "").endswith('.apk'):
os.chdir('/var/lib/fdroid')
subprocess.run(['${pkgs.wget}/bin/wget', w['browser_download_url'], '-O', '/var/tmp/lazer.apk'], check=True)
os.rename('/var/tmp/lazer.apk', '/var/lib/fdroid/repo/sh.ppy.osulazer.apk')
subprocess.run(['${fdroidserver}/bin/fdroid', 'update', '--allow-disabled-algorithms'])
sys.exit()
'';
fdroidPython = pkgs.python3.withPackages (p: with p; [ requests ]);
in {
Type = "oneshot";
ExecStart = "${fdroidPython} ${fdroidScript}";
};
environment.JAVA_HOME = "${pkgs.jdk11_headless}";
path = [ pkgs.jdk11_headless ];
};
}

View file

@ -0,0 +1,122 @@
{ config
, pkgs
, lib
, ... }:
let
cfg = config.server;
quotePotentialIpV6 = addr:
if lib.hasInfix ":" addr then "[${addr}]" else addr;
matrixServerJson = {
"m.server" = "matrix.${cfg.domainName}:443";
};
matrixClientJson = {
"m.homeserver" = { base_url = "https://matrix.${cfg.domainName}"; };
"m.identity_server" = { base_url = "https://vector.im"; };
};
matrixServerConfigResponse = ''
add_header Content-Type application/json;
return 200 '${builtins.toJSON matrixServerJson}';
'';
matrixClientConfigResponse = ''
add_header Content-Type application/json;
add_header Access-Control-Allow-Origin *;
return 200 '${builtins.toJSON matrixClientJson}';
'';
matrixAddr = "::1";
matrixPort = 8008;
in {
imports = [ ./maubot.nix ];
networking.firewall.allowedTCPPorts = [ 8008 8448 ];
systemd.services.matrix-synapse.serviceConfig.TimeoutStartSec = 180;
services.nginx.virtualHosts."${cfg.domainName}" = {
locations."= /.well-known/matrix/server".extraConfig = matrixServerConfigResponse;
locations."= /.well-known/matrix/client".extraConfig = matrixClientConfigResponse;
};
services.nginx.virtualHosts."matrix.${cfg.domainName}" = {
enableACME = true;
forceSSL = true;
locations = {
"= /.well-known/matrix/server".extraConfig = matrixServerConfigResponse;
"= /.well-known/matrix/client".extraConfig = matrixClientConfigResponse;
"/".proxyPass = "http://${quotePotentialIpV6 matrixAddr}:${toString matrixPort}";
};
};
systemd.services.heisenbridge.wants = [ "matrix-synapse.service" ];
systemd.services.heisenbridge.after = [ "matrix-synapse.service" ];
services.heisenbridge = {
enable = true;
homeserver = "http://${quotePotentialIpV6 matrixAddr}:${toString matrixPort}/";
};
# so synapse can read the registration
users.groups.heisenbridge.members = [ "matrix-synapse" ];
services.matrix-synapse = {
enable = true;
extraConfigFiles = [ "/var/lib/matrix-synapse/config.yaml" ];
settings = {
app_service_config_files = [
"/var/lib/heisenbridge/registration.yml"
];
allow_guest_access = true;
url_preview_enabled = true;
tls_certificate_path = config.security.acme.certs."matrix.${cfg.domainName}".directory + "/fullchain.pem";
tls_private_key_path = config.security.acme.certs."matrix.${cfg.domainName}".directory + "/key.pem";
public_baseurl = "https://matrix.${cfg.domainName}/";
server_name = "matrix.${cfg.domainName}";
max_upload_size = "100M";
email = {
smtp_host = "mail.pavluk.org";
smtp_port = 587;
smtp_user = "noreply";
smtp_password = cfg.unhashedNoreplyPassword;
notif_from = "${cfg.domainName} matrix homeserver <noreply@${cfg.domainName}>";
app_name = cfg.domainName;
notif_for_new_users = false;
enable_notifs = true;
};
listeners = [{
port = matrixPort;
bind_addresses = [ matrixAddr ];
type = "http";
tls = false;
x_forwarded = true;
resources = [{
names = [ "client" "federation" ];
compress = false;
}];
}];
};
};
# maubot
users.users.maubot = {
home = "/var/lib/maubot";
group = "maubot";
isSystemUser = true;
};
users.groups.maubot = { };
systemd.services.maubot = {
description = "Maubot";
wants = [ "matrix-synapse.service" "nginx.service" ];
after = [ "matrix-synapse.service" "nginx.service" ];
wantedBy = [ "multi-user.target" ];
environment = {
LD_LIBRARY_PATH = "${pkgs.stdenv.cc.cc.lib}/lib";
};
serviceConfig = {
User = "maubot";
Group = "maubot";
WorkingDirectory = "/var/lib/maubot/data";
};
script = "${pkgs.python3.withPackages (pks: with pks; [
pkgs.maubot (pkgs.pineapplebot.override {
magic = cfg.pizzabotMagic;
}) feedparser levenshtein python-dateutil pytz
])}/bin/python3 -m maubot";
};
}

View file

@ -0,0 +1,45 @@
{ config
, pkgs
, lib
, ... }:
let
cfg = config.server;
quotePotentialIpV6 = addr:
if lib.hasInfix ":" addr then "[${addr}]" else addr;
# i've yet to create a maubot module so this is hardcoded
maubotAddr = "127.0.0.1";
maubotPort = 29316;
in {
services.nginx.virtualHosts."matrix.${cfg.domainName}".locations = {
"/_matrix/maubot/" = {
proxyPass = "http://${quotePotentialIpV6 maubotAddr}:${toString maubotPort}";
proxyWebsockets = true;
};
};
users.users.maubot = {
home = "/var/lib/maubot";
group = "maubot";
isSystemUser = true;
};
users.groups.maubot = { };
systemd.services.maubot = {
description = "Maubot";
wants = [ "matrix-synapse.service" "nginx.service" ];
after = [ "matrix-synapse.service" "nginx.service" ];
wantedBy = [ "multi-user.target" ];
environment = {
LD_LIBRARY_PATH = "${pkgs.stdenv.cc.cc.lib}/lib";
};
serviceConfig = {
User = "maubot";
Group = "maubot";
WorkingDirectory = "/var/lib/maubot/data";
};
script = "${pkgs.python3.withPackages (pks: with pks; [
pkgs.maubot (pkgs.pineapplebot.override {
magic = cfg.pizzabotMagic;
}) feedparser levenshtein python-dateutil pytz
])}/bin/python3 -m maubot";
};
}

View file

@ -0,0 +1,81 @@
{ config
, lib
, ... }:
let
cfg = config.server;
quotePotentialIpV6 = addr:
if lib.hasInfix ":" addr then "[${addr}]" else addr;
in {
services.murmur = {
enable = true;
imgMsgLength = 0;
textMsgLength = 0;
registerName = "mumble.${cfg.domainName}";
registerHostname = "mumble.${cfg.domainName}";
sslCa = config.security.acme.certs."mumble.${cfg.domainName}".directory + "/chain.pem";
sslCert = config.security.acme.certs."mumble.${cfg.domainName}".directory + "/fullchain.pem";
sslKey = config.security.acme.certs."mumble.${cfg.domainName}".directory + "/key.pem";
# clientCertRequired = true;
extraConfig = ''
bandwidth=320000
opusthreshold=0
'';
};
# Allow murmur to read the certificate
security.acme.certs."mumble.${cfg.domainName}" = {
group = "nginxandmurmur";
postRun = "systemctl try-reload-or-restart murmur";
};
users.groups.nginxandmurmur.members = [ "murmur" "nginx" ];
# Mumble music bot
services.nginx.virtualHosts."mumble.${cfg.domainName}" = let inherit (config.services.botamusique) settings; in {
enableACME = true;
forceSSL = true;
globalRedirect = cfg.domainName;
locations."/music".extraConfig = "return 301 https://mumble.${cfg.domainName}/music/;";
locations."/music/".proxyPass = "http://${quotePotentialIpV6 settings.webinterface.listening_addr}:${toString settings.webinterface.listening_port}/";
};
services.botamusique = {
enable = true;
settings = {
youtube_dl = {
cookiefile = "/var/lib/botamusique/cookie_ydl";
};
webinterface = {
enabled = true;
listening_addr = "::1";
listening_port = 8181;
is_web_proxified = true;
access_address = "https://mumble.${cfg.domainName}/music";
auth_method = "token";
upload_enabled = true;
max_upload_file_size = "100MB";
delete_allowed = true;
};
bot = {
bandwidth = 200000;
volume = 1.0;
ducking = true;
ducking_volume = 0.75;
};
server.certificate = "/var/lib/botamusique/cert.pem";
};
};
systemd.services.botamusique.wants = [ "murmur.service" ];
systemd.services.botamusique.after = [ "murmur.service" ];
networking.firewall = {
allowedTCPPorts = [
64738
# Used for mumble-web signaling (not sure if it needs TCP or UDP)
# 20000 20001 20002 20003 20004 20005 20006 20007 20008 20009 20010
];
allowedUDPPorts = [
64738
# 20000 20001 20002 20003 20004 20005 20006 20007 20008 20009 20010
];
};
}

View file

@ -0,0 +1,58 @@
{ lib
, ... }:
{
options.server = with lib; mkOption {
type = types.submodule {
options = {
domainName = mkOption {
type = types.str;
default = "pavluk.org";
description = "domain name";
};
lanCidrV4 = mkOption {
type = types.str;
description = "LAN mask (IPv4)";
example = "192.168.1.0/96";
default = "0.0.0.0/0";
};
lanCidrV6 = mkOption {
type = types.str;
description = "LAN mask (IPv6)";
example = "fd01:abcd::/64";
default = "::/0";
};
localIpV4 = mkOption {
type = with types; nullOr str;
description = "server's local IPv4 address";
example = "192.168.1.2";
default = null;
};
localIpV6 = mkOption {
type = with types; nullOr str;
description = "server's local IPv6 address";
example = "fd01:abcd::2";
default = null;
};
noreplyPassword = mkOption {
type = types.str;
description = "noreply (only available via localhost) account password";
default = "totallysafe";
};
hashedNoreplyPassword = mkOption {
type = types.str;
description = "hashed noreply password via mkpasswd -sm bcrypt";
};
unhashedNoreplyPassword = mkOption {
type = types.str;
description = "unhashed noreply password. \
This should preferably be different from the password that is hashed for better security (yes, really)";
};
pizzabotMagic = mkOption {
type = types.str;
default = "<PIZZABOT_MAGIC_SEP>";
};
};
};
description = "server settings";
};
}

View file

@ -4,7 +4,7 @@
config.programs.ccache.cacheDir
"/var/cache/sccache"
];
environment.persistence."/persist".directories = lib.mkIf config.programs.ccache.enable [
impermanence.directories = lib.mkIf config.programs.ccache.enable [
config.programs.ccache.cacheDir
"/var/cache/sccache"
];

View file

@ -1,4 +1,4 @@
{ config, lib, pkgs, ... }:
{ config, lib, ... }:
let
cfg = config.impermanence;
@ -16,7 +16,7 @@ in {
description = "Default path for persistence";
};
directories = mkOption {
type = with types; listOf path;
type = with types; listOf (either path attrs);
default = [ ];
description = "Extra directories to persist";
};
@ -36,41 +36,90 @@ in {
default = { };
};
config = lib.mkIf cfg.enable {
# why is this not part of base NixOS?
systemd.tmpfiles.rules = [ "d /var/lib/systemd/pstore 0755 root root 14d" ];
# as weird as it sounds, I won't use tmpfs for /tmp in case I'll have to put files over 2GB there
boot.tmp.cleanOnBoot = lib.mkIf cfg.persistTmp true;
environment.persistence.${toString cfg.path} = {
hideMounts = true;
directories = map toString ([
directories = map (x:
if builtins.isPath x then toString x
else if builtins.isAttrs x && x?directory && builtins.isPath x.directory then x // { directory = toString x.directory; }
else x)
([
# nixos files
/etc/nixos
/var/lib/nixos
{ directory = /etc/nixos; user = "root"; group = "root"; mode = "0755"; }
{ directory = /var/lib/nixos; user = "root"; group = "root"; mode = "0755"; }
/var/log
{ directory = /var/log; user = "root"; group = "root"; mode = "0755"; }
# persist this since everything here is cleaned up by systemd-tmpfiles over time anyway
# ...or so I'd like to believe
/var/lib/systemd
/var/tmp
{ directory = /var/lib/systemd; user = "root"; group = "root"; mode = "0755"; }
{ directory = /var/tmp; user = "root"; group = "root"; mode = "1777"; }
{ directory = /var/spool; user = "root"; group = "root"; mode = "0777"; }
] ++ (lib.optionals cfg.persistTmp [
/tmp
{ directory = /tmp; user = "root"; group = "root"; mode = "1777"; }
]) ++ (lib.optionals config.services.mullvad-vpn.enable [
/etc/mullvad-vpn
/var/cache/mullvad-vpn
{ directory = /etc/mullvad-vpn; user = "root"; group = "root"; mode = "0700"; }
{ directory = /var/cache/mullvad-vpn; user = "root"; group = "root"; mode = "0755"; }
]) ++ (lib.optionals config.virtualisation.libvirtd.enable ([
/var/cache/libvirt
/var/lib/libvirt
# { directory = /var/cache/libvirt; user = "root"; group = "root"; mode = "0755"; }
{ directory = /var/lib/libvirt; user = "root"; group = "root"; mode = "0755"; }
] ++ (lib.optionals config.virtualisation.libvirtd.qemu.swtpm.enable [
/var/lib/swtpm-localca
{ directory = /var/lib/swtpm-localca; user = "root"; group = "root"; mode = "0750"; }
]))) ++ (lib.optionals config.networking.wireless.iwd.enable [
/var/lib/iwd
{ directory = /var/lib/iwd; user = "root"; group = "root"; mode = "0700"; }
]) ++ (lib.optionals (builtins.any (x: x.useDHCP) (builtins.attrValues config.networking.interfaces) || config.networking.useDHCP) [
/var/db/dhcpcd
{ directory = /var/db/dhcpcd; user = "root"; group = "root"; mode = "0755"; }
]) ++ (lib.optionals config.services.gitea.enable [
{ directory = /var/lib/gitea; user = "gitea"; group = "gitea"; mode = "0755"; }
]) ++ (lib.optionals config.services.matrix-synapse.enable [
{ directory = /var/lib/matrix-synapse; user = "matrix-synapse"; group = "matrix-synapse"; mode = "0700"; }
]) ++ (lib.optionals config.services.heisenbridge.enable [
{ directory = /var/lib/heisenbridge; user = "heisenbridge"; group = "heisenbridge"; mode = "0755"; }
]) ++ (lib.optionals config.services.murmur.enable [
{ directory = /var/lib/murmur; user = "murmur"; group = "murmur"; mode = "0700"; }
]) ++ (lib.optionals config.services.nextcloud.enable [
{ directory = /var/lib/nextcloud; user = "nextcloud"; group = "nextcloud"; mode = "0750"; }
]) ++ (lib.optionals config.services.botamusique.enable [
{ directory = /var/lib/private/botamusique; user = "root"; group = "root"; mode = "0750"; }
]) ++ (lib.optionals config.security.acme.acceptTerms [
{ directory = /var/lib/acme; user = "acme"; group = "acme"; mode = "0755"; }
]) ++ (lib.optionals config.services.printing.enable [
{ directory = /var/lib/cups; user = "root"; group = "root"; mode = "0755"; }
]) ++ (lib.optionals config.services.fail2ban.enable [
{ directory = /var/lib/fail2ban; user = "fail2ban"; group = "fail2ban"; mode = "0750"; }
]) ++ (lib.optionals config.services.opendkim.enable [
{ directory = /var/lib/opendkim; user = "opendkim"; group = "opendkim"; mode = "0700"; }
]) ++ (lib.optionals config.services.pleroma.enable [
{ directory = /var/lib/pleroma; user = "pleroma"; group = "pleroma"; mode = "0700"; }
]) ++ (lib.optionals config.services.postfix.enable [
{ directory = /var/lib/postfix; user = "root"; group = "root"; mode = "0755"; }
]) ++ (lib.optionals config.services.postgresql.enable [
{ directory = /var/lib/postgresql; user = "postgres"; group = "postgres"; mode = "0755"; }
]) ++ (lib.optionals config.services.unbound.enable [
{ directory = /var/lib/unbound; user = "unbound"; group = "unbound"; mode = "0755"; }
]) ++ (lib.optionals config.services.roundcube.enable [
{ directory = /var/lib/roundcube; user = "roundcube"; group = "roundcube"; mode = "0700"; }
]) ++ (lib.optionals config.services.rspamd.enable [
{ directory = /var/lib/rspamd; user = "rspamd"; group = "rspamd"; mode = "0700"; }
]) ++ (lib.optionals (
(builtins.hasAttr "rspamd" config.services.redis.servers)
&& (builtins.hasAttr "enable" config.services.redis.servers.rspamd)
&& config.services.redis.servers.rspamd.enable
) [
{ directory = /var/lib/redis-rspamd; user = "redis-rspamd"; group = "redis-rspamd"; mode = "0700"; }
]) ++ (lib.optionals config.services.dovecot2.enable [
{ directory = /var/lib/dhparams; user = "root"; group = "root"; mode = "0755"; }
{ directory = /var/lib/dovecot; user = "root"; group = "root"; mode = "0755"; }
]) ++ (lib.optionals config.security.sudo.enable [
/var/db/sudo/lectured
{ directory = /var/db/sudo/lectured; user = "root"; group = "root"; mode = "0700"; }
]) ++ cfg.directories);
files = map toString ([
# hardware-related
/etc/adjtime
# needed at least for /var/log
/etc/machine-id
] ++ cfg.files);
};

View file

@ -1,4 +1,4 @@
{ config, lib, pkgs, ... }:
{ options, config, lib, pkgs, ... }:
let
cfg = config.vfio;
in {
@ -73,9 +73,10 @@ in {
description = "VFIO settings";
default = { };
};
# compatibility so this module loads on non-amd hardware
config = let
enableIvshmem = config.vfio.lookingGlass.enable && (builtins.length config.vfio.lookingGlass.ivshmem) > 0;
in lib.mkIf config.vfio.enable {
enableIvshmem = cfg.lookingGlass.enable && (builtins.length cfg.lookingGlass.ivshmem) > 0;
in lib.mkIf cfg.enable {
# add a custom kernel param for early loading vfio drivers
# because if we change boot.initrd options in a specialization, two initrds will be built
# and we don't want to build two initrds
@ -151,9 +152,13 @@ in {
SUBSYSTEM=="kvmfr", KERNEL=="kvmfr${toString i}", OWNER="${ivshmem.owner}", GROUP="kvm", MODE="0660"
'')
cfg.lookingGlass.ivshmem));
# disable early KMS so GPU can be properly unbound
hardware.amdgpu.loadInInitrd = lib.mkIf (!cfg.nvidiaGpu) false;
hardware.opengl.enable = true;
hardware = {
opengl.enable = true;
} // (lib.optionalAttrs (cfg.enable && !(cfg.nvidiaGpu)) {
# disable early KMS so GPU can be properly unbound
# can't use mkif because the option may not even exist
amdgpu.loadInInitrd = false;
});
# needed for virt-manager
programs.dconf.enable = true;
virtualisation.libvirtd = {

View file

@ -1,3 +1,5 @@
{ pkgs, ... }: let inherit (pkgs) callPackage; in {
system76-scheduler = callPackage ../pkgs/system76-scheduler.nix { };
system76-scheduler = callPackage ./system76-scheduler.nix { };
maubot = callPackage ./maubot.nix { };
pineapplebot = callPackage ./pineapplebot.nix { };
}

95
system/pkgs/maubot.nix Normal file
View file

@ -0,0 +1,95 @@
{ lib
, fetchpatch
, python3
, runCommand
, encryptionSupport ? true
}:
let
python = python3.override {
packageOverrides = self: super: {
sqlalchemy = super.buildPythonPackage rec {
pname = "SQLAlchemy";
version = "1.3.24";
src = super.fetchPypi {
inherit pname version;
sha256 = "sha256-67t3fL+TEjWbiXv4G6ANrg9ctp+6KhgmXcwYpvXvdRk=";
};
postInstall = ''
sed -e 's:--max-worker-restart=5::g' -i setup.cfg
'';
doCheck = false;
};
};
};
self = with python.pkgs; buildPythonPackage rec {
pname = "maubot";
version = "0.4.1";
disabled = pythonOlder "3.8";
src = fetchPypi {
inherit pname version;
sha256 = "sha256-Ro2PPgF8818F8JewPZ3AlbfWFNNHKTZkQq+1zpm3kk4=";
};
patches = [
# add entry point
(fetchpatch {
url = "https://patch-diff.githubusercontent.com/raw/maubot/maubot/pull/146.patch";
sha256 = "0yn5357z346qzy5v5g124mgiah1xsi9yyfq42zg028c8paiw8s8x";
})
];
propagatedBuildInputs = [
# requirements.txt
mautrix
aiohttp
yarl
sqlalchemy
asyncpg
aiosqlite
CommonMark
ruamel-yaml
attrs
bcrypt
packaging
click
colorama
questionary
jinja2
]
# optional-requirements.txt
++ lib.optionals encryptionSupport [
python-olm
pycryptodome
unpaddedbase64
];
passthru.tests = {
simple = runCommand "${pname}-tests" { } ''
${self}/bin/mbc --help > $out
'';
};
# Setuptools is trying to do python -m maubot test
dontUseSetuptoolsCheck = true;
pythonImportsCheck = [
"maubot"
];
meta = with lib; {
description = "A plugin-based Matrix bot system written in Python";
homepage = "https://github.com/maubot/maubot";
changelog = "https://github.com/maubot/maubot/blob/v${version}/CHANGELOG.md";
license = licenses.agpl3Plus;
maintainers = with maintainers; [ chayleaf ];
};
};
in
self

View file

@ -0,0 +1,34 @@
{ python3
, fetchFromGitHub
, rustPlatform
, magic ? "<PIZZABOT_MAGIC_SEP>"
, ... }:
python3.pkgs.buildPythonPackage rec {
pname = "pineapplebot";
version = "0.1.0";
src = fetchFromGitHub {
owner = "chayleaf";
repo = "pizzabot_v3";
rev = "master";
sha256 = "sha256-ZLskMlllZfmqIlbSr0pNHHJehDycohiwqgYbuEYP7Qc=";
};
preBuild = ''
head -n13 Cargo.toml > Cargo.toml.new
mv Cargo.toml.new Cargo.toml
'';
sourceRoot = "source/pineapplebot";
cargoDeps = rustPlatform.fetchCargoTarball {
inherit src sourceRoot;
name = "${pname}-${version}";
sha256 = "14jxgykwg1apy97gy1j8mz7ny2cqg4q9s03a2bk9kx2y6ibm4668";
};
nativeBuildInputs = with rustPlatform; [
cargoSetupHook
maturinBuildHook
];
doCheck = false;
doInstallCheck = true;
pythonImportsCheck = [ "pineapplebot" ];
PIZZABOT_MAGIC = magic;
}

View file

@ -1,5 +0,0 @@
{
nixmsi = { ... }: {
# insert private config here
};
}

View file

@ -0,0 +1,9 @@
{
nixmsi = { pkgs, lib, ... }: {
# insert private config here
# time.timeZone = ...;
# users.users.root.initialHashedPassword = ...;
# users.users.user.initialHashedPassword = ...;
};
nixserver = { ... }: { };
}