diff options
Diffstat (limited to 'nixos')
-rw-r--r-- | nixos/doc/manual/release-notes/rl-2305.section.md | 8 | ||||
-rw-r--r-- | nixos/modules/module-list.nix | 4 | ||||
-rw-r--r-- | nixos/modules/programs/neovim.nix | 47 | ||||
-rw-r--r-- | nixos/modules/programs/tmux.nix | 5 | ||||
-rw-r--r-- | nixos/modules/services/backup/borgmatic.nix | 3 | ||||
-rw-r--r-- | nixos/modules/services/home-automation/esphome.nix | 136 | ||||
-rw-r--r-- | nixos/modules/services/misc/pufferpanel.nix | 176 | ||||
-rw-r--r-- | nixos/modules/services/networking/ivpn.nix | 51 | ||||
-rw-r--r-- | nixos/modules/services/networking/wstunnel.nix | 4 | ||||
-rw-r--r-- | nixos/modules/services/system/cachix-watch-store.nix | 6 | ||||
-rw-r--r-- | nixos/modules/services/web-apps/monica.nix | 468 | ||||
-rw-r--r-- | nixos/modules/services/web-apps/plausible.nix | 14 | ||||
-rw-r--r-- | nixos/tests/all-tests.nix | 3 | ||||
-rw-r--r-- | nixos/tests/esphome.nix | 41 | ||||
-rw-r--r-- | nixos/tests/pufferpanel.nix | 74 | ||||
-rw-r--r-- | nixos/tests/web-apps/monica.nix | 33 |
16 files changed, 1038 insertions, 35 deletions
diff --git a/nixos/doc/manual/release-notes/rl-2305.section.md b/nixos/doc/manual/release-notes/rl-2305.section.md index 31bf126787758..10224db34288c 100644 --- a/nixos/doc/manual/release-notes/rl-2305.section.md +++ b/nixos/doc/manual/release-notes/rl-2305.section.md @@ -63,6 +63,8 @@ In addition to numerous new and upgraded packages, this release has the followin - [opensearch](https://opensearch.org), a search server alternative to Elasticsearch. Available as [services.opensearch](options.html#opt-services.opensearch.enable). +- [monica](https://www.monicahq.com), an open source personal CRM. Available as [services.monica](options.html#opt-services.monica.enable). + - [authelia](https://www.authelia.com/), is an open-source authentication and authorization server. Available under [services.authelia](options.html#opt-services.authelia.enable). - [goeland](https://github.com/slurdge/goeland), an alternative to rss2email written in golang with many filters. Available as [services.goeland](#opt-services.goeland.enable). @@ -73,6 +75,8 @@ In addition to numerous new and upgraded packages, this release has the followin - [atuin](https://github.com/ellie/atuin), a sync server for shell history. Available as [services.atuin](#opt-services.atuin.enable). +- [esphome](https://esphome.io), a dashboard to configure ESP8266/ESP32 devices for use with Home Automation systems. Available as [services.esphome](#opt-services.esphome.enable). + - [networkd-dispatcher](https://gitlab.com/craftyguy/networkd-dispatcher), a dispatcher service for systemd-networkd connection status changes. Available as [services.networkd-dispatcher](#opt-services.networkd-dispatcher.enable). - [mmsd](https://gitlab.com/kop316/mmsd), a lower level daemon that transmits and recieves MMSes. Available as [services.mmsd](#opt-services.mmsd.enable). @@ -87,6 +91,8 @@ In addition to numerous new and upgraded packages, this release has the followin - [ulogd](https://www.netfilter.org/projects/ulogd/index.html), a userspace logging daemon for netfilter/iptables related logging. Available as [services.ulogd](options.html#opt-services.ulogd.enable). +- [PufferPanel](https://pufferpanel.com), game server management panel designed to be easy to use. Available as [services.pufferpanel](#opt-services.pufferpanel.enable). + - [jellyseerr](https://github.com/Fallenbagel/jellyseerr), a web-based requests manager for Jellyfin, forked from Overseerr. Available as [services.jellyseerr](#opt-services.jellyseerr.enable). - [photoprism](https://photoprism.app/), a AI-Powered Photos App for the Decentralized Web. Available as [services.photoprism](options.html#opt-services.photoprism.enable). @@ -113,6 +119,8 @@ In addition to numerous new and upgraded packages, this release has the followin - [hardware.ipu6](#opt-hardware.ipu6.enable) adds support for ipu6 based webcams on intel tiger lake and alder lake. +- [ivpn](https://www.ivpn.net/), a secure, private VPN with fast WireGuard connections. Available as [services.ivpn](#opt-services.ivpn.enable). + ## Backward Incompatibilities {#sec-release-23.05-incompatibilities} <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. --> diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 6eb3e25b6c6fb..f1c459f755708 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -515,6 +515,7 @@ ./services/hardware/usbrelayd.nix ./services/hardware/vdr.nix ./services/hardware/keyd.nix + ./services/home-automation/esphome.nix ./services/home-automation/evcc.nix ./services/home-automation/home-assistant.nix ./services/home-automation/zigbee2mqtt.nix @@ -669,6 +670,7 @@ ./services/misc/polaris.nix ./services/misc/portunus.nix ./services/misc/prowlarr.nix + ./services/misc/pufferpanel.nix ./services/misc/pykms.nix ./services/misc/radarr.nix ./services/misc/readarr.nix @@ -883,6 +885,7 @@ ./services/networking/iscsi/initiator.nix ./services/networking/iscsi/root-initiator.nix ./services/networking/iscsi/target.nix + ./services/networking/ivpn.nix ./services/networking/iwd.nix ./services/networking/jibri/default.nix ./services/networking/jicofo.nix @@ -1182,6 +1185,7 @@ ./services/web-apps/mattermost.nix ./services/web-apps/mediawiki.nix ./services/web-apps/miniflux.nix + ./services/web-apps/monica.nix ./services/web-apps/moodle.nix ./services/web-apps/netbox.nix ./services/web-apps/nextcloud.nix diff --git a/nixos/modules/programs/neovim.nix b/nixos/modules/programs/neovim.nix index 4562e5a2c29b8..3f0e9fc173bdf 100644 --- a/nixos/modules/programs/neovim.nix +++ b/nixos/modules/programs/neovim.nix @@ -4,12 +4,8 @@ with lib; let cfg = config.programs.neovim; - - runtime' = filter (f: f.enable) (attrValues cfg.runtime); - - runtime = pkgs.linkFarm "neovim-runtime" (map (x: { name = "etc/${x.target}"; path = x.source; }) runtime'); - -in { +in +{ options.programs.neovim = { enable = mkOption { type = types.bool; @@ -70,7 +66,7 @@ in { configure = mkOption { type = types.attrs; - default = {}; + default = { }; example = literalExpression '' { customRC = ''' @@ -105,7 +101,7 @@ in { }; runtime = mkOption { - default = {}; + default = { }; example = literalExpression '' { "ftplugin/c.vim".text = "setlocal omnifunc=v:lua.vim.lsp.omnifunc"; } ''; @@ -115,14 +111,15 @@ in { type = with types; attrsOf (submodule ( { name, config, ... }: - { options = { + { + options = { enable = mkOption { type = types.bool; default = true; description = lib.mdDoc '' - Whether this /etc file should be generated. This - option allows specific /etc files to be disabled. + Whether this runtime directory should be generated. This + option allows specific runtime files to be disabled. ''; }; @@ -147,14 +144,9 @@ in { }; - config = { - target = mkDefault name; - source = mkIf (config.text != null) ( - let name' = "neovim-runtime" + baseNameOf name; - in mkDefault (pkgs.writeText name' config.text)); - }; - - })); + config.target = mkDefault name; + } + )); }; }; @@ -165,14 +157,17 @@ in { ]; environment.variables.EDITOR = mkIf cfg.defaultEditor (mkOverride 900 "nvim"); - programs.neovim.finalPackage = pkgs.wrapNeovim cfg.package { - inherit (cfg) viAlias vimAlias withPython3 withNodeJs withRuby; - configure = cfg.configure // { + environment.etc = listToAttrs (attrValues (mapAttrs + (name: value: { + name = "xdg/nvim/${name}"; + value = value // { + target = "xdg/nvim/${value.target}"; + }; + }) + cfg.runtime)); - customRC = (cfg.configure.customRC or "") + '' - set runtimepath^=${runtime}/etc - ''; - }; + programs.neovim.finalPackage = pkgs.wrapNeovim cfg.package { + inherit (cfg) viAlias vimAlias withPython3 withNodeJs withRuby configure; }; }; } diff --git a/nixos/modules/programs/tmux.nix b/nixos/modules/programs/tmux.nix index 4fb9175fb8d21..58c9cbc3ed003 100644 --- a/nixos/modules/programs/tmux.nix +++ b/nixos/modules/programs/tmux.nix @@ -160,7 +160,10 @@ in { default = defaultTerminal; example = "screen-256color"; type = types.str; - description = lib.mdDoc "Set the $TERM variable."; + description = lib.mdDoc '' + Set the $TERM variable. Use tmux-direct if italics or 24bit true color + support is needed. + ''; }; secureSocket = mkOption { diff --git a/nixos/modules/services/backup/borgmatic.nix b/nixos/modules/services/backup/borgmatic.nix index e7cd6ae4bb573..5ee036e68c7bc 100644 --- a/nixos/modules/services/backup/borgmatic.nix +++ b/nixos/modules/services/backup/borgmatic.nix @@ -72,5 +72,8 @@ in cfg.configurations; systemd.packages = [ pkgs.borgmatic ]; + + # Workaround: https://github.com/NixOS/nixpkgs/issues/81138 + systemd.timers.borgmatic.wantedBy = [ "timers.target" ]; }; } diff --git a/nixos/modules/services/home-automation/esphome.nix b/nixos/modules/services/home-automation/esphome.nix new file mode 100644 index 0000000000000..d7dbb6f0b90e3 --- /dev/null +++ b/nixos/modules/services/home-automation/esphome.nix @@ -0,0 +1,136 @@ +{ config, lib, pkgs, ... }: + +let + inherit (lib) + literalExpression + maintainers + mkEnableOption + mkIf + mkOption + mdDoc + types + ; + + cfg = config.services.esphome; + + stateDir = "/var/lib/esphome"; + + esphomeParams = + if cfg.enableUnixSocket + then "--socket /run/esphome/esphome.sock" + else "--address ${cfg.address} --port ${toString cfg.port}"; +in +{ + meta.maintainers = with maintainers; [ oddlama ]; + + options.services.esphome = { + enable = mkEnableOption (mdDoc "esphome"); + + package = mkOption { + type = types.package; + default = pkgs.esphome; + defaultText = literalExpression "pkgs.esphome"; + description = mdDoc "The package to use for the esphome command."; + }; + + enableUnixSocket = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc "Listen on a unix socket `/run/esphome/esphome.sock` instead of the TCP port."; + }; + + address = mkOption { + type = types.str; + default = "localhost"; + description = mdDoc "esphome address"; + }; + + port = mkOption { + type = types.port; + default = 6052; + description = mdDoc "esphome port"; + }; + + openFirewall = mkOption { + default = false; + type = types.bool; + description = mdDoc "Whether to open the firewall for the specified port."; + }; + + allowedDevices = mkOption { + default = ["char-ttyS" "char-ttyUSB"]; + example = ["/dev/serial/by-id/usb-Silicon_Labs_CP2102_USB_to_UART_Bridge_Controller_0001-if00-port0"]; + description = lib.mdDoc '' + A list of device nodes to which {command}`esphome` has access to. + Refer to DeviceAllow in systemd.resource-control(5) for more information. + Beware that if a device is referred to by an absolute path instead of a device category, + it will only allow devices that already are plugged in when the service is started. + ''; + type = types.listOf types.str; + }; + }; + + config = mkIf cfg.enable { + networking.firewall.allowedTCPPorts = mkIf (cfg.openFirewall && !cfg.enableUnixSocket) [cfg.port]; + + systemd.services.esphome = { + description = "ESPHome dashboard"; + after = ["network.target"]; + wantedBy = ["multi-user.target"]; + path = [cfg.package]; + + # platformio fails to determine the home directory when using DynamicUser + environment.PLATFORMIO_CORE_DIR = "${stateDir}/.platformio"; + + serviceConfig = { + ExecStart = "${cfg.package}/bin/esphome dashboard ${esphomeParams} ${stateDir}"; + DynamicUser = true; + User = "esphome"; + Group = "esphome"; + WorkingDirectory = stateDir; + StateDirectory = "esphome"; + StateDirectoryMode = "0750"; + Restart = "on-failure"; + RuntimeDirectory = mkIf cfg.enableUnixSocket "esphome"; + RuntimeDirectoryMode = "0750"; + + # Hardening + CapabilityBoundingSet = ""; + LockPersonality = true; + MemoryDenyWriteExecute = true; + DevicePolicy = "closed"; + DeviceAllow = map (d: "${d} rw") cfg.allowedDevices; + SupplementaryGroups = ["dialout"]; + #NoNewPrivileges = true; # Implied by DynamicUser + PrivateUsers = true; + #PrivateTmp = true; # Implied by DynamicUser + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProcSubset = "pid"; + ProtectSystem = "strict"; + #RemoveIPC = true; # Implied by DynamicUser + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_NETLINK" + "AF_UNIX" + ]; + RestrictNamespaces = false; # Required by platformio for chroot + RestrictRealtime = true; + #RestrictSUIDSGID = true; # Implied by DynamicUser + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "@mount" # Required by platformio for chroot + ]; + UMask = "0077"; + }; + }; + }; +} diff --git a/nixos/modules/services/misc/pufferpanel.nix b/nixos/modules/services/misc/pufferpanel.nix new file mode 100644 index 0000000000000..78ec356469076 --- /dev/null +++ b/nixos/modules/services/misc/pufferpanel.nix @@ -0,0 +1,176 @@ +{ config, pkgs, lib, ... }: +let + cfg = config.services.pufferpanel; +in +{ + options.services.pufferpanel = { + enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = lib.mdDoc '' + Whether to enable PufferPanel game management server. + + Note that [PufferPanel templates] and binaries downloaded by PufferPanel + expect [FHS environment]. It is possible to set {option}`package` option + to use PufferPanel wrapper with FHS environment. For example, to use + `Download Game from Steam` and `Download Java` template operations: + ```Nix + { lib, pkgs, ... }: { + services.pufferpanel = { + enable = true; + extraPackages = with pkgs; [ bash curl gawk gnutar gzip ]; + package = pkgs.buildFHSUserEnv { + name = "pufferpanel-fhs"; + runScript = lib.getExe pkgs.pufferpanel; + targetPkgs = pkgs': with pkgs'; [ icu openssl zlib ]; + }; + }; + } + ``` + + [PufferPanel templates]: https://github.com/PufferPanel/templates + [FHS environment]: https://wikipedia.org/wiki/Filesystem_Hierarchy_Standard + ''; + }; + + package = lib.mkPackageOptionMD pkgs "pufferpanel" { }; + + extraGroups = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + example = [ "podman" ]; + description = lib.mdDoc '' + Additional groups for the systemd service. + ''; + }; + + extraPackages = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = [ ]; + example = lib.literalExpression "[ pkgs.jre ]"; + description = lib.mdDoc '' + Packages to add to the PATH environment variable. Both the {file}`bin` + and {file}`sbin` subdirectories of each package are added. + ''; + }; + + environment = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + example = lib.literalExpression '' + { + PUFFER_WEB_HOST = ":8080"; + PUFFER_DAEMON_SFTP_HOST = ":5657"; + PUFFER_DAEMON_CONSOLE_BUFFER = "1000"; + PUFFER_DAEMON_CONSOLE_FORWARD = "true"; + PUFFER_PANEL_REGISTRATIONENABLED = "false"; + } + ''; + description = lib.mdDoc '' + Environment variables to set for the service. Secrets should be + specified using {option}`environmentFile`. + + Refer to the [PufferPanel source code][] for the list of available + configuration options. Variable name is an upper-cased configuration + entry name with underscores instead of dots, prefixed with `PUFFER_`. + For example, `panel.settings.companyName` entry can be set using + {env}`PUFFER_PANEL_SETTINGS_COMPANYNAME`. + + When running with panel enabled (configured with `PUFFER_PANEL_ENABLE` + environment variable), it is recommended disable registration using + `PUFFER_PANEL_REGISTRATIONENABLED` environment variable (registration is + enabled by default). To create the initial administrator user, run + {command}`pufferpanel --workDir /var/lib/pufferpanel user add --admin`. + + Some options override corresponding settings set via web interface (e.g. + `PUFFER_PANEL_REGISTRATIONENABLED`). Those options can be temporarily + toggled or set in settings but do not persist between restarts. + + [PufferPanel source code]: https://github.com/PufferPanel/PufferPanel/blob/master/config/entries.go + ''; + }; + + environmentFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = lib.mdDoc '' + File to load environment variables from. Loaded variables override + values set in {option}`environment`. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.pufferpanel = { + description = "PufferPanel game management server"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + + path = cfg.extraPackages; + environment = cfg.environment; + + # Note that we export environment variables for service directories if the + # value is not set. An empty environment variable is considered to be set. + # E.g. + # export PUFFER_LOGS=${PUFFER_LOGS-$LOGS_DIRECTORY} + # would set PUFFER_LOGS to $LOGS_DIRECTORY if PUFFER_LOGS environment + # variable is not defined. + script = '' + ${lib.concatLines (lib.mapAttrsToList (name: value: '' + export ${name}="''${${name}-${value}}" + '') { + PUFFER_LOGS = "$LOGS_DIRECTORY"; + PUFFER_DAEMON_DATA_CACHE = "$CACHE_DIRECTORY"; + PUFFER_DAEMON_DATA_SERVERS = "$STATE_DIRECTORY/servers"; + PUFFER_DAEMON_DATA_BINARIES = "$STATE_DIRECTORY/binaries"; + })} + exec ${lib.getExe cfg.package} run --workDir "$STATE_DIRECTORY" + ''; + + serviceConfig = { + Type = "simple"; + Restart = "always"; + + UMask = "0077"; + + SupplementaryGroups = cfg.extraGroups; + + StateDirectory = "pufferpanel"; + StateDirectoryMode = "0700"; + CacheDirectory = "pufferpanel"; + CacheDirectoryMode = "0700"; + LogsDirectory = "pufferpanel"; + LogsDirectoryMode = "0700"; + + EnvironmentFile = cfg.environmentFile; + + # Command "pufferpanel shutdown --pid $MAINPID" sends SIGTERM (code 15) + # to the main process and waits for termination. This is essentially + # KillMode=mixed we are using here. See + # https://freedesktop.org/software/systemd/man/systemd.kill.html#KillMode= + KillMode = "mixed"; + + DynamicUser = true; + ProtectHome = true; + ProtectProc = "invisible"; + ProtectClock = true; + ProtectHostname = true; + ProtectControlGroups = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + PrivateUsers = true; + PrivateDevices = true; + RestrictRealtime = true; + RestrictNamespaces = [ "user" "mnt" ]; # allow buildFHSUserEnv + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; + LockPersonality = true; + DeviceAllow = [ "" ]; + DevicePolicy = "closed"; + CapabilityBoundingSet = [ "" ]; + }; + }; + }; + + meta.maintainers = [ lib.maintainers.tie ]; +} diff --git a/nixos/modules/services/networking/ivpn.nix b/nixos/modules/services/networking/ivpn.nix new file mode 100644 index 0000000000000..6df630c1f1947 --- /dev/null +++ b/nixos/modules/services/networking/ivpn.nix @@ -0,0 +1,51 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.services.ivpn; +in +with lib; +{ + options.services.ivpn = { + enable = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + This option enables iVPN daemon. + This sets {option}`networking.firewall.checkReversePath` to "loose", which might be undesirable for security. + ''; + }; + }; + + config = mkIf cfg.enable { + boot.kernelModules = [ "tun" ]; + + environment.systemPackages = with pkgs; [ ivpn ivpn-service ]; + + # iVPN writes to /etc/iproute2/rt_tables + networking.iproute2.enable = true; + networking.firewall.checkReversePath = "loose"; + + systemd.services.ivpn-service = { + description = "iVPN daemon"; + wantedBy = [ "multi-user.target" ]; + wants = [ "network.target" ]; + after = [ + "network-online.target" + "NetworkManager.service" + "systemd-resolved.service" + ]; + path = [ + # Needed for mount + "/run/wrappers" + ]; + startLimitBurst = 5; + startLimitIntervalSec = 20; + serviceConfig = { + ExecStart = "${pkgs.ivpn-service}/bin/ivpn-service --logging"; + Restart = "always"; + RestartSec = 1; + }; + }; + }; + + meta.maintainers = with maintainers; [ ataraxiasjel ]; +} diff --git a/nixos/modules/services/networking/wstunnel.nix b/nixos/modules/services/networking/wstunnel.nix index 440b617f60a39..067d5df487255 100644 --- a/nixos/modules/services/networking/wstunnel.nix +++ b/nixos/modules/services/networking/wstunnel.nix @@ -294,7 +294,7 @@ let DynamicUser = true; SupplementaryGroups = optional (serverCfg.useACMEHost != null) certConfig.group; PrivateTmp = true; - AmbientCapabilities = optional (serverCfg.listen.port < 1024) [ "CAP_NET_BIND_SERVICE" ]; + AmbientCapabilities = optionals (serverCfg.listen.port < 1024) [ "CAP_NET_BIND_SERVICE" ]; NoNewPrivileges = true; RestrictNamespaces = "uts ipc pid user cgroup"; ProtectSystem = "strict"; @@ -340,7 +340,7 @@ let EnvironmentFile = optional (clientCfg.environmentFile != null) clientCfg.environmentFile; DynamicUser = true; PrivateTmp = true; - AmbientCapabilities = (optional (clientCfg.soMark != null) [ "CAP_NET_ADMIN" ]) ++ (optional ((clientCfg.dynamicToRemote.port or 1024) < 1024 || (any (x: x.local.port < 1024) clientCfg.localToRemote)) [ "CAP_NET_BIND_SERVICE" ]); + AmbientCapabilities = (optionals (clientCfg.soMark != null) [ "CAP_NET_ADMIN" ]) ++ (optionals ((clientCfg.dynamicToRemote.port or 1024) < 1024 || (any (x: x.local.port < 1024) clientCfg.localToRemote)) [ "CAP_NET_BIND_SERVICE" ]); NoNewPrivileges = true; RestrictNamespaces = "uts ipc pid user cgroup"; ProtectSystem = "strict"; diff --git a/nixos/modules/services/system/cachix-watch-store.nix b/nixos/modules/services/system/cachix-watch-store.nix index 85e9509bcc82d..89157b460b9a4 100644 --- a/nixos/modules/services/system/cachix-watch-store.nix +++ b/nixos/modules/services/system/cachix-watch-store.nix @@ -62,7 +62,13 @@ in after = [ "network-online.target" ]; path = [ config.nix.package ]; wantedBy = [ "multi-user.target" ]; + unitConfig = { + # allow to restart indefinitely + StartLimitIntervalSec = 0; + }; serviceConfig = { + # don't put too much stress on the machine when restarting + RestartSec = 1; # we don't want to kill children processes as those are deployments KillMode = "process"; Restart = "on-failure"; diff --git a/nixos/modules/services/web-apps/monica.nix b/nixos/modules/services/web-apps/monica.nix new file mode 100644 index 0000000000000..442044fedb14e --- /dev/null +++ b/nixos/modules/services/web-apps/monica.nix @@ -0,0 +1,468 @@ +{ + config, + lib, + pkgs, + ... +}: +with lib; let + cfg = config.services.monica; + monica = pkgs.monica.override { + dataDir = cfg.dataDir; + }; + db = cfg.database; + mail = cfg.mail; + + user = cfg.user; + group = cfg.group; + + # shell script for local administration + artisan = pkgs.writeScriptBin "monica" '' + #! ${pkgs.runtimeShell} + cd ${monica} + sudo() { + if [[ "$USER" != ${user} ]]; then + exec /run/wrappers/bin/sudo -u ${user} "$@" + else + exec "$@" + fi + } + sudo ${pkgs.php}/bin/php artisan "$@" + ''; + + tlsEnabled = cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME; +in { + options.services.monica = { + enable = mkEnableOption (lib.mdDoc "monica"); + + user = mkOption { + default = "monica"; + description = lib.mdDoc "User monica runs as."; + type = types.str; + }; + + group = mkOption { + default = "monica"; + description = lib.mdDoc "Group monica runs as."; + type = types.str; + }; + + appKeyFile = mkOption { + description = lib.mdDoc '' + A file containing the Laravel APP_KEY - a 32 character long, + base64 encoded key used for encryption where needed. Can be + generated with <code>head -c 32 /dev/urandom | base64</code>. + ''; + example = "/run/keys/monica-appkey"; + type = types.path; + }; + + hostname = lib.mkOption { + type = lib.types.str; + default = + if config.networking.domain != null + then config.networking.fqdn + else config.networking.hostName; + defaultText = lib.literalExpression "config.networking.fqdn"; + example = "monica.example.com"; + description = lib.mdDoc '' + The hostname to serve monica on. + ''; + }; + + appURL = mkOption { + description = lib.mdDoc '' + The root URL that you want to host monica on. All URLs in monica will be generated using this value. + If you change this in the future you may need to run a command to update stored URLs in the database. + Command example: <code>php artisan monica:update-url https://old.example.com https://new.example.com</code> + ''; + default = "http${lib.optionalString tlsEnabled "s"}://${cfg.hostname}"; + defaultText = ''http''${lib.optionalString tlsEnabled "s"}://''${cfg.hostname}''; + example = "https://example.com"; + type = types.str; + }; + + dataDir = mkOption { + description = lib.mdDoc "monica data directory"; + default = "/var/lib/monica"; + type = types.path; + }; + + database = { + host = mkOption { + type = types.str; + default = "localhost"; + description = lib.mdDoc "Database host address."; + }; + port = mkOption { + type = types.port; + default = 3306; + description = lib.mdDoc "Database host port."; + }; + name = mkOption { + type = types.str; + default = "monica"; + description = lib.mdDoc "Database name."; + }; + user = mkOption { + type = types.str; + default = user; + defaultText = lib.literalExpression "user"; + description = lib.mdDoc "Database username."; + }; + passwordFile = mkOption { + type = with types; nullOr path; + default = null; + example = "/run/keys/monica-dbpassword"; + description = lib.mdDoc '' + A file containing the password corresponding to + <option>database.user</option>. + ''; + }; + createLocally = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc "Create the database and database user locally."; + }; + }; + + mail = { + driver = mkOption { + type = types.enum ["smtp" "sendmail"]; + default = "smtp"; + description = lib.mdDoc "Mail driver to use."; + }; + host = mkOption { + type = types.str; + default = "localhost"; + description = lib.mdDoc "Mail host address."; + }; + port = mkOption { + type = types.port; + default = 1025; + description = lib.mdDoc "Mail host port."; + }; + fromName = mkOption { + type = types.str; + default = "monica"; + description = lib.mdDoc "Mail \"from\" name."; + }; + from = mkOption { + type = types.str; + default = "mail@monica.com"; + description = lib.mdDoc "Mail \"from\" email."; + }; + user = mkOption { + type = with types; nullOr str; + default = null; + example = "monica"; + description = lib.mdDoc "Mail username."; + }; + passwordFile = mkOption { + type = with types; nullOr path; + default = null; + example = "/run/keys/monica-mailpassword"; + description = lib.mdDoc '' + A file containing the password corresponding to + <option>mail.user</option>. + ''; + }; + encryption = mkOption { + type = with types; nullOr (enum ["tls"]); + default = null; + description = lib.mdDoc "SMTP encryption mechanism to use."; + }; + }; + + maxUploadSize = mkOption { + type = types.str; + default = "18M"; + example = "1G"; + description = lib.mdDoc "The maximum size for uploads (e.g. images)."; + }; + + poolConfig = mkOption { + type = with types; attrsOf (oneOf [str int bool]); + default = { + "pm" = "dynamic"; + "pm.max_children" = 32; + "pm.start_servers" = 2; + "pm.min_spare_servers" = 2; + "pm.max_spare_servers" = 4; + "pm.max_requests" = 500; + }; + description = lib.mdDoc '' + Options for the monica PHP pool. See the documentation on <literal>php-fpm.conf</literal> + for details on configuration directives. + ''; + }; + + nginx = mkOption { + type = types.submodule ( + recursiveUpdate + (import ../web-servers/nginx/vhost-options.nix {inherit config lib;}) {} + ); + default = {}; + example = '' + { + serverAliases = [ + "monica.''${config.networking.domain}" + ]; + # To enable encryption and let let's encrypt take care of certificate + forceSSL = true; + enableACME = true; + } + ''; + description = lib.mdDoc '' + With this option, you can customize the nginx virtualHost settings. + ''; + }; + + config = mkOption { + type = with types; + attrsOf + (nullOr + (either + (oneOf [ + bool + int + port + path + str + ]) + (submodule { + options = { + _secret = mkOption { + type = nullOr str; + description = lib.mdDoc '' + The path to a file containing the value the + option should be set to in the final + configuration file. + ''; + }; + }; + }))); + default = {}; + example = '' + { + ALLOWED_IFRAME_HOSTS = "https://example.com"; + WKHTMLTOPDF = "/home/user/bins/wkhtmltopdf"; + AUTH_METHOD = "oidc"; + OIDC_NAME = "MyLogin"; + OIDC_DISPLAY_NAME_CLAIMS = "name"; + OIDC_CLIENT_ID = "monica"; + OIDC_CLIENT_SECRET = {_secret = "/run/keys/oidc_secret"}; + OIDC_ISSUER = "https://keycloak.example.com/auth/realms/My%20Realm"; + OIDC_ISSUER_DISCOVER = true; + } + ''; + description = lib.mdDoc '' + monica configuration options to set in the + <filename>.env</filename> file. + + Refer to <link xlink:href="https://github.com/monicahq/monica"/> + for details on supported values. + + Settings containing secret data should be set to an attribute + set containing the attribute <literal>_secret</literal> - a + string pointing to a file containing the value the option + should be set to. See the example to get a better picture of + this: in the resulting <filename>.env</filename> file, the + <literal>OIDC_CLIENT_SECRET</literal> key will be set to the + contents of the <filename>/run/keys/oidc_secret</filename> + file. + ''; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = db.createLocally -> db.user == user; + message = "services.monica.database.user must be set to ${user} if services.monica.database.createLocally is set true."; + } + { + assertion = db.createLocally -> db.passwordFile == null; + message = "services.monica.database.passwordFile cannot be specified if services.monica.database.createLocally is set to true."; + } + ]; + + services.monica.config = { + APP_ENV = "production"; + APP_KEY._secret = cfg.appKeyFile; + APP_URL = cfg.appURL; + DB_HOST = db.host; + DB_PORT = db.port; + DB_DATABASE = db.name; + DB_USERNAME = db.user; + MAIL_DRIVER = mail.driver; + MAIL_FROM_NAME = mail.fromName; + MAIL_FROM = mail.from; + MAIL_HOST = mail.host; + MAIL_PORT = mail.port; + MAIL_USERNAME = mail.user; + MAIL_ENCRYPTION = mail.encryption; + DB_PASSWORD._secret = db.passwordFile; + MAIL_PASSWORD._secret = mail.passwordFile; + APP_SERVICES_CACHE = "/run/monica/cache/services.php"; + APP_PACKAGES_CACHE = "/run/monica/cache/packages.php"; + APP_CONFIG_CACHE = "/run/monica/cache/config.php"; + APP_ROUTES_CACHE = "/run/monica/cache/routes-v7.php"; + APP_EVENTS_CACHE = "/run/monica/cache/events.php"; + SESSION_SECURE_COOKIE = tlsEnabled; + }; + + environment.systemPackages = [artisan]; + + services.mysql = mkIf db.createLocally { + enable = true; + package = mkDefault pkgs.mariadb; + ensureDatabases = [db.name]; + ensureUsers = [ + { + name = db.user; + ensurePermissions = {"${db.name}.*" = "ALL PRIVILEGES";}; + } + ]; + }; + + services.phpfpm.pools.monica = { + inherit user group; + phpOptions = '' + log_errors = on + post_max_size = ${cfg.maxUploadSize} + upload_max_filesize = ${cfg.maxUploadSize} + ''; + settings = { + "listen.mode" = "0660"; + "listen.owner" = user; + "listen.group" = group; + } // cfg.poolConfig; + }; + + services.nginx = { + enable = mkDefault true; + recommendedTlsSettings = true; + recommendedOptimisation = true; + recommendedGzipSettings = true; + recommendedBrotliSettings = true; + recommendedProxySettings = true; + virtualHosts.${cfg.hostname} = mkMerge [ + cfg.nginx + { + root = mkForce "${monica}/public"; + locations = { + "/" = { + index = "index.php"; + tryFiles = "$uri $uri/ /index.php?$query_string"; + }; + "~ \.php$".extraConfig = '' + fastcgi_pass unix:${config.services.phpfpm.pools."monica".socket}; + ''; + "~ \.(js|css|gif|png|ico|jpg|jpeg)$" = { + extraConfig = "expires 365d;"; + }; + }; + } + ]; + }; + + systemd.services.monica-setup = { + description = "Preperation tasks for monica"; + before = ["phpfpm-monica.service"]; + after = optional db.createLocally "mysql.service"; + wantedBy = ["multi-user.target"]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + User = user; + UMask = 077; + WorkingDirectory = "${monica}"; + RuntimeDirectory = "monica/cache"; + RuntimeDirectoryMode = 0700; + }; + path = [pkgs.replace-secret]; + script = let + isSecret = v: isAttrs v && v ? _secret && isString v._secret; + monicaEnvVars = lib.generators.toKeyValue { + mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" { + mkValueString = v: + with builtins; + if isInt v + then toString v + else if isString v + then v + else if true == v + then "true" + else if false == v + then "false" + else if isSecret v + then hashString "sha256" v._secret + else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}"; + }; + }; + secretPaths = lib.mapAttrsToList (_: v: v._secret) (lib.filterAttrs (_: isSecret) cfg.config); + mkSecretReplacement = file: '' + replace-secret ${escapeShellArgs [(builtins.hashString "sha256" file) file "${cfg.dataDir}/.env"]} + ''; + secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths; + filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! elem v [{} null])) cfg.config; + monicaEnv = pkgs.writeText "monica.env" (monicaEnvVars filteredConfig); + in '' + # error handling + set -euo pipefail + + # create .env file + install -T -m 0600 -o ${user} ${monicaEnv} "${cfg.dataDir}/.env" + ${secretReplacements} + if ! grep 'APP_KEY=base64:' "${cfg.dataDir}/.env" >/dev/null; then + sed -i 's/APP_KEY=/APP_KEY=base64:/' "${cfg.dataDir}/.env" + fi + + # migrate & seed db + ${pkgs.php}/bin/php artisan key:generate --force + ${pkgs.php}/bin/php artisan setup:production -v --force + ''; + }; + + systemd.services.monica-scheduler = { + description = "Background tasks for monica"; + startAt = "minutely"; + after = ["monica-setup.service"]; + serviceConfig = { + Type = "oneshot"; + User = user; + WorkingDirectory = "${monica}"; + ExecStart = "${pkgs.php}/bin/php ${monica}/artisan schedule:run -v"; + }; + }; + + systemd.tmpfiles.rules = [ + "d ${cfg.dataDir} 0710 ${user} ${group} - -" + "d ${cfg.dataDir}/public 0750 ${user} ${group} - -" + "d ${cfg.dataDir}/public/uploads 0750 ${user} ${group} - -" + "d ${cfg.dataDir}/storage 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/app 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/fonts 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/framework 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/framework/cache 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/framework/sessions 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/framework/views 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/logs 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/uploads 0700 ${user} ${group} - -" + ]; + + users = { + users = mkIf (user == "monica") { + monica = { + inherit group; + isSystemUser = true; + }; + "${config.services.nginx.user}".extraGroups = [group]; + }; + groups = mkIf (group == "monica") { + monica = {}; + }; + }; + }; +} + diff --git a/nixos/modules/services/web-apps/plausible.nix b/nixos/modules/services/web-apps/plausible.nix index f64254d62524e..893dfa10acbc0 100644 --- a/nixos/modules/services/web-apps/plausible.nix +++ b/nixos/modules/services/web-apps/plausible.nix @@ -9,6 +9,8 @@ in { options.services.plausible = { enable = mkEnableOption (lib.mdDoc "plausible"); + package = mkPackageOptionMD pkgs "plausible" { }; + releaseCookiePath = mkOption { type = with types; either str path; description = lib.mdDoc '' @@ -180,12 +182,12 @@ in { services.epmd.enable = true; - environment.systemPackages = [ pkgs.plausible ]; + environment.systemPackages = [ cfg.package ]; systemd.services = mkMerge [ { plausible = { - inherit (pkgs.plausible.meta) description; + inherit (cfg.package.meta) description; documentation = [ "https://plausible.io/docs/self-hosting" ]; wantedBy = [ "multi-user.target" ]; after = optional cfg.database.clickhouse.setup "clickhouse.service" @@ -233,7 +235,7 @@ in { SMTP_USER_NAME = cfg.mail.smtp.user; }); - path = [ pkgs.plausible ] + path = [ cfg.package ] ++ optional cfg.database.postgres.setup config.services.postgresql.package; script = '' export CONFIG_DIR=$CREDENTIALS_DIRECTORY @@ -241,10 +243,10 @@ in { export RELEASE_COOKIE="$(< $CREDENTIALS_DIRECTORY/RELEASE_COOKIE )" # setup - ${pkgs.plausible}/createdb.sh - ${pkgs.plausible}/migrate.sh + ${cfg.package}/createdb.sh + ${cfg.package}/migrate.sh ${optionalString cfg.adminUser.activate '' - if ! ${pkgs.plausible}/init-admin.sh | grep 'already exists'; then + if ! ${cfg.package}/init-admin.sh | grep 'already exists'; then psql -d plausible <<< "UPDATE users SET email_verified=true;" fi ''} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 061e41f7ed753..8d341dd6653ee 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -210,6 +210,7 @@ in { envoy = handleTest ./envoy.nix {}; ergo = handleTest ./ergo.nix {}; ergochat = handleTest ./ergochat.nix {}; + esphome = handleTest ./esphome.nix {}; etc = pkgs.callPackage ../modules/system/etc/test.nix { inherit evalMinimalConfig; }; activation = pkgs.callPackage ../modules/system/activation/test.nix { }; etcd = handleTestOn ["x86_64-linux"] ./etcd.nix {}; @@ -420,6 +421,7 @@ in { mjolnir = handleTest ./matrix/mjolnir.nix {}; mod_perl = handleTest ./mod_perl.nix {}; molly-brown = handleTest ./molly-brown.nix {}; + monica = handleTest ./web-apps/monica.nix {}; mongodb = handleTest ./mongodb.nix {}; moodle = handleTest ./moodle.nix {}; moonraker = handleTest ./moonraker.nix {}; @@ -586,6 +588,7 @@ in { pt2-clone = handleTest ./pt2-clone.nix {}; pykms = handleTest ./pykms.nix {}; public-inbox = handleTest ./public-inbox.nix {}; + pufferpanel = handleTest ./pufferpanel.nix {}; pulseaudio = discoverTests (import ./pulseaudio.nix); qboot = handleTestOn ["x86_64-linux" "i686-linux"] ./qboot.nix {}; qemu-vm-restrictnetwork = handleTest ./qemu-vm-restrictnetwork.nix {}; diff --git a/nixos/tests/esphome.nix b/nixos/tests/esphome.nix new file mode 100644 index 0000000000000..b8dbdb0b37957 --- /dev/null +++ b/nixos/tests/esphome.nix @@ -0,0 +1,41 @@ +import ./make-test-python.nix ({ pkgs, lib, ... }: + +let + testPort = 6052; + unixSocket = "/run/esphome/esphome.sock"; +in +with lib; +{ + name = "esphome"; + meta.maintainers = with pkgs.lib.maintainers; [ oddlama ]; + + nodes = { + esphomeTcp = { ... }: + { + services.esphome = { + enable = true; + port = testPort; + address = "0.0.0.0"; + openFirewall = true; + }; + }; + + esphomeUnix = { ... }: + { + services.esphome = { + enable = true; + enableUnixSocket = true; + }; + }; + }; + + testScript = '' + esphomeTcp.wait_for_unit("esphome.service") + esphomeTcp.wait_for_open_port(${toString testPort}) + esphomeTcp.succeed("curl --fail http://localhost:${toString testPort}/") + + esphomeUnix.wait_for_unit("esphome.service") + esphomeUnix.wait_for_file("${unixSocket}") + esphomeUnix.succeed("curl --fail --unix-socket ${unixSocket} http://localhost/") + ''; +}) diff --git a/nixos/tests/pufferpanel.nix b/nixos/tests/pufferpanel.nix new file mode 100644 index 0000000000000..e7b09c13f90bd --- /dev/null +++ b/nixos/tests/pufferpanel.nix @@ -0,0 +1,74 @@ +import ./make-test-python.nix ({ lib, ... }: { + name = "pufferpanel"; + meta.maintainers = [ lib.maintainers.tie ]; + + nodes.machine = { pkgs, ... }: { + environment.systemPackages = [ pkgs.pufferpanel ]; + services.pufferpanel = { + enable = true; + extraPackages = [ pkgs.netcat ]; + environment = { + PUFFER_PANEL_REGISTRATIONENABLED = "false"; + PUFFER_PANEL_SETTINGS_COMPANYNAME = "NixOS"; + }; + }; + }; + + testScript = '' + import shlex + import json + + curl = "curl --fail-with-body --silent" + baseURL = "http://localhost:8080" + adminName = "admin" + adminEmail = "admin@nixos.org" + adminPass = "admin" + adminCreds = json.dumps({ + "email": adminEmail, + "password": adminPass, + }) + stopCode = 9 # SIGKILL + serverPort = 1337 + serverDefinition = json.dumps({ + "name": "netcat", + "node": 0, + "users": [ + adminName, + ], + "type": "netcat", + "run": { + "stopCode": stopCode, + "command": f"nc -l {serverPort}", + }, + "environment": { + "type": "standard", + }, + }) + + start_all() + + machine.wait_for_unit("pufferpanel.service") + machine.wait_for_open_port(5657) # SFTP + machine.wait_for_open_port(8080) # HTTP + + # Note that PufferPanel does not initialize database unless necessary. + # /api/config endpoint creates database file and triggers migrations. + # On success, we run a command to create administrator user that we use to + # interact with HTTP API. + resp = json.loads(machine.succeed(f"{curl} {baseURL}/api/config")) + assert resp["branding"]["name"] == "NixOS", "Invalid company name in configuration" + assert resp["registrationEnabled"] == False, "Expected registration to be disabled" + + machine.succeed(f"pufferpanel --workDir /var/lib/pufferpanel user add --admin --name {adminName} --email {adminEmail} --password {adminPass}") + + resp = json.loads(machine.succeed(f"{curl} -d '{adminCreds}' {baseURL}/auth/login")) + assert "servers.admin" in resp["scopes"], "User is not administrator" + token = resp["session"] + authHeader = shlex.quote(f"Authorization: Bearer {token}") + + resp = json.loads(machine.succeed(f"{curl} -H {authHeader} -H 'Content-Type: application/json' -d '{serverDefinition}' {baseURL}/api/servers")) + serverID = resp["id"] + machine.succeed(f"{curl} -X POST -H {authHeader} {baseURL}/proxy/daemon/server/{serverID}/start") + machine.wait_for_open_port(serverPort) + ''; +}) diff --git a/nixos/tests/web-apps/monica.nix b/nixos/tests/web-apps/monica.nix new file mode 100644 index 0000000000000..29f5cb85bb13a --- /dev/null +++ b/nixos/tests/web-apps/monica.nix @@ -0,0 +1,33 @@ +import ../make-test-python.nix ({pkgs, ...}: +let + cert = pkgs.runCommand "selfSignedCerts" { nativeBuildInputs = [ pkgs.openssl ]; } '' + openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -nodes -subj '/CN=localhost' -days 36500 + mkdir -p $out + cp key.pem cert.pem $out + ''; +in +{ + name = "monica"; + + nodes = { + machine = {pkgs, ...}: { + services.monica = { + enable = true; + hostname = "localhost"; + appKeyFile = "${pkgs.writeText "keyfile" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}"; + nginx = { + forceSSL = true; + sslCertificate = "${cert}/cert.pem"; + sslCertificateKey = "${cert}/key.pem"; + }; + }; + }; + }; + + testScript = '' + start_all() + machine.wait_for_unit("monica-setup.service") + machine.wait_for_open_port(443) + machine.succeed("curl -k --fail https://localhost", timeout=10) + ''; +}) |