about summary refs log tree commit diff
path: root/nixos/modules
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/modules')
-rw-r--r--nixos/modules/hardware/opengl.nix41
-rw-r--r--nixos/modules/hardware/video/webcam/ipu6.nix57
-rw-r--r--nixos/modules/installer/tools/nixos-generate-config.pl4
-rw-r--r--nixos/modules/misc/ids.nix2
-rw-r--r--nixos/modules/module-list.nix15
-rw-r--r--nixos/modules/profiles/installation-device.nix10
-rw-r--r--nixos/modules/programs/clash-verge.nix41
-rw-r--r--nixos/modules/programs/hyprland.nix84
-rw-r--r--nixos/modules/programs/minipro.nix29
-rw-r--r--nixos/modules/programs/neovim.nix47
-rw-r--r--nixos/modules/programs/regreet.nix2
-rw-r--r--nixos/modules/programs/ssh.nix7
-rw-r--r--nixos/modules/programs/tmux.nix5
-rw-r--r--nixos/modules/programs/tsm-client.nix6
-rw-r--r--nixos/modules/programs/zsh/zsh.nix3
-rw-r--r--nixos/modules/rename.nix2
-rw-r--r--nixos/modules/security/ipa.nix258
-rw-r--r--nixos/modules/services/backup/borgbackup.nix10
-rw-r--r--nixos/modules/services/backup/borgmatic.nix3
-rw-r--r--nixos/modules/services/cluster/kubernetes/kubelet.nix8
-rw-r--r--nixos/modules/services/computing/boinc/client.nix2
-rw-r--r--nixos/modules/services/continuous-integration/buildbot/master.nix11
-rw-r--r--nixos/modules/services/continuous-integration/buildbot/worker.nix7
-rw-r--r--nixos/modules/services/development/lorri.nix2
-rw-r--r--nixos/modules/services/hardware/auto-cpufreq.nix21
-rw-r--r--nixos/modules/services/home-automation/esphome.nix136
-rw-r--r--nixos/modules/services/mail/maddy.nix38
-rw-r--r--nixos/modules/services/mail/roundcube.nix2
-rw-r--r--nixos/modules/services/misc/gitit.nix725
-rw-r--r--nixos/modules/services/misc/gpsd.nix46
-rw-r--r--nixos/modules/services/misc/pufferpanel.nix176
-rw-r--r--nixos/modules/services/monitoring/grafana-agent.nix2
-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/server.nix86
-rw-r--r--nixos/modules/services/networking/ddclient.nix4
-rw-r--r--nixos/modules/services/networking/dhcpd.nix7
-rw-r--r--nixos/modules/services/networking/go-neb.nix3
-rw-r--r--nixos/modules/services/networking/ivpn.nix51
-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/sshd.nix2
-rw-r--r--nixos/modules/services/networking/wgautomesh.nix161
-rw-r--r--nixos/modules/services/networking/wstunnel.nix4
-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/fail2ban.nix10
-rw-r--r--nixos/modules/services/security/kanidm.nix102
-rw-r--r--nixos/modules/services/system/cachix-watch-store.nix6
-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/video/v4l2-relayd.nix199
-rw-r--r--nixos/modules/services/web-apps/ihatemoney/default.nix153
-rw-r--r--nixos/modules/services/web-apps/mastodon.nix7
-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/plausible.nix14
-rw-r--r--nixos/modules/services/web-servers/nginx/default.nix2
-rw-r--r--nixos/modules/services/x11/desktop-managers/pantheon.nix2
-rw-r--r--nixos/modules/services/x11/desktop-managers/plasma5.nix3
-rwxr-xr-xnixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py8
-rw-r--r--nixos/modules/system/boot/resolved.nix4
-rw-r--r--nixos/modules/system/boot/tmp.nix81
-rw-r--r--nixos/modules/virtualisation/cri-o.nix7
-rw-r--r--nixos/modules/virtualisation/multipass.nix4
-rw-r--r--nixos/modules/virtualisation/proxmox-image.nix3
-rw-r--r--nixos/modules/virtualisation/qemu-vm.nix4
66 files changed, 2157 insertions, 1174 deletions
diff --git a/nixos/modules/hardware/opengl.nix b/nixos/modules/hardware/opengl.nix
index 7b5e669d47f4..9108bcbd1652 100644
--- a/nixos/modules/hardware/opengl.nix
+++ b/nixos/modules/hardware/opengl.nix
@@ -69,48 +69,19 @@ in
       package = mkOption {
         type = types.package;
         internal = true;
-        default = cfg.mesaPackage;
         description = lib.mdDoc ''
           The package that provides the OpenGL implementation.
-
-          The default is Mesa's drivers which should cover all OpenGL-capable
-          hardware. If you want to use another Mesa version, adjust
-          {option}`mesaPackage`.
         '';
       };
+
       package32 = mkOption {
         type = types.package;
         internal = true;
-        default = cfg.mesaPackage32;
-        description = lib.mdDoc ''
-          Same as {option}`package` but for the 32-bit OpenGL implementation on
-          64-bit systems. Used when {option}`driSupport32Bit` is set.
-        '';
-      };
-
-      mesaPackage = mkOption {
-        type = types.package;
-        default = pkgs.mesa;
-        defaultText = literalExpression "pkgs.mesa";
-        example = literalExpression "pkgs.mesa_22";
         description = lib.mdDoc ''
-          The Mesa driver package used for rendering support on the system.
-
-          You should only need to adjust this if you require a newer Mesa
-          version for your hardware or because you need to patch a bug.
+          The package that provides the 32-bit OpenGL implementation on
+          64-bit systems. Used when {option}`driSupport32Bit` is
+          set.
         '';
-        apply = mesa: mesa.drivers or (throw "`mesa` package must have a `drivers` output.");
-      };
-      mesaPackage32 = mkOption {
-        type = types.package;
-        default = pkgs.pkgsi686Linux.mesa;
-        defaultText = literalExpression "pkgs.pkgsi686Linux.mesa";
-        example = literalExpression "pkgs.pkgsi686Linux.mesa_22";
-        description = lib.mdDoc ''
-          Same as {option}`mesaPackage` but for the 32-bit Mesa on 64-bit
-          systems. Used when {option}`driSupport32Bit` is set.
-        '';
-        apply = mesa: mesa.drivers or (throw "`mesa` package must have a `drivers` output.");
       };
 
       extraPackages = mkOption {
@@ -126,6 +97,7 @@ in
           :::
         '';
       };
+
       extraPackages32 = mkOption {
         type = types.listOf types.package;
         default = [];
@@ -181,6 +153,9 @@ in
     environment.sessionVariables.LD_LIBRARY_PATH = mkIf cfg.setLdLibraryPath
       ([ "/run/opengl-driver/lib" ] ++ optional cfg.driSupport32Bit "/run/opengl-driver-32/lib");
 
+    hardware.opengl.package = mkDefault pkgs.mesa.drivers;
+    hardware.opengl.package32 = mkDefault pkgs.pkgsi686Linux.mesa.drivers;
+
     boot.extraModulePackages = optional (elem "virtualbox" videoDrivers) kernelPackages.virtualboxGuestAdditions;
   };
 }
diff --git a/nixos/modules/hardware/video/webcam/ipu6.nix b/nixos/modules/hardware/video/webcam/ipu6.nix
new file mode 100644
index 000000000000..fce78cda34c7
--- /dev/null
+++ b/nixos/modules/hardware/video/webcam/ipu6.nix
@@ -0,0 +1,57 @@
+{ config, lib, pkgs, ... }:
+let
+
+  inherit (lib) mkDefault mkEnableOption mkIf mkOption optional types;
+
+  cfg = config.hardware.ipu6;
+
+in
+{
+
+  options.hardware.ipu6 = {
+
+    enable = mkEnableOption (lib.mdDoc "support for Intel IPU6/MIPI cameras");
+
+    platform = mkOption {
+      type = types.enum [ "ipu6" "ipu6ep" ];
+      description = lib.mdDoc ''
+        Choose the version for your hardware platform.
+
+        Use `ipu6` for Tiger Lake and `ipu6ep` for Alder Lake respectively.
+      '';
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    boot.extraModulePackages = with config.boot.kernelPackages; [
+      ipu6-drivers
+    ];
+
+    hardware.firmware = with pkgs; [ ]
+      ++ optional (cfg.platform == "ipu6") ipu6-camera-bin
+      ++ optional (cfg.platform == "ipu6ep") ipu6ep-camera-bin;
+
+    services.udev.extraRules = ''
+      SUBSYSTEM=="intel-ipu6-psys", MODE="0660", GROUP="video"
+    '';
+
+    services.v4l2-relayd.instances.ipu6 = {
+      enable = mkDefault true;
+
+      cardLabel = mkDefault "Intel MIPI Camera";
+
+      extraPackages = with pkgs.gst_all_1; [ ]
+        ++ optional (cfg.platform == "ipu6") icamerasrc-ipu6
+        ++ optional (cfg.platform == "ipu6ep") icamerasrc-ipu6ep;
+
+      input = {
+        pipeline = "icamerasrc";
+        format = mkIf (cfg.platform == "ipu6ep") (mkDefault "NV12");
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/installer/tools/nixos-generate-config.pl b/nixos/modules/installer/tools/nixos-generate-config.pl
index 946e73dac586..a082ed3450e9 100644
--- a/nixos/modules/installer/tools/nixos-generate-config.pl
+++ b/nixos/modules/installer/tools/nixos-generate-config.pl
@@ -200,7 +200,7 @@ sub pciCheck {
     }
 
     # In case this is a virtio scsi device, we need to explicitly make this available.
-    if ($vendor eq "0x1af4" && $device eq "0x1004") {
+    if ($vendor eq "0x1af4" && ($device eq "0x1004" || $device eq "0x1048") ) {
         push @initrdAvailableKernelModules, "virtio_scsi";
     }
 
@@ -473,7 +473,7 @@ EOF
     }
 
     # Don't emit tmpfs entry for /tmp, because it most likely comes from the
-    # boot.tmpOnTmpfs option in configuration.nix (managed declaratively).
+    # boot.tmp.useTmpfs option in configuration.nix (managed declaratively).
     next if ($mountPoint eq "/tmp" && $fsType eq "tmpfs");
 
     # Emit the filesystem.
diff --git a/nixos/modules/misc/ids.nix b/nixos/modules/misc/ids.nix
index bed50b81604d..5b278b5e8062 100644
--- a/nixos/modules/misc/ids.nix
+++ b/nixos/modules/misc/ids.nix
@@ -233,7 +233,7 @@ in
       # nix-serve = 199; # unused, removed 2020-12-12
       #tvheadend = 200; # dynamically allocated as of 2021-09-18
       uwsgi = 201;
-      gitit = 202;
+      # gitit = 202; # unused, module was removed 2023-04-03
       riemanntools = 203;
       subsonic = 204;
       # riak = 205; # unused, remove 2022-07-22
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index ed6e36eb5733..f1c459f75570 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -99,6 +99,7 @@
   ./hardware/video/switcheroo-control.nix
   ./hardware/video/uvcvideo/default.nix
   ./hardware/video/webcam/facetimehd.nix
+  ./hardware/video/webcam/ipu6.nix
   ./hardware/wooting.nix
   ./hardware/xone.nix
   ./hardware/xpadneo.nix
@@ -148,6 +149,7 @@
   ./programs/cdemu.nix
   ./programs/cfs-zen-tweaks.nix
   ./programs/chromium.nix
+  ./programs/clash-verge.nix
   ./programs/cnping.nix
   ./programs/command-not-found/command-not-found.nix
   ./programs/criu.nix
@@ -179,6 +181,7 @@
   ./programs/haguichi.nix
   ./programs/hamster.nix
   ./programs/htop.nix
+  ./programs/hyprland.nix
   ./programs/iay.nix
   ./programs/iftop.nix
   ./programs/i3lock.nix
@@ -195,6 +198,7 @@
   ./programs/mdevctl.nix
   ./programs/mepo.nix
   ./programs/mininet.nix
+  ./programs/minipro.nix
   ./programs/miriway.nix
   ./programs/mosh.nix
   ./programs/msmtp.nix
@@ -276,6 +280,7 @@
   ./security/doas.nix
   ./security/duosec.nix
   ./security/google_oslogin.nix
+  ./security/ipa.nix
   ./security/lock-kernel-modules.nix
   ./security/misc.nix
   ./security/oath.nix
@@ -510,6 +515,7 @@
   ./services/hardware/usbrelayd.nix
   ./services/hardware/vdr.nix
   ./services/hardware/keyd.nix
+  ./services/home-automation/esphome.nix
   ./services/home-automation/evcc.nix
   ./services/home-automation/home-assistant.nix
   ./services/home-automation/zigbee2mqtt.nix
@@ -612,7 +618,6 @@
   ./services/misc/gammu-smsd.nix
   ./services/misc/geoipupdate.nix
   ./services/misc/gitea.nix
-  # ./services/misc/gitit.nix
   ./services/misc/gitlab.nix
   ./services/misc/gitolite.nix
   ./services/misc/gitweb.nix
@@ -665,6 +670,7 @@
   ./services/misc/polaris.nix
   ./services/misc/portunus.nix
   ./services/misc/prowlarr.nix
+  ./services/misc/pufferpanel.nix
   ./services/misc/pykms.nix
   ./services/misc/radarr.nix
   ./services/misc/readarr.nix
@@ -879,6 +885,7 @@
   ./services/networking/iscsi/initiator.nix
   ./services/networking/iscsi/root-initiator.nix
   ./services/networking/iscsi/target.nix
+  ./services/networking/ivpn.nix
   ./services/networking/iwd.nix
   ./services/networking/jibri/default.nix
   ./services/networking/jicofo.nix
@@ -1037,6 +1044,7 @@
   ./services/networking/wg-netmanager.nix
   ./services/networking/webhook.nix
   ./services/networking/wg-quick.nix
+  ./services/networking/wgautomesh.nix
   ./services/networking/wireguard.nix
   ./services/networking/wpa_supplicant.nix
   ./services/networking/wstunnel.nix
@@ -1127,8 +1135,9 @@
   ./services/video/epgstation/default.nix
   ./services/video/mirakurun.nix
   ./services/video/replay-sorcery.nix
-  ./services/video/rtsp-simple-server.nix
+  ./services/video/mediamtx.nix
   ./services/video/unifi-video.nix
+  ./services/video/v4l2-relayd.nix
   ./services/wayland/cage.nix
   ./services/web-apps/akkoma.nix
   ./services/web-apps/alps.nix
@@ -1161,7 +1170,6 @@
   ./services/web-apps/hledger-web.nix
   ./services/web-apps/icingaweb2/icingaweb2.nix
   ./services/web-apps/icingaweb2/module-monitoring.nix
-  ./services/web-apps/ihatemoney
   ./services/web-apps/invidious.nix
   ./services/web-apps/invoiceplane.nix
   ./services/web-apps/isso.nix
@@ -1177,6 +1185,7 @@
   ./services/web-apps/mattermost.nix
   ./services/web-apps/mediawiki.nix
   ./services/web-apps/miniflux.nix
+  ./services/web-apps/monica.nix
   ./services/web-apps/moodle.nix
   ./services/web-apps/netbox.nix
   ./services/web-apps/nextcloud.nix
diff --git a/nixos/modules/profiles/installation-device.nix b/nixos/modules/profiles/installation-device.nix
index 980720691a43..32884f4b8754 100644
--- a/nixos/modules/profiles/installation-device.nix
+++ b/nixos/modules/profiles/installation-device.nix
@@ -52,9 +52,9 @@ with lib;
     services.getty.helpLine = ''
       The "nixos" and "root" accounts have empty passwords.
 
-      An ssh daemon is running. You then must set a password
-      for either "root" or "nixos" with `passwd` or add an ssh key
-      to /home/nixos/.ssh/authorized_keys be able to login.
+      To log in over ssh you must set a password for either "nixos" or "root"
+      with `passwd` (prefix with `sudo` for "root"), or add your public key to
+      /home/nixos/.ssh/authorized_keys or /root/.ssh/authorized_keys.
 
       If you need a wireless connection, type
       `sudo systemctl start wpa_supplicant` and configure a
@@ -65,8 +65,8 @@ with lib;
       start the graphical user interface.
     '';
 
-    # We run sshd by default. Login via root is only possible after adding a
-    # password via "passwd" or by adding a ssh key to /home/nixos/.ssh/authorized_keys.
+    # We run sshd by default. Login is only possible after adding a
+    # password via "passwd" or by adding a ssh key to ~/.ssh/authorized_keys.
     # The latter one is particular useful if keys are manually added to
     # installation device for head-less systems i.e. arm boards by manually
     # mounting the storage in a different system.
diff --git a/nixos/modules/programs/clash-verge.nix b/nixos/modules/programs/clash-verge.nix
new file mode 100644
index 000000000000..29977be3858f
--- /dev/null
+++ b/nixos/modules/programs/clash-verge.nix
@@ -0,0 +1,41 @@
+{ config, lib, pkgs, ... }:
+
+{
+  options.programs.clash-verge = {
+    enable = lib.mkEnableOption (lib.mdDoc ''
+      Clash Verge.
+    '');
+
+    autoStart = lib.mkEnableOption (lib.mdDoc ''
+      Clash Verge Auto Launch.
+    '');
+
+    tunMode = lib.mkEnableOption (lib.mdDoc ''
+      Clash Verge Tun Mode.
+    '');
+  };
+
+  config =
+    let
+      cfg = config.programs.clash-verge;
+    in
+    lib.mkIf cfg.enable {
+
+      environment.systemPackages = [
+        pkgs.clash-verge
+        (lib.mkIf cfg.autoStart (pkgs.makeAutostartItem {
+          name = "clash-verge";
+          package = pkgs.clash-verge;
+        }))
+      ];
+
+      security.wrappers.clash-verge = lib.mkIf cfg.tunMode {
+        owner = "root";
+        group = "root";
+        capabilities = "cap_net_bind_service,cap_net_admin=+ep";
+        source = "${lib.getExe pkgs.clash-verge}";
+      };
+    };
+
+  meta.maintainers = with lib.maintainers; [ zendo ];
+}
diff --git a/nixos/modules/programs/hyprland.nix b/nixos/modules/programs/hyprland.nix
new file mode 100644
index 000000000000..b14f1f77fcf8
--- /dev/null
+++ b/nixos/modules/programs/hyprland.nix
@@ -0,0 +1,84 @@
+{ config
+, lib
+, pkgs
+, ...
+}:
+with lib; let
+  cfg = config.programs.hyprland;
+
+  defaultHyprlandPackage = pkgs.hyprland.override {
+    enableXWayland = cfg.xwayland.enable;
+    hidpiXWayland = cfg.xwayland.hidpi;
+    nvidiaPatches = cfg.nvidiaPatches;
+  };
+in
+{
+  options.programs.hyprland = {
+    enable = mkEnableOption null // {
+      description = mdDoc ''
+        Hyprland, the dynamic tiling Wayland compositor that doesn't sacrifice on its looks.
+
+        You can manually launch Hyprland by executing {command}`Hyprland` on a TTY.
+
+        A configuration file will be generated in {file}`~/.config/hypr/hyprland.conf`.
+        See <https://wiki.hyprland.org> for more information.
+      '';
+    };
+
+    package = mkOption {
+      type = types.path;
+      default = defaultHyprlandPackage;
+      defaultText = literalExpression ''
+        pkgs.hyprland.override {
+          enableXWayland = config.programs.hyprland.xwayland.enable;
+          hidpiXWayland = config.programs.hyprland.xwayland.hidpi;
+          nvidiaPatches = config.programs.hyprland.nvidiaPatches;
+        }
+      '';
+      example = literalExpression "<Hyprland flake>.packages.<system>.default";
+      description = mdDoc ''
+        The Hyprland package to use.
+        Setting this option will make {option}`programs.hyprland.xwayland` and
+        {option}`programs.hyprland.nvidiaPatches` not work.
+      '';
+    };
+
+    xwayland = {
+      enable = mkEnableOption (mdDoc "XWayland") // { default = true; };
+      hidpi = mkEnableOption null // {
+        description = mdDoc ''
+          Enable HiDPI XWayland, based on [XWayland MR 733](https://gitlab.freedesktop.org/xorg/xserver/-/merge_requests/733).
+          See <https://wiki.hyprland.org/Nix/Options-Overrides/#xwayland-hidpi> for more info.
+        '';
+      };
+    };
+
+    nvidiaPatches = mkEnableOption (mdDoc "patching wlroots for better Nvidia support");
+  };
+
+  config = mkIf cfg.enable {
+    environment = {
+      systemPackages = [ cfg.package ];
+
+    };
+
+    fonts.enableDefaultFonts = mkDefault true;
+    hardware.opengl.enable = mkDefault true;
+
+    programs = {
+      dconf.enable = mkDefault true;
+      xwayland.enable = mkDefault true;
+    };
+
+    security.polkit.enable = true;
+
+    services.xserver.displayManager.sessionPackages = [ cfg.package ];
+
+    xdg.portal = {
+      enable = mkDefault true;
+      extraPortals = [
+        pkgs.xdg-desktop-portal-hyprland
+      ];
+    };
+  };
+}
diff --git a/nixos/modules/programs/minipro.nix b/nixos/modules/programs/minipro.nix
new file mode 100644
index 000000000000..a947f83f2ee0
--- /dev/null
+++ b/nixos/modules/programs/minipro.nix
@@ -0,0 +1,29 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.programs.minipro;
+in
+{
+  options = {
+    programs.minipro = {
+      enable = lib.mkEnableOption (lib.mdDoc "minipro") // {
+        description = lib.mdDoc ''
+          Installs minipro and its udev rules.
+          Users of the `plugdev` group can interact with connected MiniPRO chip programmers.
+        '';
+      };
+
+      package = lib.mkPackageOptionMD pkgs "minipro" { };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    users.groups.plugdev = { };
+    environment.systemPackages = [ cfg.package ];
+    services.udev.packages = [ cfg.package ];
+  };
+
+  meta = {
+    maintainers = with lib.maintainers; [ infinidoge ];
+  };
+}
diff --git a/nixos/modules/programs/neovim.nix b/nixos/modules/programs/neovim.nix
index 4562e5a2c29b..3f0e9fc173bd 100644
--- a/nixos/modules/programs/neovim.nix
+++ b/nixos/modules/programs/neovim.nix
@@ -4,12 +4,8 @@ with lib;
 
 let
   cfg = config.programs.neovim;
-
-  runtime' = filter (f: f.enable) (attrValues cfg.runtime);
-
-  runtime = pkgs.linkFarm "neovim-runtime" (map (x: { name = "etc/${x.target}"; path = x.source; }) runtime');
-
-in {
+in
+{
   options.programs.neovim = {
     enable = mkOption {
       type = types.bool;
@@ -70,7 +66,7 @@ in {
 
     configure = mkOption {
       type = types.attrs;
-      default = {};
+      default = { };
       example = literalExpression ''
         {
           customRC = '''
@@ -105,7 +101,7 @@ in {
     };
 
     runtime = mkOption {
-      default = {};
+      default = { };
       example = literalExpression ''
         { "ftplugin/c.vim".text = "setlocal omnifunc=v:lua.vim.lsp.omnifunc"; }
       '';
@@ -115,14 +111,15 @@ in {
 
       type = with types; attrsOf (submodule (
         { name, config, ... }:
-        { options = {
+        {
+          options = {
 
             enable = mkOption {
               type = types.bool;
               default = true;
               description = lib.mdDoc ''
-                Whether this /etc file should be generated.  This
-                option allows specific /etc files to be disabled.
+                Whether this runtime directory should be generated.  This
+                option allows specific runtime files to be disabled.
               '';
             };
 
@@ -147,14 +144,9 @@ in {
 
           };
 
-          config = {
-            target = mkDefault name;
-            source = mkIf (config.text != null) (
-              let name' = "neovim-runtime" + baseNameOf name;
-              in mkDefault (pkgs.writeText name' config.text));
-          };
-
-        }));
+          config.target = mkDefault name;
+        }
+      ));
 
     };
   };
@@ -165,14 +157,17 @@ in {
     ];
     environment.variables.EDITOR = mkIf cfg.defaultEditor (mkOverride 900 "nvim");
 
-    programs.neovim.finalPackage = pkgs.wrapNeovim cfg.package {
-      inherit (cfg) viAlias vimAlias withPython3 withNodeJs withRuby;
-      configure = cfg.configure // {
+    environment.etc = listToAttrs (attrValues (mapAttrs
+      (name: value: {
+        name = "xdg/nvim/${name}";
+        value = value // {
+          target = "xdg/nvim/${value.target}";
+        };
+      })
+      cfg.runtime));
 
-        customRC = (cfg.configure.customRC or "") + ''
-          set runtimepath^=${runtime}/etc
-        '';
-      };
+    programs.neovim.finalPackage = pkgs.wrapNeovim cfg.package {
+      inherit (cfg) viAlias vimAlias withPython3 withNodeJs withRuby configure;
     };
   };
 }
diff --git a/nixos/modules/programs/regreet.nix b/nixos/modules/programs/regreet.nix
index 89b93737f4a2..f6c750a45bf5 100644
--- a/nixos/modules/programs/regreet.nix
+++ b/nixos/modules/programs/regreet.nix
@@ -50,7 +50,7 @@ in
   config = lib.mkIf cfg.enable {
     services.greetd = {
       enable = lib.mkDefault true;
-      settings.default_session.command = lib.mkDefault "${lib.getExe pkgs.cage} -s -- ${lib.getExe cfg.package}";
+      settings.default_session.command = lib.mkDefault "${pkgs.dbus}/bin/dbus-run-session ${lib.getExe pkgs.cage} -s -- ${lib.getExe cfg.package}";
     };
 
     environment.etc = {
diff --git a/nixos/modules/programs/ssh.nix b/nixos/modules/programs/ssh.nix
index 1ec698820a8b..7c85d1e7c3d5 100644
--- a/nixos/modules/programs/ssh.nix
+++ b/nixos/modules/programs/ssh.nix
@@ -26,7 +26,7 @@ let
       + (if h.publicKey != null then h.publicKey else readFile h.publicKeyFile)
     )) + "\n";
 
-  knownHostsFiles = [ "/etc/ssh/ssh_known_hosts" "/etc/ssh/ssh_known_hosts2" ]
+  knownHostsFiles = [ "/etc/ssh/ssh_known_hosts" ]
     ++ map pkgs.copyPathToStore cfg.knownHostsFiles;
 
 in
@@ -232,9 +232,8 @@ in
         description = lib.mdDoc ''
           Files containing SSH host keys to set as global known hosts.
           `/etc/ssh/ssh_known_hosts` (which is
-          generated by {option}`programs.ssh.knownHosts`) and
-          `/etc/ssh/ssh_known_hosts2` are always
-          included.
+          generated by {option}`programs.ssh.knownHosts`) is
+          always included.
         '';
         example = literalExpression ''
           [
diff --git a/nixos/modules/programs/tmux.nix b/nixos/modules/programs/tmux.nix
index 2e5c8c30e0f8..4f452f1d7f9b 100644
--- a/nixos/modules/programs/tmux.nix
+++ b/nixos/modules/programs/tmux.nix
@@ -160,7 +160,10 @@ in {
         default = defaultTerminal;
         example = "screen-256color";
         type = types.str;
-        description = lib.mdDoc "Set the $TERM variable.";
+        description = lib.mdDoc ''
+          Set the $TERM variable. Use tmux-direct if italics or 24bit true color
+          support is needed.
+        '';
       };
 
       secureSocket = mkOption {
diff --git a/nixos/modules/programs/tsm-client.nix b/nixos/modules/programs/tsm-client.nix
index 7adff7cd28cb..41560544c2c7 100644
--- a/nixos/modules/programs/tsm-client.nix
+++ b/nixos/modules/programs/tsm-client.nix
@@ -6,7 +6,7 @@ let
   inherit (lib.attrsets) attrNames filterAttrs hasAttr mapAttrs mapAttrsToList optionalAttrs;
   inherit (lib.modules) mkDefault mkIf;
   inherit (lib.options) literalExpression mkEnableOption mkOption;
-  inherit (lib.strings) concatStringsSep optionalString toLower;
+  inherit (lib.strings) concatLines optionalString toLower;
   inherit (lib.types) addCheck attrsOf lines nonEmptyStr nullOr package path port str strMatching submodule;
 
   # Checks if given list of strings contains unique
@@ -164,7 +164,7 @@ let
         mkLine = k: v: k + optionalString (v!="") "  ${v}";
         lines = mapAttrsToList mkLine attrset;
       in
-        concatStringsSep "\n" lines;
+        concatLines lines;
     config.stanza = ''
       server  ${config.name}
       ${config.text}
@@ -263,7 +263,7 @@ let
 
     ${optionalString (cfg.defaultServername!=null) "defaultserver  ${cfg.defaultServername}"}
 
-    ${concatStringsSep "\n" (mapAttrsToList (k: v: v.stanza) cfg.servers)}
+    ${concatLines (mapAttrsToList (k: v: v.stanza) cfg.servers)}
   '';
 
 in
diff --git a/nixos/modules/programs/zsh/zsh.nix b/nixos/modules/programs/zsh/zsh.nix
index 0b152e54cf95..6bb21cb3ef66 100644
--- a/nixos/modules/programs/zsh/zsh.nix
+++ b/nixos/modules/programs/zsh/zsh.nix
@@ -236,6 +236,9 @@ in
           setopt ${concatStringsSep " " cfg.setOptions}
         ''}
 
+        # Alternative method of determining short and full hostname.
+        HOST=${config.networking.fqdnOrHostName}
+
         # Setup command line history.
         # Don't export these, otherwise other shells (bash) will try to use same HISTFILE.
         SAVEHIST=${toString cfg.histSize}
diff --git a/nixos/modules/rename.nix b/nixos/modules/rename.nix
index 158c7934195b..45a27029dff1 100644
--- a/nixos/modules/rename.nix
+++ b/nixos/modules/rename.nix
@@ -58,6 +58,7 @@ with lib;
     (mkRemovedOptionModule [ "services" "fourStoreEndpoint" ] "The fourStoreEndpoint module has been removed")
     (mkRemovedOptionModule [ "services" "fprot" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "frab" ] "The frab module has been removed")
+    (mkRemovedOptionModule [ "services" "ihatemoney" ] "The ihatemoney module has been removed for lack of downstream maintainer")
     (mkRemovedOptionModule [ "services" "kippo" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "mailpile" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "marathon" ] "The corresponding package was removed from nixpkgs.")
@@ -106,6 +107,7 @@ with lib;
     (mkRemovedOptionModule [ "services" "openfire" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "riak" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "cryptpad" ] "The corresponding package was removed from nixpkgs.")
+    (mkRemovedOptionModule [ "services" "rtsp-simple-server" ] "Package has been completely rebranded by upstream as mediamtx, and thus the service and the package were renamed in NixOS as well.")
 
     (mkRemovedOptionModule [ "i18n" "inputMethod" "fcitx" ] "The fcitx module has been removed. Plesae use fcitx5 instead")
 
diff --git a/nixos/modules/security/ipa.nix b/nixos/modules/security/ipa.nix
new file mode 100644
index 000000000000..7075be95040e
--- /dev/null
+++ b/nixos/modules/security/ipa.nix
@@ -0,0 +1,258 @@
+{
+  config,
+  lib,
+  pkgs,
+  ...
+}:
+with lib; let
+  cfg = config.security.ipa;
+  pyBool = x:
+    if x
+    then "True"
+    else "False";
+
+  ldapConf = pkgs.writeText "ldap.conf" ''
+    # Turning this off breaks GSSAPI used with krb5 when rdns = false
+    SASL_NOCANON    on
+
+    URI ldaps://${cfg.server}
+    BASE ${cfg.basedn}
+    TLS_CACERT /etc/ipa/ca.crt
+  '';
+  nssDb =
+    pkgs.runCommand "ipa-nssdb"
+    {
+      nativeBuildInputs = [pkgs.nss.tools];
+    } ''
+      mkdir -p $out
+      certutil -d $out -N --empty-password
+      certutil -d $out -A --empty-password -n "${cfg.realm} IPA CA" -t CT,C,C -i ${cfg.certificate}
+    '';
+in {
+  options = {
+    security.ipa = {
+      enable = mkEnableOption (lib.mdDoc "FreeIPA domain integration");
+
+      certificate = mkOption {
+        type = types.package;
+        description = lib.mdDoc ''
+          IPA server CA certificate.
+
+          Use `nix-prefetch-url http://$server/ipa/config/ca.crt` to
+          obtain the file and the hash.
+        '';
+        example = literalExpression ''
+          pkgs.fetchurl {
+            url = http://ipa.example.com/ipa/config/ca.crt;
+            sha256 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
+          };
+        '';
+      };
+
+      domain = mkOption {
+        type = types.str;
+        example = "example.com";
+        description = lib.mdDoc "Domain of the IPA server.";
+      };
+
+      realm = mkOption {
+        type = types.str;
+        example = "EXAMPLE.COM";
+        description = lib.mdDoc "Kerberos realm.";
+      };
+
+      server = mkOption {
+        type = types.str;
+        example = "ipa.example.com";
+        description = lib.mdDoc "IPA Server hostname.";
+      };
+
+      basedn = mkOption {
+        type = types.str;
+        example = "dc=example,dc=com";
+        description = lib.mdDoc "Base DN to use when performing LDAP operations.";
+      };
+
+      offlinePasswords = mkOption {
+        type = types.bool;
+        default = true;
+        description = lib.mdDoc "Whether to store offline passwords when the server is down.";
+      };
+
+      cacheCredentials = mkOption {
+        type = types.bool;
+        default = true;
+        description = lib.mdDoc "Whether to cache credentials.";
+      };
+
+      ifpAllowedUids = mkOption {
+        type = types.listOf types.string;
+        default = ["root"];
+        description = lib.mdDoc "A list of users allowed to access the ifp dbus interface.";
+      };
+
+      dyndns = {
+        enable = mkOption {
+          type = types.bool;
+          default = true;
+          description = lib.mdDoc "Whether to enable FreeIPA automatic hostname updates.";
+        };
+
+        interface = mkOption {
+          type = types.str;
+          example = "eth0";
+          default = "*";
+          description = lib.mdDoc "Network interface to perform hostname updates through.";
+        };
+      };
+
+      chromiumSupport = mkOption {
+        type = types.bool;
+        default = true;
+        description = lib.mdDoc "Whether to whitelist the FreeIPA domain in Chromium.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = !config.krb5.enable;
+        message = "krb5 must be disabled through `krb5.enable` for FreeIPA integration to work.";
+      }
+      {
+        assertion = !config.users.ldap.enable;
+        message = "ldap must be disabled through `users.ldap.enable` for FreeIPA integration to work.";
+      }
+    ];
+
+    environment.systemPackages = with pkgs; [krb5Full freeipa];
+
+    environment.etc = {
+      "ipa/default.conf".text = ''
+        [global]
+        basedn = ${cfg.basedn}
+        realm = ${cfg.realm}
+        domain = ${cfg.domain}
+        server = ${cfg.server}
+        host = ${config.networking.hostName}
+        xmlrpc_uri = https://${cfg.server}/ipa/xml
+        enable_ra = True
+      '';
+
+      "ipa/nssdb".source = nssDb;
+
+      "krb5.conf".text = ''
+        [libdefaults]
+         default_realm = ${cfg.realm}
+         dns_lookup_realm = false
+         dns_lookup_kdc = true
+         rdns = false
+         ticket_lifetime = 24h
+         forwardable = true
+         udp_preference_limit = 0
+
+        [realms]
+         ${cfg.realm} = {
+          kdc = ${cfg.server}:88
+          master_kdc = ${cfg.server}:88
+          admin_server = ${cfg.server}:749
+          default_domain = ${cfg.domain}
+          pkinit_anchors = FILE:/etc/ipa/ca.crt
+        }
+
+        [domain_realm]
+         .${cfg.domain} = ${cfg.realm}
+         ${cfg.domain} = ${cfg.realm}
+         ${cfg.server} = ${cfg.realm}
+
+        [dbmodules]
+          ${cfg.realm} = {
+            db_library = ${pkgs.freeipa}/lib/krb5/plugins/kdb/ipadb.so
+          }
+      '';
+
+      "openldap/ldap.conf".source = ldapConf;
+    };
+
+    environment.etc."chromium/policies/managed/freeipa.json" = mkIf cfg.chromiumSupport {
+      text = ''
+        { "AuthServerWhitelist": "*.${cfg.domain}" }
+      '';
+    };
+
+    system.activationScripts.ipa = stringAfter ["etc"] ''
+      # libcurl requires a hard copy of the certificate
+      if ! ${pkgs.diffutils}/bin/diff ${cfg.certificate} /etc/ipa/ca.crt > /dev/null 2>&1; then
+        rm -f /etc/ipa/ca.crt
+        cp ${cfg.certificate} /etc/ipa/ca.crt
+      fi
+
+      if [ ! -f /etc/krb5.keytab ]; then
+        cat <<EOF
+
+          In order to complete FreeIPA integration, please join the domain by completing the following steps:
+          1. Authenticate as an IPA user authorized to join new hosts, e.g. kinit admin@${cfg.realm}
+          2. Join the domain and obtain the keytab file: ipa-join
+          3. Install the keytab file: sudo install -m 600 krb5.keytab /etc/
+          4. Restart sssd systemd service: sudo systemctl restart sssd
+
+      EOF
+      fi
+    '';
+
+    services.sssd.config = ''
+      [domain/${cfg.domain}]
+      id_provider = ipa
+      auth_provider = ipa
+      access_provider = ipa
+      chpass_provider = ipa
+
+      ipa_domain = ${cfg.domain}
+      ipa_server = _srv_, ${cfg.server}
+      ipa_hostname = ${config.networking.hostName}.${cfg.domain}
+
+      cache_credentials = ${pyBool cfg.cacheCredentials}
+      krb5_store_password_if_offline = ${pyBool cfg.offlinePasswords}
+      ${optionalString ((toLower cfg.domain) != (toLower cfg.realm))
+        "krb5_realm = ${cfg.realm}"}
+
+      dyndns_update = ${pyBool cfg.dyndns.enable}
+      dyndns_iface = ${cfg.dyndns.interface}
+
+      ldap_tls_cacert = /etc/ipa/ca.crt
+      ldap_user_extra_attrs = mail:mail, sn:sn, givenname:givenname, telephoneNumber:telephoneNumber, lock:nsaccountlock
+
+      [sssd]
+      debug_level = 65510
+      services = nss, sudo, pam, ssh, ifp
+      domains = ${cfg.domain}
+
+      [nss]
+      homedir_substring = /home
+
+      [pam]
+      pam_pwd_expiration_warning = 3
+      pam_verbosity = 3
+
+      [sudo]
+      debug_level = 65510
+
+      [autofs]
+
+      [ssh]
+
+      [pac]
+
+      [ifp]
+      user_attributes = +mail, +telephoneNumber, +givenname, +sn, +lock
+      allowed_uids = ${concatStringsSep ", " cfg.ifpAllowedUids}
+    '';
+
+    services.ntp.servers = singleton cfg.server;
+    services.sssd.enable = true;
+    services.ntp.enable = true;
+
+    security.pki.certificateFiles = singleton cfg.certificate;
+  };
+}
diff --git a/nixos/modules/services/backup/borgbackup.nix b/nixos/modules/services/backup/borgbackup.nix
index bc2d79ac10ac..08a2967e9c7f 100644
--- a/nixos/modules/services/backup/borgbackup.nix
+++ b/nixos/modules/services/backup/borgbackup.nix
@@ -66,6 +66,7 @@ let
       ${mkKeepArgs cfg} \
       ${optionalString (cfg.prune.prefix != null) "--glob-archives ${escapeShellArg "${cfg.prune.prefix}*"}"} \
       $extraPruneArgs
+    borg compact $extraArgs $extraCompactArgs
     ${cfg.postPrune}
   '');
 
@@ -638,6 +639,15 @@ in {
             example = "--save-space";
           };
 
+          extraCompactArgs = mkOption {
+            type = types.str;
+            description = lib.mdDoc ''
+              Additional arguments for {command}`borg compact`.
+              Can also be set at runtime using `$extraCompactArgs`.
+            '';
+            default = "";
+            example = "--cleanup-commits";
+          };
         };
       }
     ));
diff --git a/nixos/modules/services/backup/borgmatic.nix b/nixos/modules/services/backup/borgmatic.nix
index e7cd6ae4bb57..5ee036e68c7b 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/cluster/kubernetes/kubelet.nix b/nixos/modules/services/cluster/kubernetes/kubelet.nix
index 8e935d621be4..eebacb3f3ef3 100644
--- a/nixos/modules/services/cluster/kubernetes/kubelet.nix
+++ b/nixos/modules/services/cluster/kubernetes/kubelet.nix
@@ -63,6 +63,7 @@ in
     (mkRemovedOptionModule [ "services" "kubernetes" "kubelet" "cadvisorPort" ] "")
     (mkRemovedOptionModule [ "services" "kubernetes" "kubelet" "allowPrivileged" ] "")
     (mkRemovedOptionModule [ "services" "kubernetes" "kubelet" "networkPlugin" ] "")
+    (mkRemovedOptionModule [ "services" "kubernetes" "kubelet" "containerRuntime" ] "")
   ];
 
   ###### interface
@@ -134,12 +135,6 @@ in
       };
     };
 
-    containerRuntime = mkOption {
-      description = lib.mdDoc "Which container runtime type to use";
-      type = enum ["docker" "remote"];
-      default = "remote";
-    };
-
     containerRuntimeEndpoint = mkOption {
       description = lib.mdDoc "Endpoint at which to find the container runtime api interface/socket";
       type = str;
@@ -331,7 +326,6 @@ in
             ${optionalString (cfg.tlsKeyFile != null)
               "--tls-private-key-file=${cfg.tlsKeyFile}"} \
             ${optionalString (cfg.verbosity != null) "--v=${toString cfg.verbosity}"} \
-            --container-runtime=${cfg.containerRuntime} \
             --container-runtime-endpoint=${cfg.containerRuntimeEndpoint} \
             --cgroup-driver=systemd \
             ${cfg.extraOpts}
diff --git a/nixos/modules/services/computing/boinc/client.nix b/nixos/modules/services/computing/boinc/client.nix
index 5fb715f4d779..1879fef9666f 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/buildbot/master.nix b/nixos/modules/services/continuous-integration/buildbot/master.nix
index 5666199c4845..595374ea1e5b 100644
--- a/nixos/modules/services/continuous-integration/buildbot/master.nix
+++ b/nixos/modules/services/continuous-integration/buildbot/master.nix
@@ -8,7 +8,8 @@ let
   cfg = config.services.buildbot-master;
   opt = options.services.buildbot-master;
 
-  python = cfg.package.pythonModule;
+  package = pkgs.python3.pkgs.toPythonModule cfg.package;
+  python = package.pythonModule;
 
   escapeStr = escape [ "'" ];
 
@@ -212,10 +213,10 @@ in {
 
       package = mkOption {
         type = types.package;
-        default = pkgs.python3Packages.buildbot-full;
-        defaultText = literalExpression "pkgs.python3Packages.buildbot-full";
+        default = pkgs.buildbot-full;
+        defaultText = literalExpression "pkgs.buildbot-full";
         description = lib.mdDoc "Package to use for buildbot.";
-        example = literalExpression "pkgs.python3Packages.buildbot";
+        example = literalExpression "pkgs.buildbot";
       };
 
       packages = mkOption {
@@ -255,7 +256,7 @@ in {
       after = [ "network-online.target" ];
       wantedBy = [ "multi-user.target" ];
       path = cfg.packages ++ cfg.pythonPackages python.pkgs;
-      environment.PYTHONPATH = "${python.withPackages (self: cfg.pythonPackages self ++ [ cfg.package ])}/${python.sitePackages}";
+      environment.PYTHONPATH = "${python.withPackages (self: cfg.pythonPackages self ++ [ package ])}/${python.sitePackages}";
 
       preStart = ''
         mkdir -vp "${cfg.buildbotDir}"
diff --git a/nixos/modules/services/continuous-integration/buildbot/worker.nix b/nixos/modules/services/continuous-integration/buildbot/worker.nix
index 52c41c4a7584..7e78b8935f81 100644
--- a/nixos/modules/services/continuous-integration/buildbot/worker.nix
+++ b/nixos/modules/services/continuous-integration/buildbot/worker.nix
@@ -8,7 +8,8 @@ let
   cfg = config.services.buildbot-worker;
   opt = options.services.buildbot-worker;
 
-  python = cfg.package.pythonModule;
+  package = pkgs.python3.pkgs.toPythonModule cfg.package;
+  python = package.pythonModule;
 
   tacFile = pkgs.writeText "aur-buildbot-worker.tac" ''
     import os
@@ -129,7 +130,7 @@ in {
 
       package = mkOption {
         type = types.package;
-        default = pkgs.python3Packages.buildbot-worker;
+        default = pkgs.buildbot-worker;
         defaultText = literalExpression "pkgs.python3Packages.buildbot-worker";
         description = lib.mdDoc "Package to use for buildbot worker.";
         example = literalExpression "pkgs.python2Packages.buildbot-worker";
@@ -168,7 +169,7 @@ in {
       after = [ "network.target" "buildbot-master.service" ];
       wantedBy = [ "multi-user.target" ];
       path = cfg.packages;
-      environment.PYTHONPATH = "${python.withPackages (p: [ cfg.package ])}/${python.sitePackages}";
+      environment.PYTHONPATH = "${python.withPackages (p: [ package ])}/${python.sitePackages}";
 
       preStart = ''
         mkdir -vp "${cfg.buildbotDir}/info"
diff --git a/nixos/modules/services/development/lorri.nix b/nixos/modules/services/development/lorri.nix
index 8c64e3d9a560..74f56f5890fc 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/hardware/auto-cpufreq.nix b/nixos/modules/services/hardware/auto-cpufreq.nix
index 9698e72eb31e..df7c01ae54ef 100644
--- a/nixos/modules/services/hardware/auto-cpufreq.nix
+++ b/nixos/modules/services/hardware/auto-cpufreq.nix
@@ -2,10 +2,26 @@
 with lib;
 let
   cfg = config.services.auto-cpufreq;
+  cfgFilename = "auto-cpufreq.conf";
+  cfgFile = format.generate cfgFilename cfg.settings;
+
+  format = pkgs.formats.ini {};
 in {
   options = {
     services.auto-cpufreq = {
       enable = mkEnableOption (lib.mdDoc "auto-cpufreq daemon");
+
+      settings = mkOption {
+        description = lib.mdDoc ''
+          Configuration for `auto-cpufreq`.
+
+          See its [example configuration file] for supported settings.
+          [example configuration file]: https://github.com/AdnanHodzic/auto-cpufreq/blob/master/auto-cpufreq.conf-example
+          '';
+
+        default = {};
+        type = types.submodule { freeformType = format.type; };
+      };
     };
   };
 
@@ -18,6 +34,11 @@ in {
         # Workaround for https://github.com/NixOS/nixpkgs/issues/81138
         wantedBy = [ "multi-user.target" ];
         path = with pkgs; [ bash coreutils ];
+
+        serviceConfig.ExecStart = [
+          ""
+          "${lib.getExe pkgs.auto-cpufreq} --config ${cfgFile}"
+        ];
       };
     };
   };
diff --git a/nixos/modules/services/home-automation/esphome.nix b/nixos/modules/services/home-automation/esphome.nix
new file mode 100644
index 000000000000..d7dbb6f0b90e
--- /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/mail/maddy.nix b/nixos/modules/services/mail/maddy.nix
index 5f3a9b56292d..d0b525bcb002 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/roundcube.nix b/nixos/modules/services/mail/roundcube.nix
index 7b6d82219298..3aaec145930d 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/gitit.nix b/nixos/modules/services/misc/gitit.nix
deleted file mode 100644
index 0fafa76b5487..000000000000
--- a/nixos/modules/services/misc/gitit.nix
+++ /dev/null
@@ -1,725 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-
-  cfg = config.services.gitit;
-
-  homeDir = "/var/lib/gitit";
-
-  toYesNo = b: if b then "yes" else "no";
-
-  gititShared = with cfg.haskellPackages; gitit + "/share/" + ghc.targetPrefix + ghc.haskellCompilerName + "/" + gitit.pname + "-" + gitit.version;
-
-  gititWithPkgs = hsPkgs: extras: hsPkgs.ghcWithPackages (self: with self; [ gitit ] ++ (extras self));
-
-  gititSh = hsPkgs: extras: with pkgs; let
-    env = gititWithPkgs hsPkgs extras;
-  in writeScript "gitit" ''
-    #!${runtimeShell}
-    cd $HOME
-    export NIX_GHC="${env}/bin/ghc"
-    export NIX_GHCPKG="${env}/bin/ghc-pkg"
-    export NIX_GHC_DOCDIR="${env}/share/doc/ghc/html"
-    export NIX_GHC_LIBDIR=$( $NIX_GHC --print-libdir )
-    ${env}/bin/gitit -f ${configFile}
-  '';
-
-  gititOptions = {
-
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc "Enable the gitit service.";
-      };
-
-      haskellPackages = mkOption {
-        default = pkgs.haskellPackages;
-        defaultText = literalExpression "pkgs.haskellPackages";
-        example = literalExpression "pkgs.haskell.packages.ghc784";
-        description = lib.mdDoc "haskellPackages used to build gitit and plugins.";
-      };
-
-      extraPackages = mkOption {
-        type = types.functionTo (types.listOf types.package);
-        default = self: [];
-        example = literalExpression ''
-          haskellPackages: [
-            haskellPackages.wreq
-          ]
-        '';
-        description = lib.mdDoc ''
-          Extra packages available to ghc when running gitit. The
-          value must be a function which receives the attrset defined
-          in {var}`haskellPackages` as the sole argument.
-        '';
-      };
-
-      address = mkOption {
-        type = types.str;
-        default = "0.0.0.0";
-        description = lib.mdDoc "IP address on which the web server will listen.";
-      };
-
-      port = mkOption {
-        type = types.int;
-        default = 5001;
-        description = lib.mdDoc "Port on which the web server will run.";
-      };
-
-      wikiTitle = mkOption {
-        type = types.str;
-        default = "Gitit!";
-        description = lib.mdDoc "The wiki title.";
-      };
-
-      repositoryType = mkOption {
-        type = types.enum ["git" "darcs" "mercurial"];
-        default = "git";
-        description = lib.mdDoc "Specifies the type of repository used for wiki content.";
-      };
-
-      repositoryPath = mkOption {
-        type = types.path;
-        default = homeDir + "/wiki";
-        description = lib.mdDoc ''
-          Specifies the path of the repository directory. If it does not
-          exist, gitit will create it on startup.
-        '';
-      };
-
-      requireAuthentication = mkOption {
-        type = types.enum [ "none" "modify" "read" ];
-        default = "modify";
-        description = lib.mdDoc ''
-          If 'none', login is never required, and pages can be edited
-          anonymously.  If 'modify', login is required to modify the wiki
-          (edit, add, delete pages, upload files).  If 'read', login is
-          required to see any wiki pages.
-        '';
-      };
-
-      authenticationMethod = mkOption {
-        type = types.enum [ "form" "http" "generic" "github" ];
-        default = "form";
-        description = lib.mdDoc ''
-          'form' means that users will be logged in and registered using forms
-          in the gitit web interface.  'http' means that gitit will assume that
-          HTTP authentication is in place and take the logged in username from
-          the "Authorization" field of the HTTP request header (in addition,
-          the login/logout and registration links will be suppressed).
-          'generic' means that gitit will assume that some form of
-          authentication is in place that directly sets REMOTE_USER to the name
-          of the authenticated user (e.g. mod_auth_cas on apache).  'rpx' means
-          that gitit will attempt to log in through https://rpxnow.com.  This
-          requires that 'rpx-domain', 'rpx-key', and 'base-url' be set below,
-          and that 'curl' be in the system path.
-        '';
-      };
-
-      userFile = mkOption {
-        type = types.path;
-        default = homeDir + "/gitit-users";
-        description = lib.mdDoc ''
-          Specifies the path of the file containing user login information.  If
-          it does not exist, gitit will create it (with an empty user list).
-          This file is not used if 'http' is selected for
-          authentication-method.
-        '';
-      };
-
-      sessionTimeout = mkOption {
-        type = types.int;
-        default = 60;
-        description = lib.mdDoc ''
-          Number of minutes of inactivity before a session expires.
-        '';
-      };
-
-      staticDir = mkOption {
-        type = types.path;
-        default = gititShared + "/data/static";
-        description = lib.mdDoc ''
-          Specifies the path of the static directory (containing javascript,
-          css, and images).  If it does not exist, gitit will create it and
-          populate it with required scripts, stylesheets, and images.
-        '';
-      };
-
-      defaultPageType = mkOption {
-        type = types.enum [ "markdown" "rst" "latex" "html" "markdown+lhs" "rst+lhs" "latex+lhs" ];
-        default = "markdown";
-        description = lib.mdDoc ''
-          Specifies the type of markup used to interpret pages in the wiki.
-          Possible values are markdown, rst, latex, html, markdown+lhs,
-          rst+lhs, and latex+lhs. (the +lhs variants treat the input as
-          literate Haskell. See pandoc's documentation for more details.) If
-          Markdown is selected, pandoc's syntax extensions (for footnotes,
-          delimited code blocks, etc.) will be enabled. Note that pandoc's
-          restructuredtext parser is not complete, so some pages may not be
-          rendered correctly if rst is selected. The same goes for latex and
-          html.
-        '';
-      };
-
-      math = mkOption {
-        type = types.enum [ "mathml" "raw" "mathjax" "jsmath" "google" ];
-        default = "mathml";
-        description = lib.mdDoc ''
-          Specifies how LaTeX math is to be displayed.  Possible values are
-          mathml, raw, mathjax, jsmath, and google.  If mathml is selected,
-          gitit will convert LaTeX math to MathML and link in a script,
-          MathMLinHTML.js, that allows the MathML to be seen in Gecko browsers,
-          IE + mathplayer, and Opera. In other browsers you may get a jumble of
-          characters.  If raw is selected, the LaTeX math will be displayed as
-          raw LaTeX math.  If mathjax is selected, gitit will link to the
-          remote mathjax script.  If jsMath is selected, gitit will link to the
-          script /js/jsMath/easy/load.js, and will assume that jsMath has been
-          installed into the js/jsMath directory.  This is the most portable
-          solution. If google is selected, the google chart API is called to
-          render the formula as an image. This requires a connection to google,
-          and might raise a technical or a privacy problem.
-        '';
-      };
-
-      mathJaxScript = mkOption {
-        type = types.str;
-        default = "https://d3eoax9i5htok0.cloudfront.net/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML";
-        description = lib.mdDoc ''
-          Specifies the path to MathJax rendering script.  You might want to
-          use your own MathJax script to render formulas without Internet
-          connection or if you want to use some special LaTeX packages.  Note:
-          path specified there cannot be an absolute path to a script on your
-          hdd, instead you should run your (local if you wish) HTTP server
-          which will serve the MathJax.js script. You can easily (in four lines
-          of code) serve MathJax.js using
-          http://happstack.com/docs/crashcourse/FileServing.html Do not forget
-          the "http://" prefix (e.g. http://localhost:1234/MathJax.js).
-        '';
-      };
-
-      showLhsBirdTracks = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Specifies whether to show Haskell code blocks in "bird style", with
-          "> " at the beginning of each line.
-        '';
-      };
-
-      templatesDir = mkOption {
-        type = types.path;
-        default = gititShared + "/data/templates";
-        description = lib.mdDoc ''
-          Specifies the path of the directory containing page templates.  If it
-          does not exist, gitit will create it with default templates.  Users
-          may wish to edit the templates to customize the appearance of their
-          wiki. The template files are HStringTemplate templates.  Variables to
-          be interpolated appear between $\'s. Literal $\'s must be
-          backslash-escaped.
-        '';
-      };
-
-      logFile = mkOption {
-        type = types.path;
-        default = homeDir + "/gitit.log";
-        description = lib.mdDoc ''
-          Specifies the path of gitit's log file.  If it does not exist, gitit
-          will create it. The log is in Apache combined log format.
-        '';
-      };
-
-      logLevel = mkOption {
-        type = types.enum [ "DEBUG" "INFO" "NOTICE" "WARNING" "ERROR" "CRITICAL" "ALERT" "EMERGENCY" ];
-        default = "ERROR";
-        description = lib.mdDoc ''
-          Determines how much information is logged.  Possible values (from
-          most to least verbose) are DEBUG, INFO, NOTICE, WARNING, ERROR,
-          CRITICAL, ALERT, EMERGENCY.
-        '';
-      };
-
-      frontPage = mkOption {
-        type = types.str;
-        default = "Front Page";
-        description = lib.mdDoc ''
-          Specifies which wiki page is to be used as the wiki's front page.
-          Gitit creates a default front page on startup, if one does not exist
-          already.
-        '';
-      };
-
-      noDelete = mkOption {
-        type = types.str;
-        default = "Front Page, Help";
-        description = lib.mdDoc ''
-          Specifies pages that cannot be deleted through the web interface.
-          (They can still be deleted directly using git or darcs.) A
-          comma-separated list of page names.  Leave blank to allow every page
-          to be deleted.
-        '';
-      };
-
-      noEdit = mkOption {
-        type = types.str;
-        default = "Help";
-        description = lib.mdDoc ''
-          Specifies pages that cannot be edited through the web interface.
-          Leave blank to allow every page to be edited.
-        '';
-      };
-
-      defaultSummary = mkOption {
-        type = types.str;
-        default = "";
-        description = lib.mdDoc ''
-          Specifies text to be used in the change description if the author
-          leaves the "description" field blank.  If default-summary is blank
-          (the default), the author will be required to fill in the description
-          field.
-        '';
-      };
-
-      tableOfContents = mkOption {
-        type = types.bool;
-        default = true;
-        description = lib.mdDoc ''
-          Specifies whether to print a tables of contents (with links to
-          sections) on each wiki page.
-        '';
-      };
-
-      plugins = mkOption {
-        type = with types; listOf str;
-        default = [ (gititShared + "/plugins/Dot.hs") ];
-        description = lib.mdDoc ''
-          Specifies a list of plugins to load. Plugins may be specified either
-          by their path or by their module name. If the plugin name starts
-          with Gitit.Plugin., gitit will assume that the plugin is an installed
-          module and will not try to find a source file.
-        '';
-      };
-
-      useCache = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Specifies whether to cache rendered pages.  Note that if use-feed is
-          selected, feeds will be cached regardless of the value of use-cache.
-        '';
-      };
-
-      cacheDir = mkOption {
-        type = types.path;
-        default = homeDir + "/cache";
-        description = lib.mdDoc "Path where rendered pages will be cached.";
-      };
-
-      maxUploadSize = mkOption {
-        type = types.str;
-        default = "1000K";
-        description = lib.mdDoc ''
-          Specifies an upper limit on the size (in bytes) of files uploaded
-          through the wiki's web interface.  To disable uploads, set this to
-          0K.  This will result in the uploads link disappearing and the
-          _upload url becoming inactive.
-        '';
-      };
-
-      maxPageSize = mkOption {
-        type = types.str;
-        default = "1000K";
-        description = lib.mdDoc "Specifies an upper limit on the size (in bytes) of pages.";
-      };
-
-      debugMode = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc "Causes debug information to be logged while gitit is running.";
-      };
-
-      compressResponses = mkOption {
-        type = types.bool;
-        default = true;
-        description = lib.mdDoc "Specifies whether HTTP responses should be compressed.";
-      };
-
-      mimeTypesFile = mkOption {
-        type = types.path;
-        default = "/etc/mime/types.info";
-        description = lib.mdDoc ''
-          Specifies the path of a file containing mime type mappings.  Each
-          line of the file should contain two fields, separated by whitespace.
-          The first field is the mime type, the second is a file extension.
-          For example:
-          ```
-          video/x-ms-wmx  wmx
-          ```
-          If the file is not found, some simple defaults will be used.
-        '';
-      };
-
-      useReCaptcha = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          If true, causes gitit to use the reCAPTCHA service
-          (http://recaptcha.net) to prevent bots from creating accounts.
-        '';
-      };
-
-      reCaptchaPrivateKey = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc ''
-          Specifies the private key for the reCAPTCHA service.  To get
-          these, you need to create an account at http://recaptcha.net.
-        '';
-      };
-
-      reCaptchaPublicKey = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc ''
-          Specifies the public key for the reCAPTCHA service.  To get
-          these, you need to create an account at http://recaptcha.net.
-        '';
-      };
-
-      accessQuestion = mkOption {
-        type = types.str;
-        default = "What is the code given to you by Ms. X?";
-        description = lib.mdDoc ''
-          Specifies a question that users must answer when they attempt to
-          create an account
-        '';
-      };
-
-      accessQuestionAnswers = mkOption {
-        type = types.str;
-        default = "RED DOG, red dog";
-        description = lib.mdDoc ''
-          Specifies a question that users must answer when they attempt to
-          create an account, along with a comma-separated list of acceptable
-          answers.  This can be used to institute a rudimentary password for
-          signing up as a user on the wiki, or as an alternative to reCAPTCHA.
-          Example:
-          access-question:  What is the code given to you by Ms. X?
-          access-question-answers:  RED DOG, red dog
-        '';
-      };
-
-      rpxDomain = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc ''
-          Specifies the domain and key of your RPX account.  The domain is just
-          the prefix of the complete RPX domain, so if your full domain is
-          'https://foo.rpxnow.com/', use 'foo' as the value of rpx-domain.
-        '';
-      };
-
-      rpxKey = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc "RPX account access key.";
-      };
-
-      mailCommand = mkOption {
-        type = types.str;
-        default = "sendmail %s";
-        description = lib.mdDoc ''
-          Specifies the command to use to send notification emails.  '%s' will
-          be replaced by the destination email address.  The body of the
-          message will be read from stdin.  If this field is left blank,
-          password reset will not be offered.
-        '';
-      };
-
-      resetPasswordMessage = mkOption {
-        type = types.lines;
-        default = ''
-          > From: gitit@$hostname$
-          > To: $useremail$
-          > Subject: Wiki password reset
-          >
-          > Hello $username$,
-          >
-          > To reset your password, please follow the link below:
-          > http://$hostname$:$port$$resetlink$
-          >
-          > Regards
-        '';
-        description = lib.mdDoc ''
-          Gives the text of the message that will be sent to the user should
-          she want to reset her password, or change other registration info.
-          The lines must be indented, and must begin with '>'.  The initial
-          spaces and '> ' will be stripped off.  $username$ will be replaced by
-          the user's username, $useremail$ by her email address, $hostname$ by
-          the hostname on which the wiki is running (as returned by the
-          hostname system call), $port$ by the port on which the wiki is
-          running, and $resetlink$ by the relative path of a reset link derived
-          from the user's existing hashed password. If your gitit wiki is being
-          proxied to a location other than the root path of $port$, you should
-          change the link to reflect this: for example, to
-          http://$hostname$/path/to/wiki$resetlink$ or
-          http://gitit.$hostname$$resetlink$
-        '';
-      };
-
-      useFeed = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Specifies whether an ATOM feed should be enabled (for the site and
-          for individual pages).
-        '';
-      };
-
-      baseUrl = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc ''
-          The base URL of the wiki, to be used in constructing feed IDs and RPX
-          token_urls.  Set this if useFeed is false or authentication-method
-          is 'rpx'.
-        '';
-      };
-
-      absoluteUrls = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Make wikilinks absolute with respect to the base-url.  So, for
-          example, in a wiki served at the base URL '/wiki', on a page
-          Sub/Page, the wikilink `[Cactus]()` will produce a link to
-          '/wiki/Cactus' if absoluteUrls is true, and a relative link to
-          'Cactus' (referring to '/wiki/Sub/Cactus') if absolute-urls is 'no'.
-        '';
-      };
-
-      feedDays = mkOption {
-        type = types.int;
-        default = 14;
-        description = lib.mdDoc "Number of days to be included in feeds.";
-      };
-
-      feedRefreshTime = mkOption {
-        type = types.int;
-        default = 60;
-        description = lib.mdDoc "Number of minutes to cache feeds before refreshing.";
-      };
-
-      pdfExport = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          If true, PDF will appear in export options. PDF will be created using
-          pdflatex, which must be installed and in the path. Note that PDF
-          exports create significant additional server load.
-        '';
-      };
-
-      pandocUserData = mkOption {
-        type = with types; nullOr path;
-        default = null;
-        description = lib.mdDoc ''
-          If a directory is specified, this will be searched for pandoc
-          customizations. These can include a templates/ directory for custom
-          templates for various export formats, an S5 directory for custom S5
-          styles, and a reference.odt for ODT exports. If no directory is
-          specified, $HOME/.pandoc will be searched. See pandoc's README for
-          more information.
-        '';
-      };
-
-      xssSanitize = mkOption {
-        type = types.bool;
-        default = true;
-        description = lib.mdDoc ''
-          If true, all HTML (including that produced by pandoc) is filtered
-          through xss-sanitize.  Set to no only if you trust all of your users.
-        '';
-      };
-
-      oauthClientId = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc "OAuth client ID";
-      };
-
-      oauthClientSecret = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc "OAuth client secret";
-      };
-
-      oauthCallback = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc "OAuth callback URL";
-      };
-
-      oauthAuthorizeEndpoint = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc "OAuth authorize endpoint";
-      };
-
-      oauthAccessTokenEndpoint = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc "OAuth access token endpoint";
-      };
-
-      githubOrg = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc "Github organization";
-      };
-  };
-
-  configFile = pkgs.writeText "gitit.conf" ''
-    address: ${cfg.address}
-    port: ${toString cfg.port}
-    wiki-title: ${cfg.wikiTitle}
-    repository-type: ${cfg.repositoryType}
-    repository-path: ${cfg.repositoryPath}
-    require-authentication: ${cfg.requireAuthentication}
-    authentication-method: ${cfg.authenticationMethod}
-    user-file: ${cfg.userFile}
-    session-timeout: ${toString cfg.sessionTimeout}
-    static-dir: ${cfg.staticDir}
-    default-page-type: ${cfg.defaultPageType}
-    math: ${cfg.math}
-    mathjax-script: ${cfg.mathJaxScript}
-    show-lhs-bird-tracks: ${toYesNo cfg.showLhsBirdTracks}
-    templates-dir: ${cfg.templatesDir}
-    log-file: ${cfg.logFile}
-    log-level: ${cfg.logLevel}
-    front-page: ${cfg.frontPage}
-    no-delete: ${cfg.noDelete}
-    no-edit: ${cfg.noEdit}
-    default-summary: ${cfg.defaultSummary}
-    table-of-contents: ${toYesNo cfg.tableOfContents}
-    plugins: ${concatStringsSep "," cfg.plugins}
-    use-cache: ${toYesNo cfg.useCache}
-    cache-dir: ${cfg.cacheDir}
-    max-upload-size: ${cfg.maxUploadSize}
-    max-page-size: ${cfg.maxPageSize}
-    debug-mode: ${toYesNo cfg.debugMode}
-    compress-responses: ${toYesNo cfg.compressResponses}
-    mime-types-file: ${cfg.mimeTypesFile}
-    use-recaptcha: ${toYesNo cfg.useReCaptcha}
-    recaptcha-private-key: ${toString cfg.reCaptchaPrivateKey}
-    recaptcha-public-key: ${toString cfg.reCaptchaPublicKey}
-    access-question: ${cfg.accessQuestion}
-    access-question-answers: ${cfg.accessQuestionAnswers}
-    rpx-domain: ${toString cfg.rpxDomain}
-    rpx-key: ${toString cfg.rpxKey}
-    mail-command: ${cfg.mailCommand}
-    reset-password-message: ${cfg.resetPasswordMessage}
-    use-feed: ${toYesNo cfg.useFeed}
-    base-url: ${toString cfg.baseUrl}
-    absolute-urls: ${toYesNo cfg.absoluteUrls}
-    feed-days: ${toString cfg.feedDays}
-    feed-refresh-time: ${toString cfg.feedRefreshTime}
-    pdf-export: ${toYesNo cfg.pdfExport}
-    pandoc-user-data: ${toString cfg.pandocUserData}
-    xss-sanitize: ${toYesNo cfg.xssSanitize}
-
-    [Github]
-    oauthclientid: ${toString cfg.oauthClientId}
-    oauthclientsecret: ${toString cfg.oauthClientSecret}
-    oauthcallback: ${toString cfg.oauthCallback}
-    oauthauthorizeendpoint: ${toString cfg.oauthAuthorizeEndpoint}
-    oauthaccesstokenendpoint: ${toString cfg.oauthAccessTokenEndpoint}
-    github-org: ${toString cfg.githubOrg}
-  '';
-
-in
-
-{
-
-  options.services.gitit = gititOptions;
-
-  config = mkIf cfg.enable {
-
-    users.users.gitit = {
-      group = config.users.groups.gitit.name;
-      description = "Gitit user";
-      home = homeDir;
-      createHome = true;
-      uid = config.ids.uids.gitit;
-    };
-
-    users.groups.gitit.gid = config.ids.gids.gitit;
-
-    systemd.services.gitit = let
-      uid = toString config.ids.uids.gitit;
-      gid = toString config.ids.gids.gitit;
-    in {
-      description = "Git and Pandoc Powered Wiki";
-      after = [ "network.target" ];
-      wantedBy = [ "multi-user.target" ];
-      path = with pkgs; [ curl ]
-             ++ optional cfg.pdfExport texlive.combined.scheme-basic
-             ++ optional (cfg.repositoryType == "darcs") darcs
-             ++ optional (cfg.repositoryType == "mercurial") mercurial
-             ++ optional (cfg.repositoryType == "git") git;
-
-      preStart = let
-        gm = "gitit@${config.networking.hostName}";
-      in
-      with cfg; ''
-        chown ${uid}:${gid} -R ${homeDir}
-        for dir in ${repositoryPath} ${staticDir} ${templatesDir} ${cacheDir}
-        do
-          if [ ! -d $dir ]
-          then
-            mkdir -p $dir
-            find $dir -type d -exec chmod 0750 {} +
-            find $dir -type f -exec chmod 0640 {} +
-          fi
-        done
-        cd ${repositoryPath}
-        ${
-          if repositoryType == "darcs" then
-          ''
-          if [ ! -d _darcs ]
-          then
-            darcs initialize
-            echo "${gm}" > _darcs/prefs/email
-          ''
-          else if repositoryType == "mercurial" then
-          ''
-          if [ ! -d .hg ]
-          then
-            hg init
-            cat >> .hg/hgrc <<NAMED
-[ui]
-username = gitit ${gm}
-NAMED
-          ''
-          else
-          ''
-          if [ ! -d  .git ]
-          then
-            git init
-            git config user.email "${gm}"
-            git config user.name "gitit"
-          ''}
-          chown ${uid}:${gid} -R ${repositoryPath}
-          fi
-        cd -
-      '';
-
-      serviceConfig = {
-        User = config.users.users.gitit.name;
-        Group = config.users.groups.gitit.name;
-        ExecStart = with cfg; gititSh haskellPackages extraPackages;
-      };
-    };
-  };
-}
diff --git a/nixos/modules/services/misc/gpsd.nix b/nixos/modules/services/misc/gpsd.nix
index 9b03b6f9662e..ce0f9bb3ba28 100644
--- a/nixos/modules/services/misc/gpsd.nix
+++ b/nixos/modules/services/misc/gpsd.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, pkgs, utils, ... }:
 
 with lib;
 
@@ -8,12 +8,15 @@ let
   gid = config.ids.gids.gpsd;
   cfg = config.services.gpsd;
 
-in
-
-{
+in {
 
   ###### interface
 
+  imports = [
+    (lib.mkRemovedOptionModule [ "services" "gpsd" "device" ]
+      "Use `services.gpsd.devices` instead.")
+  ];
+
   options = {
 
     services.gpsd = {
@@ -26,13 +29,17 @@ in
         '';
       };
 
-      device = mkOption {
-        type = types.str;
-        default = "/dev/ttyUSB0";
+      devices = mkOption {
+        type = types.listOf types.str;
+        default = [ "/dev/ttyUSB0" ];
         description = lib.mdDoc ''
-          A device may be a local serial device for GPS input, or a URL of the form:
-               `[{dgpsip|ntrip}://][user:passwd@]host[:port][/stream]`
-          in which case it specifies an input source for DGPS or ntrip data.
+          List of devices that `gpsd` should subscribe to.
+
+          A device may be a local serial device for GPS input, or a
+          URL of the form:
+          `[{dgpsip|ntrip}://][user:passwd@]host[:port][/stream]` in
+          which case it specifies an input source for DGPS or ntrip
+          data.
         '';
       };
 
@@ -89,17 +96,16 @@ in
 
   };
 
-
   ###### implementation
 
   config = mkIf cfg.enable {
 
-    users.users.gpsd =
-      { inherit uid;
-        group = "gpsd";
-        description = "gpsd daemon user";
-        home = "/var/empty";
-      };
+    users.users.gpsd = {
+      inherit uid;
+      group = "gpsd";
+      description = "gpsd daemon user";
+      home = "/var/empty";
+    };
 
     users.groups.gpsd = { inherit gid; };
 
@@ -109,13 +115,15 @@ in
       after = [ "network.target" ];
       serviceConfig = {
         Type = "forking";
-        ExecStart = ''
+        ExecStart = let
+          devices = utils.escapeSystemdExecArgs cfg.devices;
+        in ''
           ${pkgs.gpsd}/sbin/gpsd -D "${toString cfg.debugLevel}"  \
             -S "${toString cfg.port}"                             \
             ${optionalString cfg.readonly "-b"}                   \
             ${optionalString cfg.nowait "-n"}                     \
             ${optionalString cfg.listenany "-G"}                  \
-            "${cfg.device}"
+            ${devices}
         '';
       };
     };
diff --git a/nixos/modules/services/misc/pufferpanel.nix b/nixos/modules/services/misc/pufferpanel.nix
new file mode 100644
index 000000000000..78ec35646907
--- /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/monitoring/grafana-agent.nix b/nixos/modules/services/monitoring/grafana-agent.nix
index 270d888afb78..b7761c34fe51 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/smartctl.nix b/nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix
index 0c5648c14149..50e1321a1e9c 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}"
-  ];
+  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 0cb0e126d4c5..2537bb1b8d80 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/server.nix b/nixos/modules/services/network-filesystems/openafs/server.nix
index 1c615d3bfb64..ad0fd7835670 100644
--- a/nixos/modules/services/network-filesystems/openafs/server.nix
+++ b/nixos/modules/services/network-filesystems/openafs/server.nix
@@ -4,7 +4,8 @@
 with import ./lib.nix { inherit config lib pkgs; };
 
 let
-  inherit (lib) concatStringsSep literalExpression mkIf mkOption optionalString types;
+  inherit (lib) concatStringsSep literalExpression mkIf mkOption mkEnableOption
+  optionalString types;
 
   bosConfig = pkgs.writeText "BosConfig" (''
     restrictmode 1
@@ -24,9 +25,15 @@ let
     parm ${openafsSrv}/libexec/openafs/salvageserver ${cfg.roles.fileserver.salvageserverArgs}
     parm ${openafsSrv}/libexec/openafs/dasalvager ${cfg.roles.fileserver.salvagerArgs}
     end
-  '') + (optionalString (cfg.roles.database.enable && cfg.roles.backup.enable) ''
+  '') + (optionalString (cfg.roles.database.enable && cfg.roles.backup.enable && (!cfg.roles.backup.enableFabs)) ''
     bnode simple buserver 1
-    parm ${openafsSrv}/libexec/openafs/buserver ${cfg.roles.backup.buserverArgs} ${optionalString (cfg.roles.backup.cellServDB != []) "-cellservdb /etc/openafs/backup/"}
+    parm ${openafsSrv}/libexec/openafs/buserver ${cfg.roles.backup.buserverArgs} ${optionalString useBuCellServDB "-cellservdb /etc/openafs/backup/"}
+    end
+  '') + (optionalString (cfg.roles.database.enable &&
+                         cfg.roles.backup.enable &&
+                         cfg.roles.backup.enableFabs) ''
+    bnode simple buserver 1
+    parm ${lib.getBin pkgs.fabs}/bin/fabsys server --config ${fabsConfFile} ${cfg.roles.backup.fabsArgs}
     end
   ''));
 
@@ -34,12 +41,27 @@ let
     pkgs.writeText "NetInfo" ((concatStringsSep "\nf " cfg.advertisedAddresses) + "\n")
   else null;
 
-  buCellServDB = pkgs.writeText "backup-cellServDB-${cfg.cellName}" (mkCellServDB cfg.cellName cfg.roles.backup.cellServDB);
+  buCellServDB = pkgs.writeText "backup-cellServDB-${cfg.cellName}"
+    (mkCellServDB cfg.cellName cfg.roles.backup.cellServDB);
+
+  useBuCellServDB = (cfg.roles.backup.cellServDB != []) && (!cfg.roles.backup.enableFabs);
 
   cfg = config.services.openafsServer;
 
   udpSizeStr = toString cfg.udpPacketSize;
 
+  fabsConfFile = pkgs.writeText "fabs.yaml" (builtins.toJSON ({
+    afs = {
+      aklog = cfg.package + "/bin/aklog";
+      cell = cfg.cellName;
+      dumpscan = cfg.package + "/bin/afsdump_scan";
+      fs = cfg.package + "/bin/fs";
+      pts = cfg.package + "/bin/pts";
+      vos = cfg.package + "/bin/vos";
+    };
+    k5start.command = (lib.getBin pkgs.kstart) + "/bin/k5start";
+  } // cfg.roles.backup.fabsExtraConfig));
+
 in {
 
   options = {
@@ -80,8 +102,8 @@ in {
       };
 
       package = mkOption {
-        default = pkgs.openafs.server or pkgs.openafs;
-        defaultText = literalExpression "pkgs.openafs.server or pkgs.openafs";
+        default = pkgs.openafs;
+        defaultText = literalExpression "pkgs.openafs";
         type = types.package;
         description = lib.mdDoc "OpenAFS package for the server binaries";
       };
@@ -154,16 +176,20 @@ in {
         };
 
         backup = {
-          enable = mkOption {
-            default = false;
-            type = types.bool;
-            description = lib.mdDoc ''
-              Backup server role. Use in conjunction with the
-              `database` role to maintain the Backup
-              Database. Normally only used in conjunction with tape storage
-              or IBM's Tivoli Storage Manager.
-            '';
-          };
+          enable = mkEnableOption (lib.mdDoc ''
+            Backup server role. When using OpenAFS built-in buserver, use in conjunction with the
+            `database` role to maintain the Backup
+            Database. Normally only used in conjunction with tape storage
+            or IBM's Tivoli Storage Manager.
+
+            For a modern backup server, enable this role and see
+            {option}`enableFabs`.
+          '');
+
+          enableFabs = mkEnableOption (lib.mdDoc ''
+            FABS, the flexible AFS backup system. It stores volumes as dump files, relying on other
+            pre-existing backup solutions for handling them.
+          '');
 
           buserverArgs = mkOption {
             default = "";
@@ -181,6 +207,30 @@ in {
               other database server machines.
             '';
           };
+
+          fabsArgs = mkOption {
+            default = "";
+            type = types.str;
+            description = lib.mdDoc ''
+              Arguments to the fabsys process. See
+              {manpage}`fabsys_server(1)` and
+              {manpage}`fabsys_config(1)`.
+            '';
+          };
+
+          fabsExtraConfig = mkOption {
+            default = {};
+            type = types.attrs;
+            description = lib.mdDoc ''
+              Additional configuration parameters for the FABS backup server.
+            '';
+            example = literalExpression ''
+            {
+              afs.localauth = true;
+              afs.keytab = config.sops.secrets.fabsKeytab.path;
+            }
+            '';
+          };
         };
       };
 
@@ -239,7 +289,7 @@ in {
         mode = "0644";
       };
       buCellServDB = {
-        enable = (cfg.roles.backup.cellServDB != []);
+        enable = useBuCellServDB;
         text = mkCellServDB cfg.cellName cfg.roles.backup.cellServDB;
         target = "openafs/backup/CellServDB";
       };
@@ -257,7 +307,7 @@ in {
         preStart = ''
           mkdir -m 0755 -p /var/openafs
           ${optionalString (netInfo != null) "cp ${netInfo} /var/openafs/netInfo"}
-          ${optionalString (cfg.roles.backup.cellServDB != []) "cp ${buCellServDB}"}
+          ${optionalString useBuCellServDB "cp ${buCellServDB}"}
         '';
         serviceConfig = {
           ExecStart = "${openafsBin}/bin/bosserver -nofork";
diff --git a/nixos/modules/services/networking/ddclient.nix b/nixos/modules/services/networking/ddclient.nix
index 5e6f5217c0ce..7caee8a8eb3d 100644
--- a/nixos/modules/services/networking/ddclient.nix
+++ b/nixos/modules/services/networking/ddclient.nix
@@ -29,9 +29,9 @@ let
   configFile = if (cfg.configFile != null) then cfg.configFile else configFile';
 
   preStart = ''
-    install ${configFile} /run/${RuntimeDirectory}/ddclient.conf
+    install --mode=600 --owner=$USER ${configFile} /run/${RuntimeDirectory}/ddclient.conf
     ${lib.optionalString (cfg.configFile == null) (if (cfg.protocol == "nsupdate") then ''
-      install ${cfg.passwordFile} /run/${RuntimeDirectory}/ddclient.key
+      install --mode=600 --owner=$USER ${cfg.passwordFile} /run/${RuntimeDirectory}/ddclient.key
     '' else if (cfg.passwordFile != null) then ''
       "${pkgs.replace-secret}/bin/replace-secret" "@password_placeholder@" "${cfg.passwordFile}" "/run/${RuntimeDirectory}/ddclient.conf"
     '' else ''
diff --git a/nixos/modules/services/networking/dhcpd.nix b/nixos/modules/services/networking/dhcpd.nix
index 0bd5e4ef5535..a981a255c3ee 100644
--- a/nixos/modules/services/networking/dhcpd.nix
+++ b/nixos/modules/services/networking/dhcpd.nix
@@ -218,6 +218,13 @@ in
 
     systemd.services = dhcpdService "4" cfg4 // dhcpdService "6" cfg6;
 
+    warnings = [
+      ''
+        The dhcpd4 and dhcpd6 modules will be removed from NixOS 23.11, because ISC DHCP reached its end of life.
+        See https://www.isc.org/blogs/isc-dhcp-eol/ for details.
+        Please switch to a different implementation like kea, systemd-networkd or dnsmasq.
+      ''
+    ];
   };
 
 }
diff --git a/nixos/modules/services/networking/go-neb.nix b/nixos/modules/services/networking/go-neb.nix
index 8c04542c47cc..b65bb5f548ee 100644
--- a/nixos/modules/services/networking/go-neb.nix
+++ b/nixos/modules/services/networking/go-neb.nix
@@ -60,13 +60,12 @@ in {
 
       serviceConfig = {
         ExecStartPre = lib.optional (cfg.secretFile != null)
-          (pkgs.writeShellScript "pre-start" ''
+          ("+" + pkgs.writeShellScript "pre-start" ''
             umask 077
             export $(xargs < ${cfg.secretFile})
             ${pkgs.envsubst}/bin/envsubst -i "${configFile}" > ${finalConfigFile}
             chown go-neb ${finalConfigFile}
           '');
-        PermissionsStartOnly = true;
         RuntimeDirectory = "go-neb";
         ExecStart = "${pkgs.go-neb}/bin/go-neb";
         User = "go-neb";
diff --git a/nixos/modules/services/networking/ivpn.nix b/nixos/modules/services/networking/ivpn.nix
new file mode 100644
index 000000000000..6df630c1f194
--- /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/peroxide.nix b/nixos/modules/services/networking/peroxide.nix
index 6cac4bf2f89a..885ee1d96cd0 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 c2c2a370cb00..19ab3f1aa48c 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/sshd.nix b/nixos/modules/services/networking/ssh/sshd.nix
index 9982da304a48..89ddf8215299 100644
--- a/nixos/modules/services/networking/ssh/sshd.nix
+++ b/nixos/modules/services/networking/ssh/sshd.nix
@@ -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
       ''
diff --git a/nixos/modules/services/networking/wgautomesh.nix b/nixos/modules/services/networking/wgautomesh.nix
new file mode 100644
index 000000000000..93227a9b625d
--- /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 440b617f60a3..067d5df48725 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/printing/cupsd.nix b/nixos/modules/services/printing/cupsd.nix
index 9ac89e057620..f6a23fb900f0 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 a843c44dbb5f..e1f7365d951a 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/fail2ban.nix b/nixos/modules/services/security/fail2ban.nix
index ead24d147071..1962d3f59c9f 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;
@@ -320,6 +327,9 @@ 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
diff --git a/nixos/modules/services/security/kanidm.nix b/nixos/modules/services/security/kanidm.nix
index 5583c39368f7..2f19decb5cb1 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/system/cachix-watch-store.nix b/nixos/modules/services/system/cachix-watch-store.nix
index 85e9509bcc82..89157b460b9a 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/video/rtsp-simple-server.nix b/nixos/modules/services/video/mediamtx.nix
index 2dd62edab787..18a9e3d5fe30 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/video/v4l2-relayd.nix b/nixos/modules/services/video/v4l2-relayd.nix
new file mode 100644
index 000000000000..2a9dbe00158f
--- /dev/null
+++ b/nixos/modules/services/video/v4l2-relayd.nix
@@ -0,0 +1,199 @@
+{ config, lib, pkgs, utils, ... }:
+let
+
+  inherit (lib) attrValues concatStringsSep filterAttrs length listToAttrs literalExpression
+    makeSearchPathOutput mkEnableOption mkIf mkOption nameValuePair optionals types;
+  inherit (utils) escapeSystemdPath;
+
+  cfg = config.services.v4l2-relayd;
+
+  kernelPackages = config.boot.kernelPackages;
+
+  gst = (with pkgs.gst_all_1; [
+    gst-plugins-bad
+    gst-plugins-base
+    gst-plugins-good
+    gstreamer.out
+  ]);
+
+  instanceOpts = { name, ... }: {
+    options = {
+      enable = mkEnableOption (lib.mdDoc "this v4l2-relayd instance");
+
+      name = mkOption {
+        type = types.str;
+        default = name;
+        description = lib.mdDoc ''
+          The name of the instance.
+        '';
+      };
+
+      cardLabel = mkOption {
+        type = types.str;
+        description = lib.mdDoc ''
+          The name the camera will show up as.
+        '';
+      };
+
+      extraPackages = mkOption {
+        type = with types; listOf package;
+        default = [ ];
+        description = lib.mdDoc ''
+          Extra packages to add to {env}`GST_PLUGIN_PATH` for the instance.
+        '';
+      };
+
+      input = {
+        pipeline = mkOption {
+          type = types.str;
+          description = lib.mdDoc ''
+            The gstreamer-pipeline to use for the input-stream.
+          '';
+        };
+
+        format = mkOption {
+          type = types.str;
+          default = "YUY2";
+          description = lib.mdDoc ''
+            The video-format to read from input-stream.
+          '';
+        };
+
+        width = mkOption {
+          type = types.ints.positive;
+          default = 1280;
+          description = lib.mdDoc ''
+            The width to read from input-stream.
+          '';
+        };
+
+        height = mkOption {
+          type = types.ints.positive;
+          default = 720;
+          description = lib.mdDoc ''
+            The height to read from input-stream.
+          '';
+        };
+
+        framerate = mkOption {
+          type = types.ints.positive;
+          default = 30;
+          description = lib.mdDoc ''
+            The framerate to read from input-stream.
+          '';
+        };
+      };
+
+      output = {
+        format = mkOption {
+          type = types.str;
+          default = "YUY2";
+          description = lib.mdDoc ''
+            The video-format to write to output-stream.
+          '';
+        };
+      };
+
+    };
+  };
+
+in
+{
+
+  options.services.v4l2-relayd = {
+
+    instances = mkOption {
+      type = with types; attrsOf (submodule instanceOpts);
+      default = { };
+      example = literalExpression ''
+        {
+          example = {
+            cardLabel = "Example card";
+            input.pipeline = "videotestsrc";
+          };
+        }
+      '';
+      description = lib.mdDoc ''
+        v4l2-relayd instances to be created.
+      '';
+    };
+
+  };
+
+  config =
+    let
+
+      mkInstanceService = instance: {
+        description = "Streaming relay for v4l2loopback using GStreamer";
+
+        after = [ "modprobe@v4l2loopback.service" "systemd-logind.service" ];
+        wantedBy = [ "multi-user.target" ];
+
+        serviceConfig = {
+          Type = "simple";
+          Restart = "always";
+          PrivateNetwork = true;
+          PrivateTmp = true;
+          LimitNPROC = 1;
+        };
+
+        environment = {
+          GST_PLUGIN_PATH = makeSearchPathOutput "lib" "lib/gstreamer-1.0" (gst ++ instance.extraPackages);
+          V4L2_DEVICE_FILE = "/run/v4l2-relayd-${instance.name}/device";
+        };
+
+        script =
+          let
+            appsrcOptions = concatStringsSep "," [
+              "caps=video/x-raw"
+              "format=${instance.input.format}"
+              "width=${toString instance.input.width}"
+              "height=${toString instance.input.height}"
+              "framerate=${toString instance.input.framerate}/1"
+            ];
+
+            outputPipeline = [
+              "appsrc name=appsrc ${appsrcOptions}"
+              "videoconvert"
+            ] ++ optionals (instance.input.format != instance.output.format) [
+              "video/x-raw,format=${instance.output.format}"
+              "queue"
+            ] ++ [ "v4l2sink name=v4l2sink device=$(cat $V4L2_DEVICE_FILE)" ];
+          in
+          ''
+            exec ${pkgs.v4l2-relayd}/bin/v4l2-relayd -i "${instance.input.pipeline}" -o "${concatStringsSep " ! " outputPipeline}"
+          '';
+
+        preStart = ''
+          mkdir -p $(dirname $V4L2_DEVICE_FILE)
+          ${kernelPackages.v4l2loopback.bin}/bin/v4l2loopback-ctl add -x 1 -n "${instance.cardLabel}" > $V4L2_DEVICE_FILE
+        '';
+
+        postStop = ''
+          ${kernelPackages.v4l2loopback.bin}/bin/v4l2loopback-ctl delete $(cat $V4L2_DEVICE_FILE)
+          rm -rf $(dirname $V4L2_DEVICE_FILE)
+        '';
+      };
+
+      mkInstanceServices = instances: listToAttrs (map
+        (instance:
+          nameValuePair "v4l2-relayd-${escapeSystemdPath instance.name}" (mkInstanceService instance)
+        )
+        instances);
+
+      enabledInstances = attrValues (filterAttrs (n: v: v.enable) cfg.instances);
+
+    in
+    {
+
+      boot = mkIf ((length enabledInstances) > 0) {
+        extraModulePackages = [ kernelPackages.v4l2loopback ];
+        kernelModules = [ "v4l2loopback" ];
+      };
+
+      systemd.services = mkInstanceServices enabledInstances;
+
+    };
+
+  meta.maintainers = with lib.maintainers; [ betaboon ];
+}
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 a61aa445f82c..000000000000
--- 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/mastodon.nix b/nixos/modules/services/web-apps/mastodon.nix
index 3e1286dc475d..cf5329be489c 100644
--- a/nixos/modules/services/web-apps/mastodon.nix
+++ b/nixos/modules/services/web-apps/mastodon.nix
@@ -587,6 +587,13 @@ in {
             <option>services.mastodon.smtp.authenticate</option> is enabled.
         '';
       }
+      {
+        assertion = 1 == builtins.length
+          (lib.mapAttrsToList
+            (_: v: builtins.elem "scheduler" v.jobClasses || v.jobClasses == [ ])
+            cfg.sidekiqProcesses);
+        message = "There must be one and only one Sidekiq queue in services.mastodon.sidekiqProcesses with jobClass \"scheduler\".";
+      }
     ];
 
     environment.systemPackages = [ mastodonTootctl ];
diff --git a/nixos/modules/services/web-apps/monica.nix b/nixos/modules/services/web-apps/monica.nix
new file mode 100644
index 000000000000..442044fedb14
--- /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 5f8d9c5b15f4..b617e9a59379 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/plausible.nix b/nixos/modules/services/web-apps/plausible.nix
index f64254d62524..893dfa10acbc 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/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix
index 7811876369b3..4f97fe752ef7 100644
--- a/nixos/modules/services/web-servers/nginx/default.nix
+++ b/nixos/modules/services/web-servers/nginx/default.nix
@@ -31,6 +31,7 @@ let
 
   # Mime.types values are taken from brotli sample configuration - https://github.com/google/ngx_brotli
   # and Nginx Server Configs - https://github.com/h5bp/server-configs-nginx
+  # "text/html" is implicitly included in {brotli,gzip,zstd}_types
   compressMimeTypes = [
     "application/atom+xml"
     "application/geo+json"
@@ -55,7 +56,6 @@ let
     "text/calendar"
     "text/css"
     "text/csv"
-    "text/html"
     "text/javascript"
     "text/markdown"
     "text/plain"
diff --git a/nixos/modules/services/x11/desktop-managers/pantheon.nix b/nixos/modules/services/x11/desktop-managers/pantheon.nix
index 4d0296c8254c..ab4d75b10134 100644
--- a/nixos/modules/services/x11/desktop-managers/pantheon.nix
+++ b/nixos/modules/services/x11/desktop-managers/pantheon.nix
@@ -288,7 +288,7 @@ in
         elementary-music
         elementary-photos
         elementary-screenshot
-        # elementary-tasks
+        elementary-tasks
         elementary-terminal
         elementary-videos
         epiphany
diff --git a/nixos/modules/services/x11/desktop-managers/plasma5.nix b/nixos/modules/services/x11/desktop-managers/plasma5.nix
index 73a864bb95fe..38f932ffb420 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 = [
diff --git a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py
index c92451997203..a040518a5a57 100755
--- a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py
+++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py
@@ -302,8 +302,12 @@ def main() -> None:
             if is_default:
                 write_loader_conf(*gen)
         except OSError as e:
-            profile = f"profile '{gen.profile}'" if gen.profile else "default profile"
-            print("ignoring {} in the list of boot entries because of the following error:\n{}".format(profile, e), file=sys.stderr)
+            # See https://github.com/NixOS/nixpkgs/issues/114552
+            if e.errno == errno.EINVAL:
+                profile = f"profile '{gen.profile}'" if gen.profile else "default profile"
+                print("ignoring {} in the list of boot entries because of the following error:\n{}".format(profile, e), file=sys.stderr)
+            else:
+                raise e
 
     for root, _, files in os.walk('@efiSysMountPoint@/efi/nixos/.extra-files', topdown=False):
         relative_root = root.removeprefix("@efiSysMountPoint@/efi/nixos/.extra-files").removeprefix("/")
diff --git a/nixos/modules/system/boot/resolved.nix b/nixos/modules/system/boot/resolved.nix
index 0ab2a875975d..4e7201833db6 100644
--- a/nixos/modules/system/boot/resolved.nix
+++ b/nixos/modules/system/boot/resolved.nix
@@ -16,7 +16,9 @@ in
       default = false;
       type = types.bool;
       description = lib.mdDoc ''
-        Whether to enable the systemd DNS resolver daemon.
+        Whether to enable the systemd DNS resolver daemon, `systemd-resolved`.
+
+        Search for `services.resolved` to see all options.
       '';
     };
 
diff --git a/nixos/modules/system/boot/tmp.nix b/nixos/modules/system/boot/tmp.nix
index 1f9431710aec..fd16cd3fba42 100644
--- a/nixos/modules/system/boot/tmp.nix
+++ b/nixos/modules/system/boot/tmp.nix
@@ -3,62 +3,67 @@
 with lib;
 
 let
-  cfg = config.boot;
+  cfg = config.boot.tmp;
 in
 {
-
-  ###### interface
+  imports = [
+    (mkRenamedOptionModule [ "boot" "cleanTmpDir" ] [ "boot" "tmp" "cleanOnBoot" ])
+    (mkRenamedOptionModule [ "boot" "tmpOnTmpfs" ] [ "boot" "tmp" "useTmpfs" ])
+    (mkRenamedOptionModule [ "boot" "tmpOnTmpfsSize" ] [ "boot" "tmp" "tmpfsSize" ])
+  ];
 
   options = {
+    boot.tmp = {
+      cleanOnBoot = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc ''
+          Whether to delete all files in {file}`/tmp` during boot.
+        '';
+      };
 
-    boot.cleanTmpDir = mkOption {
-      type = types.bool;
-      default = false;
-      description = lib.mdDoc ''
-        Whether to delete all files in {file}`/tmp` during boot.
-      '';
-    };
+      tmpfsSize = mkOption {
+        type = types.oneOf [ types.str types.types.ints.positive ];
+        default = "50%";
+        description = lib.mdDoc ''
+          Size of tmpfs in percentage.
+          Percentage is defined by systemd.
+        '';
+      };
 
-    boot.tmpOnTmpfs = mkOption {
-      type = types.bool;
-      default = false;
-      description = lib.mdDoc ''
-         Whether to mount a tmpfs on {file}`/tmp` during boot.
-      '';
-    };
+      useTmpfs = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc ''
+           Whether to mount a tmpfs on {file}`/tmp` during boot.
 
-    boot.tmpOnTmpfsSize = mkOption {
-      type = types.oneOf [ types.str types.types.ints.positive ];
-      default = "50%";
-      description = lib.mdDoc ''
-        Size of tmpfs in percentage.
-        Percentage is defined by systemd.
-      '';
+           ::: {.note}
+           Large Nix builds can fail if the mounted tmpfs is not large enough.
+           In such a case either increase the tmpfsSize or disable this option.
+           :::
+        '';
+      };
     };
-
   };
 
-  ###### implementation
-
   config = {
-
     # When changing remember to update /tmp mount in virtualisation/qemu-vm.nix
-    systemd.mounts = mkIf cfg.tmpOnTmpfs [
+    systemd.mounts = mkIf cfg.useTmpfs [
       {
         what = "tmpfs";
         where = "/tmp";
         type = "tmpfs";
-        mountConfig.Options = concatStringsSep "," [ "mode=1777"
-                                                     "strictatime"
-                                                     "rw"
-                                                     "nosuid"
-                                                     "nodev"
-                                                     "size=${toString cfg.tmpOnTmpfsSize}" ];
+        mountConfig.Options = concatStringsSep "," [
+          "mode=1777"
+          "strictatime"
+          "rw"
+          "nosuid"
+          "nodev"
+          "size=${toString cfg.tmpfsSize}"
+        ];
       }
     ];
 
-    systemd.tmpfiles.rules = optional config.boot.cleanTmpDir "D! /tmp 1777 root root";
-
+    systemd.tmpfiles.rules = optional cfg.cleanOnBoot "D! /tmp 1777 root root";
   };
-
 }
diff --git a/nixos/modules/virtualisation/cri-o.nix b/nixos/modules/virtualisation/cri-o.nix
index 3503a6fff6a9..dacd700537c7 100644
--- a/nixos/modules/virtualisation/cri-o.nix
+++ b/nixos/modules/virtualisation/cri-o.nix
@@ -4,7 +4,10 @@ with lib;
 let
   cfg = config.virtualisation.cri-o;
 
-  crioPackage = (pkgs.cri-o.override { inherit (cfg) extraPackages; });
+  crioPackage = pkgs.cri-o.override {
+    extraPackages = cfg.extraPackages
+      ++ lib.optional (builtins.elem "zfs" config.boot.supportedFilesystems) config.boot.zfs.package;
+  };
 
   format = pkgs.formats.toml { };
 
@@ -19,7 +22,7 @@ in
     enable = mkEnableOption (lib.mdDoc "Container Runtime Interface for OCI (CRI-O)");
 
     storageDriver = mkOption {
-      type = types.enum [ "btrfs" "overlay" "vfs" ];
+      type = types.enum [ "aufs" "btrfs" "devmapper" "overlay" "vfs" "zfs" ];
       default = "overlay";
       description = lib.mdDoc "Storage driver to be used";
     };
diff --git a/nixos/modules/virtualisation/multipass.nix b/nixos/modules/virtualisation/multipass.nix
index 6ef7de4b2bf5..b331b3be7ea5 100644
--- a/nixos/modules/virtualisation/multipass.nix
+++ b/nixos/modules/virtualisation/multipass.nix
@@ -33,8 +33,8 @@ in
       description = "Multipass orchestrates virtual Ubuntu instances.";
 
       wantedBy = [ "multi-user.target" ];
-      wants = [ "network.target" ];
-      after = [ "network.target" ];
+      wants = [ "network-online.target" ];
+      after = [ "network-online.target" ];
 
       environment = {
         "XDG_DATA_HOME" = "/var/lib/multipass/data";
diff --git a/nixos/modules/virtualisation/proxmox-image.nix b/nixos/modules/virtualisation/proxmox-image.nix
index 6a4220fd265c..c66a4f178ec7 100644
--- a/nixos/modules/virtualisation/proxmox-image.nix
+++ b/nixos/modules/virtualisation/proxmox-image.nix
@@ -135,10 +135,11 @@ with lib;
     cfgLine = name: value: ''
       ${name}: ${builtins.toString value}
     '';
+    virtio0Storage = builtins.head (builtins.split ":" cfg.qemuConf.virtio0);
     cfgFile = fileName: properties: pkgs.writeTextDir fileName ''
       # generated by NixOS
       ${lib.concatStrings (lib.mapAttrsToList cfgLine properties)}
-      #qmdump#map:virtio0:drive-virtio0:local-lvm:raw:
+      #qmdump#map:virtio0:drive-virtio0:${virtio0Storage}:raw:
     '';
     inherit (cfg) partitionTableType;
     supportEfi = partitionTableType == "efi" || partitionTableType == "hybrid";
diff --git a/nixos/modules/virtualisation/qemu-vm.nix b/nixos/modules/virtualisation/qemu-vm.nix
index 154fdd2e01be..1619c0d9410f 100644
--- a/nixos/modules/virtualisation/qemu-vm.nix
+++ b/nixos/modules/virtualisation/qemu-vm.nix
@@ -1069,12 +1069,12 @@ in
           fsType = "ext4";
           autoFormat = true;
         });
-        "/tmp" = lib.mkIf config.boot.tmpOnTmpfs {
+        "/tmp" = lib.mkIf config.boot.tmp.useTmpfs {
           device = "tmpfs";
           fsType = "tmpfs";
           neededForBoot = true;
           # Sync with systemd's tmp.mount;
-          options = [ "mode=1777" "strictatime" "nosuid" "nodev" "size=${toString config.boot.tmpOnTmpfsSize}" ];
+          options = [ "mode=1777" "strictatime" "nosuid" "nodev" "size=${toString config.boot.tmp.tmpfsSize}" ];
         };
         "/nix/${if cfg.writableStore then ".ro-store" else "store"}" = lib.mkIf cfg.useNixStoreImage {
           device = "${lookupDriveDeviceName "nix-store" cfg.qemu.drives}";