initial commit
All checks were successful
Deploy docs / build-and-deploy (push) Successful in 3s

This commit is contained in:
sid 2026-02-23 20:34:35 +01:00
commit 95a533c876
451 changed files with 18255 additions and 0 deletions

View file

@ -0,0 +1,24 @@
{ pkgs, ... }:
{
boot.initrd.kernelModules = [ "amdgpu" ];
environment.systemPackages = with pkgs; [
lact
nvtopPackages.amd
rocmPackages.clr.icd
rocmPackages.hipcc
rocmPackages.miopen
rocmPackages.rocm-runtime
rocmPackages.rocm-smi
rocmPackages.rocminfo
];
# environment.variables.ROC_ENABLE_PRE_VEGA = "1"; # for Polaris
hardware.amdgpu.opencl.enable = true;
systemd.packages = with pkgs; [ lact ];
systemd.services.lactd.wantedBy = [ "multi-user.target" ];
}

View file

@ -0,0 +1,28 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib) mkDefault;
in
{
services.pipewire = {
enable = mkDefault true;
wireplumber.enable = mkDefault true;
alsa.enable = mkDefault true;
pulse.enable = mkDefault true;
jack.enable = mkDefault true;
audio.enable = mkDefault true;
};
services.pulseaudio.enable = false;
security.rtkit.enable = mkDefault config.services.pipewire.enable;
environment.systemPackages = with pkgs; [
pulseaudioFull
];
}

View file

@ -0,0 +1,98 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.baibot;
homeDir = "/var/lib/baibot";
inherit (lib)
mkEnableOption
mkIf
mkOption
optional
types
;
in
{
options = {
services.baibot = {
enable = mkEnableOption "Baibot, a Matrix AI bot.";
configFile = mkOption {
type = types.nullOr types.path;
default = "${homeDir}/config.yml";
description = ''
Path to the baibot configuration file. Use the template for reference:
https://github.com/etkecc/baibot/blob/main/etc/app/config.yml.dist
'';
};
persistenceDataDirPath = mkOption {
type = types.nullOr types.path;
default = "${homeDir}/data";
description = ''
Path to the directory where baibot will store its persistent data.
'';
};
environmentFile = lib.mkOption {
description = ''
Path to an environment file that is passed to the systemd service.
'';
type = lib.types.nullOr lib.types.path;
default = null;
example = "/run/secrets/baibot";
};
package = mkOption {
type = types.nullOr types.package;
default = null;
description = ''
The baibot package to use for the service. This must be set by the user,
as there is no default baibot package available in Nixpkgs.
'';
};
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion = cfg.package != null;
message = "The baibot package is not specified. Please set the services.baibot.package option to a valid baibot package.";
}
];
users = {
users.baibot = {
isSystemUser = true;
description = "Baibot system user";
home = homeDir;
createHome = true;
group = "baibot";
};
groups.baibot = { };
};
systemd.services.baibot = {
description = "Baibot Service";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
environment = {
BAIBOT_CONFIG_FILE_PATH = cfg.configFile;
BAIBOT_PERSISTENCE_DATA_DIR_PATH = cfg.persistenceDataDirPath;
};
serviceConfig = {
ExecStart = "${cfg.package}/bin/baibot";
EnvironmentFile = optional (cfg.environmentFile != null) cfg.environmentFile;
Restart = "always";
User = "baibot";
Group = "baibot";
};
};
};
}

View file

@ -0,0 +1,22 @@
{ lib, pkgs, ... }:
let
inherit (lib) mkDefault;
in
{
hardware.bluetooth.enable = mkDefault true;
hardware.bluetooth.powerOnBoot = mkDefault false;
hardware.bluetooth.settings.General.Enable = mkDefault "Source,Sink,Media,Socket";
hardware.bluetooth.settings.General.Experimental = mkDefault true;
environment.systemPackages = with pkgs; [
blueman
bluez
bluez-tools
];
boot.kernelModules = [
"btusb"
"bluetooth"
];
}

View file

@ -0,0 +1,91 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.services.cifsMount;
in
{
options.services.cifsMount = {
enable = mkEnableOption "CIFS mounting of predefined remote directories.";
# List of predefined remote CIFS shares to mount
remotes = mkOption {
type = with types; listOf (attrsOf str);
default = [ ];
description = "List of predefined remote CIFS shares to mount.";
example = [
{
# Example entry for a CIFS mount.
host = "remotehost";
shareName = "share";
mountPoint = "/local/mountpoint";
credentialsFile = "/path/to/credentials";
user = "yourusername";
}
];
};
};
config = mkIf cfg.enable {
environment.systemPackages = with pkgs; [ cifs-utils ];
# Define /etc/fstab entries for each remote CIFS share
fileSystems = fold (
remote: fs:
let
mountOption = "credentials=${remote.credentialsFile},uid=${remote.user},gid=${remote.user},user,noauto";
in
fs
// {
"${toString remote.mountPoint}" = {
device = "//${remote.host}/${remote.shareName}";
fsType = "cifs";
options = [
(optionalString (remote.credentialsFile != null) "credentials=${remote.credentialsFile}")
"uid=${remote.user}"
"gid=${remote.user}"
"user"
"noauto"
];
};
}
) { } cfg.remotes;
# Create systemd user services for each remote CIFS share
systemd.user.services = fold (
remote: services:
let
serviceName = "cifs-mount-${remote.shareName}";
in
{
${serviceName} = {
description = "Mount remote share: //${remote.host}/${remote.shareName} to ${remote.mountPoint}";
wantedBy = [ "graphical-session.target" ];
after = [ "graphical-session.target" ];
# Service configuration to mount and unmount CIFS share
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "mount //${remote.host}/${remote.shareName}";
ExecStop = "umount //${remote.host}/${remote.shareName}";
Restart = "on-failure";
};
};
}
// services
) { } cfg.remotes;
# Ensure that all cifs-mount services are started with the graphical session
systemd.user.targets.graphical-session.wants = map (
remote: "cifs-mount-${remote.shareName}.service"
) cfg.remotes;
};
}

View file

@ -0,0 +1,14 @@
{
imports = [
./environment.nix
./htop.nix
./nationalization.nix
./networking.nix
./nix.nix
./sudo.nix
./well-known.nix
./zsh.nix
../../shared/common
];
}

View file

@ -0,0 +1,63 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib) mkDefault optionals;
in
{
environment.systemPackages =
with pkgs;
[
cryptsetup
curl
dig
dnsutils
fzf
gptfdisk
iproute2
jq
lm_sensors
lsof
netcat-openbsd
nettools
nixos-container
nmap
nurl
p7zip
pciutils
psmisc
rclone
rsync
tcpdump
tmux
tree
unzip
usbutils
wget
xxd
zip
(callPackage ../../../apps/rebuild { })
]
++ optionals (pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) [
pkgs.kitty.terminfo
];
environment.shellAliases = {
l = "ls -lh";
ll = "ls -lAh";
ports = "ss -tulpn";
publicip = "curl ifconfig.me/all";
sudo = "sudo "; # make aliases work with `sudo`
};
# saves one instance of nixpkgs.
environment.ldso32 = null;
boot.tmp.cleanOnBoot = mkDefault true;
boot.initrd.systemd.enable = mkDefault (!config.boot.swraid.enable && !config.boot.isContainer);
}

View file

@ -0,0 +1,8 @@
{
programs.htop = {
enable = true;
settings = {
highlight_base_name = 1;
};
};
}

View file

@ -0,0 +1,31 @@
{ lib, ... }:
let
de = "de_DE.UTF-8";
en = "en_US.UTF-8";
inherit (lib) mkDefault;
in
{
i18n = {
defaultLocale = mkDefault en;
extraLocaleSettings = {
LC_ADDRESS = mkDefault de;
LC_IDENTIFICATION = mkDefault de;
LC_MEASUREMENT = mkDefault de;
LC_MONETARY = mkDefault de;
LC_NAME = mkDefault de;
LC_NUMERIC = mkDefault de;
LC_PAPER = mkDefault de;
LC_TELEPHONE = mkDefault de;
LC_TIME = mkDefault en;
};
};
console = {
font = mkDefault "Lat2-Terminus16";
keyMap = mkDefault "de";
};
time.timeZone = mkDefault "Europe/Berlin";
}

View file

@ -0,0 +1,40 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib) mkDefault;
inherit (lib.utils) isNotEmptyStr;
in
{
config = {
assertions = [
{
assertion = isNotEmptyStr config.networking.domain;
message = "synix/nixos/common: config.networking.domain cannot be empty.";
}
{
assertion = isNotEmptyStr config.networking.hostName;
message = "synix/nixos/common: config.networking.hostName cannot be empty.";
}
];
networking = {
domain = mkDefault "${config.networking.hostName}.local";
hostId = mkDefault "8425e349"; # same as NixOS install ISO and nixos-anywhere
# NetworkManager
useDHCP = false;
networkmanager = {
enable = true;
plugins = with pkgs; [
networkmanager-openconnect
networkmanager-openvpn
];
};
};
};
}

View file

@ -0,0 +1,19 @@
{
config,
lib,
...
}:
let
inherit (lib) mkDefault;
in
{
nix = {
# use flakes
channel.enable = mkDefault false;
# De-duplicate store paths using hardlinks except in containers
# where the store is host-managed.
optimise.automatic = mkDefault (!config.boot.isContainer);
};
}

View file

@ -0,0 +1,26 @@
{ config, ... }:
{
security.sudo = {
enable = true;
execWheelOnly = true;
extraConfig = ''
Defaults lecture = never
'';
};
assertions =
let
validUsers = users: users == [ ] || users == [ "root" ];
validGroups = groups: groups == [ ] || groups == [ "wheel" ];
validUserGroups = builtins.all (
r: validUsers (r.users or [ ]) && validGroups (r.groups or [ ])
) config.security.sudo.extraRules;
in
[
{
assertion = config.security.sudo.execWheelOnly -> validUserGroups;
message = "Some definitions in `security.sudo.extraRules` refer to users other than 'root' or groups other than 'wheel'. Disable `config.security.sudo.execWheelOnly`, or adjust the rules.";
}
];
}

View file

@ -0,0 +1,17 @@
{
# avoid TOFU MITM
programs.ssh.knownHosts = {
"github.com".hostNames = [ "github.com" ];
"github.com".publicKey =
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl";
"gitlab.com".hostNames = [ "gitlab.com" ];
"gitlab.com".publicKey =
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf";
"git.sr.ht".hostNames = [ "git.sr.ht" ];
"git.sr.ht".publicKey =
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMZvRd4EtM7R+IHVMWmDkVU3VLQTSwQDSAvW0t2Tkj60";
};
# TODO: add synix
}

View file

@ -0,0 +1,26 @@
{
programs.zsh = {
enable = true;
syntaxHighlighting = {
enable = true;
highlighters = [
"main"
"brackets"
"cursor"
"pattern"
];
patterns = {
"rm -rf" = "fg=white,bold,bg=red";
"rm -fr" = "fg=white,bold,bg=red";
};
};
autosuggestions = {
enable = true;
strategy = [
"completion"
"history"
];
};
enableLsColors = true;
};
}

View file

@ -0,0 +1,125 @@
{ config, lib, ... }:
let
cfg = config.services.coturn;
inherit (lib)
mkDefault
mkEnableOption
mkIf
mkOption
optionalString
optionals
types
;
in
{
options.services.coturn = {
openFirewall = mkOption {
type = types.bool;
default = false;
description = "Whether to open the firewall for coturn.";
};
debug = mkOption {
type = types.bool;
default = true;
description = "Enable debug logging for coturn.";
};
sops = mkEnableOption "SOPS integration";
};
config = mkIf cfg.enable {
services.coturn = {
no-cli = mkDefault true;
no-tcp-relay = mkDefault true;
no-tls = mkDefault false;
min-port = mkDefault 49000;
max-port = mkDefault 50000;
listening-port = mkDefault 3478;
tls-listening-port = mkDefault 5349;
use-auth-secret = true;
static-auth-secret-file = mkIf cfg.sops config.sops.secrets."coturn/static-auth-secret".path;
realm = mkDefault "turn.${config.networking.domain}";
cert =
mkIf (!cfg.no-tls && cfg.sops)
"${config.security.acme.certs.${cfg.realm}.directory}/full.pem";
pkey =
mkIf (!cfg.no-tls && cfg.sops)
"${config.security.acme.certs.${cfg.realm}.directory}/key.pem";
extraConfig = ''
# ban private IP ranges
no-multicast-peers
denied-peer-ip=0.0.0.0-0.255.255.255
denied-peer-ip=10.0.0.0-10.255.255.255
denied-peer-ip=100.64.0.0-100.127.255.255
denied-peer-ip=127.0.0.0-127.255.255.255
denied-peer-ip=169.254.0.0-169.254.255.255
denied-peer-ip=172.16.0.0-172.31.255.255
denied-peer-ip=192.0.0.0-192.0.0.255
denied-peer-ip=192.0.2.0-192.0.2.255
denied-peer-ip=192.88.99.0-192.88.99.255
denied-peer-ip=192.168.0.0-192.168.255.255
denied-peer-ip=198.18.0.0-198.19.255.255
denied-peer-ip=198.51.100.0-198.51.100.255
denied-peer-ip=203.0.113.0-203.0.113.255
denied-peer-ip=240.0.0.0-255.255.255.255
denied-peer-ip=::1
denied-peer-ip=64:ff9b::-64:ff9b::ffff:ffff
denied-peer-ip=::ffff:0.0.0.0-::ffff:255.255.255.255
denied-peer-ip=100::-100::ffff:ffff:ffff:ffff
denied-peer-ip=2001::-2001:1ff:ffff:ffff:ffff:ffff:ffff:ffff
denied-peer-ip=2002::-2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff
denied-peer-ip=fc00::-fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
denied-peer-ip=fe80::-febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff
''
+ optionalString cfg.debug ''
# debugging
syslog
verbose
'';
};
networking.firewall = mkIf cfg.openFirewall {
allowedUDPPortRanges = [
{
from = cfg.min-port;
to = cfg.max-port;
}
];
allowedUDPPorts = [
cfg.listening-port
cfg.alt-listening-port
]
++ optionals (!cfg.no-tls) [
cfg.tls-listening-port
cfg.alt-tls-listening-port
];
allowedTCPPorts = [
cfg.listening-port
cfg.alt-listening-port
]
++ optionals (!cfg.no-tls) [
cfg.tls-listening-port
cfg.alt-tls-listening-port
];
};
security.acme.certs = mkIf (!cfg.no-tls) {
${cfg.realm} = {
postRun = "systemctl restart coturn.service";
group = "turnserver";
};
};
sops = mkIf cfg.sops {
secrets."coturn/static-auth-secret" = {
owner = "turnserver";
group = "turnserver";
mode = "0400";
};
};
};
}

36
modules/nixos/default.nix Normal file
View file

@ -0,0 +1,36 @@
{
amd = import ./amd;
audio = import ./audio;
baibot = import ./baibot;
bluetooth = import ./bluetooth;
cifsMount = import ./cifsMount;
common = import ./common;
coturn = import ./coturn;
device = import ./device;
ftp-webserver = import ./ftp-webserver;
headplane = import ./headplane;
headscale = import ./headscale;
hyprland = import ./hyprland;
i2pd = import ./i2pd;
jellyfin = import ./jellyfin;
jirafeau = import ./jirafeau;
mailserver = import ./mailserver;
matrix-synapse = import ./matrix-synapse;
maubot = import ./maubot;
mcpo = import ./mcpo;
miniflux = import ./miniflux;
nginx = import ./nginx;
normalUsers = import ./normalUsers;
nvidia = import ./nvidia;
ollama = import ./ollama;
open-webui-oci = import ./open-webui-oci;
openssh = import ./openssh;
print-server = import ./print-server;
radicale = import ./radicale;
rss-bridge = import ./rss-bridge;
sops = import ./sops;
tailscale = import ./tailscale;
virtualisation = import ./virtualisation;
webPage = import ./webPage;
windows-oci = import ./windows-oci;
}

View file

@ -0,0 +1,6 @@
{
desktop = import ./desktop.nix;
laptop = import ./laptop.nix;
server = import ./server.nix;
vm = import ./vm.nix;
}

View file

@ -0,0 +1,8 @@
{
imports = [
../audio
];
# improve desktop responsiveness when updating the system
nix.daemonCPUSchedPolicy = "idle";
}

View file

@ -0,0 +1,33 @@
{ pkgs, lib, ... }:
let
inherit (lib) mkDefault;
in
{
imports = [
./desktop.nix
../bluetooth
];
services.tlp = {
enable = mkDefault true;
settings = {
CPU_SCALING_GOVERNOR_ON_AC = mkDefault "performance";
CPU_SCALING_GOVERNOR_ON_BAT = mkDefault "powersave";
CPU_ENERGY_PERF_POLICY_ON_BAT = mkDefault "power";
CPU_ENERGY_PERF_POLICY_ON_AC = mkDefault "performance";
PLATFORM_PROFILE_ON_AC = mkDefault "performance";
PLATFORM_PROFILE_ON_BAT = mkDefault "low-power";
START_CHARGE_THRESH_BAT0 = mkDefault 75;
STOP_CHARGE_THRESH_BAT0 = mkDefault 80;
};
};
# avoid conflicts with tlp
services.power-profiles-daemon.enable = mkDefault false;
environment.systemPackages = [ pkgs.powertop ];
}

View file

@ -0,0 +1,69 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (config.programs) neovim;
inherit (lib) mkDefault mkIf;
in
{
environment = {
variables = {
BROWSER = "echo";
EDITOR = mkIf neovim.enable "nvim";
VISUAL = mkIf neovim.enable "nvim";
};
shellAliases = {
v = mkIf neovim.enable "nvim";
};
# do not install /lib/ld-linux.so.2 and /lib64/ld-linux-x86-64.so.2
stub-ld.enable = mkDefault false;
};
documentation = {
enable = mkDefault false;
nixos.enable = mkDefault false;
doc.enable = mkDefault false;
info.enable = mkDefault false;
man.enable = mkDefault false;
};
fonts.fontconfig.enable = mkDefault false;
xdg.autostart.enable = mkDefault false;
xdg.icons.enable = mkDefault false;
xdg.menus.enable = mkDefault false;
xdg.mime.enable = mkDefault false;
xdg.sounds.enable = mkDefault false;
programs.git.package = mkDefault pkgs.gitMinimal;
programs.neovim = {
enable = mkDefault true;
defaultEditor = mkDefault true;
vimAlias = mkDefault true;
viAlias = mkDefault true;
};
# emergency mode is useless on headless machines
systemd.enableEmergencyMode = false;
boot.initrd.systemd.suppressedUnits = mkIf config.systemd.enableEmergencyMode [
"emergency.service"
"emergency.target"
];
systemd.sleep.extraConfig = ''
AllowSuspend=no
AllowHibernation=no
'';
# force reboots
systemd.settings.Manager = {
RuntimeWatchdogSec = mkDefault "15s";
RebootWatchdogSec = mkDefault "30s";
KExecWatchdogSec = mkDefault "1m";
};
}

View file

@ -0,0 +1,10 @@
{ lib, ... }:
let
inherit (lib) mkDefault;
in
{
services.qemuGuest.enable = mkDefault true;
services.spice-vdagentd.enable = mkDefault true;
services.spice-webdavd.enable = mkDefault true;
}

View file

@ -0,0 +1,54 @@
{ config, lib, ... }:
let
cfg = config.services.ftp-webserver;
domain = config.networking.domain;
fqdn = if (cfg.subdomain != "") then "${cfg.subdomain}.${domain}" else domain;
nginx = config.services.nginx;
inherit (lib)
mkEnableOption
mkIf
mkOption
types
;
in
{
options.services.ftp-webserver = {
enable = mkEnableOption "FTP webserver.";
subdomain = mkOption {
type = types.str;
default = "ftp";
description = "Subdomain for Nginx virtual host. Leave empty for root domain.";
};
forceSSL = mkOption {
type = types.bool;
default = true;
description = "Force SSL for Nginx virtual host.";
};
root = mkOption {
type = types.str;
default = "/srv/www";
description = "Root directory for the FTP webserver.";
};
};
config = mkIf cfg.enable {
services.nginx.virtualHosts."${fqdn}" = {
root = cfg.root;
locations."/" = {
extraConfig = ''
autoindex on;
autoindex_exact_size off;
autoindex_localtime on;
'';
};
forceSSL = cfg.forceSSL;
enableACME = cfg.forceSSL;
sslCertificate = mkIf cfg.forceSSL "${config.security.acme.certs."${fqdn}".directory}/cert.pem";
sslCertificateKey = mkIf cfg.forceSSL "${config.security.acme.certs."${fqdn}".directory}/key.pem";
};
systemd.tmpfiles.rules = [ "d ${cfg.root} 0755 ${nginx.user} ${nginx.group}" ];
};
}

View file

@ -0,0 +1,78 @@
{
inputs,
config,
lib,
...
}:
let
cfg = config.services.headplane;
domain = config.networking.domain;
subdomain = cfg.reverseProxy.subdomain;
fqdn = if (cfg.reverseProxy.enable && subdomain != "") then "${subdomain}.${domain}" else domain;
headscale = config.services.headscale;
inherit (lib)
mkDefault
mkIf
;
inherit (lib.utils)
mkReverseProxyOption
mkVirtualHost
;
in
{
imports = [ inputs.headplane.nixosModules.headplane ];
options.services.headplane = {
reverseProxy = mkReverseProxyOption "Headplane" "hp";
};
config = mkIf cfg.enable {
nixpkgs.overlays = [
inputs.headplane.overlays.default
];
services.headplane = {
settings = {
server = {
host = mkDefault (if cfg.reverseProxy.enable then "127.0.0.1" else "0.0.0.0");
port = mkDefault 3000;
cookie_secret_path = config.sops.secrets."headplane/cookie_secret".path;
};
headscale = {
url = "http://127.0.0.1:${toString headscale.port}";
public_url = headscale.settings.server_url;
config_path = "/etc/headscale/config.yaml";
};
integration.agent = {
enabled = mkDefault true;
pre_authkey_path = config.sops.secrets."headplane/agent_pre_authkey".path;
};
};
};
services.nginx.virtualHosts = mkIf cfg.reverseProxy.enable {
"${fqdn}" = mkVirtualHost {
port = cfg.settings.server.port;
ssl = cfg.reverseProxy.forceSSL;
};
};
sops.secrets =
let
owner = headscale.user;
group = headscale.group;
mode = "0400";
in
{
"headplane/cookie_secret" = {
inherit owner group mode;
};
"headplane/agent_pre_authkey" = {
inherit owner group mode;
};
};
};
}

View file

@ -0,0 +1,17 @@
{
"acls": [
{
"action": "accept",
"src": ["*"],
"dst": ["*:*"]
}
],
"ssh": [
{
"action": "accept",
"src": ["autogroup:member"],
"dst": ["autogroup:member"],
"users": ["autogroup:nonroot", "root"]
}
]
}

View file

@ -0,0 +1,105 @@
{
config,
lib,
...
}:
let
cfg = config.services.headscale;
domain = config.networking.domain;
subdomain = cfg.reverseProxy.subdomain;
fqdn = if (cfg.reverseProxy.enable && subdomain != "") then "${subdomain}.${domain}" else domain;
acl = "headscale/acl.hujson";
inherit (lib)
mkDefault
mkIf
mkOption
optional
optionals
types
;
inherit (lib.utils)
mkReverseProxyOption
mkUrl
mkVirtualHost
;
in
{
options.services.headscale = {
reverseProxy = mkReverseProxyOption "Headscale" "hs";
openFirewall = mkOption {
type = types.bool;
default = false;
description = "Whether to automatically open firewall ports. TCP: 80, 443; UDP: 3478.";
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion = !cfg.settings.derp.server.enable || cfg.reverseProxy.forceSSL;
message = "synix/nixos/headscale: DERP requires TLS";
}
{
assertion = fqdn != cfg.settings.dns.base_domain;
message = "synix/nixos/headscale: `settings.server_url` must be different from `settings.dns.base_domain`";
}
{
assertion = !cfg.settings.dns.override_local_dns || cfg.settings.dns.nameservers.global != [ ];
message = "synix/nixos/headscale: `settings.dns.nameservers.global` must be set when `settings.dns.override_local_dns` is true";
}
];
environment.etc.${acl} = {
inherit (config.services.headscale) user group;
source = ./acl.hujson;
};
environment.shellAliases = {
hs = "${cfg.package}/bin/headscale";
};
services.headscale = {
address = mkDefault (if cfg.reverseProxy.enable then "127.0.0.1" else "0.0.0.0");
port = mkDefault 8077;
settings = {
policy.path = "/etc/${acl}";
database.type = "sqlite"; # postgres is highly discouraged as it is only supported for legacy reasons
server_url = mkUrl {
inherit fqdn;
ssl = with cfg.reverseProxy; enable && forceSSL;
};
derp.server.enable = cfg.reverseProxy.forceSSL;
dns = {
magic_dns = mkDefault true;
base_domain = mkDefault "tail";
search_domains = [ cfg.settings.dns.base_domain ];
override_local_dns = mkDefault true;
nameservers.global = optionals cfg.settings.dns.override_local_dns [
"1.1.1.1"
"1.0.0.1"
"2606:4700:4700::1111"
"2606:4700:4700::1001"
];
};
};
};
services.nginx.virtualHosts = mkIf cfg.reverseProxy.enable {
"${fqdn}" = mkVirtualHost {
inherit (cfg) address port;
ssl = cfg.reverseProxy.forceSSL;
};
};
networking.firewall = mkIf cfg.openFirewall {
allowedTCPPorts = [
80
443
];
allowedUDPPorts = optional cfg.settings.derp.server.enable 3478;
};
};
}

View file

@ -0,0 +1,27 @@
{ pkgs, lib, ... }:
let
inherit (lib) mkDefault;
in
{
programs.hyprland.enable = mkDefault true;
programs.dconf.enable = true; # fixes nixvim hm module
xdg.portal = {
enable = true;
extraPortals = with pkgs; [
xdg-desktop-portal-gtk
];
};
services.gnome.gnome-keyring.enable = true;
security.pam.services = {
login = {
enableGnomeKeyring = true;
};
hyprlock = { };
};
services.udisks2.enable = mkDefault true;
}

View file

@ -0,0 +1,48 @@
# TODO: HM config for i2p profile in LibreWolf
{ config, lib, ... }:
let
cfg = config.services.i2pd;
inherit (lib)
mkDefault
mkIf
mkOption
optional
types
;
in
{
options.services.i2pd = {
openFirewall = mkOption {
type = types.bool;
default = false;
description = "Whether to open the necessary firewall ports for enabled protoclos of i2pd.";
};
};
config = mkIf cfg.enable {
services.i2pd = {
address = mkDefault "127.0.0.1";
proto = {
http.enable = mkDefault true;
socksProxy.enable = mkDefault true;
httpProxy.enable = mkDefault true;
sam.enable = mkDefault true;
i2cp.enable = mkDefault true;
};
};
networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall (
with cfg.proto;
optional bob.enable bob.port
++ optional http.enable http.port
++ optional httpProxy.enable httpProxy.port
++ optional i2cp.enable i2cp.port
++ optional i2pControl.enable i2pControl.port
++ optional sam.enable sam.port
++ optional socksProxy.enable socksProxy.port
);
};
}

View file

@ -0,0 +1,66 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.jellyfin;
domain = config.networking.domain;
subdomain = cfg.reverseProxy.subdomain;
fqdn = if (cfg.reverseProxy.enable && subdomain != "") then "${subdomain}.${domain}" else domain;
inherit (lib)
mkDefault
mkIf
mkOption
types
;
inherit (lib.utils)
mkReverseProxyOption
mkVirtualHost
;
in
{
options.services.jellyfin = {
reverseProxy = mkReverseProxyOption "Jellyfin" "jf";
libraries = mkOption {
type = types.listOf types.str;
default = [
"movies"
"music"
"shows"
];
description = "A list of library names. Directories for these will be created under ${cfg.dataDir}/libraries.";
};
};
config = mkIf cfg.enable {
services.jellyfin = {
openFirewall = mkDefault false;
};
environment.systemPackages = with pkgs; [
jellyfin-web
jellyfin-ffmpeg
];
systemd.tmpfiles.rules =
(map (
library: "d ${cfg.dataDir}/libraries/${library} 0770 ${cfg.user} ${cfg.group} -"
) cfg.libraries)
++ [
"z ${cfg.dataDir} 0770 ${cfg.user} ${cfg.group} -"
"Z ${cfg.dataDir}/libraries 0770 ${cfg.user} ${cfg.group} -"
];
services.nginx.virtualHosts = mkIf cfg.reverseProxy.enable {
"${fqdn}" = mkVirtualHost {
port = 8096;
ssl = cfg.reverseProxy.forceSSL;
};
};
};
}

View file

@ -0,0 +1,44 @@
{ config, lib, ... }:
let
cfg = config.services.jirafeau;
domain = config.networking.domain;
subdomain = cfg.reverseProxy.subdomain;
fqdn = if (cfg.reverseProxy.enable && subdomain != "") then "${subdomain}.${domain}" else domain;
inherit (lib)
mkDefault
mkIf
;
inherit (lib.utils)
mkReverseProxyOption
;
in
{
options.services.jirafeau = {
reverseProxy = mkReverseProxyOption "Jirafeau" "share";
};
config = mkIf cfg.enable {
services.jirafeau = {
hostName = fqdn;
extraConfig = mkDefault ''
$cfg['style'] = 'dark-courgette';
$cfg['maximal_upload_size'] = 4096;
'';
nginxConfig = {
enableACME = if cfg.reverseProxy.enable then cfg.reverseProxy.forceSSL else mkDefault false;
forceSSL = if cfg.reverseProxy.enable then cfg.reverseProxy.forceSSL else mkDefault false;
listenAddresses = mkDefault [ "0.0.0.0" ]; # FIXME: 127.0.0.1 does not work
serverName = fqdn;
sslCertificate =
mkIf (with cfg.reverseProxy; enable && forceSSL)
"${config.security.acme.certs."${fqdn}".directory}/cert.pem";
sslCertificateKey =
mkIf (with cfg.reverseProxy; enable && forceSSL)
"${config.security.acme.certs."${fqdn}".directory}/key.pem";
};
};
};
}

View file

@ -0,0 +1,104 @@
{
inputs,
config,
lib,
pkgs,
...
}:
let
cfg = config.mailserver;
domain = config.networking.domain;
fqdn = "${cfg.subdomain}.${domain}";
inherit (lib)
mapAttrs'
mkDefault
mkIf
mkOption
nameValuePair
types
;
in
{
imports = [ inputs.nixos-mailserver.nixosModules.mailserver ];
options.mailserver = {
subdomain = mkOption {
type = types.str;
default = "mail";
description = "Subdomain for rDNS";
};
accounts = mkOption {
type = types.attrsOf (
types.submodule {
options = {
aliases = mkOption {
type = types.listOf types.str;
default = [ ];
description = "A list of aliases of this account. `@domain` will be appended automatically.";
};
sendOnly = mkOption {
type = types.bool;
default = false;
description = "Specifies if the account should be a send-only account.";
};
};
}
);
default = { };
description = ''
This options wraps `loginAccounts`.
`loginAccounts.<attr-name>.name` will be automatically set to `<attr-name>@<domain>`.
'';
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion = cfg.subdomain != "";
message = "synix/nixos/mailserver: config.mailserver.subdomain cannot be empty.";
}
];
mailserver = {
inherit fqdn;
domains = mkDefault [ domain ];
certificateScheme = mkDefault "acme-nginx";
stateVersion = mkDefault 1;
loginAccounts = mapAttrs' (
user: accConf:
nameValuePair "${user}@${domain}" {
name = "${user}@${domain}";
aliases = map (alias: "${alias}@${domain}") (accConf.aliases or [ ]);
sendOnly = accConf.sendOnly;
quota = mkDefault "5G";
hashedPasswordFile = config.sops.secrets."mailserver/accounts/${user}".path;
}
) cfg.accounts;
};
security.acme = {
acceptTerms = true;
defaults.email = "postmaster@${domain}";
defaults.webroot = "/var/lib/acme/acme-challenge";
};
environment.systemPackages = [ pkgs.mailutils ];
sops = {
secrets = mapAttrs' (
user: _config:
nameValuePair "mailserver/accounts/${user}" {
restartUnits = [
"postfix.service"
"dovecot.service"
];
}
) cfg.accounts;
};
};
}

View file

@ -0,0 +1,151 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.matrix-synapse;
mkMautrixBridgeOptions = name: pkgName: {
enable = mkEnableOption "Mautrix-${name} for your Matrix-Synapse instance.";
package = mkPackageOption pkgs pkgName { };
admin = mkOption {
type = types.str;
description = "The user to give admin permissions to.";
example = "@admin:example.com";
};
};
mkMautrixBridge = name: port: {
environment.systemPackages = [ cfg.bridges.${name}.package ];
services."mautrix-${name}" = {
enable = true;
package = cfg.bridges.${name}.package;
environmentFile = mkIf cfg.sops config.sops.templates."mautrix-${name}/env-file".path;
settings = {
bridge = {
permissions = {
"*" = "relay";
"${cfg.settings.server_name}" = "user";
"${cfg.bridges.${name}.admin}" = "admin";
};
};
homeserver = {
address = "http://localhost:${toString cfg.port}";
domain = cfg.settings.server_name;
};
appservice = {
address = "http://localhost:${toString port}";
public_address = cfg.settings.public_baseurl;
hostname = "localhost";
inherit port;
};
provisioning.shared_secret = "$MAUTRIX_${toUpper name}_PROVISIONING_SHARED_SECRET";
public_media = {
enabled = false;
signing_key = "$MAUTRIX_${toUpper name}_PUBLIC_MEDIA_SIGNING_KEY";
};
direct_media = {
enabled = false;
server_key = "$MAUTRIX_${toUpper name}_DIRECT_MEDIA_SERVER_KEY";
};
backfill = {
enabled = true;
};
encryption = {
allow = true;
default = true;
require = false;
pickle_key = "$MAUTRIX_${toUpper name}_ENCRYPTION_PICKLE_KEY";
};
};
};
sops = mkIf cfg.sops (
let
owner = "mautrix-${name}";
group = "mautrix-${name}";
mode = "0400";
in
{
secrets."mautrix-${name}/encryption-pickle-key" = {
inherit owner group mode;
};
secrets."mautrix-${name}/provisioning-shared-secret" = {
inherit owner group mode;
};
secrets."mautrix-${name}/public-media-signing-key" = {
inherit owner group mode;
};
secrets."mautrix-${name}/direct-media-server-key" = {
inherit owner group mode;
};
templates."mautrix-${name}/env-file" = {
inherit owner group mode;
content = ''
MAUTRIX_${toUpper name}_ENCRYPTION_PICKLE_KEY=${
config.sops.placeholder."mautrix-${name}/encryption-pickle-key"
}
MAUTRIX_${toUpper name}_PROVISIONING_SHARED_SECRET=${
config.sops.placeholder."mautrix-${name}/provisioning-shared-secret"
}
MAUTRIX_${toUpper name}_PUBLIC_MEDIA_SIGNING_KEY=${
config.sops.placeholder."mautrix-${name}/public-media-signing-key"
}
MAUTRIX_${toUpper name}_DIRECT_MEDIA_SERVER_KEY=${
config.sops.placeholder."mautrix-${name}/direct-media-server-key"
}
'';
};
}
);
};
inherit (lib)
mkEnableOption
mkIf
mkMerge
mkOption
mkPackageOption
toUpper
types
;
inherit (builtins) toString;
in
{
options.services.matrix-synapse = {
bridges = {
whatsapp = mkMautrixBridgeOptions "WhatsApp" "mautrix-whatsapp";
signal = mkMautrixBridgeOptions "Signal" "mautrix-signal";
};
};
config = mkMerge [
(mkIf cfg.bridges.whatsapp.enable (mkMautrixBridge "whatsapp" 29318))
(mkIf cfg.bridges.whatsapp.enable {
services.mautrix-whatsapp = {
settings = {
network = {
displayname_template = "{{or .BusinessName .PushName .Phone}} (WA)";
history_sync.request_full_sync = true;
};
};
};
})
(mkIf cfg.bridges.signal.enable (mkMautrixBridge "signal" 29328))
(mkIf cfg.bridges.signal.enable {
services.mautrix-signal = {
settings = {
network = {
displayname_template = "{{or .ProfileName .PhoneNumber \"Unknown user\" }} (S)";
};
};
};
})
];
}

View file

@ -0,0 +1,189 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.matrix-synapse;
baseUrl = "https://${cfg.settings.server_name}";
baseClientConfig = {
"m.homeserver".base_url = baseUrl;
"m.identity_server".base_url = "https://vector.im";
};
livekitConfig = optionalAttrs config.services.livekit.enable {
"org.matrix.msc3575.proxy".url = baseUrl;
"org.matrix.msc4143.rtc_foci" = [
{
type = "livekit";
livekit_service_url = baseUrl + "/livekit/jwt";
}
];
};
clientConfig = baseClientConfig // livekitConfig;
serverConfig."m.server" = "${cfg.settings.server_name}:443";
mkWellKnown = data: ''
default_type application/json;
add_header Access-Control-Allow-Origin *;
return 200 '${builtins.toJSON data}';
'';
inherit (lib)
mkEnableOption
mkIf
mkMerge
mkOption
optionalAttrs
types
;
in
{
imports = [
./bridges.nix
./livekit.nix
];
options.services.matrix-synapse = {
port = mkOption {
type = types.nullOr types.port;
default = 8008;
description = ''
The port to listen for HTTP(S) requests on (will be applied to a listener).
'';
};
coturn = {
enable = mkEnableOption "Coturn integration for Matrix Synapse.";
realm = mkOption {
type = types.str;
default = "turn.${config.networking.domain}";
description = "Realm for the coturn server used by Matrix Synapse.";
};
listening-port = mkOption {
type = types.port;
default = 3478;
};
tls-listening-port = mkOption {
type = types.port;
default = 5349;
};
alt-listening-port = mkOption {
type = types.port;
default = 3479;
};
alt-tls-listening-port = mkOption {
type = types.port;
default = 5350;
};
};
sops = mkEnableOption "SOPS integration";
};
config = mkIf cfg.enable {
services.postgresql = {
enable = true;
initialScript = pkgs.writeText "synapse-init.sql" ''
CREATE ROLE "matrix-synapse" WITH LOGIN PASSWORD 'synapse';
CREATE DATABASE "matrix-synapse" WITH OWNER "matrix-synapse"
TEMPLATE template0
LC_COLLATE = 'C'
LC_CTYPE = 'C';
'';
};
services.matrix-synapse = mkMerge [
{
settings = {
registration_shared_secret_path =
mkIf cfg.sops
config.sops.secrets."matrix/registration-shared-secret".path;
server_name = config.networking.domain;
public_baseurl = baseUrl;
listeners = [
{
inherit (cfg) port;
bind_addresses = [ "127.0.0.1" ];
resources = [
{
compress = true;
names = [ "client" ];
}
{
compress = false;
names = [ "federation" ];
}
];
tls = false;
type = "http";
x_forwarded = true;
}
];
};
}
(mkIf cfg.coturn.enable {
settings = {
turn_uris = with cfg.coturn; [
"turn:${realm}:${toString listening-port}?transport=udp"
"turn:${realm}:${toString listening-port}?transport=tcp"
"turn:${realm}:${toString tls-listening-port}?transport=udp"
"turn:${realm}:${toString tls-listening-port}?transport=tcp"
"turn:${realm}:${toString alt-listening-port}?transport=udp"
"turn:${realm}:${toString alt-listening-port}?transport=tcp"
"turn:${realm}:${toString alt-tls-listening-port}?transport=udp"
"turn:${realm}:${toString alt-tls-listening-port}?transport=tcp"
];
extraConfigFiles = mkIf cfg.sops [ config.sops.templates."coturn/static-auth-secret.env".path ];
turn_user_lifetime = "1h";
};
})
];
environment.shellAliases = mkIf cfg.sops {
register_new_matrix_user = "${cfg.package}/bin/register_new_matrix_user -k $(sudo cat ${cfg.settings.registration_shared_secret_path})";
};
services.nginx.virtualHosts."${cfg.settings.server_name}" = {
enableACME = true;
forceSSL = true;
locations."= /.well-known/matrix/server".extraConfig = mkWellKnown serverConfig;
locations."= /.well-known/matrix/client".extraConfig = mkWellKnown clientConfig;
locations."/_matrix".proxyPass = "http://127.0.0.1:${toString cfg.port}";
locations."/_synapse".proxyPass = "http://127.0.0.1:${toString cfg.port}";
};
sops = mkIf cfg.sops {
secrets = mkMerge [
{
"matrix/registration-shared-secret" = {
owner = "matrix-synapse";
group = "matrix-synapse";
mode = "0400";
};
}
(mkIf cfg.coturn.enable {
"coturn/static-auth-secret" = {
owner = "turnserver";
group = "turnserver";
mode = "0400";
};
})
];
templates = mkIf cfg.coturn.enable {
"coturn/static-auth-secret.env" = {
owner = "matrix-synapse";
group = "matrix-synapse";
mode = "0400";
content = ''
static-auth-secret=${config.sops.placeholder."coturn/static-auth-secret"}
'';
};
};
};
};
}

View file

@ -0,0 +1,61 @@
{
config,
lib,
...
}:
let
cfg = config.services.matrix-synapse;
domain = config.networking.domain;
inherit (lib) mkIf mkDefault;
in
{
config = mkIf cfg.enable {
services.livekit = {
enable = true;
settings.port = mkDefault 7880;
settings.room.auto_create = mkDefault false;
openFirewall = mkDefault true;
keyFile = mkIf cfg.sops config.sops.templates."livekit/key".path;
};
services.lk-jwt-service = {
enable = true;
port = mkDefault 8080;
livekitUrl = "wss://${domain}/livekit/sfu";
keyFile = mkIf cfg.sops config.sops.templates."livekit/key".path;
};
systemd.services.lk-jwt-service.environment.LIVEKIT_FULL_ACCESS_HOMESERVERS = domain;
services.nginx.virtualHosts = {
"${domain}".locations = {
"^~ /livekit/jwt/" = {
priority = 400;
proxyPass = "http://127.0.0.1:${toString config.services.lk-jwt-service.port}/";
};
"^~ /livekit/sfu/" = {
priority = 400;
proxyPass = "http://127.0.0.1:${toString config.services.livekit.settings.port}/";
proxyWebsockets = true;
extraConfig = ''
proxy_send_timeout 120;
proxy_read_timeout 120;
proxy_buffering off;
proxy_set_header Accept-Encoding gzip;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
'';
};
};
};
sops = mkIf cfg.sops {
secrets."livekit/key" = { };
templates."livekit/key".content = ''
API Secret: ${config.sops.placeholder."livekit/key"}
'';
};
};
}

View file

@ -0,0 +1,110 @@
{ config, lib, ... }:
let
cfg = config.services.maubot;
user = config.users.users.maubot;
synapse = config.services.matrix-synapse;
inherit (lib)
concatLines
mkEnableOption
mkIf
mkOption
optionalString
types
;
inherit (builtins) toString listToAttrs;
in
{
options.services.maubot = {
admins = mkOption {
type = types.listOf types.str;
default = [ ];
example = [
"alice"
"bob"
];
description = "List of admin users for Maubot. Each admin must have a corresponding entry in the SOPS file under 'maubot/admins/<admin>' containing their password";
};
sops = mkEnableOption "SOPS integration";
};
config = mkIf cfg.enable {
services.maubot = {
extraConfigFile = mkIf cfg.sops config.sops.templates."maubot/extra-config-file".path;
settings = {
server = {
port = 29316;
public_url = synapse.settings.public_baseurl;
};
plugin_directories = with user; {
upload = home + "/plugins";
load = [ (home + "/plugins") ];
trash = home + "/trash";
};
plugin_databases = with user; {
sqlite = home + "/plugins";
};
# FIXME: ValueError: dictionary doesn't specify a version
# logging = with user; {
# handlers.file.filename = home + "/maubot.log";
# };
};
};
environment.systemPackages = [
cfg.package
];
systemd.tmpfiles.rules = [
"d ${cfg.dataDir} 0755 ${user.name} ${user.group} -"
"d ${cfg.settings.plugin_directories.upload} 0755 ${user.name} ${user.group} -"
"d ${cfg.settings.plugin_directories.trash} 0755 ${user.name} ${user.group} -"
];
services.nginx.virtualHosts."${synapse.settings.server_name}".locations = {
"^~ /_matrix/maubot/" = {
proxyPass = with cfg.settings.server; "http://${hostname}:${toString port}";
proxyWebsockets = true;
};
"^~ /_matrix/maubot/v1/logs" = {
proxyPass = with cfg.settings.server; "http://${hostname}:${toString port}";
proxyWebsockets = true;
};
};
sops = mkIf cfg.sops (
let
owner = user.name;
group = user.group;
mode = "0400";
in
{
secrets = listToAttrs (
map (admin: {
name = "maubot/admins/${admin}";
value = { inherit owner group mode; };
}) cfg.admins
);
templates."maubot/extra-config-file" = {
inherit owner group mode;
content = ''
homeservers:
${synapse.settings.server_name}:
url: http://127.0.0.1:${toString synapse.port}
secret: ${config.sops.placeholder."matrix/registration-shared-secret"}
''
+ optionalString (cfg.admins != [ ]) (
''
admins:
''
+ concatLines (
map (admin: " ${admin}: ${config.sops.placeholder."maubot/admins/${admin}"}") cfg.admins
)
);
};
}
);
};
}

View file

@ -0,0 +1,111 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.mcpo;
configFile = pkgs.writeText "mcpo-config.json" (builtins.toJSON cfg.settings);
inherit (lib)
getExe
mkEnableOption
mkIf
mkOption
types
;
in
{
options.services.mcpo = {
enable = mkEnableOption "mcpo, an MCP-to-OpenAPI proxy server.";
package = mkOption {
type = types.nullOr types.package;
description = "The package to use for mcpo. You have to specify this manually.";
default = null;
};
user = mkOption {
type = types.str;
description = "The user the mcpo service will run as.";
default = "mcpo";
};
group = mkOption {
type = types.str;
description = "The group the mcpo service will run as.";
default = "mcpo";
};
workDir = mkOption {
type = types.str;
description = "The working directory for the mcpo service.";
default = "/var/lib/mcpo";
};
settings = mkOption {
type = types.attrs;
description = "A set of attributes that will be translated into the JSON configuration file for mcpo. It follows the Claude Desktop format.";
default = { };
example = {
mcpServers = {
memory = {
command = "npx";
args = [
"-y"
"@modelcontextprotocol/server-memory"
];
};
time = {
command = "uvx";
args = [
"mcp-server-time"
"--local-timezone=America/New_York"
];
};
mcp_sse = {
type = "sse";
url = "http://127.0.0.1:8001/sse";
headers = {
Authorization = "Bearer token";
X-Custom-Header = "value";
};
};
mcp_streamable_http = {
type = "streamable_http";
url = "http://127.0.0.1:8002/mcp";
};
};
};
};
};
config = mkIf cfg.enable {
systemd.tmpfiles.rules = [
"d ${cfg.workDir} - ${cfg.user} ${cfg.group} - -"
];
users.users."${cfg.user}" = {
isSystemUser = true;
group = cfg.group;
};
users.groups."${cfg.group}" = { };
systemd.services.mcpo = {
description = "Service for mcpo, an MCP-to-OpenAPI proxy server.";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${getExe cfg.package} --config ${configFile}";
Restart = "on-failure";
User = cfg.user;
Group = cfg.group;
WorkingDirectory = cfg.workDir;
};
};
};
}

View file

@ -0,0 +1,57 @@
{
config,
lib,
...
}:
let
cfg = config.services.miniflux;
domain = config.networking.domain;
subdomain = cfg.reverseProxy.subdomain;
fqdn = if (cfg.reverseProxy.enable && subdomain != "") then "${subdomain}.${domain}" else domain;
port = 8085;
inherit (lib)
mkDefault
mkIf
;
inherit (lib.utils)
mkReverseProxyOption
mkVirtualHost
;
in
{
options.services.miniflux = {
reverseProxy = mkReverseProxyOption "Miniflux" "rss";
};
config = mkIf cfg.enable {
services.miniflux = {
adminCredentialsFile = config.sops.templates."miniflux/admin-credentials".path;
createDatabaseLocally = mkDefault true;
config = {
ADMIN_USERNAME = mkDefault "admin";
CREATE_ADMIN = mkDefault 1;
PORT = mkDefault port; # overrides LISTEN_ADDR to "0.0.0.0:${PORT}"
};
};
services.nginx.virtualHosts = mkIf cfg.reverseProxy.enable {
"${fqdn}" = mkVirtualHost {
port = cfg.config.PORT;
ssl = cfg.reverseProxy.forceSSL;
};
};
sops = {
secrets."miniflux/admin-password" = { };
templates."miniflux/admin-credentials" = {
content = ''
ADMIN_USERNAME=${cfg.config.ADMIN_USERNAME}
ADMIN_PASSWORD=${config.sops.placeholder."miniflux/admin-password"}
'';
};
};
};
}

View file

@ -0,0 +1,80 @@
{ config, lib, ... }:
let
cfg = config.services.nginx;
inherit (lib)
mkDefault
mkIf
mkOption
optional
optionals
types
;
in
{
options.services.nginx = {
forceSSL = mkOption {
type = types.bool;
default = false;
description = "Force SSL for Nginx virtual host.";
};
openFirewall = mkOption {
type = types.bool;
default = false;
description = "Whether to open the firewall for HTTP (and HTTPS if forceSSL is enabled).";
};
};
config = mkIf cfg.enable {
networking.firewall.allowedTCPPorts = optionals cfg.openFirewall (
[
80
]
++ optional cfg.forceSSL 443
);
services.nginx = {
recommendedOptimisation = mkDefault true;
recommendedGzipSettings = mkDefault true;
recommendedProxySettings = mkDefault true;
recommendedTlsSettings = cfg.forceSSL;
commonHttpConfig = "access_log syslog:server=unix:/dev/log;";
resolver.addresses =
let
isIPv6 = addr: builtins.match ".*:.*:.*" addr != null;
escapeIPv6 = addr: if isIPv6 addr then "[${addr}]" else addr;
cloudflare = [
"1.1.1.1"
"2606:4700:4700::1111"
];
resolvers =
if config.networking.nameservers == [ ] then cloudflare else config.networking.nameservers;
in
map escapeIPv6 resolvers;
sslDhparam = mkIf cfg.forceSSL config.security.dhparams.params.nginx.path;
virtualHosts = {
"${config.networking.domain}" = mkDefault {
enableACME = cfg.forceSSL;
forceSSL = cfg.forceSSL;
};
};
};
security.acme = mkIf cfg.forceSSL {
acceptTerms = true;
defaults.email = mkDefault "postmaster@${config.networking.domain}";
defaults.webroot = mkDefault "/var/lib/acme/acme-challenge";
certs."${config.networking.domain}".postRun = "systemctl reload nginx.service";
};
security.dhparams = mkIf cfg.forceSSL {
enable = true;
params.nginx = { };
};
};
}

View file

@ -0,0 +1,69 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.normalUsers;
inherit (lib)
attrNames
genAttrs
mkOption
types
;
in
{
options.normalUsers = mkOption {
type = types.attrsOf (
types.submodule {
options = {
extraGroups = mkOption {
type = (types.listOf types.str);
default = [ ];
description = "Extra groups for the user";
example = [ "wheel" ];
};
shell = mkOption {
type = types.path;
default = pkgs.zsh;
description = "Shell for the user";
};
initialPassword = mkOption {
type = types.str;
default = "changeme";
description = "Initial password for the user";
};
sshKeyFiles = mkOption {
type = (types.listOf types.path);
default = [ ];
description = "SSH key files for the user";
example = [ "/path/to/id_rsa.pub" ];
};
};
}
);
default = { };
description = "Users to create. The usernames are the attribute names.";
};
config = {
# Create user groups
users.groups = genAttrs (attrNames cfg) (userName: {
name = userName;
});
# Create users
users.users = genAttrs (attrNames cfg) (userName: {
name = userName;
inherit (cfg.${userName}) extraGroups shell initialPassword;
isNormalUser = true;
group = "${userName}";
home = "/home/${userName}";
openssh.authorizedKeys.keyFiles = cfg.${userName}.sshKeyFiles;
});
};
}

View file

@ -0,0 +1,30 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib) mkDefault;
in
{
boot.blacklistedKernelModules = [ "nouveau" ];
boot.extraModulePackages = [ config.hardware.nvidia.package ];
boot.initrd.kernelModules = [ "nvidia" ];
environment.systemPackages = with pkgs; [
nvtopPackages.nvidia
];
hardware.enableRedistributableFirmware = true;
hardware.graphics.enable = true;
hardware.nvidia.modesetting.enable = true;
hardware.nvidia.nvidiaSettings = true;
hardware.nvidia.open = false;
hardware.nvidia.package = mkDefault config.boot.kernelPackages.nvidiaPackages.latest;
nixpkgs.config.cudaSupport = true;
services.xserver.videoDrivers = [ "nvidia" ];
}

View file

@ -0,0 +1,47 @@
{ config, lib, ... }:
let
cfg = config.services.ollama;
domain = config.networking.domain;
subdomain = cfg.reverseProxy.subdomain;
fqdn = if (cfg.reverseProxy.enable && subdomain != "") then "${subdomain}.${domain}" else domain;
inherit (lib)
mkDefault
mkForce
mkIf
;
inherit (lib.utils)
mkReverseProxyOption
mkVirtualHost
;
in
{
options.services.ollama = {
reverseProxy = mkReverseProxyOption "Ollama" "ollama";
};
config = mkIf cfg.enable {
services.ollama = {
host = mkDefault (if cfg.reverseProxy.enable then "127.0.0.1" else "0.0.0.0");
user = mkDefault "ollama";
group = mkDefault "ollama";
};
services.nginx.virtualHosts = mkIf cfg.reverseProxy.enable {
"${fqdn}" = mkVirtualHost {
port = cfg.port;
ssl = cfg.reverseProxy.forceSSL;
recommendedProxySettings = mkForce false;
extraConfig = ''
proxy_set_header Host ${cfg.host}:${toString cfg.port};
'';
};
};
systemd.tmpfiles.rules = [
"d ${cfg.home} 0755 ${cfg.user} ${cfg.group} -"
];
};
}

View file

@ -0,0 +1,168 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.open-webui-oci;
defaultEnv = {
ANONYMIZED_TELEMETRY = "False";
BYPASS_MODEL_ACCESS_CONTROL = "True";
DEFAULT_LOCALE = "en";
DEFAULT_USER_ROLE = "user";
DO_NOT_TRACK = "True";
ENABLE_DIRECT_CONNECTIONS = "True";
ENABLE_IMAGE_GENERATION = "True";
ENABLE_PERSISTENT_CONFIG = "False";
ENABLE_SIGNUP = "False";
ENABLE_SIGNUP_PASSWORD_CONFIRMATION = "True";
ENABLE_USER_WEBHOOKS = "True";
ENABLE_WEBSEARCH = "True";
ENV = "prod";
SCARF_NO_ANALYTICS = "True";
USER_AGENT = "OpenWebUI";
USER_PERMISSIONS_FEATURES_DIRECT_TOOL_SERVERS = "True";
WEB_SEARCH_ENGINE = "DuckDuckGo";
};
inherit (lib)
concatStringsSep
literalExpression
mkEnableOption
mkIf
mkOption
mkOverride
optional
types
;
in
{
options.services.open-webui-oci = {
enable = mkEnableOption "Open WebUI container with Podman.";
externalUrl = mkOption {
type = types.nullOr types.str;
default = null;
example = literalExpression "http://${config.networking.domain}";
description = "Public URL to add to CORS.";
};
port = mkOption {
type = types.port;
default = 3000;
description = "Which port the Open-WebUI server listens to.";
};
environment = mkOption {
default = { };
type = types.attrsOf types.str;
description = ''
Extra environment variables for Open-WebUI.
For more details see <https://docs.openwebui.com/getting-started/advanced-topics/env-configuration/>
'';
};
environmentFile = mkOption {
description = "Environment file to be passed to the Open WebUI container.";
type = types.nullOr types.path;
default = null;
example = "config.sops.templates.open-webui-env.path";
};
};
config = mkIf cfg.enable {
virtualisation.podman = {
enable = true;
autoPrune.enable = true;
dockerCompat = true;
};
networking.firewall.interfaces =
let
matchAll = if !config.networking.nftables.enable then "podman+" else "podman*";
in
{
"${matchAll}".allowedUDPPorts = [ 53 ];
};
virtualisation.oci-containers.backend = "podman";
virtualisation.oci-containers.containers."open-webui" = {
image = "ghcr.io/open-webui/open-webui:main";
environment =
defaultEnv
// cfg.environment
// {
PORT = "${toString cfg.port}";
CORS_ALLOW_ORIGIN = concatStringsSep ";" (
[
"http://localhost:${toString cfg.port}"
"http://127.0.0.1:${toString cfg.port}"
"http://0.0.0.0:${toString cfg.port}"
]
++ optional (cfg.externalUrl != null) cfg.externalUrl
);
};
environmentFiles = optional (cfg.environmentFile != null) cfg.environmentFile;
volumes = [
"open-webui_open-webui:/app/backend/data:rw"
];
log-driver = "journald";
extraOptions = [
"--network=host"
];
};
systemd.services."podman-open-webui" = {
serviceConfig = {
Restart = mkOverride 90 "always";
};
after = [
"podman-network-open-webui_default.service"
"podman-volume-open-webui_open-webui.service"
];
requires = [
"podman-network-open-webui_default.service"
"podman-volume-open-webui_open-webui.service"
];
partOf = [
"podman-compose-open-webui-root.target"
];
wantedBy = [
"podman-compose-open-webui-root.target"
];
};
systemd.services."podman-network-open-webui_default" = {
path = [ pkgs.podman ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStop = "podman network rm -f open-webui_default";
};
script = ''
podman network inspect open-webui_default || podman network create open-webui_default
'';
partOf = [ "podman-compose-open-webui-root.target" ];
wantedBy = [ "podman-compose-open-webui-root.target" ];
};
systemd.services."podman-volume-open-webui_open-webui" = {
path = [ pkgs.podman ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
podman volume inspect open-webui_open-webui || podman volume create open-webui_open-webui
'';
partOf = [ "podman-compose-open-webui-root.target" ];
wantedBy = [ "podman-compose-open-webui-root.target" ];
};
systemd.targets."podman-compose-open-webui-root" = {
unitConfig = {
Description = "Root target generated by compose2nix.";
};
wantedBy = [ "multi-user.target" ];
};
};
}

View file

@ -0,0 +1,16 @@
{ lib, ... }:
let
inherit (lib) mkDefault;
in
{
services.openssh = {
enable = mkDefault true;
ports = mkDefault [ 2299 ];
openFirewall = mkDefault true;
settings = {
PermitRootLogin = mkDefault "no";
PasswordAuthentication = mkDefault false;
};
};
}

View file

@ -0,0 +1,83 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.print-server;
domain = config.networking.domain;
subdomain = cfg.reverseProxy.subdomain;
fqdn = if (cfg.reverseProxy.enable && subdomain != "") then "${subdomain}.${domain}" else domain;
port = 631;
inherit (lib)
mkEnableOption
mkIf
mkOption
types
;
inherit (lib.utils)
mkReverseProxyOption
mkVirtualHost
;
in
{
options.services.print-server = {
enable = mkEnableOption "print server";
openFirewall = mkOption {
type = types.bool;
default = false;
description = "Open firewall for printing and avahi service.";
};
reverseProxy = mkReverseProxyOption "print-server" "print";
};
config = mkIf cfg.enable {
services.printing = {
enable = true;
listenAddresses = [ "*:${builtins.toString port}" ];
webInterface = true;
tempDir = "/tmp/cups";
allowFrom = [ "all" ];
snmpConf = ''
Address @LOCAL
'';
clientConf = "";
openFirewall = cfg.openFirewall;
drivers = with pkgs; [
brlaser
brgenml1lpr
brgenml1cupswrapper # Brother
postscript-lexmark # Lexmark
hplip
hplipWithPlugin # HP
splix
samsung-unified-linux-driver # Samsung
gutenprint
gutenprintBin # different vendors
];
defaultShared = true;
browsing = true;
browsedConf = ''
BrowsePoll ${fqdn}
'';
};
# autodiscovery of network printers
services.avahi = {
enable = true;
nssmdns4 = true;
openFirewall = cfg.openFirewall;
};
services.nginx.virtualHosts = mkIf cfg.reverseProxy.enable {
${fqdn} = mkVirtualHost {
inherit port;
ssl = cfg.reverseProxy.forceSSL;
};
};
};
}

View file

@ -0,0 +1,116 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.radicale;
domain = config.networking.domain;
subdomain = cfg.reverseProxy.subdomain;
fqdn = if (cfg.reverseProxy.enable && subdomain != "") then "${subdomain}.${domain}" else domain;
port = 5232;
inherit (lib)
concatLines
mkDefault
mkIf
mkOption
types
;
inherit (lib.utils)
mkReverseProxyOption
;
in
{
options.services.radicale = {
users = mkOption {
type = types.listOf types.str;
default = [ ];
example = [
"alice"
"bob"
];
description = "List of users for Radicale. Each user must have a corresponding entry in the SOPS file under 'radicale/<user>'";
};
reverseProxy = mkReverseProxyOption "Radicale" "dav";
};
config = mkIf cfg.enable {
services.radicale = {
settings = {
server = {
hosts = [
"${if cfg.reverseProxy.enable then "127.0.0.1" else "0.0.0.0"}:${builtins.toString port}"
];
max_connections = mkDefault 20;
max_content_length = mkDefault 500000000;
timeout = mkDefault 30;
};
auth = {
type = "htpasswd";
delay = mkDefault 1;
htpasswd_filename = config.sops.templates."radicale/users".path;
htpasswd_encryption = mkDefault "sha512";
};
storage = {
filesystem_folder = mkDefault "/var/lib/radicale/collections";
};
};
};
services.nginx.virtualHosts = mkIf cfg.reverseProxy.enable {
"${fqdn}" = {
forceSSL = cfg.reverseProxy.forceSSL;
enableACME = cfg.reverseProxy.forceSSL;
locations = {
"/" = {
proxyPass = "http://localhost:${builtins.toString port}/";
extraConfig = ''
proxy_set_header X-Script-Name /;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass_header Authorization;
'';
};
"/.well-known/caldav" = {
return = "301 $scheme://$host/";
};
"/.well-known/carddav" = {
return = "301 $scheme://$host/";
};
};
};
};
environment.systemPackages = [
pkgs.openssl
];
sops =
let
owner = "radicale";
group = "radicale";
mode = "0440";
mkSecrets =
users:
builtins.listToAttrs (
map (user: {
name = "radicale/${user}";
value = { inherit owner group mode; };
}) users
);
mkTemplate = users: {
inherit owner group mode;
content = concatLines (map (user: "${user}:${config.sops.placeholder."radicale/${user}"}") users);
};
in
{
secrets = mkSecrets cfg.users;
templates."radicale/users" = mkTemplate cfg.users;
};
};
}

View file

@ -0,0 +1,40 @@
{ config, lib, ... }:
let
cfg = config.services.rss-bridge;
domain = config.networking.domain;
subdomain = cfg.reverseProxy.subdomain;
fqdn = if (cfg.reverseProxy.enable && subdomain != "") then "${subdomain}.${domain}" else domain;
inherit (lib)
mkIf
;
inherit (lib.utils)
mkReverseProxyOption
mkVirtualHost
;
in
{
options.services.rss-bridge = {
reverseProxy = mkReverseProxyOption "RSS-Bridge" "rss-bridge";
};
config = mkIf cfg.enable {
services.rss-bridge = {
webserver = if cfg.reverseProxy.enable then "nginx" else null;
virtualHost = if cfg.reverseProxy.enable then fqdn else null;
config = {
system.enabled_bridges = [ "*" ];
};
};
systemd.tmpfiles.rules = [ "d ${cfg.dataDir} 0755 ${cfg.user} ${cfg.group} -" ];
services.nginx.virtualHosts = mkIf cfg.reverseProxy.enable {
"${cfg.virtualHost}" = mkVirtualHost {
ssl = cfg.reverseProxy.forceSSL;
};
};
};
}

View file

@ -0,0 +1,21 @@
{
inputs,
config,
lib,
pkgs,
...
}:
let
secrets = "${toString inputs.self}/hosts/${config.networking.hostName}/secrets/secrets.yaml";
in
{
imports = [ inputs.sops-nix.nixosModules.sops ];
environment.systemPackages = with pkgs; [
age
sops
];
sops.defaultSopsFile = lib.mkIf (builtins.pathExists secrets) (lib.mkDefault secrets);
}

View file

@ -0,0 +1,50 @@
{ config, lib, ... }:
let
cfg = config.services.tailscale;
inherit (lib)
mkIf
mkOption
optional
types
;
in
{
options.services.tailscale = {
loginServer = mkOption {
type = types.str;
description = "The Tailscale login server to use.";
};
enableSSH = mkOption {
type = types.bool;
default = false;
description = "Enable Tailscale SSH functionality.";
};
acceptDNS = mkOption {
type = types.bool;
default = true;
description = "Enable Tailscale's MagicDNS and custom DNS configuration.";
};
};
config = mkIf cfg.enable {
services.tailscale = {
authKeyFile = config.sops.secrets."tailscale/auth-key".path;
extraSetFlags = optional cfg.enableSSH "--ssh" ++ optional cfg.acceptDNS "--accept-dns";
extraUpFlags = [
"--login-server=${cfg.loginServer}"
]
++ optional cfg.enableSSH "--ssh"
++ optional cfg.acceptDNS "--accept-dns";
};
environment.shellAliases = {
ts = "${cfg.package}/bin/tailscale";
};
networking.firewall.trustedInterfaces = [ cfg.interfaceName ];
sops.secrets."tailscale/auth-key" = { };
};
}

View file

@ -0,0 +1,92 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.virtualisation;
boolToZeroOne = x: if x then "1" else "0";
aclString = strings.concatMapStringsSep ''
,
'' strings.escapeNixString cfg.libvirtd.deviceACL;
inherit (lib)
mkDefault
mkOption
optionalString
optionals
strings
types
;
in
{
imports = [
./hugepages.nix
./kvmfr.nix
./quickemu.nix
./vfio.nix
];
options.virtualisation = {
libvirtd = {
deviceACL = mkOption {
type = types.listOf types.str;
default = [ ];
example = [
"/dev/kvm"
"/dev/net/tun"
"/dev/vfio/vfio"
];
description = "List of device paths that QEMU processes are allowed to access.";
};
clearEmulationCapabilities = mkOption {
type = types.bool;
default = true;
description = "Whether to remove privileged Linux capabilities from QEMU processes after they start.";
};
};
};
config = {
virtualisation = {
libvirtd = {
enable = mkDefault true;
onBoot = mkDefault "ignore";
onShutdown = mkDefault "shutdown";
qemu.runAsRoot = mkDefault false;
qemu.verbatimConfig = ''
clear_emulation_capabilities = ${boolToZeroOne cfg.libvirtd.clearEmulationCapabilities}
''
+ optionalString (cfg.libvirtd.deviceACL != [ ]) ''
cgroup_device_acl = [
${aclString}
]
'';
qemu.swtpm.enable = mkDefault true; # TPM 2.0
};
spiceUSBRedirection.enable = mkDefault true;
};
users.users."qemu-libvirtd" = {
extraGroups = optionals (!cfg.libvirtd.qemu.runAsRoot) [
"kvm"
"input"
];
isSystemUser = true;
};
programs.virt-manager.enable = mkDefault true;
environment.systemPackages = [
(pkgs.writeShellScriptBin "iommu-groups" (builtins.readFile ./iommu-groups.sh))
pkgs.dnsmasq
pkgs.qemu_full
pkgs.virtio-win
];
};
}

View file

@ -0,0 +1,45 @@
{
config,
lib,
...
}:
let
cfg = config.virtualisation.hugepages;
inherit (lib)
mkEnableOption
mkOption
optionals
types
;
in
{
options.virtualisation.hugepages = {
enable = mkEnableOption "huge pages.";
defaultPageSize = mkOption {
type = types.strMatching "[0-9]*[kKmMgG]";
default = "1M";
description = "Default size of huge pages. You can use suffixes K, M, and G to specify KB, MB, and GB.";
};
pageSize = mkOption {
type = types.strMatching "[0-9]*[kKmMgG]";
default = "1M";
description = "Size of huge pages that are allocated at boot. You can use suffixes K, M, and G to specify KB, MB, and GB.";
};
numPages = mkOption {
type = types.ints.positive;
default = 1;
description = "Number of huge pages to allocate at boot.";
};
};
config.boot.kernelParams = optionals cfg.enable [
"default_hugepagesz=${cfg.defaultPageSize}"
"hugepagesz=${cfg.pageSize}"
"hugepages=${toString cfg.numPages}"
];
}

View file

@ -0,0 +1,6 @@
shopt -s nullglob
for d in /sys/kernel/iommu_groups/*/devices/*; do
n=${d#*/iommu_groups/*}; n=${n%%/*}
printf 'IOMMU Group %s ' "$n"
lspci -nns "${d##*/}"
done;

View file

@ -0,0 +1,157 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.virtualisation.kvmfr;
sizeFromResolution =
resolution:
let
ceilToPowerOf2 =
let
findNextPower = target: power: if power >= target then power else findNextPower target (power * 2);
in
n: if n <= 1 then 1 else findNextPower n 1;
pixelSize = if resolution.pixelFormat == "rgb24" then 3 else 4;
bytes = resolution.width * resolution.height * pixelSize * 2;
in
ceilToPowerOf2 (bytes / 1024 / 1024 + 10);
deviceSizes = map (device: device.size) cfg.devices;
devices = imap (index: _deviceConfig: "/dev/kvmfr${toString index}") cfg.devices;
udevPackage = pkgs.writeTextDir "/lib/udev/rules.d/99-kvmfr.rules" (
concatStringsSep "\n" (
imap0 (index: deviceConfig: ''
SUBSYSTEM=="kvmfr", KERNEL=="kvmfr${toString index}", OWNER="${deviceConfig.permissions.user}", GROUP="${deviceConfig.permissions.group}", MODE="${deviceConfig.permissions.mode}", TAG+="systemd"
'') cfg.devices
)
);
apparmorAbstraction = concatStringsSep "\n" (map (device: "${device} rw") devices);
permissionsType = types.submodule {
options = {
user = mkOption {
type = types.str;
default = "root";
description = "Owner of the shared memory device.";
};
group = mkOption {
type = types.str;
default = "root";
description = "Group of the shared memory device.";
};
mode = mkOption {
type = types.str;
default = "0600";
description = "Mode of the shared memory device.";
};
};
};
resolutionType = types.submodule {
options = {
width = mkOption {
type = types.number;
description = "Maximum horizontal video size that should be supported by this device.";
};
height = mkOption {
type = types.number;
description = "Maximum vertical video size that should be supported by this device.";
};
pixelFormat = mkOption {
type = types.enum [
"rgba32"
"rgb24"
];
description = "Pixel format to use.";
default = "rgba32";
};
};
};
deviceType = (
types.submodule (
{ config, options, ... }:
{
options = {
resolution = mkOption {
type = types.nullOr resolutionType;
default = null;
description = "Automatically calculate the minimum device size for a specific resolution. Overrides `size` if set.";
};
size = mkOption {
type = types.number;
description = "Size for the kvmfr device in megabytes.";
};
permissions = mkOption {
type = permissionsType;
default = { };
description = "Permissions of the kvmfr device.";
};
};
config = {
size = mkIf (config.resolution != null) (sizeFromResolution config.resolution);
};
}
)
);
inherit (lib)
concatStringsSep
imap
imap0
mkIf
mkOption
optionals
types
;
in
{
options.virtualisation.kvmfr = {
enable = mkOption {
type = types.bool;
default = false;
description = "Whether to enable the kvmfr kernel module.";
};
devices = mkOption {
type = types.listOf deviceType;
default = [ ];
description = "List of devices to create.";
};
};
config = mkIf cfg.enable {
boot.extraModulePackages = with config.boot.kernelPackages; [ kvmfr ];
services.udev.packages = optionals (cfg.devices != [ ]) [ udevPackage ];
environment.systemPackages = with pkgs; [ looking-glass-client ];
environment.etc = {
"modules-load.d/kvmfr.conf".text = ''
kvmfr
'';
"modprobe.d/kvmfr.conf".text = ''
options kvmfr static_size_mb=${concatStringsSep "," (map (size: toString size) deviceSizes)}
'';
"apparmor.d/local/abstractions/libvirt-qemu" = mkIf config.security.apparmor.enable {
text = apparmorAbstraction;
};
};
virtualisation.libvirtd.deviceACL = devices;
};
}

View file

@ -0,0 +1,28 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.virtualisation.quickemu;
inherit (lib) mkEnableOption mkIf;
in
{
options.virtualisation.quickemu = {
enable = mkEnableOption "quickemu";
};
config = mkIf cfg.enable {
environment.systemPackages = with pkgs; [
quickemu
];
boot.extraModprobeConfig = ''
options kvm_amd nested=1
options kvm ignore_msrs=1 report_ignored_msrs=0
'';
};
}

View file

@ -0,0 +1,103 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.virtualisation.vfio;
inherit (lib)
mkEnableOption
mkIf
mkOption
optional
optionals
types
versionOlder
;
in
{
options.virtualisation.vfio = {
enable = mkEnableOption "VFIO Configuration";
IOMMUType = mkOption {
type = types.enum [
"intel"
"amd"
];
example = "intel";
description = "Type of the IOMMU used";
};
devices = mkOption {
type = types.listOf (types.strMatching "[0-9a-f]{4}:[0-9a-f]{4}");
default = [ ];
example = [
"10de:1b80"
"10de:10f0"
];
description = "PCI IDs of devices to bind to vfio-pci";
};
disableEFIfb = mkOption {
type = types.bool;
default = false;
example = true;
description = "Disables the usage of the EFI framebuffer on boot.";
};
blacklistNvidia = mkOption {
type = types.bool;
default = false;
description = "Add Nvidia GPU modules to blacklist";
};
ignoreMSRs = mkOption {
type = types.bool;
default = false;
example = true;
description = "Enables or disables kvm guest access to model-specific registers";
};
};
config = mkIf cfg.enable {
services.udev.extraRules = ''
SUBSYSTEM=="vfio", OWNER="root", GROUP="kvm"
'';
boot.kernelParams =
(
if cfg.IOMMUType == "intel" then
[
"intel_iommu=on"
"intel_iommu=igfx_off"
]
else
[ "amd_iommu=on" ]
)
++ optional (cfg.devices != [ ]) ("vfio-pci.ids=" + builtins.concatStringsSep "," cfg.devices)
++ (optional cfg.disableEFIfb "video=efifb:off")
++ (
optionals cfg.ignoreMSRs [
"kvm.ignore_msrs=1"
"kvm.report_ignored_msrs=0"
]
++ optional cfg.blacklistNvidia "modprobe.blacklist=nouveau,nvidia,nvidia_drm,nvidia"
);
boot.initrd.kernelModules = [
"vfio_pci"
"vfio_iommu_type1"
"vfio"
]
++ optionals (versionOlder pkgs.linux.version "6.2") [ "vfio_virqfd" ];
# boot.blacklistedKernelModules = optionals cfg.blacklistNvidia [
# "nouveau"
# "nvidia"
# "nvidia_drm"
# ];
};
}

View file

@ -0,0 +1,66 @@
{ config, lib, ... }:
let
cfg = config.services.webPage;
domain = config.networking.domain;
fqdn = if (cfg.subdomain != "") then "${cfg.subdomain}.${domain}" else domain;
nginxUser = config.services.nginx.user;
inherit (lib)
mkEnableOption
mkIf
mkOption
types
;
in
{
options.services.webPage = {
enable = mkEnableOption "static web page hosting";
subdomain = mkOption {
type = types.str;
default = "www";
description = "The subdomain to serve the web page on. Leave empty for root domain.";
};
forceSSL = mkOption {
type = types.bool;
default = true;
description = "Force SSL for Nginx virtual host.";
};
webRoot = mkOption {
type = types.str;
description = "The root directory of the web page.";
example = "/var/www";
};
test = mkOption {
type = types.bool;
default = false;
description = "If true, a sample index.html will be placed in the web root for testing purposes.";
};
};
config = mkIf cfg.enable {
services.nginx.virtualHosts."${fqdn}" = {
enableACME = cfg.forceSSL;
forceSSL = cfg.forceSSL;
root = cfg.webRoot;
locations."/".index = "index.html";
sslCertificate = mkIf cfg.forceSSL "${config.security.acme.certs."${fqdn}".directory}/cert.pem";
sslCertificateKey = mkIf cfg.forceSSL "${config.security.acme.certs."${fqdn}".directory}/key.pem";
};
systemd.tmpfiles.rules = [ "d ${cfg.webRoot} 0755 ${nginxUser} ${nginxUser} -" ];
system.activationScripts.webPagePermissions = ''
chown -R ${nginxUser}:${nginxUser} ${cfg.webRoot}
chmod -R 0755 ${cfg.webRoot}
'';
# test page
environment.etc."sample.html" = mkIf cfg.test { source = ./sample.html; };
system.activationScripts.moveSampleHtmlToWebRoot = mkIf cfg.test ''
mv /etc/sample.html ${cfg.webRoot}/index.html
chown -R ${nginxUser}:${nginxUser} ${cfg.webRoot}
chmod -R 0755 ${cfg.webRoot}
'';
};
}

View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test Page</title>
</head>
<body>
<h1>Test Page</h1>
<p>This is a sample index.html file for testing purposes.</p>
</body>
</html>

View file

@ -0,0 +1,170 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.windows-oci;
inherit (lib)
mkEnableOption
mkIf
mkOption
mkOverride
optional
types
;
in
{
options.services.windows-oci = {
enable = mkEnableOption "Windows in an OCI container using Podman";
volume = mkOption {
type = types.str;
default = "/opt/windows";
description = "Path to the volume for Windows data.";
};
sharedVolume = mkOption {
type = types.nullOr types.str;
default = null;
description = "Path to a shared volume to mount inside the Windows container. You have to create this directory manually.";
};
settings = {
version = mkOption {
type = types.str;
default = "11";
example = "2025";
description = "Windows version to use.";
};
ramSize = mkOption {
type = types.str;
default = "8G";
description = "Amount of RAM to allocate to the Windows container.";
};
cpuCores = mkOption {
type = types.str;
default = "4";
description = "Number of CPU cores to allocate to the Windows container.";
};
diskSize = mkOption {
type = types.str;
default = "64G";
description = "Size of the virtual disk for the Windows container.";
};
username = mkOption {
type = types.str;
default = "admin";
description = "Username for the Windows installation.";
};
password = mkOption {
type = types.str;
default = "admin";
description = "Password for the Windows installation.";
};
language = mkOption {
type = types.str;
default = "English";
description = "Language for the Windows installation.";
};
region = mkOption {
type = types.str;
default = "en-DE";
description = "Region for the Windows installation.";
};
keyboard = mkOption {
type = types.str;
default = "de-DE";
description = "Keyboard layout for the Windows installation.";
};
};
};
config = mkIf cfg.enable {
systemd.tmpfiles.rules = [ "d ${cfg.volume} 0755 root podman -" ];
virtualisation.podman = {
enable = true;
autoPrune.enable = true;
dockerCompat = true;
defaultNetwork.settings = {
dns_enabled = true;
};
};
# https://github.com/NixOS/nixpkgs/issues/226365
networking.firewall.interfaces."podman+".allowedUDPPorts = [ 53 ];
virtualisation.oci-containers.backend = "podman";
virtualisation.oci-containers.containers."windows" = {
image = "dockurr/windows";
environment = with cfg.settings; {
"VERSION" = version;
"RAM_SIZE" = ramSize;
"CPU_CORES" = cpuCores;
"DISK_SIZE" = diskSize;
"USERNAME" = username;
"PASSWORD" = password;
"LANGUAGE" = language;
"REGION" = region;
"KEYBOARD" = keyboard;
};
volumes = [
"${cfg.volume}:/storage:rw"
]
++ optional (cfg.sharedVolume != null) "${cfg.sharedVolume}:/shared:rw";
ports = [
"8006:8006/tcp"
"3389:3389/tcp"
"3389:3389/udp"
];
log-driver = "journald";
extraOptions = [
"--cap-add=NET_ADMIN"
"--device=/dev/kvm:/dev/kvm:rwm"
"--device=/dev/net/tun:/dev/net/tun:rwm"
"--network-alias=windows"
"--network=windows_default"
];
};
systemd.services."podman-windows" = {
serviceConfig = {
Restart = mkOverride 90 "always";
};
after = [
"podman-network-windows_default.service"
];
requires = [
"podman-network-windows_default.service"
];
partOf = [
"podman-compose-windows-root.target"
];
wantedBy = [
"podman-compose-windows-root.target"
];
};
systemd.services."podman-network-windows_default" = {
path = [ pkgs.podman ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStop = "podman network rm -f windows_default";
};
script = ''
podman network inspect windows_default || podman network create windows_default
'';
partOf = [ "podman-compose-windows-root.target" ];
wantedBy = [ "podman-compose-windows-root.target" ];
};
systemd.targets."podman-compose-windows-root" = {
unitConfig = {
Description = "Root target generated by compose2nix.";
};
wantedBy = [ "multi-user.target" ];
};
};
}