about summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/configuration/x-windows.chapter.md6
-rw-r--r--nixos/doc/manual/installation/building-images-via-systemd-repart.chapter.md (renamed from nixos/modules/image/repart.md)0
-rw-r--r--nixos/doc/manual/installation/installation.md1
-rw-r--r--nixos/doc/manual/release-notes/rl-2311.section.md19
-rw-r--r--nixos/lib/systemd-lib.nix8
-rw-r--r--nixos/modules/config/nix-channel.nix3
-rw-r--r--nixos/modules/image/repart.nix55
-rw-r--r--nixos/modules/installer/cd-dvd/channel.nix54
-rw-r--r--nixos/modules/module-list.nix8
-rw-r--r--nixos/modules/programs/direnv.nix2
-rw-r--r--nixos/modules/programs/firefox.nix2
-rw-r--r--nixos/modules/security/sudo.nix4
-rw-r--r--nixos/modules/services/audio/wyoming/faster-whisper.nix1
-rw-r--r--nixos/modules/services/audio/wyoming/openwakeword.nix46
-rw-r--r--nixos/modules/services/backup/postgresql-wal-receiver.nix4
-rw-r--r--nixos/modules/services/continuous-integration/woodpecker/server.nix8
-rw-r--r--nixos/modules/services/databases/postgresql.md32
-rw-r--r--nixos/modules/services/databases/postgresql.nix10
-rw-r--r--nixos/modules/services/development/livebook.md39
-rw-r--r--nixos/modules/services/development/livebook.nix90
-rw-r--r--nixos/modules/services/hardware/fwupd.nix13
-rw-r--r--nixos/modules/services/home-automation/home-assistant.nix1
-rw-r--r--nixos/modules/services/misc/paperless.nix25
-rw-r--r--nixos/modules/services/monitoring/grafana-image-renderer.nix2
-rw-r--r--nixos/modules/services/networking/bitcoind.nix3
-rw-r--r--nixos/modules/services/security/privacyidea.nix458
-rw-r--r--nixos/modules/services/web-apps/akkoma.nix6
-rw-r--r--nixos/modules/services/web-apps/mobilizon.nix4
-rw-r--r--nixos/modules/services/web-apps/peertube.nix131
-rw-r--r--nixos/modules/services/x11/extra-layouts.nix42
-rwxr-xr-xnixos/modules/system/boot/stage-2-init.sh2
-rw-r--r--nixos/modules/virtualisation/oci-containers.nix39
-rw-r--r--nixos/tests/activation/nix-channel.nix14
-rw-r--r--nixos/tests/all-tests.nix5
-rw-r--r--nixos/tests/cinnamon.nix60
-rw-r--r--nixos/tests/freetube.nix41
-rw-r--r--nixos/tests/keymap.nix2
-rw-r--r--nixos/tests/livebook-service.nix43
-rw-r--r--nixos/tests/nginx-sandbox.nix65
-rw-r--r--nixos/tests/openresty-lua.nix48
-rw-r--r--nixos/tests/postgresql.nix4
-rw-r--r--nixos/tests/predictable-interface-names.nix2
-rw-r--r--nixos/tests/privacyidea.nix43
-rw-r--r--nixos/tests/ssh-audit.nix103
44 files changed, 713 insertions, 835 deletions
diff --git a/nixos/doc/manual/configuration/x-windows.chapter.md b/nixos/doc/manual/configuration/x-windows.chapter.md
index 5a870a46cbb82..0451e4d25265f 100644
--- a/nixos/doc/manual/configuration/x-windows.chapter.md
+++ b/nixos/doc/manual/configuration/x-windows.chapter.md
@@ -208,7 +208,7 @@ qt.style = "gtk2";
 
 It is possible to install custom [ XKB
 ](https://en.wikipedia.org/wiki/X_keyboard_extension) keyboard layouts
-using the option `services.xserver.extraLayouts`.
+using the option `services.xserver.xkb.extraLayouts`.
 
 As a first example, we are going to create a layout based on the basic
 US layout, with an additional layer to type some greek symbols by
@@ -235,7 +235,7 @@ xkb_symbols "us-greek"
 A minimal layout specification must include the following:
 
 ```nix
-services.xserver.extraLayouts.us-greek = {
+services.xserver.xkb.extraLayouts.us-greek = {
   description = "US layout with alt-gr greek";
   languages   = [ "eng" ];
   symbolsFile = /yourpath/symbols/us-greek;
@@ -298,7 +298,7 @@ xkb_symbols "media"
 As before, to install the layout do
 
 ```nix
-services.xserver.extraLayouts.media = {
+services.xserver.xkb.extraLayouts.media = {
   description  = "Multimedia keys remapping";
   languages    = [ "eng" ];
   symbolsFile  = /path/to/media-key;
diff --git a/nixos/modules/image/repart.md b/nixos/doc/manual/installation/building-images-via-systemd-repart.chapter.md
index 6d0675f21a033..6d0675f21a033 100644
--- a/nixos/modules/image/repart.md
+++ b/nixos/doc/manual/installation/building-images-via-systemd-repart.chapter.md
diff --git a/nixos/doc/manual/installation/installation.md b/nixos/doc/manual/installation/installation.md
index 140594256609f..f3b1773d865ce 100644
--- a/nixos/doc/manual/installation/installation.md
+++ b/nixos/doc/manual/installation/installation.md
@@ -8,4 +8,5 @@ installing.chapter.md
 changing-config.chapter.md
 upgrading.chapter.md
 building-nixos.chapter.md
+building-images-via-systemd-repart.chapter.md
 ```
diff --git a/nixos/doc/manual/release-notes/rl-2311.section.md b/nixos/doc/manual/release-notes/rl-2311.section.md
index 74d29ab1cf9ca..c5406e183c392 100644
--- a/nixos/doc/manual/release-notes/rl-2311.section.md
+++ b/nixos/doc/manual/release-notes/rl-2311.section.md
@@ -38,6 +38,8 @@
   true`. This is generally safe behavior, but for anyone needing to opt out from
   the check `users.users.${USERNAME}.ignoreShellProgramCheck = true` will do the job.
 
+- Cassandra now defaults to 4.x, updated from 3.11.x.
+
 ## New Services {#sec-release-23.11-new-services}
 
 - [MCHPRS](https://github.com/MCHPR/MCHPRS), a multithreaded Minecraft server built for redstone. Available as [services.mchprs](#opt-services.mchprs.enable).
@@ -72,6 +74,8 @@
 
 - [LibreNMS](https://www.librenms.org), a auto-discovering PHP/MySQL/SNMP based network monitoring. Available as [services.librenms](#opt-services.librenms.enable).
 
+- [Livebook](https://livebook.dev/), an interactive notebook with support for Elixir, graphs, machine learning, and more.
+
 - [sitespeed-io](https://sitespeed.io), a tool that can generate metrics (timings, diagnostics) for websites. Available as [services.sitespeed-io](#opt-services.sitespeed-io.enable).
 
 - [stalwart-mail](https://stalw.art), an all-in-one email server (SMTP, IMAP, JMAP). Available as [services.stalwart-mail](#opt-services.stalwart-mail.enable).
@@ -318,7 +322,7 @@
   order, or relying on `mkBefore` and `mkAfter`, but may impact users calling
   `mkOrder n` with n ≤ 400.
 
-- X keyboard extension (XKB) options have been reorganized into a single attribute set, `services.xserver.xkb`. Specifically, `services.xserver.layout` is now `services.xserver.xkb.layout`, `services.xserver.xkbModel` is now `services.xserver.xkb.model`, `services.xserver.xkbOptions` is now `services.xserver.xkb.options`, `services.xserver.xkbVariant` is now `services.xserver.xkb.variant`, and `services.xserver.xkbDir` is now `services.xserver.xkb.dir`.
+- X keyboard extension (XKB) options have been reorganized into a single attribute set, `services.xserver.xkb`. Specifically, `services.xserver.layout` is now `services.xserver.xkb.layout`, `services.xserver.extraLayouts` is now `services.xserver.xkb.extraLayouts`, `services.xserver.xkbModel` is now `services.xserver.xkb.model`, `services.xserver.xkbOptions` is now `services.xserver.xkb.options`, `services.xserver.xkbVariant` is now `services.xserver.xkb.variant`, and `services.xserver.xkbDir` is now `services.xserver.xkb.dir`.
 
 - `networking.networkmanager.firewallBackend` was removed as NixOS is now using iptables-nftables-compat even when using iptables, therefore Networkmanager now uses the nftables backend unconditionally.
 
@@ -333,10 +337,14 @@
 
 - The `services.mtr-exporter.target` has been removed in favor of `services.mtr-exporter.jobs` which allows specifying multiple targets.
 
+- `blender-with-packages` has been deprecated in favor of `blender.withPackages`, for example `blender.withPackages (ps: [ps.bpycv])`. It behaves similarly to `python3.withPackages`.
+
 - Setting `nixpkgs.config` options while providing an external `pkgs` instance will now raise an error instead of silently ignoring the options. NixOS modules no longer set `nixpkgs.config` to accomodate this. This specifically affects `services.locate`, `services.xserver.displayManager.lightdm.greeters.tiny` and `programs.firefox` NixOS modules. No manual intervention should be required in most cases, however, configurations relying on those modules affecting packages outside the system environment should switch to explicit overlays.
 
 - `service.borgmatic.settings.location` and `services.borgmatic.configurations.<name>.location` are deprecated, please move your options out of sections to the global scope.
 
+- `privacyidea` (and the corresponding `privacyidea-ldap-proxy`) has been removed from nixpkgs because it has severely outdated dependencies that became unmaintainable with nixpkgs' python package-set.
+
 - `dagger` was removed because using a package called `dagger` and packaging it from source violates their trademark policy.
 
 - `win-virtio` package was renamed to `virtio-win` to be consistent with the upstream package name.
@@ -384,6 +392,9 @@
 
 - `zfs` was updated from 2.1.x to 2.2.0, [enabling newer kernel support and adding new features](https://github.com/openzfs/zfs/releases/tag/zfs-2.2.0).
 
+- Elixir now defaults to version
+  [v1.15](https://elixir-lang.org/blog/2023/06/19/elixir-v1-15-0-released/).
+
 - A new option was added to the virtualisation module that enables specifying explicitly named network interfaces in QEMU VMs. The existing `virtualisation.vlans` is still supported for cases where the name of the network interface is irrelevant.
 
 - DocBook option documentation is no longer supported, all module documentation now uses markdown.
@@ -407,6 +418,8 @@
 
 - `services.hedgedoc` has been heavily refactored, reducing the amount of declared options in the module. Most of the options should still work without any changes. Some options have been deprecated, as they no longer have any effect. See [#244941](https://github.com/NixOS/nixpkgs/pull/244941) for more details.
 
+- The [services.woodpecker-server](#opt-services.woodpecker-server.environmentFile) type was changed to list of paths to be more consistent to the woodpecker-agent module
+
 - The module [services.ankisyncd](#opt-services.ankisyncd.package) has been switched to [anki-sync-server-rs](https://github.com/ankicommunity/anki-sync-server-rs) from the old python version, which was difficult to update, had not been updated in a while, and did not support recent versions of anki.
 Unfortunately all servers supporting new clients (newer version of anki-sync-server, anki's built in sync server and this new rust package) do not support the older sync protocol that was used in the old server, so such old clients will also need updating and in particular the anki package in nixpkgs is also being updated in this release.
 The module update takes care of the new config syntax and the data itself (user login and cards) are compatible, so users of the module will be able to just log in again after updating both client and server without any extra action.
@@ -488,10 +501,14 @@ The module update takes care of the new config syntax and the data itself (user
 
   If you use this feature, updates to CoreDNS may require updating `vendorHash` by following these steps again.
 
+- `postgresql_11` has been removed since it'll stop receiving fixes on November 9 2023.
+
 - `ffmpeg` default upgraded from `ffmpeg_5` to `ffmpeg_6`.
 
 - `fusuma` now enables the following plugins: [appmatcher](https://github.com/iberianpig/fusuma-plugin-appmatcher), [keypress](https://github.com/iberianpig/fusuma-plugin-keypress), [sendkey](https://github.com/iberianpig/fusuma-plugin-sendkey), [tap](https://github.com/iberianpig/fusuma-plugin-tap) and [wmctrl](https://github.com/iberianpig/fusuma-plugin-wmctrl).
 
+- `services.bitcoind` now properly respects the `enable` option.
+
 ## Nixpkgs internals {#sec-release-23.11-nixpkgs-internals}
 
 - The use of `sourceRoot = "source";`, `sourceRoot = "source/subdir";`, and similar lines in package derivations using the default `unpackPhase` is deprecated as it requires `unpackPhase` to always produce a directory named "source". Use `sourceRoot = src.name`, `sourceRoot = "${src.name}/subdir";`, or `setSourceRoot = "sourceRoot=$(echo */subdir)";` or similar instead.
diff --git a/nixos/lib/systemd-lib.nix b/nixos/lib/systemd-lib.nix
index 5669aae0bc19e..7b600464bb415 100644
--- a/nixos/lib/systemd-lib.nix
+++ b/nixos/lib/systemd-lib.nix
@@ -20,12 +20,16 @@ in rec {
       pkgs.runCommand "unit-${mkPathSafeName name}"
         { preferLocalBuild = true;
           allowSubstitutes = false;
-          inherit (unit) text;
+          # unit.text can be null. But variables that are null listed in
+          # passAsFile are ignored by nix, resulting in no file being created,
+          # making the mv operation fail.
+          text = optionalString (unit.text != null) unit.text;
+          passAsFile = [ "text" ];
         }
         ''
           name=${shellEscape name}
           mkdir -p "$out/$(dirname -- "$name")"
-          echo -n "$text" > "$out/$name"
+          mv "$textPath" "$out/$name"
         ''
     else
       pkgs.runCommand "unit-${mkPathSafeName name}-disabled"
diff --git a/nixos/modules/config/nix-channel.nix b/nixos/modules/config/nix-channel.nix
index 4abc846b08586..a7ca7a5c74a40 100644
--- a/nixos/modules/config/nix-channel.nix
+++ b/nixos/modules/config/nix-channel.nix
@@ -98,8 +98,7 @@ in
     nix.settings.nix-path = mkIf (! cfg.channel.enable) (mkDefault "");
 
     systemd.tmpfiles.rules = lib.mkIf cfg.channel.enable [
-      "f /root/.nix-channels -"
-      ''w "/root/.nix-channels" - - - - "${config.system.defaultChannel} nixos\n"''
+      ''f /root/.nix-channels - - - - ${config.system.defaultChannel} nixos\n''
     ];
   };
 }
diff --git a/nixos/modules/image/repart.nix b/nixos/modules/image/repart.nix
index e567485c9d342..41e6110885b85 100644
--- a/nixos/modules/image/repart.nix
+++ b/nixos/modules/image/repart.nix
@@ -34,12 +34,13 @@ let
           };
         });
         default = { };
-        example = lib.literalExpression '' {
-          "/EFI/BOOT/BOOTX64.EFI".source =
-            "''${pkgs.systemd}/lib/systemd/boot/efi/systemd-bootx64.efi";
+        example = lib.literalExpression ''
+          {
+            "/EFI/BOOT/BOOTX64.EFI".source =
+              "''${pkgs.systemd}/lib/systemd/boot/efi/systemd-bootx64.efi";
 
-          "/loader/entries/nixos.conf".source = systemdBootEntry;
-        }
+            "/loader/entries/nixos.conf".source = systemdBootEntry;
+          }
         '';
         description = lib.mdDoc "The contents to end up in the filesystem image.";
       };
@@ -90,34 +91,33 @@ in
 
     package = lib.mkPackageOption pkgs "systemd-repart" {
       default = "systemd";
-      example = lib.literalExpression ''
-        pkgs.systemdMinimal.override { withCryptsetup = true; }
-      '';
+      example = "pkgs.systemdMinimal.override { withCryptsetup = true; }";
     };
 
     partitions = lib.mkOption {
       type = with lib.types; attrsOf (submodule partitionOptions);
       default = { };
-      example = lib.literalExpression '' {
-        "10-esp" = {
-          contents = {
-            "/EFI/BOOT/BOOTX64.EFI".source =
-              "''${pkgs.systemd}/lib/systemd/boot/efi/systemd-bootx64.efi";
-          }
-          repartConfig = {
-            Type = "esp";
-            Format = "fat";
+      example = lib.literalExpression ''
+        {
+          "10-esp" = {
+            contents = {
+              "/EFI/BOOT/BOOTX64.EFI".source =
+                "''${pkgs.systemd}/lib/systemd/boot/efi/systemd-bootx64.efi";
+            }
+            repartConfig = {
+              Type = "esp";
+              Format = "fat";
+            };
           };
-        };
-        "20-root" = {
-          storePaths = [ config.system.build.toplevel ];
-          repartConfig = {
-            Type = "root";
-            Format = "ext4";
-            Minimize = "guess";
+          "20-root" = {
+            storePaths = [ config.system.build.toplevel ];
+            repartConfig = {
+              Type = "root";
+              Format = "ext4";
+              Minimize = "guess";
+            };
           };
         };
-      };
       '';
       description = lib.mdDoc ''
         Specify partitions as a set of the names of the partitions with their
@@ -208,10 +208,7 @@ in
           | tee repart-output.json
       '';
 
-    meta = {
-      maintainers = with lib.maintainers; [ nikstur ];
-      doc = ./repart.md;
-    };
+    meta.maintainers = with lib.maintainers; [ nikstur ];
 
   };
 }
diff --git a/nixos/modules/installer/cd-dvd/channel.nix b/nixos/modules/installer/cd-dvd/channel.nix
index 8426ba8fac000..bc70dc985fe00 100644
--- a/nixos/modules/installer/cd-dvd/channel.nix
+++ b/nixos/modules/installer/cd-dvd/channel.nix
@@ -3,8 +3,6 @@
 
 { config, lib, pkgs, ... }:
 
-with lib;
-
 let
   # This is copied into the installer image, so it's important that it is filtered
   # to avoid including a large .git directory.
@@ -27,38 +25,40 @@ let
       if [ ! -e $out/nixos/nixpkgs ]; then
         ln -s . $out/nixos/nixpkgs
       fi
-      ${optionalString (config.system.nixos.revision != null) ''
+      ${lib.optionalString (config.system.nixos.revision != null) ''
         echo -n ${config.system.nixos.revision} > $out/nixos/.git-revision
       ''}
       echo -n ${config.system.nixos.versionSuffix} > $out/nixos/.version-suffix
       echo ${config.system.nixos.versionSuffix} | sed -e s/pre// > $out/nixos/svn-revision
     '';
-
 in
 
 {
-  # Pin the nixpkgs flake in the installer to our cleaned up nixpkgs source.
-  # FIXME: this might be surprising and is really only needed for offline installations,
-  # see discussion in https://github.com/NixOS/nixpkgs/pull/204178#issuecomment-1336289021
-  nix.registry.nixpkgs.to = {
-    type = "path";
-    path = "${channelSources}/nixos";
-  };
+  options.system.installer.channel.enable = (lib.mkEnableOption "bundling NixOS/Nixpkgs channel in the installer") // { default = true; };
+  config = lib.mkIf config.system.installer.channel.enable {
+    # Pin the nixpkgs flake in the installer to our cleaned up nixpkgs source.
+    # FIXME: this might be surprising and is really only needed for offline installations,
+    # see discussion in https://github.com/NixOS/nixpkgs/pull/204178#issuecomment-1336289021
+    nix.registry.nixpkgs.to = {
+      type = "path";
+      path = "${channelSources}/nixos";
+    };
 
-  # Provide the NixOS/Nixpkgs sources in /etc/nixos.  This is required
-  # for nixos-install.
-  boot.postBootCommands = mkAfter
-    ''
-      if ! [ -e /var/lib/nixos/did-channel-init ]; then
-        echo "unpacking the NixOS/Nixpkgs sources..."
-        mkdir -p /nix/var/nix/profiles/per-user/root
-        ${config.nix.package.out}/bin/nix-env -p /nix/var/nix/profiles/per-user/root/channels \
-          -i ${channelSources} --quiet --option build-use-substitutes false \
-          ${optionalString config.boot.initrd.systemd.enable "--option sandbox false"} # There's an issue with pivot_root
-        mkdir -m 0700 -p /root/.nix-defexpr
-        ln -s /nix/var/nix/profiles/per-user/root/channels /root/.nix-defexpr/channels
-        mkdir -m 0755 -p /var/lib/nixos
-        touch /var/lib/nixos/did-channel-init
-      fi
-    '';
+    # Provide the NixOS/Nixpkgs sources in /etc/nixos.  This is required
+    # for nixos-install.
+    boot.postBootCommands = lib.mkAfter
+      ''
+        if ! [ -e /var/lib/nixos/did-channel-init ]; then
+          echo "unpacking the NixOS/Nixpkgs sources..."
+          mkdir -p /nix/var/nix/profiles/per-user/root
+          ${config.nix.package.out}/bin/nix-env -p /nix/var/nix/profiles/per-user/root/channels \
+            -i ${channelSources} --quiet --option build-use-substitutes false \
+            ${lib.optionalString config.boot.initrd.systemd.enable "--option sandbox false"} # There's an issue with pivot_root
+          mkdir -m 0700 -p /root/.nix-defexpr
+          ln -s /nix/var/nix/profiles/per-user/root/channels /root/.nix-defexpr/channels
+          mkdir -m 0755 -p /var/lib/nixos
+          touch /var/lib/nixos/did-channel-init
+        fi
+      '';
+  };
 }
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 55c74bc327dac..9a1bc0450d261 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -485,6 +485,7 @@
   ./services/development/hoogle.nix
   ./services/development/jupyter/default.nix
   ./services/development/jupyterhub/default.nix
+  ./services/development/livebook.nix
   ./services/development/lorri.nix
   ./services/development/rstudio-server/default.nix
   ./services/development/zammad.nix
@@ -1175,7 +1176,6 @@
   ./services/security/opensnitch.nix
   ./services/security/pass-secret-service.nix
   ./services/security/physlock.nix
-  ./services/security/privacyidea.nix
   ./services/security/shibboleth-sp.nix
   ./services/security/sks.nix
   ./services/security/sshguard.nix
@@ -1531,5 +1531,9 @@
   ./virtualisation/waydroid.nix
   ./virtualisation/xe-guest-utilities.nix
   ./virtualisation/xen-dom0.nix
-  { documentation.nixos.extraModules = [ ./virtualisation/qemu-vm.nix ]; }
+  { documentation.nixos.extraModules = [
+    ./virtualisation/qemu-vm.nix
+    ./image/repart.nix
+    ];
+  }
 ]
diff --git a/nixos/modules/programs/direnv.nix b/nixos/modules/programs/direnv.nix
index 77a6568e73b88..2566fa7699bb5 100644
--- a/nixos/modules/programs/direnv.nix
+++ b/nixos/modules/programs/direnv.nix
@@ -54,7 +54,7 @@ in {
   };
 
   imports = [
-    (lib.mkRemovedOptionModule ["programs" "direnv" "persistDerivations"] "persistDerivations was removed as it is on longer necessary")
+    (lib.mkRemovedOptionModule ["programs" "direnv" "persistDerivations"] "persistDerivations was removed as it is no longer necessary")
   ];
 
   config = lib.mkIf cfg.enable {
diff --git a/nixos/modules/programs/firefox.nix b/nixos/modules/programs/firefox.nix
index 85f47530cf5af..1edf935d1649e 100644
--- a/nixos/modules/programs/firefox.nix
+++ b/nixos/modules/programs/firefox.nix
@@ -90,7 +90,7 @@ in
       description = mdDoc ''
         Group policies to install.
 
-        See [Mozilla's documentation](https://github.com/mozilla/policy-templates/blob/master/README.md)
+        See [Mozilla's documentation](https://mozilla.github.io/policy-templates/)
         for a list of available options.
 
         This can be used to install extensions declaratively! Check out the
diff --git a/nixos/modules/security/sudo.nix b/nixos/modules/security/sudo.nix
index d225442773c69..c665c15242a53 100644
--- a/nixos/modules/security/sudo.nix
+++ b/nixos/modules/security/sudo.nix
@@ -6,8 +6,6 @@ let
 
   cfg = config.security.sudo;
 
-  inherit (pkgs) sudo;
-
   toUserString = user: if (isInt user) then "#${toString user}" else "${user}";
   toGroupString = group: if (isInt group) then "%#${toString group}" else "%${group}";
 
@@ -247,7 +245,7 @@ in
       };
     };
 
-    environment.systemPackages = [ sudo ];
+    environment.systemPackages = [ cfg.package ];
 
     security.pam.services.sudo = { sshAgentAuth = true; usshAuth = true; };
 
diff --git a/nixos/modules/services/audio/wyoming/faster-whisper.nix b/nixos/modules/services/audio/wyoming/faster-whisper.nix
index 205e05f2ed176..f156e8314a95f 100644
--- a/nixos/modules/services/audio/wyoming/faster-whisper.nix
+++ b/nixos/modules/services/audio/wyoming/faster-whisper.nix
@@ -138,6 +138,7 @@ in
               --data-dir $STATE_DIRECTORY \
               --download-dir $STATE_DIRECTORY \
               --uri ${options.uri} \
+              --device ${options.device} \
               --model ${options.model} \
               --language ${options.language} \
               --beam-size ${options.beamSize} ${options.extraArgs}
diff --git a/nixos/modules/services/audio/wyoming/openwakeword.nix b/nixos/modules/services/audio/wyoming/openwakeword.nix
index 713945c1df62e..987818246bde5 100644
--- a/nixos/modules/services/audio/wyoming/openwakeword.nix
+++ b/nixos/modules/services/audio/wyoming/openwakeword.nix
@@ -8,6 +8,7 @@ let
   cfg = config.services.wyoming.openwakeword;
 
   inherit (lib)
+    concatStringsSep
     concatMapStringsSep
     escapeShellArgs
     mkOption
@@ -15,6 +16,7 @@ let
     mkEnableOption
     mkIf
     mkPackageOptionMD
+    mkRemovedOptionModule
     types
     ;
 
@@ -25,6 +27,10 @@ let
 in
 
 {
+  imports = [
+    (mkRemovedOptionModule [ "services" "wyoming" "openwakeword" "models" ] "Configuring models has been removed, they are now dynamically discovered and loaded at runtime")
+  ];
+
   meta.buildDocsInSandbox = false;
 
   options.services.wyoming.openwakeword = with types; {
@@ -41,9 +47,20 @@ in
       '';
     };
 
-    models = mkOption {
+    customModelsDirectories = mkOption {
+      type = listOf types.path;
+      default = [];
+      description = lib.mdDoc ''
+        Paths to directories with custom wake word models (*.tflite model files).
+      '';
+    };
+
+    preloadModels = mkOption {
       type = listOf str;
       default = [
+        "ok_nabu"
+      ];
+      example = [
         # wyoming_openwakeword/models/*.tflite
         "alexa"
         "hey_jarvis"
@@ -52,16 +69,6 @@ in
         "ok_nabu"
       ];
       description = mdDoc ''
-        List of wake word models that should be made available.
-      '';
-    };
-
-    preloadModels = mkOption {
-      type = listOf str;
-      default = [
-        "ok_nabu"
-      ];
-      description = mdDoc ''
         List of wake word models to preload after startup.
       '';
     };
@@ -112,14 +119,15 @@ in
         DynamicUser = true;
         User = "wyoming-openwakeword";
         # https://github.com/home-assistant/addons/blob/master/openwakeword/rootfs/etc/s6-overlay/s6-rc.d/openwakeword/run
-        ExecStart = ''
-          ${cfg.package}/bin/wyoming-openwakeword \
-            --uri ${cfg.uri} \
-            ${concatMapStringsSep " " (model: "--model ${model}") cfg.models} \
-            ${concatMapStringsSep " " (model: "--preload-model ${model}") cfg.preloadModels} \
-            --threshold ${cfg.threshold} \
-            --trigger-level ${cfg.triggerLevel} ${cfg.extraArgs}
-        '';
+        ExecStart = concatStringsSep " " [
+          "${cfg.package}/bin/wyoming-openwakeword"
+          "--uri ${cfg.uri}"
+          (concatMapStringsSep " " (model: "--preload-model ${model}") cfg.preloadModels)
+          (concatMapStringsSep " " (dir: "--custom-model-dir ${toString dir}") cfg.customModelsDirectories)
+          "--threshold ${cfg.threshold}"
+          "--trigger-level ${cfg.triggerLevel}"
+          "${cfg.extraArgs}"
+        ];
         CapabilityBoundingSet = "";
         DeviceAllow = "";
         DevicePolicy = "closed";
diff --git a/nixos/modules/services/backup/postgresql-wal-receiver.nix b/nixos/modules/services/backup/postgresql-wal-receiver.nix
index 01fd57f5c5062..773dc0ba447dd 100644
--- a/nixos/modules/services/backup/postgresql-wal-receiver.nix
+++ b/nixos/modules/services/backup/postgresql-wal-receiver.nix
@@ -7,7 +7,7 @@ let
     options = {
       postgresqlPackage = mkOption {
         type = types.package;
-        example = literalExpression "pkgs.postgresql_11";
+        example = literalExpression "pkgs.postgresql_15";
         description = lib.mdDoc ''
           PostgreSQL package to use.
         '';
@@ -124,7 +124,7 @@ in {
         example = literalExpression ''
           {
             main = {
-              postgresqlPackage = pkgs.postgresql_11;
+              postgresqlPackage = pkgs.postgresql_15;
               directory = /mnt/pg_wal/main/;
               slot = "main_wal_receiver";
               connection = "postgresql://user@somehost";
diff --git a/nixos/modules/services/continuous-integration/woodpecker/server.nix b/nixos/modules/services/continuous-integration/woodpecker/server.nix
index cae5ed7cf1161..38b42f7288c05 100644
--- a/nixos/modules/services/continuous-integration/woodpecker/server.nix
+++ b/nixos/modules/services/continuous-integration/woodpecker/server.nix
@@ -31,9 +31,9 @@ in
         description = lib.mdDoc "woodpecker-server config environment variables, for other options read the [documentation](https://woodpecker-ci.org/docs/administration/server-config)";
       };
       environmentFile = lib.mkOption {
-        type = lib.types.nullOr lib.types.path;
-        default = null;
-        example = "/root/woodpecker-server.env";
+        type = with lib.types; coercedTo path (f: [ f ]) (listOf path);
+        default = [ ];
+        example = [ "/root/woodpecker-server.env" ];
         description = lib.mdDoc ''
           File to load environment variables
           from. This is helpful for specifying secrets.
@@ -61,7 +61,7 @@ in
           StateDirectoryMode = "0700";
           UMask = "0007";
           ConfigurationDirectory = "woodpecker-server";
-          EnvironmentFile = lib.optional (cfg.environmentFile != null) cfg.environmentFile;
+          EnvironmentFile = cfg.environmentFile;
           ExecStart = "${cfg.package}/bin/woodpecker-server";
           Restart = "on-failure";
           RestartSec = 15;
diff --git a/nixos/modules/services/databases/postgresql.md b/nixos/modules/services/databases/postgresql.md
index 4d66ee38be426..e4b679a3eee00 100644
--- a/nixos/modules/services/databases/postgresql.md
+++ b/nixos/modules/services/databases/postgresql.md
@@ -17,9 +17,9 @@ PostgreSQL is an advanced, free relational database.
 To enable PostgreSQL, add the following to your {file}`configuration.nix`:
 ```
 services.postgresql.enable = true;
-services.postgresql.package = pkgs.postgresql_11;
+services.postgresql.package = pkgs.postgresql_15;
 ```
-Note that you are required to specify the desired version of PostgreSQL (e.g. `pkgs.postgresql_11`). Since upgrading your PostgreSQL version requires a database dump and reload (see below), NixOS cannot provide a default value for [](#opt-services.postgresql.package) such as the most recent release of PostgreSQL.
+Note that you are required to specify the desired version of PostgreSQL (e.g. `pkgs.postgresql_15`). Since upgrading your PostgreSQL version requires a database dump and reload (see below), NixOS cannot provide a default value for [](#opt-services.postgresql.package) such as the most recent release of PostgreSQL.
 
 <!--
 After running {command}`nixos-rebuild`, you can verify
@@ -119,27 +119,27 @@ A complete list of options for the PostgreSQL module may be found [here](#opt-se
 
 ## Plugins {#module-services-postgres-plugins}
 
-Plugins collection for each PostgreSQL version can be accessed with `.pkgs`. For example, for `pkgs.postgresql_11` package, its plugin collection is accessed by `pkgs.postgresql_11.pkgs`:
+Plugins collection for each PostgreSQL version can be accessed with `.pkgs`. For example, for `pkgs.postgresql_15` package, its plugin collection is accessed by `pkgs.postgresql_15.pkgs`:
 ```ShellSession
 $ nix repl '<nixpkgs>'
 
 Loading '<nixpkgs>'...
 Added 10574 variables.
 
-nix-repl> postgresql_11.pkgs.<TAB><TAB>
-postgresql_11.pkgs.cstore_fdw        postgresql_11.pkgs.pg_repack
-postgresql_11.pkgs.pg_auto_failover  postgresql_11.pkgs.pg_safeupdate
-postgresql_11.pkgs.pg_bigm           postgresql_11.pkgs.pg_similarity
-postgresql_11.pkgs.pg_cron           postgresql_11.pkgs.pg_topn
-postgresql_11.pkgs.pg_hll            postgresql_11.pkgs.pgjwt
-postgresql_11.pkgs.pg_partman        postgresql_11.pkgs.pgroonga
+nix-repl> postgresql_15.pkgs.<TAB><TAB>
+postgresql_15.pkgs.cstore_fdw        postgresql_15.pkgs.pg_repack
+postgresql_15.pkgs.pg_auto_failover  postgresql_15.pkgs.pg_safeupdate
+postgresql_15.pkgs.pg_bigm           postgresql_15.pkgs.pg_similarity
+postgresql_15.pkgs.pg_cron           postgresql_15.pkgs.pg_topn
+postgresql_15.pkgs.pg_hll            postgresql_15.pkgs.pgjwt
+postgresql_15.pkgs.pg_partman        postgresql_15.pkgs.pgroonga
 ...
 ```
 
 To add plugins via NixOS configuration, set `services.postgresql.extraPlugins`:
 ```
-services.postgresql.package = pkgs.postgresql_11;
-services.postgresql.extraPlugins = with pkgs.postgresql_11.pkgs; [
+services.postgresql.package = pkgs.postgresql_12;
+services.postgresql.extraPlugins = with pkgs.postgresql_12.pkgs; [
   pg_repack
   postgis
 ];
@@ -148,7 +148,7 @@ services.postgresql.extraPlugins = with pkgs.postgresql_11.pkgs; [
 You can build custom PostgreSQL-with-plugins (to be used outside of NixOS) using function `.withPackages`. For example, creating a custom PostgreSQL package in an overlay can look like:
 ```
 self: super: {
-  postgresql_custom = self.postgresql_11.withPackages (ps: [
+  postgresql_custom = self.postgresql_12.withPackages (ps: [
     ps.pg_repack
     ps.postgis
   ]);
@@ -158,9 +158,9 @@ self: super: {
 Here's a recipe on how to override a particular plugin through an overlay:
 ```
 self: super: {
-  postgresql_11 = super.postgresql_11.override { this = self.postgresql_11; } // {
-    pkgs = super.postgresql_11.pkgs // {
-      pg_repack = super.postgresql_11.pkgs.pg_repack.overrideAttrs (_: {
+  postgresql_15 = super.postgresql_15.override { this = self.postgresql_15; } // {
+    pkgs = super.postgresql_15.pkgs // {
+      pg_repack = super.postgresql_15.pkgs.pg_repack.overrideAttrs (_: {
         name = "pg_repack-v20181024";
         src = self.fetchzip {
           url = "https://github.com/reorg/pg_repack/archive/923fa2f3c709a506e111cc963034bf2fd127aa00.tar.gz";
diff --git a/nixos/modules/services/databases/postgresql.nix b/nixos/modules/services/databases/postgresql.nix
index 2d4ef05631823..21e6a60e32a35 100644
--- a/nixos/modules/services/databases/postgresql.nix
+++ b/nixos/modules/services/databases/postgresql.nix
@@ -55,7 +55,7 @@ in
 
       package = mkOption {
         type = types.package;
-        example = literalExpression "pkgs.postgresql_11";
+        example = literalExpression "pkgs.postgresql_15";
         description = lib.mdDoc ''
           PostgreSQL package to use.
         '';
@@ -78,7 +78,7 @@ in
       dataDir = mkOption {
         type = types.path;
         defaultText = literalExpression ''"/var/lib/postgresql/''${config.services.postgresql.package.psqlSchema}"'';
-        example = "/var/lib/postgresql/11";
+        example = "/var/lib/postgresql/15";
         description = lib.mdDoc ''
           The data directory for PostgreSQL. If left as the default value
           this directory will automatically be created before the PostgreSQL server starts, otherwise
@@ -387,7 +387,7 @@ in
       extraPlugins = mkOption {
         type = types.listOf types.path;
         default = [];
-        example = literalExpression "with pkgs.postgresql_11.pkgs; [ postgis pg_repack ]";
+        example = literalExpression "with pkgs.postgresql_15.pkgs; [ postgis pg_repack ]";
         description = lib.mdDoc ''
           List of PostgreSQL plugins. PostgreSQL version for each plugin should
           match version for `services.postgresql.package` value.
@@ -399,7 +399,7 @@ in
         default = {};
         description = lib.mdDoc ''
           PostgreSQL configuration. Refer to
-          <https://www.postgresql.org/docs/11/config-setting.html#CONFIG-SETTING-CONFIGURATION-FILE>
+          <https://www.postgresql.org/docs/15/config-setting.html#CONFIG-SETTING-CONFIGURATION-FILE>
           for an overview of `postgresql.conf`.
 
           ::: {.note}
@@ -461,7 +461,7 @@ in
         base = if versionAtLeast config.system.stateVersion "23.11" then pkgs.postgresql_15
             else if versionAtLeast config.system.stateVersion "22.05" then pkgs.postgresql_14
             else if versionAtLeast config.system.stateVersion "21.11" then pkgs.postgresql_13
-            else if versionAtLeast config.system.stateVersion "20.03" then pkgs.postgresql_11
+            else if versionAtLeast config.system.stateVersion "20.03" then mkThrow "11"
             else if versionAtLeast config.system.stateVersion "17.09" then mkThrow "9_6"
             else mkThrow "9_5";
     in
diff --git a/nixos/modules/services/development/livebook.md b/nixos/modules/services/development/livebook.md
new file mode 100644
index 0000000000000..73ddc57f6179a
--- /dev/null
+++ b/nixos/modules/services/development/livebook.md
@@ -0,0 +1,39 @@
+# Livebook {#module-services-livebook}
+
+[Livebook](https://livebook.dev/) is a web application for writing
+interactive and collaborative code notebooks.
+
+## Basic Usage {#module-services-livebook-basic-usage}
+
+Enabling the `livebook` service creates a user
+[`systemd`](https://www.freedesktop.org/wiki/Software/systemd/) unit
+which runs the server.
+
+```
+{ ... }:
+
+{
+  services.livebook = {
+    enableUserService = true;
+    port = 20123;
+    # See note below about security
+    environmentFile = pkgs.writeText "livebook.env" ''
+      LIVEBOOK_PASSWORD = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
+    '';
+  };
+}
+```
+
+::: {.note}
+
+The Livebook server has the ability to run any command as the user it
+is running under, so securing access to it with a password is highly
+recommended.
+
+Putting the password in the Nix configuration like above is an easy
+way to get started but it is not recommended in the real world because
+the `livebook.env` file will be added to the world-readable Nix store.
+A better approach would be to put the password in some secure
+user-readable location and set `environmentFile = /home/user/secure/livebook.env`.
+
+:::
diff --git a/nixos/modules/services/development/livebook.nix b/nixos/modules/services/development/livebook.nix
new file mode 100644
index 0000000000000..3991a4125ec39
--- /dev/null
+++ b/nixos/modules/services/development/livebook.nix
@@ -0,0 +1,90 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.livebook;
+in
+{
+  options.services.livebook = {
+    # Since livebook doesn't have a granular permission system (a user
+    # either has access to all the data or none at all), the decision
+    # was made to run this as a user service.  If that changes in the
+    # future, this can be changed to a system service.
+    enableUserService = mkEnableOption "a user service for Livebook";
+
+    environmentFile = mkOption {
+      type = types.path;
+      description = lib.mdDoc ''
+        Environment file as defined in {manpage}`systemd.exec(5)` passed to the service.
+
+        This must contain at least `LIVEBOOK_PASSWORD` or
+        `LIVEBOOK_TOKEN_ENABLED=false`.  See `livebook server --help`
+        for other options.'';
+    };
+
+    erlang_node_short_name = mkOption {
+      type = with types; nullOr str;
+      default = null;
+      example = "livebook";
+      description = "A short name for the distributed node.";
+    };
+
+    erlang_node_name = mkOption {
+      type = with types; nullOr str;
+      default = null;
+      example = "livebook@127.0.0.1";
+      description = "The name for the app distributed node.";
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 8080;
+      description = "The port to start the web application on.";
+    };
+
+    address = mkOption {
+      type = types.str;
+      default = "127.0.0.1";
+      description = lib.mdDoc ''
+        The address to start the web application on.  Must be a valid IPv4 or
+        IPv6 address.
+      '';
+    };
+
+    options = mkOption {
+      type = with types; attrsOf str;
+      default = { };
+      description = lib.mdDoc ''
+        Additional options to pass as command-line arguments to the server.
+      '';
+      example = literalExpression ''
+        {
+          cookie = "a value shared by all nodes in this cluster";
+        }
+      '';
+    };
+  };
+
+  config = mkIf cfg.enableUserService {
+    systemd.user.services.livebook = {
+      serviceConfig = {
+        Restart = "always";
+        EnvironmentFile = cfg.environmentFile;
+        ExecStart =
+          let
+            args = lib.cli.toGNUCommandLineShell { } ({
+              inherit (cfg) port;
+              ip = cfg.address;
+              name = cfg.erlang_node_name;
+              sname = cfg.erlang_node_short_name;
+            } // cfg.options);
+          in
+          "${pkgs.livebook}/bin/livebook server ${args}";
+      };
+      path = [ pkgs.bash ];
+      wantedBy = [ "default.target" ];
+    };
+  };
+
+  meta.doc = ./livebook.md;
+}
diff --git a/nixos/modules/services/hardware/fwupd.nix b/nixos/modules/services/hardware/fwupd.nix
index 4e5913fd27515..7a938459d0cb5 100644
--- a/nixos/modules/services/hardware/fwupd.nix
+++ b/nixos/modules/services/hardware/fwupd.nix
@@ -181,7 +181,18 @@ in {
     # required to update the firmware of disks
     services.udisks2.enable = true;
 
-    systemd.packages = [ cfg.package ];
+    systemd = {
+      packages = [ cfg.package ];
+
+      # fwupd-refresh expects a user that we do not create, so just run with DynamicUser
+      # instead and ensure we take ownership of /var/lib/fwupd
+      services.fwupd-refresh.serviceConfig = {
+        DynamicUser = true;
+        StateDirectory = "fwupd";
+      };
+
+      timers.fwupd-refresh.wantedBy = [ "timers.target" ];
+    };
 
     security.polkit.enable = true;
   };
diff --git a/nixos/modules/services/home-automation/home-assistant.nix b/nixos/modules/services/home-automation/home-assistant.nix
index 0e6fa65667af0..789b06af19b1c 100644
--- a/nixos/modules/services/home-automation/home-assistant.nix
+++ b/nixos/modules/services/home-automation/home-assistant.nix
@@ -455,6 +455,7 @@ in {
           "govee_ble"
           "homekit_controller"
           "inkbird"
+          "improv_ble"
           "keymitt_ble"
           "led_ble"
           "medcom_ble"
diff --git a/nixos/modules/services/misc/paperless.nix b/nixos/modules/services/misc/paperless.nix
index 9b8bd62809c5b..1e0a8d0f928e0 100644
--- a/nixos/modules/services/misc/paperless.nix
+++ b/nixos/modules/services/misc/paperless.nix
@@ -332,12 +332,28 @@ in
       # during migrations
       bindsTo = [ "paperless-scheduler.service" ];
       after = [ "paperless-scheduler.service" ];
+      # Setup PAPERLESS_SECRET_KEY.
+      # If this environment variable is left unset, paperless-ngx defaults
+      # to a well-known value, which is insecure.
+      script = let
+        secretKeyFile = "${cfg.dataDir}/nixos-paperless-secret-key";
+      in ''
+        if [[ ! -f '${secretKeyFile}' ]]; then
+          (
+            umask 0377
+            tr -dc A-Za-z0-9 < /dev/urandom | head -c64 | ${pkgs.moreutils}/bin/sponge '${secretKeyFile}'
+          )
+        fi
+        export PAPERLESS_SECRET_KEY=$(cat '${secretKeyFile}')
+        if [[ ! $PAPERLESS_SECRET_KEY ]]; then
+          echo "PAPERLESS_SECRET_KEY is empty, refusing to start."
+          exit 1
+        fi
+        exec ${pkg.python.pkgs.gunicorn}/bin/gunicorn \
+          -c ${pkg}/lib/paperless-ngx/gunicorn.conf.py paperless.asgi:application
+      '';
       serviceConfig = defaultServiceConfig // {
         User = cfg.user;
-        ExecStart = ''
-          ${pkg.python.pkgs.gunicorn}/bin/gunicorn \
-            -c ${pkg}/lib/paperless-ngx/gunicorn.conf.py paperless.asgi:application
-        '';
         Restart = "on-failure";
 
         # gunicorn needs setuid, liblapack needs mbind
@@ -349,7 +365,6 @@ in
         CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
       };
       environment = env // {
-        PATH = mkForce pkg.path;
         PYTHONPATH = "${pkg.python.pkgs.makePythonPath pkg.propagatedBuildInputs}:${pkg}/lib/paperless-ngx/src";
       };
       # Allow the web interface to access the private /tmp directory of the server.
diff --git a/nixos/modules/services/monitoring/grafana-image-renderer.nix b/nixos/modules/services/monitoring/grafana-image-renderer.nix
index 36258866646ab..afe9eb4d7b959 100644
--- a/nixos/modules/services/monitoring/grafana-image-renderer.nix
+++ b/nixos/modules/services/monitoring/grafana-image-renderer.nix
@@ -108,7 +108,7 @@ in {
 
     services.grafana.settings.rendering = mkIf cfg.provisionGrafana {
       server_url = "http://localhost:${toString cfg.settings.service.port}/render";
-      callback_url = "http://localhost:${toString config.services.grafana.settings.server.http_port}";
+      callback_url = "http://${config.services.grafana.settings.server.http_addr}:${toString config.services.grafana.settings.server.http_port}";
     };
 
     services.grafana-image-renderer.chromium = mkDefault pkgs.chromium;
diff --git a/nixos/modules/services/networking/bitcoind.nix b/nixos/modules/services/networking/bitcoind.nix
index a86d52b7202d8..a48066b43b162 100644
--- a/nixos/modules/services/networking/bitcoind.nix
+++ b/nixos/modules/services/networking/bitcoind.nix
@@ -3,8 +3,7 @@
 with lib;
 
 let
-
-  eachBitcoind = config.services.bitcoind;
+  eachBitcoind = filterAttrs (bitcoindName: cfg: cfg.enable) config.services.bitcoind;
 
   rpcUserOpts = { name, ... }: {
     options = {
diff --git a/nixos/modules/services/security/privacyidea.nix b/nixos/modules/services/security/privacyidea.nix
deleted file mode 100644
index 664335cb58e89..0000000000000
--- a/nixos/modules/services/security/privacyidea.nix
+++ /dev/null
@@ -1,458 +0,0 @@
-{ config, lib, options, pkgs, ... }:
-
-with lib;
-
-let
-  cfg = config.services.privacyidea;
-  opt = options.services.privacyidea;
-
-  uwsgi = pkgs.uwsgi.override { plugins = [ "python3" ]; python3 = pkgs.python310; };
-  python = uwsgi.python3;
-  penv = python.withPackages (const [ pkgs.privacyidea ]);
-  logCfg = pkgs.writeText "privacyidea-log.cfg" ''
-    [formatters]
-    keys=detail
-
-    [handlers]
-    keys=stream
-
-    [formatter_detail]
-    class=privacyidea.lib.log.SecureFormatter
-    format=[%(asctime)s][%(process)d][%(thread)d][%(levelname)s][%(name)s:%(lineno)d] %(message)s
-
-    [handler_stream]
-    class=StreamHandler
-    level=NOTSET
-    formatter=detail
-    args=(sys.stdout,)
-
-    [loggers]
-    keys=root,privacyidea
-
-    [logger_privacyidea]
-    handlers=stream
-    qualname=privacyidea
-    level=INFO
-
-    [logger_root]
-    handlers=stream
-    level=ERROR
-  '';
-
-  piCfgFile = pkgs.writeText "privacyidea.cfg" ''
-    SUPERUSER_REALM = [ '${concatStringsSep "', '" cfg.superuserRealm}' ]
-    SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2:///privacyidea'
-    SECRET_KEY = '${cfg.secretKey}'
-    PI_PEPPER = '${cfg.pepper}'
-    PI_ENCFILE = '${cfg.encFile}'
-    PI_AUDIT_KEY_PRIVATE = '${cfg.auditKeyPrivate}'
-    PI_AUDIT_KEY_PUBLIC = '${cfg.auditKeyPublic}'
-    PI_LOGCONFIG = '${logCfg}'
-    ${cfg.extraConfig}
-  '';
-
-  renderValue = x:
-    if isList x then concatMapStringsSep "," (x: ''"${x}"'') x
-    else if isString x && hasInfix "," x then ''"${x}"''
-    else x;
-
-  ldapProxyConfig = pkgs.writeText "ldap-proxy.ini"
-    (generators.toINI {}
-      (flip mapAttrs cfg.ldap-proxy.settings
-        (const (mapAttrs (const renderValue)))));
-
-  privacyidea-token-janitor = pkgs.writeShellScriptBin "privacyidea-token-janitor" ''
-    exec -a privacyidea-token-janitor \
-      /run/wrappers/bin/sudo -u ${cfg.user} \
-      env PRIVACYIDEA_CONFIGFILE=${cfg.stateDir}/privacyidea.cfg \
-      ${penv}/bin/privacyidea-token-janitor $@
-  '';
-in
-
-{
-  options = {
-    services.privacyidea = {
-      enable = mkEnableOption (lib.mdDoc "PrivacyIDEA");
-
-      environmentFile = mkOption {
-        type = types.nullOr types.path;
-        default = null;
-        example = "/root/privacyidea.env";
-        description = lib.mdDoc ''
-          File to load as environment file. Environment variables
-          from this file will be interpolated into the config file
-          using `envsubst` which is helpful for specifying
-          secrets:
-          ```
-          { services.privacyidea.secretKey = "$SECRET"; }
-          ```
-
-          The environment-file can now specify the actual secret key:
-          ```
-          SECRET=veryverytopsecret
-          ```
-        '';
-      };
-
-      stateDir = mkOption {
-        type = types.str;
-        default = "/var/lib/privacyidea";
-        description = lib.mdDoc ''
-          Directory where all PrivacyIDEA files will be placed by default.
-        '';
-      };
-
-      superuserRealm = mkOption {
-        type = types.listOf types.str;
-        default = [ "super" "administrators" ];
-        description = lib.mdDoc ''
-          The realm where users are allowed to login as administrators.
-        '';
-      };
-
-      secretKey = mkOption {
-        type = types.str;
-        example = "t0p s3cr3t";
-        description = lib.mdDoc ''
-          This is used to encrypt the auth_token.
-        '';
-      };
-
-      pepper = mkOption {
-        type = types.str;
-        example = "Never know...";
-        description = lib.mdDoc ''
-          This is used to encrypt the admin passwords.
-        '';
-      };
-
-      encFile = mkOption {
-        type = types.str;
-        default = "${cfg.stateDir}/enckey";
-        defaultText = literalExpression ''"''${config.${opt.stateDir}}/enckey"'';
-        description = lib.mdDoc ''
-          This is used to encrypt the token data and token passwords
-        '';
-      };
-
-      auditKeyPrivate = mkOption {
-        type = types.str;
-        default = "${cfg.stateDir}/private.pem";
-        defaultText = literalExpression ''"''${config.${opt.stateDir}}/private.pem"'';
-        description = lib.mdDoc ''
-          Private Key for signing the audit log.
-        '';
-      };
-
-      auditKeyPublic = mkOption {
-        type = types.str;
-        default = "${cfg.stateDir}/public.pem";
-        defaultText = literalExpression ''"''${config.${opt.stateDir}}/public.pem"'';
-        description = lib.mdDoc ''
-          Public key for checking signatures of the audit log.
-        '';
-      };
-
-      adminPasswordFile = mkOption {
-        type = types.path;
-        description = lib.mdDoc "File containing password for the admin user";
-      };
-
-      adminEmail = mkOption {
-        type = types.str;
-        example = "admin@example.com";
-        description = lib.mdDoc "Mail address for the admin user";
-      };
-
-      extraConfig = mkOption {
-        type = types.lines;
-        default = "";
-        description = lib.mdDoc ''
-          Extra configuration options for pi.cfg.
-        '';
-      };
-
-      user = mkOption {
-        type = types.str;
-        default = "privacyidea";
-        description = lib.mdDoc "User account under which PrivacyIDEA runs.";
-      };
-
-      group = mkOption {
-        type = types.str;
-        default = "privacyidea";
-        description = lib.mdDoc "Group account under which PrivacyIDEA runs.";
-      };
-
-      tokenjanitor = {
-        enable = mkEnableOption (lib.mdDoc "automatic runs of the token janitor");
-        interval = mkOption {
-          default = "quarterly";
-          type = types.str;
-          description = lib.mdDoc ''
-            Interval in which the cleanup program is supposed to run.
-            See {manpage}`systemd.time(7)` for further information.
-          '';
-        };
-        action = mkOption {
-          type = types.enum [ "delete" "mark" "disable" "unassign" ];
-          description = lib.mdDoc ''
-            Which action to take for matching tokens.
-          '';
-        };
-        unassigned = mkOption {
-          default = false;
-          type = types.bool;
-          description = lib.mdDoc ''
-            Whether to search for **unassigned** tokens
-            and apply [](#opt-services.privacyidea.tokenjanitor.action)
-            onto them.
-          '';
-        };
-        orphaned = mkOption {
-          default = true;
-          type = types.bool;
-          description = lib.mdDoc ''
-            Whether to search for **orphaned** tokens
-            and apply [](#opt-services.privacyidea.tokenjanitor.action)
-            onto them.
-          '';
-        };
-      };
-
-      ldap-proxy = {
-        enable = mkEnableOption (lib.mdDoc "PrivacyIDEA LDAP Proxy");
-
-        configFile = mkOption {
-          type = types.nullOr types.path;
-          default = null;
-          description = lib.mdDoc ''
-            Path to PrivacyIDEA LDAP Proxy configuration (proxy.ini).
-          '';
-        };
-
-        user = mkOption {
-          type = types.str;
-          default = "pi-ldap-proxy";
-          description = lib.mdDoc "User account under which PrivacyIDEA LDAP proxy runs.";
-        };
-
-        group = mkOption {
-          type = types.str;
-          default = "pi-ldap-proxy";
-          description = lib.mdDoc "Group account under which PrivacyIDEA LDAP proxy runs.";
-        };
-
-        settings = mkOption {
-          type = with types; attrsOf (attrsOf (oneOf [ str bool int (listOf str) ]));
-          default = {};
-          description = lib.mdDoc ''
-            Attribute-set containing the settings for `privacyidea-ldap-proxy`.
-            It's possible to pass secrets using env-vars as substitutes and
-            use the option [](#opt-services.privacyidea.ldap-proxy.environmentFile)
-            to inject them via `envsubst`.
-          '';
-        };
-
-        environmentFile = mkOption {
-          default = null;
-          type = types.nullOr types.str;
-          description = lib.mdDoc ''
-            Environment file containing secrets to be substituted into
-            [](#opt-services.privacyidea.ldap-proxy.settings).
-          '';
-        };
-      };
-    };
-  };
-
-  config = mkMerge [
-
-    (mkIf cfg.enable {
-
-      assertions = [
-        {
-          assertion = cfg.tokenjanitor.enable -> (cfg.tokenjanitor.orphaned || cfg.tokenjanitor.unassigned);
-          message = ''
-            privacyidea-token-janitor has no effect if neither orphaned nor unassigned tokens
-            are to be searched.
-          '';
-        }
-      ];
-
-      environment.systemPackages = [ pkgs.privacyidea (hiPrio privacyidea-token-janitor) ];
-
-      services.postgresql.enable = mkDefault true;
-
-      systemd.services.privacyidea-tokenjanitor = mkIf cfg.tokenjanitor.enable {
-        environment.PRIVACYIDEA_CONFIGFILE = "${cfg.stateDir}/privacyidea.cfg";
-        path = [ penv ];
-        serviceConfig = {
-          CapabilityBoundingSet = [ "" ];
-          ExecStart = "${pkgs.writeShellScript "pi-token-janitor" ''
-            ${optionalString cfg.tokenjanitor.orphaned ''
-              echo >&2 "Removing orphaned tokens..."
-              privacyidea-token-janitor find \
-                --orphaned true \
-                --action ${cfg.tokenjanitor.action}
-            ''}
-            ${optionalString cfg.tokenjanitor.unassigned ''
-              echo >&2 "Removing unassigned tokens..."
-              privacyidea-token-janitor find \
-                --assigned false \
-                --action ${cfg.tokenjanitor.action}
-            ''}
-          ''}";
-          Group = cfg.group;
-          LockPersonality = true;
-          MemoryDenyWriteExecute = true;
-          ProtectHome = true;
-          ProtectHostname = true;
-          ProtectKernelLogs = true;
-          ProtectKernelModules = true;
-          ProtectKernelTunables = true;
-          ProtectSystem = "strict";
-          ReadWritePaths = cfg.stateDir;
-          Type = "oneshot";
-          User = cfg.user;
-          WorkingDirectory = cfg.stateDir;
-        };
-      };
-      systemd.timers.privacyidea-tokenjanitor = mkIf cfg.tokenjanitor.enable {
-        wantedBy = [ "timers.target" ];
-        timerConfig.OnCalendar = cfg.tokenjanitor.interval;
-        timerConfig.Persistent = true;
-      };
-
-      systemd.services.privacyidea = let
-        piuwsgi = pkgs.writeText "uwsgi.json" (builtins.toJSON {
-          uwsgi = {
-            buffer-size = 8192;
-            plugins = [ "python3" ];
-            pythonpath = "${penv}/${uwsgi.python3.sitePackages}";
-            socket = "/run/privacyidea/socket";
-            uid = cfg.user;
-            gid = cfg.group;
-            chmod-socket = 770;
-            chown-socket = "${cfg.user}:nginx";
-            chdir = cfg.stateDir;
-            wsgi-file = "${penv}/etc/privacyidea/privacyideaapp.wsgi";
-            processes = 4;
-            harakiri = 60;
-            reload-mercy = 8;
-            stats = "/run/privacyidea/stats.socket";
-            max-requests = 2000;
-            limit-as = 1024;
-            reload-on-as = 512;
-            reload-on-rss = 256;
-            no-orphans = true;
-            vacuum = true;
-          };
-        });
-      in {
-        wantedBy = [ "multi-user.target" ];
-        after = [ "postgresql.service" ];
-        path = with pkgs; [ openssl ];
-        environment.PRIVACYIDEA_CONFIGFILE = "${cfg.stateDir}/privacyidea.cfg";
-        preStart = let
-          pi-manage = "${config.security.sudo.package}/bin/sudo -u privacyidea -HE ${penv}/bin/pi-manage";
-          pgsu = config.services.postgresql.superUser;
-          psql = config.services.postgresql.package;
-        in ''
-          mkdir -p ${cfg.stateDir} /run/privacyidea
-          chown ${cfg.user}:${cfg.group} -R ${cfg.stateDir} /run/privacyidea
-          umask 077
-          ${lib.getBin pkgs.envsubst}/bin/envsubst -o ${cfg.stateDir}/privacyidea.cfg \
-                                                   -i "${piCfgFile}"
-          chown ${cfg.user}:${cfg.group} ${cfg.stateDir}/privacyidea.cfg
-          if ! test -e "${cfg.stateDir}/db-created"; then
-            ${config.security.sudo.package}/bin/sudo -u ${pgsu} ${psql}/bin/createuser --no-superuser --no-createdb --no-createrole ${cfg.user}
-            ${config.security.sudo.package}/bin/sudo -u ${pgsu} ${psql}/bin/createdb --owner ${cfg.user} privacyidea
-            ${pi-manage} create_enckey
-            ${pi-manage} create_audit_keys
-            ${pi-manage} createdb
-            ${pi-manage} admin add admin -e ${cfg.adminEmail} -p "$(cat ${cfg.adminPasswordFile})"
-            ${pi-manage} db stamp head -d ${penv}/lib/privacyidea/migrations
-            touch "${cfg.stateDir}/db-created"
-            chmod g+r "${cfg.stateDir}/enckey" "${cfg.stateDir}/private.pem"
-          fi
-          ${pi-manage} db upgrade -d ${penv}/lib/privacyidea/migrations
-        '';
-        serviceConfig = {
-          Type = "notify";
-          ExecStart = "${uwsgi}/bin/uwsgi --json ${piuwsgi}";
-          ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
-          EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
-          ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID";
-          NotifyAccess = "main";
-          KillSignal = "SIGQUIT";
-        };
-      };
-
-      users.users.privacyidea = mkIf (cfg.user == "privacyidea") {
-        group = cfg.group;
-        isSystemUser = true;
-      };
-
-      users.groups.privacyidea = mkIf (cfg.group == "privacyidea") {};
-    })
-
-    (mkIf cfg.ldap-proxy.enable {
-
-      assertions = [
-        { assertion = let
-            xor = a: b: a && !b || !a && b;
-          in xor (cfg.ldap-proxy.settings == {}) (cfg.ldap-proxy.configFile == null);
-          message = "configFile & settings are mutually exclusive for services.privacyidea.ldap-proxy!";
-        }
-      ];
-
-      warnings = mkIf (cfg.ldap-proxy.configFile != null) [
-        "Using services.privacyidea.ldap-proxy.configFile is deprecated! Use the RFC42-style settings option instead!"
-      ];
-
-      systemd.services.privacyidea-ldap-proxy = let
-        ldap-proxy-env = pkgs.python3.withPackages (ps: [ ps.privacyidea-ldap-proxy ]);
-      in {
-        description = "privacyIDEA LDAP proxy";
-        wantedBy = [ "multi-user.target" ];
-        serviceConfig = {
-          User = cfg.ldap-proxy.user;
-          Group = cfg.ldap-proxy.group;
-          StateDirectory = "privacyidea-ldap-proxy";
-          EnvironmentFile = mkIf (cfg.ldap-proxy.environmentFile != null)
-            [ cfg.ldap-proxy.environmentFile ];
-          ExecStartPre =
-            "${pkgs.writeShellScript "substitute-secrets-ldap-proxy" ''
-              umask 0077
-              ${pkgs.envsubst}/bin/envsubst \
-                -i ${ldapProxyConfig} \
-                -o $STATE_DIRECTORY/ldap-proxy.ini
-            ''}";
-          ExecStart = let
-            configPath = if cfg.ldap-proxy.settings != {}
-              then "%S/privacyidea-ldap-proxy/ldap-proxy.ini"
-              else cfg.ldap-proxy.configFile;
-          in ''
-            ${ldap-proxy-env}/bin/twistd \
-              --nodaemon \
-              --pidfile= \
-              -u ${cfg.ldap-proxy.user} \
-              -g ${cfg.ldap-proxy.group} \
-              ldap-proxy \
-              -c ${configPath}
-          '';
-          Restart = "always";
-        };
-      };
-
-      users.users.pi-ldap-proxy = mkIf (cfg.ldap-proxy.user == "pi-ldap-proxy") {
-        group = cfg.ldap-proxy.group;
-        isSystemUser = true;
-      };
-
-      users.groups.pi-ldap-proxy = mkIf (cfg.ldap-proxy.group == "pi-ldap-proxy") {};
-    })
-  ];
-
-}
diff --git a/nixos/modules/services/web-apps/akkoma.nix b/nixos/modules/services/web-apps/akkoma.nix
index eaee70c712bb8..5f9bbbd663744 100644
--- a/nixos/modules/services/web-apps/akkoma.nix
+++ b/nixos/modules/services/web-apps/akkoma.nix
@@ -86,7 +86,7 @@ let
   # Erlang/Elixir uses a somewhat special format for IP addresses
   erlAddr = addr: fileContents
     (pkgs.runCommand addr {
-      nativeBuildInputs = with pkgs; [ elixir ];
+      nativeBuildInputs = [ cfg.package.elixirPackage ];
       code = ''
         case :inet.parse_address('${addr}') do
           {:ok, addr} -> IO.inspect addr
@@ -96,7 +96,7 @@ let
       passAsFile = [ "code" ];
     } ''elixir "$codePath" >"$out"'');
 
-  format = pkgs.formats.elixirConf { };
+  format = pkgs.formats.elixirConf { elixir = cfg.package.elixirPackage; };
   configFile = format.generate "config.exs"
     (replaceSec
       (attrsets.updateManyAttrsByPath [{
@@ -146,7 +146,7 @@ let
 
   initSecretsScript = writeShell {
     name = "akkoma-init-secrets";
-    runtimeInputs = with pkgs; [ coreutils elixir ];
+    runtimeInputs = with pkgs; [ coreutils cfg.package.elixirPackage ];
     text = let
       key-base = web.secret_key_base;
       jwt-signer = ex.":joken".":default_signer";
diff --git a/nixos/modules/services/web-apps/mobilizon.nix b/nixos/modules/services/web-apps/mobilizon.nix
index e9264a38f0e61..343c5cead2b15 100644
--- a/nixos/modules/services/web-apps/mobilizon.nix
+++ b/nixos/modules/services/web-apps/mobilizon.nix
@@ -8,7 +8,7 @@ let
   user = "mobilizon";
   group = "mobilizon";
 
-  settingsFormat = pkgs.formats.elixirConf { elixir = pkgs.elixir_1_14; };
+  settingsFormat = pkgs.formats.elixirConf { elixir = cfg.package.elixirPackage; };
 
   configFile = settingsFormat.generate "mobilizon-config.exs" cfg.settings;
 
@@ -309,7 +309,7 @@ in
           genCookie = "IO.puts(Base.encode32(:crypto.strong_rand_bytes(32)))";
 
           evalElixir = str: ''
-            ${pkgs.elixir_1_14}/bin/elixir --eval '${str}'
+            ${cfg.package.elixirPackage}/bin/elixir --eval '${str}'
           '';
         in
         ''
diff --git a/nixos/modules/services/web-apps/peertube.nix b/nixos/modules/services/web-apps/peertube.nix
index 17e170c33deea..a22467611410b 100644
--- a/nixos/modules/services/web-apps/peertube.nix
+++ b/nixos/modules/services/web-apps/peertube.nix
@@ -352,6 +352,7 @@ in {
         };
         storage = {
           tmp = lib.mkDefault "/var/lib/peertube/storage/tmp/";
+          tmp_persistent = lib.mkDefault "/var/lib/peertube/storage/tmp_persistent/";
           bin = lib.mkDefault "/var/lib/peertube/storage/bin/";
           avatars = lib.mkDefault "/var/lib/peertube/storage/avatars/";
           videos = lib.mkDefault "/var/lib/peertube/storage/videos/";
@@ -521,6 +522,21 @@ in {
           '';
         };
 
+        locations."~ ^/api/v1/runners/jobs/[^/]+/(update|success)$" = {
+          tryFiles = "/dev/null @api";
+          root = cfg.settings.storage.tmp;
+          priority = 1135;
+
+          extraConfig = ''
+            client_max_body_size                        12G;
+            add_header X-File-Maximum-Size              8G always;
+          '' + lib.optionalString cfg.enableWebHttps ''
+            add_header Strict-Transport-Security        'max-age=63072000; includeSubDomains';
+          '' + lib.optionalString config.services.nginx.virtualHosts.${cfg.localDomain}.http3 ''
+            add_header Alt-Svc                          'h3=":443"; ma=86400';
+          '';
+        };
+
         locations."~ ^/api/v1/(videos|video-playlists|video-channels|users/me)" = {
           tryFiles = "/dev/null @api";
           priority = 1140;
@@ -607,72 +623,33 @@ in {
           '';
         };
 
-        locations."^~ /lazy-static/avatars/" = {
-          tryFiles = "$uri @api";
-          root = cfg.settings.storage.avatars;
-          priority = 1330;
-          extraConfig = ''
-            if ($request_method = 'OPTIONS') {
-              ${nginxCommonHeaders}
-              add_header Access-Control-Max-Age         1728000;
-              add_header Cache-Control                  'no-cache';
-              add_header Content-Type                   'text/plain charset=UTF-8';
-              add_header Content-Length                 0;
-              return                                    204;
-            }
-
-            ${nginxCommonHeaders}
-            add_header Cache-Control                    'public, max-age=7200';
-
-            rewrite ^/lazy-static/avatars/(.*)$         /$1 break;
-          '';
-        };
-
-        locations."^~ /lazy-static/banners/" = {
-          tryFiles = "$uri @api";
-          root = cfg.settings.storage.avatars;
-          priority = 1340;
+        locations."^~ /download/" = {
+          proxyPass = "http://127.0.0.1:${toString cfg.listenHttp}";
+          priority = 1410;
           extraConfig = ''
-            if ($request_method = 'OPTIONS') {
-              ${nginxCommonHeaders}
-              add_header Access-Control-Max-Age         1728000;
-              add_header Cache-Control                  'no-cache';
-              add_header Content-Type                   'text/plain charset=UTF-8';
-              add_header Content-Length                 0;
-              return                                    204;
-            }
-
-            ${nginxCommonHeaders}
-            add_header Cache-Control                    'public, max-age=7200';
+            proxy_set_header X-Forwarded-For            $proxy_add_x_forwarded_for;
+            proxy_set_header Host                       $host;
+            proxy_set_header X-Real-IP                  $remote_addr;
 
-            rewrite ^/lazy-static/banners/(.*)$         /$1 break;
+            proxy_limit_rate                            5M;
           '';
         };
 
-        locations."^~ /lazy-static/previews/" = {
-          tryFiles = "$uri @api";
-          root = cfg.settings.storage.previews;
-          priority = 1350;
+        locations."^~ /static/streaming-playlists/private/" = {
+          proxyPass = "http://127.0.0.1:${toString cfg.listenHttp}";
+          priority = 1420;
           extraConfig = ''
-            if ($request_method = 'OPTIONS') {
-              ${nginxCommonHeaders}
-              add_header Access-Control-Max-Age         1728000;
-              add_header Cache-Control                  'no-cache';
-              add_header Content-Type                   'text/plain charset=UTF-8';
-              add_header Content-Length                 0;
-              return                                    204;
-            }
-
-            ${nginxCommonHeaders}
-            add_header Cache-Control                    'public, max-age=7200';
+            proxy_set_header X-Forwarded-For            $proxy_add_x_forwarded_for;
+            proxy_set_header Host                       $host;
+            proxy_set_header X-Real-IP                  $remote_addr;
 
-            rewrite ^/lazy-static/previews/(.*)$        /$1 break;
+            proxy_limit_rate                            5M;
           '';
         };
 
-        locations."^~ /static/streaming-playlists/private/" = {
+        locations."^~ /static/web-videos/private/" = {
           proxyPass = "http://127.0.0.1:${toString cfg.listenHttp}";
-          priority = 1410;
+          priority = 1430;
           extraConfig = ''
             proxy_set_header X-Forwarded-For            $proxy_add_x_forwarded_for;
             proxy_set_header Host                       $host;
@@ -684,7 +661,7 @@ in {
 
         locations."^~ /static/webseed/private/" = {
           proxyPass = "http://127.0.0.1:${toString cfg.listenHttp}";
-          priority = 1420;
+          priority = 1440;
           extraConfig = ''
             proxy_set_header X-Forwarded-For            $proxy_add_x_forwarded_for;
             proxy_set_header Host                       $host;
@@ -694,31 +671,45 @@ in {
           '';
         };
 
-        locations."^~ /static/thumbnails/" = {
+        locations."^~ /static/redundancy/" = {
           tryFiles = "$uri @api";
-          root = cfg.settings.storage.thumbnails;
-          priority = 1430;
+          root = cfg.settings.storage.redundancy;
+          priority = 1450;
           extraConfig = ''
+            set $peertube_limit_rate                    800k;
+
+            if ($request_uri ~ -fragmented.mp4$) {
+              set $peertube_limit_rate                  5M;
+            }
+
             if ($request_method = 'OPTIONS') {
               ${nginxCommonHeaders}
               add_header Access-Control-Max-Age         1728000;
-              add_header Cache-Control                  'no-cache';
               add_header Content-Type                   'text/plain charset=UTF-8';
               add_header Content-Length                 0;
               return                                    204;
             }
+            if ($request_method = 'GET') {
+              ${nginxCommonHeaders}
+
+              access_log                                off;
+            }
 
-            ${nginxCommonHeaders}
-            add_header Cache-Control                    'public, max-age=7200';
+            aio                                         threads;
+            sendfile                                    on;
+            sendfile_max_chunk                          1M;
+
+            limit_rate                                  $peertube_limit_rate;
+            limit_rate_after                            5M;
 
-            rewrite ^/static/thumbnails/(.*)$           /$1 break;
+            rewrite ^/static/redundancy/(.*)$           /$1 break;
           '';
         };
 
-        locations."^~ /static/redundancy/" = {
+        locations."^~ /static/streaming-playlists/" = {
           tryFiles = "$uri @api";
-          root = cfg.settings.storage.redundancy;
-          priority = 1440;
+          root = cfg.settings.storage.streaming_playlists;
+          priority = 1460;
           extraConfig = ''
             set $peertube_limit_rate                    800k;
 
@@ -746,14 +737,14 @@ in {
             limit_rate                                  $peertube_limit_rate;
             limit_rate_after                            5M;
 
-            rewrite ^/static/redundancy/(.*)$           /$1 break;
+            rewrite ^/static/streaming-playlists/(.*)$  /$1 break;
           '';
         };
 
-        locations."^~ /static/streaming-playlists/" = {
+        locations."^~ /static/web-videos/" = {
           tryFiles = "$uri @api";
           root = cfg.settings.storage.streaming_playlists;
-          priority = 1450;
+          priority = 1470;
           extraConfig = ''
             set $peertube_limit_rate                    800k;
 
@@ -788,7 +779,7 @@ in {
         locations."^~ /static/webseed/" = {
           tryFiles = "$uri @api";
           root = cfg.settings.storage.videos;
-          priority = 1460;
+          priority = 1480;
           extraConfig = ''
             set $peertube_limit_rate                    800k;
 
diff --git a/nixos/modules/services/x11/extra-layouts.nix b/nixos/modules/services/x11/extra-layouts.nix
index 3941f50b7550e..ab7e39739eeb3 100644
--- a/nixos/modules/services/x11/extra-layouts.nix
+++ b/nixos/modules/services/x11/extra-layouts.nix
@@ -3,7 +3,7 @@
 with lib;
 
 let
-  layouts = config.services.xserver.extraLayouts;
+  layouts = config.services.xserver.xkb.extraLayouts;
 
   layoutOpts = {
     options = {
@@ -15,10 +15,10 @@ let
       languages = mkOption {
         type = types.listOf types.str;
         description =
-        lib.mdDoc ''
-          A list of languages provided by the layout.
-          (Use ISO 639-2 codes, for example: "eng" for english)
-        '';
+          lib.mdDoc ''
+            A list of languages provided by the layout.
+            (Use ISO 639-2 codes, for example: "eng" for english)
+          '';
       };
 
       compatFile = mkOption {
@@ -80,29 +80,37 @@ let
   };
 
   xkb_patched = pkgs.xorg.xkeyboardconfig_custom {
-    layouts = config.services.xserver.extraLayouts;
+    layouts = config.services.xserver.xkb.extraLayouts;
   };
 
 in
 
 {
 
+  imports = [
+    (lib.mkRenamedOptionModuleWith {
+      sinceRelease = 2311;
+      from = [ "services" "xserver" "extraLayouts" ];
+      to = [ "services" "xserver" "xkb" "extraLayouts" ];
+    })
+  ];
+
   ###### interface
 
-  options.services.xserver = {
+  options.services.xserver.xkb = {
     extraLayouts = mkOption {
       type = types.attrsOf (types.submodule layoutOpts);
-      default = {};
+      default = { };
       example = literalExpression
-      ''
-        {
-          mine = {
-            description = "My custom xkb layout.";
-            languages = [ "eng" ];
-            symbolsFile = /path/to/my/layout;
-          };
-        }
-      '';
+        ''
+          {
+            mine = {
+              description = "My custom xkb layout.";
+              languages = [ "eng" ];
+              symbolsFile = /path/to/my/layout;
+            };
+          }
+        '';
       description = lib.mdDoc ''
         Extra custom layouts that will be included in the xkb configuration.
         Information on how to create a new layout can be found here:
diff --git a/nixos/modules/system/boot/stage-2-init.sh b/nixos/modules/system/boot/stage-2-init.sh
index 5a2133f960e2b..a89e3d8176374 100755
--- a/nixos/modules/system/boot/stage-2-init.sh
+++ b/nixos/modules/system/boot/stage-2-init.sh
@@ -54,7 +54,7 @@ if [ ! -e /proc/1 ]; then
 fi
 
 
-if [ "${IN_NIXOS_SYSTEMD_STAGE1:-}" = true ]; then
+if [ "${IN_NIXOS_SYSTEMD_STAGE1:-}" = true ] || [ ! -c /dev/kmsg ] ; then
     echo "booting system configuration ${systemConfig}"
 else
     echo "booting system configuration $systemConfig" > /dev/kmsg
diff --git a/nixos/modules/virtualisation/oci-containers.nix b/nixos/modules/virtualisation/oci-containers.nix
index 71f5d7a752c8c..65e97d53724f2 100644
--- a/nixos/modules/virtualisation/oci-containers.nix
+++ b/nixos/modules/virtualisation/oci-containers.nix
@@ -239,6 +239,26 @@ let
   mkService = name: container: let
     dependsOn = map (x: "${cfg.backend}-${x}.service") container.dependsOn;
     escapedName = escapeShellArg name;
+    preStartScript = pkgs.writeShellApplication {
+      name = "pre-start";
+      runtimeInputs = [ ];
+      text = ''
+        ${cfg.backend} rm -f ${name} || true
+        ${optionalString (isValidLogin container.login) ''
+          cat ${container.login.passwordFile} | \
+          ${cfg.backend} login \
+          ${container.login.registry} \
+          --username ${container.login.username} \
+          --password-stdin
+        ''}
+        ${optionalString (container.imageFile != null) ''
+          ${cfg.backend} load -i ${container.imageFile}
+        ''}
+        ${optionalString (cfg.backend == "podman") ''
+          rm -f /run/podman-${escapedName}.ctr-id
+        ''}
+      '';
+    };
   in {
     wantedBy = [] ++ optional (container.autoStart) "multi-user.target";
     after = lib.optionals (cfg.backend == "docker") [ "docker.service" "docker.socket" ]
@@ -253,23 +273,6 @@ let
       else if cfg.backend == "podman" then [ config.virtualisation.podman.package ]
       else throw "Unhandled backend: ${cfg.backend}";
 
-    preStart = ''
-      ${cfg.backend} rm -f ${name} || true
-      ${optionalString (isValidLogin container.login) ''
-        cat ${container.login.passwordFile} | \
-          ${cfg.backend} login \
-            ${container.login.registry} \
-            --username ${container.login.username} \
-            --password-stdin
-        ''}
-      ${optionalString (container.imageFile != null) ''
-        ${cfg.backend} load -i ${container.imageFile}
-        ''}
-      ${optionalString (cfg.backend == "podman") ''
-        rm -f /run/podman-${escapedName}.ctr-id
-        ''}
-      '';
-
     script = concatStringsSep " \\\n  " ([
       "exec ${cfg.backend} run"
       "--rm"
@@ -318,7 +321,7 @@ let
       ###
       # ExecReload = ...;
       ###
-
+      ExecStartPre = [ "${preStartScript}/bin/pre-start" ];
       TimeoutStartSec = 0;
       TimeoutStopSec = 120;
       Restart = "always";
diff --git a/nixos/tests/activation/nix-channel.nix b/nixos/tests/activation/nix-channel.nix
index 8416ff0347aca..d26ea98e56cc5 100644
--- a/nixos/tests/activation/nix-channel.nix
+++ b/nixos/tests/activation/nix-channel.nix
@@ -10,7 +10,17 @@
     nix.channel.enable = true;
   };
 
-  testScript = ''
-    print(machine.succeed("cat /root/.nix-channels"))
+  testScript = { nodes, ... }: ''
+    machine.start(allow_reboot=True)
+
+    assert machine.succeed("cat /root/.nix-channels") == "${nodes.machine.system.defaultChannel} nixos\n"
+
+    nixpkgs_unstable_channel = "https://nixos.org/channels/nixpkgs-unstable nixpkgs"
+    machine.succeed(f"echo '{nixpkgs_unstable_channel}' > /root/.nix-channels")
+
+    machine.reboot()
+
+    assert machine.succeed("cat /root/.nix-channels") == f"{nixpkgs_unstable_channel}\n"
   '';
+
 }
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 69549c8d5349a..6201045b54ccb 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -306,6 +306,7 @@ in {
   forgejo = handleTest ./forgejo.nix { };
   freenet = handleTest ./freenet.nix {};
   freeswitch = handleTest ./freeswitch.nix {};
+  freetube = discoverTests (import ./freetube.nix);
   freshrss-sqlite = handleTest ./freshrss-sqlite.nix {};
   freshrss-pgsql = handleTest ./freshrss-pgsql.nix {};
   frigate = handleTest ./frigate.nix {};
@@ -371,6 +372,7 @@ in {
   honk = runTest ./honk.nix;
   installed-tests = pkgs.recurseIntoAttrs (handleTest ./installed-tests {});
   invidious = handleTest ./invidious.nix {};
+  livebook-service = handleTest ./livebook-service.nix {};
   oci-containers = handleTestOn ["aarch64-linux" "x86_64-linux"] ./oci-containers.nix {};
   odoo = handleTest ./odoo.nix {};
   odoo15 = handleTest ./odoo.nix { package = pkgs.odoo15; };
@@ -572,7 +574,6 @@ in {
   nginx-njs = handleTest ./nginx-njs.nix {};
   nginx-proxyprotocol = handleTest ./nginx-proxyprotocol {};
   nginx-pubhtml = handleTest ./nginx-pubhtml.nix {};
-  nginx-sandbox = handleTestOn ["x86_64-linux"] ./nginx-sandbox.nix {};
   nginx-sso = handleTest ./nginx-sso.nix {};
   nginx-status-page = handleTest ./nginx-status-page.nix {};
   nginx-tmpdir = handleTest ./nginx-tmpdir.nix {};
@@ -684,7 +685,6 @@ in {
   predictable-interface-names = handleTest ./predictable-interface-names.nix {};
   printing-socket = handleTest ./printing.nix { socket = true; };
   printing-service = handleTest ./printing.nix { socket = false; };
-  privacyidea = handleTest ./privacyidea.nix {};
   privoxy = handleTest ./privoxy.nix {};
   prometheus = handleTest ./prometheus.nix {};
   prometheus-exporters = handleTest ./prometheus-exporters.nix {};
@@ -763,6 +763,7 @@ in {
   spark = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./spark {};
   sqlite3-to-mysql = handleTest ./sqlite3-to-mysql.nix {};
   sslh = handleTest ./sslh.nix {};
+  ssh-audit = handleTest ./ssh-audit.nix {};
   sssd = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./sssd.nix {};
   sssd-ldap = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./sssd-ldap.nix {};
   stalwart-mail = handleTest ./stalwart-mail.nix {};
diff --git a/nixos/tests/cinnamon.nix b/nixos/tests/cinnamon.nix
index 2a1389231904c..7637b55a2b124 100644
--- a/nixos/tests/cinnamon.nix
+++ b/nixos/tests/cinnamon.nix
@@ -14,27 +14,13 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
   testScript = { nodes, ... }:
     let
       user = nodes.machine.users.users.alice;
-      uid = toString user.uid;
-      bus = "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/${uid}/bus";
-      display = "DISPLAY=:0.0";
-      env = "${bus} ${display}";
-      gdbus = "${env} gdbus";
+      env = "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/${toString user.uid}/bus DISPLAY=:0";
       su = command: "su - ${user.name} -c '${env} ${command}'";
 
       # Call javascript in cinnamon (the shell), returns a tuple (success, output),
       # where `success` is true if the dbus call was successful and `output` is what
       # the javascript evaluates to.
-      eval = "call --session -d org.Cinnamon -o /org/Cinnamon -m org.Cinnamon.Eval";
-
-      # Should be 2 (RunState.RUNNING) when startup is done.
-      # https://github.com/linuxmint/cinnamon/blob/5.4.0/js/ui/main.js#L183-L187
-      getRunState = su "${gdbus} ${eval} Main.runState";
-
-      # Start gnome-terminal.
-      gnomeTerminalCommand = su "gnome-terminal";
-
-      # Hopefully gnome-terminal's wm class.
-      wmClass = su "${gdbus} ${eval} global.display.focus_window.wm_class";
+      eval = name: su "gdbus call --session -d org.Cinnamon -o /org/Cinnamon -m org.Cinnamon.Eval ${name}";
     in
     ''
       machine.wait_for_unit("display-manager.service")
@@ -54,13 +40,43 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
 
       with subtest("Wait for the Cinnamon shell"):
           # Correct output should be (true, '2')
-          machine.wait_until_succeeds("${getRunState} | grep -q 'true,..2'")
+          # https://github.com/linuxmint/cinnamon/blob/5.4.0/js/ui/main.js#L183-L187
+          machine.wait_until_succeeds("${eval "Main.runState"} | grep -q 'true,..2'")
+
+      with subtest("Check if Cinnamon components actually start"):
+          for i in ["csd-media-keys", "cinnamon-killer-daemon", "xapp-sn-watcher", "nemo-desktop"]:
+            machine.wait_until_succeeds(f"pgrep -f {i}")
+          machine.wait_until_succeeds("journalctl -b --grep 'Loaded applet menu@cinnamon.org'")
+          machine.wait_until_succeeds("journalctl -b --grep 'calendar@cinnamon.org: Calendar events supported'")
+
+      with subtest("Open Cinnamon Settings"):
+          machine.succeed("${su "cinnamon-settings themes >&2 &"}")
+          machine.wait_until_succeeds("${eval "global.display.focus_window.wm_class"} | grep -i 'cinnamon-settings'")
+          machine.wait_for_text('(Style|Appearance|Color)')
+          machine.sleep(2)
+          machine.screenshot("cinnamon_settings")
+
+      with subtest("Lock the screen"):
+          machine.succeed("${su "cinnamon-screensaver-command -l >&2 &"}")
+          machine.wait_until_succeeds("${su "cinnamon-screensaver-command -q"} | grep 'The screensaver is active'")
+          machine.sleep(2)
+          machine.screenshot("cinnamon_screensaver")
+          machine.send_chars("${user.password}\n", delay=0.2)
+          machine.wait_until_succeeds("${su "cinnamon-screensaver-command -q"} | grep 'The screensaver is inactive'")
+          machine.sleep(2)
 
       with subtest("Open GNOME Terminal"):
-          machine.succeed("${gnomeTerminalCommand}")
-          # Correct output should be (true, '"Gnome-terminal"')
-          machine.wait_until_succeeds("${wmClass} | grep -q 'true,...Gnome-terminal'")
-          machine.sleep(20)
-          machine.screenshot("screen")
+          machine.succeed("${su "gnome-terminal"}")
+          machine.wait_until_succeeds("${eval "global.display.focus_window.wm_class"} | grep -i 'gnome-terminal'")
+          machine.sleep(2)
+
+      with subtest("Open virtual keyboard"):
+          machine.succeed("${su "dbus-send --print-reply --dest=org.Cinnamon /org/Cinnamon org.Cinnamon.ToggleKeyboard"}")
+          machine.wait_for_text('(Ctrl|Alt)')
+          machine.sleep(2)
+          machine.screenshot("cinnamon_virtual_keyboard")
+
+      with subtest("Check if Cinnamon has ever coredumped"):
+          machine.fail("coredumpctl --json=short | grep -E 'cinnamon|nemo'")
     '';
 })
diff --git a/nixos/tests/freetube.nix b/nixos/tests/freetube.nix
new file mode 100644
index 0000000000000..f285384b68e0a
--- /dev/null
+++ b/nixos/tests/freetube.nix
@@ -0,0 +1,41 @@
+let
+  tests = {
+    wayland = { pkgs, ... }: {
+      imports = [ ./common/wayland-cage.nix ];
+      services.cage.program = "${pkgs.freetube}/bin/freetube";
+      virtualisation.memorySize = 2047;
+      environment.variables.NIXOS_OZONE_WL = "1";
+      environment.variables.DISPLAY = "do not use";
+    };
+    xorg = { pkgs, ... }: {
+      imports = [ ./common/user-account.nix ./common/x11.nix ];
+      virtualisation.memorySize = 2047;
+      services.xserver.enable = true;
+      services.xserver.displayManager.sessionCommands = ''
+        ${pkgs.freetube}/bin/freetube
+      '';
+      test-support.displayManager.auto.user = "alice";
+    };
+  };
+
+  mkTest = name: machine:
+    import ./make-test-python.nix ({ pkgs, ... }: {
+      inherit name;
+      nodes = { "${name}" = machine; };
+      meta.maintainers = with pkgs.lib.maintainers; [ kirillrdy ];
+      enableOCR = true;
+
+      testScript = ''
+        start_all()
+        machine.wait_for_unit('graphical.target')
+        machine.wait_for_text('Your Subscription list is currently empty')
+        machine.send_key("ctrl-r")
+        machine.wait_for_text('Your Subscription list is currently empty')
+        machine.screenshot("main.png")
+        machine.send_key("ctrl-comma")
+        machine.wait_for_text('General Settings', timeout=30)
+        machine.screenshot("preferences.png")
+      '';
+    });
+in
+builtins.mapAttrs (k: v: mkTest k v { }) tests
diff --git a/nixos/tests/keymap.nix b/nixos/tests/keymap.nix
index 0e160269304b1..e8973a50f8524 100644
--- a/nixos/tests/keymap.nix
+++ b/nixos/tests/keymap.nix
@@ -213,7 +213,7 @@ in pkgs.lib.mapAttrs mkKeyboardTest {
 
     extraConfig.console.useXkbConfig = true;
     extraConfig.services.xserver.xkb.layout = "us-greek";
-    extraConfig.services.xserver.extraLayouts.us-greek =
+    extraConfig.services.xserver.xkb.extraLayouts.us-greek =
       { description = "US layout with alt-gr greek";
         languages   = [ "eng" ];
         symbolsFile = pkgs.writeText "us-greek" ''
diff --git a/nixos/tests/livebook-service.nix b/nixos/tests/livebook-service.nix
new file mode 100644
index 0000000000000..9397e3cb75ffa
--- /dev/null
+++ b/nixos/tests/livebook-service.nix
@@ -0,0 +1,43 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }: {
+  name = "livebook-service";
+
+  nodes = {
+    machine = { config, pkgs, ... }: {
+      imports = [
+        ./common/user-account.nix
+      ];
+
+      services.livebook = {
+        enableUserService = true;
+        port = 20123;
+        environmentFile = pkgs.writeText "livebook.env" ''
+          LIVEBOOK_PASSWORD = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
+        '';
+        options = {
+          cookie = "chocolate chip";
+        };
+      };
+    };
+  };
+
+  testScript = { nodes, ... }:
+    let
+      user = nodes.machine.config.users.users.alice;
+      sudo = lib.concatStringsSep " " [
+        "XDG_RUNTIME_DIR=/run/user/${toString user.uid}"
+        "sudo"
+        "--preserve-env=XDG_RUNTIME_DIR"
+        "-u"
+        "alice"
+      ];
+    in
+    ''
+      machine.wait_for_unit("multi-user.target")
+
+      machine.succeed("loginctl enable-linger alice")
+      machine.wait_until_succeeds("${sudo} systemctl --user is-active livebook.service")
+      machine.wait_for_open_port(20123)
+
+      machine.succeed("curl -L localhost:20123 | grep 'Type password'")
+    '';
+})
diff --git a/nixos/tests/nginx-sandbox.nix b/nixos/tests/nginx-sandbox.nix
deleted file mode 100644
index 92ba30a09cf9f..0000000000000
--- a/nixos/tests/nginx-sandbox.nix
+++ /dev/null
@@ -1,65 +0,0 @@
-import ./make-test-python.nix ({ pkgs, ... }: {
-  name = "nginx-sandbox";
-  meta = with pkgs.lib.maintainers; {
-    maintainers = [ izorkin ];
-  };
-
-  # This test checks the creation and reading of a file in sandbox mode. Used simple lua script.
-
-  nodes.machine = { pkgs, ... }: {
-    nixpkgs.overlays = [
-      (self: super: {
-        nginx-lua = super.nginx.override {
-          modules = [
-            pkgs.nginxModules.lua
-          ];
-        };
-      })
-    ];
-    services.nginx.enable = true;
-    services.nginx.package = pkgs.nginx-lua;
-    services.nginx.virtualHosts.localhost = {
-      extraConfig = ''
-        location /test1-write {
-          content_by_lua_block {
-            local create = os.execute('${pkgs.coreutils}/bin/mkdir /tmp/test1-read')
-            local create = os.execute('${pkgs.coreutils}/bin/touch /tmp/test1-read/foo.txt')
-            local echo = os.execute('${pkgs.coreutils}/bin/echo worked > /tmp/test1-read/foo.txt')
-          }
-        }
-        location /test1-read {
-          root /tmp;
-        }
-        location /test2-write {
-          content_by_lua_block {
-            local create = os.execute('${pkgs.coreutils}/bin/mkdir /var/web/test2-read')
-            local create = os.execute('${pkgs.coreutils}/bin/touch /var/web/test2-read/bar.txt')
-            local echo = os.execute('${pkgs.coreutils}/bin/echo error-worked > /var/web/test2-read/bar.txt')
-          }
-        }
-        location /test2-read {
-          root /var/web;
-        }
-      '';
-    };
-    users.users.foo.isNormalUser = true;
-  };
-
-  testScript = ''
-    machine.wait_for_unit("nginx")
-    machine.wait_for_open_port(80)
-
-    # Checking write in temporary folder
-    machine.succeed("$(curl -vvv http://localhost/test1-write)")
-    machine.succeed('test "$(curl -fvvv http://localhost/test1-read/foo.txt)" = worked')
-
-    # Checking write in protected folder. In sandbox mode for the nginx service, the folder /var/web is mounted
-    # in read-only mode.
-    machine.succeed("mkdir -p /var/web")
-    machine.succeed("chown nginx:nginx /var/web")
-    machine.succeed("$(curl -vvv http://localhost/test2-write)")
-    assert "404 Not Found" in machine.succeed(
-        "curl -vvv -s http://localhost/test2-read/bar.txt"
-    )
-  '';
-})
diff --git a/nixos/tests/openresty-lua.nix b/nixos/tests/openresty-lua.nix
index b177b3c194d78..9e987398f51d7 100644
--- a/nixos/tests/openresty-lua.nix
+++ b/nixos/tests/openresty-lua.nix
@@ -16,6 +16,12 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
 
     nodes = {
       webserver = { pkgs, lib, ... }: {
+        networking = {
+          extraHosts = ''
+            127.0.0.1 default.test
+            127.0.0.1 sandbox.test
+          '';
+        };
         services.nginx = {
           enable = true;
           package = pkgs.openresty;
@@ -24,7 +30,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
             lua_package_path '${luaPath};;';
           '';
 
-          virtualHosts."default" = {
+          virtualHosts."default.test" = {
             default = true;
             locations."/" = {
               extraConfig = ''
@@ -36,6 +42,33 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
               '';
             };
           };
+
+          virtualHosts."sandbox.test" = {
+            locations."/test1-write" = {
+              extraConfig = ''
+                content_by_lua_block {
+                  local create = os.execute('${pkgs.coreutils}/bin/mkdir /tmp/test1-read')
+                  local create = os.execute('${pkgs.coreutils}/bin/touch /tmp/test1-read/foo.txt')
+                  local echo = os.execute('${pkgs.coreutils}/bin/echo worked > /tmp/test1-read/foo.txt')
+                }
+              '';
+            };
+            locations."/test1-read" = {
+              root = "/tmp";
+            };
+            locations."/test2-write" = {
+              extraConfig = ''
+                content_by_lua_block {
+                  local create = os.execute('${pkgs.coreutils}/bin/mkdir /var/web/test2-read')
+                  local create = os.execute('${pkgs.coreutils}/bin/touch /var/web/test2-read/bar.txt')
+                  local echo = os.execute('${pkgs.coreutils}/bin/echo error-worked > /var/web/test2-read/bar.txt')
+                }
+              '';
+            };
+            locations."/test2-read" = {
+              root = "/var/web";
+            };
+          };
         };
       };
     };
@@ -51,5 +84,18 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
           f"curl -w '%{{http_code}}' --head --fail {url}"
         )
         assert http_code.split("\n")[-1] == "200"
+
+        # This test checks the creation and reading of a file in sandbox mode.
+        # Checking write in temporary folder
+        webserver.succeed("$(curl -vvv http://sandbox.test/test1-write)")
+        webserver.succeed('test "$(curl -fvvv http://sandbox.test/test1-read/foo.txt)" = worked')
+        # Checking write in protected folder. In sandbox mode for the nginx service, the folder /var/web is mounted
+        # in read-only mode.
+        webserver.succeed("mkdir -p /var/web")
+        webserver.succeed("chown nginx:nginx /var/web")
+        webserver.succeed("$(curl -vvv http://sandbox.test/test2-write)")
+        assert "404 Not Found" in machine.succeed(
+            "curl -vvv -s http://sandbox.test/test2-read/bar.txt"
+        )
       '';
   })
diff --git a/nixos/tests/postgresql.nix b/nixos/tests/postgresql.nix
index b44849e0a14e5..c0dd24cf6ad2e 100644
--- a/nixos/tests/postgresql.nix
+++ b/nixos/tests/postgresql.nix
@@ -219,8 +219,6 @@ let
 in
   concatMapAttrs (name: package: {
     ${name} = make-postgresql-test name package false;
+    ${name + "-backup-all"} = make-postgresql-test "${name + "-backup-all"}" package true;
     ${name + "-clauses"} = mk-ensure-clauses-test name package;
   }) postgresql-versions
-  // {
-    postgresql_11-backup-all = make-postgresql-test "postgresql_11-backup-all" postgresql-versions.postgresql_11 true;
-  }
diff --git a/nixos/tests/predictable-interface-names.nix b/nixos/tests/predictable-interface-names.nix
index 42183625c7c93..51d5e8ae59b92 100644
--- a/nixos/tests/predictable-interface-names.nix
+++ b/nixos/tests/predictable-interface-names.nix
@@ -36,7 +36,7 @@ in pkgs.lib.listToAttrs (builtins.map ({ predictable, withNetworkd, systemdStage
       networking.useDHCP = !withNetworkd;
 
       # Check if predictable interface names are working in stage-1
-      boot.initrd.postDeviceCommands = script;
+      boot.initrd.postDeviceCommands = lib.mkIf (!systemdStage1) script;
 
       boot.initrd.systemd = lib.mkIf systemdStage1 {
         enable = true;
diff --git a/nixos/tests/privacyidea.nix b/nixos/tests/privacyidea.nix
deleted file mode 100644
index 401ad72c37b72..0000000000000
--- a/nixos/tests/privacyidea.nix
+++ /dev/null
@@ -1,43 +0,0 @@
-# Miscellaneous small tests that don't warrant their own VM run.
-
-import ./make-test-python.nix ({ pkgs, ...} : rec {
-  name = "privacyidea";
-  meta = with pkgs.lib.maintainers; {
-    maintainers = [ ];
-  };
-
-  nodes.machine = { ... }: {
-    virtualisation.cores = 2;
-
-    services.privacyidea = {
-      enable = true;
-      secretKey = "$SECRET_KEY";
-      pepper = "$PEPPER";
-      adminPasswordFile = pkgs.writeText "admin-password" "testing";
-      adminEmail = "root@localhost";
-
-      # Don't try this at home!
-      environmentFile = pkgs.writeText "pi-secrets.env" ''
-        SECRET_KEY=testing
-        PEPPER=testing
-      '';
-    };
-    services.nginx = {
-      enable = true;
-      virtualHosts."_".locations."/".extraConfig = ''
-        uwsgi_pass unix:/run/privacyidea/socket;
-      '';
-    };
-  };
-
-  testScript = ''
-    machine.start()
-    machine.wait_for_unit("multi-user.target")
-    machine.succeed("curl --fail http://localhost | grep privacyIDEA")
-    machine.succeed("grep \"SECRET_KEY = 'testing'\" /var/lib/privacyidea/privacyidea.cfg")
-    machine.succeed("grep \"PI_PEPPER = 'testing'\" /var/lib/privacyidea/privacyidea.cfg")
-    machine.succeed(
-        "curl --fail http://localhost/auth -F username=admin -F password=testing | grep token"
-    )
-  '';
-})
diff --git a/nixos/tests/ssh-audit.nix b/nixos/tests/ssh-audit.nix
new file mode 100644
index 0000000000000..bd6255b8044d9
--- /dev/null
+++ b/nixos/tests/ssh-audit.nix
@@ -0,0 +1,103 @@
+import ./make-test-python.nix (
+  {pkgs, ...}: let
+    sshKeys = import (pkgs.path + "/nixos/tests/ssh-keys.nix") pkgs;
+    sshUsername = "any-user";
+    serverName = "server";
+    clientName = "client";
+    sshAuditPort = 2222;
+  in {
+    name = "ssh";
+
+    nodes = {
+      "${serverName}" = {
+        networking.firewall.allowedTCPPorts = [
+          sshAuditPort
+        ];
+        services.openssh.enable = true;
+        users.users."${sshUsername}" = {
+          isNormalUser = true;
+          openssh.authorizedKeys.keys = [
+            sshKeys.snakeOilPublicKey
+          ];
+        };
+      };
+      "${clientName}" = {
+        programs.ssh = {
+          ciphers = [
+            "aes128-ctr"
+            "aes128-gcm@openssh.com"
+            "aes192-ctr"
+            "aes256-ctr"
+            "aes256-gcm@openssh.com"
+            "chacha20-poly1305@openssh.com"
+          ];
+          extraConfig = ''
+            IdentitiesOnly yes
+          '';
+          hostKeyAlgorithms = [
+            "rsa-sha2-256"
+            "rsa-sha2-256-cert-v01@openssh.com"
+            "rsa-sha2-512"
+            "rsa-sha2-512-cert-v01@openssh.com"
+            "sk-ssh-ed25519-cert-v01@openssh.com"
+            "sk-ssh-ed25519@openssh.com"
+            "ssh-ed25519"
+            "ssh-ed25519-cert-v01@openssh.com"
+          ];
+          kexAlgorithms = [
+            "curve25519-sha256"
+            "curve25519-sha256@libssh.org"
+            "diffie-hellman-group-exchange-sha256"
+            "diffie-hellman-group16-sha512"
+            "diffie-hellman-group18-sha512"
+            "sntrup761x25519-sha512@openssh.com"
+          ];
+          macs = [
+            "hmac-sha2-256-etm@openssh.com"
+            "hmac-sha2-512-etm@openssh.com"
+            "umac-128-etm@openssh.com"
+          ];
+        };
+      };
+    };
+
+    testScript = ''
+      start_all()
+
+      ${serverName}.wait_for_open_port(22)
+
+      # Should pass SSH server audit
+      ${serverName}.succeed("${pkgs.ssh-audit}/bin/ssh-audit 127.0.0.1")
+
+      # Wait for client to be able to connect to the server
+      ${clientName}.wait_for_unit("network-online.target")
+
+      # Set up trusted private key
+      ${clientName}.succeed("cat ${sshKeys.snakeOilPrivateKey} > privkey.snakeoil")
+      ${clientName}.succeed("chmod 600 privkey.snakeoil")
+
+      # Fail fast and disable interactivity
+      ssh_options = "-o BatchMode=yes -o ConnectTimeout=1 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
+
+      # Should deny root user
+      ${clientName}.fail(f"ssh {ssh_options} root@${serverName} true")
+
+      # Should deny non-root user password login
+      ${clientName}.fail(f"ssh {ssh_options} -o PasswordAuthentication=yes ${sshUsername}@${serverName} true")
+
+      # Should allow non-root user certificate login
+      ${clientName}.succeed(f"ssh {ssh_options} -i privkey.snakeoil ${sshUsername}@${serverName} true")
+
+      # Should pass SSH client audit
+      service_name = "ssh-audit.service"
+      ${serverName}.succeed(f"systemd-run --unit={service_name} ${pkgs.ssh-audit}/bin/ssh-audit --client-audit --port=${toString sshAuditPort}")
+      ${clientName}.sleep(5) # We can't use wait_for_open_port because ssh-audit exits as soon as anything talks to it
+      ${clientName}.execute(
+          f"ssh {ssh_options} -i privkey.snakeoil -p ${toString sshAuditPort} ${sshUsername}@${serverName} true",
+          check_return=False,
+          timeout=10
+      )
+      ${serverName}.succeed(f"exit $(systemctl show --property=ExecMainStatus --value {service_name})")
+    '';
+  }
+)