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/installation/installing-from-other-distro.section.md4
-rw-r--r--nixos/doc/manual/installation/installing-virtualbox-guest.section.md4
-rw-r--r--nixos/doc/manual/installation/upgrading.chapter.md4
-rw-r--r--nixos/doc/manual/release-notes/rl-2405.section.md12
-rw-r--r--nixos/lib/testing/driver.nix2
-rw-r--r--nixos/modules/module-list.nix3
-rw-r--r--nixos/modules/programs/fish.nix12
-rw-r--r--nixos/modules/programs/ydotool.nix83
-rw-r--r--nixos/modules/programs/zsh/zsh-syntax-highlighting.nix2
-rw-r--r--nixos/modules/security/systemd-confinement.nix36
-rw-r--r--nixos/modules/services/backup/borgbackup.nix32
-rw-r--r--nixos/modules/services/display-managers/default.nix4
-rw-r--r--nixos/modules/services/display-managers/greetd.nix14
-rw-r--r--nixos/modules/services/hardware/thermald.nix4
-rw-r--r--nixos/modules/services/mail/stalwart-mail.nix1
-rw-r--r--nixos/modules/services/misc/bcg.nix6
-rw-r--r--nixos/modules/services/misc/devpi-server.nix128
-rw-r--r--nixos/modules/services/misc/tzupdate.nix2
-rw-r--r--nixos/modules/services/monitoring/arbtt.nix2
-rw-r--r--nixos/modules/services/networking/hostapd.nix17
-rw-r--r--nixos/modules/services/networking/smokeping.nix63
-rw-r--r--nixos/modules/services/networking/vsftpd.nix2
-rw-r--r--nixos/modules/services/security/oauth2-proxy-nginx.nix6
-rw-r--r--nixos/modules/services/web-apps/miniflux.nix15
-rw-r--r--nixos/modules/services/web-apps/your_spotify.nix191
-rw-r--r--nixos/modules/services/web-servers/caddy/default.nix2
-rw-r--r--nixos/modules/services/web-servers/garage.nix45
-rw-r--r--nixos/modules/services/x11/xserver.nix3
-rw-r--r--nixos/modules/system/activation/switchable-system.nix109
-rw-r--r--nixos/modules/system/boot/binfmt.nix2
-rw-r--r--nixos/tests/all-tests.nix8
-rw-r--r--nixos/tests/devpi-server.nix35
-rw-r--r--nixos/tests/fcitx5/default.nix3
-rw-r--r--nixos/tests/garage/default.nix1
-rw-r--r--nixos/tests/garage/with-3node-replication.nix8
-rw-r--r--nixos/tests/pgvecto-rs.nix2
-rw-r--r--nixos/tests/smokeping.nix14
-rw-r--r--nixos/tests/stalwart-mail.nix12
-rw-r--r--nixos/tests/switch-test.nix7
-rw-r--r--nixos/tests/systemd-confinement.nix184
-rw-r--r--nixos/tests/systemd-confinement/checkperms.py187
-rw-r--r--nixos/tests/systemd-confinement/default.nix274
-rw-r--r--nixos/tests/systemd-initrd-modprobe.nix12
-rw-r--r--nixos/tests/virtualbox.nix1
-rw-r--r--nixos/tests/ydotool.nix115
-rw-r--r--nixos/tests/your_spotify.nix33
46 files changed, 1362 insertions, 344 deletions
diff --git a/nixos/doc/manual/installation/installing-from-other-distro.section.md b/nixos/doc/manual/installation/installing-from-other-distro.section.md
index 10ac2be4e161f..38f0e5301472b 100644
--- a/nixos/doc/manual/installation/installing-from-other-distro.section.md
+++ b/nixos/doc/manual/installation/installing-from-other-distro.section.md
@@ -42,9 +42,11 @@ The first steps to all these are the same:
     will be safer to use the `nixos-*` channels instead:
 
     ```ShellSession
-    $ nix-channel --add https://nixos.org/channels/nixos-version nixpkgs
+    $ nix-channel --add https://nixos.org/channels/nixos-<version> nixpkgs
     ```
 
+    Where `<version>` corresponds to the latest version available on [channels.nixos.org](https://channels.nixos.org/).
+
     You may want to throw in a `nix-channel --update` for good measure.
 
 1.  Install the NixOS installation tools:
diff --git a/nixos/doc/manual/installation/installing-virtualbox-guest.section.md b/nixos/doc/manual/installation/installing-virtualbox-guest.section.md
index 4b9ae0a9c55f0..415119bd8c898 100644
--- a/nixos/doc/manual/installation/installing-virtualbox-guest.section.md
+++ b/nixos/doc/manual/installation/installing-virtualbox-guest.section.md
@@ -3,8 +3,8 @@
 Installing NixOS into a VirtualBox guest is convenient for users who
 want to try NixOS without installing it on bare metal. If you want to
 use a pre-made VirtualBox appliance, it is available at [the downloads
-page](https://nixos.org/nixos/download.html). If you want to set up a
-VirtualBox guest manually, follow these instructions:
+page](https://nixos.org/download/#nixos-virtualbox). If you want to set
+up a VirtualBox guest manually, follow these instructions:
 
 1.  Add a New Machine in VirtualBox with OS Type "Linux / Other Linux"
 
diff --git a/nixos/doc/manual/installation/upgrading.chapter.md b/nixos/doc/manual/installation/upgrading.chapter.md
index 09338bf8723d2..11fc65502f953 100644
--- a/nixos/doc/manual/installation/upgrading.chapter.md
+++ b/nixos/doc/manual/installation/upgrading.chapter.md
@@ -33,8 +33,8 @@ To see what channels are available, go to <https://channels.nixos.org>.
 contains the channel's latest version and includes ISO images and
 VirtualBox appliances.) Please note that during the release process,
 channels that are not yet released will be present here as well. See the
-Getting NixOS page <https://nixos.org/nixos/download.html> to find the
-newest supported stable release.
+Getting NixOS page <https://nixos.org/download/> to find the newest
+supported stable release.
 
 When you first install NixOS, you're automatically subscribed to the
 NixOS channel that corresponds to your installation source. For
diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md
index a43e8f26cabe7..d6b8ffe8609fa 100644
--- a/nixos/doc/manual/release-notes/rl-2405.section.md
+++ b/nixos/doc/manual/release-notes/rl-2405.section.md
@@ -187,6 +187,8 @@ The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been m
 
 - [xdg-terminal-exec](https://github.com/Vladimir-csp/xdg-terminal-exec), the proposed Default Terminal Execution Specification.
 
+- [your_spotify](https://github.com/Yooooomi/your_spotify), a self hosted Spotify tracking dashboard. Available as [services.your_spotify](#opt-services.your_spotify.enable)
+
 - [RustDesk](https://rustdesk.com), a full-featured open source remote control alternative for self-hosting and security with minimal configuration. Alternative to TeamViewer. Available as [services.rustdesk-server](#opt-services.rustdesk-server.enable).
 
 - [Scrutiny](https://github.com/AnalogJ/scrutiny), a S.M.A.R.T monitoring tool for hard disks with a web frontend. Available as [services.scrutiny](#opt-services.scrutiny.enable).
@@ -209,8 +211,12 @@ The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been m
 
 - [isolate](https://github.com/ioi/isolate), a sandbox for securely executing untrusted programs. Available as [security.isolate](#opt-security.isolate.enable).
 
+- [ydotool](https://github.com/ReimuNotMoe/ydotool), a generic command-line automation tool now has a module. Available as [programs.ydotool](#opt-programs.ydotool.enable).
+
 - [private-gpt](https://github.com/zylon-ai/private-gpt), a service to interact with your documents using the power of LLMs, 100% privately, no data leaks. Available as [services.private-gpt](#opt-services.private-gpt.enable).
 
+- [keto](https://www.ory.sh/keto/), a permission & access control server, the first open source implementation of ["Zanzibar: Google's Consistent, Global Authorization System"](https://research.google/pubs/zanzibar-googles-consistent-global-authorization-system/).
+
 ## Backward Incompatibilities {#sec-release-24.05-incompatibilities}
 
 <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
@@ -402,6 +408,10 @@ The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been m
 
 - `halloy` package was updated past 2024.5 which introduced a breaking change by switching the config format from YAML to TOML. See https://github.com/squidowl/halloy/releases/tag/2024.5 for details.
 
+- If `services.smokeping.webService` was enabled, smokeping is now served via nginx instead of thttpd. This change brings the following consequences:
+  - The default port for smokeping is now the nginx default port 80 instead of 8081.
+  - The option `services.smokeping.port` has been removed. To customize the port, use `services.nginx.virtualHosts.smokeping.listen.*.port`.
+
 - The `wpaperd` package has a breaking change moving to 1.0.1, previous version 0.3.0 had 2 different configuration files, one for wpaperd and one for the wallpapers. Remove the former and move the latter (`wallpaper.toml`) to `config.toml`.
 
 - Ada packages (libraries and tools) have been moved into the `gnatPackages` scope. `gnatPackages` uses the default GNAT compiler, `gnat12Packages` and `gnat13Packages` use the respective matching compiler version.
@@ -713,6 +723,8 @@ The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been m
 - `documentation.man.mandoc` now by default uses `MANPATH` to set the directories where mandoc will search for manual pages.
   This enables mandoc to find manual pages in Nix profiles. To set the manual search paths via the `mandoc.conf` configuration file like before, use `documentation.man.mandoc.settings.manpath` instead.
 
+- The `systemd-confinement` module extension is now compatible with `DynamicUser=true` and thus `ProtectSystem=strict` too.
+
 - `grafana-loki` package was updated to 3.0.0 which includes [breaking changes](https://github.com/grafana/loki/releases/tag/v3.0.0).
 
 - `programs.fish.package` now allows you to override the package used in the `fish` module.
diff --git a/nixos/lib/testing/driver.nix b/nixos/lib/testing/driver.nix
index 7eb06e023918b..d4f8e0f0c6e38 100644
--- a/nixos/lib/testing/driver.nix
+++ b/nixos/lib/testing/driver.nix
@@ -139,7 +139,7 @@ in
     enableOCR = mkOption {
       description = ''
         Whether to enable Optical Character Recognition functionality for
-        testing graphical programs. See [Machine objects](`ssec-machine-objects`).
+        testing graphical programs. See [`Machine objects`](#ssec-machine-objects).
       '';
       type = types.bool;
       default = false;
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 3cbb4617517aa..b14b83a8119ac 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -308,6 +308,7 @@
   ./programs/xwayland.nix
   ./programs/yabar.nix
   ./programs/yazi.nix
+  ./programs/ydotool.nix
   ./programs/yubikey-touch-detector.nix
   ./programs/zmap.nix
   ./programs/zsh/oh-my-zsh.nix
@@ -699,6 +700,7 @@
   ./services/misc/cpuminer-cryptonight.nix
   ./services/misc/db-rest.nix
   ./services/misc/devmon.nix
+  ./services/misc/devpi-server.nix
   ./services/misc/dictd.nix
   ./services/misc/disnix.nix
   ./services/misc/docker-registry.nix
@@ -1429,6 +1431,7 @@
   ./services/web-apps/windmill.nix
   ./services/web-apps/wordpress.nix
   ./services/web-apps/writefreely.nix
+  ./services/web-apps/your_spotify.nix
   ./services/web-apps/youtrack.nix
   ./services/web-apps/zabbix.nix
   ./services/web-apps/zitadel.nix
diff --git a/nixos/modules/programs/fish.nix b/nixos/modules/programs/fish.nix
index 3a927300dc4c6..5a6fdb9b5ec5a 100644
--- a/nixos/modules/programs/fish.nix
+++ b/nixos/modules/programs/fish.nix
@@ -6,14 +6,14 @@ let
 
   cfg = config.programs.fish;
 
-  fishAbbrs = builtins.concatStringsSep "\n" (
-    lib.mapAttrsFlatten (k: v: "abbr -ag ${k} ${builtins.escapeShellArg v}")
+  fishAbbrs = lib.concatStringsSep "\n" (
+    lib.mapAttrsFlatten (k: v: "abbr -ag ${k} ${lib.escapeShellArg v}")
       cfg.shellAbbrs
   );
 
-  fishAliases = builtins.concatStringsSep "\n" (
-    builtins.mapAttrsFlatten (k: v: "alias ${k} ${builtins.escapeShellArg v}")
-      (builtins.filterAttrs (k: v: v != null) cfg.shellAliases)
+  fishAliases = lib.concatStringsSep "\n" (
+    lib.mapAttrsFlatten (k: v: "alias ${k} ${lib.escapeShellArg v}")
+      (lib.filterAttrs (k: v: v != null) cfg.shellAliases)
   );
 
   envShellInit = pkgs.writeText "shellInit" cfge.shellInit;
@@ -147,7 +147,7 @@ in
 
   config = lib.mkIf cfg.enable {
 
-    programs.fish.shellAliases = builtins.mapAttrs (name: lib.mkDefault) cfge.shellAliases;
+    programs.fish.shellAliases = lib.mapAttrs (name: lib.mkDefault) cfge.shellAliases;
 
     # Required for man completions
     documentation.man.generateCaches = lib.mkDefault true;
diff --git a/nixos/modules/programs/ydotool.nix b/nixos/modules/programs/ydotool.nix
new file mode 100644
index 0000000000000..f639e9283de42
--- /dev/null
+++ b/nixos/modules/programs/ydotool.nix
@@ -0,0 +1,83 @@
+{
+  config,
+  lib,
+  pkgs,
+  ...
+}:
+let
+  cfg = config.programs.ydotool;
+in
+{
+  meta = {
+    maintainers = with lib.maintainers; [ quantenzitrone ];
+  };
+
+  options.programs.ydotool = {
+    enable = lib.mkEnableOption ''
+      ydotoold system service and install ydotool.
+      Add yourself to the 'ydotool' group to be able to use it.
+    '';
+  };
+
+  config = lib.mkIf cfg.enable {
+    users.groups.ydotool = { };
+
+    systemd.services.ydotoold = {
+      description = "ydotoold - backend for ydotool";
+      wantedBy = [ "multi-user.target" ];
+      partOf = [ "multi-user.target" ];
+      serviceConfig = {
+        Group = "ydotool";
+        RuntimeDirectory = "ydotoold";
+        RuntimeDirectoryMode = "0750";
+        ExecStart = "${lib.getExe' pkgs.ydotool "ydotoold"} --socket-path=/run/ydotoold/socket --socket-perm=0660";
+
+        # hardening
+
+        ## allow access to uinput
+        DeviceAllow = [ "/dev/uinput" ];
+        DevicePolicy = "closed";
+
+        ## allow creation of unix sockets
+        RestrictAddressFamilies = [ "AF_UNIX" ];
+
+        CapabilityBoundingSet = "";
+        IPAddressDeny = "any";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateNetwork = true;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProcSubset = "pid";
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProtectSystem = "strict";
+        ProtectUser = true;
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [
+          "@system-service"
+          "~@privileged"
+          "~@resources"
+        ];
+        UMask = "0077";
+
+        # -> systemd-analyze security score 0.7 SAFE 😀
+      };
+    };
+
+    environment.variables = {
+      YDOTOOL_SOCKET = "/run/ydotoold/socket";
+    };
+    environment.systemPackages = with pkgs; [ ydotool ];
+  };
+}
diff --git a/nixos/modules/programs/zsh/zsh-syntax-highlighting.nix b/nixos/modules/programs/zsh/zsh-syntax-highlighting.nix
index e6036fc39d3e5..3f70c14048c75 100644
--- a/nixos/modules/programs/zsh/zsh-syntax-highlighting.nix
+++ b/nixos/modules/programs/zsh/zsh-syntax-highlighting.nix
@@ -87,7 +87,7 @@ in
     ];
 
     programs.zsh.interactiveShellInit =
-      lib.lib.mkAfter (lib.concatStringsSep "\n" ([
+      lib.mkAfter (lib.concatStringsSep "\n" ([
         "source ${pkgs.zsh-syntax-highlighting}/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh"
       ] ++ lib.optional (builtins.length(cfg.highlighters) > 0)
         "ZSH_HIGHLIGHT_HIGHLIGHTERS=(${builtins.concatStringsSep " " cfg.highlighters})"
diff --git a/nixos/modules/security/systemd-confinement.nix b/nixos/modules/security/systemd-confinement.nix
index 0304749b8d109..041c900338864 100644
--- a/nixos/modules/security/systemd-confinement.nix
+++ b/nixos/modules/security/systemd-confinement.nix
@@ -79,13 +79,20 @@ in {
         description = ''
           The value `full-apivfs` (the default) sets up
           private {file}`/dev`, {file}`/proc`,
-          {file}`/sys` and {file}`/tmp` file systems in a separate user
-          name space.
+          {file}`/sys`, {file}`/tmp` and {file}`/var/tmp` file systems
+          in a separate user name space.
 
           If this is set to `chroot-only`, only the file
           system name space is set up along with the call to
           {manpage}`chroot(2)`.
 
+          In all cases, unless `serviceConfig.PrivateTmp=true` is set,
+          both {file}`/tmp` and {file}`/var/tmp` paths are added to `InaccessiblePaths=`.
+          This is to overcome options like `DynamicUser=true`
+          implying `PrivateTmp=true` without letting it being turned off.
+          Beware however that giving processes the `CAP_SYS_ADMIN` and `@mount` privileges
+          can let them undo the effects of `InaccessiblePaths=`.
+
           ::: {.note}
           This doesn't cover network namespaces and is solely for
           file system level isolation.
@@ -98,8 +105,12 @@ in {
         wantsAPIVFS = lib.mkDefault (config.confinement.mode == "full-apivfs");
       in lib.mkIf config.confinement.enable {
         serviceConfig = {
-          RootDirectory = "/var/empty";
-          TemporaryFileSystem = "/";
+          ReadOnlyPaths = [ "+/" ];
+          RuntimeDirectory = [ "confinement/${mkPathSafeName name}" ];
+          RootDirectory = "/run/confinement/${mkPathSafeName name}";
+          InaccessiblePaths = [
+            "-+/run/confinement/${mkPathSafeName name}"
+          ];
           PrivateMounts = lib.mkDefault true;
 
           # https://github.com/NixOS/nixpkgs/issues/14645 is a future attempt
@@ -148,16 +159,6 @@ in {
               + " Please either define a separate service or find a way to run"
               + " commands other than ExecStart within the chroot.";
     }
-    { assertion = !cfg.serviceConfig.DynamicUser or false;
-      message = "${whatOpt "DynamicUser"}. Please create a dedicated user via"
-              + " the 'users.users' option instead as this combination is"
-              + " currently not supported.";
-    }
-    { assertion = cfg.serviceConfig ? ProtectSystem -> cfg.serviceConfig.ProtectSystem == false;
-      message = "${whatOpt "ProtectSystem"}. ProtectSystem is not compatible"
-              + " with service confinement as it fails to remount /usr within"
-              + " our chroot. Please disable the option.";
-    }
   ]) config.systemd.services);
 
   config.systemd.packages = lib.concatLists (lib.mapAttrsToList (name: cfg: let
@@ -183,6 +184,13 @@ in {
         echo "BindReadOnlyPaths=$realprog:/bin/sh" >> "$serviceFile"
       ''}
 
+      # If DynamicUser= is enabled, PrivateTmp=true is implied (and cannot be turned off).
+      # so disable them unless PrivateTmp=true is explicitely set.
+      ${lib.optionalString (!cfg.serviceConfig.PrivateTmp) ''
+        echo "InaccessiblePaths=-+/tmp" >> "$serviceFile"
+        echo "InaccessiblePaths=-+/var/tmp" >> "$serviceFile"
+      ''}
+
       while read storePath; do
         if [ -L "$storePath" ]; then
           # Currently, systemd can't cope with symlinks in Bind(ReadOnly)Paths,
diff --git a/nixos/modules/services/backup/borgbackup.nix b/nixos/modules/services/backup/borgbackup.nix
index 570f8931bd9e9..04f971008073e 100644
--- a/nixos/modules/services/backup/borgbackup.nix
+++ b/nixos/modules/services/backup/borgbackup.nix
@@ -33,13 +33,24 @@ let
     }
     trap on_exit EXIT
 
+    borgWrapper () {
+      local result
+      borg "$@" && result=$? || result=$?
+      if [[ -z "${toString cfg.failOnWarnings}" ]] && [[ "$result" == 1 ]]; then
+        echo "ignoring warning return value 1"
+        return 0
+      else
+        return "$result"
+      fi
+    }
+
     archiveName="${optionalString (cfg.archiveBaseName != null) (cfg.archiveBaseName + "-")}$(date ${cfg.dateFormat})"
     archiveSuffix="${optionalString cfg.appendFailedSuffix ".failed"}"
     ${cfg.preHook}
   '' + optionalString cfg.doInit ''
     # Run borg init if the repo doesn't exist yet
-    if ! borg list $extraArgs > /dev/null; then
-      borg init $extraArgs \
+    if ! borgWrapper list $extraArgs > /dev/null; then
+      borgWrapper init $extraArgs \
         --encryption ${cfg.encryption.mode} \
         $extraInitArgs
       ${cfg.postInit}
@@ -48,7 +59,7 @@ let
     (
       set -o pipefail
       ${optionalString (cfg.dumpCommand != null) ''${escapeShellArg cfg.dumpCommand} | \''}
-      borg create $extraArgs \
+      borgWrapper create $extraArgs \
         --compression ${cfg.compression} \
         --exclude-from ${mkExcludeFile cfg} \
         --patterns-from ${mkPatternsFile cfg} \
@@ -57,16 +68,16 @@ let
         ${if cfg.paths == null then "-" else escapeShellArgs cfg.paths}
     )
   '' + optionalString cfg.appendFailedSuffix ''
-    borg rename $extraArgs \
+    borgWrapper rename $extraArgs \
       "::$archiveName$archiveSuffix" "$archiveName"
   '' + ''
     ${cfg.postCreate}
   '' + optionalString (cfg.prune.keep != { }) ''
-    borg prune $extraArgs \
+    borgWrapper prune $extraArgs \
       ${mkKeepArgs cfg} \
       ${optionalString (cfg.prune.prefix != null) "--glob-archives ${escapeShellArg "${cfg.prune.prefix}*"}"} \
       $extraPruneArgs
-    borg compact $extraArgs $extraCompactArgs
+    borgWrapper compact $extraArgs $extraCompactArgs
     ${cfg.postPrune}
   '');
 
@@ -488,6 +499,15 @@ in {
             default = true;
           };
 
+          failOnWarnings = mkOption {
+            type = types.bool;
+            description = ''
+              Fail the whole backup job if any borg command returns a warning
+              (exit code 1), for example because a file changed during backup.
+            '';
+            default = true;
+          };
+
           doInit = mkOption {
             type = types.bool;
             description = ''
diff --git a/nixos/modules/services/display-managers/default.nix b/nixos/modules/services/display-managers/default.nix
index 6fa8556e39bee..feba4b163ccd2 100644
--- a/nixos/modules/services/display-managers/default.nix
+++ b/nixos/modules/services/display-managers/default.nix
@@ -113,7 +113,7 @@ in
         type = lib.types.nullOr lib.types.str // {
           description = "session name";
           check = d:
-            lib.assertMsg (d != null -> (lib.types.str.check d && lib.elem d config.services.displayManager.sessionData.sessionNames)) ''
+            lib.assertMsg (d != null -> (lib.types.str.check d && lib.elem d cfg.sessionData.sessionNames)) ''
                 Default graphical session, '${d}', not found.
                 Valid names for 'services.displayManager.defaultSession' are:
                   ${lib.concatStringsSep "\n  " cfg.sessionData.sessionNames}
@@ -187,7 +187,7 @@ in
 
     services.displayManager.sessionData = {
       desktops = installedSessions;
-      sessionNames = lib.concatMap (p: p.providedSessions) config.services.displayManager.sessionPackages;
+      sessionNames = lib.concatMap (p: p.providedSessions) cfg.sessionPackages;
       # We do not want to force users to set defaultSession when they have only single DE.
       autologinSession =
         if cfg.defaultSession != null then
diff --git a/nixos/modules/services/display-managers/greetd.nix b/nixos/modules/services/display-managers/greetd.nix
index c07b225fc4d95..118a3e1df378c 100644
--- a/nixos/modules/services/display-managers/greetd.nix
+++ b/nixos/modules/services/display-managers/greetd.nix
@@ -27,6 +27,17 @@ in
       '';
     };
 
+    greeterManagesPlymouth = mkOption {
+      type = types.bool;
+      internal = true;
+      default = false;
+      description = ''
+        Don't configure the greetd service to wait for Plymouth to exit.
+
+        Enable this if the greeter you're using can manage Plymouth itself to provide a smoother handoff.
+      '';
+    };
+
     vt = mkOption {
       type = types.int;
       default = 1;
@@ -72,8 +83,9 @@ in
         ];
         After = [
           "systemd-user-sessions.service"
-          "plymouth-quit-wait.service"
           "getty@${tty}.service"
+        ] ++ lib.optionals (!cfg.greeterManagesPlymouth) [
+          "plymouth-quit-wait.service"
         ];
         Conflicts = [
           "getty@${tty}.service"
diff --git a/nixos/modules/services/hardware/thermald.nix b/nixos/modules/services/hardware/thermald.nix
index 4f9202d13d903..25cfd97016288 100644
--- a/nixos/modules/services/hardware/thermald.nix
+++ b/nixos/modules/services/hardware/thermald.nix
@@ -50,8 +50,8 @@ in
             ${optionalString cfg.debug "--loglevel=debug"} \
             ${optionalString cfg.ignoreCpuidCheck "--ignore-cpuid-check"} \
             ${optionalString (cfg.configFile != null) "--config-file ${cfg.configFile}"} \
-            --dbus-enable \
-            --adaptive
+            ${optionalString (cfg.configFile == null) "--adaptive"} \
+            --dbus-enable
         '';
       };
     };
diff --git a/nixos/modules/services/mail/stalwart-mail.nix b/nixos/modules/services/mail/stalwart-mail.nix
index 9cc919fd117d6..08b07b885b696 100644
--- a/nixos/modules/services/mail/stalwart-mail.nix
+++ b/nixos/modules/services/mail/stalwart-mail.nix
@@ -38,6 +38,7 @@ in {
       store.blob.path = mkDefault "${dataDir}/data/blobs";
       storage.data = mkDefault "db";
       storage.fts = mkDefault "db";
+      storage.lookup = mkDefault "db";
       storage.blob = mkDefault "blob";
       resolver.type = mkDefault "system";
       resolver.public-suffix = mkDefault ["https://publicsuffix.org/list/public_suffix_list.dat"];
diff --git a/nixos/modules/services/misc/bcg.nix b/nixos/modules/services/misc/bcg.nix
index 626a67f66d08b..63c441833d958 100644
--- a/nixos/modules/services/misc/bcg.nix
+++ b/nixos/modules/services/misc/bcg.nix
@@ -149,20 +149,20 @@ in
     systemd.services.bcg = let
       envConfig = cfg.environmentFiles != [];
       finalConfig = if envConfig
-                    then "$RUNTIME_DIRECTORY/bcg.config.yaml"
+                    then "\${RUNTIME_DIRECTORY}/bcg.config.yaml"
                     else configFile;
     in {
       description = "BigClown Gateway";
       wantedBy = [ "multi-user.target" ];
       wants = [ "network-online.target" ] ++ lib.optional config.services.mosquitto.enable "mosquitto.service";
       after = [ "network-online.target" ];
-      preStart = ''
+      preStart = mkIf envConfig ''
         umask 077
         ${pkgs.envsubst}/bin/envsubst -i "${configFile}" -o "${finalConfig}"
         '';
       serviceConfig = {
         EnvironmentFile = cfg.environmentFiles;
-        ExecStart="${cfg.package}/bin/bcg -c ${finalConfig} -v ${cfg.verbose}";
+        ExecStart = "${cfg.package}/bin/bcg -c ${finalConfig} -v ${cfg.verbose}";
         RuntimeDirectory = "bcg";
       };
     };
diff --git a/nixos/modules/services/misc/devpi-server.nix b/nixos/modules/services/misc/devpi-server.nix
new file mode 100644
index 0000000000000..0234db4bc2c5b
--- /dev/null
+++ b/nixos/modules/services/misc/devpi-server.nix
@@ -0,0 +1,128 @@
+{
+  pkgs,
+  lib,
+  config,
+  ...
+}:
+with lib;
+let
+  cfg = config.services.devpi-server;
+
+  secretsFileName = "devpi-secret-file";
+
+  stateDirName = "devpi";
+
+  runtimeDir = "/run/${stateDirName}";
+  serverDir = "/var/lib/${stateDirName}";
+in
+{
+  options.services.devpi-server = {
+    enable = mkEnableOption "Devpi Server";
+
+    package = mkPackageOption pkgs "devpi-server" { };
+
+    primaryUrl = mkOption {
+      type = types.str;
+      description = "Url for the primary node. Required option for replica nodes.";
+    };
+
+    replica = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Run node as a replica.
+        Requires the secretFile option and the primaryUrl to be enabled.
+      '';
+    };
+
+    secretFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        Path to a shared secret file used for synchronization,
+        Required for all nodes in a replica/primary setup.
+      '';
+    };
+
+    host = mkOption {
+      type = types.str;
+      default = "localhost";
+      description = ''
+        domain/ip address to listen on
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 3141;
+      description = "The port on which Devpi Server will listen.";
+    };
+
+    openFirewall = mkEnableOption "opening the default ports in the firewall for Devpi Server";
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd.services.devpi-server = {
+      enable = true;
+      description = "devpi PyPI-compatible server";
+      documentation = [ "https://devpi.net/docs/devpi/devpi/stable/+d/index.html" ];
+      wants = [ "network-online.target" ];
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      # Since at least devpi-server 6.10.0, devpi requires the secrets file to
+      # have 0600 permissions.
+      preStart =
+        ''
+          cp ${cfg.secretFile} ${runtimeDir}/${secretsFileName}
+          chmod 0600 ${runtimeDir}/*${secretsFileName}
+
+          if [ -f ${serverDir}/.nodeinfo ]; then
+            # already initialized the package index, exit gracefully
+            exit 0
+          fi
+          ${cfg.package}/bin/devpi-init --serverdir ${serverDir} ''
+        + strings.optionalString cfg.replica "--role=replica --master-url=${cfg.primaryUrl}";
+
+      serviceConfig = {
+        Restart = "always";
+        ExecStart =
+          let
+            args =
+              [
+                "--request-timeout=5"
+                "--serverdir=${serverDir}"
+                "--host=${cfg.host}"
+                "--port=${builtins.toString cfg.port}"
+              ]
+              ++ lib.optionals (! isNull cfg.secretFile) [
+                "--secretfile=${runtimeDir}/${secretsFileName}"
+              ]
+              ++ (
+                if cfg.replica then
+                  [
+                    "--role=replica"
+                    "--master-url=${cfg.primaryUrl}"
+                  ]
+                else
+                  [ "--role=master" ]
+              );
+          in
+          "${cfg.package}/bin/devpi-server ${concatStringsSep " " args}";
+        DynamicUser = true;
+        StateDirectory = stateDirName;
+        RuntimeDirectory = stateDirName;
+        PrivateDevices = true;
+        PrivateTmp = true;
+        ProtectHome = true;
+        ProtectSystem = "strict";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.port ];
+    };
+
+    meta.maintainers = [ cafkafk ];
+  };
+}
diff --git a/nixos/modules/services/misc/tzupdate.nix b/nixos/modules/services/misc/tzupdate.nix
index eac1e1112a5ab..be63bb179e423 100644
--- a/nixos/modules/services/misc/tzupdate.nix
+++ b/nixos/modules/services/misc/tzupdate.nix
@@ -41,5 +41,5 @@ in {
     };
   };
 
-  meta.maintainers = [ maintainers.michaelpj ];
+  meta.maintainers = [ ];
 }
diff --git a/nixos/modules/services/monitoring/arbtt.nix b/nixos/modules/services/monitoring/arbtt.nix
index 6dad6bdec3284..cf9a236c079c0 100644
--- a/nixos/modules/services/monitoring/arbtt.nix
+++ b/nixos/modules/services/monitoring/arbtt.nix
@@ -45,5 +45,5 @@ in {
     };
   };
 
-  meta.maintainers = [ maintainers.michaelpj ];
+  meta.maintainers = [ ];
 }
diff --git a/nixos/modules/services/networking/hostapd.nix b/nixos/modules/services/networking/hostapd.nix
index 1bef5a1f0a9e8..b678656f2e046 100644
--- a/nixos/modules/services/networking/hostapd.nix
+++ b/nixos/modules/services/networking/hostapd.nix
@@ -687,7 +687,7 @@ in {
                   authentication = {
                     mode = mkOption {
                       default = "wpa3-sae";
-                      type = types.enum ["none" "wpa2-sha256" "wpa3-sae-transition" "wpa3-sae"];
+                      type = types.enum ["none" "wpa2-sha1" "wpa2-sha256" "wpa3-sae-transition" "wpa3-sae"];
                       description = ''
                         Selects the authentication mode for this AP.
 
@@ -695,7 +695,9 @@ in {
                           and create an open AP. Use {option}`settings` together with this option if you
                           want to configure the authentication manually. Any password options will still be
                           effective, if set.
-                        - {var}`"wpa2-sha256"`: WPA2-Personal using SHA256 (IEEE 802.11i/RSN). Passwords are set
+                        - {var}`"wpa2-sha1"`: Not recommended. WPA2-Personal using HMAC-SHA1. Passwords are set
+                          using {option}`wpaPassword` or preferably by {option}`wpaPasswordFile` or {option}`wpaPskFile`.
+                        - {var}`"wpa2-sha256"`: WPA2-Personal using HMAC-SHA256 (IEEE 802.11i/RSN). Passwords are set
                           using {option}`wpaPassword` or preferably by {option}`wpaPasswordFile` or {option}`wpaPskFile`.
                         - {var}`"wpa3-sae-transition"`: Use WPA3-Personal (SAE) if possible, otherwise fallback
                           to WPA2-SHA256. Only use if necessary and switch to the newer WPA3-SAE when possible.
@@ -812,7 +814,7 @@ in {
                         Warning: These entries will get put into a world-readable file in
                         the Nix store! Using {option}`saePasswordFile` instead is recommended.
 
-                        Not used when {option}`mode` is {var}`"wpa2-sha256"`.
+                        Not used when {option}`mode` is {var}`"wpa2-sha1"` or {var}`"wpa2-sha256"`.
                       '';
                       type = types.listOf (types.submodule {
                         options = {
@@ -884,7 +886,7 @@ in {
                         parameters doesn't matter:
                         `<password>[|mac=<peer mac>][|vlanid=<VLAN ID>][|pk=<m:ECPrivateKey-base64>][|id=<identifier>]`
 
-                        Not used when {option}`mode` is {var}`"wpa2-sha256"`.
+                        Not used when {option}`mode` is {var}`"wpa2-sha1"` or {var}`"wpa2-sha256"`.
                       '';
                     };
 
@@ -959,6 +961,9 @@ in {
                   } // optionalAttrs (bssCfg.authentication.mode == "wpa3-sae-transition") {
                     wpa = 2;
                     wpa_key_mgmt = "WPA-PSK-SHA256 SAE";
+                  } // optionalAttrs (bssCfg.authentication.mode == "wpa2-sha1") {
+                    wpa = 2;
+                    wpa_key_mgmt = "WPA-PSK";
                   } // optionalAttrs (bssCfg.authentication.mode == "wpa2-sha256") {
                     wpa = 2;
                     wpa_key_mgmt = "WPA-PSK-SHA256";
@@ -1186,8 +1191,8 @@ in {
                   message = ''hostapd radio ${radio} bss ${bss}: uses WPA3-SAE in transition mode requires defining both a wpa password option and a sae password option'';
                 }
                 {
-                  assertion = auth.mode == "wpa2-sha256" -> countWpaPasswordDefinitions == 1;
-                  message = ''hostapd radio ${radio} bss ${bss}: uses WPA2-SHA256 which requires defining a wpa password option'';
+                  assertion = (auth.mode == "wpa2-sha1" || auth.mode == "wpa2-sha256") -> countWpaPasswordDefinitions == 1;
+                  message = ''hostapd radio ${radio} bss ${bss}: uses WPA2-PSK which requires defining a wpa password option'';
                 }
               ])
               radioCfg.networks))
diff --git a/nixos/modules/services/networking/smokeping.nix b/nixos/modules/services/networking/smokeping.nix
index 38d6e4452c97b..3fb3eac45cc82 100644
--- a/nixos/modules/services/networking/smokeping.nix
+++ b/nixos/modules/services/networking/smokeping.nix
@@ -47,6 +47,13 @@ let
 in
 
 {
+  imports = [
+    (mkRemovedOptionModule [ "services" "smokeping" "port" ] ''
+      The smokeping web service is now served by nginx.
+      In order to change the port, you need to change the nginx configuration under `services.nginx.virtualHosts.smokeping.listen.*.port`.
+    '')
+  ];
+
   options = {
     services.smokeping = {
       enable = mkEnableOption "smokeping service";
@@ -71,8 +78,8 @@ in
       };
       cgiUrl = mkOption {
         type = types.str;
-        default = "http://${cfg.hostName}:${toString cfg.port}/smokeping.cgi";
-        defaultText = literalExpression ''"http://''${hostName}:''${toString port}/smokeping.cgi"'';
+        default = "http://${cfg.hostName}/smokeping.cgi";
+        defaultText = literalExpression ''"http://''${hostName}/smokeping.cgi"'';
         example = "https://somewhere.example.com/smokeping.cgi";
         description = "URL to the smokeping cgi.";
       };
@@ -177,11 +184,6 @@ in
           which makes it bind to all interfaces.
         '';
       };
-      port = mkOption {
-        type = types.port;
-        default = 8081;
-        description = "TCP port to use for the web server.";
-      };
       presentationConfig = mkOption {
         type = types.lines;
         default = ''
@@ -312,17 +314,8 @@ in
       description = "smokeping daemon user";
       home = smokepingHome;
       createHome = true;
-      # When `cfg.webService` is enabled, `thttpd` makes SmokePing available
-      # under `${cfg.host}:${cfg.port}/smokeping.fcgi` as per the `ln -s` below.
-      # We also want that going to `${cfg.host}:${cfg.port}` without `smokeping.fcgi`
-      # makes it easy for the user to find SmokePing.
-      # However `thttpd` does not seem to support easy redirections from `/` to `smokeping.fcgi`
-      # and only allows directory listings or `/` -> `index.html` resolution if the directory
-      # has `chmod 755` (see https://acme.com/software/thttpd/thttpd_man.html#PERMISSIONS,
-      # " directories should be 755 if you want to allow indexing").
-      # Otherwise it shows `403 Forbidden` on `/`.
-      # Thus, we need to make `smokepingHome` (which is given to `thttpd -d` below) `755`.
-      homeMode = "755";
+      # When `cfg.webService` is enabled, `nginx` requires read permissions on the home directory.
+      homeMode = "711";
     };
     users.groups.${cfg.user} = { };
     systemd.services.smokeping = {
@@ -342,21 +335,25 @@ in
         ${cfg.package}/bin/smokeping --static --config=${configPath}
       '';
     };
-    systemd.services.thttpd = mkIf cfg.webService {
-      requiredBy = [ "multi-user.target" ];
-      requires = [ "smokeping.service" ];
-      path = with pkgs; [ bash rrdtool smokeping thttpd ];
-      serviceConfig = {
-        Restart = "always";
-        ExecStart = lib.concatStringsSep " " (lib.concatLists [
-          [ "${pkgs.thttpd}/bin/thttpd" ]
-          [ "-u ${cfg.user}" ]
-          [ ''-c "**.fcgi"'' ]
-          [ "-d ${smokepingHome}" ]
-          (lib.optional (cfg.host != null) "-h ${cfg.host}")
-          [ "-p ${builtins.toString cfg.port}" ]
-          [ "-D -nos" ]
-        ]);
+
+    # use nginx to serve the smokeping web service
+    services.fcgiwrap.enable = mkIf cfg.webService true;
+    services.nginx = mkIf cfg.webService {
+      enable = true;
+      virtualHosts."smokeping" = {
+        serverName = mkDefault cfg.host;
+        locations."/" = {
+          root = smokepingHome;
+          index = "smokeping.fcgi";
+        };
+        locations."/smokeping.fcgi" = {
+          extraConfig = ''
+            include ${config.services.nginx.package}/conf/fastcgi_params;
+            fastcgi_pass unix:${config.services.fcgiwrap.socketAddress};
+            fastcgi_param SCRIPT_FILENAME ${smokepingHome}/smokeping.fcgi;
+            fastcgi_param DOCUMENT_ROOT ${smokepingHome};
+          '';
+        };
       };
     };
   };
diff --git a/nixos/modules/services/networking/vsftpd.nix b/nixos/modules/services/networking/vsftpd.nix
index 25f950600b91c..07b93e92a7509 100644
--- a/nixos/modules/services/networking/vsftpd.nix
+++ b/nixos/modules/services/networking/vsftpd.nix
@@ -278,7 +278,7 @@ in
       }
       {
         assertion = (cfg.enableVirtualUsers -> cfg.userDbPath != null)
-                 && (cfg.enableVirtualUsers -> cfg.localUsers != null);
+                 && (cfg.enableVirtualUsers -> cfg.localUsers);
         message = "vsftpd: If enableVirtualUsers is true, you need to setup both the userDbPath and localUsers options.";
       }];
 
diff --git a/nixos/modules/services/security/oauth2-proxy-nginx.nix b/nixos/modules/services/security/oauth2-proxy-nginx.nix
index c05bd304752d1..07192e7287b05 100644
--- a/nixos/modules/services/security/oauth2-proxy-nginx.nix
+++ b/nixos/modules/services/security/oauth2-proxy-nginx.nix
@@ -64,11 +64,11 @@ in
     };
   };
 
-  config.services.oauth2-proxy = lib.mkIf (cfg.virtualHosts != [] && (lib.hasPrefix "127.0.0.1:" cfg.proxy)) {
+  config.services.oauth2-proxy = lib.mkIf (cfg.virtualHosts != {} && (lib.hasPrefix "127.0.0.1:" cfg.proxy)) {
     enable = true;
   };
 
-  config.services.nginx = lib.mkIf (cfg.virtualHosts != [] && config.services.oauth2-proxy.enable) (lib.mkMerge ([
+  config.services.nginx = lib.mkIf (cfg.virtualHosts != {} && config.services.oauth2-proxy.enable) (lib.mkMerge ([
     {
       virtualHosts.${cfg.domain}.locations."/oauth2/" = {
         proxyPass = cfg.proxy;
@@ -78,7 +78,7 @@ in
         '';
       };
     }
-  ] ++ lib.optional (cfg.virtualHosts != []) {
+  ] ++ lib.optional (cfg.virtualHosts != {}) {
     recommendedProxySettings = true; # needed because duplicate headers
   } ++ (lib.mapAttrsToList (vhost: conf: {
     virtualHosts.${vhost} = {
diff --git a/nixos/modules/services/web-apps/miniflux.nix b/nixos/modules/services/web-apps/miniflux.nix
index d65d6db3cdaaa..61243a63c582e 100644
--- a/nixos/modules/services/web-apps/miniflux.nix
+++ b/nixos/modules/services/web-apps/miniflux.nix
@@ -1,7 +1,7 @@
 { config, lib, pkgs, ... }:
 
-with lib;
 let
+  inherit (lib) mkEnableOption mkPackageOption mkOption types literalExpression mkIf mkDefault;
   cfg = config.services.miniflux;
 
   defaultAddress = "localhost:8080";
@@ -20,8 +20,8 @@ in
 
       package = mkPackageOption pkgs "miniflux" { };
 
-      createDatabaseLocally = lib.mkOption {
-        type = lib.types.bool;
+      createDatabaseLocally = mkOption {
+        type = types.bool;
         default = true;
         description = ''
           Whether a PostgreSQL database should be automatically created and
@@ -66,6 +66,7 @@ in
       DATABASE_URL = lib.mkIf cfg.createDatabaseLocally "user=miniflux host=/run/postgresql dbname=miniflux";
       RUN_MIGRATIONS = 1;
       CREATE_ADMIN = 1;
+      WATCHDOG = 1;
     };
 
     services.postgresql = lib.mkIf cfg.createDatabaseLocally {
@@ -96,12 +97,18 @@ in
         ++ lib.optionals cfg.createDatabaseLocally [ "postgresql.service" "miniflux-dbsetup.service" ];
 
       serviceConfig = {
-        ExecStart = "${cfg.package}/bin/miniflux";
+        Type = "notify";
+        ExecStart = lib.getExe cfg.package;
         User = "miniflux";
         DynamicUser = true;
         RuntimeDirectory = "miniflux";
         RuntimeDirectoryMode = "0750";
         EnvironmentFile = cfg.adminCredentialsFile;
+        WatchdogSec = 60;
+        WatchdogSignal = "SIGKILL";
+        Restart = "always";
+        RestartSec = 5;
+
         # Hardening
         CapabilityBoundingSet = [ "" ];
         DeviceAllow = [ "" ];
diff --git a/nixos/modules/services/web-apps/your_spotify.nix b/nixos/modules/services/web-apps/your_spotify.nix
new file mode 100644
index 0000000000000..3eb2ffef4f933
--- /dev/null
+++ b/nixos/modules/services/web-apps/your_spotify.nix
@@ -0,0 +1,191 @@
+{
+  pkgs,
+  config,
+  lib,
+  ...
+}: let
+  inherit
+    (lib)
+    boolToString
+    concatMapAttrs
+    concatStrings
+    isBool
+    mapAttrsToList
+    mkEnableOption
+    mkIf
+    mkOption
+    mkPackageOption
+    optionalAttrs
+    types
+    mkDefault
+    ;
+  cfg = config.services.your_spotify;
+
+  configEnv = concatMapAttrs (name: value:
+    optionalAttrs (value != null) {
+      ${name} =
+        if isBool value
+        then boolToString value
+        else toString value;
+    })
+  cfg.settings;
+
+  configFile = pkgs.writeText "your_spotify.env" (concatStrings (mapAttrsToList (name: value: "${name}=${value}\n") configEnv));
+in {
+  options.services.your_spotify = let
+    inherit (types) nullOr port str path package;
+  in {
+    enable = mkEnableOption "your_spotify";
+
+    enableLocalDB = mkEnableOption "a local mongodb instance";
+    nginxVirtualHost = mkOption {
+      type = nullOr str;
+      default = null;
+      description = ''
+        If set creates an nginx virtual host for the client.
+        In most cases this should be the CLIENT_ENDPOINT without
+        protocol prefix.
+      '';
+    };
+
+    package = mkPackageOption pkgs "your_spotify" {};
+
+    clientPackage = mkOption {
+      type = package;
+      description = "Client package to use.";
+    };
+
+    spotifySecretFile = mkOption {
+      type = path;
+      description = ''
+        A file containing the secret key of your Spotify application.
+        Refer to: [Creating the Spotify Application](https://github.com/Yooooomi/your_spotify#creating-the-spotify-application).
+      '';
+    };
+
+    settings = mkOption {
+      description = ''
+        Your Spotify Configuration. Refer to [Your Spotify](https://github.com/Yooooomi/your_spotify) for definitions and values.
+      '';
+      example = lib.literalExpression ''
+        {
+          CLIENT_ENDPOINT = "https://example.com";
+          API_ENDPOINT = "https://api.example.com";
+          SPOTIFY_PUBLIC = "spotify_client_id";
+        }
+      '';
+      type = types.submodule {
+        freeformType = types.attrsOf types.str;
+        options = {
+          CLIENT_ENDPOINT = mkOption {
+            type = str;
+            description = ''
+              The endpoint of your web application.
+              Has to include a protocol Prefix (e.g. `http://`)
+            '';
+            example = "https://your_spotify.example.org";
+          };
+          API_ENDPOINT = mkOption {
+            type = str;
+            description = ''
+              The endpoint of your server
+              This api has to be reachable from the device you use the website from not from the server.
+              This means that for example you may need two nginx virtual hosts if you want to expose this on the
+              internet.
+              Has to include a protocol Prefix (e.g. `http://`)
+            '';
+            example = "https://localhost:3000";
+          };
+          SPOTIFY_PUBLIC = mkOption {
+            type = str;
+            description = ''
+              The public client ID of your Spotify application.
+              Refer to: [Creating the Spotify Application](https://github.com/Yooooomi/your_spotify#creating-the-spotify-application)
+            '';
+          };
+          MONGO_ENDPOINT = mkOption {
+            type = str;
+            description = ''The endpoint of the Mongo database.'';
+            default = "mongodb://localhost:27017/your_spotify";
+          };
+          PORT = mkOption {
+            type = port;
+            description = "The port of the api server";
+            default = 3000;
+          };
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.your_spotify.clientPackage = mkDefault (cfg.package.client.override {apiEndpoint = cfg.settings.API_ENDPOINT;});
+    systemd.services.your_spotify = {
+      after = ["network.target"];
+      script = ''
+        export SPOTIFY_SECRET=$(< "$CREDENTIALS_DIRECTORY/SPOTIFY_SECRET")
+        ${lib.getExe' cfg.package "your_spotify_migrate"}
+        exec ${lib.getExe cfg.package}
+      '';
+      serviceConfig = {
+        User = "your_spotify";
+        Group = "your_spotify";
+        DynamicUser = true;
+        EnvironmentFile = [configFile];
+        StateDirectory = "your_spotify";
+        LimitNOFILE = "1048576";
+        PrivateTmp = true;
+        PrivateDevices = true;
+        StateDirectoryMode = "0700";
+        Restart = "always";
+
+        LoadCredential = ["SPOTIFY_SECRET:${cfg.spotifySecretFile}"];
+
+        # Hardening
+        CapabilityBoundingSet = "";
+        LockPersonality = true;
+        #MemoryDenyWriteExecute = true; # Leads to coredump because V8 does JIT
+        PrivateUsers = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProcSubset = "pid";
+        ProtectSystem = "strict";
+        RestrictAddressFamilies = [
+          "AF_INET"
+          "AF_INET6"
+          "AF_NETLINK"
+        ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [
+          "@system-service"
+          "@pkey"
+        ];
+        UMask = "0077";
+      };
+      wantedBy = ["multi-user.target"];
+    };
+    services.nginx = mkIf (cfg.nginxVirtualHost != null) {
+      enable = true;
+      virtualHosts.${cfg.nginxVirtualHost} = {
+        root = cfg.clientPackage;
+        locations."/".extraConfig = ''
+          add_header Content-Security-Policy "frame-ancestors 'none';" ;
+          add_header X-Content-Type-Options "nosniff" ;
+          try_files = $uri $uri/ /index.html ;
+        '';
+      };
+    };
+    services.mongodb = mkIf cfg.enableLocalDB {
+      enable = true;
+    };
+  };
+  meta.maintainers = with lib.maintainers; [patrickdag];
+}
diff --git a/nixos/modules/services/web-servers/caddy/default.nix b/nixos/modules/services/web-servers/caddy/default.nix
index 1cd1448c7d567..064a0c71b586b 100644
--- a/nixos/modules/services/web-servers/caddy/default.nix
+++ b/nixos/modules/services/web-servers/caddy/default.nix
@@ -365,7 +365,7 @@ in
         # If the empty string is assigned to this option, the list of commands to start is reset, prior assignments of this option will have no effect.
         ExecStart = [ "" ''${cfg.package}/bin/caddy run ${runOptions} ${optionalString cfg.resume "--resume"}'' ];
         # Validating the configuration before applying it ensures we’ll get a proper error that will be reported when switching to the configuration
-        ExecReload = [ "" ''${cfg.package}/bin/caddy reload ${runOptions} --force'' ];
+        ExecReload = [ "" ] ++ lib.optional cfg.enableReload "${lib.getExe cfg.package} reload ${runOptions} --force";
         User = cfg.user;
         Group = cfg.group;
         ReadWritePaths = [ cfg.dataDir ];
diff --git a/nixos/modules/services/web-servers/garage.nix b/nixos/modules/services/web-servers/garage.nix
index 39ea8f21b126f..d2a5109e266a9 100644
--- a/nixos/modules/services/web-servers/garage.nix
+++ b/nixos/modules/services/web-servers/garage.nix
@@ -52,13 +52,6 @@ in
             type = types.path;
             description = "The main data storage, put this on your large storage (e.g. high capacity HDD)";
           };
-
-          replication_mode = mkOption {
-            default = "none";
-            type = types.enum ([ "none" "1" "2" "3" "2-dangerous" "3-dangerous" "3-degraded" 1 2 3 ]);
-            apply = v: toString v;
-            description = "Garage replication mode, defaults to none, see: <https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#replication-mode> for reference.";
-          };
         };
       };
       description = "Garage configuration, see <https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/> for reference.";
@@ -71,6 +64,44 @@ in
   };
 
   config = mkIf cfg.enable {
+
+    assertions = [
+      # We removed our module-level default for replication_mode. If a user upgraded
+      # to garage 1.0.0 while relying on the module-level default, they would be left
+      # with a config which evaluates and builds, but then garage refuses to start
+      # because either replication_factor or replication_mode is required.
+      # The replication_factor option also was `toString`'ed before, which is
+      # now not possible anymore, so we prompt the user to change it to a string
+      # if present.
+      # These assertions can be removed in NixOS 24.11, when all users have been
+      # warned once.
+      {
+        assertion = (cfg.settings ? replication_factor || cfg.settings ? replication_mode) || lib.versionOlder cfg.package "1.0.0";
+        message = ''
+          Garage 1.0.0 requires an explicit replication factor to be set.
+          Please set replication_factor to 1 explicitly to preserve the previous behavior.
+          https://git.deuxfleurs.fr/Deuxfleurs/garage/src/tag/v1.0.0/doc/book/reference-manual/configuration.md#replication_factor
+
+        '';
+      }
+      {
+        assertion = lib.isString (cfg.settings.replication_mode or "");
+        message = ''
+          The explicit `replication_mode` option in `services.garage.settings`
+          has been removed and is now handled by the freeform settings in order
+          to allow it being completely absent (for Garage 1.x).
+          That module option previously `toString`'ed the value it's configured
+          with, which is now no longer possible.
+
+          You're still using a non-string here, please manually set it to
+          a string, or migrate to the separate setting keys introduced in 1.x.
+
+          Refer to https://garagehq.deuxfleurs.fr/documentation/working-documents/migration-1/
+          for the migration guide.
+        '';
+      }
+    ];
+
     environment.etc."garage.toml" = {
       source = configFile;
     };
diff --git a/nixos/modules/services/x11/xserver.nix b/nixos/modules/services/x11/xserver.nix
index e13c273746701..5a86d055c2719 100644
--- a/nixos/modules/services/x11/xserver.nix
+++ b/nixos/modules/services/x11/xserver.nix
@@ -728,9 +728,6 @@ in
             rm -f /tmp/.X0-lock
           '';
 
-        # TODO: move declaring the systemd service to its own mkIf
-        script = mkIf (config.systemd.services.display-manager.enable == true) "${config.services.displayManager.execCmd}";
-
         # Stop restarting if the display manager stops (crashes) 2 times
         # in one minute. Starting X typically takes 3-4s.
         startLimitIntervalSec = 30;
diff --git a/nixos/modules/system/activation/switchable-system.nix b/nixos/modules/system/activation/switchable-system.nix
index d5bd8cc1dc115..d70fefd0920b4 100644
--- a/nixos/modules/system/activation/switchable-system.nix
+++ b/nixos/modules/system/activation/switchable-system.nix
@@ -4,52 +4,93 @@ let
 
   perlWrapped = pkgs.perl.withPackages (p: with p; [ ConfigIniFiles FileSlurp ]);
 
+  description = extra: ''
+    Whether to include the capability to switch configurations.
+
+    Disabling this makes the system unable to be reconfigured via `nixos-rebuild`.
+
+    ${extra}
+  '';
+
 in
 
 {
 
-  options = {
-    system.switch.enable = lib.mkOption {
+  options.system.switch = {
+    enable = lib.mkOption {
       type = lib.types.bool;
       default = true;
-      description = ''
-        Whether to include the capability to switch configurations.
-
-        Disabling this makes the system unable to be reconfigured via `nixos-rebuild`.
-
+      description = description ''
         This is good for image based appliances where updates are handled
         outside the image. Reducing features makes the image lighter and
         slightly more secure.
       '';
     };
-  };
 
-  config = lib.mkIf config.system.switch.enable {
-    system.activatableSystemBuilderCommands = ''
-      mkdir $out/bin
-      substitute ${./switch-to-configuration.pl} $out/bin/switch-to-configuration \
-        --subst-var out \
-        --subst-var-by toplevel ''${!toplevelVar} \
-        --subst-var-by coreutils "${pkgs.coreutils}" \
-        --subst-var-by distroId ${lib.escapeShellArg config.system.nixos.distroId} \
-        --subst-var-by installBootLoader ${lib.escapeShellArg config.system.build.installBootLoader} \
-        --subst-var-by localeArchive "${config.i18n.glibcLocales}/lib/locale/locale-archive" \
-        --subst-var-by perl "${perlWrapped}" \
-        --subst-var-by shell "${pkgs.bash}/bin/sh" \
-        --subst-var-by su "${pkgs.shadow.su}/bin/su" \
-        --subst-var-by systemd "${config.systemd.package}" \
-        --subst-var-by utillinux "${pkgs.util-linux}" \
-        ;
-
-      chmod +x $out/bin/switch-to-configuration
-      ${lib.optionalString (pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) ''
-        if ! output=$(${perlWrapped}/bin/perl -c $out/bin/switch-to-configuration 2>&1); then
-          echo "switch-to-configuration syntax is not valid:"
-          echo "$output"
-          exit 1
-        fi
-      ''}
-    '';
+    enableNg = lib.mkOption {
+      type = lib.types.bool;
+      default = false;
+      description = description ''
+        Whether to use `switch-to-configuration-ng`, an experimental
+        re-implementation of `switch-to-configuration` with the goal of
+        replacing the original.
+      '';
+    };
   };
 
+  config = lib.mkMerge [
+    {
+      assertions = [{
+        assertion = with config.system.switch; enable -> !enableNg;
+        message = "Only one of system.switch.enable and system.switch.enableNg may be enabled at a time";
+      }];
+    }
+    (lib.mkIf config.system.switch.enable {
+      system.activatableSystemBuilderCommands = ''
+        mkdir $out/bin
+        substitute ${./switch-to-configuration.pl} $out/bin/switch-to-configuration \
+          --subst-var out \
+          --subst-var-by toplevel ''${!toplevelVar} \
+          --subst-var-by coreutils "${pkgs.coreutils}" \
+          --subst-var-by distroId ${lib.escapeShellArg config.system.nixos.distroId} \
+          --subst-var-by installBootLoader ${lib.escapeShellArg config.system.build.installBootLoader} \
+          --subst-var-by localeArchive "${config.i18n.glibcLocales}/lib/locale/locale-archive" \
+          --subst-var-by perl "${perlWrapped}" \
+          --subst-var-by shell "${pkgs.bash}/bin/sh" \
+          --subst-var-by su "${pkgs.shadow.su}/bin/su" \
+          --subst-var-by systemd "${config.systemd.package}" \
+          --subst-var-by utillinux "${pkgs.util-linux}" \
+          ;
+
+        chmod +x $out/bin/switch-to-configuration
+        ${lib.optionalString (pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) ''
+          if ! output=$(${perlWrapped}/bin/perl -c $out/bin/switch-to-configuration 2>&1); then
+            echo "switch-to-configuration syntax is not valid:"
+            echo "$output"
+            exit 1
+          fi
+        ''}
+      '';
+    })
+    (lib.mkIf config.system.switch.enableNg {
+      # Use a subshell so we can source makeWrapper's setup hook without
+      # affecting the rest of activatableSystemBuilderCommands.
+      system.activatableSystemBuilderCommands = ''
+        (
+          source ${pkgs.buildPackages.makeWrapper}/nix-support/setup-hook
+
+          mkdir $out/bin
+          ln -sf ${lib.getExe pkgs.switch-to-configuration-ng} $out/bin/switch-to-configuration
+          wrapProgram $out/bin/switch-to-configuration \
+            --set OUT $out \
+            --set TOPLEVEL ''${!toplevelVar} \
+            --set DISTRO_ID ${lib.escapeShellArg config.system.nixos.distroId} \
+            --set INSTALL_BOOTLOADER ${lib.escapeShellArg config.system.build.installBootLoader} \
+            --set LOCALE_ARCHIVE ${config.i18n.glibcLocales}/lib/locale/locale-archive \
+            --set SYSTEMD ${config.systemd.package}
+        )
+      '';
+    })
+  ];
+
 }
diff --git a/nixos/modules/system/boot/binfmt.nix b/nixos/modules/system/boot/binfmt.nix
index 3605ce56910ed..572bfeda7a8ff 100644
--- a/nixos/modules/system/boot/binfmt.nix
+++ b/nixos/modules/system/boot/binfmt.nix
@@ -280,7 +280,7 @@ in {
   };
 
   config = {
-    boot.binfmt.registrations = builtins.listToAttrs (map (system: {
+    boot.binfmt.registrations = builtins.listToAttrs (map (system: assert system != pkgs.system; {
       name = system;
       value = { config, ... }: let
         interpreter = getEmulator system;
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index f9e81f2bbd8d6..c6ec2474e6052 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -243,6 +243,7 @@ in {
   deepin = handleTest ./deepin.nix {};
   deluge = handleTest ./deluge.nix {};
   dendrite = handleTest ./matrix/dendrite.nix {};
+  devpi-server = handleTest ./devpi-server.nix {};
   dex-oidc = handleTest ./dex-oidc.nix {};
   dhparams = handleTest ./dhparams.nix {};
   disable-installer-tools = handleTest ./disable-installer-tools.nix {};
@@ -872,7 +873,8 @@ in {
   swap-random-encryption = handleTest ./swap-random-encryption.nix {};
   sway = handleTest ./sway.nix {};
   swayfx = handleTest ./swayfx.nix {};
-  switchTest = handleTest ./switch-test.nix {};
+  switchTest = handleTest ./switch-test.nix { ng = false; };
+  switchTestNg = handleTest ./switch-test.nix { ng = true; };
   sympa = handleTest ./sympa.nix {};
   syncthing = handleTest ./syncthing.nix {};
   syncthing-no-settings = handleTest ./syncthing-no-settings.nix {};
@@ -885,7 +887,7 @@ in {
   systemd-binfmt = handleTestOn ["x86_64-linux"] ./systemd-binfmt.nix {};
   systemd-boot = handleTest ./systemd-boot.nix {};
   systemd-bpf = handleTest ./systemd-bpf.nix {};
-  systemd-confinement = handleTest ./systemd-confinement.nix {};
+  systemd-confinement = handleTest ./systemd-confinement {};
   systemd-coredump = handleTest ./systemd-coredump.nix {};
   systemd-cryptenroll = handleTest ./systemd-cryptenroll.nix {};
   systemd-credentials-tpm2 = handleTest ./systemd-credentials-tpm2.nix {};
@@ -1038,7 +1040,9 @@ in {
   xterm = handleTest ./xterm.nix {};
   xxh = handleTest ./xxh.nix {};
   yabar = handleTest ./yabar.nix {};
+  ydotool = handleTest ./ydotool.nix {};
   yggdrasil = handleTest ./yggdrasil.nix {};
+  your_spotify = handleTest ./your_spotify.nix {};
   zammad = handleTest ./zammad.nix {};
   zeronet-conservancy = handleTest ./zeronet-conservancy.nix {};
   zfs = handleTest ./zfs.nix {};
diff --git a/nixos/tests/devpi-server.nix b/nixos/tests/devpi-server.nix
new file mode 100644
index 0000000000000..2a16d49724dbc
--- /dev/null
+++ b/nixos/tests/devpi-server.nix
@@ -0,0 +1,35 @@
+import ./make-test-python.nix ({pkgs, ...}: let
+  server-port = 3141;
+in {
+  name = "devpi-server";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [cafkafk];
+  };
+
+  nodes = {
+    devpi = {...}: {
+      services.devpi-server = {
+        enable = true;
+        host = "0.0.0.0";
+        port = server-port;
+        openFirewall = true;
+        secretFile = pkgs.writeText "devpi-secret" "v263P+V3YGDYUyfYL/RBURw+tCPMDw94R/iCuBNJrDhaYrZYjpA6XPFVDDH8ViN20j77y2PHoMM/U0opNkVQ2g==";
+      };
+    };
+
+    client1 = {...}: {
+      environment.systemPackages = with pkgs; [
+        devpi-client
+        jq
+      ];
+    };
+  };
+
+  testScript = ''
+    start_all()
+    devpi.wait_for_unit("devpi-server.service")
+    devpi.wait_for_open_port(${builtins.toString server-port})
+
+    client1.succeed("devpi getjson http://devpi:${builtins.toString server-port}")
+  '';
+})
diff --git a/nixos/tests/fcitx5/default.nix b/nixos/tests/fcitx5/default.nix
index c113f2e2c052c..feea621f6b5b2 100644
--- a/nixos/tests/fcitx5/default.nix
+++ b/nixos/tests/fcitx5/default.nix
@@ -89,10 +89,13 @@ rec {
             machine.succeed("xauth merge ${xauth}")
             machine.sleep(5)
 
+            machine.wait_until_succeeds("pgrep fcitx5")
             machine.succeed("su - ${user.name} -c 'kill $(pgrep fcitx5)'")
             machine.sleep(1)
 
             machine.succeed("su - ${user.name} -c 'alacritty >&2 &'")
+            machine.wait_for_window("alice@machine")
+
             machine.succeed("su - ${user.name} -c 'fcitx5 >&2 &'")
             machine.sleep(10)
 
diff --git a/nixos/tests/garage/default.nix b/nixos/tests/garage/default.nix
index a42236e9a5bbe..b7f9bb4b865bd 100644
--- a/nixos/tests/garage/default.nix
+++ b/nixos/tests/garage/default.nix
@@ -51,4 +51,5 @@ in
   [
     "0_8"
     "0_9"
+    "1_x"
   ]
diff --git a/nixos/tests/garage/with-3node-replication.nix b/nixos/tests/garage/with-3node-replication.nix
index d4387b198d976..266a1082893f7 100644
--- a/nixos/tests/garage/with-3node-replication.nix
+++ b/nixos/tests/garage/with-3node-replication.nix
@@ -7,10 +7,10 @@ args@{ mkNode, ver, ... }:
   };
 
   nodes = {
-    node1 = mkNode { replicationMode = 3; publicV6Address = "fc00:1::1"; };
-    node2 = mkNode { replicationMode = 3; publicV6Address = "fc00:1::2"; };
-    node3 = mkNode { replicationMode = 3; publicV6Address = "fc00:1::3"; };
-    node4 = mkNode { replicationMode = 3; publicV6Address = "fc00:1::4"; };
+    node1 = mkNode { replicationMode = "3"; publicV6Address = "fc00:1::1"; };
+    node2 = mkNode { replicationMode = "3"; publicV6Address = "fc00:1::2"; };
+    node3 = mkNode { replicationMode = "3"; publicV6Address = "fc00:1::3"; };
+    node4 = mkNode { replicationMode = "3"; publicV6Address = "fc00:1::4"; };
   };
 
   testScript = ''
diff --git a/nixos/tests/pgvecto-rs.nix b/nixos/tests/pgvecto-rs.nix
index cd871dab6a0f1..8d9d6c0b88f51 100644
--- a/nixos/tests/pgvecto-rs.nix
+++ b/nixos/tests/pgvecto-rs.nix
@@ -66,7 +66,7 @@ let
     '';
 
   };
-  applicablePostgresqlVersions = filterAttrs (_: value: versionAtLeast value.version "12") postgresql-versions;
+  applicablePostgresqlVersions = filterAttrs (_: value: versionAtLeast value.version "14") postgresql-versions;
 in
 mapAttrs'
   (name: package: {
diff --git a/nixos/tests/smokeping.nix b/nixos/tests/smokeping.nix
index 04f8139642918..fe1ecad9969b0 100644
--- a/nixos/tests/smokeping.nix
+++ b/nixos/tests/smokeping.nix
@@ -11,7 +11,6 @@ import ./make-test-python.nix ({ pkgs, ...} : {
         networking.domain = "example.com"; # FQDN: sm.example.com
         services.smokeping = {
           enable = true;
-          port = 8081;
           mailHost = "127.0.0.2";
           probeConfig = ''
             + FPing
@@ -25,12 +24,19 @@ import ./make-test-python.nix ({ pkgs, ...} : {
   testScript = ''
     start_all()
     sm.wait_for_unit("smokeping")
-    sm.wait_for_unit("thttpd")
+    sm.wait_for_unit("nginx")
     sm.wait_for_file("/var/lib/smokeping/data/Local/LocalMachine.rrd")
-    sm.succeed("curl -s -f localhost:8081/smokeping.fcgi?target=Local")
+    sm.succeed("curl -s -f localhost/smokeping.fcgi?target=Local")
     # Check that there's a helpful page without explicit path as well.
-    sm.succeed("curl -s -f localhost:8081")
+    sm.succeed("curl -s -f localhost")
     sm.succeed("ls /var/lib/smokeping/cache/Local/LocalMachine_mini.png")
     sm.succeed("ls /var/lib/smokeping/cache/index.html")
+
+    # stop and start the service like nixos-rebuild would do
+    # see https://github.com/NixOS/nixpkgs/issues/265953)
+    sm.succeed("systemctl stop smokeping")
+    sm.succeed("systemctl start smokeping")
+    # ensure all services restarted properly
+    sm.succeed("systemctl --failed | grep -q '0 loaded units listed'")
   '';
 })
diff --git a/nixos/tests/stalwart-mail.nix b/nixos/tests/stalwart-mail.nix
index 634c0e2e39261..581090cd70f48 100644
--- a/nixos/tests/stalwart-mail.nix
+++ b/nixos/tests/stalwart-mail.nix
@@ -40,12 +40,14 @@ in import ./make-test-python.nix ({ lib, ... }: {
           };
         };
 
-        session.auth.mechanisms = [ "PLAIN" ];
-        session.auth.directory = "in-memory";
-        storage.directory = "in-memory";  # shared with imap
+        resolver.public-suffix = [ ];  # do not fetch from web in sandbox
 
-        session.rcpt.directory = "in-memory";
-        queue.outbound.next-hop = [ "local" ];
+        session.auth.mechanisms = "[plain]";
+        session.auth.directory = "'in-memory'";
+        storage.directory = "in-memory";
+
+        session.rcpt.directory = "'in-memory'";
+        queue.outbound.next-hop = "'local'";
 
         directory."in-memory" = {
           type = "memory";
diff --git a/nixos/tests/switch-test.nix b/nixos/tests/switch-test.nix
index 4a7bcd5a82264..d90e5bb088cee 100644
--- a/nixos/tests/switch-test.nix
+++ b/nixos/tests/switch-test.nix
@@ -1,6 +1,6 @@
 # Test configuration switching.
 
-import ./make-test-python.nix ({ lib, pkgs, ...} : let
+import ./make-test-python.nix ({ lib, pkgs, ng, ...} : let
 
   # Simple service that can either be socket-activated or that will
   # listen on port 1234 if not socket-activated.
@@ -48,6 +48,11 @@ in {
 
   nodes = {
     machine = { pkgs, lib, ... }: {
+      system.switch = {
+        enable = !ng;
+        enableNg = ng;
+      };
+
       environment.systemPackages = [ pkgs.socat ]; # for the socket activation stuff
       users.mutableUsers = false;
 
diff --git a/nixos/tests/systemd-confinement.nix b/nixos/tests/systemd-confinement.nix
deleted file mode 100644
index bde5b770ea50d..0000000000000
--- a/nixos/tests/systemd-confinement.nix
+++ /dev/null
@@ -1,184 +0,0 @@
-import ./make-test-python.nix {
-  name = "systemd-confinement";
-
-  nodes.machine = { pkgs, lib, ... }: let
-    testServer = pkgs.writeScript "testserver.sh" ''
-      #!${pkgs.runtimeShell}
-      export PATH=${lib.escapeShellArg "${pkgs.coreutils}/bin"}
-      ${lib.escapeShellArg pkgs.runtimeShell} 2>&1
-      echo "exit-status:$?"
-    '';
-
-    testClient = pkgs.writeScriptBin "chroot-exec" ''
-      #!${pkgs.runtimeShell} -e
-      output="$(echo "$@" | nc -NU "/run/test$(< /teststep).sock")"
-      ret="$(echo "$output" | sed -nre '$s/^exit-status:([0-9]+)$/\1/p')"
-      echo "$output" | head -n -1
-      exit "''${ret:-1}"
-    '';
-
-    mkTestStep = num: {
-      testScript,
-      config ? {},
-      serviceName ? "test${toString num}",
-    }: {
-      systemd.sockets.${serviceName} = {
-        description = "Socket for Test Service ${toString num}";
-        wantedBy = [ "sockets.target" ];
-        socketConfig.ListenStream = "/run/test${toString num}.sock";
-        socketConfig.Accept = true;
-      };
-
-      systemd.services."${serviceName}@" = {
-        description = "Confined Test Service ${toString num}";
-        confinement = (config.confinement or {}) // { enable = true; };
-        serviceConfig = (config.serviceConfig or {}) // {
-          ExecStart = testServer;
-          StandardInput = "socket";
-        };
-      } // removeAttrs config [ "confinement" "serviceConfig" ];
-
-      __testSteps = lib.mkOrder num (''
-        machine.succeed("echo ${toString num} > /teststep")
-      '' + testScript);
-    };
-
-  in {
-    imports = lib.imap1 mkTestStep [
-      { config.confinement.mode = "chroot-only";
-        testScript = ''
-          with subtest("chroot-only confinement"):
-              paths = machine.succeed('chroot-exec ls -1 / | paste -sd,').strip()
-              assert_eq(paths, "bin,nix,run")
-              uid = machine.succeed('chroot-exec id -u').strip()
-              assert_eq(uid, "0")
-              machine.succeed("chroot-exec chown 65534 /bin")
-        '';
-      }
-      { testScript = ''
-          with subtest("full confinement with APIVFS"):
-              machine.fail("chroot-exec ls -l /etc")
-              machine.fail("chroot-exec chown 65534 /bin")
-              assert_eq(machine.succeed('chroot-exec id -u').strip(), "0")
-              machine.succeed("chroot-exec chown 0 /bin")
-        '';
-      }
-      { config.serviceConfig.BindReadOnlyPaths = [ "/etc" ];
-        testScript = ''
-          with subtest("check existence of bind-mounted /etc"):
-              passwd = machine.succeed('chroot-exec cat /etc/passwd').strip()
-              assert len(passwd) > 0, "/etc/passwd must not be empty"
-        '';
-      }
-      { config.serviceConfig.User = "chroot-testuser";
-        config.serviceConfig.Group = "chroot-testgroup";
-        testScript = ''
-          with subtest("check if User/Group really runs as non-root"):
-              machine.succeed("chroot-exec ls -l /dev")
-              uid = machine.succeed('chroot-exec id -u').strip()
-              assert uid != "0", "UID of chroot-testuser shouldn't be 0"
-              machine.fail("chroot-exec touch /bin/test")
-        '';
-      }
-      (let
-        symlink = pkgs.runCommand "symlink" {
-          target = pkgs.writeText "symlink-target" "got me\n";
-        } "ln -s \"$target\" \"$out\"";
-      in {
-        config.confinement.packages = lib.singleton symlink;
-        testScript = ''
-          with subtest("check if symlinks are properly bind-mounted"):
-              machine.fail("chroot-exec test -e /etc")
-              text = machine.succeed('chroot-exec cat ${symlink}').strip()
-              assert_eq(text, "got me")
-        '';
-      })
-      { config.serviceConfig.User = "chroot-testuser";
-        config.serviceConfig.Group = "chroot-testgroup";
-        config.serviceConfig.StateDirectory = "testme";
-        testScript = ''
-          with subtest("check if StateDirectory works"):
-              machine.succeed("chroot-exec touch /tmp/canary")
-              machine.succeed('chroot-exec "echo works > /var/lib/testme/foo"')
-              machine.succeed('test "$(< /var/lib/testme/foo)" = works')
-              machine.succeed("test ! -e /tmp/canary")
-        '';
-      }
-      { testScript = ''
-          with subtest("check if /bin/sh works"):
-              machine.succeed(
-                  "chroot-exec test -e /bin/sh",
-                  'test "$(chroot-exec \'/bin/sh -c "echo bar"\')" = bar',
-              )
-        '';
-      }
-      { config.confinement.binSh = null;
-        testScript = ''
-          with subtest("check if suppressing /bin/sh works"):
-              machine.succeed("chroot-exec test ! -e /bin/sh")
-              machine.succeed('test "$(chroot-exec \'/bin/sh -c "echo foo"\')" != foo')
-        '';
-      }
-      { config.confinement.binSh = "${pkgs.hello}/bin/hello";
-        testScript = ''
-          with subtest("check if we can set /bin/sh to something different"):
-              machine.succeed("chroot-exec test -e /bin/sh")
-              machine.succeed('test "$(chroot-exec /bin/sh -g foo)" = foo')
-        '';
-      }
-      { config.environment.FOOBAR = pkgs.writeText "foobar" "eek\n";
-        testScript = ''
-          with subtest("check if only Exec* dependencies are included"):
-              machine.succeed('test "$(chroot-exec \'cat "$FOOBAR"\')" != eek')
-        '';
-      }
-      { config.environment.FOOBAR = pkgs.writeText "foobar" "eek\n";
-        config.confinement.fullUnit = true;
-        testScript = ''
-          with subtest("check if all unit dependencies are included"):
-              machine.succeed('test "$(chroot-exec \'cat "$FOOBAR"\')" = eek')
-        '';
-      }
-      { serviceName = "shipped-unitfile";
-        config.confinement.mode = "chroot-only";
-        testScript = ''
-          with subtest("check if shipped unit file still works"):
-              machine.succeed(
-                  'chroot-exec \'kill -9 $$ 2>&1 || :\' | '
-                  'grep -q "Too many levels of symbolic links"'
-              )
-        '';
-      }
-    ];
-
-    options.__testSteps = lib.mkOption {
-      type = lib.types.lines;
-      description = "All of the test steps combined as a single script.";
-    };
-
-    config.environment.systemPackages = lib.singleton testClient;
-    config.systemd.packages = lib.singleton (pkgs.writeTextFile {
-      name = "shipped-unitfile";
-      destination = "/etc/systemd/system/shipped-unitfile@.service";
-      text = ''
-        [Service]
-        SystemCallFilter=~kill
-        SystemCallErrorNumber=ELOOP
-      '';
-    });
-
-    config.users.groups.chroot-testgroup = {};
-    config.users.users.chroot-testuser = {
-      isSystemUser = true;
-      description = "Chroot Test User";
-      group = "chroot-testgroup";
-    };
-  };
-
-  testScript = { nodes, ... }: ''
-    def assert_eq(a, b):
-        assert a == b, f"{a} != {b}"
-
-    machine.wait_for_unit("multi-user.target")
-  '' + nodes.machine.config.__testSteps;
-}
diff --git a/nixos/tests/systemd-confinement/checkperms.py b/nixos/tests/systemd-confinement/checkperms.py
new file mode 100644
index 0000000000000..3c7ba279a3d20
--- /dev/null
+++ b/nixos/tests/systemd-confinement/checkperms.py
@@ -0,0 +1,187 @@
+import errno
+import os
+
+from enum import IntEnum
+from pathlib import Path
+
+
+class Accessibility(IntEnum):
+    """
+    The level of accessibility we have on a file or directory.
+
+    This is needed to assess the attack surface on the file system namespace we
+    have within a confined service. Higher levels mean more permissions for the
+    user and thus a bigger attack surface.
+    """
+    NONE = 0
+
+    # Directories can be listed or files can be read.
+    READABLE = 1
+
+    # This is for special file systems such as procfs and for stuff such as
+    # FIFOs or character special files. The reason why this has a lower value
+    # than WRITABLE is because those files are more restricted on what and how
+    # they can be written to.
+    SPECIAL = 2
+
+    # Another special case are sticky directories, which do allow write access
+    # but restrict deletion. This does *not* apply to sticky directories that
+    # are read-only.
+    STICKY = 3
+
+    # Essentially full permissions, the kind of accessibility we want to avoid
+    # in most cases.
+    WRITABLE = 4
+
+    def assert_on(self, path: Path) -> None:
+        """
+        Raise an AssertionError if the given 'path' allows for more
+        accessibility than 'self'.
+        """
+        actual = self.NONE
+
+        if path.is_symlink():
+            actual = self.READABLE
+        elif path.is_dir():
+            writable = True
+
+            dummy_file = path / 'can_i_write'
+            try:
+                dummy_file.touch()
+            except OSError as e:
+                if e.errno in [errno.EROFS, errno.EACCES]:
+                    writable = False
+                else:
+                    raise
+            else:
+                dummy_file.unlink()
+
+            if writable:
+                # The reason why we test this *after* we made sure it's
+                # writable is because we could have a sticky directory where
+                # the current user doesn't have write access.
+                if path.stat().st_mode & 0o1000 == 0o1000:
+                    actual = self.STICKY
+                else:
+                    actual = self.WRITABLE
+            else:
+                actual = self.READABLE
+        elif path.is_file():
+            try:
+                with path.open('rb') as fp:
+                    fp.read(1)
+                actual = self.READABLE
+            except PermissionError:
+                pass
+
+            writable = True
+            try:
+                with path.open('ab') as fp:
+                    fp.write('x')
+                    size = fp.tell()
+                    fp.truncate(size)
+            except PermissionError:
+                writable = False
+            except OSError as e:
+                if e.errno == errno.ETXTBSY:
+                    writable = os.access(path, os.W_OK)
+                elif e.errno == errno.EROFS:
+                    writable = False
+                else:
+                    raise
+
+            # Let's always try to fail towards being writable, so if *either*
+            # access(2) or a real write is successful it's writable. This is to
+            # make sure we don't accidentally introduce no-ops if we have bugs
+            # in the more complicated real write code above.
+            if writable or os.access(path, os.W_OK):
+                actual = self.WRITABLE
+        else:
+            # We need to be very careful when writing to or reading from
+            # special files (eg.  FIFOs), since they can possibly block. So if
+            # it's not a file, just trust that access(2) won't lie.
+            if os.access(path, os.R_OK):
+                actual = self.READABLE
+
+            if os.access(path, os.W_OK):
+                actual = self.SPECIAL
+
+        if actual > self:
+            stat = path.stat()
+            details = ', '.join([
+                f'permissions: {stat.st_mode & 0o7777:o}',
+                f'uid: {stat.st_uid}',
+                f'group: {stat.st_gid}',
+            ])
+
+            raise AssertionError(
+                f'Expected at most {self!r} but got {actual!r} for path'
+                f' {path} ({details}).'
+            )
+
+
+def is_special_fs(path: Path) -> bool:
+    """
+    Check whether the given path truly is a special file system such as procfs
+    or sysfs.
+    """
+    try:
+        if path == Path('/proc'):
+            return (path / 'version').read_text().startswith('Linux')
+        elif path == Path('/sys'):
+            return b'Linux' in (path / 'kernel' / 'notes').read_bytes()
+    except FileNotFoundError:
+        pass
+    return False
+
+
+def is_empty_dir(path: Path) -> bool:
+    try:
+        next(path.iterdir())
+        return False
+    except (StopIteration, PermissionError):
+        return True
+
+
+def _assert_permissions_in_directory(
+    directory: Path,
+    accessibility: Accessibility,
+    subdirs: dict[Path, Accessibility],
+) -> None:
+    accessibility.assert_on(directory)
+
+    for file in directory.iterdir():
+        if is_special_fs(file):
+            msg = f'Got unexpected special filesystem at {file}.'
+            assert subdirs.pop(file) == Accessibility.SPECIAL, msg
+        elif not file.is_symlink() and file.is_dir():
+            subdir_access = subdirs.pop(file, accessibility)
+            if is_empty_dir(file):
+                # Whenever we got an empty directory, we check the permission
+                # constraints on the current directory (except if specified
+                # explicitly in subdirs) because for example if we're non-root
+                # (the constraints of the current directory are thus
+                # Accessibility.READABLE), we really have to make sure that
+                # empty directories are *never* writable.
+                subdir_access.assert_on(file)
+            else:
+                _assert_permissions_in_directory(file, subdir_access, subdirs)
+        else:
+            subdirs.pop(file, accessibility).assert_on(file)
+
+
+def assert_permissions(subdirs: dict[str, Accessibility]) -> None:
+    """
+    Recursively check whether the file system conforms to the accessibility
+    specification we specified via 'subdirs'.
+    """
+    root = Path('/')
+    absolute_subdirs = {root / p: a for p, a in subdirs.items()}
+    _assert_permissions_in_directory(
+        root,
+        Accessibility.WRITABLE if os.getuid() == 0 else Accessibility.READABLE,
+        absolute_subdirs,
+    )
+    for file in absolute_subdirs.keys():
+        msg = f'Expected {file} to exist, but it was nowwhere to be found.'
+        raise AssertionError(msg)
diff --git a/nixos/tests/systemd-confinement/default.nix b/nixos/tests/systemd-confinement/default.nix
new file mode 100644
index 0000000000000..15d442d476b08
--- /dev/null
+++ b/nixos/tests/systemd-confinement/default.nix
@@ -0,0 +1,274 @@
+import ../make-test-python.nix {
+  name = "systemd-confinement";
+
+  nodes.machine = { pkgs, lib, ... }: let
+    testLib = pkgs.python3Packages.buildPythonPackage {
+      name = "confinement-testlib";
+      unpackPhase = ''
+        cat > setup.py <<EOF
+        from setuptools import setup
+        setup(name='confinement-testlib', py_modules=["checkperms"])
+        EOF
+        cp ${./checkperms.py} checkperms.py
+      '';
+    };
+
+    mkTest = name: testScript: pkgs.writers.writePython3 "${name}.py" {
+      libraries = [ pkgs.python3Packages.pytest testLib ];
+    } ''
+      # This runs our test script by using pytest's assertion rewriting, so
+      # that whenever we use "assert <something>", the actual values are
+      # printed rather than getting a generic AssertionError or the need to
+      # pass an explicit assertion error message.
+      import ast
+      from pathlib import Path
+      from _pytest.assertion.rewrite import rewrite_asserts
+
+      script = Path('${pkgs.writeText "${name}-main.py" ''
+        import errno, os, pytest, signal
+        from subprocess import run
+        from checkperms import Accessibility, assert_permissions
+
+        ${testScript}
+      ''}') # noqa
+      filename = str(script)
+      source = script.read_bytes()
+
+      tree = ast.parse(source, filename=filename)
+      rewrite_asserts(tree, source, filename)
+      exec(compile(tree, filename, 'exec', dont_inherit=True))
+    '';
+
+    mkTestStep = num: {
+      description,
+      testScript,
+      config ? {},
+      serviceName ? "test${toString num}",
+      rawUnit ? null,
+    }: {
+      systemd.packages = lib.optional (rawUnit != null) (pkgs.writeTextFile {
+        name = serviceName;
+        destination = "/etc/systemd/system/${serviceName}.service";
+        text = rawUnit;
+      });
+
+      systemd.services.${serviceName} = {
+        inherit description;
+        requiredBy = [ "multi-user.target" ];
+        confinement = (config.confinement or {}) // { enable = true; };
+        serviceConfig = (config.serviceConfig or {}) // {
+          ExecStart = mkTest serviceName testScript;
+          Type = "oneshot";
+        };
+      } // removeAttrs config [ "confinement" "serviceConfig" ];
+    };
+
+    parametrisedTests = lib.concatMap ({ user, privateTmp }: let
+      withTmp = if privateTmp then "with PrivateTmp" else "without PrivateTmp";
+
+      serviceConfig = if user == "static-user" then {
+        User = "chroot-testuser";
+        Group = "chroot-testgroup";
+      } else if user == "dynamic-user" then {
+        DynamicUser = true;
+      } else {};
+
+    in [
+      { description = "${user}, chroot-only confinement ${withTmp}";
+        config = {
+          confinement.mode = "chroot-only";
+          # Only set if privateTmp is true to ensure that the default is false.
+          serviceConfig = serviceConfig // lib.optionalAttrs privateTmp {
+            PrivateTmp = true;
+          };
+        };
+        testScript = if user == "root" then ''
+          assert os.getuid() == 0
+          assert os.getgid() == 0
+
+          assert_permissions({
+            'bin': Accessibility.READABLE,
+            'nix': Accessibility.READABLE,
+            'run': Accessibility.READABLE,
+            ${lib.optionalString privateTmp "'tmp': Accessibility.STICKY,"}
+            ${lib.optionalString privateTmp "'var': Accessibility.READABLE,"}
+            ${lib.optionalString privateTmp "'var/tmp': Accessibility.STICKY,"}
+          })
+        '' else ''
+          assert os.getuid() != 0
+          assert os.getgid() != 0
+
+          assert_permissions({
+            'bin': Accessibility.READABLE,
+            'nix': Accessibility.READABLE,
+            'run': Accessibility.READABLE,
+            ${lib.optionalString privateTmp "'tmp': Accessibility.STICKY,"}
+            ${lib.optionalString privateTmp "'var': Accessibility.READABLE,"}
+            ${lib.optionalString privateTmp "'var/tmp': Accessibility.STICKY,"}
+          })
+        '';
+      }
+      { description = "${user}, full APIVFS confinement ${withTmp}";
+        config = {
+          # Only set if privateTmp is false to ensure that the default is true.
+          serviceConfig = serviceConfig // lib.optionalAttrs (!privateTmp) {
+            PrivateTmp = false;
+          };
+        };
+        testScript = if user == "root" then ''
+          assert os.getuid() == 0
+          assert os.getgid() == 0
+
+          assert_permissions({
+            'bin': Accessibility.READABLE,
+            'nix': Accessibility.READABLE,
+            ${lib.optionalString privateTmp "'tmp': Accessibility.STICKY,"}
+            'run': Accessibility.WRITABLE,
+
+            'proc': Accessibility.SPECIAL,
+            'sys': Accessibility.SPECIAL,
+            'dev': Accessibility.WRITABLE,
+
+            ${lib.optionalString privateTmp "'var': Accessibility.READABLE,"}
+            ${lib.optionalString privateTmp "'var/tmp': Accessibility.STICKY,"}
+          })
+        '' else ''
+          assert os.getuid() != 0
+          assert os.getgid() != 0
+
+          assert_permissions({
+            'bin': Accessibility.READABLE,
+            'nix': Accessibility.READABLE,
+            ${lib.optionalString privateTmp "'tmp': Accessibility.STICKY,"}
+            'run': Accessibility.STICKY,
+
+            'proc': Accessibility.SPECIAL,
+            'sys': Accessibility.SPECIAL,
+            'dev': Accessibility.SPECIAL,
+            'dev/shm': Accessibility.STICKY,
+            'dev/mqueue': Accessibility.STICKY,
+
+            ${lib.optionalString privateTmp "'var': Accessibility.READABLE,"}
+            ${lib.optionalString privateTmp "'var/tmp': Accessibility.STICKY,"}
+          })
+        '';
+      }
+    ]) (lib.cartesianProductOfSets {
+      user = [ "root" "dynamic-user" "static-user" ];
+      privateTmp = [ true false ];
+    });
+
+  in {
+    imports = lib.imap1 mkTestStep (parametrisedTests ++ [
+      { description = "existence of bind-mounted /etc";
+        config.serviceConfig.BindReadOnlyPaths = [ "/etc" ];
+        testScript = ''
+          assert Path('/etc/passwd').read_text()
+        '';
+      }
+      (let
+        symlink = pkgs.runCommand "symlink" {
+          target = pkgs.writeText "symlink-target" "got me";
+        } "ln -s \"$target\" \"$out\"";
+      in {
+        description = "check if symlinks are properly bind-mounted";
+        config.confinement.packages = lib.singleton symlink;
+        testScript = ''
+          assert Path('${symlink}').read_text() == 'got me'
+        '';
+      })
+      { description = "check if StateDirectory works";
+        config.serviceConfig.User = "chroot-testuser";
+        config.serviceConfig.Group = "chroot-testgroup";
+        config.serviceConfig.StateDirectory = "testme";
+
+        # We restart on purpose here since we want to check whether the state
+        # directory actually persists.
+        config.serviceConfig.Restart = "on-failure";
+        config.serviceConfig.RestartMode = "direct";
+
+        testScript = ''
+          assert not Path('/tmp/canary').exists()
+          Path('/tmp/canary').touch()
+
+          if (foo := Path('/var/lib/testme/foo')).exists():
+            assert Path('/var/lib/testme/foo').read_text() == 'works'
+          else:
+            Path('/var/lib/testme/foo').write_text('works')
+            print('<4>Exiting with failure to check persistence on restart.')
+            raise SystemExit(1)
+        '';
+      }
+      { description = "check if /bin/sh works";
+        testScript = ''
+          assert Path('/bin/sh').exists()
+
+          result = run(
+            ['/bin/sh', '-c', 'echo -n bar'],
+            capture_output=True,
+            check=True,
+          )
+          assert result.stdout == b'bar'
+        '';
+      }
+      { description = "check if suppressing /bin/sh works";
+        config.confinement.binSh = null;
+        testScript = ''
+          assert not Path('/bin/sh').exists()
+          with pytest.raises(FileNotFoundError):
+            run(['/bin/sh', '-c', 'echo foo'])
+        '';
+      }
+      { description = "check if we can set /bin/sh to something different";
+        config.confinement.binSh = "${pkgs.hello}/bin/hello";
+        testScript = ''
+          assert Path('/bin/sh').exists()
+          result = run(
+            ['/bin/sh', '-g', 'foo'],
+            capture_output=True,
+            check=True,
+          )
+          assert result.stdout == b'foo\n'
+        '';
+      }
+      { description = "check if only Exec* dependencies are included";
+        config.environment.FOOBAR = pkgs.writeText "foobar" "eek";
+        testScript = ''
+          with pytest.raises(FileNotFoundError):
+            Path(os.environ['FOOBAR']).read_text()
+        '';
+      }
+      { description = "check if fullUnit includes all dependencies";
+        config.environment.FOOBAR = pkgs.writeText "foobar" "eek";
+        config.confinement.fullUnit = true;
+        testScript = ''
+          assert Path(os.environ['FOOBAR']).read_text() == 'eek'
+        '';
+      }
+      { description = "check if shipped unit file still works";
+        config.confinement.mode = "chroot-only";
+        rawUnit = ''
+          [Service]
+          SystemCallFilter=~kill
+          SystemCallErrorNumber=ELOOP
+        '';
+        testScript = ''
+          with pytest.raises(OSError) as excinfo:
+            os.kill(os.getpid(), signal.SIGKILL)
+          assert excinfo.value.errno == errno.ELOOP
+        '';
+      }
+    ]);
+
+    config.users.groups.chroot-testgroup = {};
+    config.users.users.chroot-testuser = {
+      isSystemUser = true;
+      description = "Chroot Test User";
+      group = "chroot-testgroup";
+    };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("multi-user.target")
+  '';
+}
diff --git a/nixos/tests/systemd-initrd-modprobe.nix b/nixos/tests/systemd-initrd-modprobe.nix
index 0f93492176b44..e563552a645fd 100644
--- a/nixos/tests/systemd-initrd-modprobe.nix
+++ b/nixos/tests/systemd-initrd-modprobe.nix
@@ -4,21 +4,21 @@ import ./make-test-python.nix ({ lib, pkgs, ... }: {
   nodes.machine = { pkgs, ... }: {
     testing.initrdBackdoor = true;
     boot.initrd.systemd.enable = true;
-    boot.initrd.kernelModules = [ "loop" ]; # Load module in initrd.
+    boot.initrd.kernelModules = [ "tcp_hybla" ]; # Load module in initrd.
     boot.extraModprobeConfig = ''
-      options loop max_loop=42
+      options tcp_hybla rtt0=42
     '';
   };
 
   testScript = ''
     machine.wait_for_unit("initrd.target")
-    max_loop = machine.succeed("cat /sys/module/loop/parameters/max_loop")
-    assert int(max_loop) == 42, "Parameter should be respected for initrd kernel modules"
+    rtt = machine.succeed("cat /sys/module/tcp_hybla/parameters/rtt0")
+    assert int(rtt) == 42, "Parameter should be respected for initrd kernel modules"
 
     # Make sure it sticks in stage 2
     machine.switch_root()
     machine.wait_for_unit("multi-user.target")
-    max_loop = machine.succeed("cat /sys/module/loop/parameters/max_loop")
-    assert int(max_loop) == 42, "Parameter should be respected for initrd kernel modules"
+    rtt = machine.succeed("cat /sys/module/tcp_hybla/parameters/rtt0")
+    assert int(rtt) == 42, "Parameter should be respected for initrd kernel modules"
   '';
 })
diff --git a/nixos/tests/virtualbox.nix b/nixos/tests/virtualbox.nix
index 3c2a391233dbd..5fce3ba548123 100644
--- a/nixos/tests/virtualbox.nix
+++ b/nixos/tests/virtualbox.nix
@@ -98,7 +98,6 @@ let
     cfg = (import ../lib/eval-config.nix {
       system = if use64bitGuest then "x86_64-linux" else "i686-linux";
       modules = [
-        ../modules/profiles/minimal.nix
         (testVMConfig vmName vmScript)
       ];
     }).config;
diff --git a/nixos/tests/ydotool.nix b/nixos/tests/ydotool.nix
new file mode 100644
index 0000000000000..818ac6f2d50de
--- /dev/null
+++ b/nixos/tests/ydotool.nix
@@ -0,0 +1,115 @@
+import ./make-test-python.nix (
+  { pkgs, lib, ... }:
+  let
+    textInput = "This works.";
+    inputBoxText = "Enter input";
+    inputBox = pkgs.writeShellScript "zenity-input" ''
+      ${lib.getExe pkgs.gnome.zenity} --entry --text '${inputBoxText}:' > /tmp/output &
+    '';
+  in
+  {
+    name = "ydotool";
+
+    meta = {
+      maintainers = with lib.maintainers; [
+        OPNA2608
+        quantenzitrone
+      ];
+    };
+
+    nodes = {
+      headless =
+        { config, ... }:
+        {
+          imports = [ ./common/user-account.nix ];
+
+          users.users.alice.extraGroups = [ "ydotool" ];
+
+          programs.ydotool.enable = true;
+
+          services.getty.autologinUser = "alice";
+        };
+
+      x11 =
+        { config, ... }:
+        {
+          imports = [
+            ./common/user-account.nix
+            ./common/auto.nix
+            ./common/x11.nix
+          ];
+
+          users.users.alice.extraGroups = [ "ydotool" ];
+
+          programs.ydotool.enable = true;
+
+          test-support.displayManager.auto = {
+            enable = true;
+            user = "alice";
+          };
+
+          services.xserver.windowManager.dwm.enable = true;
+          services.displayManager.defaultSession = lib.mkForce "none+dwm";
+        };
+
+      wayland =
+        { config, ... }:
+        {
+          imports = [ ./common/user-account.nix ];
+
+          services.cage = {
+            enable = true;
+            user = "alice";
+          };
+
+          programs.ydotool.enable = true;
+
+          services.cage.program = inputBox;
+        };
+    };
+
+    enableOCR = true;
+
+    testScript =
+      { nodes, ... }:
+      ''
+        def as_user(cmd: str):
+          """
+          Return a shell command for running a shell command as a specific user.
+          """
+          return f"sudo -u alice -i {cmd}"
+
+        start_all()
+
+        # Headless
+        headless.wait_for_unit("multi-user.target")
+        headless.wait_for_text("alice")
+        headless.succeed(as_user("ydotool type 'echo ${textInput} > /tmp/output'")) # text input
+        headless.succeed(as_user("ydotool key 28:1 28:0")) # text input
+        headless.screenshot("headless_input")
+        headless.wait_for_file("/tmp/output")
+        headless.wait_until_succeeds("grep '${textInput}' /tmp/output") # text input
+
+        # X11
+        x11.wait_for_x()
+        x11.execute(as_user("${inputBox}"))
+        x11.wait_for_text("${inputBoxText}")
+        x11.succeed(as_user("ydotool type '${textInput}'")) # text input
+        x11.screenshot("x11_input")
+        x11.succeed(as_user("ydotool mousemove -a 400 110")) # mouse input
+        x11.succeed(as_user("ydotool click 0xC0")) # mouse input
+        x11.wait_for_file("/tmp/output")
+        x11.wait_until_succeeds("grep '${textInput}' /tmp/output") # text input
+
+        # Wayland
+        wayland.wait_for_unit("graphical.target")
+        wayland.wait_for_text("${inputBoxText}")
+        wayland.succeed("ydotool type '${textInput}'") # text input
+        wayland.screenshot("wayland_input")
+        wayland.succeed("ydotool mousemove -a 100 100") # mouse input
+        wayland.succeed("ydotool click 0xC0") # mouse input
+        wayland.wait_for_file("/tmp/output")
+        wayland.wait_until_succeeds("grep '${textInput}' /tmp/output") # text input
+      '';
+  }
+)
diff --git a/nixos/tests/your_spotify.nix b/nixos/tests/your_spotify.nix
new file mode 100644
index 0000000000000..a1fa0e459a8e1
--- /dev/null
+++ b/nixos/tests/your_spotify.nix
@@ -0,0 +1,33 @@
+import ./make-test-python.nix ({pkgs, ...}: {
+  name = "your_spotify";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [patrickdag];
+  };
+
+  nodes.machine = {
+    services.your_spotify = {
+      enable = true;
+      spotifySecretFile = pkgs.writeText "spotifySecretFile" "deadbeef";
+      settings = {
+        CLIENT_ENDPOINT = "http://localhost";
+        API_ENDPOINT = "http://localhost:3000";
+        SPOTIFY_PUBLIC = "beefdead";
+      };
+      enableLocalDB = true;
+      nginxVirtualHost = "localhost";
+    };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("your_spotify.service")
+
+    machine.wait_for_open_port(3000)
+    machine.wait_for_open_port(80)
+
+    out = machine.succeed("curl --fail -X GET 'http://localhost:3000/'")
+    assert "Hello !" in out
+
+    out = machine.succeed("curl --fail -X GET 'http://localhost:80/'")
+    assert "<title>Your Spotify</title>" in out
+  '';
+})