diff --git a/modules/nixos-modules/server/actual.nix b/modules/nixos-modules/server/actual.nix deleted file mode 100644 index 80f4fab..0000000 --- a/modules/nixos-modules/server/actual.nix +++ /dev/null @@ -1,56 +0,0 @@ -{ - lib, - config, - ... -}: let - dataDirectory = "/var/lib/actual/"; -in { - options.services.actual = { - subdomain = lib.mkOption { - type = lib.types.str; - default = "actual"; - description = "subdomain of base domain that actual will be hosted at"; - }; - }; - - config = lib.mkIf config.services.actual.enable (lib.mkMerge [ - { - systemd.tmpfiles.rules = [ - "d ${dataDirectory} 2770 actual actual" - ]; - - services.actual = { - settings = { - ACTUAL_DATA_DIR = dataDirectory; - }; - }; - } - (lib.mkIf config.host.reverse_proxy.enable { - host = { - reverse_proxy.subdomains.${config.services.actual.subdomain} = { - target = "http://localhost:${toString config.services.actual.settings.port}"; - }; - }; - }) - (lib.mkIf config.services.fail2ban.enable { - # TODO: configuration for fail2ban for actual - }) - (lib.mkIf config.host.impermanence.enable { - assertions = [ - { - assertion = config.services.actual.settings.ACTUAL_DATA_DIR == dataDirectory; - message = "actual data location does not match persistence"; - } - ]; - environment.persistence."/persist/system/root" = { - directories = [ - { - directory = dataDirectory; - user = "actual"; - group = "actual"; - } - ]; - }; - }) - ]); -} diff --git a/modules/nixos-modules/server/actual/const.nix b/modules/nixos-modules/server/actual/const.nix new file mode 100644 index 0000000..13b068e --- /dev/null +++ b/modules/nixos-modules/server/actual/const.nix @@ -0,0 +1,3 @@ +{ + dataDirectory = "/var/lib/actual/"; +} diff --git a/modules/nixos-modules/server/actual/default.nix b/modules/nixos-modules/server/actual/default.nix new file mode 100644 index 0000000..bef7a05 --- /dev/null +++ b/modules/nixos-modules/server/actual/default.nix @@ -0,0 +1,34 @@ +{ + lib, + config, + ... +}: let + const = import ./const.nix; + dataDirectory = const.dataDirectory; +in { + imports = [ + ./proxy.nix + ./fail2ban.nix + ./impermanence.nix + ]; + + options.services.actual = { + subdomain = lib.mkOption { + type = lib.types.str; + default = "actual"; + description = "subdomain of base domain that actual will be hosted at"; + }; + }; + + config = lib.mkIf config.services.actual.enable { + systemd.tmpfiles.rules = [ + "d ${dataDirectory} 2770 actual actual" + ]; + + services.actual = { + settings = { + ACTUAL_DATA_DIR = dataDirectory; + }; + }; + }; +} diff --git a/modules/nixos-modules/server/actual/fail2ban.nix b/modules/nixos-modules/server/actual/fail2ban.nix new file mode 100644 index 0000000..3ad754e --- /dev/null +++ b/modules/nixos-modules/server/actual/fail2ban.nix @@ -0,0 +1,9 @@ +{ + lib, + config, + ... +}: { + config = lib.mkIf (config.services.actual.enable && config.services.fail2ban.enable) { + # TODO: configuration for fail2ban for actual + }; +} diff --git a/modules/nixos-modules/server/actual/impermanence.nix b/modules/nixos-modules/server/actual/impermanence.nix new file mode 100644 index 0000000..5eee95a --- /dev/null +++ b/modules/nixos-modules/server/actual/impermanence.nix @@ -0,0 +1,26 @@ +{ + lib, + config, + ... +}: let + const = import ./const.nix; + dataDirectory = const.dataDirectory; +in { + config = lib.mkIf (config.services.actual.enable && config.host.impermanence.enable) { + assertions = [ + { + assertion = config.services.actual.settings.ACTUAL_DATA_DIR == dataDirectory; + message = "actual data location does not match persistence"; + } + ]; + environment.persistence."/persist/system/root" = { + directories = [ + { + directory = dataDirectory; + user = "actual"; + group = "actual"; + } + ]; + }; + }; +} diff --git a/modules/nixos-modules/server/actual/proxy.nix b/modules/nixos-modules/server/actual/proxy.nix new file mode 100644 index 0000000..e20a6cd --- /dev/null +++ b/modules/nixos-modules/server/actual/proxy.nix @@ -0,0 +1,13 @@ +{ + lib, + config, + ... +}: { + 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}"; + }; + }; + }; +} diff --git a/modules/nixos-modules/server/default.nix b/modules/nixos-modules/server/default.nix index 87f3dae..15f833b 100644 --- a/modules/nixos-modules/server/default.nix +++ b/modules/nixos-modules/server/default.nix @@ -1,19 +1,20 @@ {...}: { imports = [ - ./fail2ban.nix - ./network_storage ./reverse_proxy.nix + ./fail2ban.nix ./postgres.nix + ./network_storage ./podman.nix - ./jellyfin.nix - ./forgejo.nix - ./searx.nix - ./home-assistant.nix - ./wyoming.nix - ./immich.nix + + ./actual + ./immich + ./panoramax + ./forgejo + ./home-assistant + ./jellyfin + ./paperless + ./searx ./qbittorent.nix - ./paperless.nix - ./actual.nix - ./panoramax.nix + ./wyoming.nix ]; } diff --git a/modules/nixos-modules/server/forgejo.nix b/modules/nixos-modules/server/forgejo.nix deleted file mode 100644 index 3b19695..0000000 --- a/modules/nixos-modules/server/forgejo.nix +++ /dev/null @@ -1,128 +0,0 @@ -{ - lib, - config, - pkgs, - ... -}: let - forgejoPort = 8081; - stateDir = "/var/lib/forgejo"; - db_user = "forgejo"; - sshPort = 22222; -in { - options.services.forgejo = { - subdomain = lib.mkOption { - type = lib.types.str; - description = "subdomain of base domain that forgejo will be hosted at"; - default = "forgejo"; - }; - }; - - config = lib.mkIf config.services.forgejo.enable (lib.mkMerge [ - { - 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"; - } - ]; - host = { - postgres = { - enable = true; - extraUsers = { - ${db_user} = { - isClient = true; - createUser = true; - }; - }; - extraDatabases = { - ${db_user} = { - name = db_user; - }; - }; - }; - }; - - 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 = forgejoPort; - 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; - }; - }; - }; - - networking.firewall.allowedTCPPorts = [ - config.services.forgejo.settings.server.SSH_LISTEN_PORT - ]; - } - (lib.mkIf config.host.reverse_proxy.enable { - host = { - reverse_proxy.subdomains.${config.services.forgejo.subdomain} = { - target = "http://localhost:${toString forgejoPort}"; - }; - }; - }) - (lib.mkIf config.services.fail2ban.enable { - environment.etc = { - "fail2ban/filter.d/forgejo.local".text = lib.mkIf config.services.forgejo.enable ( - pkgs.lib.mkDefault (pkgs.lib.mkAfter '' - [Definition] - failregex = ".*(Failed authentication attempt|invalid credentials|Attempted access of unknown user).* from " - '') - ); - }; - - services.fail2ban = { - jails = { - forgejo-iptables.settings = lib.mkIf config.services.forgejo.enable { - enabled = true; - filter = "forgejo"; - action = ''iptables-multiport[name=HTTP, port="http,https"]''; - logpath = "${config.services.forgejo.settings.log.ROOT_PATH}/*.log"; - backend = "auto"; - findtime = 600; - bantime = 600; - maxretry = 5; - }; - }; - }; - }) - (lib.mkIf config.host.impermanence.enable { - assertions = [ - { - assertion = config.services.forgejo.stateDir == stateDir; - message = "forgejo state directory does not match persistence"; - } - ]; - environment.persistence."/persist/system/root" = { - enable = true; - hideMounts = true; - directories = [ - { - directory = stateDir; - user = "forgejo"; - group = "forgejo"; - } - ]; - }; - }) - ]); -} diff --git a/modules/nixos-modules/server/forgejo/const.nix b/modules/nixos-modules/server/forgejo/const.nix new file mode 100644 index 0000000..10e3974 --- /dev/null +++ b/modules/nixos-modules/server/forgejo/const.nix @@ -0,0 +1,4 @@ +{ + httpPort = 8081; + sshPort = 22222; +} diff --git a/modules/nixos-modules/server/forgejo/database.nix b/modules/nixos-modules/server/forgejo/database.nix new file mode 100644 index 0000000..0417aab --- /dev/null +++ b/modules/nixos-modules/server/forgejo/database.nix @@ -0,0 +1,41 @@ +{ + lib, + config, + ... +}: { + config = lib.mkIf config.services.forgejo.enable ( + lib.mkMerge [ + { + host = { + postgres = { + enable = true; + }; + }; + + assertions = [ + { + assertion = config.services.forgejo.settings.database.DB_TYPE == "postgres"; + message = "Forgejo database type must be postgres"; + } + ]; + } + (lib.mkIf config.host.postgres.enable { + host = { + postgres = { + extraUsers = { + forgejo = { + isClient = true; + createUser = true; + }; + }; + extraDatabases = { + forgejo = { + name = "forgejo"; + }; + }; + }; + }; + }) + ] + ); +} diff --git a/modules/nixos-modules/server/forgejo/default.nix b/modules/nixos-modules/server/forgejo/default.nix new file mode 100644 index 0000000..cec2630 --- /dev/null +++ b/modules/nixos-modules/server/forgejo/default.nix @@ -0,0 +1,61 @@ +{ + lib, + config, + ... +}: let + const = import ./const.nix; + httpPort = const.httpPort; + sshPort = const.sshPort; + db_user = "forgejo"; +in { + imports = [ + ./proxy.nix + ./database.nix + ./fail2ban.nix + ./impermanence.nix + ]; + + options.services.forgejo = { + subdomain = lib.mkOption { + type = lib.types.str; + description = "subdomain of base domain that forgejo will be hosted at"; + default = "forgejo"; + }; + }; + + 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; + }; + }; + }; + }; +} diff --git a/modules/nixos-modules/server/forgejo/fail2ban.nix b/modules/nixos-modules/server/forgejo/fail2ban.nix new file mode 100644 index 0000000..213c804 --- /dev/null +++ b/modules/nixos-modules/server/forgejo/fail2ban.nix @@ -0,0 +1,32 @@ +{ + lib, + config, + pkgs, + ... +}: { + config = lib.mkIf (config.services.forgejo.enable && config.services.fail2ban.enable) { + environment.etc = { + "fail2ban/filter.d/forgejo.local".text = lib.mkIf config.services.forgejo.enable ( + pkgs.lib.mkDefault (pkgs.lib.mkAfter '' + [Definition] + failregex = ".*(Failed authentication attempt|invalid credentials|Attempted access of unknown user).* from " + '') + ); + }; + + services.fail2ban = { + jails = { + forgejo-iptables.settings = lib.mkIf config.services.forgejo.enable { + enabled = true; + filter = "forgejo"; + action = ''iptables-multiport[name=HTTP, port="http,https"]''; + logpath = "${config.services.forgejo.settings.log.ROOT_PATH}/*.log"; + backend = "auto"; + findtime = 600; + bantime = 600; + maxretry = 5; + }; + }; + }; + }; +} diff --git a/modules/nixos-modules/server/forgejo/impermanence.nix b/modules/nixos-modules/server/forgejo/impermanence.nix new file mode 100644 index 0000000..04f21a5 --- /dev/null +++ b/modules/nixos-modules/server/forgejo/impermanence.nix @@ -0,0 +1,28 @@ +{ + lib, + config, + ... +}: let + stateDir = "/var/lib/forgejo"; +in { + config = lib.mkIf (config.services.forgejo.enable && config.host.impermanence.enable) { + assertions = [ + { + assertion = config.services.forgejo.stateDir == stateDir; + message = "forgejo state directory does not match persistence"; + } + ]; + + environment.persistence."/persist/system/root" = { + enable = true; + hideMounts = true; + directories = [ + { + directory = stateDir; + user = "forgejo"; + group = "forgejo"; + } + ]; + }; + }; +} diff --git a/modules/nixos-modules/server/forgejo/proxy.nix b/modules/nixos-modules/server/forgejo/proxy.nix new file mode 100644 index 0000000..9e85f78 --- /dev/null +++ b/modules/nixos-modules/server/forgejo/proxy.nix @@ -0,0 +1,18 @@ +{ + lib, + config, + ... +}: let + const = import ./const.nix; + httpPort = const.httpPort; +in { + config = lib.mkIf (config.services.forgejo.enable && config.host.reverse_proxy.enable) { + host.reverse_proxy.subdomains.${config.services.forgejo.subdomain} = { + target = "http://localhost:${toString httpPort}"; + }; + + networking.firewall.allowedTCPPorts = [ + config.services.forgejo.settings.server.SSH_LISTEN_PORT + ]; + }; +} diff --git a/modules/nixos-modules/server/home-assistant.nix b/modules/nixos-modules/server/home-assistant.nix deleted file mode 100644 index baf6683..0000000 --- a/modules/nixos-modules/server/home-assistant.nix +++ /dev/null @@ -1,230 +0,0 @@ -{ - lib, - pkgs, - config, - ... -}: let - configDir = "/var/lib/hass"; - dbUser = "hass"; -in { - options.services.home-assistant = { - subdomain = lib.mkOption { - type = lib.types.str; - description = "subdomain of base domain that home-assistant will be hosted at"; - default = "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 = configDir; - 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://@/${dbUser}"; - "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" - ]; - } - (lib.mkIf (config.services.home-assistant.extensions.sonos.enable) { - services.home-assistant.extraComponents = ["sonos"]; - networking.firewall.allowedTCPPorts = [ - config.services.home-assistant.extensions.sonos.port - ]; - }) - (lib.mkIf (config.services.home-assistant.extensions.jellyfin.enable) { - services.home-assistant.extraComponents = ["jellyfin"]; - # TODO: configure port, address, and login information here - }) - (lib.mkIf (config.services.home-assistant.extensions.wyoming.enable) { - services.home-assistant.extraComponents = ["wyoming"]; - services.wyoming.enable = true; - }) - (lib.mkIf (config.services.home-assistant.database == "postgres") { - host = { - postgres = { - enable = true; - extraUsers = { - ${dbUser} = { - isClient = true; - createUser = true; - }; - }; - extraDatabases = { - ${dbUser} = { - name = dbUser; - }; - }; - }; - }; - - services.home-assistant = { - extraPackages = python3Packages: - with python3Packages; [ - psycopg2 - ]; - }; - - systemd.services.home-assistant = { - requires = [ - config.systemd.services.postgresql.name - ]; - }; - }) - (lib.mkIf config.host.reverse_proxy.enable { - host = { - reverse_proxy.subdomains.${config.services.home-assistant.subdomain} = { - target = "http://localhost:${toString config.services.home-assistant.config.http.server_port}"; - - websockets.enable = true; - forwardHeaders.enable = true; - - extraConfig = '' - add_header Upgrade $http_upgrade; - add_header Connection \"upgrade\"; - - proxy_buffering off; - - proxy_read_timeout 90; - ''; - }; - }; - }) - (lib.mkIf config.services.fail2ban.enable { - environment.etc = { - "fail2ban/filter.d/hass.local".text = lib.mkIf config.services.home-assistant.enable ( - pkgs.lib.mkDefault (pkgs.lib.mkAfter '' - [INCLUDES] - before = common.conf - - [Definition] - failregex = ^%(__prefix_line)s.*Login attempt or request with invalid authentication from .*$ - - ignoreregex = - - [Init] - datepattern = ^%%Y-%%m-%%d %%H:%%M:%%S - '') - ); - }; - - services.fail2ban = { - jails = { - home-assistant-iptables.settings = lib.mkIf config.services.home-assistant.enable { - 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; - }; - }; - }; - }) - (lib.mkIf config.host.impermanence.enable { - assertions = [ - { - assertion = config.services.home-assistant.configDir == configDir; - message = "home assistant config directory does not match persistence"; - } - ]; - environment.persistence."/persist/system/root" = { - enable = true; - hideMounts = true; - directories = [ - { - directory = configDir; - user = "hass"; - group = "hass"; - } - ]; - }; - }) - ]); -} diff --git a/modules/nixos-modules/server/home-assistant/database.nix b/modules/nixos-modules/server/home-assistant/database.nix new file mode 100644 index 0000000..0ac8002 --- /dev/null +++ b/modules/nixos-modules/server/home-assistant/database.nix @@ -0,0 +1,56 @@ +{ + lib, + config, + ... +}: let + dbUser = "hass"; +in { + config = lib.mkIf config.services.home-assistant.enable ( + lib.mkMerge [ + { + host = { + postgres = { + enable = true; + }; + }; + + assertions = [ + { + assertion = config.services.home-assistant.database == "postgres"; + message = "Home Assistant database type must be postgres"; + } + ]; + } + (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 + ]; + }; + + systemd.services.home-assistant = { + requires = [ + config.systemd.services.postgresql.name + ]; + }; + }) + ] + ); +} diff --git a/modules/nixos-modules/server/home-assistant/default.nix b/modules/nixos-modules/server/home-assistant/default.nix new file mode 100644 index 0000000..6edf0c0 --- /dev/null +++ b/modules/nixos-modules/server/home-assistant/default.nix @@ -0,0 +1,118 @@ +{ + lib, + config, + ... +}: { + imports = [ + ./proxy.nix + ./database.nix + ./fail2ban.nix + ./impermanence.nix + ./extensions + ]; + + options.services.home-assistant = { + subdomain = lib.mkOption { + type = lib.types.str; + description = "subdomain of base domain that home-assistant will be hosted at"; + default = "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" + ]; + } + ]); +} diff --git a/modules/nixos-modules/server/home-assistant/extensions/default.nix b/modules/nixos-modules/server/home-assistant/extensions/default.nix new file mode 100644 index 0000000..9ef84a3 --- /dev/null +++ b/modules/nixos-modules/server/home-assistant/extensions/default.nix @@ -0,0 +1,12 @@ +{ + config, + lib, + pkgs, + ... +}: { + imports = [ + ./sonos.nix + ./jellyfin.nix + ./wyoming.nix + ]; +} diff --git a/modules/nixos-modules/server/home-assistant/extensions/jellyfin.nix b/modules/nixos-modules/server/home-assistant/extensions/jellyfin.nix new file mode 100644 index 0000000..29af274 --- /dev/null +++ b/modules/nixos-modules/server/home-assistant/extensions/jellyfin.nix @@ -0,0 +1,9 @@ +{ + lib, + config, + ... +}: +lib.mkIf (config.services.home-assistant.extensions.jellyfin.enable) { + services.home-assistant.extraComponents = ["jellyfin"]; + # TODO: configure port, address, and login information here +} diff --git a/modules/nixos-modules/server/home-assistant/extensions/sonos.nix b/modules/nixos-modules/server/home-assistant/extensions/sonos.nix new file mode 100644 index 0000000..c70649f --- /dev/null +++ b/modules/nixos-modules/server/home-assistant/extensions/sonos.nix @@ -0,0 +1,11 @@ +{ + lib, + config, + ... +}: +lib.mkIf (config.services.home-assistant.extensions.sonos.enable) { + services.home-assistant.extraComponents = ["sonos"]; + networking.firewall.allowedTCPPorts = [ + config.services.home-assistant.extensions.sonos.port + ]; +} diff --git a/modules/nixos-modules/server/home-assistant/extensions/wyoming.nix b/modules/nixos-modules/server/home-assistant/extensions/wyoming.nix new file mode 100644 index 0000000..840d360 --- /dev/null +++ b/modules/nixos-modules/server/home-assistant/extensions/wyoming.nix @@ -0,0 +1,9 @@ +{ + lib, + config, + ... +}: +lib.mkIf (config.services.home-assistant.extensions.wyoming.enable) { + services.home-assistant.extraComponents = ["wyoming"]; + services.wyoming.enable = true; +} diff --git a/modules/nixos-modules/server/home-assistant/fail2ban.nix b/modules/nixos-modules/server/home-assistant/fail2ban.nix new file mode 100644 index 0000000..6ac5900 --- /dev/null +++ b/modules/nixos-modules/server/home-assistant/fail2ban.nix @@ -0,0 +1,39 @@ +{ + lib, + 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 .*$ + + 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; + }; + }; + }; +} diff --git a/modules/nixos-modules/server/home-assistant/impermanence.nix b/modules/nixos-modules/server/home-assistant/impermanence.nix new file mode 100644 index 0000000..8c056a1 --- /dev/null +++ b/modules/nixos-modules/server/home-assistant/impermanence.nix @@ -0,0 +1,26 @@ +{ + lib, + config, + ... +}: let + configDir = "/var/lib/hass"; +in + lib.mkIf (config.host.impermanence.enable && config.services.home-assistant.enable) { + assertions = [ + { + assertion = config.services.home-assistant.configDir == configDir; + message = "home assistant config directory does not match persistence"; + } + ]; + environment.persistence."/persist/system/root" = { + enable = true; + hideMounts = true; + directories = [ + { + directory = configDir; + user = "hass"; + group = "hass"; + } + ]; + }; + } diff --git a/modules/nixos-modules/server/home-assistant/proxy.nix b/modules/nixos-modules/server/home-assistant/proxy.nix new file mode 100644 index 0000000..63396b5 --- /dev/null +++ b/modules/nixos-modules/server/home-assistant/proxy.nix @@ -0,0 +1,24 @@ +{ + lib, + 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}"; + + websockets.enable = true; + forwardHeaders.enable = true; + + extraConfig = '' + add_header Upgrade $http_upgrade; + add_header Connection \"upgrade\"; + + proxy_buffering off; + + proxy_read_timeout 90; + ''; + }; + }; +} diff --git a/modules/nixos-modules/server/immich.nix b/modules/nixos-modules/server/immich.nix deleted file mode 100644 index fa376e4..0000000 --- a/modules/nixos-modules/server/immich.nix +++ /dev/null @@ -1,99 +0,0 @@ -{ - lib, - config, - pkgs, - ... -}: let - mediaLocation = "/var/lib/immich"; -in { - options.services.immich = { - subdomain = lib.mkOption { - type = lib.types.str; - description = "subdomain of base domain that immich will be hosted at"; - default = "immich"; - }; - }; - - config = lib.mkIf config.services.immich.enable (lib.mkMerge [ - { - host = { - postgres = { - enable = true; - extraUsers = { - ${config.services.immich.database.user} = { - isClient = true; - }; - }; - }; - }; - - networking.firewall.interfaces.${config.services.tailscale.interfaceName} = { - allowedUDPPorts = [ - config.services.immich.port - ]; - allowedTCPPorts = [ - config.services.immich.port - ]; - }; - } - (lib.mkIf config.host.reverse_proxy.enable { - host = { - reverse_proxy.subdomains.${config.services.immich.subdomain} = { - target = "http://localhost:${toString config.services.immich.port}"; - - websockets.enable = true; - forwardHeaders.enable = true; - - 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; - ''; - }; - }; - }) - (lib.mkIf config.services.fail2ban.enable { - environment.etc = { - "fail2ban/filter.d/immich.local".text = lib.mkIf config.services.immich.enable ( - pkgs.lib.mkDefault (pkgs.lib.mkAfter '' - [Definition] - failregex = immich-server.*Failed login attempt for user.+from ip address\s? - journalmatch = CONTAINER_TAG=immich-server - '') - ); - }; - - services.fail2ban = { - jails = { - immich-iptables.settings = lib.mkIf config.services.immich.enable { - enabled = true; - filter = "immich"; - backend = "systemd"; - }; - }; - }; - }) - (lib.mkIf config.host.impermanence.enable { - assertions = [ - { - assertion = config.services.immich.mediaLocation == mediaLocation; - message = "immich media location does not match persistence"; - } - ]; - environment.persistence."/persist/system/root" = { - directories = [ - { - directory = mediaLocation; - user = "immich"; - group = "immich"; - } - ]; - }; - }) - ]); -} diff --git a/modules/nixos-modules/server/immich/database.nix b/modules/nixos-modules/server/immich/database.nix new file mode 100644 index 0000000..74b1aaa --- /dev/null +++ b/modules/nixos-modules/server/immich/database.nix @@ -0,0 +1,26 @@ +{ + lib, + 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; + }; + }; + }; + }; + }) + ]); +} diff --git a/modules/nixos-modules/server/immich/default.nix b/modules/nixos-modules/server/immich/default.nix new file mode 100644 index 0000000..9d782f0 --- /dev/null +++ b/modules/nixos-modules/server/immich/default.nix @@ -0,0 +1,28 @@ +{lib, ...}: { + imports = [ + ./proxy.nix + ./database.nix + ./fail2ban.nix + ./impermanence.nix + ]; + + options.services.immich = { + subdomain = lib.mkOption { + type = lib.types.str; + description = "subdomain of base domain that immich will be hosted at"; + default = "immich"; + }; + }; + + # NOTE: This shouldn't be needed now that we are out of testing + # config = lib.mkIf config.services.immich.enable { + # networking.firewall.interfaces.${config.services.tailscale.interfaceName} = { + # allowedUDPPorts = [ + # config.services.immich.port + # ]; + # allowedTCPPorts = [ + # config.services.immich.port + # ]; + # }; + # }; +} diff --git a/modules/nixos-modules/server/immich/fail2ban.nix b/modules/nixos-modules/server/immich/fail2ban.nix new file mode 100644 index 0000000..c9ec87b --- /dev/null +++ b/modules/nixos-modules/server/immich/fail2ban.nix @@ -0,0 +1,26 @@ +{ + lib, + config, + pkgs, + ... +}: { + config = lib.mkIf (config.services.fail2ban.enable && config.services.immich.enable) { + environment.etc = { + "fail2ban/filter.d/immich.local".text = pkgs.lib.mkDefault (pkgs.lib.mkAfter '' + [Definition] + failregex = immich-server.*Failed login attempt for user.+from ip address\s? + journalmatch = CONTAINER_TAG=immich-server + ''); + }; + + services.fail2ban = { + jails = { + immich-iptables.settings = { + enabled = true; + filter = "immich"; + backend = "systemd"; + }; + }; + }; + }; +} diff --git a/modules/nixos-modules/server/immich/impermanence.nix b/modules/nixos-modules/server/immich/impermanence.nix new file mode 100644 index 0000000..f63d178 --- /dev/null +++ b/modules/nixos-modules/server/immich/impermanence.nix @@ -0,0 +1,25 @@ +{ + lib, + config, + ... +}: let + mediaLocation = "/var/lib/immich"; +in { + config = lib.mkIf (config.services.immich.enable && config.host.impermanence.enable) { + assertions = [ + { + assertion = config.services.immich.mediaLocation == mediaLocation; + message = "immich media location does not match persistence"; + } + ]; + environment.persistence."/persist/system/root" = { + directories = [ + { + directory = mediaLocation; + user = "immich"; + group = "immich"; + } + ]; + }; + }; +} diff --git a/modules/nixos-modules/server/immich/proxy.nix b/modules/nixos-modules/server/immich/proxy.nix new file mode 100644 index 0000000..9d8790a --- /dev/null +++ b/modules/nixos-modules/server/immich/proxy.nix @@ -0,0 +1,27 @@ +{ + lib, + config, + ... +}: { + 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}"; + + websockets.enable = true; + forwardHeaders.enable = true; + + 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; + ''; + }; + }; + }; +} diff --git a/modules/nixos-modules/server/jellyfin.nix b/modules/nixos-modules/server/jellyfin.nix deleted file mode 100644 index 85c870f..0000000 --- a/modules/nixos-modules/server/jellyfin.nix +++ /dev/null @@ -1,147 +0,0 @@ -{ - lib, - pkgs, - config, - ... -}: let - jellyfinPort = 8096; - dlanPort = 1900; - jellyfin_data_directory = "/var/lib/jellyfin"; - jellyfin_cache_directory = "/var/cache/jellyfin"; -in { - options.services.jellyfin = { - subdomain = lib.mkOption { - type = lib.types.str; - description = "subdomain of base domain that jellyfin will be hosted at"; - default = "jellyfin"; - }; - extraSubdomains = lib.mkOption { - type = lib.types.listOf lib.types.str; - description = "ex subdomain of base domain that jellyfin will be hosted at"; - default = []; - }; - 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 ( - lib.mkMerge [ - { - 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::-" - ]; - } - (lib.mkIf config.host.reverse_proxy.enable { - host.reverse_proxy.subdomains.jellyfin = { - target = "http://localhost:${toString jellyfinPort}"; - - 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; - ''; - }; - }) - (lib.mkIf config.services.fail2ban.enable { - environment.etc = { - "fail2ban/filter.d/jellyfin.local".text = ( - pkgs.lib.mkDefault (pkgs.lib.mkAfter '' - [Definition] - failregex = "^.*Authentication request for .* has been denied \\\(IP: \"\"\\\)\\\." - '') - ); - }; - - services.fail2ban = { - jails = { - jellyfin-iptables.settings = { - enabled = true; - filter = "jellyfin"; - action = ''iptables-multiport[name=HTTP, port="http,https"]''; - logpath = "${config.services.jellyfin.dataDir}/log/*.log"; - backend = "auto"; - findtime = 600; - bantime = 600; - maxretry = 5; - }; - }; - }; - }) - (lib.mkIf config.host.impermanence.enable { - fileSystems."/persist/system/jellyfin".neededForBoot = true; - - host.storage.pool.extraDatasets = { - # sops age key needs to be available to pre persist for user generation - "persist/system/jellyfin" = { - type = "zfs_fs"; - mountpoint = "/persist/system/jellyfin"; - options = { - atime = "off"; - relatime = "off"; - canmount = "on"; - }; - }; - }; - - assertions = [ - { - assertion = config.services.jellyfin.dataDir == jellyfin_data_directory; - message = "jellyfin data directory does not match persistence"; - } - { - assertion = config.services.jellyfin.cacheDir == jellyfin_cache_directory; - message = "jellyfin cache directory does not match persistence"; - } - ]; - - environment.persistence = { - "/persist/system/root" = { - directories = [ - { - directory = jellyfin_data_directory; - user = "jellyfin"; - group = "jellyfin"; - } - { - directory = jellyfin_cache_directory; - user = "jellyfin"; - group = "jellyfin"; - } - ]; - }; - - "/persist/system/jellyfin" = { - enable = true; - hideMounts = true; - directories = [ - { - directory = config.services.jellyfin.media_directory; - user = "jellyfin"; - group = "jellyfin_media"; - mode = "1770"; - } - ]; - }; - }; - }) - ] - ); -} diff --git a/modules/nixos-modules/server/jellyfin/default.nix b/modules/nixos-modules/server/jellyfin/default.nix new file mode 100644 index 0000000..238ce3a --- /dev/null +++ b/modules/nixos-modules/server/jellyfin/default.nix @@ -0,0 +1,48 @@ +{ + lib, + pkgs, + config, + ... +}: let + jellyfinPort = 8096; + dlanPort = 1900; +in { + imports = [ + ./proxy.nix + ./fail2ban.nix + ./impermanence.nix + ]; + + options.services.jellyfin = { + subdomain = lib.mkOption { + type = lib.types.str; + description = "subdomain of base domain that jellyfin will be hosted at"; + default = "jellyfin"; + }; + extraSubdomains = lib.mkOption { + type = lib.types.listOf lib.types.str; + description = "ex subdomain of base domain that jellyfin will be hosted at"; + default = []; + }; + 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::-" + ]; + }; +} diff --git a/modules/nixos-modules/server/jellyfin/fail2ban.nix b/modules/nixos-modules/server/jellyfin/fail2ban.nix new file mode 100644 index 0000000..ba8d8ba --- /dev/null +++ b/modules/nixos-modules/server/jellyfin/fail2ban.nix @@ -0,0 +1,32 @@ +{ + lib, + pkgs, + config, + ... +}: { + config = lib.mkIf (config.services.jellyfin.enable && config.services.fail2ban.enable) { + environment.etc = { + "fail2ban/filter.d/jellyfin.local".text = ( + pkgs.lib.mkDefault (pkgs.lib.mkAfter '' + [Definition] + failregex = "^.*Authentication request for .* has been denied \\\\\\(IP: \\\"\\\"\\\\\\)\\\\\\." + '') + ); + }; + + services.fail2ban = { + jails = { + jellyfin-iptables.settings = { + enabled = true; + filter = "jellyfin"; + action = ''iptables-multiport[name=HTTP, port="http,https"]''; + logpath = "${config.services.jellyfin.dataDir}/log/*.log"; + backend = "auto"; + findtime = 600; + bantime = 600; + maxretry = 5; + }; + }; + }; + }; +} diff --git a/modules/nixos-modules/server/jellyfin/impermanence.nix b/modules/nixos-modules/server/jellyfin/impermanence.nix new file mode 100644 index 0000000..e0b3b5d --- /dev/null +++ b/modules/nixos-modules/server/jellyfin/impermanence.nix @@ -0,0 +1,66 @@ +{ + lib, + config, + ... +}: let + jellyfin_data_directory = "/var/lib/jellyfin"; + jellyfin_cache_directory = "/var/cache/jellyfin"; +in { + config = lib.mkIf (config.services.jellyfin.enable && config.host.impermanence.enable) { + fileSystems."/persist/system/jellyfin".neededForBoot = true; + + host.storage.pool.extraDatasets = { + # sops age key needs to be available to pre persist for user generation + "persist/system/jellyfin" = { + type = "zfs_fs"; + mountpoint = "/persist/system/jellyfin"; + options = { + atime = "off"; + relatime = "off"; + canmount = "on"; + }; + }; + }; + + assertions = [ + { + assertion = config.services.jellyfin.dataDir == jellyfin_data_directory; + message = "jellyfin data directory does not match persistence"; + } + { + assertion = config.services.jellyfin.cacheDir == jellyfin_cache_directory; + message = "jellyfin cache directory does not match persistence"; + } + ]; + + environment.persistence = { + "/persist/system/root" = { + directories = [ + { + directory = jellyfin_data_directory; + user = "jellyfin"; + group = "jellyfin"; + } + { + directory = jellyfin_cache_directory; + user = "jellyfin"; + group = "jellyfin"; + } + ]; + }; + + "/persist/system/jellyfin" = { + enable = true; + hideMounts = true; + directories = [ + { + directory = config.services.jellyfin.media_directory; + user = "jellyfin"; + group = "jellyfin_media"; + mode = "1770"; + } + ]; + }; + }; + }; +} diff --git a/modules/nixos-modules/server/jellyfin/proxy.nix b/modules/nixos-modules/server/jellyfin/proxy.nix new file mode 100644 index 0000000..5edb865 --- /dev/null +++ b/modules/nixos-modules/server/jellyfin/proxy.nix @@ -0,0 +1,25 @@ +{ + lib, + config, + ... +}: let + jellyfinPort = 8096; +in { + config = lib.mkIf (config.services.jellyfin.enable && config.host.reverse_proxy.enable) { + host.reverse_proxy.subdomains.jellyfin = { + target = "http://localhost:${toString jellyfinPort}"; + + 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; + ''; + }; + }; +} diff --git a/modules/nixos-modules/server/panoramax.nix b/modules/nixos-modules/server/panoramax.nix deleted file mode 100644 index dd026cd..0000000 --- a/modules/nixos-modules/server/panoramax.nix +++ /dev/null @@ -1,408 +0,0 @@ -{ - config, - lib, - pkgs, - osConfig, - ... -}: -with lib; let - # Database configuration assertions - dbUrlConfigured = config.services.panoramax.database.url != null; - individualDbConfigured = all (x: x != null) [ - config.services.panoramax.database.host - config.services.panoramax.database.port - config.services.panoramax.database.username - config.services.panoramax.database.password - config.services.panoramax.database.name - ]; - - envContent = '' - # Panoramax Configuration - FLASK_APP=geovisio - ${ - if dbUrlConfigured - then "DB_URL=${config.services.panoramax.database.url}" - else '' - DB_HOST=${config.services.panoramax.database.host} - DB_PORT=${toString config.services.panoramax.database.port} - DB_USERNAME=${config.services.panoramax.database.username} - DB_PASSWORD=${config.services.panoramax.database.password} - DB_NAME=${config.services.panoramax.database.name} - '' - } - ${optionalString (config.services.panoramax.storage.fsUrl != null) "FS_URL=${config.services.panoramax.storage.fsUrl}"} - ${optionalString (config.services.panoramax.infrastructure.nbProxies != null) "INFRA_NB_PROXIES=${toString config.services.panoramax.infrastructure.nbProxies}"} - ${optionalString (config.services.panoramax.flask.secretKey != null) "FLASK_SECRET_KEY=${config.services.panoramax.flask.secretKey}"} - ${optionalString (config.services.panoramax.flask.sessionCookieDomain != null) "FLASK_SESSION_COOKIE_DOMAIN=${config.services.panoramax.flask.sessionCookieDomain}"} - ${optionalString (config.services.panoramax.api.pictures.licenseSpdxId != null) "API_PICTURES_LICENSE_SPDX_ID=${config.services.panoramax.api.pictures.licenseSpdxId}"} - ${optionalString (config.services.panoramax.api.pictures.licenseUrl != null) "API_PICTURES_LICENSE_URL=${config.services.panoramax.api.pictures.licenseUrl}"} - ${optionalString (config.services.panoramax.port != null) "PORT=${toString config.services.panoramax.port}"} - ${optionalString (config.services.panoramax.sgblur.enable) "SGBLUR_API_URL=${config.services.panoramax.sgblur.url}"} - ${concatStringsSep "\n" (mapAttrsToList (name: value: "${name}=${value}") config.services.panoramax.extraEnvironment)} - ''; - - envFile = pkgs.writeText "panoramax.env" envContent; -in { - options.services.panoramax = { - enable = lib.mkEnableOption "panoramax"; - - package = lib.mkOption { - type = lib.types.package; - default = pkgs.panoramax; - description = "The panoramax package to use"; - }; - - subdomain = lib.mkOption { - type = lib.types.str; - description = "subdomain of base domain that panoramax will be hosted at"; - default = "panoramax"; - }; - - database = { - createDB = mkOption { - type = types.bool; - default = true; - description = "Whether to automatically create the database and user"; - }; - - url = mkOption { - type = types.nullOr types.str; - default = null; - description = '' - Complete database URL connection string (e.g., "postgresql://user:password@host:port/dbname"). - If provided, individual database options (host, port, username, password, name) are ignored. - ''; - }; - - port = mkOption { - type = types.nullOr types.port; - default = 5432; - description = "Database port (ignored if database.url is set)"; - }; - - host = mkOption { - type = types.nullOr types.str; - default = "localhost"; - description = "Database host (ignored if database.url is set)"; - }; - - username = mkOption { - type = types.nullOr types.str; - default = "panoramax"; - description = "Database username (ignored if database.url is set)"; - }; - - password = mkOption { - type = types.nullOr types.str; - default = null; - description = "Database password (ignored if database.url is set)"; - }; - - name = mkOption { - type = types.str; - default = "panoramax"; - description = "Database name (ignored if database.url is set)"; - }; - }; - - sgblur = { - enable = mkOption { - type = types.bool; - default = false; - description = "Whether to enable sgblur integration for face and license plate blurring"; - }; - - package = mkOption { - type = types.package; - default = pkgs.sgblur; - description = "The sgblur package to use"; - }; - - port = mkOption { - type = types.port; - default = 8080; - description = "Port for the sgblur service"; - }; - - host = mkOption { - type = types.str; - default = "127.0.0.1"; - description = "Host to bind the sgblur service to"; - }; - - url = mkOption { - type = types.str; - default = "http://127.0.0.1:8080"; - description = "URL where sgblur service is accessible"; - }; - }; - - port = mkOption { - type = types.nullOr types.port; - default = 5000; - description = "Port for the Panoramax service"; - }; - - host = mkOption { - type = types.str; - default = "127.0.0.1"; - description = "Host to bind the Panoramax service to"; - }; - - urlScheme = mkOption { - type = types.enum ["http" "https"]; - default = "https"; - description = "URL scheme for the application"; - }; - - storage = { - fsUrl = mkOption { - type = types.nullOr types.str; - default = "/var/lib/panoramax/storage"; - description = "File system URL for storage"; - }; - }; - - infrastructure = { - nbProxies = mkOption { - type = types.nullOr types.int; - default = 1; - description = "Number of proxies in front of the application"; - }; - }; - - flask = { - secretKey = mkOption { - type = types.nullOr types.str; - default = null; - description = "Flask secret key for session security"; - }; - - sessionCookieDomain = mkOption { - type = types.nullOr types.str; - default = null; - description = "Flask session cookie domain"; - }; - }; - - api = { - pictures = { - licenseSpdxId = mkOption { - type = types.nullOr types.str; - default = null; - description = "SPDX license identifier for API pictures"; - }; - - licenseUrl = mkOption { - type = types.nullOr types.str; - default = null; - description = "License URL for API pictures"; - }; - }; - }; - - extraEnvironment = mkOption { - type = types.attrsOf types.str; - default = {}; - description = "Additional environment variables"; - example = { - CUSTOM_SETTING = "value"; - DEBUG = "true"; - }; - }; - }; - - config = lib.mkIf config.services.panoramax.enable ( - lib.mkMerge [ - { - environment.systemPackages = with pkgs; - [ - config.services.panoramax.package - python3Packages.waitress - ] - ++ optionals config.services.panoramax.sgblur.enable [ - config.services.panoramax.sgblur.package - ]; - - systemd.services.panoramax = { - description = "Panoramax Service"; - after = ["network.target"]; - wantedBy = ["multi-user.target"]; - serviceConfig = { - ExecStart = "${pkgs.python3Packages.waitress}/bin/waitress-serve --env-file=${envFile} --host=${config.services.panoramax.host} --port=${toString config.services.panoramax.port} --url-scheme=${config.services.panoramax.urlScheme} --call geovisio:create_app"; - Restart = "always"; - User = "panoramax"; - Group = "panoramax"; - WorkingDirectory = "/var/lib/panoramax"; - Environment = "PYTHONPATH=${config.services.panoramax.package}/lib/python3.11/site-packages"; - }; - }; - - users.users.panoramax = { - isSystemUser = true; - group = "panoramax"; - home = "/var/lib/panoramax"; - createHome = true; - }; - - users.groups.panoramax = {}; - - systemd.tmpfiles.rules = [ - "d /var/lib/panoramax 0755 panoramax panoramax -" - "d ${config.services.panoramax.storage.fsUrl} 0755 panoramax panoramax -" - ]; - - assertions = [ - { - assertion = dbUrlConfigured || individualDbConfigured; - message = '' - Panoramax database configuration requires either: - - A complete database URL (services.panoramax.database.url), OR - - All individual database options (host, port, username, password, name) - - Currently configured: - - database.url: ${ - if dbUrlConfigured - then "✓ configured" - else "✗ not configured" - } - - individual options: ${ - if individualDbConfigured - then "✓ all configured" - else "✗ some missing" - } - ''; - } - { - assertion = !config.services.panoramax.database.createDB || config.services.panoramax.database.url == null || (lib.hasPrefix "/run/" config.services.panoramax.database.url || lib.hasPrefix "unix:" config.services.panoramax.database.url || lib.hasPrefix "/" config.services.panoramax.database.host); - message = '' - Panoramax createDB option can only be used with socket connections when a database URL is provided. - Socket connections are identified by: - - URLs starting with "unix:" - - URLs starting with "/run/" - - Host paths starting with "/" - - Current configuration: - - createDB: ${lib.boolToString config.services.panoramax.database.createDB} - - database.url: ${ - if config.services.panoramax.database.url != null - then config.services.panoramax.database.url - else "not set" - } - - database.host: ${config.services.panoramax.database.host} - ''; - } - ]; - } - ( - lib.mkIf config.services.panoramax.sgblur.enable { - systemd.services.sgblur = { - description = "SGBlur AI-powered face and license plate blurring service"; - after = ["network.target"]; - wantedBy = ["multi-user.target"]; - serviceConfig = { - ExecStart = "${config.services.panoramax.sgblur.package}/bin/uvicorn sgblur.main:app --host ${config.services.panoramax.sgblur.host} --port ${toString config.services.panoramax.sgblur.port}"; - Restart = "always"; - User = "sgblur"; - Group = "sgblur"; - WorkingDirectory = "/var/lib/sgblur"; - Environment = "PYTHONPATH=${config.services.panoramax.sgblur.package}/lib/python3.11/site-packages"; - }; - }; - - users.users.sgblur = { - isSystemUser = true; - group = "sgblur"; - home = "/var/lib/sgblur"; - createHome = true; - }; - - users.groups.sgblur = {}; - - systemd.tmpfiles.rules = [ - "d /var/lib/sgblur 0755 sgblur sgblur -" - ]; - - # Update panoramax service dependencies when sgblur is enabled - systemd.services.panoramax = { - after = ["sgblur.service"]; - wants = ["sgblur.service"]; - }; - } - ) - ( - lib.mkIf config.services.panoramax.database.createDB { - services.postgresql = { - enable = true; - ensureDatabases = [config.services.panoramax.database.name]; - ensureUsers = [ - { - name = config.services.panoramax.database.username; - ensureDBOwnership = true; - ensureClauses.login = true; - } - ]; - extensions = ps: with ps; [postgis]; - settings = { - shared_preload_libraries = ["postgis"]; - }; - }; - - systemd.services.postgresql.serviceConfig.ExecStartPost = let - sqlFile = pkgs.writeText "panoramax-postgis-setup.sql" '' - CREATE EXTENSION IF NOT EXISTS postgis; - CREATE EXTENSION IF NOT EXISTS postgis_topology; - CREATE EXTENSION IF NOT EXISTS fuzzystrmatch; - CREATE EXTENSION IF NOT EXISTS postgis_tiger_geocoder; - - ALTER SCHEMA public OWNER TO ${config.services.panoramax.database.username}; - GRANT ALL ON SCHEMA public TO ${config.services.panoramax.database.username}; - ''; - in [ - '' - ${lib.getExe' config.services.postgresql.package "psql"} -d "${config.services.panoramax.database.name}" -f "${sqlFile}" - '' - ]; - - systemd.services.panoramax = { - after = ["postgresql.service"]; - requires = ["postgresql.service"]; - }; - } - ) - ( - lib.mkIf config.host.reverse_proxy.enable { - host = { - reverse_proxy.subdomains.${config.services.panoramax.subdomain} = { - target = "http://localhost:${toString config.services.panoramax.port}"; - - websockets.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; - ''; - }; - }; - } - ) - ( - lib.mkIf config.services.fail2ban { - # TODO: configure options for fail2ban - } - ) - ( - lib.mkIf osConfig.host.impermanence.enable { - # TODO: configure impermanence for panoramax data - } - ) - ] - ); -} diff --git a/modules/nixos-modules/server/panoramax/default.nix b/modules/nixos-modules/server/panoramax/default.nix new file mode 100644 index 0000000..e506b80 --- /dev/null +++ b/modules/nixos-modules/server/panoramax/default.nix @@ -0,0 +1,340 @@ +{ + config, + lib, + pkgs, + ... +}: +with lib; let + # Database configuration assertions + dbUrlConfigured = config.services.panoramax.database.url != null; + individualDbConfigured = all (x: x != null) [ + config.services.panoramax.database.host + config.services.panoramax.database.port + config.services.panoramax.database.username + config.services.panoramax.database.password + config.services.panoramax.database.name + ]; + + envContent = '' + # Panoramax Configuration + FLASK_APP=geovisio + ${ + if dbUrlConfigured + then "DB_URL=${config.services.panoramax.database.url}" + else '' + DB_HOST=${config.services.panoramax.database.host} + DB_PORT=${toString config.services.panoramax.database.port} + DB_USERNAME=${config.services.panoramax.database.username} + DB_PASSWORD=${config.services.panoramax.database.password} + DB_NAME=${config.services.panoramax.database.name} + '' + } + ${optionalString (config.services.panoramax.storage.fsUrl != null) "FS_URL=${config.services.panoramax.storage.fsUrl}"} + ${optionalString (config.services.panoramax.infrastructure.nbProxies != null) "INFRA_NB_PROXIES=${toString config.services.panoramax.infrastructure.nbProxies}"} + ${optionalString (config.services.panoramax.flask.secretKey != null) "FLASK_SECRET_KEY=${config.services.panoramax.flask.secretKey}"} + ${optionalString (config.services.panoramax.flask.sessionCookieDomain != null) "FLASK_SESSION_COOKIE_DOMAIN=${config.services.panoramax.flask.sessionCookieDomain}"} + ${optionalString (config.services.panoramax.api.pictures.licenseSpdxId != null) "API_PICTURES_LICENSE_SPDX_ID=${config.services.panoramax.api.pictures.licenseSpdxId}"} + ${optionalString (config.services.panoramax.api.pictures.licenseUrl != null) "API_PICTURES_LICENSE_URL=${config.services.panoramax.api.pictures.licenseUrl}"} + ${optionalString (config.services.panoramax.port != null) "PORT=${toString config.services.panoramax.port}"} + ${optionalString (config.services.panoramax.sgblur.enable) "SGBLUR_API_URL=${config.services.panoramax.sgblur.url}"} + ${concatStringsSep "\n" (mapAttrsToList (name: value: "${name}=${value}") config.services.panoramax.extraEnvironment)} + ''; + + envFile = pkgs.writeText "panoramax.env" envContent; +in { + imports = [ + ./proxy.nix + ./fail2ban.nix + ./impermanence.nix + ]; + + options.services.panoramax = { + enable = lib.mkEnableOption "panoramax"; + + package = lib.mkOption { + type = lib.types.package; + default = pkgs.panoramax; + description = "The panoramax package to use"; + }; + + subdomain = lib.mkOption { + type = lib.types.str; + description = "subdomain of base domain that panoramax will be hosted at"; + default = "panoramax"; + }; + + database = { + createDB = mkOption { + type = types.bool; + default = true; + description = "Whether to automatically create the database and user"; + }; + + url = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Complete database URL connection string (e.g., "postgresql://user:password@host:port/dbname"). + If provided, individual database options (host, port, username, password, name) are ignored. + ''; + }; + + port = mkOption { + type = types.nullOr types.port; + default = 5432; + description = "Database port (ignored if database.url is set)"; + }; + + host = mkOption { + type = types.nullOr types.str; + default = "localhost"; + description = "Database host (ignored if database.url is set)"; + }; + + username = mkOption { + type = types.nullOr types.str; + default = "panoramax"; + description = "Database username (ignored if database.url is set)"; + }; + + password = mkOption { + type = types.nullOr types.str; + default = null; + description = "Database password (ignored if database.url is set)"; + }; + + name = mkOption { + type = types.str; + default = "panoramax"; + description = "Database name (ignored if database.url is set)"; + }; + }; + + sgblur = { + enable = mkOption { + type = types.bool; + default = false; + description = "Whether to enable sgblur integration for face and license plate blurring"; + }; + + package = mkOption { + type = types.package; + default = pkgs.sgblur; + description = "The sgblur package to use"; + }; + + port = mkOption { + type = types.port; + default = 8080; + description = "Port for the sgblur service"; + }; + + host = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "Host to bind the sgblur service to"; + }; + + url = mkOption { + type = types.str; + default = "http://127.0.0.1:8080"; + description = "URL where sgblur service is accessible"; + }; + }; + + port = mkOption { + type = types.nullOr types.port; + default = 5000; + description = "Port for the Panoramax service"; + }; + + host = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "Host to bind the Panoramax service to"; + }; + + urlScheme = mkOption { + type = types.enum ["http" "https"]; + default = "https"; + description = "URL scheme for the application"; + }; + + storage = { + fsUrl = mkOption { + type = types.nullOr types.str; + default = "/var/lib/panoramax/storage"; + description = "File system URL for storage"; + }; + }; + + infrastructure = { + nbProxies = mkOption { + type = types.nullOr types.int; + default = 1; + description = "Number of proxies in front of the application"; + }; + }; + + flask = { + secretKey = mkOption { + type = types.nullOr types.str; + default = null; + description = "Flask secret key for session security"; + }; + + sessionCookieDomain = mkOption { + type = types.nullOr types.str; + default = null; + description = "Flask session cookie domain"; + }; + }; + + api = { + pictures = { + licenseSpdxId = mkOption { + type = types.nullOr types.str; + default = null; + description = "SPDX license identifier for API pictures"; + }; + + licenseUrl = mkOption { + type = types.nullOr types.str; + default = null; + description = "License URL for API pictures"; + }; + }; + }; + + extraEnvironment = mkOption { + type = types.attrsOf types.str; + default = {}; + description = "Additional environment variables"; + example = { + CUSTOM_SETTING = "value"; + DEBUG = "true"; + }; + }; + }; + + config = lib.mkIf config.services.panoramax.enable (lib.mkMerge [ + { + environment.systemPackages = with pkgs; + [ + config.services.panoramax.package + python3Packages.waitress + ] + ++ optionals config.services.panoramax.sgblur.enable [ + config.services.panoramax.sgblur.package + ]; + + systemd.services.panoramax = { + description = "Panoramax Service"; + after = ["network.target"]; + wantedBy = ["multi-user.target"]; + serviceConfig = { + ExecStart = "${pkgs.python3Packages.waitress}/bin/waitress-serve --env-file=${envFile} --host=${config.services.panoramax.host} --port=${toString config.services.panoramax.port} --url-scheme=${config.services.panoramax.urlScheme} --call geovisio:create_app"; + Restart = "always"; + User = "panoramax"; + Group = "panoramax"; + WorkingDirectory = "/var/lib/panoramax"; + Environment = "PYTHONPATH=${config.services.panoramax.package}/lib/python3.11/site-packages"; + }; + }; + + users.users.panoramax = { + isSystemUser = true; + group = "panoramax"; + home = "/var/lib/panoramax"; + createHome = true; + }; + + users.groups.panoramax = {}; + + systemd.tmpfiles.rules = [ + "d /var/lib/panoramax 0755 panoramax panoramax -" + "d ${config.services.panoramax.storage.fsUrl} 0755 panoramax panoramax -" + ]; + + assertions = [ + { + assertion = dbUrlConfigured || individualDbConfigured; + message = '' + Panoramax database configuration requires either: + - A complete database URL (services.panoramax.database.url), OR + - All individual database options (host, port, username, password, name) + + Currently configured: + - database.url: ${ + if dbUrlConfigured + then "✓ configured" + else "✗ not configured" + } + - individual options: ${ + if individualDbConfigured + then "✓ all configured" + else "✗ some missing" + } + ''; + } + { + assertion = !config.services.panoramax.database.createDB || config.services.panoramax.database.url == null || (lib.hasPrefix "/run/" config.services.panoramax.database.url || lib.hasPrefix "unix:" config.services.panoramax.database.url || lib.hasPrefix "/" config.services.panoramax.database.host); + message = '' + Panoramax createDB option can only be used with socket connections when a database URL is provided. + Socket connections are identified by: + - URLs starting with "unix:" + - URLs starting with "/run/" + - Host paths starting with "/" + + Current configuration: + - createDB: ${lib.boolToString config.services.panoramax.database.createDB} + - database.url: ${ + if config.services.panoramax.database.url != null + then config.services.panoramax.database.url + else "not set" + } + - database.host: ${config.services.panoramax.database.host} + ''; + } + ]; + } + (lib.mkIf config.services.panoramax.database.createDB { + systemd.services.panoramax = { + after = ["postgresql.service"]; + requires = ["postgresql.service"]; + }; + + services.postgresql = { + enable = true; + ensureDatabases = [config.services.panoramax.database.name]; + ensureUsers = [ + { + name = config.services.panoramax.database.username; + ensureDBOwnership = true; + ensureClauses.login = true; + } + ]; + extensions = ps: with ps; [postgis]; + settings = { + shared_preload_libraries = ["postgis"]; + }; + }; + + systemd.services.postgresql.serviceConfig.ExecStartPost = let + sqlFile = pkgs.writeText "panoramax-postgis-setup.sql" '' + CREATE EXTENSION IF NOT EXISTS postgis; + CREATE EXTENSION IF NOT EXISTS postgis_topology; + CREATE EXTENSION IF NOT EXISTS fuzzystrmatch; + CREATE EXTENSION IF NOT EXISTS postgis_tiger_geocoder; + + ALTER SCHEMA public OWNER TO ${config.services.panoramax.database.username}; + GRANT ALL ON SCHEMA public TO ${config.services.panoramax.database.username}; + ''; + in [ + '' + ${lib.getExe' config.services.postgresql.package "psql"} -d "${config.services.panoramax.database.name}" -f "${sqlFile}" + '' + ]; + }) + ]); +} diff --git a/modules/nixos-modules/server/panoramax/fail2ban.nix b/modules/nixos-modules/server/panoramax/fail2ban.nix new file mode 100644 index 0000000..649b53a --- /dev/null +++ b/modules/nixos-modules/server/panoramax/fail2ban.nix @@ -0,0 +1,11 @@ +{ + lib, + config, + ... +}: { + config = lib.mkIf (config.services.panoramax.enable && config.services.fail2ban.enable) { + # TODO: configure options for fail2ban + # This is a placeholder - panoramax fail2ban configuration would need to be defined + # based on the specific log patterns and security requirements + }; +} diff --git a/modules/nixos-modules/server/panoramax/impermanence.nix b/modules/nixos-modules/server/panoramax/impermanence.nix new file mode 100644 index 0000000..011c322 --- /dev/null +++ b/modules/nixos-modules/server/panoramax/impermanence.nix @@ -0,0 +1,14 @@ +{ + lib, + config, + osConfig, + ... +}: { + config = lib.mkIf (config.services.panoramax.enable && osConfig.host.impermanence.enable) { + # TODO: configure impermanence for panoramax data + # This would typically include directories like: + # - /var/lib/panoramax + # - panoramax storage directories + # - any cache or temporary directories that need to persist + }; +} diff --git a/modules/nixos-modules/server/panoramax/proxy.nix b/modules/nixos-modules/server/panoramax/proxy.nix new file mode 100644 index 0000000..70e3f5b --- /dev/null +++ b/modules/nixos-modules/server/panoramax/proxy.nix @@ -0,0 +1,27 @@ +{ + lib, + config, + ... +}: { + 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}"; + + websockets.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; + ''; + }; + }; + }; +} diff --git a/modules/nixos-modules/server/paperless.nix b/modules/nixos-modules/server/paperless.nix deleted file mode 100644 index 303d742..0000000 --- a/modules/nixos-modules/server/paperless.nix +++ /dev/null @@ -1,113 +0,0 @@ -{ - config, - lib, - pkgs, - ... -}: let - dataDir = "/var/lib/paperless"; -in { - options.services.paperless = { - subdomain = lib.mkOption { - type = lib.types.str; - description = "subdomain of base domain that paperless will be hosted at"; - default = "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 (lib.mkMerge [ - { - host = { - postgres = { - enable = true; - extraUsers = { - ${config.services.paperless.database.user} = { - isClient = true; - createUser = true; - }; - }; - extraDatabases = { - ${config.services.paperless.database.user} = { - name = config.services.paperless.database.user; - }; - }; - }; - }; - 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; - }; - }; - } - (lib.mkIf config.host.reverse_proxy.enable { - host = { - reverse_proxy.subdomains.${config.services.paperless.subdomain} = { - target = "http://${config.services.paperless.address}:${toString config.services.paperless.port}"; - - websockets.enable = true; - forwardHeaders.enable = true; - - extraConfig = '' - # allow large file uploads - client_max_body_size 50000M; - ''; - }; - }; - }) - (lib.mkIf config.services.fail2ban.enable { - environment.etc = { - "fail2ban/filter.d/paperless.local".text = ( - pkgs.lib.mkDefault (pkgs.lib.mkAfter '' - [Definition] - failregex = Login failed for user `.*` from (?:IP|private IP) ``\.$ - ignoreregex = - - '') - ); - }; - - services.fail2ban = { - jails = { - paperless.settings = { - enabled = true; - filter = "paperless"; - action = ''iptables-multiport[name=HTTP, port="http,https"]''; - logpath = "${config.services.paperless.dataDir}/log/*.log"; - backend = "auto"; - findtime = 600; - bantime = 600; - maxretry = 5; - }; - }; - }; - }) - (lib.mkIf config.host.impermanence.enable { - assertions = [ - { - assertion = config.services.paperless.dataDir == dataDir; - message = "paperless data location does not match persistence"; - } - ]; - environment.persistence."/persist/system/root" = { - directories = [ - { - directory = dataDir; - user = "paperless"; - group = "paperless"; - } - ]; - }; - }) - ]); -} diff --git a/modules/nixos-modules/server/paperless/database.nix b/modules/nixos-modules/server/paperless/database.nix new file mode 100644 index 0000000..6f4ce51 --- /dev/null +++ b/modules/nixos-modules/server/paperless/database.nix @@ -0,0 +1,34 @@ +{ + config, + 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; + }; + }; + }; + }; + } + ) + ]); +} diff --git a/modules/nixos-modules/server/paperless/default.nix b/modules/nixos-modules/server/paperless/default.nix new file mode 100644 index 0000000..ec01fef --- /dev/null +++ b/modules/nixos-modules/server/paperless/default.nix @@ -0,0 +1,40 @@ +{ + config, + lib, + ... +}: { + imports = [ + ./proxy.nix + ./database.nix + ./fail2ban.nix + ./impermanence.nix + ]; + + options.services.paperless = { + subdomain = lib.mkOption { + type = lib.types.str; + description = "subdomain of base domain that paperless will be hosted at"; + default = "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; + }; + }; + }; +} diff --git a/modules/nixos-modules/server/paperless/fail2ban.nix b/modules/nixos-modules/server/paperless/fail2ban.nix new file mode 100644 index 0000000..e1a70f9 --- /dev/null +++ b/modules/nixos-modules/server/paperless/fail2ban.nix @@ -0,0 +1,34 @@ +{ + config, + lib, + pkgs, + ... +}: { + config = lib.mkIf (config.services.paperless.enable && config.services.fail2ban.enable) { + environment.etc = { + "fail2ban/filter.d/paperless.local".text = ( + pkgs.lib.mkDefault (pkgs.lib.mkAfter '' + [Definition] + failregex = Login failed for user `.*` from (?:IP|private IP) ``\.$ + ignoreregex = + + '') + ); + }; + + services.fail2ban = { + jails = { + paperless.settings = { + enabled = true; + filter = "paperless"; + action = ''iptables-multiport[name=HTTP, port="http,https"]''; + logpath = "${config.services.paperless.dataDir}/log/*.log"; + backend = "auto"; + findtime = 600; + bantime = 600; + maxretry = 5; + }; + }; + }; + }; +} diff --git a/modules/nixos-modules/server/paperless/impermanence.nix b/modules/nixos-modules/server/paperless/impermanence.nix new file mode 100644 index 0000000..d9e17bd --- /dev/null +++ b/modules/nixos-modules/server/paperless/impermanence.nix @@ -0,0 +1,25 @@ +{ + config, + lib, + ... +}: let + dataDir = "/var/lib/paperless"; +in { + config = lib.mkIf (config.services.paperless.enable && config.host.impermanence.enable) { + assertions = [ + { + assertion = config.services.paperless.dataDir == dataDir; + message = "paperless data location does not match persistence"; + } + ]; + environment.persistence."/persist/system/root" = { + directories = [ + { + directory = dataDir; + user = "paperless"; + group = "paperless"; + } + ]; + }; + }; +} diff --git a/modules/nixos-modules/server/paperless/proxy.nix b/modules/nixos-modules/server/paperless/proxy.nix new file mode 100644 index 0000000..cb0f157 --- /dev/null +++ b/modules/nixos-modules/server/paperless/proxy.nix @@ -0,0 +1,21 @@ +{ + config, + lib, + ... +}: { + 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}"; + + websockets.enable = true; + forwardHeaders.enable = true; + + extraConfig = '' + # allow large file uploads + client_max_body_size 50000M; + ''; + }; + }; + }; +} diff --git a/modules/nixos-modules/server/searx.nix b/modules/nixos-modules/server/searx.nix deleted file mode 100644 index 0e547af..0000000 --- a/modules/nixos-modules/server/searx.nix +++ /dev/null @@ -1,78 +0,0 @@ -{ - config, - lib, - inputs, - ... -}: { - options.services.searx = { - subdomain = lib.mkOption { - type = lib.types.str; - description = "subdomain of base domain that searx will be hosted at"; - default = "searx"; - }; - }; - - config = lib.mkIf config.services.searx.enable ( - lib.mkMerge [ - { - 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" - ]; - }; - }; - } - (lib.mkIf 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}"; - }; - }; - }) - ] - ); -} diff --git a/modules/nixos-modules/server/searx/default.nix b/modules/nixos-modules/server/searx/default.nix new file mode 100644 index 0000000..73ec489 --- /dev/null +++ b/modules/nixos-modules/server/searx/default.nix @@ -0,0 +1,71 @@ +{ + config, + lib, + inputs, + ... +}: { + imports = [ + ./proxy.nix + ]; + + options.services.searx = { + subdomain = lib.mkOption { + type = lib.types.str; + description = "subdomain of base domain that searx will be hosted at"; + default = "searx"; + }; + }; + + 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" + ]; + }; + }; + }; +} diff --git a/modules/nixos-modules/server/searx/proxy.nix b/modules/nixos-modules/server/searx/proxy.nix new file mode 100644 index 0000000..d925918 --- /dev/null +++ b/modules/nixos-modules/server/searx/proxy.nix @@ -0,0 +1,14 @@ +{ + config, + lib, + ... +}: { + 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}"; + }; + }; + }; +}