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/release-notes/rl-2405.section.md4
-rw-r--r--nixos/doc/manual/release-notes/rl-2411.section.md5
-rw-r--r--nixos/lib/systemd-network-units.nix5
-rw-r--r--nixos/lib/test-driver/default.nix13
-rw-r--r--nixos/modules/hardware/video/nvidia.nix27
-rw-r--r--nixos/modules/misc/locate.nix154
-rw-r--r--nixos/modules/module-list.nix4
-rw-r--r--nixos/modules/programs/shadow.nix239
-rw-r--r--nixos/modules/programs/wayland/hyprland.nix3
-rw-r--r--nixos/modules/security/krb5/default.nix18
-rw-r--r--nixos/modules/security/krb5/krb5-conf-format.nix73
-rw-r--r--nixos/modules/services/audio/navidrome.nix2
-rw-r--r--nixos/modules/services/desktops/espanso.nix1
-rw-r--r--nixos/modules/services/hardware/amdvlk.nix61
-rw-r--r--nixos/modules/services/hardware/power-profiles-daemon.nix6
-rw-r--r--nixos/modules/services/logging/journalwatch.nix4
-rw-r--r--nixos/modules/services/mail/public-inbox.nix2
-rw-r--r--nixos/modules/services/misc/jellyfin.nix2
-rw-r--r--nixos/modules/services/misc/mqtt2influxdb.nix3
-rw-r--r--nixos/modules/services/misc/pghero.nix142
-rw-r--r--nixos/modules/services/misc/renovate.nix153
-rw-r--r--nixos/modules/services/monitoring/alloy.nix80
-rw-r--r--nixos/modules/services/monitoring/loki.nix1
-rw-r--r--nixos/modules/services/monitoring/scrutiny.nix4
-rw-r--r--nixos/modules/services/networking/aria2.nix165
-rw-r--r--nixos/modules/services/networking/tailscale.nix23
-rw-r--r--nixos/modules/services/networking/wstunnel.nix198
-rw-r--r--nixos/modules/services/system/kerberos/default.nix84
-rw-r--r--nixos/modules/services/system/kerberos/heimdal.nix105
-rw-r--r--nixos/modules/services/system/kerberos/kerberos-server.md55
-rw-r--r--nixos/modules/services/system/kerberos/mit.nix78
-rw-r--r--nixos/modules/services/ttys/getty.nix3
-rw-r--r--nixos/modules/services/web-apps/freshrss.nix11
-rw-r--r--nixos/modules/services/x11/window-managers/qtile.nix39
-rw-r--r--nixos/modules/system/boot/initrd-ssh.nix2
-rw-r--r--nixos/modules/system/boot/networkd.nix20
-rw-r--r--nixos/modules/system/etc/build-composefs-dump.py35
-rw-r--r--nixos/modules/system/etc/etc.nix2
-rw-r--r--nixos/tests/activation/etc-overlay-immutable.nix4
-rw-r--r--nixos/tests/all-tests.nix7
-rw-r--r--nixos/tests/alloy.nix32
-rw-r--r--nixos/tests/aria2.nix43
-rw-r--r--nixos/tests/clatd.nix95
-rw-r--r--nixos/tests/firefly-iii.nix4
-rw-r--r--nixos/tests/greetd-no-shadow.nix49
-rw-r--r--nixos/tests/kerberos/heimdal.nix2
-rw-r--r--nixos/tests/kerberos/mit.nix2
-rw-r--r--nixos/tests/kubo/default.nix4
-rw-r--r--nixos/tests/kubo/kubo-fuse.nix2
-rw-r--r--nixos/tests/pghero.nix63
-rw-r--r--nixos/tests/qtile.nix2
-rw-r--r--nixos/tests/renovate.nix69
-rw-r--r--nixos/tests/vaultwarden.nix264
53 files changed, 1723 insertions, 745 deletions
diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md
index 165e3e13a78ef..aa0f035cdbb13 100644
--- a/nixos/doc/manual/release-notes/rl-2405.section.md
+++ b/nixos/doc/manual/release-notes/rl-2405.section.md
@@ -534,6 +534,10 @@ Use `services.pipewire.extraConfig` or `services.pipewire.configPackages` for Pi
   - to get previous behaviour of upstream defaults, set it to `null`
   - default value has changed from `[]` to `null`, in order to preserve default behaviour
 
+- `androidenv.androidPkgs_9_0` has been removed, and replaced with `androidenv.androidPkgs` for a more complete Android SDK including support for Android 9 and later.
+
+- `gtest` package has been updated past v1.13.0, which requires C++14 or higher.
+
 - `services.vikunja` systemd service now uses `vikunja` as dynamic user instead of `vikunja-api`. Database users might need to be changed.
 
 - `services.vikunja.setupNginx` setting has been removed. Users now need to set up the webserver configuration on their own with a proxy pass to the vikunja service.
diff --git a/nixos/doc/manual/release-notes/rl-2411.section.md b/nixos/doc/manual/release-notes/rl-2411.section.md
index 6c92a56592c2e..47e89f6449960 100644
--- a/nixos/doc/manual/release-notes/rl-2411.section.md
+++ b/nixos/doc/manual/release-notes/rl-2411.section.md
@@ -4,7 +4,8 @@
 
 ## Highlights {#sec-release-24.11-highlights}
 
-- Create the first release note entry in this section!
+- [AMDVLK](https://github.com/GPUOpen-Drivers/AMDVLK), AMD's open source Vulkan driver, is now available to be configured as `hardware.amdgpu.amdvlk` option.
+  This also allows configuring runtime settings of AMDVLK and enabling experimental features.
 
 ## New Services {#sec-release-24.11-new-services}
 
@@ -14,6 +15,8 @@
 
 - [Quickwit](https://quickwit.io), sub-second search & analytics engine on cloud storage. Available as [services.quickwit](options.html#opt-services.quickwit).
 
+- [Renovate](https://github.com/renovatebot/renovate), a dependency updating tool for various git forges and language ecosystems. Available as [services.renovate](#opt-services.renovate.enable).
+
 ## Backward Incompatibilities {#sec-release-24.11-incompatibilities}
 
 - `nginx` package no longer includes `gd` and `geoip` dependencies. For enabling it, override `nginx` package with the optionals `withImageFilter` and `withGeoIP`.
diff --git a/nixos/lib/systemd-network-units.nix b/nixos/lib/systemd-network-units.nix
index d15485240bd0a..c35309a6d2628 100644
--- a/nixos/lib/systemd-network-units.nix
+++ b/nixos/lib/systemd-network-units.nix
@@ -147,7 +147,10 @@ in {
     '' + optionalString (def.ipv6SendRAConfig != { }) ''
       [IPv6SendRA]
       ${attrsToSection def.ipv6SendRAConfig}
-    '' + flip concatMapStrings def.ipv6Prefixes (x: ''
+    '' + flip concatMapStrings def.ipv6PREF64Prefixes (x: ''
+      [IPv6PREF64Prefix]
+      ${attrsToSection x}
+    '') + flip concatMapStrings def.ipv6Prefixes (x: ''
       [IPv6Prefix]
       ${attrsToSection x}
     '') + flip concatMapStrings def.ipv6RoutePrefixes (x: ''
diff --git a/nixos/lib/test-driver/default.nix b/nixos/lib/test-driver/default.nix
index 7a88694b3167e..26652db6016e6 100644
--- a/nixos/lib/test-driver/default.nix
+++ b/nixos/lib/test-driver/default.nix
@@ -13,11 +13,20 @@
 , extraPythonPackages ? (_ : [])
 , nixosTests
 }:
-
+let
+  fs = lib.fileset;
+in
 python3Packages.buildPythonApplication {
   pname = "nixos-test-driver";
   version = "1.1";
-  src = ./.;
+  src = fs.toSource {
+    root = ./.;
+    fileset = fs.unions [
+      ./pyproject.toml
+      ./test_driver
+      ./extract-docstrings.py
+    ];
+  };
   pyproject = true;
 
   propagatedBuildInputs = [
diff --git a/nixos/modules/hardware/video/nvidia.nix b/nixos/modules/hardware/video/nvidia.nix
index f866bbe17643c..ae5c2aa7a034a 100644
--- a/nixos/modules/hardware/video/nvidia.nix
+++ b/nixos/modules/hardware/video/nvidia.nix
@@ -206,6 +206,19 @@ in
         option is supported is used
       '';
 
+      prime.reverseSync.setupCommands.enable =
+        (lib.mkEnableOption ''
+          configure the display manager to be able to use the outputs
+          attached to the NVIDIA GPU.
+          Disable in order to configure the NVIDIA GPU outputs manually using xrandr.
+          Note that this configuration will only be successful when a display manager
+          for which the {option}`services.xserver.displayManager.setupCommands`
+          option is supported is used
+        '')
+        // {
+          default = true;
+        };
+
       nvidiaSettings =
         (lib.mkEnableOption ''
           nvidia-settings, NVIDIA's GUI configuration tool
@@ -275,7 +288,7 @@ in
               softdep nvidia post: nvidia-uvm
             '';
           };
-          systemd.tmpfiles.rules = lib.mkIf config.virtualisation.docker.enableNvidia "L+ /run/nvidia-docker/bin - - - - ${nvidia_x11.bin}/origBin";
+          systemd.tmpfiles.rules = lib.mkIf config.virtualisation.docker.enableNvidia [ "L+ /run/nvidia-docker/bin - - - - ${nvidia_x11.bin}/origBin" ];
           services.udev.extraRules = ''
             # Create /dev/nvidia-uvm when the nvidia-uvm module is loaded.
             KERNEL=="nvidia", RUN+="${pkgs.runtimeShell} -c 'mknod -m 666 /dev/nvidiactl c 195 255'"
@@ -437,11 +450,13 @@ in
               providerCmdParams =
                 if syncCfg.enable then "\"${gpuProviderName}\" NVIDIA-0" else "NVIDIA-G0 \"${gpuProviderName}\"";
             in
-            lib.optionalString (syncCfg.enable || reverseSyncCfg.enable) ''
-              # Added by nvidia configuration module for Optimus/PRIME.
-              ${lib.getExe pkgs.xorg.xrandr} --setprovideroutputsource ${providerCmdParams}
-              ${lib.getExe pkgs.xorg.xrandr} --auto
-            '';
+            lib.optionalString
+              (syncCfg.enable || (reverseSyncCfg.enable && reverseSyncCfg.setupCommands.enable))
+              ''
+                # Added by nvidia configuration module for Optimus/PRIME.
+                ${lib.getExe pkgs.xorg.xrandr} --setprovideroutputsource ${providerCmdParams}
+                ${lib.getExe pkgs.xorg.xrandr} --auto
+              '';
 
           environment.etc = {
             "nvidia/nvidia-application-profiles-rc" = lib.mkIf nvidia_x11.useProfiles {
diff --git a/nixos/modules/misc/locate.nix b/nixos/modules/misc/locate.nix
index 84c711c2b4efa..0e9adefff5e1e 100644
--- a/nixos/modules/misc/locate.nix
+++ b/nixos/modules/misc/locate.nix
@@ -1,24 +1,22 @@
 { config, lib, pkgs, ... }:
 
-with lib;
-
 let
   cfg = config.services.locate;
-  isMLocate = hasPrefix "mlocate" cfg.package.name;
-  isPLocate = hasPrefix "plocate" cfg.package.name;
+  isMLocate = lib.hasPrefix "mlocate" cfg.package.name;
+  isPLocate = lib.hasPrefix "plocate" cfg.package.name;
   isMorPLocate = isMLocate || isPLocate;
-  isFindutils = hasPrefix "findutils" cfg.package.name;
+  isFindutils = lib.hasPrefix "findutils" cfg.package.name;
 in
 {
   imports = [
-    (mkRenamedOptionModule [ "services" "locate" "period" ] [ "services" "locate" "interval" ])
-    (mkRenamedOptionModule [ "services" "locate" "locate" ] [ "services" "locate" "package" ])
-    (mkRemovedOptionModule [ "services" "locate" "includeStore" ] "Use services.locate.prunePaths")
+    (lib.mkRenamedOptionModule [ "services" "locate" "period" ] [ "services" "locate" "interval" ])
+    (lib.mkRenamedOptionModule [ "services" "locate" "locate" ] [ "services" "locate" "package" ])
+    (lib.mkRemovedOptionModule [ "services" "locate" "includeStore" ] "Use services.locate.prunePaths")
   ];
 
-  options.services.locate = with types; {
-    enable = mkOption {
-      type = bool;
+  options.services.locate = {
+    enable = lib.mkOption {
+      type = lib.types.bool;
       default = false;
       description = ''
         If enabled, NixOS will periodically update the database of
@@ -26,12 +24,12 @@ in
       '';
     };
 
-    package = mkPackageOption pkgs [ "findutils" "locate" ] {
+    package = lib.mkPackageOption pkgs [ "findutils" "locate" ] {
       example = "mlocate";
     };
 
-    interval = mkOption {
-      type = str;
+    interval = lib.mkOption {
+      type = lib.types.str;
       default = "02:15";
       example = "hourly";
       description = ''
@@ -46,24 +44,24 @@ in
       '';
     };
 
-    extraFlags = mkOption {
-      type = listOf str;
+    extraFlags = lib.mkOption {
+      type = lib.types.listOf lib.types.str;
       default = [ ];
       description = ''
         Extra flags to pass to {command}`updatedb`.
       '';
     };
 
-    output = mkOption {
-      type = path;
+    output = lib.mkOption {
+      type = lib.types.path;
       default = "/var/cache/locatedb";
       description = ''
         The database file to build.
       '';
     };
 
-    localuser = mkOption {
-      type = nullOr str;
+    localuser = lib.mkOption {
+      type = lib.types.nullOr lib.types.str;
       default = "nobody";
       description = ''
         The user to search non-network directories as, using
@@ -71,8 +69,8 @@ in
       '';
     };
 
-    pruneFS = mkOption {
-      type = listOf str;
+    pruneFS = lib.mkOption {
+      type = lib.types.listOf lib.types.str;
       default = [
         "afs"
         "anon_inodefs"
@@ -158,8 +156,8 @@ in
       '';
     };
 
-    prunePaths = mkOption {
-      type = listOf path;
+    prunePaths = lib.mkOption {
+      type = lib.types.listOf lib.types.path;
       default = [
         "/tmp"
         "/var/tmp"
@@ -175,10 +173,10 @@ in
       '';
     };
 
-    pruneNames = mkOption {
-      type = listOf str;
+    pruneNames = lib.mkOption {
+      type = lib.types.listOf lib.types.str;
       default = lib.optionals (!isFindutils) [ ".bzr" ".cache" ".git" ".hg" ".svn" ];
-      defaultText = literalMD ''
+      defaultText = lib.literalMD ''
         `[ ".bzr" ".cache" ".git" ".hg" ".svn" ]`, if
         supported by the locate implementation (i.e. mlocate or plocate).
       '';
@@ -187,8 +185,8 @@ in
       '';
     };
 
-    pruneBindMounts = mkOption {
-      type = bool;
+    pruneBindMounts = lib.mkOption {
+      type = lib.types.bool;
       default = false;
       description = ''
         Whether not to index bind mounts
@@ -197,10 +195,10 @@ in
 
   };
 
-  config = mkIf cfg.enable {
-    users.groups = mkMerge [
-      (mkIf isMLocate { mlocate = { }; })
-      (mkIf isPLocate { plocate = { }; })
+  config = lib.mkIf cfg.enable {
+    users.groups = lib.mkMerge [
+      (lib.mkIf isMLocate { mlocate = { }; })
+      (lib.mkIf isPLocate { plocate = { }; })
     ];
 
     security.wrappers =
@@ -211,46 +209,46 @@ in
           setgid = true;
           setuid = false;
         };
-        mlocate = mkIf isMLocate {
+        mlocate = lib.mkIf isMLocate {
           group = "mlocate";
           source = "${cfg.package}/bin/locate";
         };
-        plocate = mkIf isPLocate {
+        plocate = lib.mkIf isPLocate {
           group = "plocate";
           source = "${cfg.package}/bin/plocate";
         };
       in
-      mkIf isMorPLocate {
-        locate = mkMerge [ common mlocate plocate ];
-        plocate = mkIf isPLocate (mkMerge [ common plocate ]);
+      lib.mkIf isMorPLocate {
+        locate = lib.mkMerge [ common mlocate plocate ];
+        plocate = lib.mkIf isPLocate (lib.mkMerge [ common plocate ]);
       };
 
-    environment.systemPackages = [ cfg.package ];
+    environment = {
+      # write /etc/updatedb.conf for manual calls to `updatedb`
+      etc."updatedb.conf".text = ''
+        PRUNEFS="${lib.concatStringsSep " " cfg.pruneFS}"
+        PRUNENAMES="${lib.concatStringsSep " " cfg.pruneNames}"
+        PRUNEPATHS="${lib.concatStringsSep " " cfg.prunePaths}"
+        PRUNE_BIND_MOUNTS="${if cfg.pruneBindMounts then "yes" else "no"}"
+      '';
 
-    environment.variables.LOCATE_PATH = cfg.output;
+      systemPackages = [ cfg.package ];
 
-    environment.etc = {
-      # write /etc/updatedb.conf for manual calls to `updatedb`
-      "updatedb.conf" = {
-        text = ''
-          PRUNEFS="${lib.concatStringsSep " " cfg.pruneFS}"
-          PRUNENAMES="${lib.concatStringsSep " " cfg.pruneNames}"
-          PRUNEPATHS="${lib.concatStringsSep " " cfg.prunePaths}"
-          PRUNE_BIND_MOUNTS="${if cfg.pruneBindMounts then "yes" else "no"}"
-        '';
+      variables = lib.mkIf isFindutils {
+        LOCATE_PATH = cfg.output;
       };
     };
 
-    warnings = optional (isMorPLocate && cfg.localuser != null)
+    warnings = lib.optional (isMorPLocate && cfg.localuser != null)
       "mlocate and plocate do not support the services.locate.localuser option. updatedb will run as root. Silence this warning by setting services.locate.localuser = null."
-    ++ optional (isFindutils && cfg.pruneNames != [ ])
+    ++ lib.optional (isFindutils && cfg.pruneNames != [ ])
       "findutils locate does not support pruning by directory component"
-    ++ optional (isFindutils && cfg.pruneBindMounts)
+    ++ lib.optional (isFindutils && cfg.pruneBindMounts)
       "findutils locate does not support skipping bind mounts";
 
     systemd.services.update-locatedb = {
       description = "Update Locate Database";
-      path = mkIf (!isMorPLocate) [ pkgs.su ];
+      path = lib.mkIf (!isMorPLocate) [ pkgs.su ];
 
       # mlocate's updatedb takes flags via a configuration file or
       # on the command line, but not by environment variable.
@@ -258,42 +256,44 @@ in
         if isMorPLocate then
           let
             toFlags = x:
-              optional (cfg.${x} != [ ])
-                "--${lib.toLower x} '${concatStringsSep " " cfg.${x}}'";
-            args = concatLists (map toFlags [ "pruneFS" "pruneNames" "prunePaths" ]);
+              lib.optional (cfg.${x} != [ ])
+                "--${lib.toLower x} '${lib.concatStringsSep " " cfg.${x}}'";
+            args = lib.concatLists (map toFlags [ "pruneFS" "pruneNames" "prunePaths" ]);
           in
           ''
             exec ${cfg.package}/bin/updatedb \
-              --output ${toString cfg.output} ${concatStringsSep " " args} \
+              --output ${toString cfg.output} ${lib.concatStringsSep " " args} \
               --prune-bind-mounts ${if cfg.pruneBindMounts then "yes" else "no"} \
-              ${concatStringsSep " " cfg.extraFlags}
+              ${lib.concatStringsSep " " cfg.extraFlags}
           ''
         else ''
           exec ${cfg.package}/bin/updatedb \
-            ${optionalString (cfg.localuser != null && !isMorPLocate) "--localuser=${cfg.localuser}"} \
-            --output=${toString cfg.output} ${concatStringsSep " " cfg.extraFlags}
+            ${lib.optionalString (cfg.localuser != null && !isMorPLocate) "--localuser=${cfg.localuser}"} \
+            --output=${toString cfg.output} ${lib.concatStringsSep " " cfg.extraFlags}
         '';
-      environment = optionalAttrs (!isMorPLocate) {
-        PRUNEFS = concatStringsSep " " cfg.pruneFS;
-        PRUNEPATHS = concatStringsSep " " cfg.prunePaths;
-        PRUNENAMES = concatStringsSep " " cfg.pruneNames;
+      environment = lib.optionalAttrs (!isMorPLocate) {
+        PRUNEFS = lib.concatStringsSep " " cfg.pruneFS;
+        PRUNEPATHS = lib.concatStringsSep " " cfg.prunePaths;
+        PRUNENAMES = lib.concatStringsSep " " cfg.pruneNames;
         PRUNE_BIND_MOUNTS = if cfg.pruneBindMounts then "yes" else "no";
       };
-      serviceConfig.Nice = 19;
-      serviceConfig.IOSchedulingClass = "idle";
-      serviceConfig.PrivateTmp = "yes";
-      serviceConfig.PrivateNetwork = "yes";
-      serviceConfig.NoNewPrivileges = "yes";
-      serviceConfig.ReadOnlyPaths = "/";
-      # Use dirOf cfg.output because mlocate creates temporary files next to
-      # the actual database. We could specify and create them as well,
-      # but that would make this quite brittle when they change something.
-      # NOTE: If /var/cache does not exist, this leads to the misleading error message:
-      # update-locatedb.service: Failed at step NAMESPACE spawning …/update-locatedb-start: No such file or directory
-      serviceConfig.ReadWritePaths = dirOf cfg.output;
+      serviceConfig = {
+        Nice = 19;
+        IOSchedulingClass = "idle";
+        PrivateTmp = "yes";
+        PrivateNetwork = "yes";
+        NoNewPrivileges = "yes";
+        ReadOnlyPaths = "/";
+        # Use dirOf cfg.output because mlocate creates temporary files next to
+        # the actual database. We could specify and create them as well,
+        # but that would make this quite brittle when they change something.
+        # NOTE: If /var/cache does not exist, this leads to the misleading error message:
+        # update-locatedb.service: Failed at step NAMESPACE spawning …/update-locatedb-start: No such file or directory
+        ReadWritePaths = dirOf cfg.output;
+      };
     };
 
-    systemd.timers.update-locatedb = mkIf (cfg.interval != "never") {
+    systemd.timers.update-locatedb = lib.mkIf (cfg.interval != "never") {
       description = "Update timer for locate database";
       partOf = [ "update-locatedb.service" ];
       wantedBy = [ "timers.target" ];
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 745dae2ef7247..b20e98a9f229b 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -549,6 +549,7 @@
   ./services/games/xonotic.nix
   ./services/hardware/acpid.nix
   ./services/hardware/actkbd.nix
+  ./services/hardware/amdvlk.nix
   ./services/hardware/argonone.nix
   ./services/hardware/asusd.nix
   ./services/hardware/auto-cpufreq.nix
@@ -779,6 +780,7 @@
   ./services/misc/paperless.nix
   ./services/misc/parsoid.nix
   ./services/misc/persistent-evdev.nix
+  ./services/misc/pghero.nix
   ./services/misc/pinnwand.nix
   ./services/misc/plex.nix
   ./services/misc/plikd.nix
@@ -793,6 +795,7 @@
   ./services/misc/radarr.nix
   ./services/misc/readarr.nix
   ./services/misc/redmine.nix
+  ./services/misc/renovate.nix
   ./services/misc/ripple-data-api.nix
   ./services/misc/rippled.nix
   ./services/misc/rmfakecloud.nix
@@ -835,6 +838,7 @@
   ./services/misc/zoneminder.nix
   ./services/misc/zookeeper.nix
   ./services/monitoring/alerta.nix
+  ./services/monitoring/alloy.nix
   ./services/monitoring/apcupsd.nix
   ./services/monitoring/arbtt.nix
   ./services/monitoring/below.nix
diff --git a/nixos/modules/programs/shadow.nix b/nixos/modules/programs/shadow.nix
index f09bfaa5393d7..ef5bad69e934e 100644
--- a/nixos/modules/programs/shadow.nix
+++ b/nixos/modules/programs/shadow.nix
@@ -4,7 +4,18 @@ let
   cfg = config.security.loginDefs;
 in
 {
-  options = with lib.types; {
+  options = {
+
+    security.shadow.enable = lib.mkEnableOption "" // {
+      default = true;
+      description = ''
+        Enable the shadow authentication suite, which provides critical programs such as su, login, passwd.
+
+        Note: This is currently experimental. Only disable this if you're
+        confident that you can recover your system if it breaks.
+      '';
+    };
+
     security.loginDefs = {
       package = lib.mkPackageOption pkgs "shadow" { };
 
@@ -12,7 +23,7 @@ in
         description = ''
           Use chfn SUID to allow non-root users to change their account GECOS information.
         '';
-        type = nullOr str;
+        type = lib.types.nullOr lib.types.str;
         default = null;
       };
 
@@ -22,7 +33,7 @@ in
           the site-specific configuration for the shadow password suite.
           See login.defs(5) man page for available options.
         '';
-        type = submodule {
+        type = lib.types.submodule {
           freeformType = (pkgs.formats.keyValue { }).type;
           /* There are three different sources for user/group id ranges, each of which gets
              used by different programs:
@@ -37,62 +48,62 @@ in
             DEFAULT_HOME = lib.mkOption {
               description = "Indicate if login is allowed if we can't cd to the home directory.";
               default = "yes";
-              type = enum [ "yes" "no" ];
+              type = lib.types.enum [ "yes" "no" ];
             };
 
             ENCRYPT_METHOD = lib.mkOption {
               description = "This defines the system default encryption algorithm for encrypting passwords.";
               # The default crypt() method, keep in sync with the PAM default
               default = "YESCRYPT";
-              type = enum [ "YESCRYPT" "SHA512" "SHA256" "MD5" "DES"];
+              type = lib.types.enum [ "YESCRYPT" "SHA512" "SHA256" "MD5" "DES"];
             };
 
             SYS_UID_MIN = lib.mkOption {
               description = "Range of user IDs used for the creation of system users by useradd or newusers.";
               default = 400;
-              type = int;
+              type = lib.types.int;
             };
 
             SYS_UID_MAX = lib.mkOption {
               description = "Range of user IDs used for the creation of system users by useradd or newusers.";
               default = 999;
-              type = int;
+              type = lib.types.int;
             };
 
             UID_MIN = lib.mkOption {
               description = "Range of user IDs used for the creation of regular users by useradd or newusers.";
               default = 1000;
-              type = int;
+              type = lib.types.int;
             };
 
             UID_MAX = lib.mkOption {
               description = "Range of user IDs used for the creation of regular users by useradd or newusers.";
               default = 29999;
-              type = int;
+              type = lib.types.int;
             };
 
             SYS_GID_MIN = lib.mkOption {
               description = "Range of group IDs used for the creation of system groups by useradd, groupadd, or newusers";
               default = 400;
-              type = int;
+              type = lib.types.int;
             };
 
             SYS_GID_MAX = lib.mkOption {
               description = "Range of group IDs used for the creation of system groups by useradd, groupadd, or newusers";
               default = 999;
-              type = int;
+              type = lib.types.int;
             };
 
             GID_MIN = lib.mkOption {
               description = "Range of group IDs used for the creation of regular groups by useradd, groupadd, or newusers.";
               default = 1000;
-              type = int;
+              type = lib.types.int;
             };
 
             GID_MAX = lib.mkOption {
               description = "Range of group IDs used for the creation of regular groups by useradd, groupadd, or newusers.";
               default = 29999;
-              type = int;
+              type = lib.types.int;
             };
 
             TTYGROUP = lib.mkOption {
@@ -100,7 +111,7 @@ in
                 The terminal permissions: the login tty will be owned by the TTYGROUP group,
                 and the permissions will be set to TTYPERM'';
               default = "tty";
-              type = str;
+              type = lib.types.str;
             };
 
             TTYPERM = lib.mkOption {
@@ -108,14 +119,14 @@ in
                 The terminal permissions: the login tty will be owned by the TTYGROUP group,
                 and the permissions will be set to TTYPERM'';
               default = "0620";
-              type = str;
+              type = lib.types.str;
             };
 
             # Ensure privacy for newly created home directories.
             UMASK = lib.mkOption {
               description = "The file mode creation mask is initialized to this value.";
               default = "077";
-              type = str;
+              type = lib.types.str;
             };
           };
         };
@@ -132,107 +143,115 @@ in
         used outside the store (in particular in /etc/passwd).
       '';
       example = lib.literalExpression "pkgs.zsh";
-      type = either path shellPackage;
+      type = lib.types.either lib.types.path lib.types.shellPackage;
     };
   };
 
   ###### implementation
 
-  config = {
-    assertions = [
-      {
-        assertion = cfg.settings.SYS_UID_MIN <= cfg.settings.SYS_UID_MAX;
-        message = "SYS_UID_MIN must be less than or equal to SYS_UID_MAX";
-      }
-      {
-        assertion = cfg.settings.UID_MIN <= cfg.settings.UID_MAX;
-        message = "UID_MIN must be less than or equal to UID_MAX";
-      }
-      {
-        assertion = cfg.settings.SYS_GID_MIN <= cfg.settings.SYS_GID_MAX;
-        message = "SYS_GID_MIN must be less than or equal to SYS_GID_MAX";
-      }
-      {
-        assertion = cfg.settings.GID_MIN <= cfg.settings.GID_MAX;
-        message = "GID_MIN must be less than or equal to GID_MAX";
-      }
-    ];
-
-    security.loginDefs.settings.CHFN_RESTRICT =
-      lib.mkIf (cfg.chfnRestrict != null) cfg.chfnRestrict;
-
-    environment.systemPackages = lib.optional config.users.mutableUsers cfg.package
-      ++ lib.optional (lib.types.shellPackage.check config.users.defaultUserShell) config.users.defaultUserShell
-      ++ lib.optional (cfg.chfnRestrict != null) pkgs.util-linux;
-
-    environment.etc =
-      # Create custom toKeyValue generator
-      # see https://man7.org/linux/man-pages/man5/login.defs.5.html for config specification
-      let
-        toKeyValue = lib.generators.toKeyValue {
-          mkKeyValue = lib.generators.mkKeyValueDefault { } " ";
-        };
-      in
-      {
-        # /etc/login.defs: global configuration for pwdutils.
-        # You cannot login without it!
-        "login.defs".source = pkgs.writeText "login.defs" (toKeyValue cfg.settings);
-
-        # /etc/default/useradd: configuration for useradd.
-        "default/useradd".source = pkgs.writeText "useradd" ''
-          GROUP=100
-          HOME=/home
-          SHELL=${utils.toShellPath config.users.defaultUserShell}
-        '';
-      };
+  config = lib.mkMerge [
+    {
+      assertions = [
+        {
+          assertion = config.security.shadow.enable || config.services.greetd.enable;
+          message = "You must enable at least one VT login method, either security.shadow.enable or services.greetd.enable";
+        }
+      ];
+    }
+    (lib.mkIf config.security.shadow.enable {
+      assertions = [
+        {
+          assertion = cfg.settings.SYS_UID_MIN <= cfg.settings.SYS_UID_MAX;
+          message = "SYS_UID_MIN must be less than or equal to SYS_UID_MAX";
+        }
+        {
+          assertion = cfg.settings.UID_MIN <= cfg.settings.UID_MAX;
+          message = "UID_MIN must be less than or equal to UID_MAX";
+        }
+        {
+          assertion = cfg.settings.SYS_GID_MIN <= cfg.settings.SYS_GID_MAX;
+          message = "SYS_GID_MIN must be less than or equal to SYS_GID_MAX";
+        }
+        {
+          assertion = cfg.settings.GID_MIN <= cfg.settings.GID_MAX;
+          message = "GID_MIN must be less than or equal to GID_MAX";
+        }
+      ];
 
-    security.pam.services = {
-      chsh = { rootOK = true; };
-      chfn = { rootOK = true; };
-      su = {
-        rootOK = true;
-        forwardXAuth = true;
-        logFailures = true;
-      };
-      passwd = { };
-      # Note: useradd, groupadd etc. aren't setuid root, so it
-      # doesn't really matter what the PAM config says as long as it
-      # lets root in.
-      useradd.rootOK = true;
-      usermod.rootOK = true;
-      userdel.rootOK = true;
-      groupadd.rootOK = true;
-      groupmod.rootOK = true;
-      groupmems.rootOK = true;
-      groupdel.rootOK = true;
-      login = {
-        startSession = true;
-        allowNullPassword = true;
-        showMotd = true;
-        updateWtmp = true;
-      };
-      chpasswd = { rootOK = true; };
-    };
+      security.loginDefs.settings.CHFN_RESTRICT = lib.mkIf (cfg.chfnRestrict != null) cfg.chfnRestrict;
+
+      environment.systemPackages = lib.optional config.users.mutableUsers cfg.package
+        ++ lib.optional (lib.types.shellPackage.check config.users.defaultUserShell) config.users.defaultUserShell
+        ++ lib.optional (cfg.chfnRestrict != null) pkgs.util-linux;
+
+      environment.etc =
+        # Create custom toKeyValue generator
+        # see https://man7.org/linux/man-pages/man5/login.defs.5.html for config specification
+        let
+          toKeyValue = lib.generators.toKeyValue {
+            mkKeyValue = lib.generators.mkKeyValueDefault { } " ";
+          };
+        in {
+          # /etc/login.defs: global configuration for pwdutils.
+          # You cannot login without it!
+          "login.defs".source = pkgs.writeText "login.defs" (toKeyValue cfg.settings);
+
+          # /etc/default/useradd: configuration for useradd.
+          "default/useradd".source = pkgs.writeText "useradd" ''
+            GROUP=100
+            HOME=/home
+            SHELL=${utils.toShellPath config.users.defaultUserShell}
+          '';
+        };
 
-    security.wrappers =
-      let
-        mkSetuidRoot = source: {
-          setuid = true;
-          owner = "root";
-          group = "root";
-          inherit source;
+      security.pam.services = {
+        chsh.rootOK = true;
+        chfn.rootOK = true;
+        su = {
+          rootOK = true;
+          forwardXAuth = true;
+          logFailures = true;
         };
-      in
-      {
-        su = mkSetuidRoot "${cfg.package.su}/bin/su";
-        sg = mkSetuidRoot "${cfg.package.out}/bin/sg";
-        newgrp = mkSetuidRoot "${cfg.package.out}/bin/newgrp";
-        newuidmap = mkSetuidRoot "${cfg.package.out}/bin/newuidmap";
-        newgidmap = mkSetuidRoot "${cfg.package.out}/bin/newgidmap";
-      }
-      // lib.optionalAttrs config.users.mutableUsers {
-        chsh = mkSetuidRoot "${cfg.package.out}/bin/chsh";
-        passwd = mkSetuidRoot "${cfg.package.out}/bin/passwd";
+        passwd = { };
+        # Note: useradd, groupadd etc. aren't setuid root, so it
+        # doesn't really matter what the PAM config says as long as it
+        # lets root in.
+        useradd.rootOK = true;
+        usermod.rootOK = true;
+        userdel.rootOK = true;
+        groupadd.rootOK = true;
+        groupmod.rootOK = true;
+        groupmems.rootOK = true;
+        groupdel.rootOK = true;
+        login = {
+          startSession = true;
+          allowNullPassword = true;
+          showMotd = true;
+          updateWtmp = true;
+        };
+        chpasswd.rootOK = true;
       };
-  };
+
+      security.wrappers =
+        let
+          mkSetuidRoot = source: {
+            setuid = true;
+            owner = "root";
+            group = "root";
+            inherit source;
+          };
+        in
+          {
+            su = mkSetuidRoot "${cfg.package.su}/bin/su";
+            sg = mkSetuidRoot "${cfg.package.out}/bin/sg";
+            newgrp = mkSetuidRoot "${cfg.package.out}/bin/newgrp";
+            newuidmap = mkSetuidRoot "${cfg.package.out}/bin/newuidmap";
+            newgidmap = mkSetuidRoot "${cfg.package.out}/bin/newgidmap";
+          }
+          // lib.optionalAttrs config.users.mutableUsers {
+            chsh = mkSetuidRoot "${cfg.package.out}/bin/chsh";
+            passwd = mkSetuidRoot "${cfg.package.out}/bin/passwd";
+          };
+    })
+  ];
 }
diff --git a/nixos/modules/programs/wayland/hyprland.nix b/nixos/modules/programs/wayland/hyprland.nix
index 98779ea7d03a3..5a21bd153b632 100644
--- a/nixos/modules/programs/wayland/hyprland.nix
+++ b/nixos/modules/programs/wayland/hyprland.nix
@@ -56,6 +56,7 @@ in
       services.displayManager.sessionPackages = [ cfg.package ];
 
       xdg.portal = {
+        enable = true;
         extraPortals = [ cfg.portalPackage ];
         configPackages = lib.mkDefault [ cfg.package ];
       };
@@ -70,7 +71,7 @@ in
     (import ./wayland-session.nix {
       inherit lib pkgs;
       enableXWayland = cfg.xwayland.enable;
-      enableWlrPortal = false; # Hyprland has its own portal, wlr is not needed
+      enableWlrPortal = lib.mkDefault false; # Hyprland has its own portal, wlr is not needed
     })
   ]);
 
diff --git a/nixos/modules/security/krb5/default.nix b/nixos/modules/security/krb5/default.nix
index 78426c07cbc98..6714c41d8a07c 100644
--- a/nixos/modules/security/krb5/default.nix
+++ b/nixos/modules/security/krb5/default.nix
@@ -77,8 +77,22 @@ in {
     };
   };
 
-  config = mkIf cfg.enable {
-    environment = {
+  config = {
+    assertions = mkIf (cfg.enable || config.services.kerberos_server.enable) [(let
+      implementation = cfg.package.passthru.implementation or "<NOT SET>";
+    in {
+      assertion = lib.elem implementation [ "krb5" "heimdal" ];
+      message = ''
+        `security.krb5.package` must be one of:
+
+          - krb5
+          - heimdal
+
+        Currently chosen implementation: ${implementation}
+      '';
+    })];
+
+    environment = mkIf cfg.enable {
       systemPackages = [ cfg.package ];
       etc."krb5.conf".source = format.generate "krb5.conf" cfg.settings;
     };
diff --git a/nixos/modules/security/krb5/krb5-conf-format.nix b/nixos/modules/security/krb5/krb5-conf-format.nix
index 5a6bbed9fd188..3e5e64ae0cb04 100644
--- a/nixos/modules/security/krb5/krb5-conf-format.nix
+++ b/nixos/modules/security/krb5/krb5-conf-format.nix
@@ -7,17 +7,61 @@
 let
   inherit (lib) boolToString concatMapStringsSep concatStringsSep filter
     isAttrs isBool isList mapAttrsToList mkOption singleton splitString;
-  inherit (lib.types) attrsOf bool coercedTo either int listOf oneOf path
-    str submodule;
+  inherit (lib.types) attrsOf bool coercedTo either enum int listOf oneOf
+    path str submodule;
 in
-{ }: {
-  type = let
-    section = attrsOf relation;
-    relation = either (attrsOf value) value;
+{
+  enableKdcACLEntries ? false
+}: rec {
+  sectionType = let
+    relation = oneOf [
+      (listOf (attrsOf value))
+      (attrsOf value)
+      value
+    ];
     value = either (listOf atom) atom;
     atom = oneOf [int str bool];
+  in attrsOf relation;
+
+  type = let
+    aclEntry = submodule {
+      options = {
+        principal = mkOption {
+          type = str;
+          description = "Which principal the rule applies to";
+        };
+        access = mkOption {
+          type = either
+            (listOf (enum ["add" "cpw" "delete" "get" "list" "modify"]))
+            (enum ["all"]);
+          default = "all";
+          description = "The changes the principal is allowed to make.";
+        };
+        target = mkOption {
+          type = str;
+          default = "*";
+          description = "The principals that 'access' applies to.";
+        };
+      };
+    };
+
+    realm = submodule ({ name, ... }: {
+      freeformType = sectionType;
+      options = {
+        acl = mkOption {
+          type = listOf aclEntry;
+          default = [
+            { principal = "*/admin"; access = "all"; }
+            { principal = "admin"; access = "all"; }
+          ];
+          description = ''
+            The privileges granted to a user.
+          '';
+        };
+      };
+    });
   in submodule {
-    freeformType = attrsOf section;
+    freeformType = attrsOf sectionType;
     options = {
       include = mkOption {
         default = [ ];
@@ -40,7 +84,17 @@ in
         '';
         type = coercedTo path singleton (listOf path);
       };
-    };
+
+    }
+    //
+    (lib.optionalAttrs enableKdcACLEntries {
+      realms = mkOption {
+        type = attrsOf realm;
+        description = ''
+          The realm(s) to serve keys for.
+        '';
+      };
+    });
   };
 
   generate = let
@@ -71,6 +125,9 @@ in
         ${name} = {
         ${indent (concatStringsSep "\n" (mapAttrsToList formatValue relation))}
         }''
+      else if isList relation
+      then
+        concatMapStringsSep "\n" (formatRelation name) relation
       else formatValue name relation;
 
     formatValue = name: value:
diff --git a/nixos/modules/services/audio/navidrome.nix b/nixos/modules/services/audio/navidrome.nix
index a9db9228827a2..06d2d174a4df3 100644
--- a/nixos/modules/services/audio/navidrome.nix
+++ b/nixos/modules/services/audio/navidrome.nix
@@ -157,5 +157,5 @@ in
 
       networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.settings.Port ];
     };
-  meta.maintainers = with maintainers; [ nu-nu-ko ];
+  meta.maintainers = with maintainers; [ fsnkty ];
 }
diff --git a/nixos/modules/services/desktops/espanso.nix b/nixos/modules/services/desktops/espanso.nix
index a9b15b2659459..a6b8a078247b1 100644
--- a/nixos/modules/services/desktops/espanso.nix
+++ b/nixos/modules/services/desktops/espanso.nix
@@ -15,7 +15,6 @@ in {
   };
 
   config = mkIf cfg.enable {
-    services.espanso.package = mkIf cfg.wayland pkgs.espanso-wayland;
     systemd.user.services.espanso = {
       description = "Espanso daemon";
       serviceConfig = {
diff --git a/nixos/modules/services/hardware/amdvlk.nix b/nixos/modules/services/hardware/amdvlk.nix
new file mode 100644
index 0000000000000..20879f2f21b43
--- /dev/null
+++ b/nixos/modules/services/hardware/amdvlk.nix
@@ -0,0 +1,61 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.hardware.amdgpu.amdvlk;
+in {
+  options.hardware.amdgpu.amdvlk = {
+    enable = lib.mkEnableOption "AMDVLK Vulkan driver";
+
+    package = lib.mkPackageOption pkgs "amdvlk" { };
+
+    supportExperimental.enable = lib.mkEnableOption "Experimental features support";
+
+    support32Bit.enable = lib.mkEnableOption "32-bit driver support";
+    support32Bit.package = lib.mkPackageOption pkgs [ "driversi686Linux" "amdvlk" ] { };
+
+    settings = lib.mkOption {
+      type = with lib.types; attrsOf (either str int);
+      default = { };
+      example = {
+        AllowVkPipelineCachingToDisk = 1;
+        ShaderCacheMode = 1;
+        IFH = 0;
+        EnableVmAlwaysValid = 1;
+        IdleAfterSubmitGpuMask = 1;
+      };
+      description = ''
+        Runtime settings for AMDVLK to be configured {file}`/etc/amd/amdVulkanSettings.cfg`.
+        See [AMDVLK GitHub page](https://github.com/GPUOpen-Drivers/AMDVLK?tab=readme-ov-file#runtime-settings).
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    hardware.opengl = {
+      enable = true;
+      driSupport = true;
+      extraPackages = [ cfg.package ];
+      driSupport32Bit = cfg.support32Bit.enable;
+      extraPackages32 = [ cfg.support32Bit.package ];
+    };
+
+    services.xserver.videoDrivers = [ "amdgpu" ];
+
+    environment.sessionVariables = lib.mkIf cfg.supportExperimental.enable {
+      AMDVLK_ENABLE_DEVELOPING_EXT = "all";
+    };
+
+    environment.etc = lib.mkIf (cfg.settings != { }) {
+      "amd/amdVulkanSettings.cfg".text = lib.concatStrings
+        (lib.mapAttrsToList
+          (n: v: ''
+            ${n},${builtins.toString v}
+          '')
+          cfg.settings);
+    };
+  };
+
+  meta = {
+    maintainers = with lib.maintainers; [ johnrtitor ];
+  };
+}
diff --git a/nixos/modules/services/hardware/power-profiles-daemon.nix b/nixos/modules/services/hardware/power-profiles-daemon.nix
index 05e5b7a00b420..7651c65b9f181 100644
--- a/nixos/modules/services/hardware/power-profiles-daemon.nix
+++ b/nixos/modules/services/hardware/power-profiles-daemon.nix
@@ -39,6 +39,12 @@ in
           which conflicts with services.tlp.enable = true;
         '';
       }
+      { assertion = !config.services.auto-cpufreq.enable;
+        message = ''
+          You have set services.power-profiles-daemon.enable = true;
+          which conflicts with services.auto-cpufreq.enable = true;
+        '';
+      }
     ];
 
     environment.systemPackages = [ cfg.package ];
diff --git a/nixos/modules/services/logging/journalwatch.nix b/nixos/modules/services/logging/journalwatch.nix
index 71b29d57b7eb7..48fd992ffb65a 100644
--- a/nixos/modules/services/logging/journalwatch.nix
+++ b/nixos/modules/services/logging/journalwatch.nix
@@ -56,6 +56,8 @@ in {
         '';
       };
 
+      package = mkPackageOption pkgs "journalwatch" { };
+
       priority = mkOption {
         type = types.int;
         default = 6;
@@ -240,7 +242,7 @@ in {
         # requires a relative directory name to create beneath /var/lib
         StateDirectory = user;
         StateDirectoryMode = "0750";
-        ExecStart = "${pkgs.python3Packages.journalwatch}/bin/journalwatch mail";
+        ExecStart = "${getExe cfg.package} mail";
         # lowest CPU and IO priority, but both still in best-effort class to prevent starvation
         Nice=19;
         IOSchedulingPriority=7;
diff --git a/nixos/modules/services/mail/public-inbox.nix b/nixos/modules/services/mail/public-inbox.nix
index 14a2ab48fa250..98063e0331bd8 100644
--- a/nixos/modules/services/mail/public-inbox.nix
+++ b/nixos/modules/services/mail/public-inbox.nix
@@ -455,7 +455,7 @@ in
           after = [ "public-inbox-init.service" "public-inbox-watch.service" ];
           requires = [ "public-inbox-init.service" ];
           serviceConfig = {
-            BindPathsReadOnly =
+            BindReadOnlyPaths =
               map (c: c.dir) (lib.attrValues cfg.settings.coderepo);
             ExecStart = escapeShellArgs (
               [ "${cfg.package}/bin/public-inbox-httpd" ] ++
diff --git a/nixos/modules/services/misc/jellyfin.nix b/nixos/modules/services/misc/jellyfin.nix
index a1d3910bd93b0..a006090878422 100644
--- a/nixos/modules/services/misc/jellyfin.nix
+++ b/nixos/modules/services/misc/jellyfin.nix
@@ -160,5 +160,5 @@ in
 
   };
 
-  meta.maintainers = with maintainers; [ minijackson nu-nu-ko ];
+  meta.maintainers = with maintainers; [ minijackson fsnkty ];
 }
diff --git a/nixos/modules/services/misc/mqtt2influxdb.nix b/nixos/modules/services/misc/mqtt2influxdb.nix
index a2d6a2b34a239..925139b449b8e 100644
--- a/nixos/modules/services/misc/mqtt2influxdb.nix
+++ b/nixos/modules/services/misc/mqtt2influxdb.nix
@@ -125,6 +125,7 @@ in {
   options = {
     services.mqtt2influxdb = {
       enable = mkEnableOption "BigClown MQTT to InfluxDB bridge.";
+      package = mkPackageOption pkgs ["python3Packages" "mqtt2influxdb"] {};
       environmentFiles = mkOption {
         type = types.listOf types.path;
         default = [];
@@ -245,7 +246,7 @@ in {
       '';
       serviceConfig = {
         EnvironmentFile = cfg.environmentFiles;
-        ExecStart = "${cfg.package}/bin/mqtt2influxdb -dc ${finalConfig}";
+        ExecStart = "${lib.getExe cfg.package} -dc ${finalConfig}";
         RuntimeDirectory = "mqtt2influxdb";
       };
     };
diff --git a/nixos/modules/services/misc/pghero.nix b/nixos/modules/services/misc/pghero.nix
new file mode 100644
index 0000000000000..39515f10c8e1d
--- /dev/null
+++ b/nixos/modules/services/misc/pghero.nix
@@ -0,0 +1,142 @@
+{ config, pkgs, lib, utils, ... }:
+let
+  cfg = config.services.pghero;
+  settingsFormat = pkgs.formats.yaml { };
+  settingsFile = settingsFormat.generate "pghero.yaml" cfg.settings;
+in
+{
+  options.services.pghero = {
+    enable = lib.mkEnableOption "PgHero service";
+    package = lib.mkPackageOption pkgs "pghero" { };
+
+    listenAddress = lib.mkOption {
+      type = lib.types.str;
+      example = "[::1]:3000";
+      description = ''
+        `hostname:port` to listen for HTTP traffic.
+
+        This is bound using the systemd socket activation.
+      '';
+    };
+
+    extraArgs = lib.mkOption {
+      type = lib.types.listOf lib.types.str;
+      default = [ ];
+      description = ''
+        Additional command-line arguments for the systemd service.
+
+        Refer to the [Puma web server documentation] for available arguments.
+
+        [Puma web server documentation]: https://puma.io/puma#configuration
+      '';
+    };
+
+    settings = lib.mkOption {
+      type = settingsFormat.type;
+      default = { };
+      example = {
+        databases = {
+          primary = {
+            url = "<%= ENV['PRIMARY_DATABASE_URL'] %>";
+          };
+        };
+      };
+      description = ''
+        PgHero configuration. Refer to the [PgHero documentation] for more
+        details.
+
+        [PgHero documentation]: https://github.com/ankane/pghero/blob/master/guides/Linux.md#multiple-databases
+      '';
+    };
+
+    environment = lib.mkOption {
+      type = lib.types.attrsOf lib.types.str;
+      default = { };
+      description = ''
+        Environment variables to set for the service. Secrets should be
+        specified using {option}`environmentFile`.
+      '';
+    };
+
+    environmentFiles = lib.mkOption {
+      type = lib.types.listOf lib.types.path;
+      default = [ ];
+      description = ''
+        File to load environment variables from. Loaded variables override
+        values set in {option}`environment`.
+      '';
+    };
+
+    extraGroups = lib.mkOption {
+      type = lib.types.listOf lib.types.str;
+      default = [ ];
+      example = [ "tlskeys" ];
+      description = ''
+        Additional groups for the systemd service.
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.sockets.pghero = {
+      unitConfig.Description = "PgHero HTTP socket";
+      wantedBy = [ "sockets.target" ];
+      listenStreams = [ cfg.listenAddress ];
+    };
+
+    systemd.services.pghero = {
+      description = "PgHero performance dashboard for PostgreSQL";
+      wantedBy = [ "multi-user.target" ];
+      requires = [ "pghero.socket" ];
+      after = [ "pghero.socket" "network.target" ];
+
+      environment = {
+        RAILS_ENV = "production";
+        PGHERO_CONFIG_PATH = settingsFile;
+      } // cfg.environment;
+
+      serviceConfig = {
+        Type = "notify";
+        WatchdogSec = "10";
+
+        ExecStart = utils.escapeSystemdExecArgs ([
+          (lib.getExe cfg.package)
+          "--bind-to-activated-sockets"
+          "only"
+        ] ++ cfg.extraArgs);
+        Restart = "always";
+
+        WorkingDirectory = "${cfg.package}/share/pghero";
+
+        EnvironmentFile = cfg.environmentFiles;
+        SupplementaryGroups = cfg.extraGroups;
+
+        DynamicUser = true;
+        UMask = "0077";
+
+        ProtectHome = true;
+        ProtectProc = "invisible";
+        ProcSubset = "pid";
+        ProtectClock = true;
+        ProtectHostname = true;
+        ProtectControlGroups = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        PrivateUsers = true;
+        PrivateDevices = true;
+        RestrictRealtime = true;
+        RestrictNamespaces = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+        DeviceAllow = [ "" ];
+        DevicePolicy = "closed";
+        CapabilityBoundingSet = [ "" ];
+        MemoryDenyWriteExecute = true;
+        LockPersonality = true;
+        SystemCallArchitectures = "native";
+        SystemCallErrorNumber = "EPERM";
+        SystemCallFilter = [ "@system-service" ];
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/renovate.nix b/nixos/modules/services/misc/renovate.nix
new file mode 100644
index 0000000000000..25a719c91cbd8
--- /dev/null
+++ b/nixos/modules/services/misc/renovate.nix
@@ -0,0 +1,153 @@
+{
+  config,
+  lib,
+  pkgs,
+  ...
+}:
+let
+  inherit (lib)
+    mkEnableOption
+    mkPackageOption
+    mkOption
+    types
+    mkIf
+    ;
+  json = pkgs.formats.json { };
+  cfg = config.services.renovate;
+  generateValidatedConfig =
+    name: value:
+    pkgs.callPackage (
+      { runCommand, jq }:
+      runCommand name
+        {
+          nativeBuildInputs = [
+            jq
+            cfg.package
+          ];
+          value = builtins.toJSON value;
+          passAsFile = [ "value" ];
+          preferLocalBuild = true;
+        }
+        ''
+          jq . "$valuePath"> $out
+          renovate-config-validator $out
+        ''
+    ) { };
+  generateConfig = if cfg.validateSettings then generateValidatedConfig else json.generate;
+in
+{
+  meta.maintainers = with lib.maintainers; [ marie natsukium ];
+
+  options.services.renovate = {
+    enable = mkEnableOption "renovate";
+    package = mkPackageOption pkgs "renovate" { };
+    schedule = mkOption {
+      type = with types; nullOr str;
+      description = "How often to run renovate. See {manpage}`systemd.time(7)` for the format.";
+      example = "*:0/10";
+      default = null;
+    };
+    credentials = mkOption {
+      type = with types; attrsOf path;
+      description = ''
+        Allows configuring environment variable credentials for renovate, read from files.
+        This should always be used for passing confidential data to renovate.
+      '';
+      example = {
+        RENOVATE_TOKEN = "/etc/renovate/token";
+      };
+      default = { };
+    };
+    runtimePackages = mkOption {
+      type = with types; listOf package;
+      description = "Packages available to renovate.";
+      default = [ ];
+    };
+    validateSettings = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Weither to run renovate's config validator on the built configuration.";
+    };
+    settings = mkOption {
+      type = json.type;
+      default = { };
+      example = {
+        platform = "gitea";
+        endpoint = "https://git.example.com";
+        gitAuthor = "Renovate <renovate@example.com>";
+      };
+      description = ''
+        Renovate's global configuration.
+        If you want to pass secrets to renovate, please use {option}`services.renovate.credentials` for that.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.renovate.settings = {
+      cacheDir = "/var/cache/renovate";
+      baseDir = "/var/lib/renovate";
+    };
+
+    systemd.services.renovate = {
+      description = "Renovate dependency updater";
+      documentation = [ "https://docs.renovatebot.com/" ];
+      after = [ "network.target" ];
+      startAt = lib.optional (cfg.schedule != null) cfg.schedule;
+      path = [
+        config.systemd.package
+        pkgs.git
+      ] ++ cfg.runtimePackages;
+
+      serviceConfig = {
+        Type = "oneshot";
+        User = "renovate";
+        Group = "renovate";
+        DynamicUser = true;
+        LoadCredential = lib.mapAttrsToList (name: value: "SECRET-${name}:${value}") cfg.credentials;
+        RemainAfterExit = false;
+        Restart = "on-failure";
+        CacheDirectory = "renovate";
+        StateDirectory = "renovate";
+
+        # Hardening
+        CapabilityBoundingSet = [ "" ];
+        DeviceAllow = [ "" ];
+        LockPersonality = true;
+        PrivateDevices = true;
+        PrivateUsers = true;
+        ProcSubset = "pid";
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        RestrictAddressFamilies = [
+          "AF_INET"
+          "AF_INET6"
+        ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        SystemCallArchitectures = "native";
+        UMask = "0077";
+      };
+
+      script = ''
+        ${lib.concatStringsSep "\n" (
+          builtins.map (name: "export ${name}=$(systemd-creds cat 'SECRET-${name}')") (
+            lib.attrNames cfg.credentials
+          )
+        )}
+        exec ${lib.escapeShellArg (lib.getExe cfg.package)}
+      '';
+
+      environment = {
+        RENOVATE_CONFIG_FILE = generateConfig "renovate-config.json" cfg.settings;
+        HOME = "/var/lib/renovate";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/alloy.nix b/nixos/modules/services/monitoring/alloy.nix
new file mode 100644
index 0000000000000..abe8fcd7e1beb
--- /dev/null
+++ b/nixos/modules/services/monitoring/alloy.nix
@@ -0,0 +1,80 @@
+{ lib, pkgs, config, ... }:
+with lib;
+let
+  cfg = config.services.alloy;
+in
+{
+  meta = {
+    maintainers = with maintainers; [ flokli hbjydev ];
+  };
+
+  options.services.alloy = {
+    enable = mkEnableOption "Grafana Alloy";
+
+    package = mkPackageOption pkgs "grafana-alloy" { };
+
+    configPath = mkOption {
+      type = lib.types.path;
+      default = "/etc/alloy";
+      description = ''
+        Alloy configuration file/directory path.
+
+        We default to `/etc/alloy` here, and expect the user to configure a
+        configuration file via `environment.etc."alloy/config.alloy"`.
+
+        This allows config reload, contrary to specifying a store path.
+        A `reloadTrigger` for `config.alloy` is configured.
+
+        Other `*.alloy` files in the same directory (ignoring subdirs) are also
+        honored, but it's necessary to manually extend
+        `systemd.services.alloy.reloadTriggers` to enable config reload
+        during nixos-rebuild switch.
+
+        This can also point to another directory containing `*.alloy` files, or
+        a single Alloy file in the Nix store (at the cost of reload).
+
+        Component names must be unique across all Alloy configuration files, and
+        configuration blocks must not be repeated.
+
+        Alloy will continue to run if subsequent reloads of the configuration
+        file fail, potentially marking components as unhealthy depending on
+        the nature of the failure. When this happens, Alloy will continue
+        functioning in the last valid state.
+      '';
+    };
+
+    extraFlags = mkOption {
+      type = with lib.types; listOf str;
+      default = [ ];
+      example = [ "--server.http.listen-addr=127.0.0.1:12346" "--disable-reporting" ];
+      description = ''
+        Extra command-line flags passed to {command}`alloy run`.
+
+        See <https://grafana.com/docs/alloy/latest/reference/cli/run/>
+      '';
+    };
+  };
+
+
+  config = mkIf cfg.enable {
+    systemd.services.alloy = {
+      wantedBy = [ "multi-user.target" ];
+      reloadTriggers = [ config.environment.etc."alloy/config.alloy".source or null ];
+      serviceConfig = {
+        Restart = "always";
+        DynamicUser = true;
+        RestartSec = 2;
+        SupplementaryGroups = [
+          # allow to read the systemd journal for loki log forwarding
+          "systemd-journal"
+        ];
+        ExecStart = "${lib.getExe cfg.package} run ${cfg.configPath} ${escapeShellArgs cfg.extraFlags}";
+        ExecReload = "${pkgs.coreutils}/bin/kill -SIGHUP $MAINPID";
+        ConfigurationDirectory = "alloy";
+        StateDirectory = "alloy";
+        WorkingDirectory = "%S/alloy";
+        Type = "simple";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/loki.nix b/nixos/modules/services/monitoring/loki.nix
index de4f1bc7aa23e..307119ecbf8ba 100644
--- a/nixos/modules/services/monitoring/loki.nix
+++ b/nixos/modules/services/monitoring/loki.nix
@@ -94,6 +94,7 @@ in {
     systemd.services.loki = {
       description = "Loki Service Daemon";
       wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
 
       serviceConfig = let
         conf = if cfg.configFile == null
diff --git a/nixos/modules/services/monitoring/scrutiny.nix b/nixos/modules/services/monitoring/scrutiny.nix
index 031f5a30cada6..c649d333e401a 100644
--- a/nixos/modules/services/monitoring/scrutiny.nix
+++ b/nixos/modules/services/monitoring/scrutiny.nix
@@ -140,8 +140,8 @@ in
 
             options.api.endpoint = mkOption {
               type = str;
-              default = "http://localhost:${toString cfg.settings.web.listen.port}";
-              defaultText = literalExpression ''"http://localhost:''${config.services.scrutiny.settings.web.listen.port}"'';
+              default = "http://${cfg.settings.web.listen.host}:${toString cfg.settings.web.listen.port}";
+              defaultText = literalExpression ''"http://''${config.services.scrutiny.settings.web.listen.host}:''${config.services.scrutiny.settings.web.listen.port}"'';
               description = "Scrutiny app API endpoint for sending metrics to.";
             };
 
diff --git a/nixos/modules/services/networking/aria2.nix b/nixos/modules/services/networking/aria2.nix
index f32f5682c9801..f0d5c5c8a21e3 100644
--- a/nixos/modules/services/networking/aria2.nix
+++ b/nixos/modules/services/networking/aria2.nix
@@ -1,98 +1,132 @@
 { config, lib, pkgs, ... }:
 
-with lib;
-
 let
   cfg = config.services.aria2;
 
   homeDir = "/var/lib/aria2";
-
-  settingsDir = "${homeDir}";
-  sessionFile = "${homeDir}/aria2.session";
-  downloadDir = "${homeDir}/Downloads";
-
-  rangesToStringList = map (x: builtins.toString x.from +"-"+ builtins.toString x.to);
-
-  settingsFile = pkgs.writeText "aria2.conf"
-  ''
-    dir=${cfg.downloadDir}
-    listen-port=${concatStringsSep "," (rangesToStringList cfg.listenPortRange)}
-    rpc-listen-port=${toString cfg.rpcListenPort}
-  '';
-
+  defaultRpcListenPort = 6800;
+  defaultDir = "${homeDir}/Downloads";
+
+  portRangesToString = ranges: lib.concatStringsSep "," (map
+    (x:
+      if x.from == x.to
+      then builtins.toString x.from
+      else builtins.toString x.from + "-" + builtins.toString x.to
+    )
+    ranges);
+
+  customToKeyValue = lib.generators.toKeyValue {
+    mkKeyValue = lib.generators.mkKeyValueDefault
+      {
+        mkValueString = v:
+          if builtins.isList v then portRangesToString v
+          else lib.generators.mkValueStringDefault { } v;
+      } "=";
+  };
 in
 {
   imports = [
-    (mkRemovedOptionModule [ "services" "aria2" "rpcSecret" ] "Use services.aria2.rpcSecretFile instead")
+    (lib.mkRemovedOptionModule [ "services" "aria2" "rpcSecret" ] "Use services.aria2.rpcSecretFile instead")
+    (lib.mkRemovedOptionModule [ "services" "aria2" "extraArguments" ] "Use services.aria2.settings instead")
+    (lib.mkRenamedOptionModule [ "services" "aria2" "downloadDir" ] [ "services" "aria2" "settings" "dir" ])
+    (lib.mkRenamedOptionModule [ "services" "aria2" "listenPortRange" ] [ "services" "aria2" "settings" "listen-port" ])
+    (lib.mkRenamedOptionModule [ "services" "aria2" "rpcListenPort" ] [ "services" "aria2" "settings" "rpc-listen-port" ])
   ];
 
   options = {
     services.aria2 = {
-      enable = mkOption {
-        type = types.bool;
+      enable = lib.mkOption {
+        type = lib.types.bool;
         default = false;
         description = ''
           Whether or not to enable the headless Aria2 daemon service.
 
-          Aria2 daemon can be controlled via the RPC interface using
-          one of many WebUI (http://localhost:6800/ by default).
+          Aria2 daemon can be controlled via the RPC interface using one of many
+          WebUIs (http://localhost:${toString defaultRpcListenPort}/ by default).
 
-          Targets are downloaded to ${downloadDir} by default and are
-          accessible to users in the "aria2" group.
+          Targets are downloaded to `${defaultDir}` by default and are
+          accessible to users in the `aria2` group.
         '';
       };
-      openPorts = mkOption {
-        type = types.bool;
+      openPorts = lib.mkOption {
+        type = lib.types.bool;
         default = false;
         description = ''
-          Open listen and RPC ports found in listenPortRange and rpcListenPort
-          options in the firewall.
-        '';
-      };
-      downloadDir = mkOption {
-        type = types.path;
-        default = downloadDir;
-        description = ''
-          Directory to store downloaded files.
-        '';
-      };
-      listenPortRange = mkOption {
-        type = types.listOf types.attrs;
-        default = [ { from = 6881; to = 6999; } ];
-        description = ''
-          Set UDP listening port range used by DHT(IPv4, IPv6) and UDP tracker.
+          Open listen and RPC ports found in `settings.listen-port` and
+          `settings.rpc-listen-port` options in the firewall.
         '';
       };
-      rpcListenPort = mkOption {
-        type = types.int;
-        default = 6800;
-        description = "Specify a port number for JSON-RPC/XML-RPC server to listen to. Possible Values: 1024-65535";
-      };
-      rpcSecretFile = mkOption {
-        type = types.path;
+      rpcSecretFile = lib.mkOption {
+        type = lib.types.path;
         example = "/run/secrets/aria2-rpc-token.txt";
         description = ''
           A file containing the RPC secret authorization token.
           Read https://aria2.github.io/manual/en/html/aria2c.html#rpc-auth to know how this option value is used.
         '';
       };
-      extraArguments = mkOption {
-        type = types.separatedString " ";
-        example = "--rpc-listen-all --remote-time=true";
-        default = "";
+      settings = lib.mkOption {
         description = ''
-          Additional arguments to be passed to Aria2.
+          Generates the `aria2.conf` file. Refer to [the documentation][0] for
+          all possible settings.
+
+          [0]: https://aria2.github.io/manual/en/html/aria2c.html#synopsis
         '';
+        default = { };
+        type = lib.types.submodule {
+          freeformType = with lib.types; attrsOf (oneOf [ bool int float singleLineStr ]);
+          options = {
+            save-session = lib.mkOption {
+              type = lib.types.singleLineStr;
+              default = "${homeDir}/aria2.session";
+              description = "Save error/unfinished downloads to FILE on exit.";
+            };
+            dir = lib.mkOption {
+              type = lib.types.singleLineStr;
+              default = defaultDir;
+              description = "Directory to store downloaded files.";
+            };
+            conf-path = lib.mkOption {
+              type = lib.types.singleLineStr;
+              default = "${homeDir}/aria2.conf";
+              description = "Configuration file path.";
+            };
+            enable-rpc = lib.mkOption {
+              type = lib.types.bool;
+              default = true;
+              description = "Enable JSON-RPC/XML-RPC server.";
+            };
+            listen-port = lib.mkOption {
+              type = with lib.types; listOf (attrsOf port);
+              default = [{ from = 6881; to = 6999; }];
+              description = "Set UDP listening port range used by DHT(IPv4, IPv6) and UDP tracker.";
+            };
+            rpc-listen-port = lib.mkOption {
+              type = lib.types.port;
+              default = defaultRpcListenPort;
+              description = "Specify a port number for JSON-RPC/XML-RPC server to listen to. Possible Values: 1024-65535";
+            };
+          };
+        };
       };
     };
   };
 
-  config = mkIf cfg.enable {
+  config = lib.mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = cfg.settings.enable-rpc;
+        message = "RPC has to be enabled, the default module option takes care of that.";
+      }
+      {
+        assertion = !(cfg.settings ? rpc-secret);
+        message = "Set the RPC secret through services.aria2.rpcSecretFile so it will not end up in the world-readable nix store.";
+      }
+    ];
 
     # Need to open ports for proper functioning
-    networking.firewall = mkIf cfg.openPorts {
-      allowedUDPPortRanges = config.services.aria2.listenPortRange;
-      allowedTCPPorts = [ config.services.aria2.rpcListenPort ];
+    networking.firewall = lib.mkIf cfg.openPorts {
+      allowedUDPPortRanges = config.services.aria2.settings.listen-port;
+      allowedTCPPorts = [ config.services.aria2.settings.rpc-listen-port ];
     };
 
     users.users.aria2 = {
@@ -107,7 +141,7 @@ in
 
     systemd.tmpfiles.rules = [
       "d '${homeDir}' 0770 aria2 aria2 - -"
-      "d '${config.services.aria2.downloadDir}' 0770 aria2 aria2 - -"
+      "d '${config.services.aria2.settings.dir}' 0770 aria2 aria2 - -"
     ];
 
     systemd.services.aria2 = {
@@ -115,22 +149,25 @@ in
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
       preStart = ''
-        if [[ ! -e "${sessionFile}" ]]
+        if [[ ! -e "${cfg.settings.save-session}" ]]
         then
-          touch "${sessionFile}"
+          touch "${cfg.settings.save-session}"
         fi
-        cp -f "${settingsFile}" "${settingsDir}/aria2.conf"
-        echo "rpc-secret=$(cat "$CREDENTIALS_DIRECTORY/rpcSecretFile")" >> "${settingsDir}/aria2.conf"
+        cp -f "${pkgs.writeText "aria2.conf" (customToKeyValue cfg.settings)}" "${cfg.settings.conf-path}"
+        chmod +w "${cfg.settings.conf-path}"
+        echo "rpc-secret=$(cat "$CREDENTIALS_DIRECTORY/rpcSecretFile")" >> "${cfg.settings.conf-path}"
       '';
 
       serviceConfig = {
         Restart = "on-abort";
-        ExecStart = "${pkgs.aria2}/bin/aria2c --enable-rpc --conf-path=${settingsDir}/aria2.conf ${config.services.aria2.extraArguments} --save-session=${sessionFile}";
+        ExecStart = "${pkgs.aria2}/bin/aria2c --conf-path=${cfg.settings.conf-path}";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
         User = "aria2";
         Group = "aria2";
-        LoadCredential="rpcSecretFile:${cfg.rpcSecretFile}";
+        LoadCredential = "rpcSecretFile:${cfg.rpcSecretFile}";
       };
     };
   };
+
+  meta.maintainers = [ lib.maintainers.timhae ];
 }
diff --git a/nixos/modules/services/networking/tailscale.nix b/nixos/modules/services/networking/tailscale.nix
index a79e47d8491b8..a690dc610e825 100644
--- a/nixos/modules/services/networking/tailscale.nix
+++ b/nixos/modules/services/networking/tailscale.nix
@@ -61,12 +61,21 @@ in {
     };
 
     extraUpFlags = mkOption {
-      description = "Extra flags to pass to {command}`tailscale up`.";
+      description = ''
+        Extra flags to pass to {command}`tailscale up`. Only applied if `authKeyFile` is specified.";
+      '';
       type = types.listOf types.str;
       default = [];
       example = ["--ssh"];
     };
 
+    extraSetFlags = mkOption {
+      description = "Extra flags to pass to {command}`tailscale set`.";
+      type = types.listOf types.str;
+      default = [];
+      example = ["--advertise-exit-node"];
+    };
+
     extraDaemonFlags = mkOption {
       description = "Extra flags to pass to {command}`tailscaled`.";
       type = types.listOf types.str;
@@ -120,6 +129,18 @@ in {
       '';
     };
 
+    systemd.services.tailscaled-set = mkIf (cfg.extraSetFlags != []) {
+      after = ["tailscaled.service"];
+      wants = ["tailscaled.service"];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "oneshot";
+      };
+      script = ''
+        ${cfg.package}/bin/tailscale set ${escapeShellArgs cfg.extraSetFlags}
+      '';
+    };
+
     boot.kernel.sysctl = mkIf (cfg.useRoutingFeatures == "server" || cfg.useRoutingFeatures == "both") {
       "net.ipv4.conf.all.forwarding" = mkOverride 97 true;
       "net.ipv6.conf.all.forwarding" = mkOverride 97 true;
diff --git a/nixos/modules/services/networking/wstunnel.nix b/nixos/modules/services/networking/wstunnel.nix
index efb65aead116a..1b169567624c0 100644
--- a/nixos/modules/services/networking/wstunnel.nix
+++ b/nixos/modules/services/networking/wstunnel.nix
@@ -7,6 +7,9 @@ let
     (name: value: if value == true then "--${name}" else "--${name}=${value}")
     attrs
   );
+
+  hostPortToString = { host, port }: "${host}:${builtins.toString port}";
+
   hostPortSubmodule = {
     options = {
       host = mkOption {
@@ -19,28 +22,7 @@ let
       };
     };
   };
-  localRemoteSubmodule = {
-    options = {
-      local = mkOption {
-        description = "Local address and port to listen on.";
-        type = types.submodule hostPortSubmodule;
-        example = {
-          host = "127.0.0.1";
-          port = 51820;
-        };
-      };
-      remote = mkOption {
-        description = "Address and port on remote to forward traffic to.";
-        type = types.submodule hostPortSubmodule;
-        example = {
-          host = "127.0.0.1";
-          port = 51820;
-        };
-      };
-    };
-  };
-  hostPortToString = { host, port }: "${host}:${builtins.toString port}";
-  localRemoteToString = { local, remote }: utils.escapeSystemdExecArg "${hostPortToString local}:${hostPortToString remote}";
+
   commonOptions = {
     enable = mkOption {
       description = "Whether to enable this `wstunnel` instance.";
@@ -66,10 +48,16 @@ let
       };
     };
 
-    verboseLogging = mkOption {
-      description = "Enable verbose logging.";
-      type = types.bool;
-      default = false;
+    loggingLevel = mkOption {
+      description = ''
+        Passed to --log-lvl
+
+        Control the log verbosity. i.e: TRACE, DEBUG, INFO, WARN, ERROR, OFF
+        For more details, checkout [EnvFilter](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#example-syntax)
+      '';
+      type = types.nullOr types.str;
+      example = "INFO";
+      default = null;
     };
 
     environmentFile = mkOption {
@@ -99,11 +87,12 @@ let
 
       restrictTo = mkOption {
         description = "Accepted traffic will be forwarded only to this service. Set to `null` to allow forwarding to arbitrary addresses.";
-        type = types.nullOr (types.submodule hostPortSubmodule);
-        example = {
+        type = types.listOf (types.submodule hostPortSubmodule);
+        default = [];
+        example = [{
           host = "127.0.0.1";
           port = 51820;
-        };
+        }];
       };
 
       enableHTTPS = mkOption {
@@ -134,59 +123,36 @@ let
       };
     };
   };
+
   clientSubmodule = { config, ... }: {
     options = commonOptions // {
       connectTo = mkOption {
         description = "Server address and port to connect to.";
-        type = types.submodule hostPortSubmodule;
-        example = {
-          host = "example.com";
-        };
-      };
-
-      enableHTTPS = mkOption {
-        description = "Enable HTTPS when connecting to the server.";
-        type = types.bool;
-        default = true;
+        type = types.str;
+        example = "https://wstunnel.server.com:8443";
       };
 
       localToRemote = mkOption {
-        description = "Local hosts and ports to listen on, plus the hosts and ports on remote to forward traffic to. Setting a local port to a value less than 1024 will additionally give the process the required CAP_NET_BIND_SERVICE capability.";
-        type = types.listOf (types.submodule localRemoteSubmodule);
+        description = ''Listen on local and forwards traffic from remote.'';
+        type = types.listOf (types.str);
         default = [];
-        example = [ {
-          local = {
-            host = "127.0.0.1";
-            port = 8080;
-          };
-          remote = {
-            host = "127.0.0.1";
-            port = 8080;
-          };
-        } ];
+        example = [
+          "tcp://1212:google.com:443"
+          "unix:///tmp/wstunnel.sock:g.com:443"
+        ];
       };
 
-      dynamicToRemote = mkOption {
-        description = "Host and port for the SOCKS5 proxy to dynamically forward traffic to. Leave this at `null` to disable the SOCKS5 proxy. Setting the port to a value less than 1024 will additionally give the service the required CAP_NET_BIND_SERVICE capability.";
-        type = types.nullOr (types.submodule hostPortSubmodule);
-        default = null;
-        example = {
-          host = "127.0.0.1";
-          port = 1080;
-        };
-      };
-
-      udp = mkOption {
-        description = "Whether to forward UDP instead of TCP traffic.";
-        type = types.bool;
-        default = false;
+      remoteToLocal = mkOption {
+        description = "Listen on remote and forwards traffic from local. Only tcp is supported";
+        type = types.listOf (types.str);
+        default = [];
+        example = [
+          "tcp://1212:google.com:443"
+          "unix://wstunnel.sock:g.com:443"
+        ];
       };
 
-      udpTimeout = mkOption {
-        description = "When using UDP forwarding, timeout in seconds after which the tunnel connection is closed. `-1` means no timeout.";
-        type = types.int;
-        default = 30;
-      };
+      addNetBind = mkEnableOption "Whether add CAP_NET_BIND_SERVICE to the tunnel service, this should be enabled if you want to bind port < 1024";
 
       httpProxy = mkOption {
         description = ''
@@ -214,12 +180,6 @@ let
         example = "wstunnel";
       };
 
-      hostHeader = mkOption {
-        description = "Use this as the HTTP host header instead of the real hostname. Useful for circumventing hostname-based firewalls.";
-        type = types.nullOr types.str;
-        default = null;
-      };
-
       tlsSNI = mkOption {
         description = "Use this as the SNI while connecting via TLS. Useful for circumventing hostname-based firewalls.";
         type = types.nullOr types.str;
@@ -234,7 +194,7 @@ let
 
       # The original argument name `websocketPingFrequency` is a misnomer, as the frequency is the inverse of the interval.
       websocketPingInterval = mkOption {
-        description = "Do a heartbeat ping every N seconds to keep up the websocket connection.";
+        description = "Frequency at which the client will send websocket ping to the server.";
         type = types.nullOr types.ints.unsigned;
         default = null;
       };
@@ -261,6 +221,7 @@ let
       };
     };
   };
+
   generateServerUnit = name: serverCfg: {
     name = "wstunnel-server-${name}";
     value = {
@@ -282,11 +243,11 @@ let
             else tlsKey;
         in ''
           ${package}/bin/wstunnel \
-            --server \
-            ${optionalString (restrictTo != null)     "--restrictTo=${utils.escapeSystemdExecArg (hostPortToString restrictTo)}"} \
-            ${optionalString (resolvedTlsCertificate != null) "--tlsCertificate=${utils.escapeSystemdExecArg resolvedTlsCertificate}"} \
-            ${optionalString (resolvedTlsKey != null)         "--tlsKey=${utils.escapeSystemdExecArg resolvedTlsKey}"} \
-            ${optionalString verboseLogging "--verbose"} \
+            server \
+            ${concatStringsSep " " (builtins.map (hostPair:   "--restrict-to ${utils.escapeSystemdExecArg (hostPortToString hostPair)}") restrictTo)} \
+            ${optionalString (resolvedTlsCertificate != null) "--tls-certificate ${utils.escapeSystemdExecArg resolvedTlsCertificate}"} \
+            ${optionalString (resolvedTlsKey != null)         "--tls-private-key ${utils.escapeSystemdExecArg resolvedTlsKey}"} \
+            ${optionalString (loggingLevel != null) "--log-lvl ${loggingLevel}"} \
             ${attrsToArgs extraArgs} \
             ${utils.escapeSystemdExecArg "${if enableHTTPS then "wss" else "ws"}://${hostPortToString listen}"}
         '';
@@ -304,10 +265,10 @@ let
         ProtectControlGroups = true;
         PrivateDevices = true;
         RestrictSUIDSGID = true;
-
       };
     };
   };
+
   generateClientUnit = name: clientCfg: {
     name = "wstunnel-client-${name}";
     value = {
@@ -319,28 +280,25 @@ let
       serviceConfig = {
         Type = "simple";
         ExecStart = with clientCfg; ''
-          ${package}/bin/wstunnel \
-            ${concatStringsSep " " (builtins.map (x:          "--localToRemote=${localRemoteToString x}") localToRemote)} \
-            ${concatStringsSep " " (mapAttrsToList (n: v:     "--customHeaders=\"${n}: ${v}\"") customHeaders)} \
-            ${optionalString (dynamicToRemote != null)        "--dynamicToRemote=${utils.escapeSystemdExecArg (hostPortToString dynamicToRemote)}"} \
-            ${optionalString udp                              "--udp"} \
-            ${optionalString (httpProxy != null)              "--httpProxy=${httpProxy}"} \
-            ${optionalString (soMark != null)                 "--soMark=${toString soMark}"} \
-            ${optionalString (upgradePathPrefix != null)      "--upgradePathPrefix=${upgradePathPrefix}"} \
-            ${optionalString (hostHeader != null)             "--hostHeader=${hostHeader}"} \
-            ${optionalString (tlsSNI != null)                 "--tlsSNI=${tlsSNI}"} \
-            ${optionalString tlsVerifyCertificate             "--tlsVerifyCertificate"} \
-            ${optionalString (websocketPingInterval != null)  "--websocketPingFrequency=${toString websocketPingInterval}"} \
-            ${optionalString (upgradeCredentials != null)     "--upgradeCredentials=${upgradeCredentials}"} \
-            --udpTimeoutSec=${toString udpTimeout} \
-            ${optionalString verboseLogging "--verbose"} \
+          ${package}/bin/wstunnel client \
+            ${concatStringsSep " " (builtins.map (x:          "--local-to-remote ${x}") localToRemote)} \
+            ${concatStringsSep " " (builtins.map (x:          "--remote-to-local ${x}") remoteToLocal)} \
+            ${concatStringsSep " " (mapAttrsToList (n: v:     "--http-headers \"${n}: ${v}\"") customHeaders)} \
+            ${optionalString (httpProxy != null)              "--http-proxy ${httpProxy}"} \
+            ${optionalString (soMark != null)                 "--socket-so-mark=${toString soMark}"} \
+            ${optionalString (upgradePathPrefix != null)      "--http-upgrade-path-prefix ${upgradePathPrefix}"} \
+            ${optionalString (tlsSNI != null)                 "--tls-sni-override ${tlsSNI}"} \
+            ${optionalString tlsVerifyCertificate             "--tls-verify-certificate"} \
+            ${optionalString (websocketPingInterval != null)  "--websocket-ping-frequency-sec ${toString websocketPingInterval}"} \
+            ${optionalString (upgradeCredentials != null)     "--http-upgrade-credentials ${upgradeCredentials}"} \
+            ${optionalString (loggingLevel != null) "--log-lvl ${loggingLevel}"} \
             ${attrsToArgs extraArgs} \
-            ${utils.escapeSystemdExecArg "${if enableHTTPS then "wss" else "ws"}://${hostPortToString connectTo}"}
+            ${utils.escapeSystemdExecArg connectTo}
         '';
         EnvironmentFile = optional (clientCfg.environmentFile != null) clientCfg.environmentFile;
         DynamicUser = true;
         PrivateTmp = true;
-        AmbientCapabilities = (optionals (clientCfg.soMark != null) [ "CAP_NET_ADMIN" ]) ++ (optionals ((clientCfg.dynamicToRemote.port or 1024) < 1024 || (any (x: x.local.port < 1024) clientCfg.localToRemote)) [ "CAP_NET_BIND_SERVICE" ]);
+        AmbientCapabilities = (optionals (clientCfg.soMark != null) [ "CAP_NET_ADMIN" ]) ++ (optionals (clientCfg.addNetBind) [ "CAP_NET_BIND_SERVICE" ]);
         NoNewPrivileges = true;
         RestrictNamespaces = "uts ipc pid user cgroup";
         ProtectSystem = "strict";
@@ -363,14 +321,17 @@ in {
       default = {};
       example = {
         "wg-tunnel" = {
-          listen.port = 8080;
+          listen = {
+            host = "0.0.0.0";
+            port = 8080;
+          };
           enableHTTPS = true;
           tlsCertificate = "/var/lib/secrets/fullchain.pem";
           tlsKey = "/var/lib/secrets/key.pem";
-          restrictTo = {
+          restrictTo = [{
             host = "127.0.0.1";
             port = 51820;
-          };
+          }];
         };
       };
     };
@@ -381,22 +342,15 @@ in {
       default = {};
       example = {
         "wg-tunnel" = {
-          connectTo = {
-            host = "example.com";
-            port = 8080;
-          };
-          enableHTTPS = true;
-          localToRemote = {
-            local = {
-              host = "127.0.0.1";
-              port = 51820;
-            };
-            remote = {
-              host = "127.0.0.1";
-              port = 51820;
-            };
-          };
-          udp = true;
+          connectTo = "https://wstunnel.server.com:8443";
+          localToRemote = [
+            "tcp://1212:google.com:443"
+            "tcp://2:n.lan:4?proxy_protocol"
+          ];
+          remoteToLocal = [
+            "socks5://[::1]:1212"
+            "unix://wstunnel.sock:g.com:443"
+          ];
         };
       };
     };
@@ -418,12 +372,12 @@ in {
       '';
     }) cfg.servers) ++
     (mapAttrsToList (name: clientCfg: {
-      assertion = !(clientCfg.localToRemote == [] && clientCfg.dynamicToRemote == null);
+      assertion = !(clientCfg.localToRemote == [] && clientCfg.remoteToLocal == []);
       message = ''
-        Either one of services.wstunnel.clients."${name}".localToRemote or services.wstunnel.clients."${name}".dynamicToRemote must be set.
+        Either one of services.wstunnel.clients."${name}".localToRemote or services.wstunnel.clients."${name}".remoteToLocal must be set.
       '';
     }) cfg.clients);
   };
 
-  meta.maintainers = with maintainers; [ alyaeanyx ];
+  meta.maintainers = with maintainers; [ alyaeanyx neverbehave ];
 }
diff --git a/nixos/modules/services/system/kerberos/default.nix b/nixos/modules/services/system/kerberos/default.nix
index 7fe970c9609a9..34c7c6c84f865 100644
--- a/nixos/modules/services/system/kerberos/default.nix
+++ b/nixos/modules/services/system/kerberos/default.nix
@@ -1,75 +1,59 @@
-{config, lib, ...}:
+{ config, pkgs, lib, ... }:
 
 let
-  inherit (lib) mkOption mkIf types length attrNames;
+  inherit (lib) mkOption types;
   cfg = config.services.kerberos_server;
-  kerberos = config.security.krb5.package;
+  inherit (config.security.krb5) package;
 
-  aclEntry = {
-    options = {
-      principal = mkOption {
-        type = types.str;
-        description = "Which principal the rule applies to";
-      };
-      access = mkOption {
-        type = types.either
-          (types.listOf (types.enum ["add" "cpw" "delete" "get" "list" "modify"]))
-          (types.enum ["all"]);
-        default = "all";
-        description = "The changes the principal is allowed to make.";
-      };
-      target = mkOption {
-        type = types.str;
-        default = "*";
-        description = "The principals that 'access' applies to.";
-      };
-    };
-  };
-
-  realm = {
-    options = {
-      acl = mkOption {
-        type = types.listOf (types.submodule aclEntry);
-        default = [
-          { principal = "*/admin"; access = "all"; }
-          { principal = "admin"; access = "all"; }
-        ];
-        description = ''
-          The privileges granted to a user.
-        '';
-      };
-    };
-  };
+  format = import ../../../security/krb5/krb5-conf-format.nix { inherit pkgs lib; } { enableKdcACLEntries = true; };
 in
 
 {
   imports = [
+    (lib.mkRenamedOptionModule [ "services" "kerberos_server" "realms" ] [ "services" "kerberos_server" "settings" "realms" ])
+
     ./mit.nix
     ./heimdal.nix
   ];
 
-  ###### interface
   options = {
     services.kerberos_server = {
       enable = lib.mkEnableOption "the kerberos authentication server";
 
-      realms = mkOption {
-        type = types.attrsOf (types.submodule realm);
+      settings = mkOption {
+        type = format.type;
         description = ''
-          The realm(s) to serve keys for.
+          Settings for the kerberos server of choice.
+
+          See the following documentation:
+          - Heimdal: {manpage}`kdc.conf(5)`
+          - MIT Kerberos: <https://web.mit.edu/kerberos/krb5-1.21/doc/admin/conf_files/kdc_conf.html>
         '';
+        default = { };
       };
     };
   };
 
+  config = lib.mkIf cfg.enable {
+    environment.systemPackages = [ package ];
+    assertions = [
+      {
+        assertion = cfg.settings.realms != { };
+        message = "The server needs at least one realm";
+      }
+      {
+        assertion = lib.length (lib.attrNames cfg.settings.realms) <= 1;
+        message = "Only one realm per server is currently supported.";
+      }
+    ];
+
+    systemd.slices.system-kerberos-server = { };
+    systemd.targets.kerberos-server = {
+      wantedBy = [ "multi-user.target" ];
+    };
+  };
 
-  ###### implementation
-
-  config = mkIf cfg.enable {
-    environment.systemPackages = [ kerberos ];
-    assertions = [{
-      assertion = length (attrNames cfg.realms) <= 1;
-      message = "Only one realm per server is currently supported.";
-    }];
+  meta = {
+    doc = ./kerberos-server.md;
   };
 }
diff --git a/nixos/modules/services/system/kerberos/heimdal.nix b/nixos/modules/services/system/kerberos/heimdal.nix
index ecafc92766704..cec4dd276e6b9 100644
--- a/nixos/modules/services/system/kerberos/heimdal.nix
+++ b/nixos/modules/services/system/kerberos/heimdal.nix
@@ -1,68 +1,87 @@
 { pkgs, config, lib, ... } :
 
 let
-  inherit (lib) mkIf concatStringsSep concatMapStrings toList mapAttrs
-    mapAttrsToList;
+  inherit (lib)  mapAttrs;
   cfg = config.services.kerberos_server;
-  kerberos = config.security.krb5.package;
-  stateDir = "/var/heimdal";
-  aclFiles = mapAttrs
-    (name: {acl, ...}: pkgs.writeText "${name}.acl" (concatMapStrings ((
-      {principal, access, target, ...} :
-      "${principal}\t${concatStringsSep "," (toList access)}\t${target}\n"
-    )) acl)) cfg.realms;
+  package = config.security.krb5.package;
 
-  kdcConfigs = mapAttrsToList (name: value: ''
-    database = {
-      dbname = ${stateDir}/heimdal
-      acl_file = ${value}
-    }
-  '') aclFiles;
-  kdcConfFile = pkgs.writeText "kdc.conf" ''
-    [kdc]
-    ${concatStringsSep "\n" kdcConfigs}
-  '';
+  aclConfigs = lib.pipe cfg.settings.realms [
+    (mapAttrs (name: { acl, ... }: lib.concatMapStringsSep "\n" (
+      { principal, access, target, ... }:
+      "${principal}\t${lib.concatStringsSep "," (lib.toList access)}\t${target}"
+    ) acl))
+    (lib.mapAttrsToList (name: text:
+      {
+        dbname = "/var/lib/heimdal/heimdal";
+        acl_file = pkgs.writeText "${name}.acl" text;
+      }
+    ))
+  ];
+
+  finalConfig = cfg.settings // {
+    realms = mapAttrs (_: v: removeAttrs v [ "acl" ]) (cfg.settings.realms or { });
+    kdc = (cfg.settings.kdc or { }) // {
+      database = aclConfigs;
+    };
+  };
+
+  format = import ../../../security/krb5/krb5-conf-format.nix { inherit pkgs lib; } { enableKdcACLEntries = true; };
+
+  kdcConfFile = format.generate "kdc.conf" finalConfig;
 in
 
 {
-  # No documentation about correct triggers, so guessing at them.
+  config = lib.mkIf (cfg.enable && package.passthru.implementation == "heimdal") {
+    environment.etc."heimdal-kdc/kdc.conf".source = kdcConfFile;
+
+    systemd.tmpfiles.settings."10-heimdal" = let
+      databases = lib.pipe finalConfig.kdc.database [
+        (map (dbAttrs: dbAttrs.dbname or null))
+        (lib.filter (x: x != null))
+        lib.unique
+      ];
+    in lib.genAttrs databases (_: {
+      d = {
+        user = "root";
+        group = "root";
+        mode = "0700";
+      };
+    });
 
-  config = mkIf (cfg.enable && kerberos == pkgs.heimdal) {
     systemd.services.kadmind = {
       description = "Kerberos Administration Daemon";
-      wantedBy = [ "multi-user.target" ];
-      preStart = ''
-        mkdir -m 0755 -p ${stateDir}
-      '';
-      serviceConfig.ExecStart =
-        "${kerberos}/libexec/kadmind --config-file=/etc/heimdal-kdc/kdc.conf";
+      partOf = [ "kerberos-server.target" ];
+      wantedBy = [ "kerberos-server.target" ];
+      serviceConfig = {
+        ExecStart = "${package}/libexec/kadmind --config-file=/etc/heimdal-kdc/kdc.conf";
+        Slice = "system-kerberos-server.slice";
+        StateDirectory = "heimdal";
+      };
       restartTriggers = [ kdcConfFile ];
     };
 
     systemd.services.kdc = {
       description = "Key Distribution Center daemon";
-      wantedBy = [ "multi-user.target" ];
-      preStart = ''
-        mkdir -m 0755 -p ${stateDir}
-      '';
-      serviceConfig.ExecStart =
-        "${kerberos}/libexec/kdc --config-file=/etc/heimdal-kdc/kdc.conf";
+      partOf = [ "kerberos-server.target" ];
+      wantedBy = [ "kerberos-server.target" ];
+      serviceConfig = {
+        ExecStart = "${package}/libexec/kdc --config-file=/etc/heimdal-kdc/kdc.conf";
+        Slice = "system-kerberos-server.slice";
+        StateDirectory = "heimdal";
+      };
       restartTriggers = [ kdcConfFile ];
     };
 
     systemd.services.kpasswdd = {
       description = "Kerberos Password Changing daemon";
-      wantedBy = [ "multi-user.target" ];
-      preStart = ''
-        mkdir -m 0755 -p ${stateDir}
-      '';
-      serviceConfig.ExecStart = "${kerberos}/libexec/kpasswdd";
+      partOf = [ "kerberos-server.target" ];
+      wantedBy = [ "kerberos-server.target" ];
+      serviceConfig = {
+        ExecStart = "${package}/libexec/kpasswdd";
+        Slice = "system-kerberos-server.slice";
+        StateDirectory = "heimdal";
+      };
       restartTriggers = [ kdcConfFile ];
     };
-
-    environment.etc = {
-      # Can be set via the --config-file option to KDC
-      "heimdal-kdc/kdc.conf".source = kdcConfFile;
-    };
   };
 }
diff --git a/nixos/modules/services/system/kerberos/kerberos-server.md b/nixos/modules/services/system/kerberos/kerberos-server.md
new file mode 100644
index 0000000000000..80c71be1541e4
--- /dev/null
+++ b/nixos/modules/services/system/kerberos/kerberos-server.md
@@ -0,0 +1,55 @@
+# kerberos_server {#module-services-kerberos-server}
+
+Kerberos is a computer-network authentication protocol that works on the basis of tickets to allow nodes communicating over a non-secure network to prove their identity to one another in a secure manner.
+
+This module provides both the MIT and Heimdal implementations of the a Kerberos server.
+
+## Usage {#module-services-kerberos-server-usage}
+
+To enable a Kerberos server:
+
+```nix
+{
+  security.krb5 = {
+    # Here you can choose between the MIT and Heimdal implementations.
+    package = pkgs.krb5;
+    # package = pkgs.heimdal;
+
+    # Optionally set up a client on the same machine as the server
+    enable = true;
+    settings = {
+      libdefaults.default_realm = "EXAMPLE.COM";
+      realms."EXAMPLE.COM" = {
+        kdc = "kerberos.example.com";
+        admin_server = "kerberos.example.com";
+      };
+    };
+  }
+
+  services.kerberos-server = {
+    enable = true;
+    settings = {
+      realms."EXAMPLE.COM" = {
+        acl = [{ principal = "adminuser"; access=  ["add" "cpw"]; }];
+      };
+    };
+  };
+}
+```
+
+## Notes {#module-services-kerberos-server-notes}
+
+- The Heimdal documentation will sometimes assume that state is stored in `/var/heimdal`, but this module uses `/var/lib/heimdal` instead.
+- Due to the heimdal implementation being chosen through `security.krb5.package`, it is not possible to have a system with one implementation of the client and another of the server.
+- While `services.kerberos_server.settings` has a common freeform type between the two implementations, the actual settings that can be set can vary between the two implementations. To figure out what settings are available, you should consult the upstream documentation for the implementation you are using.
+
+## Upstream Documentation {#module-services-kerberos-server-upstream-documentation}
+
+- MIT Kerberos homepage: https://web.mit.edu/kerberos
+- MIT Kerberos docs: https://web.mit.edu/kerberos/krb5-latest/doc/index.html
+
+- Heimdal Kerberos GitHub wiki: https://github.com/heimdal/heimdal/wiki
+- Heimdal kerberos doc manpages (Debian unstable): https://manpages.debian.org/unstable/heimdal-docs/index.html
+- Heimdal Kerberos kdc manpages (Debian unstable): https://manpages.debian.org/unstable/heimdal-kdc/index.html
+
+Note the version number in the URLs, it may be different for the latest version.
diff --git a/nixos/modules/services/system/kerberos/mit.nix b/nixos/modules/services/system/kerberos/mit.nix
index a654bd1fe7e1b..9ce58986e27af 100644
--- a/nixos/modules/services/system/kerberos/mit.nix
+++ b/nixos/modules/services/system/kerberos/mit.nix
@@ -1,31 +1,37 @@
 { pkgs, config, lib, ... } :
 
 let
-  inherit (lib) mkIf concatStrings concatStringsSep concatMapStrings toList
-    mapAttrs mapAttrsToList;
+  inherit (lib) mapAttrs;
   cfg = config.services.kerberos_server;
-  kerberos = config.security.krb5.package;
-  stateDir = "/var/lib/krb5kdc";
+  package = config.security.krb5.package;
   PIDFile = "/run/kdc.pid";
+
+  format = import ../../../security/krb5/krb5-conf-format.nix { inherit pkgs lib; } { enableKdcACLEntries = true; };
+
   aclMap = {
     add = "a"; cpw = "c"; delete = "d"; get = "i"; list = "l"; modify = "m";
     all = "*";
   };
-  aclFiles = mapAttrs
-    (name: {acl, ...}: (pkgs.writeText "${name}.acl" (concatMapStrings (
-      {principal, access, target, ...} :
-      let access_code = map (a: aclMap.${a}) (toList access); in
-      "${principal} ${concatStrings access_code} ${target}\n"
-    ) acl))) cfg.realms;
-  kdcConfigs = mapAttrsToList (name: value: ''
-    ${name} = {
-      acl_file = ${value}
-    }
-  '') aclFiles;
-  kdcConfFile = pkgs.writeText "kdc.conf" ''
-    [realms]
-    ${concatStringsSep "\n" kdcConfigs}
-  '';
+
+  aclConfigs = lib.pipe cfg.settings.realms [
+    (mapAttrs (name: { acl, ... }: lib.concatMapStringsSep "\n" (
+      { principal, access, target, ... }: let
+        access_code = map (a: aclMap.${a}) (lib.toList access);
+      in "${principal} ${lib.concatStrings access_code} ${target}"
+    ) acl))
+
+    (lib.concatMapAttrs (name: text: {
+      ${name} = {
+        acl_file = pkgs.writeText "${name}.acl" text;
+      };
+    }))
+  ];
+
+  finalConfig = cfg.settings // {
+    realms = mapAttrs (n: v: (removeAttrs v [ "acl" ]) // aclConfigs.${n}) (cfg.settings.realms or { });
+  };
+
+  kdcConfFile = format.generate "kdc.conf" finalConfig;
   env = {
     # What Debian uses, could possibly link directly to Nix store?
     KRB5_KDC_PROFILE = "/etc/krb5kdc/kdc.conf";
@@ -33,36 +39,38 @@ let
 in
 
 {
-  config = mkIf (cfg.enable && kerberos == pkgs.krb5) {
+  config = lib.mkIf (cfg.enable && package.passthru.implementation == "krb5") {
+    environment = {
+      etc."krb5kdc/kdc.conf".source = kdcConfFile;
+      variables = env;
+    };
+
     systemd.services.kadmind = {
       description = "Kerberos Administration Daemon";
-      wantedBy = [ "multi-user.target" ];
-      preStart = ''
-        mkdir -m 0755 -p ${stateDir}
-      '';
-      serviceConfig.ExecStart = "${kerberos}/bin/kadmind -nofork";
+      partOf = [ "kerberos-server.target" ];
+      wantedBy = [ "kerberos-server.target" ];
+      serviceConfig = {
+        ExecStart = "${package}/bin/kadmind -nofork";
+        Slice = "system-kerberos-server.slice";
+        StateDirectory = "krb5kdc";
+      };
       restartTriggers = [ kdcConfFile ];
       environment = env;
     };
 
     systemd.services.kdc = {
       description = "Key Distribution Center daemon";
-      wantedBy = [ "multi-user.target" ];
-      preStart = ''
-        mkdir -m 0755 -p ${stateDir}
-      '';
+      partOf = [ "kerberos-server.target" ];
+      wantedBy = [ "kerberos-server.target" ];
       serviceConfig = {
         Type = "forking";
         PIDFile = PIDFile;
-        ExecStart = "${kerberos}/bin/krb5kdc -P ${PIDFile}";
+        ExecStart = "${package}/bin/krb5kdc -P ${PIDFile}";
+        Slice = "system-kerberos-server.slice";
+        StateDirectory = "krb5kdc";
       };
       restartTriggers = [ kdcConfFile ];
       environment = env;
     };
-
-    environment.etc = {
-      "krb5kdc/kdc.conf".source = kdcConfFile;
-    };
-    environment.variables = env;
   };
 }
diff --git a/nixos/modules/services/ttys/getty.nix b/nixos/modules/services/ttys/getty.nix
index 011016dd5fd14..e88bb4628635e 100644
--- a/nixos/modules/services/ttys/getty.nix
+++ b/nixos/modules/services/ttys/getty.nix
@@ -101,7 +101,7 @@ in
   config = {
     # Note: this is set here rather than up there so that changing
     # nixos.label would not rebuild manual pages
-    services.getty.greetingLine = mkDefault ''<<< Welcome to NixOS ${config.system.nixos.label} (\m) - \l >>>'';
+    services.getty.greetingLine = mkDefault ''<<< Welcome to ${config.system.nixos.distroName} ${config.system.nixos.label} (\m) - \l >>>'';
     services.getty.helpLine = mkIf (config.documentation.nixos.enable && config.documentation.doc.enable) "\nRun 'nixos-help' for the NixOS manual.";
 
     systemd.services."getty@" =
@@ -158,4 +158,5 @@ in
 
   };
 
+  meta.maintainers = with maintainers; [ RossComputerGuy ];
 }
diff --git a/nixos/modules/services/web-apps/freshrss.nix b/nixos/modules/services/web-apps/freshrss.nix
index 77c5ecb246171..1035c961c02c9 100644
--- a/nixos/modules/services/web-apps/freshrss.nix
+++ b/nixos/modules/services/web-apps/freshrss.nix
@@ -255,13 +255,10 @@ in
         {
           description = "Set up the state directory for FreshRSS before use";
           wantedBy = [ "multi-user.target" ];
-          serviceConfig = defaultServiceConfig //{
-            Type = "oneshot";
-            User = "freshrss";
-            Group = "freshrss";
-            StateDirectory = "freshrss";
-            WorkingDirectory = cfg.package;
+          serviceConfig = defaultServiceConfig // {
+            RemainAfterExit = true;
           };
+          restartIfChanged = true;
           environment = {
             DATA_PATH = cfg.dataDir;
           };
@@ -299,7 +296,7 @@ in
         environment = {
           DATA_PATH = cfg.dataDir;
         };
-        serviceConfig = defaultServiceConfig //{
+        serviceConfig = defaultServiceConfig // {
           ExecStart = "${cfg.package}/app/actualize_script.php";
         };
       };
diff --git a/nixos/modules/services/x11/window-managers/qtile.nix b/nixos/modules/services/x11/window-managers/qtile.nix
index 700ead8366008..4603ca3fb50f0 100644
--- a/nixos/modules/services/x11/window-managers/qtile.nix
+++ b/nixos/modules/services/x11/window-managers/qtile.nix
@@ -7,6 +7,10 @@ let
 in
 
 {
+  imports = [
+    (mkRemovedOptionModule [ "services" "xserver" "windowManager" "qtile" "backend" ] "The qtile package now provides separate display sessions for both X11 and Wayland.")
+  ];
+
   options.services.xserver.windowManager.qtile = {
     enable = mkEnableOption "qtile";
 
@@ -22,14 +26,6 @@ in
       '';
     };
 
-    backend = mkOption {
-      type = types.enum [ "x11" "wayland" ];
-      default = "x11";
-      description = ''
-          Backend to use in qtile: `x11` or `wayland`.
-      '';
-    };
-
     extraPackages = mkOption {
         type = types.functionTo (types.listOf types.package);
         default = _: [];
@@ -57,25 +53,14 @@ in
   };
 
   config = mkIf cfg.enable {
-    services.xserver.windowManager.qtile.finalPackage = pkgs.python3.withPackages (p:
-      [ (cfg.package.unwrapped or cfg.package) ] ++ (cfg.extraPackages p)
-    );
-
-    services.xserver.windowManager.session = [{
-      name = "qtile";
-      start = ''
-        ${cfg.finalPackage}/bin/qtile start -b ${cfg.backend} \
-        ${optionalString (cfg.configFile != null)
-        "--config \"${cfg.configFile}\""} &
-        waitPID=$!
-      '';
-    }];
+    services = {
+      xserver.windowManager.qtile.finalPackage = pkgs.python3.pkgs.qtile.override { extraPackages = cfg.extraPackages pkgs.python3.pkgs; };
+      displayManager.sessionPackages = [ cfg.finalPackage ];
+    };
 
-    environment.systemPackages = [
-      # pkgs.qtile is currently a buildenv of qtile and its dependencies.
-      # For userland commands, we want the underlying package so that
-      # packages such as python don't bleed into userland and overwrite intended behavior.
-      (cfg.package.unwrapped or cfg.package)
-    ];
+    environment = {
+      etc."xdg/qtile/config.py" = mkIf (cfg.configFile != null) { source = cfg.configFile; };
+      systemPackages = [ cfg.finalPackage ];
+    };
   };
 }
diff --git a/nixos/modules/system/boot/initrd-ssh.nix b/nixos/modules/system/boot/initrd-ssh.nix
index 9ce5a85b4f073..d1cd601c2d9b1 100644
--- a/nixos/modules/system/boot/initrd-ssh.nix
+++ b/nixos/modules/system/boot/initrd-ssh.nix
@@ -82,7 +82,7 @@ in
       type = types.bool;
       default = false;
       description = ''
-        Allow leaving {option}`config.boot.initrd.network.ssh` empty,
+        Allow leaving {option}`config.boot.initrd.network.ssh.hostKeys` empty,
         to deploy ssh host keys out of band.
       '';
     };
diff --git a/nixos/modules/system/boot/networkd.nix b/nixos/modules/system/boot/networkd.nix
index 79d76a8caa94b..761bbe6e03d4a 100644
--- a/nixos/modules/system/boot/networkd.nix
+++ b/nixos/modules/system/boot/networkd.nix
@@ -952,6 +952,7 @@ let
           "UseGateway"
           "UseRoutePrefix"
           "Token"
+          "UsePREF64"
         ])
         (assertValueOneOf "UseDNS" boolValues)
         (assertValueOneOf "UseDomains" (boolValues ++ ["route"]))
@@ -962,6 +963,7 @@ let
         (assertValueOneOf "UseMTU" boolValues)
         (assertValueOneOf "UseGateway" boolValues)
         (assertValueOneOf "UseRoutePrefix" boolValues)
+        (assertValueOneOf "UsePREF64" boolValues)
       ];
 
       sectionDHCPServer = checkUnitConfig "DHCPServer" [
@@ -1033,6 +1035,14 @@ let
         (assertValueOneOf "EmitDomains" boolValues)
       ];
 
+      sectionIPv6PREF64Prefix = checkUnitConfigWithLegacyKey "ipv6PREF64PrefixConfig" "IPv6PREF64Prefix" [
+        (assertOnlyFields [
+          "Prefix"
+          "LifetimeSec"
+        ])
+        (assertInt "LifetimeSec")
+      ];
+
       sectionIPv6Prefix = checkUnitConfigWithLegacyKey "ipv6PrefixConfig" "IPv6Prefix" [
         (assertOnlyFields [
           "AddressAutoconfiguration"
@@ -2013,6 +2023,16 @@ let
       '';
     };
 
+    ipv6PREF64Prefixes = mkOption {
+      default = [];
+      example = [ { Prefix = "64:ff9b::/96"; } ];
+      type = types.listOf (mkSubsectionType "ipv6PREF64PrefixConfig" check.network.sectionIPv6PREF64Prefix);
+      description = ''
+        A list of IPv6PREF64Prefix sections to be added to the unit. See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
     dhcpServerStaticLeases = mkOption {
       default = [];
       example = [ { MACAddress = "65:43:4a:5b:d8:5f"; Address = "192.168.1.42"; } ];
diff --git a/nixos/modules/system/etc/build-composefs-dump.py b/nixos/modules/system/etc/build-composefs-dump.py
index bba454dd888d6..fe739a621ec4d 100644
--- a/nixos/modules/system/etc/build-composefs-dump.py
+++ b/nixos/modules/system/etc/build-composefs-dump.py
@@ -175,7 +175,7 @@ def main() -> None:
                 paths[glob_target] = composefs_path
                 add_leading_directories(glob_target, attrs, paths)
         else:  # Without globbing
-            if mode == "symlink":
+            if mode == "symlink" or mode == "direct-symlink":
                 composefs_path = ComposefsPath(
                     attrs,
                     # A high approximation of the size of a symlink
@@ -184,24 +184,23 @@ def main() -> None:
                     mode="0777",
                     payload=source,
                 )
+            elif os.path.isdir(source):
+                composefs_path = ComposefsPath(
+                    attrs,
+                    size=4096,
+                    filetype=FileType.directory,
+                    mode=mode,
+                    payload=source,
+                )
             else:
-                if os.path.isdir(source):
-                    composefs_path = ComposefsPath(
-                        attrs,
-                        size=4096,
-                        filetype=FileType.directory,
-                        mode=mode,
-                        payload=source,
-                    )
-                else:
-                    composefs_path = ComposefsPath(
-                        attrs,
-                        size=os.stat(source).st_size,
-                        filetype=FileType.file,
-                        mode=mode,
-                        # payload needs to be relative path in this case
-                        payload=target.lstrip("/"),
-                    )
+                composefs_path = ComposefsPath(
+                    attrs,
+                    size=os.stat(source).st_size,
+                    filetype=FileType.file,
+                    mode=mode,
+                    # payload needs to be relative path in this case
+                    payload=target.lstrip("/"),
+                )
             paths[target] = composefs_path
             add_leading_directories(target, attrs, paths)
 
diff --git a/nixos/modules/system/etc/etc.nix b/nixos/modules/system/etc/etc.nix
index 9fded1e1c9742..80ca69e495e9d 100644
--- a/nixos/modules/system/etc/etc.nix
+++ b/nixos/modules/system/etc/etc.nix
@@ -62,7 +62,7 @@ let
     ]) etc'}
   '';
 
-  etcHardlinks = filter (f: f.mode != "symlink") etc';
+  etcHardlinks = filter (f: f.mode != "symlink" && f.mode != "direct-symlink") etc';
 
   build-composefs-dump = pkgs.runCommand "build-composefs-dump.py"
     {
diff --git a/nixos/tests/activation/etc-overlay-immutable.nix b/nixos/tests/activation/etc-overlay-immutable.nix
index f347f9cf8efe2..f0abf70d350ff 100644
--- a/nixos/tests/activation/etc-overlay-immutable.nix
+++ b/nixos/tests/activation/etc-overlay-immutable.nix
@@ -13,6 +13,7 @@
     users.mutableUsers = false;
     boot.initrd.systemd.enable = true;
     boot.kernelPackages = pkgs.linuxPackages_latest;
+    time.timeZone = "Utc";
 
     specialisation.new-generation.configuration = {
       environment.etc."newgen".text = "newgen";
@@ -23,6 +24,9 @@
     with subtest("/etc is mounted as an overlay"):
       machine.succeed("findmnt --kernel --type overlay /etc")
 
+    with subtest("direct symlinks point to the target without indirection"):
+      assert machine.succeed("readlink -n /etc/localtime") == "/etc/zoneinfo/Utc"
+
     with subtest("switching to the same generation"):
       machine.succeed("/run/current-system/bin/switch-to-configuration test")
 
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index ddeeeb298c356..746b29fd27258 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -115,6 +115,7 @@ in {
   akkoma = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./akkoma.nix {};
   akkoma-confined = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./akkoma.nix { confined = true; };
   alice-lg = handleTest ./alice-lg.nix {};
+  alloy = handleTest ./alloy.nix {};
   allTerminfo = handleTest ./all-terminfo.nix {};
   alps = handleTest ./alps.nix {};
   amazon-init-shell = handleTest ./amazon-init-shell.nix {};
@@ -129,6 +130,7 @@ in {
   appliance-repart-image = runTest ./appliance-repart-image.nix;
   apparmor = handleTest ./apparmor.nix {};
   archi = handleTest ./archi.nix {};
+  aria2 = handleTest ./aria2.nix {};
   armagetronad = handleTest ./armagetronad.nix {};
   artalk = handleTest ./artalk.nix {};
   atd = handleTest ./atd.nix {};
@@ -379,6 +381,7 @@ in {
   grafana-agent = handleTest ./grafana-agent.nix {};
   graphite = handleTest ./graphite.nix {};
   graylog = handleTest ./graylog.nix {};
+  greetd-no-shadow = handleTest ./greetd-no-shadow.nix {};
   grocy = handleTest ./grocy.nix {};
   grow-partition = runTest ./grow-partition.nix;
   grub = handleTest ./grub.nix {};
@@ -720,6 +723,7 @@ in {
   pg_anonymizer = handleTest ./pg_anonymizer.nix {};
   pgadmin4 = handleTest ./pgadmin4.nix {};
   pgbouncer = handleTest ./pgbouncer.nix {};
+  pghero = runTest ./pghero.nix;
   pgjwt = handleTest ./pgjwt.nix {};
   pgmanage = handleTest ./pgmanage.nix {};
   pgvecto-rs = handleTest ./pgvecto-rs.nix {};
@@ -800,6 +804,7 @@ in {
   redis = handleTest ./redis.nix {};
   redlib = handleTest ./redlib.nix {};
   redmine = handleTest ./redmine.nix {};
+  renovate = handleTest ./renovate.nix {};
   restartByActivationScript = handleTest ./restart-by-activation-script.nix {};
   restic-rest-server = handleTest ./restic-rest-server.nix {};
   restic = handleTest ./restic.nix {};
@@ -1014,7 +1019,7 @@ in {
   vault-agent = handleTest ./vault-agent.nix {};
   vault-dev = handleTest ./vault-dev.nix {};
   vault-postgresql = handleTest ./vault-postgresql.nix {};
-  vaultwarden = handleTest ./vaultwarden.nix {};
+  vaultwarden = discoverTests (import ./vaultwarden.nix);
   vector = handleTest ./vector {};
   vengi-tools = handleTest ./vengi-tools.nix {};
   victoriametrics = handleTest ./victoriametrics.nix {};
diff --git a/nixos/tests/alloy.nix b/nixos/tests/alloy.nix
new file mode 100644
index 0000000000000..d87492127d5bb
--- /dev/null
+++ b/nixos/tests/alloy.nix
@@ -0,0 +1,32 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }:
+
+  let
+    nodes = {
+      machine = {
+        services.alloy = {
+          enable = true;
+        };
+        environment.etc."alloy/config.alloy".text = "";
+      };
+    };
+  in
+  {
+    name = "alloy";
+
+    meta = with lib.maintainers; {
+      maintainers = [ flokli hbjydev ];
+    };
+
+    inherit nodes;
+
+    testScript = ''
+      start_all()
+
+      machine.wait_for_unit("alloy.service")
+      machine.wait_for_open_port(12345)
+      machine.succeed(
+          "curl -sSfN http://127.0.0.1:12345/-/healthy"
+      )
+      machine.shutdown()
+    '';
+  })
diff --git a/nixos/tests/aria2.nix b/nixos/tests/aria2.nix
new file mode 100644
index 0000000000000..48fe2094b5dcf
--- /dev/null
+++ b/nixos/tests/aria2.nix
@@ -0,0 +1,43 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+let
+  rpcSecret = "supersecret";
+  rpc-listen-port = 6800;
+  curlBody = {
+    jsonrpc = 2.0;
+    id = 1;
+    method = "aria2.getVersion";
+    params = [ "token:${rpcSecret}" ];
+  };
+in
+rec {
+  name = "aria2";
+
+  nodes.machine = {
+    environment.etc."aria2Rpc".text = rpcSecret;
+    services.aria2 = {
+      enable = true;
+      rpcSecretFile = "/etc/aria2Rpc";
+      settings = {
+        inherit rpc-listen-port;
+        allow-overwrite = false;
+        check-integrity = true;
+        console-log-level = "warn";
+        listen-port = [{ from = 20000; to = 20010; } { from = 22222; to = 22222; }];
+        max-concurrent-downloads = 50;
+        seed-ratio = 1.2;
+        summary-interval = 0;
+      };
+    };
+  };
+
+  testScript = ''
+    machine.start()
+    machine.wait_for_unit("aria2.service")
+    curl_cmd = 'curl --fail-with-body -X POST -H "Content-Type: application/json" \
+                -d \'${builtins.toJSON curlBody}\' http://localhost:${toString rpc-listen-port}/jsonrpc'
+    print(machine.wait_until_succeeds(curl_cmd, timeout=10))
+    machine.shutdown()
+  '';
+
+  meta.maintainers = [ pkgs.lib.maintainers.timhae ];
+})
diff --git a/nixos/tests/clatd.nix b/nixos/tests/clatd.nix
index f4d2242ce54f4..d0d504851ce4e 100644
--- a/nixos/tests/clatd.nix
+++ b/nixos/tests/clatd.nix
@@ -6,8 +6,8 @@
 # Client | clat    Address: 192.0.0.1/32  (configured via clatd)
 #        |         Route:   default
 #        |
-#        | eth1    Address: 2001:db8::2/64
-#        |  |      Route:   default via 2001:db8::1
+#        | eth1    Address: Assigned via SLAAC within 2001:db8::/64
+#        |  |      Route:   default via IPv6LL address
 #        +--|---
 #           | VLAN 3
 #        +--|---
@@ -31,7 +31,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
 {
   name = "clatd";
   meta = with pkgs.lib.maintainers; {
-    maintainers = [ hax404 ];
+    maintainers = [ hax404 jmbaur ];
   };
 
   nodes = {
@@ -66,18 +66,19 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
     };
 
     # The router is configured with static IPv4 addresses towards the server
-    # and IPv6 addresses towards the client. For NAT64, the Well-Known prefix
-    # 64:ff9b::/96 is used. NAT64 is done with TAYGA which provides the
-    # tun-interface nat64 and does the translation over it. The IPv6 packets
-    # are sent to this interfaces and received as IPv4 packets and vice versa.
-    # As TAYGA only translates IPv6 addresses to dedicated IPv4 addresses, it
-    # needs a pool of IPv4 addresses which must be at least as big as the
-    # expected amount of clients. In this test, the packets from the pool are
-    # directly routed towards the client. In normal cases, there would be a
-    # second source NAT44 to map all clients behind one IPv4 address.
+    # and IPv6 addresses towards the client. DNS64 is exposed towards the
+    # client so clatd is able to auto-discover the PLAT prefix. For NAT64, the
+    # Well-Known prefix 64:ff9b::/96 is used. NAT64 is done with TAYGA which
+    # provides the tun-interface nat64 and does the translation over it. The
+    # IPv6 packets are sent to this interfaces and received as IPv4 packets and
+    # vice versa. As TAYGA only translates IPv6 addresses to dedicated IPv4
+    # addresses, it needs a pool of IPv4 addresses which must be at least as
+    # big as the expected amount of clients. In this test, the packets from the
+    # pool are directly routed towards the client. In normal cases, there would
+    # be a second source NAT44 to map all clients behind one IPv4 address.
     router = {
       boot.kernel.sysctl = {
-        "net.ipv4.ip_forward" = 1;
+        "net.ipv4.conf.all.forwarding" = 1;
         "net.ipv6.conf.all.forwarding" = 1;
       };
 
@@ -102,6 +103,36 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
         };
       };
 
+      systemd.network.networks."40-eth2" = {
+        networkConfig.IPv6SendRA = true;
+        ipv6Prefixes = [ { Prefix = "2001:db8::/64"; } ];
+        ipv6PREF64Prefixes = [ { Prefix = "64:ff9b::/96"; } ];
+        ipv6SendRAConfig = {
+          EmitDNS = true;
+          DNS = "_link_local";
+        };
+      };
+
+      services.resolved.extraConfig = ''
+        DNSStubListener=no
+      '';
+
+      networking.extraHosts = ''
+        192.0.0.171 ipv4only.arpa
+        192.0.0.170 ipv4only.arpa
+      '';
+
+      services.coredns = {
+        enable = true;
+        config = ''
+          .:53 {
+            bind ::
+            hosts /etc/hosts
+            dns64 64:ff9b::/96
+          }
+        '';
+      };
+
       services.tayga = {
         enable = true;
         ipv4 = {
@@ -127,10 +158,10 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
       };
     };
 
-    # The client is configured with static IPv6 addresses. It has also a static
-    # default route towards the router. To reach the IPv4-only server, the
-    # client starts the clat daemon which starts and configures the local
-    # IPv4 -> IPv6 translation via Tayga.
+    # The client uses SLAAC to assign IPv6 addresses. To reach the IPv4-only
+    # server, the client starts the clat daemon which starts and configures the
+    # local IPv4 -> IPv6 translation via Tayga after discovering the PLAT
+    # prefix via DNS64.
     client = {
       virtualisation.vlans = [
         3 # towards router
@@ -145,25 +176,36 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
         enable = true;
         networks."vlan1" = {
           matchConfig.Name = "eth1";
-          address = [
-            "2001:db8::2/64"
-          ];
-          routes = [
-            { Destination = "::/0"; Gateway = "2001:db8::1"; }
-          ];
+
+          # NOTE: clatd does not actually use the PREF64 prefix discovered by
+          # systemd-networkd (nor does systemd-networkd do anything with it,
+          # yet), but we set this to confirm it works. See the test script
+          # below.
+          ipv6AcceptRAConfig.UsePREF64 = true;
         };
       };
 
       services.clatd = {
         enable = true;
-        settings.plat-prefix = "64:ff9b::/96";
+        # NOTE: Perl's Net::DNS resolver does not seem to work well querying
+        # for AAAA records to systemd-resolved's default IPv4 bind address
+        # (127.0.0.53), so we add an IPv6 listener address to systemd-resolved
+        # and tell clatd to use that instead.
+        settings.dns64-servers = "::1";
       };
 
+      # Allow clatd to find dns server. See comment above.
+      services.resolved.extraConfig = ''
+        DNSStubListenerExtra=::1
+      '';
+
       environment.systemPackages = [ pkgs.mtr ];
     };
   };
 
   testScript = ''
+    import json
+
     start_all()
 
     # wait for all machines to start up
@@ -178,6 +220,11 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
         'journalctl -u clatd -e | grep -q "Starting up TAYGA, using config file"'
       )
 
+    with subtest("networkd exports PREF64 prefix"):
+      assert json.loads(client.succeed("networkctl status eth1 --json=short"))[
+          "NDisc"
+      ]["PREF64"][0]["Prefix"] == [0x0, 0x64, 0xFF, 0x9B] + ([0] * 12)
+
     with subtest("Test ICMP"):
       client.wait_until_succeeds("ping -c 3 100.64.0.2 >&2")
 
diff --git a/nixos/tests/firefly-iii.nix b/nixos/tests/firefly-iii.nix
index 2373ba8360264..f8e4ca4bfe2b4 100644
--- a/nixos/tests/firefly-iii.nix
+++ b/nixos/tests/firefly-iii.nix
@@ -39,12 +39,13 @@ in
         DB_DATABASE = "firefly";
         DB_USERNAME = "firefly";
         DB_PASSWORD_FILE = "/etc/postgres-pass";
+        PGSQL_SCHEMA = "firefly";
       };
     };
 
     services.postgresql = {
       enable = true;
-      package = pkgs.postgresql_15;
+      package = pkgs.postgresql_16;
       authentication = ''
         local all postgres peer
         local firefly firefly password
@@ -52,6 +53,7 @@ in
       initialScript = pkgs.writeText "firefly-init.sql" ''
         CREATE USER "firefly" WITH LOGIN PASSWORD '${db-pass}';
         CREATE DATABASE "firefly" WITH OWNER "firefly";
+        \c firefly
         CREATE SCHEMA AUTHORIZATION firefly;
       '';
     };
diff --git a/nixos/tests/greetd-no-shadow.nix b/nixos/tests/greetd-no-shadow.nix
new file mode 100644
index 0000000000000..382218ffa948f
--- /dev/null
+++ b/nixos/tests/greetd-no-shadow.nix
@@ -0,0 +1,49 @@
+import ./make-test-python.nix ({ pkgs, latestKernel ? false, ... }:
+{
+  name = "greetd-no-shadow";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ ];
+  };
+
+  nodes.machine =
+    { pkgs, lib, ... }: {
+
+      users.users.alice = {
+        isNormalUser = true;
+        group = "alice";
+        password = "foobar";
+      };
+      users.groups.alice = {};
+
+      # This means login(1) breaks, so we must use greetd/agreety instead.
+      security.shadow.enable = false;
+
+      services.greetd = {
+        enable = true;
+        settings = {
+          default_session = {
+            command = "${pkgs.greetd.greetd}/bin/agreety --cmd bash";
+          };
+        };
+      };
+    };
+
+  testScript = ''
+      machine.start()
+
+      machine.wait_for_unit("multi-user.target")
+      machine.wait_until_succeeds("pgrep -f 'agretty.*tty1'")
+      machine.screenshot("postboot")
+
+      with subtest("Log in as alice on a virtual console"):
+          machine.wait_until_tty_matches("1", "login: ")
+          machine.send_chars("alice\n")
+          machine.wait_until_tty_matches("1", "login: alice")
+          machine.wait_until_succeeds("pgrep login")
+          machine.wait_until_tty_matches("1", "Password: ")
+          machine.send_chars("foobar\n")
+          machine.wait_until_succeeds("pgrep -u alice bash")
+          machine.send_chars("touch done\n")
+          machine.wait_for_file("/home/alice/done")
+  '';
+})
diff --git a/nixos/tests/kerberos/heimdal.nix b/nixos/tests/kerberos/heimdal.nix
index 393289f7a92ca..098080a84592e 100644
--- a/nixos/tests/kerberos/heimdal.nix
+++ b/nixos/tests/kerberos/heimdal.nix
@@ -4,7 +4,7 @@ import ../make-test-python.nix ({pkgs, ...}: {
   nodes.machine = { config, libs, pkgs, ...}:
   { services.kerberos_server =
     { enable = true;
-      realms = {
+      settings.realms = {
         "FOO.BAR".acl = [{principal = "admin"; access = ["add" "cpw"];}];
       };
     };
diff --git a/nixos/tests/kerberos/mit.nix b/nixos/tests/kerberos/mit.nix
index 1191d047abbf0..172261f95fe6b 100644
--- a/nixos/tests/kerberos/mit.nix
+++ b/nixos/tests/kerberos/mit.nix
@@ -4,7 +4,7 @@ import ../make-test-python.nix ({pkgs, ...}: {
   nodes.machine = { config, libs, pkgs, ...}:
   { services.kerberos_server =
     { enable = true;
-      realms = {
+      settings.realms = {
         "FOO.BAR".acl = [{principal = "admin"; access = ["add" "cpw"];}];
       };
     };
diff --git a/nixos/tests/kubo/default.nix b/nixos/tests/kubo/default.nix
index d8c0c69dc1fbd..629922fc366db 100644
--- a/nixos/tests/kubo/default.nix
+++ b/nixos/tests/kubo/default.nix
@@ -1,7 +1,5 @@
 { recurseIntoAttrs, runTest }:
 recurseIntoAttrs {
   kubo = runTest ./kubo.nix;
-  # The FUSE functionality is completely broken since Kubo v0.24.0
-  # See https://github.com/ipfs/kubo/issues/10242
-  # kubo-fuse = runTest ./kubo-fuse.nix;
+  kubo-fuse = runTest ./kubo-fuse.nix;
 }
diff --git a/nixos/tests/kubo/kubo-fuse.nix b/nixos/tests/kubo/kubo-fuse.nix
index 71a5bf61649f6..c8c273fc0dfc7 100644
--- a/nixos/tests/kubo/kubo-fuse.nix
+++ b/nixos/tests/kubo/kubo-fuse.nix
@@ -23,7 +23,7 @@
 
     with subtest("FUSE mountpoint"):
         machine.fail("echo a | su bob -l -c 'ipfs add --quieter'")
-        # The FUSE mount functionality is broken as of v0.13.0 and v0.17.0.
+        # The FUSE mount functionality is broken as of v0.13.0. This is still the case with v0.29.0.
         # See https://github.com/ipfs/kubo/issues/9044.
         # Workaround: using CID Version 1 avoids that.
         ipfs_hash = machine.succeed(
diff --git a/nixos/tests/pghero.nix b/nixos/tests/pghero.nix
new file mode 100644
index 0000000000000..bce32da008862
--- /dev/null
+++ b/nixos/tests/pghero.nix
@@ -0,0 +1,63 @@
+let
+  pgheroPort = 1337;
+  pgheroUser = "pghero";
+  pgheroPass = "pghero";
+in
+{ lib, ... }: {
+  name = "pghero";
+  meta.maintainers = [ lib.maintainers.tie ];
+
+  nodes.machine = { config, ... }: {
+    services.postgresql = {
+      enable = true;
+      # This test uses default peer authentication (socket and its directory is
+      # world-readably by default), so we essentially test that we can connect
+      # with DynamicUser= set.
+      ensureUsers = [{
+        name = "pghero";
+        ensureClauses.superuser = true;
+      }];
+    };
+    services.pghero = {
+      enable = true;
+      listenAddress = "[::]:${toString pgheroPort}";
+      settings = {
+        databases = {
+          postgres.url = "<%= ENV['POSTGRES_DATABASE_URL'] %>";
+          nulldb.url = "nulldb:///";
+        };
+      };
+      environment = {
+        PGHERO_USERNAME = pgheroUser;
+        PGHERO_PASSWORD = pgheroPass;
+        POSTGRES_DATABASE_URL = "postgresql:///postgres?host=/run/postgresql";
+      };
+    };
+  };
+
+  testScript = ''
+    pgheroPort = ${toString pgheroPort}
+    pgheroUser = "${pgheroUser}"
+    pgheroPass = "${pgheroPass}"
+
+    pgheroUnauthorizedURL = f"http://localhost:{pgheroPort}"
+    pgheroBaseURL = f"http://{pgheroUser}:{pgheroPass}@localhost:{pgheroPort}"
+
+    def expect_http_code(node, code, url):
+        http_code = node.succeed(f"curl -s -o /dev/null -w '%{{http_code}}' '{url}'")
+        assert http_code.split("\n")[-1].strip() == code, \
+          f"expected HTTP status code {code} but got {http_code}"
+
+    machine.wait_for_unit("postgresql.service")
+    machine.wait_for_unit("pghero.service")
+
+    with subtest("requires HTTP Basic Auth credentials"):
+      expect_http_code(machine, "401", pgheroUnauthorizedURL)
+
+    with subtest("works with some databases being unavailable"):
+      expect_http_code(machine, "500", pgheroBaseURL + "/nulldb")
+
+    with subtest("connects to the PostgreSQL database"):
+      expect_http_code(machine, "200", pgheroBaseURL + "/postgres")
+  '';
+}
diff --git a/nixos/tests/qtile.nix b/nixos/tests/qtile.nix
index b4d8f9d421144..96afaa342c524 100644
--- a/nixos/tests/qtile.nix
+++ b/nixos/tests/qtile.nix
@@ -10,7 +10,7 @@ import ./make-test-python.nix ({ lib, ...} : {
     test-support.displayManager.auto.user = "alice";
 
     services.xserver.windowManager.qtile.enable = true;
-    services.displayManager.defaultSession = lib.mkForce "none+qtile";
+    services.displayManager.defaultSession = lib.mkForce "qtile";
 
     environment.systemPackages = [ pkgs.kitty ];
   };
diff --git a/nixos/tests/renovate.nix b/nixos/tests/renovate.nix
new file mode 100644
index 0000000000000..a30b5b3d60b9c
--- /dev/null
+++ b/nixos/tests/renovate.nix
@@ -0,0 +1,69 @@
+import ./make-test-python.nix (
+  { pkgs, ... }:
+  {
+    name = "renovate";
+    meta.maintainers = with pkgs.lib.maintainers; [ marie natsukium ];
+
+    nodes.machine =
+      { config, ... }:
+      {
+        services.renovate = {
+          enable = true;
+          settings = {
+            platform = "gitea";
+            endpoint = "http://localhost:3000";
+            autodiscover = true;
+            gitAuthor = "Renovate <renovate@example.com>";
+          };
+          credentials = {
+            RENOVATE_TOKEN = "/etc/renovate-token";
+          };
+        };
+        environment.systemPackages = [
+          config.services.forgejo.package
+          pkgs.tea
+          pkgs.git
+        ];
+        services.forgejo = {
+          enable = true;
+          settings.server.HTTP_PORT = 3000;
+        };
+      };
+
+    testScript = ''
+      def gitea(command):
+        return machine.succeed(f"cd /var/lib/forgejo && sudo --user=forgejo GITEA_WORK_DIR=/var/lib/forgejo GITEA_CUSTOM=/var/lib/forgejo/custom gitea {command}")
+
+      machine.wait_for_unit("forgejo.service")
+      machine.wait_for_open_port(3000)
+
+      machine.systemctl("stop forgejo.service")
+
+      gitea("admin user create --username meow --email meow@example.com --password meow")
+
+      machine.systemctl("start forgejo.service")
+      machine.wait_for_unit("forgejo.service")
+      machine.wait_for_open_port(3000)
+
+      accessToken = gitea("admin user generate-access-token --raw --username meow --scopes all | tr -d '\n'")
+
+      machine.succeed(f"tea login add --name default --user meow --token '{accessToken}' --password meow --url http://localhost:3000")
+      machine.succeed("tea repo create --name kitty --init")
+      machine.succeed("git config --global user.name Meow")
+      machine.succeed("git config --global user.email meow@example.com")
+      machine.succeed(f"git clone http://meow:{accessToken}@localhost:3000/meow/kitty.git /tmp/kitty")
+      machine.succeed("echo '{ \"name\": \"meow\", \"version\": \"0.1.0\" }' > /tmp/kitty/package.json")
+      machine.succeed("git -C /tmp/kitty add /tmp/kitty/package.json")
+      machine.succeed("git -C /tmp/kitty commit -m 'add package.json'")
+      machine.succeed("git -C /tmp/kitty push origin")
+
+      machine.succeed(f"echo '{accessToken}' > /etc/renovate-token")
+      machine.systemctl("start renovate.service")
+
+      machine.succeed("tea pulls list --repo meow/kitty | grep 'Configure Renovate'")
+      machine.succeed("tea pulls merge --repo meow/kitty 1")
+
+      machine.systemctl("start renovate.service")
+    '';
+  }
+)
diff --git a/nixos/tests/vaultwarden.nix b/nixos/tests/vaultwarden.nix
index 28ff170e36107..3aba3f6845fa7 100644
--- a/nixos/tests/vaultwarden.nix
+++ b/nixos/tests/vaultwarden.nix
@@ -1,38 +1,94 @@
-{ system ? builtins.currentSystem
-, config ? { }
-, pkgs ? import ../.. { inherit system config; }
-}:
-
 # These tests will:
 #  * Set up a vaultwarden server
-#  * Have Firefox use the web vault to create an account, log in, and save a password to the valut
+#  * Have Firefox use the web vault to create an account, log in, and save a password to the vault
 #  * Have the bw cli log in and read that password from the vault
 #
 # Note that Firefox must be on the same machine as the server for WebCrypto APIs to be available (or HTTPS must be configured)
 #
 # The same tests should work without modification on the official bitwarden server, if we ever package that.
 
-with import ../lib/testing-python.nix { inherit system pkgs; };
-with pkgs.lib;
 let
-  backends = [ "sqlite" "mysql" "postgresql" ];
-
-  dbPassword = "please_dont_hack";
-
-  userEmail = "meow@example.com";
-  userPassword = "also_super_secret_ZJWpBKZi668QGt"; # Must be complex to avoid interstitial warning on the signup page
-
-  storedPassword = "seeeecret";
+  makeVaultwardenTest = name: {
+    backend ? name,
+    withClient ? true,
+    testScript ? null,
+  }: import ./make-test-python.nix ({ lib, pkgs, ...}: let
+    dbPassword = "please_dont_hack";
+    userEmail = "meow@example.com";
+    userPassword = "also_super_secret_ZJWpBKZi668QGt"; # Must be complex to avoid interstitial warning on the signup page
+    storedPassword = "seeeecret";
+
+    testRunner = pkgs.writers.writePython3Bin "test-runner" {
+      libraries = [ pkgs.python3Packages.selenium ];
+      flakeIgnore = [  "E501" ];
+    } ''
+
+      from selenium.webdriver.common.by import By
+      from selenium.webdriver import Firefox
+      from selenium.webdriver.firefox.options import Options
+      from selenium.webdriver.support.ui import WebDriverWait
+      from selenium.webdriver.support import expected_conditions as EC
+
+      options = Options()
+      options.add_argument('--headless')
+      driver = Firefox(options=options)
+
+      driver.implicitly_wait(20)
+      driver.get('http://localhost/#/register')
+
+      wait = WebDriverWait(driver, 10)
+
+      wait.until(EC.title_contains("Vaultwarden Web"))
+
+      driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_email').send_keys(
+          '${userEmail}'
+      )
+      driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_name').send_keys(
+          'A Cat'
+      )
+      driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_master-password').send_keys(
+          '${userPassword}'
+      )
+      driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_confirm-master-password').send_keys(
+          '${userPassword}'
+      )
+      if driver.find_element(By.CSS_SELECTOR, 'input#checkForBreaches').is_selected():
+          driver.find_element(By.CSS_SELECTOR, 'input#checkForBreaches').click()
+
+      driver.find_element(By.XPATH, "//button[contains(., 'Create account')]").click()
+
+      wait.until_not(EC.title_contains("Create account"))
+
+      driver.find_element(By.XPATH, "//button[contains(., 'Continue')]").click()
+
+      driver.find_element(By.CSS_SELECTOR, 'input#login_input_master-password').send_keys(
+          '${userPassword}'
+      )
+      driver.find_element(By.XPATH, "//button[contains(., 'Log in')]").click()
+
+      wait.until(EC.title_contains("Vaults"))
+
+      driver.find_element(By.XPATH, "//button[contains(., 'New item')]").click()
+
+      driver.find_element(By.CSS_SELECTOR, 'input#name').send_keys(
+          'secrets'
+      )
+      driver.find_element(By.CSS_SELECTOR, 'input#loginPassword').send_keys(
+          '${storedPassword}'
+      )
+
+      driver.find_element(By.XPATH, "//button[contains(., 'Save')]").click()
+    '';
+  in {
+    inherit name;
 
-  makeVaultwardenTest = backend: makeTest {
-    name = "vaultwarden-${backend}";
     meta = {
-      maintainers = with pkgs.lib.maintainers; [ jjjollyjim ];
+      maintainers = with pkgs.lib.maintainers; [ dotlambda SuperSandro2000 ];
     };
 
     nodes = {
-      server = { pkgs, ... }:
-        let backendConfig = {
+      server = { pkgs, ... }: lib.mkMerge [
+        {
           mysql = {
             services.mysql = {
               enable = true;
@@ -53,113 +109,47 @@ let
           postgresql = {
             services.postgresql = {
               enable = true;
-              initialScript = pkgs.writeText "postgresql-init.sql" ''
-                CREATE USER bitwardenuser WITH PASSWORD '${dbPassword}';
-                CREATE DATABASE bitwarden WITH OWNER bitwardenuser;
-              '';
+              ensureDatabases = [ "vaultwarden" ];
+              ensureUsers = [{
+                name = "vaultwarden";
+                ensureDBOwnership = true;
+              }];
             };
 
-            services.vaultwarden.config.databaseUrl = "postgresql://bitwardenuser:${dbPassword}@localhost/bitwarden";
+            services.vaultwarden.config.databaseUrl = "postgresql:///vaultwarden?host=/run/postgresql";
 
             systemd.services.vaultwarden.after = [ "postgresql.service" ];
           };
 
-          sqlite = { };
-        };
-        in
-        mkMerge [
-          backendConfig.${backend}
-          {
-            services.vaultwarden = {
-              enable = true;
-              dbBackend = backend;
-              config = {
-                rocketAddress = "0.0.0.0";
-                rocketPort = 80;
-              };
-            };
+          sqlite = {
+            services.vaultwarden.backupDir = "/var/lib/vaultwarden/backups";
+
+            environment.systemPackages = [ pkgs.sqlite ];
+          };
+        }.${backend}
 
-            networking.firewall.allowedTCPPorts = [ 80 ];
-
-            environment.systemPackages =
-              let
-                testRunner = pkgs.writers.writePython3Bin "test-runner"
-                  {
-                    libraries = [ pkgs.python3Packages.selenium ];
-                    flakeIgnore = [
-                      "E501"
-                    ];
-                  } ''
-
-                  from selenium.webdriver.common.by import By
-                  from selenium.webdriver import Firefox
-                  from selenium.webdriver.firefox.options import Options
-                  from selenium.webdriver.support.ui import WebDriverWait
-                  from selenium.webdriver.support import expected_conditions as EC
-
-                  options = Options()
-                  options.add_argument('--headless')
-                  driver = Firefox(options=options)
-
-                  driver.implicitly_wait(20)
-                  driver.get('http://localhost/#/register')
-
-                  wait = WebDriverWait(driver, 10)
-
-                  wait.until(EC.title_contains("Vaultwarden Web"))
-
-                  driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_email').send_keys(
-                      '${userEmail}'
-                  )
-                  driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_name').send_keys(
-                      'A Cat'
-                  )
-                  driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_master-password').send_keys(
-                      '${userPassword}'
-                  )
-                  driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_confirm-master-password').send_keys(
-                      '${userPassword}'
-                  )
-                  if driver.find_element(By.CSS_SELECTOR, 'input#checkForBreaches').is_selected():
-                      driver.find_element(By.CSS_SELECTOR, 'input#checkForBreaches').click()
-
-                  driver.find_element(By.XPATH, "//button[contains(., 'Create account')]").click()
-
-                  wait.until_not(EC.title_contains("Create account"))
-
-                  driver.find_element(By.XPATH, "//button[contains(., 'Continue')]").click()
-
-                  driver.find_element(By.CSS_SELECTOR, 'input#login_input_master-password').send_keys(
-                      '${userPassword}'
-                  )
-                  driver.find_element(By.XPATH, "//button[contains(., 'Log in')]").click()
-
-                  wait.until(EC.title_contains("Vaults"))
-
-                  driver.find_element(By.XPATH, "//button[contains(., 'New item')]").click()
-
-                  driver.find_element(By.CSS_SELECTOR, 'input#name').send_keys(
-                      'secrets'
-                  )
-                  driver.find_element(By.CSS_SELECTOR, 'input#loginPassword').send_keys(
-                      '${storedPassword}'
-                  )
-
-                  driver.find_element(By.XPATH, "//button[contains(., 'Save')]").click()
-                '';
-              in
-              [ pkgs.firefox-unwrapped pkgs.geckodriver testRunner ];
-
-          }
-        ];
-
-      client = { pkgs, ... }:
         {
-          environment.systemPackages = [ pkgs.bitwarden-cli ];
-        };
+          services.vaultwarden = {
+            enable = true;
+            dbBackend = backend;
+            config = {
+              rocketAddress = "0.0.0.0";
+              rocketPort = 80;
+            };
+          };
+
+          networking.firewall.allowedTCPPorts = [ 80 ];
+
+          environment.systemPackages = [ pkgs.firefox-unwrapped pkgs.geckodriver testRunner ];
+        }
+      ];
+    } // lib.optionalAttrs withClient {
+      client = { pkgs, ... }: {
+        environment.systemPackages = [ pkgs.bitwarden-cli ];
+      };
     };
 
-    testScript = ''
+    testScript = if testScript != null then testScript else ''
       start_all()
       server.wait_for_unit("vaultwarden.service")
       server.wait_for_open_port(80)
@@ -184,15 +174,37 @@ let
           client.succeed(f"bw --nointeraction --raw --session {key} sync -f")
 
       with subtest("get the password with the cli"):
-          password = client.succeed(
-              f"bw --nointeraction --raw --session {key} list items | ${pkgs.jq}/bin/jq -r .[].login.password"
+          password = client.wait_until_succeeds(
+              f"bw --nointeraction --raw --session {key} list items | ${pkgs.jq}/bin/jq -r .[].login.password",
+              timeout=60
           )
           assert password.strip() == "${storedPassword}"
     '';
-  };
+  });
 in
-builtins.listToAttrs (
-  map
-    (backend: { name = backend; value = makeVaultwardenTest backend; })
-    backends
-)
+builtins.mapAttrs (k: v: makeVaultwardenTest k v) {
+  mysql = {};
+  postgresql = {};
+  sqlite = {};
+  sqlite-backup = {
+    backend = "sqlite";
+    withClient = false;
+
+    testScript = ''
+      start_all()
+      server.wait_for_unit("vaultwarden.service")
+      server.wait_for_open_port(80)
+
+      with subtest("Set up vaultwarden"):
+          server.succeed("PYTHONUNBUFFERED=1 test-runner | systemd-cat -t test-runner")
+
+      with subtest("Run the backup script"):
+          server.start_job("backup-vaultwarden.service")
+
+      with subtest("Check that backup exists"):
+          server.succeed('[ -d "/var/lib/vaultwarden/backups" ]')
+          server.succeed('[ -f "/var/lib/vaultwarden/backups/db.sqlite3" ]')
+          server.succeed('[ -d "/var/lib/vaultwarden/backups/attachments" ]')
+    '';
+  };
+}