about summary refs log tree commit diff
path: root/nixos/modules/services
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/modules/services')
-rw-r--r--nixos/modules/services/audio/snapserver.nix4
-rw-r--r--nixos/modules/services/backup/borgmatic.nix3
-rw-r--r--nixos/modules/services/backup/mysql-backup.nix2
-rw-r--r--nixos/modules/services/backup/restic.nix2
-rw-r--r--nixos/modules/services/blockchain/ethereum/geth.nix4
-rw-r--r--nixos/modules/services/computing/boinc/client.nix2
-rw-r--r--nixos/modules/services/continuous-integration/jenkins/job-builder.nix2
-rw-r--r--nixos/modules/services/development/lorri.nix2
-rw-r--r--nixos/modules/services/games/minetest-server.nix2
-rw-r--r--nixos/modules/services/hardware/udev.nix11
-rw-r--r--nixos/modules/services/home-automation/esphome.nix136
-rw-r--r--nixos/modules/services/logging/logrotate.nix5
-rw-r--r--nixos/modules/services/logging/syslogd.nix2
-rw-r--r--nixos/modules/services/mail/maddy.nix38
-rw-r--r--nixos/modules/services/mail/postfix.nix2
-rw-r--r--nixos/modules/services/mail/roundcube.nix2
-rw-r--r--nixos/modules/services/misc/gammu-smsd.nix10
-rw-r--r--nixos/modules/services/misc/gitea.nix234
-rw-r--r--nixos/modules/services/misc/gitlab.nix2
-rw-r--r--nixos/modules/services/misc/mbpfan.nix2
-rw-r--r--nixos/modules/services/misc/pufferpanel.nix176
-rw-r--r--nixos/modules/services/misc/redmine.nix14
-rw-r--r--nixos/modules/services/misc/siproxd.nix2
-rw-r--r--nixos/modules/services/monitoring/grafana-agent.nix2
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/collectd.nix4
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix12
-rw-r--r--nixos/modules/services/network-filesystems/kubo.nix73
-rw-r--r--nixos/modules/services/network-filesystems/openafs/lib.nix4
-rw-r--r--nixos/modules/services/networking/iscsi/root-initiator.nix4
-rw-r--r--nixos/modules/services/networking/ivpn.nix51
-rw-r--r--nixos/modules/services/networking/ndppd.nix2
-rw-r--r--nixos/modules/services/networking/netbird.nix5
-rw-r--r--nixos/modules/services/networking/ntopng.nix2
-rw-r--r--nixos/modules/services/networking/peroxide.nix2
-rw-r--r--nixos/modules/services/networking/smokeping.nix11
-rw-r--r--nixos/modules/services/networking/ssh/lshd.nix4
-rw-r--r--nixos/modules/services/networking/ssh/sshd.nix12
-rw-r--r--nixos/modules/services/networking/strongswan.nix6
-rw-r--r--nixos/modules/services/networking/stunnel.nix4
-rw-r--r--nixos/modules/services/networking/wgautomesh.nix161
-rw-r--r--nixos/modules/services/networking/wstunnel.nix4
-rw-r--r--nixos/modules/services/networking/xinetd.nix2
-rw-r--r--nixos/modules/services/printing/cupsd.nix1
-rw-r--r--nixos/modules/services/search/qdrant.nix1
-rw-r--r--nixos/modules/services/security/authelia.nix2
-rw-r--r--nixos/modules/services/security/fail2ban.nix28
-rw-r--r--nixos/modules/services/security/kanidm.nix102
-rw-r--r--nixos/modules/services/security/oauth2_proxy.nix5
-rw-r--r--nixos/modules/services/system/cachix-agent/default.nix2
-rw-r--r--nixos/modules/services/system/cachix-watch-store.nix6
-rw-r--r--nixos/modules/services/system/dbus.nix21
-rw-r--r--nixos/modules/services/video/mediamtx.nix (renamed from nixos/modules/services/video/rtsp-simple-server.nix)32
-rw-r--r--nixos/modules/services/web-apps/discourse.nix4
-rw-r--r--nixos/modules/services/web-apps/ihatemoney/default.nix153
-rw-r--r--nixos/modules/services/web-apps/monica.nix468
-rw-r--r--nixos/modules/services/web-apps/moodle.nix2
-rw-r--r--nixos/modules/services/web-apps/nextcloud.md4
-rw-r--r--nixos/modules/services/web-apps/nextcloud.nix6
-rw-r--r--nixos/modules/services/web-apps/plausible.nix14
-rw-r--r--nixos/modules/services/web-servers/fcgiwrap.nix2
-rw-r--r--nixos/modules/services/web-servers/lighttpd/default.nix10
-rw-r--r--nixos/modules/services/web-servers/nginx/default.nix4
-rw-r--r--nixos/modules/services/web-servers/tomcat.nix6
-rw-r--r--nixos/modules/services/x11/desktop-managers/plasma5.nix3
64 files changed, 1447 insertions, 453 deletions
diff --git a/nixos/modules/services/audio/snapserver.nix b/nixos/modules/services/audio/snapserver.nix
index 2af42eeb3705b..dbab741bf6fc7 100644
--- a/nixos/modules/services/audio/snapserver.nix
+++ b/nixos/modules/services/audio/snapserver.nix
@@ -275,9 +275,9 @@ in {
 
     warnings =
       # https://github.com/badaix/snapcast/blob/98ac8b2fb7305084376607b59173ce4097c620d8/server/streamreader/stream_manager.cpp#L85
-      filter (w: w != "") (mapAttrsToList (k: v: if v.type == "spotify" then ''
+      filter (w: w != "") (mapAttrsToList (k: v: optionalString (v.type == "spotify") ''
         services.snapserver.streams.${k}.type = "spotify" is deprecated, use services.snapserver.streams.${k}.type = "librespot" instead.
-      '' else "") cfg.streams);
+      '') cfg.streams);
 
     systemd.services.snapserver = {
       after = [ "network.target" ];
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/backup/mysql-backup.nix b/nixos/modules/services/backup/mysql-backup.nix
index 289291c6bd2f9..9fbc599cd41af 100644
--- a/nixos/modules/services/backup/mysql-backup.nix
+++ b/nixos/modules/services/backup/mysql-backup.nix
@@ -20,7 +20,7 @@ let
   '';
   backupDatabaseScript = db: ''
     dest="${cfg.location}/${db}.gz"
-    if ${mariadb}/bin/mysqldump ${if cfg.singleTransaction then "--single-transaction" else ""} ${db} | ${gzip}/bin/gzip -c > $dest.tmp; then
+    if ${mariadb}/bin/mysqldump ${optionalString cfg.singleTransaction "--single-transaction"} ${db} | ${gzip}/bin/gzip -c > $dest.tmp; then
       mv $dest.tmp $dest
       echo "Backed up to $dest"
     else
diff --git a/nixos/modules/services/backup/restic.nix b/nixos/modules/services/backup/restic.nix
index ca796cf7797e6..d19b98a3e4bbb 100644
--- a/nixos/modules/services/backup/restic.nix
+++ b/nixos/modules/services/backup/restic.nix
@@ -300,7 +300,7 @@ in
             filesFromTmpFile = "/run/restic-backups-${name}/includes";
             backupPaths =
               if (backup.dynamicFilesFrom == null)
-              then if (backup.paths != null) then concatStringsSep " " backup.paths else ""
+              then optionalString (backup.paths != null) (concatStringsSep " " backup.paths)
               else "--files-from ${filesFromTmpFile}";
             pruneCmd = optionals (builtins.length backup.pruneOpts > 0) [
               (resticCmd + " forget --prune " + (concatStringsSep " " backup.pruneOpts))
diff --git a/nixos/modules/services/blockchain/ethereum/geth.nix b/nixos/modules/services/blockchain/ethereum/geth.nix
index eca308dc366d1..d12516ca2f249 100644
--- a/nixos/modules/services/blockchain/ethereum/geth.nix
+++ b/nixos/modules/services/blockchain/ethereum/geth.nix
@@ -196,9 +196,9 @@ in
           --gcmode ${cfg.gcmode} \
           --port ${toString cfg.port} \
           --maxpeers ${toString cfg.maxpeers} \
-          ${if cfg.http.enable then ''--http --http.addr ${cfg.http.address} --http.port ${toString cfg.http.port}'' else ""} \
+          ${optionalString cfg.http.enable ''--http --http.addr ${cfg.http.address} --http.port ${toString cfg.http.port}''} \
           ${optionalString (cfg.http.apis != null) ''--http.api ${lib.concatStringsSep "," cfg.http.apis}''} \
-          ${if cfg.websocket.enable then ''--ws --ws.addr ${cfg.websocket.address} --ws.port ${toString cfg.websocket.port}'' else ""} \
+          ${optionalString cfg.websocket.enable ''--ws --ws.addr ${cfg.websocket.address} --ws.port ${toString cfg.websocket.port}''} \
           ${optionalString (cfg.websocket.apis != null) ''--ws.api ${lib.concatStringsSep "," cfg.websocket.apis}''} \
           ${optionalString cfg.metrics.enable ''--metrics --metrics.addr ${cfg.metrics.address} --metrics.port ${toString cfg.metrics.port}''} \
           --authrpc.addr ${cfg.authrpc.address} --authrpc.port ${toString cfg.authrpc.port} --authrpc.vhosts ${lib.concatStringsSep "," cfg.authrpc.vhosts} \
diff --git a/nixos/modules/services/computing/boinc/client.nix b/nixos/modules/services/computing/boinc/client.nix
index 5fb715f4d779a..1879fef9666f7 100644
--- a/nixos/modules/services/computing/boinc/client.nix
+++ b/nixos/modules/services/computing/boinc/client.nix
@@ -6,7 +6,7 @@ let
   cfg = config.services.boinc;
   allowRemoteGuiRpcFlag = optionalString cfg.allowRemoteGuiRpc "--allow_remote_gui_rpc";
 
-  fhsEnv = pkgs.buildFHSUserEnv {
+  fhsEnv = pkgs.buildFHSEnv {
     name = "boinc-fhs-env";
     targetPkgs = pkgs': [ cfg.package ] ++ cfg.extraEnvPackages;
     runScript = "/bin/boinc_client";
diff --git a/nixos/modules/services/continuous-integration/jenkins/job-builder.nix b/nixos/modules/services/continuous-integration/jenkins/job-builder.nix
index 3a1c6c1a371df..d6a8c2a3f7cce 100644
--- a/nixos/modules/services/continuous-integration/jenkins/job-builder.nix
+++ b/nixos/modules/services/continuous-integration/jenkins/job-builder.nix
@@ -242,7 +242,7 @@ in {
                 jobdir="${jenkinsCfg.home}/$jenkinsjobname"
                 rm -rf "$jobdir"
             done
-          '' + (if cfg.accessUser != "" then reloadScript else "");
+          '' + (optionalString (cfg.accessUser != "") reloadScript);
       serviceConfig = {
         Type = "oneshot";
         User = jenkinsCfg.user;
diff --git a/nixos/modules/services/development/lorri.nix b/nixos/modules/services/development/lorri.nix
index 8c64e3d9a5605..74f56f5890fce 100644
--- a/nixos/modules/services/development/lorri.nix
+++ b/nixos/modules/services/development/lorri.nix
@@ -50,6 +50,6 @@ in {
       };
     };
 
-    environment.systemPackages = [ cfg.package ];
+    environment.systemPackages = [ cfg.package pkgs.direnv ];
   };
 }
diff --git a/nixos/modules/services/games/minetest-server.nix b/nixos/modules/services/games/minetest-server.nix
index e8c96881673b5..578364ec542bb 100644
--- a/nixos/modules/services/games/minetest-server.nix
+++ b/nixos/modules/services/games/minetest-server.nix
@@ -4,7 +4,7 @@ with lib;
 
 let
   cfg   = config.services.minetest-server;
-  flag  = val: name: if val != null then "--${name} ${toString val} " else "";
+  flag  = val: name: optionalString (val != null) "--${name} ${toString val} ";
   flags = [
     (flag cfg.gameId "gameid")
     (flag cfg.world "world")
diff --git a/nixos/modules/services/hardware/udev.nix b/nixos/modules/services/hardware/udev.nix
index d95261332419d..95c2a4fc5c3e1 100644
--- a/nixos/modules/services/hardware/udev.nix
+++ b/nixos/modules/services/hardware/udev.nix
@@ -16,16 +16,6 @@ let
   '';
 
 
-  # networkd link files are used early by udev to set up interfaces early.
-  # This must be done in stage 1 to avoid race conditions between udev and
-  # network daemons.
-  # TODO move this into the initrd-network module when it exists
-  initrdLinkUnits = pkgs.runCommand "initrd-link-units" {} ''
-    mkdir -p $out
-    ln -s ${udev}/lib/systemd/network/*.link $out/
-    ${lib.concatMapStringsSep "\n" (file: "ln -s ${file} $out/") (lib.mapAttrsToList (n: v: "${v.unit}/${n}") (lib.filterAttrs (n: _: hasSuffix ".link" n) config.systemd.network.units))}
-  '';
-
   extraUdevRules = pkgs.writeTextFile {
     name = "extra-udev-rules";
     text = cfg.extraRules;
@@ -398,7 +388,6 @@ in
         systemd = config.boot.initrd.systemd.package;
         binPackages = config.boot.initrd.services.udev.binPackages ++ [ config.boot.initrd.systemd.contents."/bin".source ];
       };
-      "/etc/systemd/network".source = initrdLinkUnits;
     };
     # Insert initrd rules
     boot.initrd.services.udev.packages = [
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/logging/logrotate.nix b/nixos/modules/services/logging/logrotate.nix
index b056f96c3630b..342ac5ec6e049 100644
--- a/nixos/modules/services/logging/logrotate.nix
+++ b/nixos/modules/services/logging/logrotate.nix
@@ -83,9 +83,8 @@ let
   };
 
   mailOption =
-    if foldr (n: a: a || (n.mail or false) != false) false (attrValues cfg.settings)
-    then "--mail=${pkgs.mailutils}/bin/mail"
-    else "";
+    optionalString (foldr (n: a: a || (n.mail or false) != false) false (attrValues cfg.settings))
+    "--mail=${pkgs.mailutils}/bin/mail";
 in
 {
   imports = [
diff --git a/nixos/modules/services/logging/syslogd.nix b/nixos/modules/services/logging/syslogd.nix
index 43969402588db..553973e255f7e 100644
--- a/nixos/modules/services/logging/syslogd.nix
+++ b/nixos/modules/services/logging/syslogd.nix
@@ -7,7 +7,7 @@ let
   cfg = config.services.syslogd;
 
   syslogConf = pkgs.writeText "syslog.conf" ''
-    ${if (cfg.tty != "") then "kern.warning;*.err;authpriv.none /dev/${cfg.tty}" else ""}
+    ${optionalString (cfg.tty != "") "kern.warning;*.err;authpriv.none /dev/${cfg.tty}"}
     ${cfg.defaultConfig}
     ${cfg.extraConfig}
   '';
diff --git a/nixos/modules/services/mail/maddy.nix b/nixos/modules/services/mail/maddy.nix
index 5f3a9b56292d2..d0b525bcb0027 100644
--- a/nixos/modules/services/mail/maddy.nix
+++ b/nixos/modules/services/mail/maddy.nix
@@ -228,8 +228,8 @@ in {
         default = [];
         description = lib.mdDoc ''
           List of IMAP accounts which get automatically created. Note that for
-          a complete setup, user credentials for these accounts are required too
-          and can be created using the command `maddyctl creds`.
+          a complete setup, user credentials for these accounts are required
+          and can be created using the `ensureCredentials` option.
           This option does not delete accounts which are not (anymore) listed.
         '';
         example = [
@@ -238,6 +238,33 @@ in {
         ];
       };
 
+      ensureCredentials = mkOption {
+        default = {};
+        description = lib.mdDoc ''
+          List of user accounts which get automatically created if they don't
+          exist yet. Note that for a complete setup, corresponding mail boxes
+          have to get created using the `ensureAccounts` option.
+          This option does not delete accounts which are not (anymore) listed.
+        '';
+        example = {
+          "user1@localhost".passwordFile = /secrets/user1-localhost;
+          "user2@localhost".passwordFile = /secrets/user2-localhost;
+        };
+        type = types.attrsOf (types.submodule {
+          options = {
+            passwordFile = mkOption {
+              type = types.path;
+              example = "/path/to/file";
+              default = null;
+              description = lib.mdDoc ''
+                Specifies the path to a file containing the
+                clear text password for the user.
+              '';
+            };
+          };
+        });
+      };
+
     };
   };
 
@@ -265,6 +292,13 @@ in {
                 fi
               '') cfg.ensureAccounts}
             ''}
+            ${optionalString (cfg.ensureCredentials != {}) ''
+              ${concatStringsSep "\n" (mapAttrsToList (name: cfg: ''
+                if ! ${pkgs.maddy}/bin/maddyctl creds list | grep "${name}"; then
+                  ${pkgs.maddy}/bin/maddyctl creds create --password $(cat ${escapeShellArg cfg.passwordFile}) ${name}
+                fi
+              '') cfg.ensureCredentials)}
+            ''}
           '';
           serviceConfig = {
             Type = "oneshot";
diff --git a/nixos/modules/services/mail/postfix.nix b/nixos/modules/services/mail/postfix.nix
index 852340c05aa7a..23c47aaca7e23 100644
--- a/nixos/modules/services/mail/postfix.nix
+++ b/nixos/modules/services/mail/postfix.nix
@@ -234,7 +234,7 @@ let
 
   headerChecks = concatStringsSep "\n" (map (x: "${x.pattern} ${x.action}") cfg.headerChecks) + cfg.extraHeaderChecks;
 
-  aliases = let separator = if cfg.aliasMapType == "hash" then ":" else ""; in
+  aliases = let separator = optionalString (cfg.aliasMapType == "hash") ":"; in
     optionalString (cfg.postmasterAlias != "") ''
       postmaster${separator} ${cfg.postmasterAlias}
     ''
diff --git a/nixos/modules/services/mail/roundcube.nix b/nixos/modules/services/mail/roundcube.nix
index 7b6d82219298c..3aaec145930db 100644
--- a/nixos/modules/services/mail/roundcube.nix
+++ b/nixos/modules/services/mail/roundcube.nix
@@ -7,7 +7,7 @@ let
   fpm = config.services.phpfpm.pools.roundcube;
   localDB = cfg.database.host == "localhost";
   user = cfg.database.username;
-  phpWithPspell = pkgs.php80.withExtensions ({ enabled, all }: [ all.pspell ] ++ enabled);
+  phpWithPspell = pkgs.php81.withExtensions ({ enabled, all }: [ all.pspell ] ++ enabled);
 in
 {
   options.services.roundcube = {
diff --git a/nixos/modules/services/misc/gammu-smsd.nix b/nixos/modules/services/misc/gammu-smsd.nix
index 83f4efe695a27..eff725f5a8685 100644
--- a/nixos/modules/services/misc/gammu-smsd.nix
+++ b/nixos/modules/services/misc/gammu-smsd.nix
@@ -10,7 +10,7 @@ let
     Connection = ${cfg.device.connection}
     SynchronizeTime = ${if cfg.device.synchronizeTime then "yes" else "no"}
     LogFormat = ${cfg.log.format}
-    ${if (cfg.device.pin != null) then "PIN = ${cfg.device.pin}" else ""}
+    ${optionalString (cfg.device.pin != null) "PIN = ${cfg.device.pin}"}
     ${cfg.extraConfig.gammu}
 
 
@@ -33,10 +33,10 @@ let
     ${optionalString (cfg.backend.service == "sql" && cfg.backend.sql.driver == "native_pgsql") (
       with cfg.backend; ''
         Driver = ${sql.driver}
-        ${if (sql.database!= null) then "Database = ${sql.database}" else ""}
-        ${if (sql.host != null) then "Host = ${sql.host}" else ""}
-        ${if (sql.user != null) then "User = ${sql.user}" else ""}
-        ${if (sql.password != null) then "Password = ${sql.password}" else ""}
+        ${optionalString (sql.database!= null) "Database = ${sql.database}"}
+        ${optionalString (sql.host != null) "Host = ${sql.host}"}
+        ${optionalString (sql.user != null) "User = ${sql.user}"}
+        ${optionalString (sql.password != null) "Password = ${sql.password}"}
       '')}
 
     ${cfg.extraConfig.smsd}
diff --git a/nixos/modules/services/misc/gitea.nix b/nixos/modules/services/misc/gitea.nix
index e019e431a1890..a5d7a73dd06aa 100644
--- a/nixos/modules/services/misc/gitea.nix
+++ b/nixos/modules/services/misc/gitea.nix
@@ -26,9 +26,18 @@ in
   imports = [
     (mkRenamedOptionModule [ "services" "gitea" "cookieSecure" ] [ "services" "gitea" "settings" "session" "COOKIE_SECURE" ])
     (mkRenamedOptionModule [ "services" "gitea" "disableRegistration" ] [ "services" "gitea" "settings" "service" "DISABLE_REGISTRATION" ])
+    (mkRenamedOptionModule [ "services" "gitea" "domain" ] [ "services" "gitea" "settings" "server" "DOMAIN" ])
+    (mkRenamedOptionModule [ "services" "gitea" "httpAddress" ] [ "services" "gitea" "settings" "server" "HTTP_ADDR" ])
+    (mkRenamedOptionModule [ "services" "gitea" "httpPort" ] [ "services" "gitea" "settings" "server" "HTTP_PORT" ])
     (mkRenamedOptionModule [ "services" "gitea" "log" "level" ] [ "services" "gitea" "settings" "log" "LEVEL" ])
     (mkRenamedOptionModule [ "services" "gitea" "log" "rootPath" ] [ "services" "gitea" "settings" "log" "ROOT_PATH" ])
+    (mkRenamedOptionModule [ "services" "gitea" "rootUrl" ] [ "services" "gitea" "settings" "server" "ROOT_URL" ])
     (mkRenamedOptionModule [ "services" "gitea" "ssh" "clonePort" ] [ "services" "gitea" "settings" "server" "SSH_PORT" ])
+    (mkRenamedOptionModule [ "services" "gitea" "staticRootPath" ] [ "services" "gitea" "settings" "server" "STATIC_ROOT_PATH" ])
+
+    (mkChangedOptionModule [ "services" "gitea" "enableUnixSocket" ] [ "services" "gitea" "settings" "server" "PROTOCOL" ] (
+      config: if config.services.gitea.enableUnixSocket then "http+unix" else "http"
+    ))
 
     (mkRemovedOptionModule [ "services" "gitea" "ssh" "enable" ] "services.gitea.ssh.enable has been migrated into freeform setting services.gitea.settings.server.DISABLE_SSH. Keep in mind that the setting is inverted")
   ];
@@ -57,7 +66,14 @@ in
       stateDir = mkOption {
         default = "/var/lib/gitea";
         type = types.str;
-        description = lib.mdDoc "gitea data directory.";
+        description = lib.mdDoc "Gitea data directory.";
+      };
+
+      customDir = mkOption {
+        default = "${cfg.stateDir}/custom";
+        defaultText = literalExpression ''"''${config.${opt.stateDir}}/custom"'';
+        type = types.str;
+        description = lib.mdDoc "Gitea custom directory. Used for config, custom templates and other options.";
       };
 
       user = mkOption {
@@ -66,6 +82,12 @@ in
         description = lib.mdDoc "User account under which gitea runs.";
       };
 
+      group = mkOption {
+        type = types.str;
+        default = "gitea";
+        description = lib.mdDoc "Group under which gitea runs.";
+      };
+
       database = {
         type = mkOption {
           type = types.enum [ "sqlite3" "mysql" "postgres" ];
@@ -216,44 +238,6 @@ in
         description = lib.mdDoc "Path to the git repositories.";
       };
 
-      domain = mkOption {
-        type = types.str;
-        default = "localhost";
-        description = lib.mdDoc "Domain name of your server.";
-      };
-
-      rootUrl = mkOption {
-        type = types.str;
-        default = "http://localhost:3000/";
-        description = lib.mdDoc "Full public URL of gitea server.";
-      };
-
-      httpAddress = mkOption {
-        type = types.str;
-        default = "0.0.0.0";
-        description = lib.mdDoc "HTTP listen address.";
-      };
-
-      httpPort = mkOption {
-        type = types.port;
-        default = 3000;
-        description = lib.mdDoc "HTTP listen port.";
-      };
-
-      enableUnixSocket = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc "Configure Gitea to listen on a unix socket instead of the default TCP port.";
-      };
-
-      staticRootPath = mkOption {
-        type = types.either types.str types.path;
-        default = cfg.package.data;
-        defaultText = literalExpression "package.data";
-        example = "/var/lib/gitea/data";
-        description = lib.mdDoc "Upper level of template and static files path.";
-      };
-
       mailerPasswordFile = mkOption {
         type = types.nullOr types.str;
         default = null;
@@ -285,7 +269,7 @@ in
             };
           }
         '';
-        type = with types; submodule {
+        type = types.submodule {
           freeformType = format.type;
           options = {
             log = {
@@ -303,6 +287,46 @@ in
             };
 
             server = {
+              PROTOCOL = mkOption {
+                type = types.enum [ "http" "https" "fcgi" "http+unix" "fcgi+unix" ];
+                default = "http";
+                description = lib.mdDoc ''Listen protocol. `+unix` means "over unix", not "in addition to."'';
+              };
+
+              HTTP_ADDR = mkOption {
+                type = types.either types.str types.path;
+                default = if lib.hasSuffix "+unix" cfg.settings.server.PROTOCOL then "/run/gitea/gitea.sock" else "0.0.0.0";
+                defaultText = literalExpression ''if lib.hasSuffix "+unix" cfg.settings.server.PROTOCOL then "/run/gitea/gitea.sock" else "0.0.0.0"'';
+                description = lib.mdDoc "Listen address. Must be a path when using a unix socket.";
+              };
+
+              HTTP_PORT = mkOption {
+                type = types.port;
+                default = 3000;
+                description = lib.mdDoc "Listen port. Ignored when using a unix socket.";
+              };
+
+              DOMAIN = mkOption {
+                type = types.str;
+                default = "localhost";
+                description = lib.mdDoc "Domain name of your server.";
+              };
+
+              ROOT_URL = mkOption {
+                type = types.str;
+                default = "http://${cfg.settings.server.DOMAIN}:${toString cfg.settings.server.HTTP_PORT}/";
+                defaultText = literalExpression ''"http://''${config.services.gitea.settings.server.DOMAIN}:''${toString config.services.gitea.settings.server.HTTP_PORT}/"'';
+                description = lib.mdDoc "Full public URL of gitea server.";
+              };
+
+              STATIC_ROOT_PATH = mkOption {
+                type = types.either types.str types.path;
+                default = cfg.package.data;
+                defaultText = literalExpression "config.${opt.package}.data";
+                example = "/var/lib/gitea/data";
+                description = lib.mdDoc "Upper level of template and static files path.";
+              };
+
               DISABLE_SSH = mkOption {
                 type = types.bool;
                 default = false;
@@ -359,7 +383,7 @@ in
 
   config = mkIf cfg.enable {
     assertions = [
-      { assertion = cfg.database.createDatabase -> cfg.database.user == cfg.user;
+      { assertion = cfg.database.createDatabase -> useSqlite || cfg.database.user == cfg.user;
         message = "services.gitea.database.user must match services.gitea.user if the database is to be automatically provisioned";
       }
     ];
@@ -389,26 +413,10 @@ in
         ROOT = cfg.repositoryRoot;
       };
 
-      server = mkMerge [
-        {
-          DOMAIN = cfg.domain;
-          STATIC_ROOT_PATH = toString cfg.staticRootPath;
-          LFS_JWT_SECRET = "#lfsjwtsecret#";
-          ROOT_URL = cfg.rootUrl;
-        }
-        (mkIf cfg.enableUnixSocket {
-          PROTOCOL = "http+unix";
-          HTTP_ADDR = "/run/gitea/gitea.sock";
-        })
-        (mkIf (!cfg.enableUnixSocket) {
-          HTTP_ADDR = cfg.httpAddress;
-          HTTP_PORT = cfg.httpPort;
-        })
-        (mkIf cfg.lfs.enable {
-          LFS_START_SERVER = true;
-        })
-
-      ];
+      server = mkIf cfg.lfs.enable {
+        LFS_START_SERVER = true;
+        LFS_JWT_SECRET = "#lfsjwtsecret#";
+      };
 
       session = {
         COOKIE_NAME = lib.mkDefault "session";
@@ -428,7 +436,7 @@ in
         JWT_SECRET = "#oauth2jwtsecret#";
       };
 
-      lfs = mkIf (cfg.lfs.enable) {
+      lfs = mkIf cfg.lfs.enable {
         PATH = cfg.lfs.contentDir;
       };
     };
@@ -457,33 +465,35 @@ in
     };
 
     systemd.tmpfiles.rules = [
-      "d '${cfg.dump.backupDir}' 0750 ${cfg.user} gitea - -"
-      "z '${cfg.dump.backupDir}' 0750 ${cfg.user} gitea - -"
-      "Z '${cfg.dump.backupDir}' - ${cfg.user} gitea - -"
-      "d '${cfg.lfs.contentDir}' 0750 ${cfg.user} gitea - -"
-      "z '${cfg.lfs.contentDir}' 0750 ${cfg.user} gitea - -"
-      "Z '${cfg.lfs.contentDir}' - ${cfg.user} gitea - -"
-      "d '${cfg.repositoryRoot}' 0750 ${cfg.user} gitea - -"
-      "z '${cfg.repositoryRoot}' 0750 ${cfg.user} gitea - -"
-      "Z '${cfg.repositoryRoot}' - ${cfg.user} gitea - -"
-      "d '${cfg.stateDir}' 0750 ${cfg.user} gitea - -"
-      "d '${cfg.stateDir}/conf' 0750 ${cfg.user} gitea - -"
-      "d '${cfg.stateDir}/custom' 0750 ${cfg.user} gitea - -"
-      "d '${cfg.stateDir}/custom/conf' 0750 ${cfg.user} gitea - -"
-      "d '${cfg.stateDir}/data' 0750 ${cfg.user} gitea - -"
-      "d '${cfg.stateDir}/log' 0750 ${cfg.user} gitea - -"
-      "z '${cfg.stateDir}' 0750 ${cfg.user} gitea - -"
-      "z '${cfg.stateDir}/.ssh' 0700 ${cfg.user} gitea - -"
-      "z '${cfg.stateDir}/conf' 0750 ${cfg.user} gitea - -"
-      "z '${cfg.stateDir}/custom' 0750 ${cfg.user} gitea - -"
-      "z '${cfg.stateDir}/custom/conf' 0750 ${cfg.user} gitea - -"
-      "z '${cfg.stateDir}/data' 0750 ${cfg.user} gitea - -"
-      "z '${cfg.stateDir}/log' 0750 ${cfg.user} gitea - -"
-      "Z '${cfg.stateDir}' - ${cfg.user} gitea - -"
+      "d '${cfg.dump.backupDir}' 0750 ${cfg.user} ${cfg.group} - -"
+      "z '${cfg.dump.backupDir}' 0750 ${cfg.user} ${cfg.group} - -"
+      "Z '${cfg.dump.backupDir}' - ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.repositoryRoot}' 0750 ${cfg.user} ${cfg.group} - -"
+      "z '${cfg.repositoryRoot}' 0750 ${cfg.user} ${cfg.group} - -"
+      "Z '${cfg.repositoryRoot}' - ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/conf' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.customDir}' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.customDir}/conf' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/data' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -"
+      "z '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -"
+      "z '${cfg.stateDir}/.ssh' 0700 ${cfg.user} ${cfg.group} - -"
+      "z '${cfg.stateDir}/conf' 0750 ${cfg.user} ${cfg.group} - -"
+      "z '${cfg.customDir}' 0750 ${cfg.user} ${cfg.group} - -"
+      "z '${cfg.customDir}/conf' 0750 ${cfg.user} ${cfg.group} - -"
+      "z '${cfg.stateDir}/data' 0750 ${cfg.user} ${cfg.group} - -"
+      "z '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -"
+      "Z '${cfg.stateDir}' - ${cfg.user} ${cfg.group} - -"
 
       # If we have a folder or symlink with gitea locales, remove it
       # And symlink the current gitea locales in place
       "L+ '${cfg.stateDir}/conf/locale' - - - - ${cfg.package.out}/locale"
+
+    ] ++ lib.optionals cfg.lfs.enable [
+      "d '${cfg.lfs.contentDir}' 0750 ${cfg.user} ${cfg.group} - -"
+      "z '${cfg.lfs.contentDir}' 0750 ${cfg.user} ${cfg.group} - -"
+      "Z '${cfg.lfs.contentDir}' - ${cfg.user} ${cfg.group} - -"
     ];
 
     systemd.services.gitea = {
@@ -500,47 +510,52 @@ in
       # lfs_jwt_secret.
       # We have to consider this to stay compatible with older installations.
       preStart = let
-        runConfig = "${cfg.stateDir}/custom/conf/app.ini";
-        secretKey = "${cfg.stateDir}/custom/conf/secret_key";
-        oauth2JwtSecret = "${cfg.stateDir}/custom/conf/oauth2_jwt_secret";
-        oldLfsJwtSecret = "${cfg.stateDir}/custom/conf/jwt_secret"; # old file for LFS_JWT_SECRET
-        lfsJwtSecret = "${cfg.stateDir}/custom/conf/lfs_jwt_secret"; # new file for LFS_JWT_SECRET
-        internalToken = "${cfg.stateDir}/custom/conf/internal_token";
+        runConfig = "${cfg.customDir}/conf/app.ini";
+        secretKey = "${cfg.customDir}/conf/secret_key";
+        oauth2JwtSecret = "${cfg.customDir}/conf/oauth2_jwt_secret";
+        oldLfsJwtSecret = "${cfg.customDir}/conf/jwt_secret"; # old file for LFS_JWT_SECRET
+        lfsJwtSecret = "${cfg.customDir}/conf/lfs_jwt_secret"; # new file for LFS_JWT_SECRET
+        internalToken = "${cfg.customDir}/conf/internal_token";
         replaceSecretBin = "${pkgs.replace-secret}/bin/replace-secret";
       in ''
-        # copy custom configuration and generate a random secret key if needed
+        # copy custom configuration and generate random secrets if needed
         ${optionalString (!cfg.useWizard) ''
           function gitea_setup {
-            cp -f ${configFile} ${runConfig}
+            cp -f '${configFile}' '${runConfig}'
 
-            if [ ! -s ${secretKey} ]; then
-                ${exe} generate secret SECRET_KEY > ${secretKey}
+            if [ ! -s '${secretKey}' ]; then
+                ${exe} generate secret SECRET_KEY > '${secretKey}'
             fi
 
             # Migrate LFS_JWT_SECRET filename
-            if [[ -s ${oldLfsJwtSecret} && ! -s ${lfsJwtSecret} ]]; then
-                mv ${oldLfsJwtSecret} ${lfsJwtSecret}
+            if [[ -s '${oldLfsJwtSecret}' && ! -s '${lfsJwtSecret}' ]]; then
+                mv '${oldLfsJwtSecret}' '${lfsJwtSecret}'
             fi
 
-            if [ ! -s ${oauth2JwtSecret} ]; then
-                ${exe} generate secret JWT_SECRET > ${oauth2JwtSecret}
+            if [ ! -s '${oauth2JwtSecret}' ]; then
+                ${exe} generate secret JWT_SECRET > '${oauth2JwtSecret}'
             fi
 
-            if [ ! -s ${lfsJwtSecret} ]; then
-                ${exe} generate secret LFS_JWT_SECRET > ${lfsJwtSecret}
+            ${lib.optionalString cfg.lfs.enable ''
+            if [ ! -s '${lfsJwtSecret}' ]; then
+                ${exe} generate secret LFS_JWT_SECRET > '${lfsJwtSecret}'
             fi
+            ''}
 
-            if [ ! -s ${internalToken} ]; then
-                ${exe} generate secret INTERNAL_TOKEN > ${internalToken}
+            if [ ! -s '${internalToken}' ]; then
+                ${exe} generate secret INTERNAL_TOKEN > '${internalToken}'
             fi
 
             chmod u+w '${runConfig}'
             ${replaceSecretBin} '#secretkey#' '${secretKey}' '${runConfig}'
             ${replaceSecretBin} '#dbpass#' '${cfg.database.passwordFile}' '${runConfig}'
             ${replaceSecretBin} '#oauth2jwtsecret#' '${oauth2JwtSecret}' '${runConfig}'
-            ${replaceSecretBin} '#lfsjwtsecret#' '${lfsJwtSecret}' '${runConfig}'
             ${replaceSecretBin} '#internaltoken#' '${internalToken}' '${runConfig}'
 
+            ${lib.optionalString cfg.lfs.enable ''
+              ${replaceSecretBin} '#lfsjwtsecret#' '${lfsJwtSecret}' '${runConfig}'"
+            ''}
+
             ${lib.optionalString (cfg.mailerPasswordFile != null) ''
               ${replaceSecretBin} '#mailerpass#' '${cfg.mailerPasswordFile}' '${runConfig}'
             ''}
@@ -565,7 +580,7 @@ in
       serviceConfig = {
         Type = "simple";
         User = cfg.user;
-        Group = "gitea";
+        Group = cfg.group;
         WorkingDirectory = cfg.stateDir;
         ExecStart = "${exe} web --pid /run/gitea/gitea.pid";
         Restart = "always";
@@ -573,7 +588,7 @@ in
         RuntimeDirectory = "gitea";
         RuntimeDirectoryMode = "0755";
         # Access write directories
-        ReadWritePaths = [ cfg.dump.backupDir cfg.repositoryRoot cfg.stateDir cfg.lfs.contentDir ];
+        ReadWritePaths = [ cfg.customDir cfg.dump.backupDir cfg.repositoryRoot cfg.stateDir cfg.lfs.contentDir ];
         UMask = "0027";
         # Capabilities
         CapabilityBoundingSet = "";
@@ -606,6 +621,7 @@ in
         USER = cfg.user;
         HOME = cfg.stateDir;
         GITEA_WORK_DIR = cfg.stateDir;
+        GITEA_CUSTOM = cfg.customDir;
       };
     };
 
@@ -614,12 +630,14 @@ in
         description = "Gitea Service";
         home = cfg.stateDir;
         useDefaultShell = true;
-        group = "gitea";
+        group = cfg.group;
         isSystemUser = true;
       };
     };
 
-    users.groups.gitea = {};
+    users.groups = mkIf (cfg.group == "gitea") {
+      gitea = {};
+    };
 
     warnings =
       optional (cfg.database.password != "") "config.services.gitea.database.password will be stored as plaintext in the Nix store. Use database.passwordFile instead." ++
diff --git a/nixos/modules/services/misc/gitlab.nix b/nixos/modules/services/misc/gitlab.nix
index d278b571a6410..12c67c5f5a1e7 100644
--- a/nixos/modules/services/misc/gitlab.nix
+++ b/nixos/modules/services/misc/gitlab.nix
@@ -1215,7 +1215,7 @@ in {
       enableDelete = true; # This must be true, otherwise GitLab won't manage it correctly
       extraConfig = {
         auth.token = {
-          realm = "http${if cfg.https == true then "s" else ""}://${cfg.host}/jwt/auth";
+          realm = "http${optionalString (cfg.https == true) "s"}://${cfg.host}/jwt/auth";
           service = cfg.registry.serviceName;
           issuer = cfg.registry.issuer;
           rootcertbundle = cfg.registry.certFile;
diff --git a/nixos/modules/services/misc/mbpfan.nix b/nixos/modules/services/misc/mbpfan.nix
index 1a6b54854d1cd..e75c352541438 100644
--- a/nixos/modules/services/misc/mbpfan.nix
+++ b/nixos/modules/services/misc/mbpfan.nix
@@ -3,7 +3,7 @@ with lib;
 
 let
   cfg = config.services.mbpfan;
-  verbose = if cfg.verbose then "v" else "";
+  verbose = optionalString cfg.verbose "v";
   settingsFormat = pkgs.formats.ini {};
   settingsFile = settingsFormat.generate "mbpfan.ini" cfg.settings;
 
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/misc/redmine.nix b/nixos/modules/services/misc/redmine.nix
index 58a595b5c76f5..d881ea913695c 100644
--- a/nixos/modules/services/misc/redmine.nix
+++ b/nixos/modules/services/misc/redmine.nix
@@ -283,13 +283,13 @@ in
 
     services.redmine.settings = {
       production = {
-        scm_subversion_command = if cfg.components.subversion then "${pkgs.subversion}/bin/svn" else "";
-        scm_mercurial_command = if cfg.components.mercurial then "${pkgs.mercurial}/bin/hg" else "";
-        scm_git_command = if cfg.components.git then "${pkgs.git}/bin/git" else "";
-        scm_cvs_command = if cfg.components.cvs then "${pkgs.cvs}/bin/cvs" else "";
-        scm_bazaar_command = if cfg.components.breezy then "${pkgs.breezy}/bin/bzr" else "";
-        imagemagick_convert_command = if cfg.components.imagemagick then "${pkgs.imagemagick}/bin/convert" else "";
-        gs_command = if cfg.components.ghostscript then "${pkgs.ghostscript}/bin/gs" else "";
+        scm_subversion_command = optionalString cfg.components.subversion "${pkgs.subversion}/bin/svn";
+        scm_mercurial_command = optionalString cfg.components.mercurial "${pkgs.mercurial}/bin/hg";
+        scm_git_command = optionalString cfg.components.git "${pkgs.git}/bin/git";
+        scm_cvs_command = optionalString cfg.components.cvs "${pkgs.cvs}/bin/cvs";
+        scm_bazaar_command = optionalString cfg.components.breezy "${pkgs.breezy}/bin/bzr";
+        imagemagick_convert_command = optionalString cfg.components.imagemagick "${pkgs.imagemagick}/bin/convert";
+        gs_command = optionalString cfg.components.ghostscript "${pkgs.ghostscript}/bin/gs";
         minimagick_font_path = "${cfg.components.minimagick_font_path}";
       };
     };
diff --git a/nixos/modules/services/misc/siproxd.nix b/nixos/modules/services/misc/siproxd.nix
index f1a1ed4d29b38..99b25bdb8e9ed 100644
--- a/nixos/modules/services/misc/siproxd.nix
+++ b/nixos/modules/services/misc/siproxd.nix
@@ -20,7 +20,7 @@ let
     ${optionalString (cfg.hostsAllowReg != []) "hosts_allow_reg = ${concatStringsSep "," cfg.hostsAllowReg}"}
     ${optionalString (cfg.hostsAllowSip != []) "hosts_allow_sip = ${concatStringsSep "," cfg.hostsAllowSip}"}
     ${optionalString (cfg.hostsDenySip != []) "hosts_deny_sip  = ${concatStringsSep "," cfg.hostsDenySip}"}
-    ${if (cfg.passwordFile != "") then "proxy_auth_pwfile = ${cfg.passwordFile}" else ""}
+    ${optionalString (cfg.passwordFile != "") "proxy_auth_pwfile = ${cfg.passwordFile}"}
     ${cfg.extraConfig}
   '';
 
diff --git a/nixos/modules/services/monitoring/grafana-agent.nix b/nixos/modules/services/monitoring/grafana-agent.nix
index 270d888afb781..b7761c34fe51a 100644
--- a/nixos/modules/services/monitoring/grafana-agent.nix
+++ b/nixos/modules/services/monitoring/grafana-agent.nix
@@ -140,7 +140,7 @@ in
         # We can't use Environment=HOSTNAME=%H, as it doesn't include the domain part.
         export HOSTNAME=$(< /proc/sys/kernel/hostname)
 
-        exec ${cfg.package}/bin/agent -config.expand-env -config.file ${configFile}
+        exec ${lib.getExe cfg.package} -config.expand-env -config.file ${configFile}
       '';
       serviceConfig = {
         Restart = "always";
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix b/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix
index 0c2de683ecf72..f67596f05a3a1 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix
@@ -58,10 +58,10 @@ in
     };
   };
   serviceOpts = let
-    collectSettingsArgs = if (cfg.collectdBinary.enable) then ''
+    collectSettingsArgs = optionalString (cfg.collectdBinary.enable) ''
       --collectd.listen-address ${cfg.collectdBinary.listenAddress}:${toString cfg.collectdBinary.port} \
       --collectd.security-level ${cfg.collectdBinary.securityLevel} \
-    '' else "";
+    '';
   in {
     serviceConfig = {
       ExecStart = ''
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix b/nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix
index f80aeae9c6b7d..50e1321a1e9ce 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix
@@ -4,12 +4,12 @@ with lib;
 
 let
   cfg = config.services.prometheus.exporters.smartctl;
-  args = concatStrings [
-    "--web.listen-address=\"${cfg.listenAddress}:${toString cfg.port}\" "
-    "--smartctl.path=\"${pkgs.smartmontools}/bin/smartctl\" "
-    "--smartctl.interval=\"${cfg.maxInterval}\" "
-    "${concatMapStringsSep " " (device: "--smartctl.device=${device}") cfg.devices}"
-  ] ++ cfg.extraFlags;
+  args = lib.escapeShellArgs ([
+    "--web.listen-address=${cfg.listenAddress}:${toString cfg.port}"
+    "--smartctl.path=${pkgs.smartmontools}/bin/smartctl"
+    "--smartctl.interval=${cfg.maxInterval}"
+  ] ++ map (device: "--smartctl.device=${device}") cfg.devices
+  ++ cfg.extraFlags);
 in {
   port = 9633;
 
diff --git a/nixos/modules/services/network-filesystems/kubo.nix b/nixos/modules/services/network-filesystems/kubo.nix
index 0cb0e126d4c50..2537bb1b8d80f 100644
--- a/nixos/modules/services/network-filesystems/kubo.nix
+++ b/nixos/modules/services/network-filesystems/kubo.nix
@@ -22,6 +22,18 @@ let
 
   configFile = settingsFormat.generate "kubo-config.json" customizedConfig;
 
+  # Create a fake repo containing only the file "api".
+  # $IPFS_PATH will point to this directory instead of the real one.
+  # For some reason the Kubo CLI tools insist on reading the
+  # config file when it exists. But the Kubo daemon sets the file
+  # permissions such that only the ipfs user is allowed to read
+  # this file. This prevents normal users from talking to the daemon.
+  # To work around this terrible design, create a fake repo with no
+  # config file, only an api file and everything should work as expected.
+  fakeKuboRepo = pkgs.writeTextDir "api" ''
+    /unix/run/ipfs.sock
+  '';
+
   kuboFlags = utils.escapeSystemdExecArgs (
     optional cfg.autoMount "--mount" ++
     optional cfg.enableGC "--enable-gc" ++
@@ -38,6 +50,22 @@ let
 
   splitMulitaddr = addrRaw: lib.tail (lib.splitString "/" addrRaw);
 
+  multiaddrsToListenStreams = addrIn:
+    let
+      addrs = if builtins.typeOf addrIn == "list"
+      then addrIn else [ addrIn ];
+      unfilteredResult = map multiaddrToListenStream addrs;
+    in
+      builtins.filter (addr: addr != null) unfilteredResult;
+
+  multiaddrsToListenDatagrams = addrIn:
+    let
+      addrs = if builtins.typeOf addrIn == "list"
+      then addrIn else [ addrIn ];
+      unfilteredResult = map multiaddrToListenDatagram addrs;
+    in
+      builtins.filter (addr: addr != null) unfilteredResult;
+
   multiaddrToListenStream = addrRaw:
     let
       addr = splitMulitaddr addrRaw;
@@ -154,13 +182,18 @@ in
 
           options = {
             Addresses.API = mkOption {
-              type = types.str;
-              default = "/ip4/127.0.0.1/tcp/5001";
-              description = lib.mdDoc "Where Kubo exposes its API to";
+              type = types.oneOf [ types.str (types.listOf types.str) ];
+              default = [ ];
+              description = lib.mdDoc ''
+                Multiaddr or array of multiaddrs describing the address to serve the local HTTP API on.
+                In addition to the multiaddrs listed here, the daemon will also listen on a Unix domain socket.
+                To allow the ipfs CLI tools to communicate with the daemon over that socket,
+                add your user to the correct group, e.g. `users.users.alice.extraGroups = [ config.services.kubo.group ];`
+              '';
             };
 
             Addresses.Gateway = mkOption {
-              type = types.str;
+              type = types.oneOf [ types.str (types.listOf types.str) ];
               default = "/ip4/127.0.0.1/tcp/8080";
               description = lib.mdDoc "Where the IPFS Gateway can be reached";
             };
@@ -248,7 +281,7 @@ in
     ];
 
     environment.systemPackages = [ cfg.package ];
-    environment.variables.IPFS_PATH = cfg.dataDir;
+    environment.variables.IPFS_PATH = fakeKuboRepo;
 
     # https://github.com/lucas-clemente/quic-go/wiki/UDP-Receive-Buffer-Size
     boot.kernel.sysctl."net.core.rmem_max" = mkDefault 2500000;
@@ -319,6 +352,10 @@ in
           # change when the changes are applied. Whyyyyyy.....
           ipfs --offline config replace -
       '';
+      postStop = mkIf cfg.autoMount ''
+        # After an unclean shutdown the fuse mounts at cfg.ipnsMountDir and cfg.ipfsMountDir are locked
+        umount --quiet '${cfg.ipnsMountDir}' '${cfg.ipfsMountDir}' || true
+      '';
       serviceConfig = {
         ExecStart = [ "" "${cfg.package}/bin/ipfs daemon ${kuboFlags}" ];
         User = cfg.user;
@@ -334,27 +371,23 @@ in
       wantedBy = [ "sockets.target" ];
       socketConfig = {
         ListenStream =
-          let
-            fromCfg = multiaddrToListenStream cfg.settings.Addresses.Gateway;
-          in
-          [ "" ] ++ lib.optional (fromCfg != null) fromCfg;
+          [ "" ] ++ (multiaddrsToListenStreams cfg.settings.Addresses.Gateway);
         ListenDatagram =
-          let
-            fromCfg = multiaddrToListenDatagram cfg.settings.Addresses.Gateway;
-          in
-          [ "" ] ++ lib.optional (fromCfg != null) fromCfg;
+          [ "" ] ++ (multiaddrsToListenDatagrams cfg.settings.Addresses.Gateway);
       };
     };
 
     systemd.sockets.ipfs-api = {
       wantedBy = [ "sockets.target" ];
-      # We also include "%t/ipfs.sock" because there is no way to put the "%t"
-      # in the multiaddr.
-      socketConfig.ListenStream =
-        let
-          fromCfg = multiaddrToListenStream cfg.settings.Addresses.API;
-        in
-        [ "" "%t/ipfs.sock" ] ++ lib.optional (fromCfg != null) fromCfg;
+      socketConfig = {
+        # We also include "%t/ipfs.sock" because there is no way to put the "%t"
+        # in the multiaddr.
+        ListenStream =
+          [ "" "%t/ipfs.sock" ] ++ (multiaddrsToListenStreams cfg.settings.Addresses.API);
+        SocketMode = "0660";
+        SocketUser = cfg.user;
+        SocketGroup = cfg.group;
+      };
     };
   };
 
diff --git a/nixos/modules/services/network-filesystems/openafs/lib.nix b/nixos/modules/services/network-filesystems/openafs/lib.nix
index 80628f4dfaf29..e5e147a8dc338 100644
--- a/nixos/modules/services/network-filesystems/openafs/lib.nix
+++ b/nixos/modules/services/network-filesystems/openafs/lib.nix
@@ -1,13 +1,13 @@
 { config, lib, ...}:
 
 let
-  inherit (lib) concatStringsSep mkOption types;
+  inherit (lib) concatStringsSep mkOption types optionalString;
 
 in {
 
   mkCellServDB = cellName: db: ''
     >${cellName}
-  '' + (concatStringsSep "\n" (map (dbm: if (dbm.ip != "" && dbm.dnsname != "") then dbm.ip + " #" + dbm.dnsname else "")
+  '' + (concatStringsSep "\n" (map (dbm: optionalString (dbm.ip != "" && dbm.dnsname != "") "${dbm.ip} #${dbm.dnsname}")
                                    db))
      + "\n";
 
diff --git a/nixos/modules/services/networking/iscsi/root-initiator.nix b/nixos/modules/services/networking/iscsi/root-initiator.nix
index 4434fedce1eb8..895467cc674ab 100644
--- a/nixos/modules/services/networking/iscsi/root-initiator.nix
+++ b/nixos/modules/services/networking/iscsi/root-initiator.nix
@@ -185,6 +185,10 @@ in
         assertion = cfg.loginAll -> cfg.target == null;
         message = "iSCSI target name is set while login on all portals is enabled.";
       }
+      {
+        assertion = !config.boot.initrd.systemd.enable;
+        message = "systemd stage 1 does not support iscsi yet.";
+      }
     ];
   };
 }
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/ndppd.nix b/nixos/modules/services/networking/ndppd.nix
index 98c58d2d5db1b..d221c95ae6200 100644
--- a/nixos/modules/services/networking/ndppd.nix
+++ b/nixos/modules/services/networking/ndppd.nix
@@ -17,7 +17,7 @@ let
       ttl ${toString proxy.ttl}
       ${render proxy.rules (ruleNetworkName: rule: ''
       rule ${prefer rule.network ruleNetworkName} {
-        ${rule.method}${if rule.method == "iface" then " ${rule.interface}" else ""}
+        ${rule.method}${optionalString (rule.method == "iface") " ${rule.interface}"}
       }'')}
     }'')}
   '');
diff --git a/nixos/modules/services/networking/netbird.nix b/nixos/modules/services/networking/netbird.nix
index 5bd9e9ca61696..647c0ce3e6d1f 100644
--- a/nixos/modules/services/networking/netbird.nix
+++ b/nixos/modules/services/networking/netbird.nix
@@ -41,9 +41,10 @@ in {
       documentation = [ "https://netbird.io/docs/" ];
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
+      path = with pkgs; [
+        openresolv
+      ];
       serviceConfig = {
-        AmbientCapabilities = [ "CAP_NET_ADMIN" ];
-        DynamicUser = true;
         Environment = [
           "NB_CONFIG=/var/lib/netbird/config.json"
           "NB_LOG_FILE=console"
diff --git a/nixos/modules/services/networking/ntopng.nix b/nixos/modules/services/networking/ntopng.nix
index e6344d7ff3b34..bf7ec19f02a68 100644
--- a/nixos/modules/services/networking/ntopng.nix
+++ b/nixos/modules/services/networking/ntopng.nix
@@ -86,7 +86,7 @@ in
 
       redis.createInstance = mkOption {
         type = types.nullOr types.str;
-        default = if versionAtLeast config.system.stateVersion "22.05" then "ntopng" else "";
+        default = optionalString (versionAtLeast config.system.stateVersion "22.05") "ntopng";
         description = lib.mdDoc ''
           Local Redis instance name. Set to `null` to disable
           local Redis instance. Defaults to `""` for
diff --git a/nixos/modules/services/networking/peroxide.nix b/nixos/modules/services/networking/peroxide.nix
index 6cac4bf2f89a1..885ee1d96cd05 100644
--- a/nixos/modules/services/networking/peroxide.nix
+++ b/nixos/modules/services/networking/peroxide.nix
@@ -9,7 +9,7 @@ let
 in
 {
   options.services.peroxide = {
-    enable = mkEnableOption (lib.mdDoc "enable");
+    enable = mkEnableOption (lib.mdDoc "peroxide");
 
     package = mkPackageOptionMD pkgs "peroxide" {
       default = [ "peroxide" ];
diff --git a/nixos/modules/services/networking/smokeping.nix b/nixos/modules/services/networking/smokeping.nix
index c2c2a370cb004..19ab3f1aa48c0 100644
--- a/nixos/modules/services/networking/smokeping.nix
+++ b/nixos/modules/services/networking/smokeping.nix
@@ -339,14 +339,9 @@ in
       };
       preStart = ''
         mkdir -m 0755 -p ${smokepingHome}/cache ${smokepingHome}/data
-        rm -f ${smokepingHome}/cropper
-        ln -s ${cfg.package}/htdocs/cropper ${smokepingHome}/cropper
-        rm -f ${smokepingHome}/css
-        ln -s ${cfg.package}/htdocs/css ${smokepingHome}/css
-        rm -f ${smokepingHome}/js
-        ln -s ${cfg.package}/htdocs/js ${smokepingHome}/js
-        rm -f ${smokepingHome}/smokeping.fcgi
-        ln -s ${cgiHome} ${smokepingHome}/smokeping.fcgi
+        ln -sf ${cfg.package}/htdocs/css ${smokepingHome}/css
+        ln -sf ${cfg.package}/htdocs/js ${smokepingHome}/js
+        ln -sf ${cgiHome} ${smokepingHome}/smokeping.fcgi
         ${cfg.package}/bin/smokeping --check --config=${configPath}
         ${cfg.package}/bin/smokeping --static --config=${configPath}
       '';
diff --git a/nixos/modules/services/networking/ssh/lshd.nix b/nixos/modules/services/networking/ssh/lshd.nix
index 7932bac9ca3a1..af64969c2fcd4 100644
--- a/nixos/modules/services/networking/ssh/lshd.nix
+++ b/nixos/modules/services/networking/ssh/lshd.nix
@@ -169,11 +169,11 @@ in
             else (concatStrings (map (i: "--interface=\"${i}\"")
                                      interfaces))} \
           -h "${hostKey}" \
-          ${if !syslog then "--no-syslog" else ""} \
+          ${optionalString (!syslog) "--no-syslog" } \
           ${if passwordAuthentication then "--password" else "--no-password" } \
           ${if publicKeyAuthentication then "--publickey" else "--no-publickey" } \
           ${if rootLogin then "--root-login" else "--no-root-login" } \
-          ${if loginShell != null then "--login-shell=\"${loginShell}\"" else "" } \
+          ${optionalString (loginShell != null) "--login-shell=\"${loginShell}\"" } \
           ${if srpKeyExchange then "--srp-keyexchange" else "--no-srp-keyexchange" } \
           ${if !tcpForwarding then "--no-tcpip-forward" else "--tcpip-forward"} \
           ${if x11Forwarding then "--x11-forward" else "--no-x11-forward" } \
diff --git a/nixos/modules/services/networking/ssh/sshd.nix b/nixos/modules/services/networking/ssh/sshd.nix
index 5f225682b7779..89ddf82152993 100644
--- a/nixos/modules/services/networking/ssh/sshd.nix
+++ b/nixos/modules/services/networking/ssh/sshd.nix
@@ -474,10 +474,10 @@ in
                       mkdir -m 0755 -p "$(dirname '${k.path}')"
                       ssh-keygen \
                         -t "${k.type}" \
-                        ${if k ? bits then "-b ${toString k.bits}" else ""} \
-                        ${if k ? rounds then "-a ${toString k.rounds}" else ""} \
-                        ${if k ? comment then "-C '${k.comment}'" else ""} \
-                        ${if k ? openSSHFormat && k.openSSHFormat then "-o" else ""} \
+                        ${optionalString (k ? bits) "-b ${toString k.bits}"} \
+                        ${optionalString (k ? rounds) "-a ${toString k.rounds}"} \
+                        ${optionalString (k ? comment) "-C '${k.comment}'"} \
+                        ${optionalString (k ? openSSHFormat && k.openSSHFormat) "-o"} \
                         -f "${k.path}" \
                         -N ""
                   fi
@@ -536,7 +536,7 @@ in
     # https://github.com/NixOS/nixpkgs/pull/10155
     # https://github.com/NixOS/nixpkgs/pull/41745
     services.openssh.authorizedKeysFiles =
-      [ "%h/.ssh/authorized_keys" "%h/.ssh/authorized_keys2" "/etc/ssh/authorized_keys.d/%u" ];
+      [ "%h/.ssh/authorized_keys" "/etc/ssh/authorized_keys.d/%u" ];
 
     services.openssh.extraConfig = mkOrder 0
       ''
@@ -550,7 +550,7 @@ in
         '') cfg.ports}
 
         ${concatMapStrings ({ port, addr, ... }: ''
-          ListenAddress ${addr}${if port != null then ":" + toString port else ""}
+          ListenAddress ${addr}${optionalString (port != null) (":" + toString port)}
         '') cfg.listenAddresses}
 
         ${optionalString cfgc.setXAuthLocation ''
diff --git a/nixos/modules/services/networking/strongswan.nix b/nixos/modules/services/networking/strongswan.nix
index 8b1398bfd47d4..e58526814d1ad 100644
--- a/nixos/modules/services/networking/strongswan.nix
+++ b/nixos/modules/services/networking/strongswan.nix
@@ -4,7 +4,7 @@ let
 
   inherit (builtins) toFile;
   inherit (lib) concatMapStringsSep concatStringsSep mapAttrsToList
-                mkIf mkEnableOption mkOption types literalExpression;
+                mkIf mkEnableOption mkOption types literalExpression optionalString;
 
   cfg = config.services.strongswan;
 
@@ -34,8 +34,8 @@ let
 
   strongswanConf = {setup, connections, ca, secretsFile, managePlugins, enabledPlugins}: toFile "strongswan.conf" ''
     charon {
-      ${if managePlugins then "load_modular = no" else ""}
-      ${if managePlugins then ("load = " + (concatStringsSep " " enabledPlugins)) else ""}
+      ${optionalString managePlugins "load_modular = no"}
+      ${optionalString managePlugins ("load = " + (concatStringsSep " " enabledPlugins))}
       plugins {
         stroke {
           secrets_file = ${secretsFile}
diff --git a/nixos/modules/services/networking/stunnel.nix b/nixos/modules/services/networking/stunnel.nix
index 4f592fb312d33..996e9b2253921 100644
--- a/nixos/modules/services/networking/stunnel.nix
+++ b/nixos/modules/services/networking/stunnel.nix
@@ -154,8 +154,8 @@ in
     environment.systemPackages = [ pkgs.stunnel ];
 
     environment.etc."stunnel.cfg".text = ''
-      ${ if cfg.user != null then "setuid = ${cfg.user}" else "" }
-      ${ if cfg.group != null then "setgid = ${cfg.group}" else "" }
+      ${ optionalString (cfg.user != null) "setuid = ${cfg.user}" }
+      ${ optionalString (cfg.group != null) "setgid = ${cfg.group}" }
 
       debug = ${cfg.logLevel}
 
diff --git a/nixos/modules/services/networking/wgautomesh.nix b/nixos/modules/services/networking/wgautomesh.nix
new file mode 100644
index 0000000000000..93227a9b625d0
--- /dev/null
+++ b/nixos/modules/services/networking/wgautomesh.nix
@@ -0,0 +1,161 @@
+{ lib, config, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.wgautomesh;
+  settingsFormat = pkgs.formats.toml { };
+  configFile =
+    # Have to remove nulls manually as TOML generator will not just skip key
+    # if value is null
+    settingsFormat.generate "wgautomesh-config.toml"
+      (filterAttrs (k: v: v != null)
+        (mapAttrs
+          (k: v:
+            if k == "peers"
+            then map (e: filterAttrs (k: v: v != null) e) v
+            else v)
+          cfg.settings));
+  runtimeConfigFile =
+    if cfg.enableGossipEncryption
+    then "/run/wgautomesh/wgautomesh.toml"
+    else configFile;
+in
+{
+  options.services.wgautomesh = {
+    enable = mkEnableOption (mdDoc "the wgautomesh daemon");
+    logLevel = mkOption {
+      type = types.enum [ "trace" "debug" "info" "warn" "error" ];
+      default = "info";
+      description = mdDoc "wgautomesh log level.";
+    };
+    enableGossipEncryption = mkOption {
+      type = types.bool;
+      default = true;
+      description = mdDoc "Enable encryption of gossip traffic.";
+    };
+    gossipSecretFile = mkOption {
+      type = types.path;
+      description = mdDoc ''
+        File containing the shared secret key to use for gossip encryption.
+        Required if `enableGossipEncryption` is set.
+      '';
+    };
+    enablePersistence = mkOption {
+      type = types.bool;
+      default = true;
+      description = mdDoc "Enable persistence of Wireguard peer info between restarts.";
+    };
+    openFirewall = mkOption {
+      type = types.bool;
+      default = true;
+      description = mdDoc "Automatically open gossip port in firewall (recommended).";
+    };
+    settings = mkOption {
+      type = types.submodule {
+        freeformType = settingsFormat.type;
+        options = {
+
+          interface = mkOption {
+            type = types.str;
+            description = mdDoc ''
+              Wireguard interface to manage (it is NOT created by wgautomesh, you
+              should use another NixOS option to create it such as
+              `networking.wireguard.interfaces.wg0 = {...};`).
+            '';
+            example = "wg0";
+          };
+          gossip_port = mkOption {
+            type = types.port;
+            description = mdDoc ''
+              wgautomesh gossip port, this MUST be the same number on all nodes in
+              the wgautomesh network.
+            '';
+            default = 1666;
+          };
+          lan_discovery = mkOption {
+            type = types.bool;
+            default = true;
+            description = mdDoc "Enable discovery of peers on the same LAN using UDP broadcast.";
+          };
+          upnp_forward_external_port = mkOption {
+            type = types.nullOr types.port;
+            default = null;
+            description = mdDoc ''
+              Public port number to try to redirect to this machine's Wireguard
+              daemon using UPnP IGD.
+            '';
+          };
+          peers = mkOption {
+            type = types.listOf (types.submodule {
+              options = {
+                pubkey = mkOption {
+                  type = types.str;
+                  description = mdDoc "Wireguard public key of this peer.";
+                };
+                address = mkOption {
+                  type = types.str;
+                  description = mdDoc ''
+                    Wireguard address of this peer (a single IP address, multliple
+                    addresses or address ranges are not supported).
+                  '';
+                  example = "10.0.0.42";
+                };
+                endpoint = mkOption {
+                  type = types.nullOr types.str;
+                  description = mdDoc ''
+                    Bootstrap endpoint for connecting to this Wireguard peer if no
+                    other address is known or none are working.
+                  '';
+                  default = null;
+                  example = "wgnode.mydomain.example:51820";
+                };
+              };
+            });
+            default = [ ];
+            description = mdDoc "wgautomesh peer list.";
+          };
+        };
+
+      };
+      default = { };
+      description = mdDoc "Configuration for wgautomesh.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.wgautomesh.settings = {
+      gossip_secret_file = mkIf cfg.enableGossipEncryption "$CREDENTIALS_DIRECTORY/gossip_secret";
+      persist_file = mkIf cfg.enablePersistence "/var/lib/wgautomesh/state";
+    };
+
+    systemd.services.wgautomesh = {
+      path = [ pkgs.wireguard-tools ];
+      environment = { RUST_LOG = "wgautomesh=${cfg.logLevel}"; };
+      description = "wgautomesh";
+      serviceConfig = {
+        Type = "simple";
+
+        ExecStart = "${getExe pkgs.wgautomesh} ${runtimeConfigFile}";
+        Restart = "always";
+        RestartSec = "30";
+        LoadCredential = mkIf cfg.enableGossipEncryption [ "gossip_secret:${cfg.gossipSecretFile}" ];
+
+        ExecStartPre = mkIf cfg.enableGossipEncryption [
+          ''${pkgs.envsubst}/bin/envsubst \
+              -i ${configFile} \
+              -o ${runtimeConfigFile}''
+        ];
+
+        DynamicUser = true;
+        StateDirectory = "wgautomesh";
+        StateDirectoryMode = "0700";
+        RuntimeDirectory = "wgautomesh";
+        AmbientCapabilities = "CAP_NET_ADMIN";
+        CapabilityBoundingSet = "CAP_NET_ADMIN";
+      };
+      wantedBy = [ "multi-user.target" ];
+    };
+    networking.firewall.allowedUDPPorts =
+      mkIf cfg.openFirewall [ cfg.settings.gossip_port ];
+  };
+}
+
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/networking/xinetd.nix b/nixos/modules/services/networking/xinetd.nix
index b9120f37ba247..fb3de7077e31e 100644
--- a/nixos/modules/services/networking/xinetd.nix
+++ b/nixos/modules/services/networking/xinetd.nix
@@ -27,7 +27,7 @@ let
         ${optionalString srv.unlisted "type        = UNLISTED"}
         ${optionalString (srv.flags != "") "flags = ${srv.flags}"}
         socket_type = ${if srv.protocol == "udp" then "dgram" else "stream"}
-        ${if srv.port != 0 then "port        = ${toString srv.port}" else ""}
+        ${optionalString (srv.port != 0) "port        = ${toString srv.port}"}
         wait        = ${if srv.protocol == "udp" then "yes" else "no"}
         user        = ${srv.user}
         server      = ${srv.server}
diff --git a/nixos/modules/services/printing/cupsd.nix b/nixos/modules/services/printing/cupsd.nix
index 9ac89e057620b..f6a23fb900f08 100644
--- a/nixos/modules/services/printing/cupsd.nix
+++ b/nixos/modules/services/printing/cupsd.nix
@@ -317,6 +317,7 @@ in
     environment.etc.cups.source = "/var/lib/cups";
 
     services.dbus.packages = [ cups.out ] ++ optional polkitEnabled cups-pk-helper;
+    services.udev.packages = cfg.drivers;
 
     # Allow asswordless printer admin for members of wheel group
     security.polkit.extraConfig = mkIf polkitEnabled ''
diff --git a/nixos/modules/services/search/qdrant.nix b/nixos/modules/services/search/qdrant.nix
index a843c44dbb5f9..e1f7365d951a0 100644
--- a/nixos/modules/services/search/qdrant.nix
+++ b/nixos/modules/services/search/qdrant.nix
@@ -100,6 +100,7 @@ in {
       after = [ "network.target" ];
 
       serviceConfig = {
+        LimitNOFILE=65536;
         ExecStart = "${pkgs.qdrant}/bin/qdrant --config-path ${configFile}";
         DynamicUser = true;
         Restart = "on-failure";
diff --git a/nixos/modules/services/security/authelia.nix b/nixos/modules/services/security/authelia.nix
index 143c441c7e153..28c5fd0a1df59 100644
--- a/nixos/modules/services/security/authelia.nix
+++ b/nixos/modules/services/security/authelia.nix
@@ -336,7 +336,7 @@ in
             ProtectProc = "noaccess";
             ProtectSystem = "strict";
 
-            RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+            RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
             RestrictNamespaces = true;
             RestrictRealtime = true;
             RestrictSUIDSGID = true;
diff --git a/nixos/modules/services/security/fail2ban.nix b/nixos/modules/services/security/fail2ban.nix
index ead24d1470717..93962d40ce4b4 100644
--- a/nixos/modules/services/security/fail2ban.nix
+++ b/nixos/modules/services/security/fail2ban.nix
@@ -78,6 +78,13 @@ in
         '';
       };
 
+      bantime = mkOption {
+        default = null;
+        type = types.nullOr types.str;
+        example = "10m";
+        description = lib.mdDoc "Number of seconds that a host is banned.";
+      };
+
       maxretry = mkOption {
         default = 3;
         type = types.ints.unsigned;
@@ -202,6 +209,20 @@ in
        '';
       };
 
+      extraSettings = mkOption {
+        type = with types; attrsOf (oneOf [ bool ints.positive str ]);
+        default = {};
+        description = lib.mdDoc ''
+          Extra default configuration for all jails (i.e. `[DEFAULT]`). See
+          <https://github.com/fail2ban/fail2ban/blob/master/config/jail.conf> for an overview.
+        '';
+        example = literalExpression ''
+          {
+            findtime = "15m";
+          }
+        '';
+      };
+
       jails = mkOption {
         default = { };
         example = literalExpression ''
@@ -320,11 +341,18 @@ in
       ''}
       # Miscellaneous options
       ignoreip    = 127.0.0.1/8 ${optionalString config.networking.enableIPv6 "::1"} ${concatStringsSep " " cfg.ignoreIP}
+      ${optionalString (cfg.bantime != null) ''
+        bantime     = ${cfg.bantime}
+      ''}
       maxretry    = ${toString cfg.maxretry}
       backend     = systemd
       # Actions
       banaction   = ${cfg.banaction}
       banaction_allports = ${cfg.banaction-allports}
+      ${optionalString (cfg.extraSettings != {}) ''
+        # Extra settings
+        ${generators.toKeyValue {} cfg.extraSettings}
+      ''}
     '';
     # Block SSH if there are too many failing connection attempts.
     # Benefits from verbose sshd logging to observe failed login attempts,
diff --git a/nixos/modules/services/security/kanidm.nix b/nixos/modules/services/security/kanidm.nix
index 5583c39368f77..2f19decb5cb17 100644
--- a/nixos/modules/services/security/kanidm.nix
+++ b/nixos/modules/services/security/kanidm.nix
@@ -7,6 +7,18 @@ let
   serverConfigFile = settingsFormat.generate "server.toml" (filterConfig cfg.serverSettings);
   clientConfigFile = settingsFormat.generate "kanidm-config.toml" (filterConfig cfg.clientSettings);
   unixConfigFile = settingsFormat.generate "kanidm-unixd.toml" (filterConfig cfg.unixSettings);
+  certPaths = builtins.map builtins.dirOf [ cfg.serverSettings.tls_chain cfg.serverSettings.tls_key ];
+
+  # Merge bind mount paths and remove paths where a prefix is already mounted.
+  # This makes sure that if e.g. the tls_chain is in the nix store and /nix/store is alread in the mount
+  # paths, no new bind mount is added. Adding subpaths caused problems on ofborg.
+  hasPrefixInList = list: newPath: lib.any (path: lib.hasPrefix (builtins.toString path) (builtins.toString newPath)) list;
+  mergePaths = lib.foldl' (merged: newPath: let
+      # If the new path is a prefix to some existing path, we need to filter it out
+      filteredPaths = lib.filter (p: !lib.hasPrefix (builtins.toString newPath) (builtins.toString p)) merged;
+      # If a prefix of the new path is already in the list, do not add it
+      filteredNew = if hasPrefixInList filteredPaths newPath then [] else [ newPath ];
+    in filteredPaths ++ filteredNew) [];
 
   defaultServiceConfig = {
     BindReadOnlyPaths = [
@@ -16,7 +28,7 @@ let
       "-/etc/hosts"
       "-/etc/localtime"
     ];
-    CapabilityBoundingSet = "";
+    CapabilityBoundingSet = [];
     # ProtectClock= adds DeviceAllow=char-rtc r
     DeviceAllow = "";
     # Implies ProtectSystem=strict, which re-mounts all paths
@@ -216,22 +228,28 @@ in
       description = "kanidm identity management daemon";
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" ];
-      serviceConfig = defaultServiceConfig // {
-        StateDirectory = "kanidm";
-        StateDirectoryMode = "0700";
-        ExecStart = "${pkgs.kanidm}/bin/kanidmd server -c ${serverConfigFile}";
-        User = "kanidm";
-        Group = "kanidm";
+      serviceConfig = lib.mkMerge [
+        # Merge paths and ignore existing prefixes needs to sidestep mkMerge
+        (defaultServiceConfig // {
+          BindReadOnlyPaths = mergePaths (defaultServiceConfig.BindReadOnlyPaths ++ certPaths);
+        })
+        {
+          StateDirectory = "kanidm";
+          StateDirectoryMode = "0700";
+          ExecStart = "${pkgs.kanidm}/bin/kanidmd server -c ${serverConfigFile}";
+          User = "kanidm";
+          Group = "kanidm";
 
-        AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
-        CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
-        # This would otherwise override the CAP_NET_BIND_SERVICE capability.
-        PrivateUsers = false;
-        # Port needs to be exposed to the host network
-        PrivateNetwork = false;
-        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
-        TemporaryFileSystem = "/:ro";
-      };
+          AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
+          CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
+          # This would otherwise override the CAP_NET_BIND_SERVICE capability.
+          PrivateUsers = lib.mkForce false;
+          # Port needs to be exposed to the host network
+          PrivateNetwork = lib.mkForce false;
+          RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+          TemporaryFileSystem = "/:ro";
+        }
+      ];
       environment.RUST_LOG = "info";
     };
 
@@ -240,34 +258,32 @@ in
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" ];
       restartTriggers = [ unixConfigFile clientConfigFile ];
-      serviceConfig = defaultServiceConfig // {
-        CacheDirectory = "kanidm-unixd";
-        CacheDirectoryMode = "0700";
-        RuntimeDirectory = "kanidm-unixd";
-        ExecStart = "${pkgs.kanidm}/bin/kanidm_unixd";
-        User = "kanidm-unixd";
-        Group = "kanidm-unixd";
+      serviceConfig = lib.mkMerge [
+        defaultServiceConfig
+        {
+          CacheDirectory = "kanidm-unixd";
+          CacheDirectoryMode = "0700";
+          RuntimeDirectory = "kanidm-unixd";
+          ExecStart = "${pkgs.kanidm}/bin/kanidm_unixd";
+          User = "kanidm-unixd";
+          Group = "kanidm-unixd";
 
-        BindReadOnlyPaths = [
-          "/nix/store"
-          "-/etc/resolv.conf"
-          "-/etc/nsswitch.conf"
-          "-/etc/hosts"
-          "-/etc/localtime"
-          "-/etc/kanidm"
-          "-/etc/static/kanidm"
-          "-/etc/ssl"
-          "-/etc/static/ssl"
-        ];
-        BindPaths = [
-          # To create the socket
-          "/run/kanidm-unixd:/var/run/kanidm-unixd"
-        ];
-        # Needs to connect to kanidmd
-        PrivateNetwork = false;
-        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
-        TemporaryFileSystem = "/:ro";
-      };
+          BindReadOnlyPaths = [
+            "-/etc/kanidm"
+            "-/etc/static/kanidm"
+            "-/etc/ssl"
+            "-/etc/static/ssl"
+          ];
+          BindPaths = [
+            # To create the socket
+            "/run/kanidm-unixd:/var/run/kanidm-unixd"
+          ];
+          # Needs to connect to kanidmd
+          PrivateNetwork = lib.mkForce false;
+          RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+          TemporaryFileSystem = "/:ro";
+        }
+      ];
       environment.RUST_LOG = "info";
     };
 
diff --git a/nixos/modules/services/security/oauth2_proxy.nix b/nixos/modules/services/security/oauth2_proxy.nix
index e3f8e75ca2476..12547acabfe05 100644
--- a/nixos/modules/services/security/oauth2_proxy.nix
+++ b/nixos/modules/services/security/oauth2_proxy.nix
@@ -72,15 +72,14 @@ let
   } // (getProviderOptions cfg cfg.provider) // cfg.extraConfig;
 
   mapConfig = key: attr:
-  if attr != null && attr != [] then (
+  optionalString (attr != null && attr != []) (
     if isDerivation attr then mapConfig key (toString attr) else
     if (builtins.typeOf attr) == "set" then concatStringsSep " "
       (mapAttrsToList (name: value: mapConfig (key + "-" + name) value) attr) else
     if (builtins.typeOf attr) == "list" then concatMapStringsSep " " (mapConfig key) attr else
     if (builtins.typeOf attr) == "bool" then "--${key}=${boolToString attr}" else
     if (builtins.typeOf attr) == "string" then "--${key}='${attr}'" else
-    "--${key}=${toString attr}")
-    else "";
+    "--${key}=${toString attr}");
 
   configString = concatStringsSep " " (mapAttrsToList mapConfig allConfig);
 in
diff --git a/nixos/modules/services/system/cachix-agent/default.nix b/nixos/modules/services/system/cachix-agent/default.nix
index 11769d4e3095f..06494ddb631af 100644
--- a/nixos/modules/services/system/cachix-agent/default.nix
+++ b/nixos/modules/services/system/cachix-agent/default.nix
@@ -72,7 +72,7 @@ in {
         EnvironmentFile = cfg.credentialsFile;
         ExecStart = ''
           ${cfg.package}/bin/cachix ${lib.optionalString cfg.verbose "--verbose"} ${lib.optionalString (cfg.host != null) "--host ${cfg.host}"} \
-            deploy agent ${cfg.name} ${if cfg.profile != null then cfg.profile else ""}
+            deploy agent ${cfg.name} ${optionalString (cfg.profile != null) cfg.profile}
         '';
       };
     };
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/system/dbus.nix b/nixos/modules/services/system/dbus.nix
index c677088101f0c..9d8a62ec78c53 100644
--- a/nixos/modules/services/system/dbus.nix
+++ b/nixos/modules/services/system/dbus.nix
@@ -14,13 +14,17 @@ let
     serviceDirectories = cfg.packages;
   };
 
-  inherit (lib) mkOption mkIf mkMerge types;
+  inherit (lib) mkOption mkEnableOption mkIf mkMerge types;
 
 in
 
 {
   options = {
 
+    boot.initrd.systemd.dbus = {
+      enable = mkEnableOption (lib.mdDoc "dbus in stage 1") // { visible = false; };
+    };
+
     services.dbus = {
 
       enable = mkOption {
@@ -111,6 +115,21 @@ in
       ];
     }
 
+    (mkIf config.boot.initrd.systemd.dbus.enable {
+      boot.initrd.systemd = {
+        users.messagebus = { };
+        groups.messagebus = { };
+        contents."/etc/dbus-1".source = pkgs.makeDBusConf {
+          inherit (cfg) apparmor;
+          suidHelper = "/bin/false";
+          serviceDirectories = [ pkgs.dbus ];
+        };
+        packages = [ pkgs.dbus ];
+        storePaths = [ "${pkgs.dbus}/bin/dbus-daemon" ];
+        targets.sockets.wants = [ "dbus.socket" ];
+      };
+    })
+
     (mkIf (cfg.implementation == "dbus") {
       environment.systemPackages = [
         pkgs.dbus
diff --git a/nixos/modules/services/video/rtsp-simple-server.nix b/nixos/modules/services/video/mediamtx.nix
index 2dd62edab7871..18a9e3d5fe305 100644
--- a/nixos/modules/services/video/rtsp-simple-server.nix
+++ b/nixos/modules/services/video/mediamtx.nix
@@ -3,19 +3,19 @@
 with lib;
 
 let
-  cfg = config.services.rtsp-simple-server;
-  package = pkgs.rtsp-simple-server;
+  cfg = config.services.mediamtx;
+  package = pkgs.mediamtx;
   format = pkgs.formats.yaml {};
 in
 {
   options = {
-    services.rtsp-simple-server = {
-      enable = mkEnableOption (lib.mdDoc "RTSP Simple Server");
+    services.mediamtx = {
+      enable = mkEnableOption (lib.mdDoc "MediaMTX");
 
       settings = mkOption {
         description = lib.mdDoc ''
-          Settings for rtsp-simple-server.
-          Read more at <https://github.com/aler9/rtsp-simple-server/blob/main/rtsp-simple-server.yml>
+          Settings for MediaMTX.
+          Read more at <https://github.com/aler9/mediamtx/blob/main/mediamtx.yml>
         '';
         type = format.type;
 
@@ -25,7 +25,7 @@ in
             "stdout"
           ];
           # we set this so when the user uses it, it just works (see LogsDirectory below). but it's not used by default.
-          logFile = "/var/log/rtsp-simple-server/rtsp-simple-server.log";
+          logFile = "/var/log/mediamtx/mediamtx.log";
         };
 
         example = {
@@ -40,20 +40,20 @@ in
 
       env = mkOption {
         type = with types; attrsOf anything;
-        description = lib.mdDoc "Extra environment variables for RTSP Simple Server";
+        description = lib.mdDoc "Extra environment variables for MediaMTX";
         default = {};
         example = {
-          RTSP_CONFKEY = "mykey";
+          MTX_CONFKEY = "mykey";
         };
       };
     };
   };
 
   config = mkIf (cfg.enable) {
-    # NOTE: rtsp-simple-server watches this file and automatically reloads if it changes
-    environment.etc."rtsp-simple-server.yaml".source = format.generate "rtsp-simple-server.yaml" cfg.settings;
+    # NOTE: mediamtx watches this file and automatically reloads if it changes
+    environment.etc."mediamtx.yaml".source = format.generate "mediamtx.yaml" cfg.settings;
 
-    systemd.services.rtsp-simple-server = {
+    systemd.services.mediamtx = {
       environment = cfg.env;
 
       after = [ "network.target" ];
@@ -65,15 +65,15 @@ in
 
       serviceConfig = {
         DynamicUser = true;
-        User = "rtsp-simple-server";
-        Group = "rtsp-simple-server";
+        User = "mediamtx";
+        Group = "mediamtx";
 
-        LogsDirectory = "rtsp-simple-server";
+        LogsDirectory = "mediamtx";
 
         # user likely may want to stream cameras, can't hurt to add video group
         SupplementaryGroups = "video";
 
-        ExecStart = "${package}/bin/rtsp-simple-server /etc/rtsp-simple-server.yaml";
+        ExecStart = "${package}/bin/mediamtx /etc/mediamtx.yaml";
       };
     };
   };
diff --git a/nixos/modules/services/web-apps/discourse.nix b/nixos/modules/services/web-apps/discourse.nix
index 151fb812ddea6..5b2bd5aeeb09c 100644
--- a/nixos/modules/services/web-apps/discourse.nix
+++ b/nixos/modules/services/web-apps/discourse.nix
@@ -1025,8 +1025,8 @@ in
 
     services.postfix = lib.mkIf cfg.mail.incoming.enable {
       enable = true;
-      sslCert = if cfg.sslCertificate != null then cfg.sslCertificate else "";
-      sslKey = if cfg.sslCertificateKey != null then cfg.sslCertificateKey else "";
+      sslCert = lib.optionalString (cfg.sslCertificate != null) cfg.sslCertificate;
+      sslKey = lib.optionalString (cfg.sslCertificateKey != null) cfg.sslCertificateKey;
 
       origin = cfg.hostname;
       relayDomains = [ cfg.hostname ];
diff --git a/nixos/modules/services/web-apps/ihatemoney/default.nix b/nixos/modules/services/web-apps/ihatemoney/default.nix
deleted file mode 100644
index a61aa445f82c5..0000000000000
--- a/nixos/modules/services/web-apps/ihatemoney/default.nix
+++ /dev/null
@@ -1,153 +0,0 @@
-{ config, pkgs, lib, ... }:
-with lib;
-let
-  cfg = config.services.ihatemoney;
-  user = "ihatemoney";
-  group = "ihatemoney";
-  db = "ihatemoney";
-  python3 = config.services.uwsgi.package.python3;
-  pkg = python3.pkgs.ihatemoney;
-  toBool = x: if x then "True" else "False";
-  configFile = pkgs.writeText "ihatemoney.cfg" ''
-        from secrets import token_hex
-        # load a persistent secret key
-        SECRET_KEY_FILE = "/var/lib/ihatemoney/secret_key"
-        SECRET_KEY = ""
-        try:
-          with open(SECRET_KEY_FILE) as f:
-            SECRET_KEY = f.read()
-        except FileNotFoundError:
-          pass
-        if not SECRET_KEY:
-          print("ihatemoney: generating a new secret key")
-          SECRET_KEY = token_hex(50)
-          with open(SECRET_KEY_FILE, "w") as f:
-            f.write(SECRET_KEY)
-        del token_hex
-        del SECRET_KEY_FILE
-
-        # "normal" configuration
-        DEBUG = False
-        SQLALCHEMY_DATABASE_URI = '${
-          if cfg.backend == "sqlite"
-          then "sqlite:////var/lib/ihatemoney/ihatemoney.sqlite"
-          else "postgresql:///${db}"}'
-        SQLALCHEMY_TRACK_MODIFICATIONS = False
-        MAIL_DEFAULT_SENDER = (r"${cfg.defaultSender.name}", r"${cfg.defaultSender.email}")
-        ACTIVATE_DEMO_PROJECT = ${toBool cfg.enableDemoProject}
-        ADMIN_PASSWORD = r"${toString cfg.adminHashedPassword /*toString null == ""*/}"
-        ALLOW_PUBLIC_PROJECT_CREATION = ${toBool cfg.enablePublicProjectCreation}
-        ACTIVATE_ADMIN_DASHBOARD = ${toBool cfg.enableAdminDashboard}
-        SESSION_COOKIE_SECURE = ${toBool cfg.secureCookie}
-        ENABLE_CAPTCHA = ${toBool cfg.enableCaptcha}
-        LEGAL_LINK = r"${toString cfg.legalLink}"
-
-        ${cfg.extraConfig}
-  '';
-in
-  {
-    options.services.ihatemoney = {
-      enable = mkEnableOption (lib.mdDoc "ihatemoney webapp. Note that this will set uwsgi to emperor mode");
-      backend = mkOption {
-        type = types.enum [ "sqlite" "postgresql" ];
-        default = "sqlite";
-        description = lib.mdDoc ''
-          The database engine to use for ihatemoney.
-          If `postgresql` is selected, then a database called
-          `${db}` will be created. If you disable this option,
-          it will however not be removed.
-        '';
-      };
-      adminHashedPassword = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        description = lib.mdDoc "The hashed password of the administrator. To obtain it, run `ihatemoney generate_password_hash`";
-      };
-      uwsgiConfig = mkOption {
-        type = types.attrs;
-        example = {
-          http = ":8000";
-        };
-        description = lib.mdDoc "Additional configuration of the UWSGI vassal running ihatemoney. It should notably specify on which interfaces and ports the vassal should listen.";
-      };
-      defaultSender = {
-        name = mkOption {
-          type = types.str;
-          default = "Budget manager";
-          description = lib.mdDoc "The display name of the sender of ihatemoney emails";
-        };
-        email = mkOption {
-          type = types.str;
-          default = "ihatemoney@${config.networking.hostName}";
-          defaultText = literalExpression ''"ihatemoney@''${config.networking.hostName}"'';
-          description = lib.mdDoc "The email of the sender of ihatemoney emails";
-        };
-      };
-      secureCookie = mkOption {
-        type = types.bool;
-        default = true;
-        description = lib.mdDoc "Use secure cookies. Disable this when ihatemoney is served via http instead of https";
-      };
-      enableDemoProject = mkEnableOption (lib.mdDoc "access to the demo project in ihatemoney");
-      enablePublicProjectCreation = mkEnableOption (lib.mdDoc "permission to create projects in ihatemoney by anyone");
-      enableAdminDashboard = mkEnableOption (lib.mdDoc "ihatemoney admin dashboard");
-      enableCaptcha = mkEnableOption (lib.mdDoc "a simplistic captcha for some forms");
-      legalLink = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        description = lib.mdDoc "The URL to a page explaining legal statements about your service, eg. GDPR-related information.";
-      };
-      extraConfig = mkOption {
-        type = types.str;
-        default = "";
-        description = lib.mdDoc "Extra configuration appended to ihatemoney's configuration file. It is a python file, so pay attention to indentation.";
-      };
-    };
-    config = mkIf cfg.enable {
-      services.postgresql = mkIf (cfg.backend == "postgresql") {
-        enable = true;
-        ensureDatabases = [ db ];
-        ensureUsers = [ {
-          name = user;
-          ensurePermissions = {
-            "DATABASE ${db}" = "ALL PRIVILEGES";
-          };
-        } ];
-      };
-      systemd.services.postgresql = mkIf (cfg.backend == "postgresql") {
-        wantedBy = [ "uwsgi.service" ];
-        before = [ "uwsgi.service" ];
-      };
-      systemd.tmpfiles.rules = [
-        "d /var/lib/ihatemoney 770 ${user} ${group}"
-      ];
-      users = {
-        users.${user} = {
-          isSystemUser = true;
-          inherit group;
-        };
-        groups.${group} = {};
-      };
-      services.uwsgi = {
-        enable = true;
-        plugins = [ "python3" ];
-        instance = {
-          type = "emperor";
-          vassals.ihatemoney = {
-            type = "normal";
-            strict = true;
-            immediate-uid = user;
-            immediate-gid = group;
-            # apparently flask uses threads: https://github.com/spiral-project/ihatemoney/commit/c7815e48781b6d3a457eaff1808d179402558f8c
-            enable-threads = true;
-            module = "wsgi:application";
-            chdir = "${pkg}/${pkg.pythonModule.sitePackages}/ihatemoney";
-            env = [ "IHATEMONEY_SETTINGS_FILE_PATH=${configFile}" ];
-            pythonPackages = self: [ self.ihatemoney ];
-          } // cfg.uwsgiConfig;
-        };
-      };
-    };
-  }
-
-
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/moodle.nix b/nixos/modules/services/web-apps/moodle.nix
index 5f8d9c5b15f4c..b617e9a593795 100644
--- a/nixos/modules/services/web-apps/moodle.nix
+++ b/nixos/modules/services/web-apps/moodle.nix
@@ -56,7 +56,7 @@ let
   mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql";
   pgsqlLocal = cfg.database.createLocally && cfg.database.type == "pgsql";
 
-  phpExt = pkgs.php80.buildEnv {
+  phpExt = pkgs.php81.buildEnv {
     extensions = { all, ... }: with all; [ iconv mbstring curl openssl tokenizer soap ctype zip gd simplexml dom intl sqlite3 pgsql pdo_sqlite pdo_pgsql pdo_odbc pdo_mysql pdo mysqli session zlib xmlreader fileinfo filter opcache exif sodium ];
     extraConfig = "max_input_vars = 5000";
   };
diff --git a/nixos/modules/services/web-apps/nextcloud.md b/nixos/modules/services/web-apps/nextcloud.md
index 7ef3cca281f9e..15c1f2da2724b 100644
--- a/nixos/modules/services/web-apps/nextcloud.md
+++ b/nixos/modules/services/web-apps/nextcloud.md
@@ -132,7 +132,9 @@ Auto updates for Nextcloud apps can be enabled using
     Nextcloud supports [server-side encryption (SSE)](https://docs.nextcloud.com/server/latest/admin_manual/configuration_files/encryption_configuration.html).
     This is not an end-to-end encryption, but can be used to encrypt files that will be persisted
     to external storage such as S3. Please note that this won't work anymore when using OpenSSL 3
-    for PHP's openssl extension because this is implemented using the legacy cipher RC4.
+    for PHP's openssl extension and **Nextcloud 25 or older** because this is implemented using the
+    legacy cipher RC4. For Nextcloud26 this isn't relevant anymore, because Nextcloud has an RC4 implementation
+    written in native PHP and thus doesn't need `ext-openssl` for that anymore.
     If [](#opt-system.stateVersion) is *above* `22.05`,
     this is disabled by default. To turn it on again and for further information please refer to
     [](#opt-services.nextcloud.enableBrokenCiphersForSSE).
diff --git a/nixos/modules/services/web-apps/nextcloud.nix b/nixos/modules/services/web-apps/nextcloud.nix
index 76a0172747ffd..2824b7ee24562 100644
--- a/nixos/modules/services/web-apps/nextcloud.nix
+++ b/nixos/modules/services/web-apps/nextcloud.nix
@@ -204,7 +204,7 @@ in {
     package = mkOption {
       type = types.package;
       description = lib.mdDoc "Which package to use for the Nextcloud instance.";
-      relatedPackages = [ "nextcloud24" "nextcloud25" "nextcloud26" ];
+      relatedPackages = [ "nextcloud25" "nextcloud26" ];
     };
     phpPackage = mkOption {
       type = types.package;
@@ -712,6 +712,10 @@ in {
           See <https://docs.nextcloud.com/server/latest/admin_manual/configuration_files/encryption_configuration.html#disabling-encryption> on how to achieve this.
 
           For more context, here is the implementing pull request: https://github.com/NixOS/nixpkgs/pull/198470
+        '')
+        ++ (optional (cfg.enableBrokenCiphersForSSE && versionAtLeast cfg.package.version "26") ''
+          Nextcloud26 supports RC4 without requiring legacy OpenSSL, so
+          `services.nextcloud.enableBrokenCiphersForSSE` can be set to `false`.
         '');
 
       services.nextcloud.package = with pkgs;
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/modules/services/web-servers/fcgiwrap.nix b/nixos/modules/services/web-servers/fcgiwrap.nix
index f9c91fb35db23..3a57ef383065b 100644
--- a/nixos/modules/services/web-servers/fcgiwrap.nix
+++ b/nixos/modules/services/web-servers/fcgiwrap.nix
@@ -54,7 +54,7 @@ in {
 
       serviceConfig = {
         ExecStart = "${pkgs.fcgiwrap}/sbin/fcgiwrap -c ${builtins.toString cfg.preforkProcesses} ${
-          if (cfg.socketType != "unix") then "-s ${cfg.socketType}:${cfg.socketAddress}" else ""
+          optionalString (cfg.socketType != "unix") "-s ${cfg.socketType}:${cfg.socketAddress}"
         }";
       } // (if cfg.user != null && cfg.group != null then {
         User = cfg.user;
diff --git a/nixos/modules/services/web-servers/lighttpd/default.nix b/nixos/modules/services/web-servers/lighttpd/default.nix
index 811afe8e0af66..0438e12e7da82 100644
--- a/nixos/modules/services/web-servers/lighttpd/default.nix
+++ b/nixos/modules/services/web-servers/lighttpd/default.nix
@@ -64,7 +64,7 @@ let
   ];
 
   maybeModuleString = moduleName:
-    if elem moduleName cfg.enableModules then ''"${moduleName}"'' else "";
+    optionalString (elem moduleName cfg.enableModules) ''"${moduleName}"'';
 
   modulesIncludeString = concatStringsSep ",\n"
     (filter (x: x != "") (map maybeModuleString allKnownModules));
@@ -106,15 +106,15 @@ let
       static-file.exclude-extensions = ( ".fcgi", ".php", ".rb", "~", ".inc" )
       index-file.names = ( "index.html" )
 
-      ${if cfg.mod_userdir then ''
+      ${optionalString cfg.mod_userdir ''
         userdir.path = "public_html"
-      '' else ""}
+      ''}
 
-      ${if cfg.mod_status then ''
+      ${optionalString cfg.mod_status ''
         status.status-url = "/server-status"
         status.statistics-url = "/server-statistics"
         status.config-url = "/server-config"
-      '' else ""}
+      ''}
 
       ${cfg.extraConfig}
     '';
diff --git a/nixos/modules/services/web-servers/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix
index 3d19186e1a9d3..1e6cb0d374053 100644
--- a/nixos/modules/services/web-servers/nginx/default.nix
+++ b/nixos/modules/services/web-servers/nginx/default.nix
@@ -318,7 +318,7 @@ let
 
         listenString = { addr, port, ssl, extraParameters ? [], ... }:
           # UDP listener for QUIC transport protocol.
-          (if ssl && vhost.quic then "
+          (optionalString (ssl && vhost.quic) ("
             listen ${addr}:${toString port} quic "
           + optionalString vhost.default "default_server "
           + optionalString vhost.reuseport "reuseport "
@@ -326,7 +326,7 @@ let
             let inCompatibleParameters = [ "ssl" "proxy_protocol" "http2" ];
                 isCompatibleParameter = param: !(any (p: p == param) inCompatibleParameters);
             in filter isCompatibleParameter extraParameters))
-          + ";" else "")
+          + ";"))
           + "
 
             listen ${addr}:${toString port} "
diff --git a/nixos/modules/services/web-servers/tomcat.nix b/nixos/modules/services/web-servers/tomcat.nix
index d8bfee547c79a..4d2c36287be69 100644
--- a/nixos/modules/services/web-servers/tomcat.nix
+++ b/nixos/modules/services/web-servers/tomcat.nix
@@ -234,11 +234,11 @@ in
           ln -sfn ${tomcat}/conf/$i ${cfg.baseDir}/conf/`basename $i`
         done
 
-        ${if cfg.extraConfigFiles != [] then ''
+        ${optionalString (cfg.extraConfigFiles != []) ''
           for i in ${toString cfg.extraConfigFiles}; do
             ln -sfn $i ${cfg.baseDir}/conf/`basename $i`
           done
-        '' else ""}
+        ''}
 
         # Create a modified catalina.properties file
         # Change all references from CATALINA_HOME to CATALINA_BASE and add support for shared libraries
@@ -345,7 +345,7 @@ in
 
           # Symlink all the given web applications files or paths into the webapps/ directory
           # of this virtual host
-          for i in "${if virtualHost ? webapps then toString virtualHost.webapps else ""}"; do
+          for i in "${optionalString (virtualHost ? webapps) (toString virtualHost.webapps)}"; do
             if [ -f $i ]; then
               # If the given web application is a file, symlink it into the webapps/ directory
               ln -sfn $i ${cfg.baseDir}/virtualhosts/${virtualHost.name}/webapps/`basename $i`
diff --git a/nixos/modules/services/x11/desktop-managers/plasma5.nix b/nixos/modules/services/x11/desktop-managers/plasma5.nix
index 73a864bb95fe8..38f932ffb4206 100644
--- a/nixos/modules/services/x11/desktop-managers/plasma5.nix
+++ b/nixos/modules/services/x11/desktop-managers/plasma5.nix
@@ -429,7 +429,8 @@ in
             dolphin-plugins
             ffmpegthumbs
             kdegraphics-thumbnailers
-            pkgs.kio-admin
+            kde-inotify-survey
+            kio-admin
             kio-extras
           ];
           optionalPackages = [