merge: merged leyla/main

This commit is contained in:
Eve 2025-11-27 14:57:56 -06:00
parent 3a58722815
commit 0a8b3e1496
120 changed files with 2396 additions and 4519 deletions

View file

@ -2,5 +2,9 @@
{inputs, ...}: {
nixpkgs.overlays = [
inputs.nix-vscode-extensions.overlays.default
# Add noita_entangled_worlds from upstream flake to pkgs
(final: prev: {
noita_entangled_worlds = inputs.noita-entangled-worlds.packages.${prev.stdenv.hostPlatform.system}.noita-proxy;
})
];
}

View file

@ -1,4 +1,8 @@
{pkgs, ...}: {
{
pkgs,
inputs,
...
}: {
imports = [
./python
];
@ -16,15 +20,17 @@
./prostudiomasters.nix
{};
})
(final: prev: {
noita_entangled_worlds = pkgs.callPackage ./noita-entangled-worlds.nix {};
})
(final: prev: {
gdx-liftoff = pkgs.callPackage ./gdx-liftoff.nix {};
})
(final: prev: {
codium-extensions = pkgs.callPackage ./codium-extensions {};
})
(final: prev: {
firefox-extensions = pkgs.callPackage ./firefox-extensions {
inherit inputs;
};
})
(final: prev: {
mapillary-uploader = pkgs.callPackage ./mapillary-uploader.nix {};
})
@ -38,8 +44,5 @@
# Override h3 C library to version 4.3.0
h3 = pkgs.callPackage ./h3-c-lib.nix {};
})
(final: prev: {
polycule = pkgs.callPackage ./polycule {};
})
];
}

View file

@ -0,0 +1,17 @@
{
pkgs,
inputs,
...
}: let
inherit (inputs.firefox-addons.lib.${pkgs.stdenv.hostPlatform.system}) buildFirefoxXpiAddon;
in {
italiano-it-language-pack = pkgs.callPackage ./italiano-it-language-pack.nix {
inherit buildFirefoxXpiAddon;
};
dizionario-italiano = pkgs.callPackage ./dizionario-italiano.nix {
inherit buildFirefoxXpiAddon;
};
deutsch-de-language-pack = pkgs.callPackage ./deutsch-de-language-pack.nix {
inherit buildFirefoxXpiAddon;
};
}

View file

@ -0,0 +1,18 @@
{
lib,
buildFirefoxXpiAddon,
...
}:
buildFirefoxXpiAddon rec {
pname = "deutsch-de-language-pack";
version = "145.0.20251106.194447";
addonId = "langpack-de@firefox.mozilla.org";
url = "https://addons.mozilla.org/firefox/downloads/file/4614311/deutsch_de_language_pack-${version}.xpi";
sha256 = "aaaa95c29984fb3802a5e7edb6b7e5020c391d81f389b8a8133c163959ea4299";
meta = with lib; {
description = "Firefox Language Pack for Deutsch (de) German";
license = licenses.mpl20;
mozPermissions = [];
platforms = platforms.all;
};
}

View file

@ -0,0 +1,18 @@
{
lib,
buildFirefoxXpiAddon,
...
}:
buildFirefoxXpiAddon rec {
pname = "dizionario-italiano";
version = "5.1";
addonId = "it-IT@dictionaries.addons.mozilla.org";
url = "https://addons.mozilla.org/firefox/downloads/file/3693497/dizionario_italiano-${version}.xpi";
sha256 = "90b173ffdde34a77108152a5ff51879767b1dd84e0aa0dfb7b2bab94cd2e7f53";
meta = with lib; {
description = "Add support for Italian to spellchecking";
license = licenses.gpl3;
mozPermissions = [];
platforms = platforms.all;
};
}

View file

@ -0,0 +1,18 @@
{
lib,
buildFirefoxXpiAddon,
...
}:
buildFirefoxXpiAddon rec {
pname = "italiano-it-language-pack";
version = "145.0.20251106.194447";
addonId = "langpack-it@firefox.mozilla.org";
url = "https://addons.mozilla.org/firefox/downloads/file/4614309/italiano_it_language_pack-${version}.xpi";
sha256 = "1eb271cedbf326543e222ba1b9a1da62fceef9d3c523ac02a098df296f155038";
meta = with lib; {
description = "Firefox Language Pack for Italiano (it) Italian";
license = licenses.mpl20;
mozPermissions = [];
platforms = platforms.all;
};
}

View file

@ -9,7 +9,7 @@
src = fetchurl {
url = "http://tools.mapillary.com/uploader/download/linux/${version}";
name = "mapillary-uploader.AppImage";
sha256 = "sha256-OY3SiMHUyjwPDrPWfa+mFg2BHZrz6GG/9/D5sCP2Da8=";
sha256 = "sha256-hpWdfeuhYylO+SFD3BsKI0s/xtObCDd5OcuJ6i/aEuI=";
};
appimageContents = appimageTools.extractType2 {

View file

@ -1,46 +0,0 @@
# not working yet
{
pkgs,
rustPlatform,
fetchFromGitHub,
...
}: let
version = "1.5.3";
repo = fetchFromGitHub {
owner = "IntQuant";
repo = "noita_entangled_worlds";
rev = "v${version}";
hash = "sha256-frrpD0aWTeDbZYtp15R+quUUAZf7OvHlbSLtGJJtAqk=";
};
in
rustPlatform.buildRustPackage {
name = "noita-proxy-${version}";
src = repo + "/noita-proxy";
prePatch = ''
substituteInPlace Cargo.toml \
--replace "path = \"../shared\"" "path = \"${repo + "/shared"}\""
'';
nativeBuildInputs = with pkgs; [
pkg-config
python3
cmake
];
buildInputs = with pkgs; [
openssl
openssl.dev
libpulseaudio
libjack2
alsa-lib
xorg.libxcb
xorg.libxcb.dev
libopus
];
propagatedBuildInputs = with pkgs; [
steamworks-sdk-redist
];
runtimeDependencies = with pkgs; [
steamworks-sdk-redist
];
doCheck = false;
cargoHash = "sha256-TzUS6d6PopgGf2i1yVaXaXdzNrvfSz+Gv67BAtxYmb4=";
}

View file

@ -1,149 +0,0 @@
{
lib,
flutter332,
fetchFromGitLab,
pkg-config,
wrapGAppsHook,
gtk3,
glib,
glib-networking,
webkitgtk_4_1,
libsecret,
libnotify,
dbus,
sqlcipher,
openssl,
mpv,
alsa-lib,
libass,
ffmpeg-full,
libplacebo,
libunwind,
shaderc,
vulkan-headers,
vulkan-loader,
lcms2,
libdovi,
libdvdnav,
libdvdread,
mujs,
libbluray,
lua,
rubberband,
libuchardet,
zimg,
openal,
pipewire,
libpulseaudio,
libcaca,
libdrm,
libdisplay-info,
libgbm,
xorg,
nv-codec-headers-11,
libva,
libvdpau,
}:
flutter332.buildFlutterApplication rec {
pname = "polycule";
version = "0.3.4";
src = fetchFromGitLab {
owner = "polycule_client";
repo = "polycule";
rev = "v${version}";
hash = "sha256-RUu8DKuX2NUU5Ce5WLHtDaORkn7CSrgTj3KhM/z+yHc=";
};
pubspecLock = lib.importJSON ./polycule-pubspec.lock.json;
gitHashes = {
matrix = "sha256-w/QB5nYJ9Lh77TcYKEN/DnNQjWfp+9NX0dwQ9GOzWE8=";
media_kit = "sha256-1sVX+aHFLFJBtrNZrR6tWkb80vFELW2N9EejyQKlBPg=";
media_kit_libs_android_video = "sha256-N6QoktM8u9NYF8MAXLsxM9RlV8nICM4NbnmABHTRkZg=";
};
nativeBuildInputs = [
pkg-config
wrapGAppsHook
];
buildInputs = [
gtk3
glib
glib-networking
webkitgtk_4_1
libsecret
libnotify
dbus
sqlcipher
openssl
mpv
alsa-lib
libass
ffmpeg-full
libplacebo
libunwind
shaderc
vulkan-headers
vulkan-loader
lcms2
libdovi
libdvdnav
libdvdread
mujs
libbluray
lua
rubberband
libuchardet
zimg
openal
pipewire
libpulseaudio
libcaca
libdrm
libdisplay-info
libgbm
xorg.libXScrnSaver
xorg.libXpresent
nv-codec-headers-11
libva
libvdpau
];
flutterBuildFlags = [
"--release"
"--target"
"lib/main.dart"
"--dart-define=POLYCULE_VERSION=v${version}"
"--dart-define=POLYCULE_IS_STABLE=true"
"--no-tree-shake-icons"
];
postInstall = ''
# Install desktop files and icons from the source
install -Dm644 linux/business.braid.polycule.desktop $out/share/applications/polycule.desktop
install -Dm644 assets/logo/logo-circle.png $out/share/pixmaps/polycule.png
# Update desktop file to use correct executable name
substituteInPlace $out/share/applications/polycule.desktop \
--replace 'Exec=business.braid.polycule' 'Exec=polycule'
# Create a symlink with the expected name
ln -sf $out/bin/polycule $out/bin/business.braid.polycule
'';
meta = with lib; {
description = "A geeky and efficient [matrix] client for power users";
longDescription = ''
Polycule is a modern Matrix client built with Flutter, designed for power users
who want a fast, efficient, and feature-rich Matrix experience.
'';
homepage = "https://polycule.im/";
license = licenses.eupl12;
maintainers = [];
platforms = ["x86_64-linux" "aarch64-linux"];
sourceProvenance = with sourceTypes; [fromSource];
mainProgram = "polycule";
};
}

File diff suppressed because it is too large Load diff

View file

@ -8,21 +8,25 @@
in {
options.impermanence = {
enable = lib.mkEnableOption "impermanence for home directory";
fallbackPersistence.enable = lib.mkOption {
type = lib.types.bool;
default = true;
};
};
config = lib.mkMerge [
(lib.mkIf config.impermanence.enable {
assertions = [
{
assertion = osConfig.impermanence.enable;
message = "impermanence can not be enabled for a user when it is not enabled for a configuration";
assertion = osConfig.host.impermanence.enable;
message = "impermanence can not be enabled for a user when it is not enabled for the system";
}
];
})
(lib.mkIf osConfig.host.impermanence.enable {
# If impermanence is not enabled for this user but system impermanence is enabled,
# persist the entire home directory as fallback
home.persistence."/persist/home/${config.home.username}" = lib.mkIf (!cfg.enable) {
# If impermanence is not enabled for this user but system impermanence is enabled,
# persist the entire home directory as fallback
(lib.mkIf (osConfig.host.impermanence.enable && !cfg.enable && cfg.fallbackPersistence.enable) {
home.persistence."/persist/home/${config.home.username}" = {
directories = ["."];
allowOther = true;
};

View file

@ -11,7 +11,7 @@
config = lib.mkIf config.programs.bitwarden.enable (lib.mkMerge [
{
home.packages = with pkgs; [
bitwarden
bitwarden-desktop
];
}
(

View file

@ -31,6 +31,7 @@
./freecad.nix
./onionshare.nix
./mfoc.nix
./noita-entangled-worlds.nix
./pdfarranger.nix
./picard.nix
./qflipper.nix
@ -43,7 +44,6 @@
./davinci-resolve.nix
./gdx-liftoff.nix
./tor-browser.nix
./polycule.nix
./vmware-workstation.nix
];
}

View file

@ -1,19 +1,9 @@
{
lib,
pkgs,
config,
...
}: {
options.programs.discord = {
enable = lib.mkEnableOption "enable discord";
};
config = lib.mkIf config.programs.discord.enable (lib.mkMerge [
{
home.packages = with pkgs; [
discord
];
}
(
lib.mkIf config.impermanence.enable {
home.persistence."/persist${config.home.homeDirectory}" = {

View file

@ -0,0 +1,18 @@
{
lib,
pkgs,
config,
...
}: {
options = {
programs.noita-entangled-worlds = {
enable = lib.mkEnableOption "Noita Entangled Worlds multiplayer mod";
};
};
config = lib.mkIf config.programs.noita-entangled-worlds.enable {
home.packages = with pkgs; [
noita_entangled_worlds
];
};
}

View file

@ -1,31 +0,0 @@
{
lib,
pkgs,
config,
...
}: {
options.programs.polycule = {
enable = lib.mkEnableOption "enable polycule matrix client";
package = lib.mkPackageOption pkgs "polycule" {};
};
config = lib.mkIf config.programs.polycule.enable (lib.mkMerge [
{
home.packages = [
config.programs.polycule.package
];
}
(
lib.mkIf config.impermanence.enable {
home.persistence."/persist${config.home.homeDirectory}" = {
# TODO: check that these are actually the correct folders
# directories = [
# "${config.xdg.configHome}/polycule"
# "${config.xdg.dataHome}/polycule"
# "${config.xdg.cacheHome}/polycule"
# ];
};
}
)
]);
}

View file

@ -19,6 +19,10 @@ in {
gitmoji = lib.mkEnableOption "should emoji be prompted for as a part of the commit message./";
promptScopes = lib.mkEnableOption "prompting for scopes in conventional commits";
promptFooter = lib.mkEnableOption "prompting for footer in conventional commits";
showNewVersionNotes = lib.mkEnableOption "showing new version notes for conventional commits";
};
};
config = lib.mkIf config.extraExtensions.conventionalCommits.enable {
@ -27,6 +31,8 @@ in {
userSettings = {
"conventionalCommits.gitmoji" = config.extraExtensions.conventionalCommits.gitmoji;
"conventionalCommits.promptScopes" = config.extraExtensions.conventionalCommits.promptScopes;
"conventionalCommits.promptFooter" = config.extraExtensions.conventionalCommits.promptFooter;
"conventionalCommits.showNewVersionNotes" = config.extraExtensions.conventionalCommits.showNewVersionNotes;
};
};
}));

View file

@ -16,6 +16,7 @@
./go.nix
./evenBetterToml.nix
./openRemoteSsh.nix
./rustAnalyzer.nix
./astroVscode.nix
./vscodeMdx.nix
./claudeDev.nix

View file

@ -0,0 +1,27 @@
{
lib,
pkgs,
config,
...
}: let
pkgsRepositories = pkgs.nix-vscode-extensions.forVSCodeVersion config.programs.vscode.package.version;
pkgsRepository = pkgsRepositories.open-vsx;
in {
options.programs.vscode.profiles = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule ({config, ...}: {
options = {
extraExtensions.rustAnalyzer = {
enable = lib.mkEnableOption "should the rust-analyzer extension for vscode be enabled";
extension = lib.mkPackageOption pkgsRepository "rust-analyzer" {
default = ["rust-lang" "rust-analyzer"];
};
};
};
config = lib.mkIf config.extraExtensions.rustAnalyzer.enable {
extensions = [
config.extraExtensions.rustAnalyzer.extension
];
};
}));
};
}

View file

@ -3,6 +3,39 @@
lib,
...
}: {
# options.storage = {
# zfs = {
# # TODO: enable option
# # when this option is enabled we need to configure and enable disko things
# # TODO: we need some way of managing notifications
# # TODO: we need options to configure zfs pools
# # we should have warnings when the configured pool is missing drives
# # TODO: dataset option that is a submodule that adds datasets to the system
# # warnings for when a dataset was created in the past on a system but it is now missing some of the options defined for it
# # TODO: pools and datasets need to be passed to disko
# };
# impermanence = {
# # TODO: enable option
# # TODO: datasets option that is a submodule that will be used to define what datasets to add to the storage system
# # We should by default create the `local`, `local/system/nix`, `local/system/root`, `persist` `persist/system/root`, and `persist/system/var/log` datasets
# # Then we should make a dataset for user folders local and persist
# # We should also create datasets for systemd modules that have have impermanence enabled for them
# # we need to figure out what options a dataset can have in zfs
# };
# # TODO: we should have an impermanence module for home manager that proxies its values namespaced to the user down here that matches the same interface
# # TODO: we should have a way of enabling impermanence for a systemd config
# # these should have an option to put their folder into their own dataset (this needs to support private vs non private)
# # options for features that can be added to the dataset
# };
options.host.impermanence.enable = lib.mkEnableOption "are we going to use impermanence on this device";
config = lib.mkMerge [
@ -93,6 +126,7 @@
];
};
# TODO: this should live in leylas home manager configuration
security.sudo.extraConfig = "Defaults lecture=never";
}
)

View file

@ -0,0 +1,24 @@
{
lib,
config,
...
}: let
const = import ./const.nix;
dataDirectory = const.dataDirectory;
in {
options.services.actual = {
port = lib.mkOption {
type = lib.types.port;
description = "The port to listen on";
default = 5006;
};
};
config = lib.mkIf config.services.actual.enable {
services.actual = {
settings = {
port = config.services.actual.port;
dataDir = dataDirectory;
};
};
};
}

View file

@ -1,3 +1,3 @@
{
dataDirectory = "/var/lib/actual/";
dataDirectory = "/var/lib/private/actual";
}

View file

@ -1,26 +1,8 @@
{
lib,
config,
...
}: let
const = import ./const.nix;
dataDirectory = const.dataDirectory;
in {
imports = [
./actual.nix
./proxy.nix
./fail2ban.nix
./impermanence.nix
];
config = lib.mkIf config.services.actual.enable {
systemd.tmpfiles.rules = [
"d ${dataDirectory} 2770 actual actual"
];
services.actual = {
settings = {
ACTUAL_DATA_DIR = dataDirectory;
};
};
};
}

View file

@ -6,11 +6,22 @@
const = import ./const.nix;
dataDirectory = const.dataDirectory;
in {
config = lib.mkIf (config.services.actual.enable && config.host.impermanence.enable) {
options.services.actual = {
impermanence.enable = lib.mkOption {
type = lib.types.bool;
default = config.services.actual.enable && config.host.impermanence.enable;
};
};
config = lib.mkIf config.services.actual.impermanence.enable {
assertions = [
{
assertion = config.services.actual.settings.ACTUAL_DATA_DIR == dataDirectory;
message = "actual data location does not match persistence";
assertion = config.services.actual.settings.dataDir == dataDirectory;
message = "actual data location does not match persistence\nconfig directory: ${config.services.actual.settings.dataDir}\npersistence directory: ${dataDirectory}";
}
{
assertion = config.systemd.services.actual.serviceConfig.DynamicUser or false;
message = "actual systemd service must have DynamicUser enabled to use private directory";
}
];
environment.persistence."/persist/system/root" = {

View file

@ -4,17 +4,30 @@
...
}: {
options.services.actual = {
subdomain = lib.mkOption {
domain = lib.mkOption {
type = lib.types.str;
default = "actual";
description = "subdomain of base domain that actual will be hosted at";
description = "domain that actual will be hosted at";
default = "actual.arpa";
};
extraDomains = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "extra domains that should be configured for actual";
default = [];
};
reverseProxy.enable = lib.mkOption {
type = lib.types.bool;
default = config.services.actual.enable && config.services.reverseProxy.enable;
};
};
config = lib.mkIf (config.services.actual.enable && config.host.reverse_proxy.enable) {
host = {
reverse_proxy.subdomains.${config.services.actual.subdomain} = {
target = "http://localhost:${toString config.services.actual.settings.port}";
config = lib.mkIf config.services.actual.reverseProxy.enable {
services.reverseProxy.services.actual = {
target = "http://localhost:${toString config.services.actual.settings.port}";
domain = config.services.actual.domain;
extraDomains = config.services.actual.extraDomains;
settings = {
forwardHeaders.enable = true;
};
};
};

View file

@ -1,6 +1,5 @@
{...}: {
imports = [
./proxy.nix
./impermanence.nix
];
}

View file

@ -5,7 +5,14 @@
}: let
bazarr_data_directory = "/var/lib/bazarr";
in {
config = lib.mkIf (config.services.bazarr.enable && config.host.impermanence.enable) {
options.services.bazarr = {
impermanence.enable = lib.mkOption {
type = lib.types.bool;
default = config.services.bazarr.enable && config.host.impermanence.enable;
};
};
config = lib.mkIf config.services.bazarr.impermanence.enable {
assertions = [
{
assertion = config.services.bazarr.dataDir == bazarr_data_directory;

View file

@ -1,28 +0,0 @@
{
lib,
config,
...
}: {
options.services.bazarr = {
subdomain = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Subdomain for reverse proxy. If null, service will be local only.";
};
extraSubdomains = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [];
description = "Extra subdomains for reverse proxy.";
};
};
config = lib.mkIf (config.services.bazarr.enable && config.services.bazarr.subdomain != null) {
host.reverse_proxy.subdomains.bazarr = {
subdomain = config.services.bazarr.subdomain;
extraSubdomains = config.services.bazarr.extraSubdomains;
target = "http://127.0.0.1:6767";
websockets.enable = true;
forwardHeaders.enable = true;
};
};
}

View file

@ -27,9 +27,19 @@ in {
show_doc = lib.mkEnableOption "OpenAPI documentation (loads content from third party websites)";
downstreams = {
loopback = {
enable = lib.mkEnableOption "loopback downstream DNS server on localhost:53";
openFirewall = lib.mkEnableOption "automatic port forwarding for the loopback downstream";
host = {
enable = lib.mkEnableOption "host downstream DNS server accessible from network on all interfaces";
port = lib.mkOption {
type = lib.types.port;
default = 53;
description = "Port for the host downstream DNS server to listen on.";
};
openFirewall = lib.mkEnableOption "automatic port forwarding for the host downstream";
disableSystemdResolved = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to automatically disable systemd-resolved when using port 53. Set to false if you want to handle the conflict manually.";
};
};
};
@ -79,9 +89,44 @@ in {
default = [];
description = "List of additional upstream DNS server configurations.";
};
blocklists = {
ad_malware = {
enable = lib.mkEnableOption "Host file for blocking ads and malware";
url = lib.mkOption {
type = lib.types.str;
default = "http://sbc.io/hosts/hosts";
description = "URL of the ad and malware blocklist host file";
};
};
};
extraBlocklists = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [];
description = "Additional blocklist URLs to be added to the configuration";
};
};
config = lib.mkIf cfg.enable {
# Assertions for proper configuration
assertions = [
{
assertion = !(cfg.downstreams.host.enable && cfg.downstreams.host.port == 53 && config.services.resolved.enable && cfg.downstreams.host.disableSystemdResolved);
message = "crab-hole host downstream cannot use port 53 while systemd-resolved is enabled. Either disable systemd-resolved or use a different port.";
}
{
assertion = !(cfg.downstreams.host.enable && cfg.downstreams.host.port == 53 && !cfg.downstreams.host.disableSystemdResolved && config.services.resolved.enable);
message = "crab-hole host downstream is configured to use port 53 but systemd-resolved is still enabled and disableSystemdResolved is false. Set disableSystemdResolved = true or manually disable systemd-resolved.";
}
];
# Automatically disable systemd-resolved if using port 53
services.resolved.enable = lib.mkIf (cfg.downstreams.host.enable && cfg.downstreams.host.port == 53 && cfg.downstreams.host.disableSystemdResolved) (lib.mkForce false);
# Configure DNS nameservers when disabling systemd-resolved
networking.nameservers = lib.mkIf (cfg.downstreams.host.enable && cfg.downstreams.host.port == 53 && cfg.downstreams.host.disableSystemdResolved) (lib.mkDefault ["127.0.0.1" "1.1.1.1" "8.8.8.8"]);
services.crab-hole.settings = lib.mkMerge [
{
api = {
@ -91,13 +136,17 @@ in {
};
downstream = cfg.extraDownstreams;
upstream.name_servers = cfg.extraUpstreams;
blocklist.lists = cfg.extraBlocklists;
}
(lib.mkIf cfg.downstreams.loopback.enable {
(lib.mkIf cfg.blocklists.ad_malware.enable {
blocklist.lists = [cfg.blocklists.ad_malware.url];
})
(lib.mkIf cfg.downstreams.host.enable {
downstream = [
{
protocol = "udp";
listen = "localhost";
port = 53;
listen = "0.0.0.0";
port = cfg.downstreams.host.port;
}
];
})
@ -136,8 +185,8 @@ in {
(lib.mkIf cfg.openFirewall {
allowedTCPPorts = [cfg.port];
})
(lib.mkIf (cfg.downstreams.loopback.enable && cfg.downstreams.loopback.openFirewall) {
allowedUDPPorts = [53];
(lib.mkIf (cfg.downstreams.host.enable && cfg.downstreams.host.openFirewall) {
allowedUDPPorts = [cfg.downstreams.host.port];
})
];
};

View file

@ -5,7 +5,14 @@
}: let
workingDirectory = "/var/lib/private/crab-hole";
in {
config = lib.mkIf (config.services.immich.enable && config.host.impermanence.enable) {
options.services.crab-hole = {
impermanence.enable = lib.mkOption {
type = lib.types.bool;
default = config.services.crab-hole.enable && config.host.impermanence.enable;
};
};
config = lib.mkIf config.services.crab-hole.impermanence.enable {
assertions = [
{
assertion =

View file

@ -1,10 +1,9 @@
{...}: {
imports = [
./reverse_proxy.nix
./fail2ban.nix
./postgres.nix
./reverseProxy
./fail2ban
./postgres
./network_storage
./podman.nix
./actual
./bazarr
@ -18,7 +17,7 @@
./lidarr
./panoramax
./paperless
./qbittorent.nix
./qbittorent
./radarr
./searx
./sonarr

View file

@ -1,74 +0,0 @@
{
lib,
pkgs,
config,
...
}: let
dataFolder = "/var/lib/fail2ban";
dataFile = "fail2ban.sqlite3";
in {
config = lib.mkIf config.services.fail2ban.enable (lib.mkMerge [
{
environment.etc = {
"fail2ban/filter.d/nginx.local".text = lib.mkIf config.services.nginx.enable (
pkgs.lib.mkDefault (pkgs.lib.mkAfter ''
[Definition]
failregex = "limiting requests, excess:.* by zone.*client: <HOST>"
'')
);
};
services.fail2ban = {
maxretry = 5;
ignoreIP = [
# Whitelist local networks
"10.0.0.0/8"
"172.16.0.0/12"
"192.168.0.0/16"
# tail scale tailnet
"100.64.0.0/10"
"fd7a:115c:a1e0::/48"
];
bantime = "24h"; # Ban IPs for one day on the first ban
bantime-increment = {
enable = true; # Enable increment of bantime after each violation
formula = "ban.Time * math.exp(float(ban.Count+1)*banFactor)/math.exp(1*banFactor)";
maxtime = "168h"; # Do not ban for more than 1 week
overalljails = true; # Calculate the ban time based on all the violations
};
jails = {
nginx-iptables.settings = lib.mkIf config.services.nginx.enable {
enabled = true;
filter = "nginx";
action = ''iptables-multiport[name=HTTP, port="http,https"]'';
backend = "auto";
findtime = 600;
bantime = 600;
maxretry = 5;
};
# TODO; figure out if there is any fail2ban things we can do on searx
# searx-iptables.settings = lib.mkIf config.services.searx.enable {};
};
};
}
(lib.mkIf config.host.impermanence.enable {
assertions = [
{
assertion = config.services.fail2ban.daemonSettings.Definition.dbfile == "${dataFolder}/${dataFile}";
message = "fail2ban data file does not match persistence";
}
];
environment.persistence."/persist/system/root" = {
directories = [
{
directory = dataFolder;
user = "fail2ban";
group = "fail2ban";
}
];
};
})
]);
}

View file

@ -0,0 +1,6 @@
{...}: {
imports = [
./fail2ban.nix
./impermanence.nix
];
}

View file

@ -0,0 +1,51 @@
{
lib,
pkgs,
config,
...
}: {
config = lib.mkIf config.services.fail2ban.enable {
environment.etc = {
"fail2ban/filter.d/nginx.local".text = lib.mkIf config.services.nginx.enable (
pkgs.lib.mkDefault (pkgs.lib.mkAfter ''
[Definition]
failregex = "limiting requests, excess:.* by zone.*client: <HOST>"
'')
);
};
services.fail2ban = {
maxretry = 5;
ignoreIP = [
# Whitelist local networks
"10.0.0.0/8"
"172.16.0.0/12"
"192.168.0.0/16"
# tail scale tailnet
"100.64.0.0/10"
"fd7a:115c:a1e0::/48"
];
bantime = "24h"; # Ban IPs for one day on the first ban
bantime-increment = {
enable = true; # Enable increment of bantime after each violation
formula = "ban.Time * math.exp(float(ban.Count+1)*banFactor)/math.exp(1*banFactor)";
maxtime = "168h"; # Do not ban for more than 1 week
overalljails = true; # Calculate the ban time based on all the violations
};
jails = {
nginx-iptables.settings = lib.mkIf config.services.nginx.enable {
enabled = true;
filter = "nginx";
action = ''iptables-multiport[name=HTTP, port="http,https"]'';
backend = "auto";
findtime = 600;
bantime = 600;
maxretry = 5;
};
# TODO; figure out if there is any fail2ban things we can do on searx
# searx-iptables.settings = lib.mkIf config.services.searx.enable {};
};
};
};
}

View file

@ -0,0 +1,34 @@
{
lib,
config,
...
}: let
dataFolder = "/var/lib/fail2ban";
dataFile = "fail2ban.sqlite3";
in {
options.services.fail2ban = {
impermanence.enable = lib.mkOption {
type = lib.types.bool;
default = config.services.fail2ban.enable && config.host.impermanence.enable;
};
};
config = lib.mkIf config.services.fail2ban.impermanence.enable {
assertions = [
{
assertion = config.services.fail2ban.daemonSettings.Definition.dbfile == "${dataFolder}/${dataFile}";
message = "fail2ban data file does not match persistence";
}
];
environment.persistence."/persist/system/root" = {
directories = [
{
directory = dataFolder;
user = "fail2ban";
group = "fail2ban";
}
];
};
};
}

View file

@ -1,6 +1,5 @@
{...}: {
imports = [
./proxy.nix
./impermanence.nix
];
}

View file

@ -3,7 +3,14 @@
config,
...
}: {
config = lib.mkIf (config.services.flaresolverr.enable && config.host.impermanence.enable) {
options.services.flaresolverr = {
impermanence.enable = lib.mkOption {
type = lib.types.bool;
default = config.services.flaresolverr.enable && config.host.impermanence.enable;
};
};
config = lib.mkIf config.services.flaresolverr.impermanence.enable {
# FlareSolverr typically doesn't need persistent storage as it's a proxy service
# but we'll add basic structure in case it's needed for logs or configuration
environment.persistence."/persist/system/root" = {

View file

@ -1,28 +0,0 @@
{
lib,
config,
...
}: {
options.services.flaresolverr = {
subdomain = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Subdomain for reverse proxy. If null, service will be local only.";
};
extraSubdomains = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [];
description = "Extra subdomains for reverse proxy.";
};
};
config = lib.mkIf (config.services.flaresolverr.enable && config.services.flaresolverr.subdomain != null) {
host.reverse_proxy.subdomains.flaresolverr = {
subdomain = config.services.flaresolverr.subdomain;
extraSubdomains = config.services.flaresolverr.extraSubdomains;
target = "http://127.0.0.1:${toString config.services.flaresolverr.port}";
websockets.enable = true;
forwardHeaders.enable = true;
};
};
}

View file

@ -2,40 +2,31 @@
lib,
config,
...
}: {
config = lib.mkIf config.services.forgejo.enable (
lib.mkMerge [
}: let
usingPostgres = config.services.forgejo.database.type == "postgres";
in {
config = lib.mkIf config.services.forgejo.enable {
assertions = [
{
host = {
postgres = {
enable = true;
};
};
assertions = [
{
assertion = config.services.forgejo.settings.database.DB_TYPE == "postgres";
message = "Forgejo database type must be postgres";
}
];
assertion = !usingPostgres || config.services.postgresql.enable;
message = "PostgreSQL must be enabled when Forgejo database type is postgres";
}
(lib.mkIf config.host.postgres.enable {
host = {
postgres = {
extraUsers = {
forgejo = {
isClient = true;
createUser = true;
};
};
extraDatabases = {
forgejo = {
name = "forgejo";
};
};
};
};
})
]
);
{
assertion = !(usingPostgres && config.services.forgejo.database.createDatabase) || (builtins.any (db: db == "forgejo") config.services.postgresql.ensureDatabases);
message = "Forgejo built-in database creation failed - expected 'forgejo' in ensureDatabases but got: ${builtins.toString config.services.postgresql.ensureDatabases}";
}
{
assertion = !(usingPostgres && config.services.forgejo.database.createDatabase) || (builtins.any (user: user.name == "forgejo") config.services.postgresql.ensureUsers);
message = "Forgejo built-in user creation failed - expected user 'forgejo' in ensureUsers but got: ${builtins.toString (builtins.map (u: u.name) config.services.postgresql.ensureUsers)}";
}
];
services.forgejo.database.createDatabase = lib.mkDefault usingPostgres;
systemd.services.forgejo = lib.mkIf usingPostgres {
requires = [
config.systemd.services.postgresql.name
];
};
};
}

View file

@ -1,53 +1,9 @@
{
lib,
config,
...
}: let
const = import ./const.nix;
httpPort = const.httpPort;
sshPort = const.sshPort;
db_user = "forgejo";
in {
imports = [
./forgejo.nix
./proxy.nix
./database.nix
./fail2ban.nix
./impermanence.nix
];
config = lib.mkIf config.services.forgejo.enable {
assertions = [
{
assertion = config.services.forgejo.settings.server.BUILTIN_SSH_SERVER_USER == config.users.users.git.name;
message = "Forgejo BUILTIN_SSH_SERVER_USER hardcoded value does not match expected git user name";
}
];
services.forgejo = {
database = {
type = "postgres";
socket = "/run/postgresql";
};
lfs.enable = true;
settings = {
server = {
DOMAIN = "${config.services.forgejo.subdomain}.${config.host.reverse_proxy.hostname}";
HTTP_PORT = httpPort;
START_SSH_SERVER = true;
SSH_LISTEN_PORT = sshPort;
SSH_PORT = 22;
BUILTIN_SSH_SERVER_USER = "git";
ROOT_URL = "https://git.jan-leila.com";
};
service = {
DISABLE_REGISTRATION = true;
};
database = {
DB_TYPE = "postgres";
NAME = db_user;
USER = db_user;
};
};
};
};
}

View file

@ -4,7 +4,16 @@
pkgs,
...
}: {
config = lib.mkIf (config.services.forgejo.enable && config.services.fail2ban.enable) {
options.services.forgejo = {
fail2ban = {
enable = lib.mkOption {
type = lib.types.bool;
default = config.services.forgejo.enable && config.services.fail2ban.enable;
};
};
};
config = lib.mkIf config.services.forgejo.fail2ban.enable {
environment.etc = {
"fail2ban/filter.d/forgejo.local".text = lib.mkIf config.services.forgejo.enable (
pkgs.lib.mkDefault (pkgs.lib.mkAfter ''

View file

@ -0,0 +1,46 @@
{
lib,
config,
...
}: let
const = import ./const.nix;
httpPort = const.httpPort;
sshPort = const.sshPort;
db_user = "forgejo";
in {
config = lib.mkIf config.services.forgejo.enable {
assertions = [
{
assertion = config.services.forgejo.settings.server.BUILTIN_SSH_SERVER_USER == config.users.users.git.name;
message = "Forgejo BUILTIN_SSH_SERVER_USER hardcoded value does not match expected git user name";
}
];
services.forgejo = {
database = {
type = "postgres";
socket = "/run/postgresql";
};
lfs.enable = true;
settings = {
server = {
DOMAIN = config.services.forgejo.reverseProxy.domain;
HTTP_PORT = httpPort;
START_SSH_SERVER = true;
SSH_LISTEN_PORT = sshPort;
SSH_PORT = 22;
BUILTIN_SSH_SERVER_USER = "git";
ROOT_URL = "https://git.jan-leila.com";
};
service = {
DISABLE_REGISTRATION = true;
};
database = {
DB_TYPE = "postgres";
NAME = db_user;
USER = db_user;
};
};
};
};
}

View file

@ -5,7 +5,14 @@
}: let
stateDir = "/var/lib/forgejo";
in {
config = lib.mkIf (config.services.forgejo.enable && config.host.impermanence.enable) {
options.services.forgejo = {
impermanence.enable = lib.mkOption {
type = lib.types.bool;
default = config.services.forgejo.enable && config.host.impermanence.enable;
};
};
config = lib.mkIf config.services.forgejo.impermanence.enable {
assertions = [
{
assertion = config.services.forgejo.stateDir == stateDir;

View file

@ -7,16 +7,33 @@
httpPort = const.httpPort;
in {
options.services.forgejo = {
subdomain = lib.mkOption {
type = lib.types.str;
description = "subdomain of base domain that forgejo will be hosted at";
default = "forgejo";
reverseProxy = {
enable = lib.mkOption {
type = lib.types.bool;
default = config.services.forgejo.enable && config.services.reverseProxy.enable;
};
domain = lib.mkOption {
type = lib.types.str;
description = "domain that forgejo will be hosted at";
default = "git.jan-leila.com";
};
extraDomains = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "extra domains that should be configured for forgejo";
default = [];
};
};
};
config = lib.mkIf (config.services.forgejo.enable && config.host.reverse_proxy.enable) {
host.reverse_proxy.subdomains.${config.services.forgejo.subdomain} = {
config = lib.mkIf config.services.forgejo.reverseProxy.enable {
services.reverseProxy.services.forgejo = {
target = "http://localhost:${toString httpPort}";
domain = config.services.forgejo.reverseProxy.domain;
extraDomains = config.services.forgejo.reverseProxy.extraDomains;
settings = {
forwardHeaders.enable = true;
};
};
networking.firewall.allowedTCPPorts = [

View file

@ -2,55 +2,52 @@
lib,
config,
...
}: let
dbUser = "hass";
in {
config = lib.mkIf config.services.home-assistant.enable (
lib.mkMerge [
}: {
options.services.home-assistant = {
postgres = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Use PostgreSQL instead of SQLite";
};
user = lib.mkOption {
type = lib.types.str;
default = "hass";
description = "Database user name";
};
database = lib.mkOption {
type = lib.types.str;
default = "hass";
description = "Database name";
};
};
};
config = lib.mkIf config.services.home-assistant.enable {
assertions = [
{
host = {
postgres = {
enable = true;
};
};
assertions = [
{
assertion = config.services.home-assistant.database == "postgres";
message = "Home Assistant database type must be postgres";
}
];
assertion = !config.services.home-assistant.postgres.enable || config.services.postgresql.enable;
message = "PostgreSQL must be enabled when using postgres database for Home Assistant";
}
(lib.mkIf config.host.postgres.enable {
host = {
postgres = {
extraUsers = {
${dbUser} = {
isClient = true;
createUser = true;
};
};
extraDatabases = {
${dbUser} = {
name = dbUser;
};
};
};
};
];
services.home-assistant = {
extraPackages = python3Packages:
with python3Packages; [
psycopg2
];
};
services.postgresql.databases.home-assistant = lib.mkIf config.services.home-assistant.postgres.enable {
enable = true;
user = config.services.home-assistant.postgres.user;
database = config.services.home-assistant.postgres.database;
};
systemd.services.home-assistant = {
requires = [
config.systemd.services.postgresql.name
];
};
})
]
);
services.home-assistant = lib.mkIf config.services.home-assistant.postgres.enable {
extraPackages = python3Packages:
with python3Packages; [
psycopg2
];
};
systemd.services.home-assistant = lib.mkIf config.services.home-assistant.postgres.enable {
requires = [
config.systemd.services.postgresql.name
];
};
};
}

View file

@ -1,112 +1,10 @@
{
lib,
config,
...
}: {
imports = [
./home-assistant.nix
./proxy.nix
./database.nix
./fail2ban.nix
./impermanence.nix
./extensions
];
options.services.home-assistant = {
database = lib.mkOption {
type = lib.types.enum [
"builtin"
"postgres"
];
description = "what database do we want to use";
default = "builtin";
};
extensions = {
sonos = {
enable = lib.mkEnableOption "enable the sonos plugin";
port = lib.mkOption {
type = lib.types.int;
default = 1400;
description = "what port to use for sonos discovery";
};
};
jellyfin = {
enable = lib.mkEnableOption "enable the jellyfin plugin";
};
wyoming = {
enable = lib.mkEnableOption "enable wyoming";
};
};
};
config = lib.mkIf config.services.home-assistant.enable (lib.mkMerge [
{
services.home-assistant = {
configDir = "/var/lib/hass";
extraComponents = [
"default_config"
"esphome"
"met"
"radio_browser"
"isal"
"zha"
"webostv"
"tailscale"
"syncthing"
"analytics_insights"
"unifi"
"openweathermap"
"ollama"
"mobile_app"
"logbook"
"ssdp"
"usb"
"webhook"
"bluetooth"
"dhcp"
"energy"
"history"
"backup"
"assist_pipeline"
"conversation"
"sun"
"zeroconf"
"cpuspeed"
];
config = {
http = {
server_port = 8123;
use_x_forwarded_for = true;
trusted_proxies = ["127.0.0.1" "::1"];
ip_ban_enabled = true;
login_attempts_threshold = 10;
};
homeassistant = {
external_url = "https://${config.services.home-assistant.subdomain}.${config.host.reverse_proxy.hostname}";
# internal_url = "http://192.168.1.2:8123";
};
recorder.db_url = "postgresql://@/${config.services.home-assistant.configDir}";
"automation manual" = [];
"automation ui" = "!include automations.yaml";
mobile_app = {};
};
extraPackages = python3Packages:
with python3Packages; [
hassil
numpy
gtts
];
};
# TODO: configure /var/lib/hass/secrets.yaml via sops
networking.firewall.allowedUDPPorts = [
1900
];
systemd.tmpfiles.rules = [
"f ${config.services.home-assistant.configDir}/automations.yaml 0755 hass hass"
];
}
]);
}

View file

@ -3,36 +3,46 @@
pkgs,
config,
...
}:
lib.mkIf (config.services.fail2ban.enable && config.services.home-assistant.enable) {
environment.etc = {
"fail2ban/filter.d/hass.local".text = (
pkgs.lib.mkDefault (pkgs.lib.mkAfter ''
[INCLUDES]
before = common.conf
[Definition]
failregex = ^%(__prefix_line)s.*Login attempt or request with invalid authentication from <HOST>.*$
ignoreregex =
[Init]
datepattern = ^%%Y-%%m-%%d %%H:%%M:%%S
'')
);
}: {
options.services.home-assistant = {
fail2ban = {
enable = lib.mkOption {
type = lib.types.bool;
default = config.services.fail2ban.enable && config.services.home-assistant.enable;
};
};
};
services.fail2ban = {
jails = {
home-assistant-iptables.settings = {
enabled = true;
filter = "hass";
action = ''iptables-multiport[name=HTTP, port="http,https"]'';
logpath = "${config.services.home-assistant.configDir}/*.log";
backend = "auto";
findtime = 600;
bantime = 600;
maxretry = 5;
config = lib.mkIf config.services.home-assistant.fail2ban.enable {
environment.etc = {
"fail2ban/filter.d/hass.local".text = (
pkgs.lib.mkDefault (pkgs.lib.mkAfter ''
[INCLUDES]
before = common.conf
[Definition]
failregex = ^%(__prefix_line)s.*Login attempt or request with invalid authentication from <HOST>.*$
ignoreregex =
[Init]
datepattern = ^%%Y-%%m-%%d %%H:%%M:%%S
'')
);
};
services.fail2ban = {
jails = {
home-assistant-iptables.settings = {
enabled = true;
filter = "hass";
action = ''iptables-multiport[name=HTTP, port="http,https"]'';
logpath = "${config.services.home-assistant.configDir}/*.log";
backend = "auto";
findtime = 600;
bantime = 600;
maxretry = 5;
};
};
};
};

View file

@ -0,0 +1,104 @@
{
lib,
config,
...
}: {
options.services.home-assistant = {
database = lib.mkOption {
type = lib.types.enum [
"builtin"
"postgres"
];
description = "what database do we want to use";
default = "builtin";
};
extensions = {
sonos = {
enable = lib.mkEnableOption "enable the sonos plugin";
port = lib.mkOption {
type = lib.types.int;
default = 1400;
description = "what port to use for sonos discovery";
};
};
jellyfin = {
enable = lib.mkEnableOption "enable the jellyfin plugin";
};
wyoming = {
enable = lib.mkEnableOption "enable wyoming";
};
};
};
config = lib.mkIf config.services.home-assistant.enable (lib.mkMerge [
{
services.home-assistant = {
configDir = "/var/lib/hass";
extraComponents = [
"default_config"
"esphome"
"met"
"radio_browser"
"isal"
"zha"
"webostv"
"tailscale"
"syncthing"
"analytics_insights"
"unifi"
"openweathermap"
"ollama"
"mobile_app"
"logbook"
"ssdp"
"usb"
"webhook"
"bluetooth"
"dhcp"
"energy"
"history"
"backup"
"assist_pipeline"
"conversation"
"sun"
"zeroconf"
"cpuspeed"
];
config = {
http = {
server_port = 8123;
use_x_forwarded_for = true;
trusted_proxies = ["127.0.0.1" "::1"];
ip_ban_enabled = true;
login_attempts_threshold = 10;
};
homeassistant = {
external_url = "https://${config.services.home-assistant.domain}";
# internal_url = "http://192.168.1.2:8123";
};
recorder.db_url = "postgresql://@/${config.services.home-assistant.configDir}";
"automation manual" = [];
"automation ui" = "!include automations.yaml";
mobile_app = {};
};
extraPackages = python3Packages:
with python3Packages; [
hassil
numpy
gtts
];
};
# TODO: configure /var/lib/hass/secrets.yaml via sops
networking.firewall.allowedUDPPorts = [
1900
];
systemd.tmpfiles.rules = [
"f ${config.services.home-assistant.configDir}/automations.yaml 0755 hass hass"
];
}
]);
}

View file

@ -4,29 +4,39 @@
...
}: {
options.services.home-assistant = {
subdomain = lib.mkOption {
domain = lib.mkOption {
type = lib.types.str;
description = "subdomain of base domain that home-assistant will be hosted at";
default = "home-assistant";
description = "domain that home-assistant will be hosted at";
default = "home-assistant.arpa";
};
extraDomains = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "extra domains that should be configured for home-assistant";
default = [];
};
reverseProxy = {
enable = lib.mkOption {
type = lib.types.bool;
default = config.services.reverseProxy.enable && config.services.home-assistant.enable;
};
};
};
config = lib.mkIf (config.host.reverse_proxy.enable && config.services.home-assistant.enable) {
host = {
reverse_proxy.subdomains.${config.services.home-assistant.subdomain} = {
target = "http://localhost:${toString config.services.home-assistant.config.http.server_port}";
config = lib.mkIf config.services.home-assistant.reverseProxy.enable {
services.reverseProxy.services.home-assistant = {
target = "http://localhost:${toString config.services.home-assistant.config.http.server_port}";
domain = config.services.home-assistant.domain;
extraDomains = config.services.home-assistant.extraDomains;
websockets.enable = true;
settings = {
proxyWebsockets.enable = true;
forwardHeaders.enable = true;
extraConfig = ''
add_header Upgrade $http_upgrade;
add_header Connection \"upgrade\";
proxy_buffering off;
proxy_read_timeout 90;
'';
# Custom timeout settings
proxyHeaders = {
enable = true;
timeout = 90;
};
};
};
};

View file

@ -3,24 +3,28 @@
config,
...
}: {
config = lib.mkIf config.services.immich.enable (lib.mkMerge [
{
host = {
postgres = {
enable = true;
};
};
}
(lib.mkIf config.host.postgres.enable {
host = {
postgres = {
extraUsers = {
${config.services.immich.database.user} = {
isClient = true;
};
};
};
};
})
]);
config = lib.mkIf config.services.immich.enable {
assertions = [
{
assertion = !config.services.immich.database.enable || config.services.postgresql.enable;
message = "PostgreSQL must be enabled when using postgres database for Immich";
}
{
assertion = !(config.services.immich.database.enable && config.services.immich.database.createDB) || (builtins.any (db: db == "immich") config.services.postgresql.ensureDatabases);
message = "Immich built-in database creation failed - expected 'immich' in ensureDatabases but got: ${builtins.toString config.services.postgresql.ensureDatabases}";
}
{
assertion = !(config.services.immich.database.enable && config.services.immich.database.createDB) || (builtins.any (user: user.name == "immich") config.services.postgresql.ensureUsers);
message = "Immich built-in user creation failed - expected user 'immich' in ensureUsers but got: ${builtins.toString (builtins.map (u: u.name) config.services.postgresql.ensureUsers)}";
}
];
# Note: Immich has built-in database creation via services.immich.database.createDB we only add the systemd dependency
systemd.services.immich-server = lib.mkIf config.services.immich.database.enable {
requires = [
config.systemd.services.postgresql.name
];
};
};
}

View file

@ -4,7 +4,16 @@
pkgs,
...
}: {
config = lib.mkIf (config.services.fail2ban.enable && config.services.immich.enable) {
options.services.immich = {
fail2ban = {
enable = lib.mkOption {
type = lib.types.bool;
default = config.services.fail2ban.enable && config.services.immich.enable;
};
};
};
config = lib.mkIf config.services.immich.fail2ban.enable {
environment.etc = {
"fail2ban/filter.d/immich.local".text = pkgs.lib.mkDefault (pkgs.lib.mkAfter ''
[Definition]

View file

@ -5,7 +5,14 @@
}: let
mediaLocation = "/var/lib/immich";
in {
config = lib.mkIf (config.services.immich.enable && config.host.impermanence.enable) {
options.services.immich = {
impermanence.enable = lib.mkOption {
type = lib.types.bool;
default = config.services.immich.enable && config.host.impermanence.enable;
};
};
config = lib.mkIf config.services.immich.impermanence.enable {
assertions = [
{
assertion = config.services.immich.mediaLocation == mediaLocation;

View file

@ -4,31 +4,40 @@
...
}: {
options.services.immich = {
subdomain = lib.mkOption {
domain = lib.mkOption {
type = lib.types.str;
description = "subdomain of base domain that immich will be hosted at";
default = "immich";
description = "domain that immich will be hosted at";
default = "immich.arpa";
};
extraDomains = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "extra domains that should be configured for immich";
default = [];
};
reverseProxy = {
enable = lib.mkOption {
type = lib.types.bool;
default = config.services.immich.enable && config.services.reverseProxy.enable;
};
};
};
config = lib.mkIf (config.services.immich.enable && config.host.reverse_proxy.enable) {
host = {
reverse_proxy.subdomains.${config.services.immich.subdomain} = {
target = "http://localhost:${toString config.services.immich.port}";
config = lib.mkIf config.services.immich.reverseProxy.enable {
services.reverseProxy.services.immich = {
target = "http://localhost:${toString config.services.immich.port}";
domain = config.services.immich.domain;
extraDomains = config.services.immich.extraDomains;
websockets.enable = true;
settings = {
proxyWebsockets.enable = true;
forwardHeaders.enable = true;
maxBodySize = 50000;
extraConfig = ''
# allow large file uploads
client_max_body_size 50000M;
# set timeout
proxy_read_timeout 600s;
proxy_send_timeout 600s;
send_timeout 600s;
proxy_redirect off;
'';
# Custom timeout settings
proxyHeaders = {
enable = true;
timeout = 600;
};
};
};
};

View file

@ -1,6 +1,5 @@
{...}: {
imports = [
./proxy.nix
./impermanence.nix
];
}

View file

@ -5,7 +5,14 @@
}: let
jackett_data_directory = "/var/lib/jackett/.config/Jackett";
in {
config = lib.mkIf (config.services.jackett.enable && config.host.impermanence.enable) {
options.services.jackett = {
impermanence.enable = lib.mkOption {
type = lib.types.bool;
default = config.services.jackett.enable && config.host.impermanence.enable;
};
};
config = lib.mkIf config.services.jackett.impermanence.enable {
assertions = [
{
assertion = config.services.jackett.dataDir == jackett_data_directory;

View file

@ -1,28 +0,0 @@
{
lib,
config,
...
}: {
options.services.jackett = {
subdomain = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Subdomain for reverse proxy. If null, service will be local only.";
};
extraSubdomains = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [];
description = "Extra subdomains for reverse proxy.";
};
};
config = lib.mkIf (config.services.jackett.enable && config.services.jackett.subdomain != null) {
host.reverse_proxy.subdomains.jackett = {
subdomain = config.services.jackett.subdomain;
extraSubdomains = config.services.jackett.extraSubdomains;
target = "http://127.0.0.1:9117";
websockets.enable = true;
forwardHeaders.enable = true;
};
};
}

View file

@ -1,38 +1,8 @@
{
lib,
pkgs,
config,
...
}: let
jellyfinPort = 8096;
dlanPort = 1900;
in {
imports = [
./jellyfin.nix
./proxy.nix
./fail2ban.nix
./impermanence.nix
];
options.services.jellyfin = {
media_directory = lib.mkOption {
type = lib.types.str;
description = "directory jellyfin media will be hosted at";
default = "/srv/jellyfin/media";
};
};
config = lib.mkIf config.services.jellyfin.enable {
environment.systemPackages = [
pkgs.jellyfin
pkgs.jellyfin-web
pkgs.jellyfin-ffmpeg
];
networking.firewall.allowedTCPPorts = [jellyfinPort dlanPort];
systemd.tmpfiles.rules = [
"d ${config.services.jellyfin.media_directory} 2770 jellyfin jellyfin_media"
"A ${config.services.jellyfin.media_directory} - - - - u:jellyfin:rwX,g:jellyfin_media:rwX,o::-"
];
};
}

View file

@ -6,7 +6,14 @@
jellyfin_data_directory = "/var/lib/jellyfin";
jellyfin_cache_directory = "/var/cache/jellyfin";
in {
config = lib.mkIf (config.services.jellyfin.enable && config.host.impermanence.enable) {
options.services.jellyfin = {
impermanence.enable = lib.mkOption {
type = lib.types.bool;
default = config.services.jellyfin.enable && config.host.impermanence.enable;
};
};
config = lib.mkIf config.services.jellyfin.impermanence.enable {
fileSystems."/persist/system/jellyfin".neededForBoot = true;
host.storage.pool.extraDatasets = {

View file

@ -0,0 +1,32 @@
{
lib,
pkgs,
config,
...
}: let
jellyfinPort = 8096;
dlanPort = 1900;
in {
options.services.jellyfin = {
media_directory = lib.mkOption {
type = lib.types.str;
description = "directory jellyfin media will be hosted at";
default = "/srv/jellyfin/media";
};
};
config = lib.mkIf config.services.jellyfin.enable {
environment.systemPackages = [
pkgs.jellyfin
pkgs.jellyfin-web
pkgs.jellyfin-ffmpeg
];
networking.firewall.allowedTCPPorts = [jellyfinPort dlanPort];
systemd.tmpfiles.rules = [
"d ${config.services.jellyfin.media_directory} 2770 jellyfin jellyfin_media"
"A ${config.services.jellyfin.media_directory} - - - - u:jellyfin:rwX,g:jellyfin_media:rwX,o::-"
];
};
}

View file

@ -6,33 +6,36 @@
jellyfinPort = 8096;
in {
options.services.jellyfin = {
subdomain = lib.mkOption {
domain = lib.mkOption {
type = lib.types.str;
description = "subdomain of base domain that jellyfin will be hosted at";
default = "jellyfin";
description = "domain that jellyfin will be hosted at";
default = "jellyfin.arpa";
};
extraSubdomains = lib.mkOption {
extraDomains = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "ex subdomain of base domain that jellyfin will be hosted at";
description = "extra domains that should be configured for jellyfin";
default = [];
};
reverseProxy = {
enable = lib.mkOption {
type = lib.types.bool;
default = config.services.jellyfin.enable && config.services.reverseProxy.enable;
};
};
};
config = lib.mkIf (config.services.jellyfin.enable && config.host.reverse_proxy.enable) {
host.reverse_proxy.subdomains.jellyfin = {
config = lib.mkIf config.services.jellyfin.reverseProxy.enable {
services.reverseProxy.services.jellyfin = {
target = "http://localhost:${toString jellyfinPort}";
domain = config.services.jellyfin.domain;
extraDomains = config.services.jellyfin.extraDomains;
subdomain = config.services.jellyfin.subdomain;
extraSubdomains = config.services.jellyfin.extraSubdomains;
forwardHeaders.enable = true;
extraConfig = ''
client_max_body_size 20M;
add_header X-Content-Type-Options "nosniff";
proxy_buffering off;
'';
settings = {
forwardHeaders.enable = true;
maxBodySize = 20;
noSniff.enable = true;
proxyBuffering.enable = false;
};
};
};
}

View file

@ -1,6 +1,5 @@
{...}: {
imports = [
./proxy.nix
./impermanence.nix
];
}

View file

@ -5,7 +5,14 @@
}: let
lidarr_data_directory = "/var/lib/lidarr/.config/Lidarr";
in {
config = lib.mkIf (config.services.lidarr.enable && config.host.impermanence.enable) {
options.services.lidarr = {
impermanence.enable = lib.mkOption {
type = lib.types.bool;
default = config.services.lidarr.enable && config.host.impermanence.enable;
};
};
config = lib.mkIf config.services.lidarr.impermanence.enable {
assertions = [
{
assertion = config.services.lidarr.dataDir == lidarr_data_directory;

View file

@ -1,28 +0,0 @@
{
lib,
config,
...
}: {
options.services.lidarr = {
subdomain = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Subdomain for reverse proxy. If null, service will be local only.";
};
extraSubdomains = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [];
description = "Extra subdomains for reverse proxy.";
};
};
config = lib.mkIf (config.services.lidarr.enable && config.services.lidarr.subdomain != null) {
host.reverse_proxy.subdomains.lidarr = {
subdomain = config.services.lidarr.subdomain;
extraSubdomains = config.services.lidarr.extraSubdomains;
target = "http://127.0.0.1:8686";
websockets.enable = true;
forwardHeaders.enable = true;
};
};
}

View file

@ -1,90 +1,6 @@
{
config,
lib,
...
}: let
export_directory = config.host.network_storage.export_directory;
in {
imports = [
./network_storage.nix
./nfs.nix
];
options = {
host.network_storage = {
enable = lib.mkEnableOption "is this machine going to export network storage";
export_directory = lib.mkOption {
type = lib.types.path;
description = "what are exports going to be stored in";
default = "/exports";
};
directories = lib.mkOption {
type = lib.types.listOf (lib.types.submodule ({config, ...}: {
options = {
folder = lib.mkOption {
type = lib.types.str;
description = "what is the name of this export directory";
};
bind = lib.mkOption {
type = lib.types.nullOr lib.types.path;
description = "is this directory bound to anywhere";
default = null;
};
user = lib.mkOption {
type = lib.types.str;
description = "what user owns this directory";
default = "nouser";
};
group = lib.mkOption {
type = lib.types.str;
description = "what group owns this directory";
default = "nogroup";
};
_directory = lib.mkOption {
internal = true;
readOnly = true;
type = lib.types.path;
default = "${export_directory}/${config.folder}";
};
};
}));
description = "list of directory names to export";
};
};
};
config = lib.mkIf config.host.network_storage.enable (lib.mkMerge [
{
# create any folders that we need to have for our exports
systemd.tmpfiles.rules =
[
"d ${config.host.network_storage.export_directory} 2775 nobody nogroup -"
]
++ (
builtins.map (
directory: "d ${directory._directory} 2770 ${directory.user} ${directory.group}"
)
config.host.network_storage.directories
);
# set up any bind mounts that we need for our exports
fileSystems = builtins.listToAttrs (
builtins.map (directory:
lib.attrsets.nameValuePair directory._directory {
device = directory.bind;
options = ["bind"];
}) (
builtins.filter (directory: directory.bind != null) config.host.network_storage.directories
)
);
}
# (lib.mkIf config.host.impermanence.enable {
# environment.persistence."/persist/system/root" = {
# enable = true;
# hideMounts = true;
# directories = [
# config.host.network_storage.export_directory
# ];
# };
# })
]);
}

View file

@ -0,0 +1,86 @@
{
config,
lib,
...
}: let
export_directory = config.host.network_storage.export_directory;
in {
options = {
host.network_storage = {
enable = lib.mkEnableOption "is this machine going to export network storage";
export_directory = lib.mkOption {
type = lib.types.path;
description = "what are exports going to be stored in";
default = "/exports";
};
directories = lib.mkOption {
type = lib.types.listOf (lib.types.submodule ({config, ...}: {
options = {
folder = lib.mkOption {
type = lib.types.str;
description = "what is the name of this export directory";
};
bind = lib.mkOption {
type = lib.types.nullOr lib.types.path;
description = "is this directory bound to anywhere";
default = null;
};
user = lib.mkOption {
type = lib.types.str;
description = "what user owns this directory";
default = "nouser";
};
group = lib.mkOption {
type = lib.types.str;
description = "what group owns this directory";
default = "nogroup";
};
_directory = lib.mkOption {
internal = true;
readOnly = true;
type = lib.types.path;
default = "${export_directory}/${config.folder}";
};
};
}));
description = "list of directory names to export";
};
};
};
config = lib.mkIf config.host.network_storage.enable (lib.mkMerge [
{
# create any folders that we need to have for our exports
systemd.tmpfiles.rules =
[
"d ${config.host.network_storage.export_directory} 2775 nobody nogroup -"
]
++ (
builtins.map (
directory: "d ${directory._directory} 2770 ${directory.user} ${directory.group}"
)
config.host.network_storage.directories
);
# set up any bind mounts that we need for our exports
fileSystems = builtins.listToAttrs (
builtins.map (directory:
lib.attrsets.nameValuePair directory._directory {
device = directory.bind;
options = ["bind"];
}) (
builtins.filter (directory: directory.bind != null) config.host.network_storage.directories
)
);
}
# (lib.mkIf config.host.impermanence.enable {
# environment.persistence."/persist/system/root" = {
# enable = true;
# hideMounts = true;
# directories = [
# config.host.network_storage.export_directory
# ];
# };
# })
]);
}

View file

@ -3,32 +3,46 @@
config,
...
}: {
config = lib.mkIf config.services.panoramax.enable (lib.mkMerge [
{
host = {
postgres = {
enable = true;
options.services.panoramax = {
database = {
postgres = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Use PostgreSQL instead of SQLite";
};
user = lib.mkOption {
type = lib.types.str;
default = "panoramax";
description = "Database user name";
};
database = lib.mkOption {
type = lib.types.str;
default = "panoramax";
description = "Database name";
};
};
}
(
lib.mkIf config.host.postgres.enable {
host = {
postgres = {
extraUsers = {
${config.services.panoramax.database.user} = {
isClient = true;
createUser = true;
};
};
extraDatabases = {
${config.services.panoramax.database.name} = {
name = config.services.panoramax.database.user;
};
};
};
};
};
};
config = lib.mkIf config.services.panoramax.enable {
assertions = [
{
assertion = !config.services.panoramax.database.postgres.enable || config.services.postgresql.enable;
message = "PostgreSQL must be enabled when using postgres database for Panoramax";
}
)
]);
];
services.postgresql.databases.panoramax = lib.mkIf config.services.panoramax.database.postgres.enable {
enable = true;
user = config.services.panoramax.database.postgres.user;
database = config.services.panoramax.database.postgres.database;
};
systemd.services.panoramax = lib.mkIf config.services.panoramax.database.postgres.enable {
requires = [
config.systemd.services.postgresql.name
];
};
};
}

View file

@ -3,7 +3,14 @@
config,
...
}: {
config = lib.mkIf (config.services.panoramax.enable && config.host.impermanence.enable) {
options.services.panoramax = {
impermanence.enable = lib.mkOption {
type = lib.types.bool;
default = config.services.panoramax.enable && config.host.impermanence.enable;
};
};
config = lib.mkIf config.services.panoramax.impermanence.enable {
# TODO: configure impermanence for panoramax data
# This would typically include directories like:
# - /var/lib/panoramax

View file

@ -4,31 +4,35 @@
...
}: {
options.services.panoramax = {
subdomain = lib.mkOption {
domain = lib.mkOption {
type = lib.types.str;
description = "subdomain of base domain that panoramax will be hosted at";
default = "panoramax";
description = "domain that panoramax will be hosted at";
default = "panoramax.arpa";
};
extraDomains = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "extra domains that should be configured for panoramax";
default = [];
};
reverseProxy = {
enable = lib.mkOption {
type = lib.types.bool;
default = config.services.panoramax.enable && config.services.reverseProxy.enable;
};
};
};
config = lib.mkIf (config.services.panoramax.enable && config.host.reverse_proxy.enable) {
host = {
reverse_proxy.subdomains.${config.services.panoramax.subdomain} = {
target = "http://localhost:${toString config.services.panoramax.port}";
config = lib.mkIf config.services.panoramax.reverseProxy.enable {
services.reverseProxy.services.panoramax = {
target = "http://localhost:${toString config.services.panoramax.port}";
domain = config.services.panoramax.domain;
extraDomains = config.services.panoramax.extraDomains;
websockets.enable = true;
settings = {
proxyWebsockets.enable = true;
forwardHeaders.enable = true;
extraConfig = ''
# allow large file uploads for panoramic images
client_max_body_size 100M;
# set timeout for image processing
proxy_read_timeout 300s;
proxy_send_timeout 300s;
send_timeout 300s;
proxy_redirect off;
'';
maxBodySize = 100000;
timeout = 300;
};
};
};

View file

@ -3,32 +3,28 @@
lib,
...
}: {
config = lib.mkIf config.services.paperless.enable (lib.mkMerge [
{
host = {
postgres = {
enable = true;
};
};
}
(
lib.mkIf config.host.postgres.enable {
host = {
postgres = {
extraUsers = {
${config.services.paperless.database.user} = {
isClient = true;
createUser = true;
};
};
extraDatabases = {
${config.services.paperless.database.user} = {
name = config.services.paperless.database.user;
};
};
};
};
config = lib.mkIf config.services.paperless.enable {
assertions = [
{
assertion = !config.services.paperless.database.createLocally || config.services.postgresql.enable;
message = "PostgreSQL must be enabled when using local postgres database for Paperless";
}
)
]);
{
assertion = !config.services.paperless.database.createLocally || (builtins.any (db: db == "paperless") config.services.postgresql.ensureDatabases);
message = "Paperless built-in database creation failed - expected 'paperless' in ensureDatabases but got: ${builtins.toString config.services.postgresql.ensureDatabases}";
}
{
assertion = !config.services.paperless.database.createLocally || (builtins.any (user: user.name == "paperless") config.services.postgresql.ensureUsers);
message = "Paperless built-in user creation failed - expected user 'paperless' in ensureUsers but got: ${builtins.toString (builtins.map (u: u.name) config.services.postgresql.ensureUsers)}";
}
];
services.paperless.database.createLocally = lib.mkDefault true;
systemd.services.paperless-scheduler = lib.mkIf config.services.paperless.database.createLocally {
requires = [
config.systemd.services.postgresql.name
];
};
};
}

View file

@ -1,35 +1,9 @@
{
config,
lib,
...
}: {
imports = [
./paperless.nix
./proxy.nix
./database.nix
./fail2ban.nix
./impermanence.nix
];
options.services.paperless = {
database = {
user = lib.mkOption {
type = lib.types.str;
description = "what is the user and database that we are going to use for paperless";
default = "paperless";
};
};
};
config = lib.mkIf config.services.paperless.enable {
services.paperless = {
domain = "${config.services.paperless.subdomain}.${config.host.reverse_proxy.hostname}";
configureTika = true;
settings = {
PAPERLESS_DBENGINE = "postgresql";
PAPERLESS_DBHOST = "/run/postgresql";
PAPERLESS_DBNAME = config.services.paperless.database.user;
PAPERLESS_DBUSER = config.services.paperless.database.user;
};
};
};
}

View file

@ -5,7 +5,14 @@
}: let
dataDir = "/var/lib/paperless";
in {
config = lib.mkIf (config.services.paperless.enable && config.host.impermanence.enable) {
options.services.paperless = {
impermanence.enable = lib.mkOption {
type = lib.types.bool;
default = config.services.paperless.enable && config.host.impermanence.enable;
};
};
config = lib.mkIf config.services.paperless.impermanence.enable {
assertions = [
{
assertion = config.services.paperless.dataDir == dataDir;

View file

@ -0,0 +1,27 @@
{
config,
lib,
...
}: {
options.services.paperless = {
database = {
user = lib.mkOption {
type = lib.types.str;
description = "what is the user and database that we are going to use for paperless";
default = "paperless";
};
};
};
config = lib.mkIf config.services.paperless.enable {
services.paperless = {
configureTika = true;
settings = {
PAPERLESS_DBENGINE = "postgresql";
PAPERLESS_DBHOST = "/run/postgresql";
PAPERLESS_DBNAME = config.services.paperless.database.user;
PAPERLESS_DBUSER = config.services.paperless.database.user;
};
};
};
}

View file

@ -4,25 +4,29 @@
...
}: {
options.services.paperless = {
subdomain = lib.mkOption {
type = lib.types.str;
description = "subdomain of base domain that paperless will be hosted at";
default = "paperless";
extraDomains = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "extra domains that should be configured for paperless";
default = [];
};
reverseProxy = {
enable = lib.mkOption {
type = lib.types.bool;
default = config.services.paperless.enable && config.services.reverseProxy.enable;
};
};
};
config = lib.mkIf (config.services.paperless.enable && config.host.reverse_proxy.enable) {
host = {
reverse_proxy.subdomains.${config.services.paperless.subdomain} = {
target = "http://${config.services.paperless.address}:${toString config.services.paperless.port}";
config = lib.mkIf config.services.paperless.reverseProxy.enable {
services.reverseProxy.services.paperless = {
target = "http://${config.services.paperless.address}:${toString config.services.paperless.port}";
domain = config.services.paperless.domain;
extraDomains = config.services.paperless.extraDomains;
websockets.enable = true;
settings = {
proxyWebsockets.enable = true;
forwardHeaders.enable = true;
extraConfig = ''
# allow large file uploads
client_max_body_size 50000M;
'';
maxBodySize = 50000;
};
};
};

View file

@ -1,73 +0,0 @@
{
lib,
config,
...
}: {
options.host.podman = {
enable = lib.mkEnableOption "should podman be enabled on this computer";
macvlan = {
subnet = lib.mkOption {
type = lib.types.str;
description = "Subnet for macvlan address range";
};
gateway = lib.mkOption {
type = lib.types.str;
description = "Gateway for macvlan";
# TODO: see if we can default this to systemd network gateway
};
networkInterface = lib.mkOption {
type = lib.types.str;
description = "Parent network interface for macvlan";
# TODO: see if we can default this some interface?
};
};
};
config = lib.mkIf config.host.podman.enable {
systemd = {
services = {
# "podman-network-macvlan" = {
# path = [pkgs.podman];
# serviceConfig = {
# Type = "oneshot";
# RemainAfterExit = true;
# ExecStop = "podman network rm -f macvlan";
# };
# script = ''
# podman network inspect macvlan || podman network create --driver macvlan --subnet ${config.host.podman.macvlan.subnet} --gateway ${config.host.podman.macvlan.gateway} --opt parent=${config.host.podman.macvlan.networkInterface} macvlan
# '';
# partOf = ["podman-compose-root.target"];
# wantedBy = ["podman-compose-root.target"];
# };
};
# disable computer sleeping
targets = {
# Root service
# When started, this will automatically create all resources and start
# the containers. When stopped, this will teardown all resources.
"podman-compose-root" = {
unitConfig = {
Description = "Root target for podman targets.";
};
wantedBy = ["multi-user.target"];
};
};
};
virtualisation = {
# Runtime
podman = {
enable = true;
autoPrune.enable = true;
dockerCompat = true;
# defaultNetwork.settings = {
# # Required for container networking to be able to use names.
# dns_enabled = true;
# };
};
oci-containers = {
backend = "podman";
};
};
};
}

View file

@ -1,121 +0,0 @@
{
config,
lib,
pkgs,
...
}: let
dataDir = "/var/lib/postgresql/16";
adminUsers = lib.lists.filter (user: user.isAdmin) (lib.attrsets.mapAttrsToList (_: user: user) config.host.postgres.extraUsers);
clientUsers = lib.lists.filter (user: user.isClient) (lib.attrsets.mapAttrsToList (_: user: user) config.host.postgres.extraUsers);
createUsers = lib.lists.filter (user: user.createUser) (lib.attrsets.mapAttrsToList (_: user: user) config.host.postgres.extraUsers);
createDatabases = lib.attrsets.mapAttrsToList (_: user: user) config.host.postgres.extraDatabases;
in {
options = {
host.postgres = {
enable = lib.mkEnableOption "enable postgres";
extraUsers = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule ({name, ...}: {
options = {
name = lib.mkOption {
type = lib.types.str;
default = name;
};
isAdmin = lib.mkOption {
type = lib.types.bool;
default = false;
};
isClient = lib.mkOption {
type = lib.types.bool;
default = false;
};
createUser = lib.mkOption {
type = lib.types.bool;
default = false;
};
};
}));
default = {};
};
extraDatabases = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule ({name, ...}: {
options = {
name = lib.mkOption {
type = lib.types.str;
default = name;
};
};
}));
default = {};
};
};
};
config = lib.mkIf config.host.postgres.enable (lib.mkMerge [
{
services = {
postgresql = {
enable = true;
package = pkgs.postgresql_16;
ensureUsers =
[
{
name = "postgres";
}
]
++ (
builtins.map (user: {
name = user.name;
ensureDBOwnership = true;
})
createUsers
);
ensureDatabases = builtins.map (database: database.name) createDatabases;
identMap =
''
# ArbitraryMapName systemUser DBUser
# Administration Users
superuser_map root postgres
superuser_map postgres postgres
''
+ (
lib.strings.concatLines (builtins.map (user: "superuser_map ${user.name} postgres") adminUsers)
)
+ ''
# Client Users
''
+ (
lib.strings.concatLines (builtins.map (user: "user_map ${user.name} ${user.name}") clientUsers)
);
# configuration here lets users access the db that matches their name and lets user postgres access everything
authentication = pkgs.lib.mkOverride 10 ''
# type database DBuser origin-address auth-method optional_ident_map
local all postgres peer map=superuser_map
local sameuser all peer map=user_map
'';
};
};
}
(lib.mkIf config.host.impermanence.enable {
assertions = [
{
assertion = config.services.postgresql.dataDir == dataDir;
message = "postgres data directory does not match persistence";
}
];
environment.persistence."/persist/system/root" = {
enable = true;
hideMounts = true;
directories = [
{
directory = dataDir;
user = "postgres";
group = "postgres";
}
];
};
})
]);
}

View file

@ -0,0 +1,6 @@
{...}: {
imports = [
./postgres.nix
./impermanence.nix
];
}

View file

@ -0,0 +1,27 @@
{
config,
lib,
...
}: let
dataDir = "/var/lib/postgresql/16";
in {
config = lib.mkIf (config.services.postgresql.enable && config.host.impermanence.enable) {
assertions = [
{
assertion = config.services.postgresql.dataDir == dataDir;
message = "postgres data directory does not match persistence";
}
];
environment.persistence."/persist/system/root" = {
enable = true;
hideMounts = true;
directories = [
{
directory = dataDir;
user = "postgres";
group = "postgres";
}
];
};
};
}

View file

@ -0,0 +1,122 @@
{
config,
lib,
pkgs,
...
}: let
enabledDatabases = lib.filterAttrs (_: db: db.enable) config.services.postgresql.databases;
extraDatabasesList = config.services.postgresql.extraDatabases;
serviceDatabaseUsers = lib.mapAttrsToList (_: db: {
name = db.user;
ensureDBOwnership = true;
}) (lib.filterAttrs (_: db: db.ensureUser) enabledDatabases);
extraDatabaseUsers =
builtins.map (dbName: {
name = dbName;
ensureDBOwnership = true;
})
extraDatabasesList;
serviceDatabases = lib.mapAttrsToList (_: db: db.database) enabledDatabases;
extraDatabaseNames = extraDatabasesList;
serviceUserMappings = lib.mapAttrsToList (_: db: "user_map ${db.user} ${db.user}") enabledDatabases;
extraUserMappings = builtins.map (dbName: "user_map ${dbName} ${dbName}") extraDatabasesList;
builtinServiceMappings = let
forgejoMapping = lib.optional (config.services.forgejo.enable && config.services.forgejo.database.type == "postgres") "user_map forgejo forgejo";
immichMapping = lib.optional (config.services.immich.enable && config.services.immich.database.enable) "user_map immich immich";
paperlessMapping = lib.optional (config.services.paperless.enable && config.services.paperless.database.createLocally) "user_map paperless paperless";
in
forgejoMapping ++ immichMapping ++ paperlessMapping;
in {
options = {
services.postgresql = {
databases = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule ({name, ...}: {
options = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to create this database and user";
};
user = lib.mkOption {
type = lib.types.str;
default = name;
description = "Database user name";
};
database = lib.mkOption {
type = lib.types.str;
default = name;
description = "Database name";
};
ensureUser = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to ensure the user exists";
};
};
}));
default = {};
description = "Databases to create for services";
};
extraDatabases = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [];
description = "Additional databases to create (user name will match database name)";
example = ["custom_db" "test_db"];
};
adminUsers = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [];
description = "System users who should have PostgreSQL superuser access";
example = ["leyla" "admin"];
};
};
};
config = lib.mkIf config.services.postgresql.enable {
services = {
postgresql = {
package = pkgs.postgresql_16;
ensureUsers =
[
{name = "postgres";}
]
++ serviceDatabaseUsers ++ extraDatabaseUsers;
ensureDatabases = serviceDatabases ++ extraDatabaseNames;
identMap =
''
# ArbitraryMapName systemUser DBUser
# Administration Users
superuser_map root postgres
superuser_map postgres postgres
''
+ (
lib.strings.concatLines (builtins.map (user: "superuser_map ${user} postgres") config.services.postgresql.adminUsers)
)
+ ''
# Client Users
''
+ (
lib.strings.concatLines (serviceUserMappings ++ extraUserMappings ++ builtinServiceMappings)
);
authentication = pkgs.lib.mkOverride 10 ''
# type database DBuser origin-address auth-method optional_ident_map
local all postgres peer map=superuser_map
local sameuser all peer map=user_map
'';
};
};
};
}

View file

@ -1,65 +0,0 @@
{
lib,
config,
...
}: let
qbittorent_profile_directory = "/var/lib/qBittorrent/";
in {
options.services.qbittorrent = {
mediaDir = lib.mkOption {
type = lib.types.path;
description = lib.mdDoc ''
The directory to create to store qbittorrent media.
'';
};
};
config = lib.mkIf config.services.qbittorrent.enable (lib.mkMerge [
(lib.mkIf config.host.impermanence.enable {
fileSystems."/persist/system/qbittorrent".neededForBoot = true;
host.storage.pool.extraDatasets = {
# sops age key needs to be available to pre persist for user generation
"persist/system/qbittorrent" = {
type = "zfs_fs";
mountpoint = "/persist/system/qbittorrent";
options = {
canmount = "on";
};
};
};
assertions = [
{
assertion = config.services.qbittorrent.profileDir == qbittorent_profile_directory;
message = "qbittorrent data directory does not match persistence";
}
];
environment.persistence = {
"/persist/system/root" = {
directories = [
{
directory = qbittorent_profile_directory;
user = "qbittorrent";
group = "qbittorrent";
}
];
};
"/persist/system/qbittorrent" = {
enable = true;
hideMounts = true;
directories = [
{
directory = config.services.qbittorrent.mediaDir;
user = "qbittorrent";
group = "qbittorrent";
mode = "1775";
}
];
};
};
})
]);
}

View file

@ -0,0 +1,6 @@
{...}: {
imports = [
./qbittorent.nix
./impermanence.nix
];
}

View file

@ -0,0 +1,61 @@
{
lib,
config,
...
}: let
qbittorent_profile_directory = "/var/lib/qBittorrent/";
in {
options.services.qbittorrent = {
impermanence.enable = lib.mkOption {
type = lib.types.bool;
default = config.services.qbittorrent.enable && config.host.impermanence.enable;
};
};
config = lib.mkIf config.services.qbittorrent.impermanence.enable {
fileSystems."/persist/system/qbittorrent".neededForBoot = true;
host.storage.pool.extraDatasets = {
# sops age key needs to be available to pre persist for user generation
"persist/system/qbittorrent" = {
type = "zfs_fs";
mountpoint = "/persist/system/qbittorrent";
options = {
canmount = "on";
};
};
};
assertions = [
{
assertion = config.services.qbittorrent.profileDir == qbittorent_profile_directory;
message = "qbittorrent data directory does not match persistence";
}
];
environment.persistence = {
"/persist/system/root" = {
directories = [
{
directory = qbittorent_profile_directory;
user = "qbittorrent";
group = "qbittorrent";
}
];
};
"/persist/system/qbittorrent" = {
enable = true;
hideMounts = true;
directories = [
{
directory = config.services.qbittorrent.mediaDir;
user = "qbittorrent";
group = "qbittorrent";
mode = "1775";
}
];
};
};
};
}

View file

@ -0,0 +1,18 @@
{
lib,
config,
...
}: {
options.services.qbittorrent = {
mediaDir = lib.mkOption {
type = lib.types.path;
description = lib.mdDoc ''
The directory to create to store qbittorrent media.
'';
};
};
config = lib.mkIf config.services.qbittorrent.enable {
# Main qbittorrent configuration goes here if needed
};
}

View file

@ -1,6 +1,5 @@
{...}: {
imports = [
./proxy.nix
./impermanence.nix
];
}

View file

@ -5,7 +5,14 @@
}: let
radarr_data_directory = "/var/lib/radarr/.config/Radarr";
in {
config = lib.mkIf (config.services.radarr.enable && config.host.impermanence.enable) {
options.services.radarr = {
impermanence.enable = lib.mkOption {
type = lib.types.bool;
default = config.services.radarr.enable && config.host.impermanence.enable;
};
};
config = lib.mkIf config.services.radarr.impermanence.enable {
assertions = [
{
assertion = config.services.radarr.dataDir == radarr_data_directory;

View file

@ -1,28 +0,0 @@
{
lib,
config,
...
}: {
options.services.radarr = {
subdomain = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Subdomain for reverse proxy. If null, service will be local only.";
};
extraSubdomains = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [];
description = "Extra subdomains for reverse proxy.";
};
};
config = lib.mkIf (config.services.radarr.enable && config.services.radarr.subdomain != null) {
host.reverse_proxy.subdomains.radarr = {
subdomain = config.services.radarr.subdomain;
extraSubdomains = config.services.radarr.extraSubdomains;
target = "http://127.0.0.1:7878";
websockets.enable = true;
forwardHeaders.enable = true;
};
};
}

View file

@ -0,0 +1,6 @@
{...}: {
imports = [
./reverseProxy.nix
./impermanence.nix
];
}

View file

@ -0,0 +1,21 @@
{
lib,
config,
...
}: let
dataDir = "/var/lib/acme";
in {
config = lib.mkIf (config.host.impermanence.enable && config.services.reverseProxy.enable) {
environment.persistence."/persist/system/root" = {
enable = true;
hideMounts = true;
directories = [
{
directory = dataDir;
user = "acme";
group = "acme";
}
];
};
};
}

View file

@ -0,0 +1,176 @@
{
lib,
config,
...
}: {
options.services.reverseProxy = {
enable = lib.mkEnableOption "turn on the reverse proxy";
openFirewall = lib.mkEnableOption "open the firewall";
refuseUnmatchedDomains = lib.mkOption {
type = lib.types.bool;
description = "refuse connections for domains that don't match any configured virtual hosts";
default = true;
};
ports = {
http = lib.mkOption {
type = lib.types.port;
description = "HTTP port for the reverse proxy";
default = 80;
};
https = lib.mkOption {
type = lib.types.port;
description = "HTTPS port for the reverse proxy";
default = 443;
};
};
acme = {
enable = lib.mkOption {
type = lib.types.bool;
description = "enable ACME certificate management";
default = true;
};
email = lib.mkOption {
type = lib.types.str;
description = "email address for ACME certificate registration";
};
};
services = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule ({name, ...}: {
options = {
target = lib.mkOption {
type = lib.types.str;
description = "what url will all traffic to this application be forwarded to";
};
domain = lib.mkOption {
type = lib.types.str;
description = "what is the default subdomain to be used for this application to be used for";
default = name;
};
extraDomains = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "extra domains that should be configured for this domain";
default = [];
};
settings = {
certificateRenewal.enable = lib.mkOption {
type = lib.types.bool;
description = "auto renew certificates";
default = true;
};
forceSSL.enable = lib.mkOption {
type = lib.types.bool;
description = "auto renew certificates";
default = true;
};
proxyHeaders = {
enable = lib.mkEnableOption "should we proxy headers";
timeout = lib.mkOption {
type = lib.types.int;
default = 60;
};
};
proxyWebsockets.enable = lib.mkEnableOption "should the default config proxy websockets";
forwardHeaders.enable = lib.mkEnableOption "should the default config contain forward headers";
noSniff.enable = lib.mkEnableOption "should the no sniff flags be set";
proxyBuffering.enable = lib.mkOption {
type = lib.types.bool;
description = "should proxy buffering be enabled";
default = true;
};
maxBodySize = lib.mkOption {
type = lib.types.nullOr lib.types.int;
description = "";
default = null;
};
};
};
}));
};
};
config = let
httpPort = config.services.reverseProxy.ports.http;
httpsPort = config.services.reverseProxy.ports.https;
in
lib.mkIf config.services.reverseProxy.enable {
security.acme = lib.mkIf config.services.reverseProxy.acme.enable {
acceptTerms = true;
defaults.email = config.services.reverseProxy.acme.email;
};
services.nginx = {
enable = true;
virtualHosts = lib.mkMerge (
(lib.optionals config.services.reverseProxy.refuseUnmatchedDomains [
{
"_" = {
default = true;
serverName = "_";
locations."/" = {
extraConfig = ''
return 444;
'';
};
};
}
])
++ lib.lists.flatten (
lib.attrsets.mapAttrsToList (
name: service: let
hostConfig = {
forceSSL = service.settings.forceSSL.enable;
enableACME = service.settings.certificateRenewal.enable;
locations = {
"/" = {
proxyPass = service.target;
proxyWebsockets = service.settings.proxyWebsockets.enable;
recommendedProxySettings = service.settings.forwardHeaders.enable;
extraConfig = let
# Client upload size configuration
maxBodySizeConfig =
lib.optionalString (service.settings.maxBodySize != null)
"client_max_body_size ${toString service.settings.maxBodySize}M;";
# Security header configuration
noSniffConfig =
lib.optionalString service.settings.noSniff.enable
"add_header X-Content-Type-Options nosniff;";
# Proxy buffering configuration
proxyBufferingConfig =
lib.optionalString (!service.settings.proxyBuffering.enable)
"proxy_buffering off;";
# Proxy timeout configuration
proxyTimeoutConfig =
lib.optionalString service.settings.proxyHeaders.enable
''
proxy_read_timeout ${toString service.settings.proxyHeaders.timeout}s;
proxy_connect_timeout ${toString service.settings.proxyHeaders.timeout}s;
proxy_send_timeout ${toString service.settings.proxyHeaders.timeout}s;
'';
in
maxBodySizeConfig + noSniffConfig + proxyBufferingConfig + proxyTimeoutConfig;
};
};
};
in (
[
{
${service.domain} = hostConfig;
}
]
++ builtins.map (domain: {${domain} = hostConfig;})
service.extraDomains
)
)
config.services.reverseProxy.services
)
);
};
networking.firewall.allowedTCPPorts = lib.mkIf config.services.reverseProxy.openFirewall [
httpPort
httpsPort
];
};
}

View file

@ -1,128 +0,0 @@
{
lib,
config,
...
}: let
dataDir = "/var/lib/acme";
httpPort = 80;
httpsPort = 443;
in {
options.host.reverse_proxy = {
enable = lib.mkEnableOption "turn on the reverse proxy";
hostname = lib.mkOption {
type = lib.types.str;
description = "what host name are we going to be proxying from";
};
forceSSL = lib.mkOption {
type = lib.types.bool;
description = "force connections to use https";
default = config.host.reverse_proxy.enableACME;
};
enableACME = lib.mkOption {
type = lib.types.bool;
description = "auto renew certificates";
default = true;
};
subdomains = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule ({name, ...}: {
options = {
subdomain = lib.mkOption {
type = lib.types.str;
description = "what is the default subdomain to be used for this application to be used for";
default = name;
};
extraSubdomains = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "extra domains that should be configured for this domain";
default = [];
};
target = lib.mkOption {
type = lib.types.str;
description = "what url will all traffic to this application be forwarded to";
};
websockets.enable = lib.mkEnableOption "should the default config proxy websockets";
forwardHeaders.enable = lib.mkEnableOption "should the default config contain forward headers";
extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = ''
These lines go to the end of the upstream verbatim.
'';
};
};
}));
};
};
config = lib.mkIf config.host.reverse_proxy.enable (lib.mkMerge [
{
security.acme = lib.mkIf config.host.reverse_proxy.enableACME {
acceptTerms = true;
defaults.email = "jan-leila@protonmail.com";
};
services.nginx = {
enable = true;
virtualHosts = lib.mkMerge (
lib.lists.flatten (
lib.attrsets.mapAttrsToList (
name: value: let
hostConfig = {
forceSSL = config.host.reverse_proxy.forceSSL;
enableACME = config.host.reverse_proxy.enableACME;
locations = {
"/" = {
proxyPass = value.target;
proxyWebsockets = value.websockets.enable;
recommendedProxySettings = value.forwardHeaders.enable;
extraConfig =
value.extraConfig;
};
};
};
in (
[
{
${"${value.subdomain}.${config.host.reverse_proxy.hostname}"} = hostConfig;
}
]
++ builtins.map (subdomain: {${"${subdomain}.${config.host.reverse_proxy.hostname}"} = hostConfig;})
value.extraSubdomains
)
)
config.host.reverse_proxy.subdomains
)
);
};
networking.firewall.allowedTCPPorts = [
httpPort
httpsPort
];
}
(lib.mkIf config.host.impermanence.enable {
# TODO: figure out how to write an assertion for this
# assertions = [
# {
# assertion = security.acme.certs.<name>.directory == dataDir;
# message = "postgres data directory does not match persistence";
# }
# ];
environment.persistence."/persist/system/root" = {
enable = true;
hideMounts = true;
directories = [
{
directory = dataDir;
user = "acme";
group = "acme";
}
];
};
})
]);
}

View file

@ -1,63 +1,6 @@
{
config,
lib,
inputs,
...
}: {
imports = [
./searx.nix
./proxy.nix
];
config = lib.mkIf config.services.searx.enable {
sops.secrets = {
"services/searx" = {
sopsFile = "${inputs.secrets}/defiant-services.yaml";
};
};
services.searx = {
environmentFile = config.sops.secrets."services/searx".path;
# Rate limiting
limiterSettings = {
real_ip = {
x_for = 1;
ipv4_prefix = 32;
ipv6_prefix = 56;
};
botdetection = {
ip_limit = {
filter_link_local = true;
link_token = true;
};
};
};
settings = {
server = {
port = 8083;
secret_key = "@SEARXNG_SECRET@";
};
# Search engine settings
search = {
safe_search = 2;
autocomplete_min = 2;
autocomplete = "duckduckgo";
};
# Enabled plugins
enabled_plugins = [
"Basic Calculator"
"Hash plugin"
"Tor check plugin"
"Open Access DOI rewrite"
"Hostnames plugin"
"Unit converter plugin"
"Tracker URL remover"
];
};
};
};
}

View file

@ -4,18 +4,27 @@
...
}: {
options.services.searx = {
subdomain = lib.mkOption {
type = lib.types.str;
description = "subdomain of base domain that searx will be hosted at";
default = "searx";
extraDomains = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "extra domains that should be configured for searx";
default = [];
};
reverseProxy = {
enable = lib.mkOption {
type = lib.types.bool;
default = config.services.searx.enable && config.services.reverseProxy.enable;
};
};
};
config = lib.mkIf (config.services.searx.enable && config.host.reverse_proxy.enable) {
host = {
reverse_proxy.subdomains.searx = {
subdomain = config.services.searx.subdomain;
target = "http://localhost:${toString config.services.searx.settings.server.port}";
config = lib.mkIf config.services.searx.reverseProxy.enable {
services.reverseProxy.services.searx = {
target = "http://localhost:${toString config.services.searx.settings.server.port}";
domain = config.services.searx.domain;
extraDomains = config.services.searx.extraDomains;
settings = {
forwardHeaders.enable = true;
};
};
};

View file

@ -0,0 +1,59 @@
{
config,
lib,
inputs,
...
}: {
config = lib.mkIf config.services.searx.enable {
sops.secrets = {
"services/searx" = {
sopsFile = "${inputs.secrets}/defiant-services.yaml";
};
};
services.searx = {
environmentFile = config.sops.secrets."services/searx".path;
# Rate limiting
limiterSettings = {
real_ip = {
x_for = 1;
ipv4_prefix = 32;
ipv6_prefix = 56;
};
botdetection = {
ip_limit = {
filter_link_local = true;
link_token = true;
};
};
};
settings = {
server = {
port = 8083;
secret_key = "@SEARXNG_SECRET@";
};
# Search engine settings
search = {
safe_search = 2;
autocomplete_min = 2;
autocomplete = "duckduckgo";
};
# Enabled plugins
enabled_plugins = [
"Basic Calculator"
"Hash plugin"
"Tor check plugin"
"Open Access DOI rewrite"
"Hostnames plugin"
"Unit converter plugin"
"Tracker URL remover"
];
};
};
};
}

View file

@ -1,6 +1,5 @@
{...}: {
imports = [
./proxy.nix
./impermanence.nix
];
}

View file

@ -5,7 +5,14 @@
}: let
sonarr_data_directory = "/var/lib/sonarr/.config/NzbDrone";
in {
config = lib.mkIf (config.services.sonarr.enable && config.host.impermanence.enable) {
options.services.sonarr = {
impermanence.enable = lib.mkOption {
type = lib.types.bool;
default = config.services.sonarr.enable && config.host.impermanence.enable;
};
};
config = lib.mkIf config.services.sonarr.impermanence.enable {
assertions = [
{
assertion = config.services.sonarr.dataDir == sonarr_data_directory;

View file

@ -1,28 +0,0 @@
{
lib,
config,
...
}: {
options.services.sonarr = {
subdomain = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Subdomain for reverse proxy. If null, service will be local only.";
};
extraSubdomains = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [];
description = "Extra subdomains for reverse proxy.";
};
};
config = lib.mkIf (config.services.sonarr.enable && config.services.sonarr.subdomain != null) {
host.reverse_proxy.subdomains.sonarr = {
subdomain = config.services.sonarr.subdomain;
extraSubdomains = config.services.sonarr.extraSubdomains;
target = "http://127.0.0.1:8989";
websockets.enable = true;
forwardHeaders.enable = true;
};
};
}

View file

@ -37,9 +37,9 @@
openwakeword = {
enable = true;
uri = "tcp://0.0.0.0:10400";
preloadModels = [
"ok_nabu"
];
# preloadModels = [
# "ok_nabu"
# ];
# TODO: custom models
};
};

View file

@ -16,6 +16,8 @@
};
};
};
programs.ssh.kexAlgorithms = config.services.openssh.settings.KexAlgorithms;
}
(lib.mkIf config.host.impermanence.enable {
environment.persistence."/persist/system/root" = {

View file

@ -1,7 +1,7 @@
{
config,
lib,
outputs,
syncthingConfiguration,
...
}: let
mountDir = "/mnt/sync";
@ -27,7 +27,7 @@ in {
configDir = configDir;
overrideDevices = true;
overrideFolders = true;
configuration = outputs.syncthingConfiguration;
configuration = syncthingConfiguration;
deviceName = config.networking.hostName;
};
}