about summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/configuration/file-systems.chapter.md1
-rw-r--r--nixos/doc/manual/configuration/overlayfs.section.md27
-rw-r--r--nixos/doc/manual/development/option-types.section.md2
-rw-r--r--nixos/doc/manual/release-notes/rl-2405.section.md26
-rw-r--r--nixos/lib/systemd-lib.nix10
-rw-r--r--nixos/lib/systemd-unit-options.nix17
-rw-r--r--nixos/maintainers/scripts/ec2/README.md7
-rw-r--r--nixos/maintainers/scripts/ec2/amazon-image.nix2
-rw-r--r--nixos/maintainers/scripts/lxd/lxd-virtual-machine-image-inner.nix18
-rw-r--r--nixos/maintainers/scripts/lxd/lxd-virtual-machine-image.nix18
-rw-r--r--nixos/modules/misc/ids.nix4
-rw-r--r--nixos/modules/module-list.nix3
-rw-r--r--nixos/modules/programs/chromium.nix48
-rw-r--r--nixos/modules/rename.nix1
-rw-r--r--nixos/modules/services/cluster/kubernetes/pki.nix2
-rw-r--r--nixos/modules/services/continuous-integration/github-runner.nix25
-rw-r--r--nixos/modules/services/continuous-integration/github-runner/options.nix457
-rw-r--r--nixos/modules/services/continuous-integration/github-runner/service.nix511
-rw-r--r--nixos/modules/services/continuous-integration/github-runners.nix62
-rw-r--r--nixos/modules/services/hardware/fwupd.nix25
-rw-r--r--nixos/modules/services/hardware/pcscd.nix6
-rw-r--r--nixos/modules/services/home-automation/home-assistant.nix42
-rw-r--r--nixos/modules/services/matrix/synapse.md5
-rw-r--r--nixos/modules/services/matrix/synapse.nix166
-rw-r--r--nixos/modules/services/misc/gitea.nix5
-rw-r--r--nixos/modules/services/misc/nix-gc.nix3
-rw-r--r--nixos/modules/services/misc/sourcehut/default.nix2
-rw-r--r--nixos/modules/services/networking/cloudflared.nix7
-rw-r--r--nixos/modules/services/networking/dhcpcd.nix2
-rw-r--r--nixos/modules/services/networking/jibri/default.nix15
-rw-r--r--nixos/modules/services/networking/jicofo.nix15
-rw-r--r--nixos/modules/services/networking/jitsi-videobridge.nix15
-rw-r--r--nixos/modules/services/networking/murmur.nix23
-rw-r--r--nixos/modules/services/networking/nftables.nix30
-rw-r--r--nixos/modules/services/networking/pyload.nix27
-rw-r--r--nixos/modules/services/web-apps/nextcloud.nix26
-rw-r--r--nixos/modules/services/web-apps/restya-board.nix380
-rw-r--r--nixos/modules/services/web-apps/suwayomi-server.nix55
-rw-r--r--nixos/modules/services/x11/desktop-managers/plasma5.nix1
-rw-r--r--nixos/modules/system/boot/uki.nix16
-rw-r--r--nixos/modules/tasks/filesystems/overlayfs.nix144
-rw-r--r--nixos/modules/virtualisation/amazon-image.nix1
-rw-r--r--nixos/modules/virtualisation/amazon-init.nix1
-rw-r--r--nixos/modules/virtualisation/oci-containers.nix7
-rw-r--r--nixos/modules/virtualisation/qemu-vm.nix39
-rw-r--r--nixos/tests/all-tests.nix1
-rw-r--r--nixos/tests/filesystems-overlayfs.nix89
-rw-r--r--nixos/tests/freetube.nix2
-rw-r--r--nixos/tests/installed-tests/fwupd.nix9
-rw-r--r--nixos/tests/oci-containers.nix5
50 files changed, 1305 insertions, 1100 deletions
diff --git a/nixos/doc/manual/configuration/file-systems.chapter.md b/nixos/doc/manual/configuration/file-systems.chapter.md
index aca978be064d..3dfdd20ac33e 100644
--- a/nixos/doc/manual/configuration/file-systems.chapter.md
+++ b/nixos/doc/manual/configuration/file-systems.chapter.md
@@ -39,4 +39,5 @@ and non-critical by adding `options = [ "nofail" ];`.
 ```{=include=} sections
 luks-file-systems.section.md
 sshfs-file-systems.section.md
+overlayfs.section.md
 ```
diff --git a/nixos/doc/manual/configuration/overlayfs.section.md b/nixos/doc/manual/configuration/overlayfs.section.md
new file mode 100644
index 000000000000..592fb7c2e6f7
--- /dev/null
+++ b/nixos/doc/manual/configuration/overlayfs.section.md
@@ -0,0 +1,27 @@
+# Overlayfs {#sec-overlayfs}
+
+NixOS offers a convenient abstraction to create both read-only as well writable
+overlays.
+
+```nix
+fileSystems = {
+  "/writable-overlay" = {
+    overlay = {
+      lowerdir = [ writableOverlayLowerdir ];
+      upperdir = "/.rw-writable-overlay/upper";
+      workdir = "/.rw-writable-overlay/work";
+    };
+    # Mount the writable overlay in the initrd.
+    neededForBoot = true;
+  };
+  "/readonly-overlay".overlay.lowerdir = [
+    writableOverlayLowerdir
+    writableOverlayLowerdir2
+  ];
+};
+```
+
+If `upperdir` and `workdir` are not null, they will be created before the
+overlay is mounted.
+
+To mount an overlay as read-only, you need to provide at least two `lowerdir`s.
diff --git a/nixos/doc/manual/development/option-types.section.md b/nixos/doc/manual/development/option-types.section.md
index f9c7ac80018e..04edf99e70b0 100644
--- a/nixos/doc/manual/development/option-types.section.md
+++ b/nixos/doc/manual/development/option-types.section.md
@@ -326,7 +326,7 @@ Composed types are types that take a type as parameter. `listOf
 `types.uniq` *`t`*
 
 :   Ensures that type *`t`* cannot be merged. It is used to ensure option
-    definitions are declared only once.
+    definitions are provided only once.
 
 `types.unique` `{ message = m }` *`t`*
 
diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md
index efb54da3fee7..f6a77036674e 100644
--- a/nixos/doc/manual/release-notes/rl-2405.section.md
+++ b/nixos/doc/manual/release-notes/rl-2405.section.md
@@ -8,6 +8,10 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
 
+- `cryptsetup` has been upgraded from 2.6.1 to 2.7.0. Cryptsetup is a critical component enabling LUKS-based (but not only) full disk encryption.
+  Take the time to review [the release notes](https://gitlab.com/cryptsetup/cryptsetup/-/raw/v2.7.0/docs/v2.7.0-ReleaseNotes).
+  One of the highlight is that it is now possible to use hardware OPAL-based encryption of your disk with `cryptsetup`, it has a lot of caveats, see the above notes for the full details.
+
 - `screen`'s module has been cleaned, and will now require you to set `programs.screen.enable` in order to populate `screenrc` and add the program to the environment.
 
 - `linuxPackages_testing_bcachefs` is now fully deprecated by `linuxPackages_latest`, and is therefore no longer available.
@@ -246,6 +250,9 @@ The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been m
 - `services.postgresql.extraPlugins` changed its type from just a list of packages to also a function that returns such a list.
   For example a config line like ``services.postgresql.extraPlugins = with pkgs.postgresql_11.pkgs; [ postgis ];`` is recommended to be changed to ``services.postgresql.extraPlugins = ps: with ps; [ postgis ];``;
 
+- The Matrix homeserver [Synapse](https://element-hq.github.io/synapse/) module now supports configuring UNIX domain socket [listeners](#opt-services.matrix-synapse.settings.listeners) through the `path` option.
+  The default replication worker on the main instance has been migrated away from TCP sockets to UNIX domain sockets.
+
 - Programs written in [Nim](https://nim-lang.org/) are built with libraries selected by lockfiles.
   The `nimPackages` and `nim2Packages` sets have been removed.
   See https://nixos.org/manual/nixpkgs/unstable#nim for more information.
@@ -255,6 +262,8 @@ The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been m
   After upgrading, follow the instructions on the [upstream release notes](https://github.com/majewsky/portunus/releases/tag/v2.0.0) to upgrade all user accounts to strong password hashes.
   Support for weak password hashes will be removed in NixOS 24.11.
 
+- A stdenv's default set of hardening flags can now be set via its `bintools-wrapper`'s `defaultHardeningFlags` argument. A convenient stdenv adapter, `withDefaultHardeningFlags`, can be used to override an existing stdenv's `defaultHardeningFlags`.
+
 - `libass` now uses the native CoreText backend on Darwin, which may fix subtitle rendering issues with `mpv`, `ffmpeg`, etc.
 
 - [Lilypond](https://lilypond.org/index.html) and [Denemo](https://www.denemo.org) are now compiled with Guile 3.0.
@@ -272,8 +281,19 @@ The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been m
 - The option [`services.nextcloud.config.dbport`] of the Nextcloud module was removed to match upstream.
   The port can be specified in [`services.nextcloud.config.dbhost`](#opt-services.nextcloud.config.dbhost).
 
+- A new abstraction to create both read-only as well as writable overlay file
+  systems was added. Available via
+  [fileSystems.overlay](#opt-fileSystems._name_.overlay.lowerdir). See also the
+  [NixOS docs](#sec-overlayfs).
+
+- systemd units can now specify the `Upholds=` and `UpheldBy=` unit dependencies via the aptly
+  named `upholds` and `upheldBy` options. These options get systemd to enforce that the
+  dependencies remain continuosly running for as long as the dependent unit is in a running state.
+
 - `stdenv`: The `--replace` flag in `substitute`, `substituteInPlace`, `substituteAll`, `substituteAllStream`, and `substituteStream` is now deprecated if favor of the new `--replace-fail`, `--replace-warn` and `--replace-quiet`. The deprecated `--replace` equates to `--replace-warn`.
 
+- A new hardening flag, `zerocallusedregs` was made available, corresponding to the gcc/clang option `-fzero-call-used-regs=used-gpr`.
+
 - New options were added to the dnsdist module to enable and configure a DNSCrypt endpoint (see `services.dnsdist.dnscrypt.enable`, etc.).
   The module can generate the DNSCrypt provider key pair, certificates and also performs their rotation automatically with no downtime.
 
@@ -290,6 +310,8 @@ The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been m
   `globalRedirect` can now have redirect codes other than 301 through
   `redirectCode`.
 
+- `libjxl` 0.9.0 [dropped support for the butteraugli API](https://github.com/libjxl/libjxl/pull/2576). You will no longer be able to set `enableButteraugli` on `libaom`.
+
 - The source of the `mockgen` package has changed to the [go.uber.org/mock](https://github.com/uber-go/mock) fork because [the original repository is no longer maintained](https://github.com/golang/mock#gomock).
 
 - `security.pam.enableSSHAgentAuth` was renamed to `security.pam.sshAgentAuth.enable` and an `authorizedKeysFiles`
@@ -298,6 +320,8 @@ The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been m
 
 - [](#opt-boot.kernel.sysctl._net.core.wmem_max_) changed from a string to an integer because of the addition of a custom merge option (taking the highest value defined to avoid conflicts between 2 services trying to set that value), just as [](#opt-boot.kernel.sysctl._net.core.rmem_max_) since 22.11.
 
+- A new top-level package set, `pkgsExtraHardening` is added. This is a set of packages built with stricter hardening flags - those that have not yet received enough testing to be applied universally, those that are more likely to cause build failures or those that have drawbacks to their use (e.g. performance or required hardware features).
+
 - `services.zfs.zed.enableMail` now uses the global `sendmail` wrapper defined by an email module
   (such as msmtp or Postfix). It no longer requires using a special ZFS build with email support.
 
@@ -313,6 +337,8 @@ The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been m
 
 - The `hardware.pulseaudio` module now sets permission of pulse user home directory to 755 when running in "systemWide" mode. It fixes [issue 114399](https://github.com/NixOS/nixpkgs/issues/114399).
 
+- The module `services.github-runner` has been removed. To configure a single GitHub Actions Runner refer to `services.github-runners.*`. Note that this will trigger a new runner registration.
+
 - The `btrbk` module now automatically selects and provides required compression
   program depending on the configured `stream_compress` option. Since this
   replaces the need for the `extraPackages` option, this option will be
diff --git a/nixos/lib/systemd-lib.nix b/nixos/lib/systemd-lib.nix
index 347ee7303936..c9cca619ed70 100644
--- a/nixos/lib/systemd-lib.nix
+++ b/nixos/lib/systemd-lib.nix
@@ -242,7 +242,7 @@ in rec {
             ln -sfn '${name}' $out/'${name2}'
           '') (unit.aliases or [])) units)}
 
-      # Create .wants and .requires symlinks from the wantedBy and
+      # Create .wants, .upholds and .requires symlinks from the wantedBy, upheldBy and
       # requiredBy options.
       ${concatStrings (mapAttrsToList (name: unit:
           concatMapStrings (name2: ''
@@ -252,6 +252,12 @@ in rec {
 
       ${concatStrings (mapAttrsToList (name: unit:
           concatMapStrings (name2: ''
+            mkdir -p $out/'${name2}.upholds'
+            ln -sfn '../${name}' $out/'${name2}.upholds'/
+          '') (unit.upheldBy or [])) units)}
+
+      ${concatStrings (mapAttrsToList (name: unit:
+          concatMapStrings (name2: ''
             mkdir -p $out/'${name2}.requires'
             ln -sfn '../${name}' $out/'${name2}.requires'/
           '') (unit.requiredBy or [])) units)}
@@ -289,6 +295,8 @@ in rec {
           { Requires = toString config.requires; }
         // optionalAttrs (config.wants != [])
           { Wants = toString config.wants; }
+        // optionalAttrs (config.upholds != [])
+          { Upholds = toString config.upholds; }
         // optionalAttrs (config.after != [])
           { After = toString config.after; }
         // optionalAttrs (config.before != [])
diff --git a/nixos/lib/systemd-unit-options.nix b/nixos/lib/systemd-unit-options.nix
index 9c69bda471bb..df05d165d9e8 100644
--- a/nixos/lib/systemd-unit-options.nix
+++ b/nixos/lib/systemd-unit-options.nix
@@ -74,6 +74,15 @@ in rec {
       '';
     };
 
+    upheldBy = mkOption {
+      default = [];
+      type = types.listOf unitNameType;
+      description = lib.mdDoc ''
+        Keep this unit running as long as the listed units are running. This is a continuously
+        enforced version of wantedBy.
+      '';
+    };
+
     wantedBy = mkOption {
       default = [];
       type = types.listOf unitNameType;
@@ -147,6 +156,14 @@ in rec {
         '';
       };
 
+      upholds = mkOption {
+        default = [];
+        type = types.listOf unitNameType;
+        description = lib.mdDoc ''
+          Keeps the specified running while this unit is running. A continuous version of `wants`.
+        '';
+      };
+
       after = mkOption {
         default = [];
         type = types.listOf unitNameType;
diff --git a/nixos/maintainers/scripts/ec2/README.md b/nixos/maintainers/scripts/ec2/README.md
new file mode 100644
index 000000000000..1328109d464a
--- /dev/null
+++ b/nixos/maintainers/scripts/ec2/README.md
@@ -0,0 +1,7 @@
+# Amazon images
+
+* The `create-amis.sh` script will be replaced by https://github.com/NixOS/amis which will regularly upload AMIs per NixOS channel bump.
+
+* @arianvp is planning to drop zfs support
+* @arianvp is planning to rewrite the image builder to use the repart-based image builder.
+
diff --git a/nixos/maintainers/scripts/ec2/amazon-image.nix b/nixos/maintainers/scripts/ec2/amazon-image.nix
index d12339bca1f8..055d44ba6576 100644
--- a/nixos/maintainers/scripts/ec2/amazon-image.nix
+++ b/nixos/maintainers/scripts/ec2/amazon-image.nix
@@ -157,4 +157,6 @@ in {
       '';
     };
   in if config.ec2.zfs.enable then zfsBuilder else extBuilder;
+
+  meta.maintainers = with maintainers; [ arianvp ];
 }
diff --git a/nixos/maintainers/scripts/lxd/lxd-virtual-machine-image-inner.nix b/nixos/maintainers/scripts/lxd/lxd-virtual-machine-image-inner.nix
index c1c50b32ff5b..2d1761401fcd 100644
--- a/nixos/maintainers/scripts/lxd/lxd-virtual-machine-image-inner.nix
+++ b/nixos/maintainers/scripts/lxd/lxd-virtual-machine-image-inner.nix
@@ -13,8 +13,22 @@
       ./lxd.nix
     ];
 
-  networking.useDHCP = false;
-  networking.interfaces.eth0.useDHCP = true;
+  networking = {
+    dhcdpcd.enable = false;
+    useDHCP = false;
+  };
+
+  systemd.network = {
+    enable = true;
+    networks."50-eth0" = {
+      matchConfig.Name = "eth0";
+      networkConfig = {
+        DHCP = "ipv4";
+        IPv6AcceptRA = true;
+      };
+      linkConfig.RequiredForOnline = "routable";
+    };
+  };
 
   system.stateVersion = "@stateVersion@"; # Did you read the comment?
 }
diff --git a/nixos/maintainers/scripts/lxd/lxd-virtual-machine-image.nix b/nixos/maintainers/scripts/lxd/lxd-virtual-machine-image.nix
index 0d96eea0e2d2..a58579914465 100644
--- a/nixos/maintainers/scripts/lxd/lxd-virtual-machine-image.nix
+++ b/nixos/maintainers/scripts/lxd/lxd-virtual-machine-image.nix
@@ -26,6 +26,20 @@
   '';
 
   # Network
-  networking.useDHCP = false;
-  networking.interfaces.enp5s0.useDHCP = true;
+  networking = {
+    dhcdpcd.enable = false;
+    useDHCP = false;
+  };
+
+  systemd.network = {
+    enable = true;
+    networks."50-enp5s0" = {
+      matchConfig.Name = "enp5s0";
+      networkConfig = {
+        DHCP = "ipv4";
+        IPv6AcceptRA = true;
+      };
+      linkConfig.RequiredForOnline = "routable";
+    };
+  };
 }
diff --git a/nixos/modules/misc/ids.nix b/nixos/modules/misc/ids.nix
index 5af7284ac71a..cfa98c838af5 100644
--- a/nixos/modules/misc/ids.nix
+++ b/nixos/modules/misc/ids.nix
@@ -313,7 +313,7 @@ in
       kanboard = 281;
       # pykms = 282; # DynamicUser = true
       kodi = 283;
-      restya-board = 284;
+      # restya-board = 284; # removed 2024-01-22
       mighttpd2 = 285;
       hass = 286;
       #monero = 287; # dynamically allocated as of 2021-05-08
@@ -623,7 +623,7 @@ in
       kanboard = 281;
       # pykms = 282; # DynamicUser = true
       kodi = 283;
-      restya-board = 284;
+      # restya-board = 284; # removed 2024-01-22
       mighttpd2 = 285;
       hass = 286;
       # monero = 287; # dynamically allocated as of 2021-05-08
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 1384c7c6dfa2..ec022713e12e 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -410,7 +410,6 @@
   ./services/continuous-integration/buildbot/worker.nix
   ./services/continuous-integration/buildkite-agents.nix
   ./services/continuous-integration/gitea-actions-runner.nix
-  ./services/continuous-integration/github-runner.nix
   ./services/continuous-integration/github-runners.nix
   ./services/continuous-integration/gitlab-runner.nix
   ./services/continuous-integration/gocd-agent/default.nix
@@ -1348,7 +1347,6 @@
   ./services/web-apps/powerdns-admin.nix
   ./services/web-apps/pretalx.nix
   ./services/web-apps/prosody-filer.nix
-  ./services/web-apps/restya-board.nix
   ./services/web-apps/rimgo.nix
   ./services/web-apps/sftpgo.nix
   ./services/web-apps/suwayomi-server.nix
@@ -1527,6 +1525,7 @@
   ./tasks/filesystems/jfs.nix
   ./tasks/filesystems/nfs.nix
   ./tasks/filesystems/ntfs.nix
+  ./tasks/filesystems/overlayfs.nix
   ./tasks/filesystems/reiserfs.nix
   ./tasks/filesystems/sshfs.nix
   ./tasks/filesystems/squashfs.nix
diff --git a/nixos/modules/programs/chromium.nix b/nixos/modules/programs/chromium.nix
index 4024f337dfcd..287d93c82cad 100644
--- a/nixos/modules/programs/chromium.nix
+++ b/nixos/modules/programs/chromium.nix
@@ -1,4 +1,4 @@
-{ config, lib, ... }:
+{ config, lib, pkgs, ... }:
 
 with lib;
 
@@ -21,8 +21,12 @@ in
     programs.chromium = {
       enable = mkEnableOption (lib.mdDoc "{command}`chromium` policies");
 
+      enablePlasmaBrowserIntegration = mkEnableOption (lib.mdDoc "Native Messaging Host for Plasma Browser Integration");
+
+      plasmaBrowserIntegrationPackage = mkPackageOption pkgs "plasma5Packages.plasma-browser-integration" { };
+
       extensions = mkOption {
-        type = types.listOf types.str;
+        type = with types; nullOr (listOf str);
         description = lib.mdDoc ''
           List of chromium extensions to install.
           For list of plugins ids see id in url of extensions on
@@ -33,7 +37,7 @@ in
           [ExtensionInstallForcelist](https://cloud.google.com/docs/chrome-enterprise/policies/?policy=ExtensionInstallForcelist)
           for additional details.
         '';
-        default = [];
+        default = null;
         example = literalExpression ''
           [
             "chlffgpmiacpedhhbkiomidkjlcfhogd" # pushbullet
@@ -62,16 +66,14 @@ in
         type = types.nullOr types.str;
         description = lib.mdDoc "Chromium default search provider url.";
         default = null;
-        example =
-          "https://encrypted.google.com/search?q={searchTerms}&{google:RLZ}{google:originalQueryForSuggestion}{google:assistedQueryStats}{google:searchFieldtrialParameter}{google:searchClient}{google:sourceId}{google:instantExtendedEnabledParameter}ie={inputEncoding}";
+        example = "https://encrypted.google.com/search?q={searchTerms}&{google:RLZ}{google:originalQueryForSuggestion}{google:assistedQueryStats}{google:searchFieldtrialParameter}{google:searchClient}{google:sourceId}{google:instantExtendedEnabledParameter}ie={inputEncoding}";
       };
 
       defaultSearchProviderSuggestURL = mkOption {
         type = types.nullOr types.str;
         description = lib.mdDoc "Chromium default search provider url for suggestions.";
         default = null;
-        example =
-          "https://encrypted.google.com/complete/search?output=chrome&q={searchTerms}";
+        example = "https://encrypted.google.com/complete/search?output=chrome&q={searchTerms}";
       };
 
       extraOpts = mkOption {
@@ -90,9 +92,9 @@ in
             "PasswordManagerEnabled" = false;
             "SpellcheckEnabled" = true;
             "SpellcheckLanguage" = [
-                                     "de"
-                                     "en-US"
-                                   ];
+              "de"
+              "en-US"
+            ];
           }
         '';
       };
@@ -101,15 +103,21 @@ in
 
   ###### implementation
 
-  config = lib.mkIf cfg.enable {
-    # for chromium
-    environment.etc."chromium/policies/managed/default.json".text = builtins.toJSON defaultProfile;
-    environment.etc."chromium/policies/managed/extra.json".text = builtins.toJSON cfg.extraOpts;
-    # for google-chrome https://www.chromium.org/administrators/linux-quick-start
-    environment.etc."opt/chrome/policies/managed/default.json".text = builtins.toJSON defaultProfile;
-    environment.etc."opt/chrome/policies/managed/extra.json".text = builtins.toJSON cfg.extraOpts;
-    # for brave
-    environment.etc."brave/policies/managed/default.json".text = builtins.toJSON defaultProfile;
-    environment.etc."brave/policies/managed/extra.json".text = builtins.toJSON cfg.extraOpts;
+  config = {
+    environment.etc = lib.mkIf cfg.enable {
+      # for chromium
+      "chromium/native-messaging-hosts/org.kde.plasma.browser_integration.json" = lib.mkIf cfg.enablePlasmaBrowserIntegration
+        { source = "${cfg.plasmaBrowserIntegrationPackage}/etc/chromium/native-messaging-hosts/org.kde.plasma.browser_integration.json"; };
+      "chromium/policies/managed/default.json" = lib.mkIf (defaultProfile != {}) { text = builtins.toJSON defaultProfile; };
+      "chromium/policies/managed/extra.json" = lib.mkIf (cfg.extraOpts != {}) { text = builtins.toJSON cfg.extraOpts; };
+      # for google-chrome https://www.chromium.org/administrators/linux-quick-start
+      "opt/chrome/native-messaging-hosts/org.kde.plasma.browser_integration.json" = lib.mkIf cfg.enablePlasmaBrowserIntegration
+        { source = "${cfg.plasmaBrowserIntegrationPackage}/etc/opt/chrome/native-messaging-hosts/org.kde.plasma.browser_integration.json"; };
+      "opt/chrome/policies/managed/default.json" = lib.mkIf (defaultProfile != {}) { text = builtins.toJSON defaultProfile; };
+      "opt/chrome/policies/managed/extra.json" = lib.mkIf (cfg.extraOpts != {}) { text = builtins.toJSON cfg.extraOpts; };
+      # for brave
+      "brave/policies/managed/default.json" = lib.mkIf (defaultProfile != {}) { text = builtins.toJSON defaultProfile; };
+      "brave/policies/managed/extra.json" = lib.mkIf (cfg.extraOpts != {}) { text = builtins.toJSON cfg.extraOpts; };
+    };
   };
 }
diff --git a/nixos/modules/rename.nix b/nixos/modules/rename.nix
index 3fab863adb7f..0a975fcd98c8 100644
--- a/nixos/modules/rename.nix
+++ b/nixos/modules/rename.nix
@@ -112,6 +112,7 @@ in
     (mkRemovedOptionModule [ "services" "cryptpad" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "rtsp-simple-server" ] "Package has been completely rebranded by upstream as mediamtx, and thus the service and the package were renamed in NixOS as well.")
     (mkRemovedOptionModule [ "services" "prayer" ] "The corresponding package was removed from nixpkgs.")
+    (mkRemovedOptionModule [ "services" "restya-board" ] "The corresponding package was removed from nixpkgs.")
 
     (mkRemovedOptionModule [ "i18n" "inputMethod" "fcitx" ] "The fcitx module has been removed. Please use fcitx5 instead")
     (mkRemovedOptionModule [ "services" "dhcpd4" ] ''
diff --git a/nixos/modules/services/cluster/kubernetes/pki.nix b/nixos/modules/services/cluster/kubernetes/pki.nix
index c47ceb218e66..9a01238c2391 100644
--- a/nixos/modules/services/cluster/kubernetes/pki.nix
+++ b/nixos/modules/services/cluster/kubernetes/pki.nix
@@ -174,7 +174,7 @@ in
       '')
       (optionalString cfg.genCfsslAPIToken ''
         if [ ! -f "${cfsslAPITokenPath}" ]; then
-          install -u cfssl -m 400 <(head -c ${toString (cfsslAPITokenLength / 2)} /dev/urandom | od -An -t x | tr -d ' ') "${cfsslAPITokenPath}"
+          install -o cfssl -m 400 <(head -c ${toString (cfsslAPITokenLength / 2)} /dev/urandom | od -An -t x | tr -d ' ') "${cfsslAPITokenPath}"
         fi
       '')]);
 
diff --git a/nixos/modules/services/continuous-integration/github-runner.nix b/nixos/modules/services/continuous-integration/github-runner.nix
deleted file mode 100644
index 27cfee92c75a..000000000000
--- a/nixos/modules/services/continuous-integration/github-runner.nix
+++ /dev/null
@@ -1,25 +0,0 @@
-{ config
-, pkgs
-, lib
-, ...
-}@args:
-
-with lib;
-
-let
-  cfg = config.services.github-runner;
-in
-
-{
-  options.services.github-runner = import ./github-runner/options.nix (args // {
-    # Users don't need to specify options.services.github-runner.name; it will default
-    # to the hostname.
-    includeNameDefault = true;
-  });
-
-  config = mkIf cfg.enable {
-    services.github-runners.${cfg.name} = cfg;
-  };
-
-  meta.maintainers = with maintainers; [ veehaitch newam thomasjm ];
-}
diff --git a/nixos/modules/services/continuous-integration/github-runner/options.nix b/nixos/modules/services/continuous-integration/github-runner/options.nix
index b9b1ea05e967..193261fc2a9f 100644
--- a/nixos/modules/services/continuous-integration/github-runner/options.nix
+++ b/nixos/modules/services/continuous-integration/github-runner/options.nix
@@ -1,213 +1,266 @@
-{ config
-, lib
+{ lib
 , pkgs
-, includeNameDefault
 , ...
 }:
 
 with lib;
-
 {
-  enable = mkOption {
-    default = false;
-    example = true;
-    description = lib.mdDoc ''
-      Whether to enable GitHub Actions runner.
-
-      Note: GitHub recommends using self-hosted runners with private repositories only. Learn more here:
-      [About self-hosted runners](https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners).
-    '';
-    type = lib.types.bool;
-  };
-
-  url = mkOption {
-    type = types.str;
-    description = lib.mdDoc ''
-      Repository to add the runner to.
-
-      Changing this option triggers a new runner registration.
-
-      IMPORTANT: If your token is org-wide (not per repository), you need to
-      provide a github org link, not a single repository, so do it like this
-      `https://github.com/nixos`, not like this
-      `https://github.com/nixos/nixpkgs`.
-      Otherwise, you are going to get a `404 NotFound`
-      from `POST https://api.github.com/actions/runner-registration`
-      in the configure script.
-    '';
-    example = "https://github.com/nixos/nixpkgs";
-  };
-
-  tokenFile = mkOption {
-    type = types.path;
-    description = lib.mdDoc ''
-      The full path to a file which contains either
-
-      * a fine-grained personal access token (PAT),
-      * a classic PAT
-      * or a runner registration token
-
-      Changing this option or the `tokenFile`’s content triggers a new runner registration.
-
-      We suggest using the fine-grained PATs. A runner registration token is valid
-      only for 1 hour after creation, so the next time the runner configuration changes
-      this will give you hard-to-debug HTTP 404 errors in the configure step.
-
-      The file should contain exactly one line with the token without any newline.
-      (Use `echo -n '…token…' > …token file…` to make sure no newlines sneak in.)
-
-      If the file contains a PAT, the service creates a new registration token
-      on startup as needed.
-      If a registration token is given, it can be used to re-register a runner of the same
-      name but is time-limited as noted above.
-
-      For fine-grained PATs:
-
-      Give it "Read and Write access to organization/repository self hosted runners",
-      depending on whether it is organization wide or per-repository. You might have to
-      experiment a little, fine-grained PATs are a `beta` Github feature and still subject
-      to change; nonetheless they are the best option at the moment.
-
-      For classic PATs:
-
-      Make sure the PAT has a scope of `admin:org` for organization-wide registrations
-      or a scope of `repo` for a single repository.
-
-      For runner registration tokens:
-
-      Nothing special needs to be done, but updating will break after one hour,
-      so these are not recommended.
-    '';
-    example = "/run/secrets/github-runner/nixos.token";
-  };
-
-  name = let
-    # Same pattern as for `networking.hostName`
-    baseType = types.strMatching "^$|^[[:alnum:]]([[:alnum:]_-]{0,61}[[:alnum:]])?$";
-  in mkOption {
-    type = if includeNameDefault then baseType else types.nullOr baseType;
-    description = lib.mdDoc ''
-      Name of the runner to configure. Defaults to the hostname.
-
-      Changing this option triggers a new runner registration.
-    '';
-    example = "nixos";
-  } // (if includeNameDefault then {
-    default = config.networking.hostName;
-    defaultText = literalExpression "config.networking.hostName";
-  } else {
-    default = null;
-  });
-
-  runnerGroup = mkOption {
-    type = types.nullOr types.str;
-    description = lib.mdDoc ''
-      Name of the runner group to add this runner to (defaults to the default runner group).
-
-      Changing this option triggers a new runner registration.
-    '';
-    default = null;
-  };
-
-  extraLabels = mkOption {
-    type = types.listOf types.str;
-    description = lib.mdDoc ''
-      Extra labels in addition to the default (`["self-hosted", "Linux", "X64"]`).
-
-      Changing this option triggers a new runner registration.
-    '';
-    example = literalExpression ''[ "nixos" ]'';
-    default = [ ];
-  };
-
-  replace = mkOption {
-    type = types.bool;
-    description = lib.mdDoc ''
-      Replace any existing runner with the same name.
-
-      Without this flag, registering a new runner with the same name fails.
-    '';
-    default = false;
-  };
-
-  extraPackages = mkOption {
-    type = types.listOf types.package;
-    description = lib.mdDoc ''
-      Extra packages to add to `PATH` of the service to make them available to workflows.
-    '';
-    default = [ ];
-  };
-
-  extraEnvironment = mkOption {
-    type = types.attrs;
-    description = lib.mdDoc ''
-      Extra environment variables to set for the runner, as an attrset.
-    '';
-    example = {
-      GIT_CONFIG = "/path/to/git/config";
-    };
-    default = {};
-  };
-
-  serviceOverrides = mkOption {
-    type = types.attrs;
-    description = lib.mdDoc ''
-      Modify the systemd service. Can be used to, e.g., adjust the sandboxing options.
-      See {manpage}`systemd.exec(5)` for more options.
+  options.services.github-runners = mkOption {
+    description = mdDoc ''
+      Multiple GitHub Runners.
     '';
     example = {
-      ProtectHome = false;
-      RestrictAddressFamilies = [ "AF_PACKET" ];
+      runner1 = {
+        enable = true;
+        url = "https://github.com/owner/repo";
+        name = "runner1";
+        tokenFile = "/secrets/token1";
+      };
+
+      runner2 = {
+        enable = true;
+        url = "https://github.com/owner/repo";
+        name = "runner2";
+        tokenFile = "/secrets/token2";
+      };
     };
-    default = {};
-  };
-
-  package = mkPackageOption pkgs "github-runner" { };
-
-  ephemeral = mkOption {
-    type = types.bool;
-    description = lib.mdDoc ''
-      If enabled, causes the following behavior:
-
-      - Passes the `--ephemeral` flag to the runner configuration script
-      - De-registers and stops the runner with GitHub after it has processed one job
-      - On stop, systemd wipes the runtime directory (this always happens, even without using the ephemeral option)
-      - Restarts the service after its successful exit
-      - On start, wipes the state directory and configures a new runner
-
-      You should only enable this option if `tokenFile` points to a file which contains a
-      personal access token (PAT). If you're using the option with a registration token, restarting the
-      service will fail as soon as the registration token expired.
-    '';
-    default = false;
-  };
-
-  user = mkOption {
-    type = types.nullOr types.str;
-    description = lib.mdDoc ''
-      User under which to run the service. If null, will use a systemd dynamic user.
-    '';
-    default = null;
-    defaultText = literalExpression "username";
-  };
-
-  workDir = mkOption {
-    type = with types; nullOr str;
-    description = lib.mdDoc ''
-      Working directory, available as `$GITHUB_WORKSPACE` during workflow runs
-      and used as a default for [repository checkouts](https://github.com/actions/checkout).
-      The service cleans this directory on every service start.
-
-      A value of `null` will default to the systemd `RuntimeDirectory`.
-    '';
-    default = null;
-  };
-
-  nodeRuntimes = mkOption {
-    type = with types; nonEmptyListOf (enum [ "node16" "node20" ]);
-    default = [ "node20" ];
-    description = mdDoc ''
-      List of Node.js runtimes the runner should support.
-    '';
+    default = { };
+    type = types.attrsOf (types.submodule ({ name, ... }: {
+      options = {
+        enable = mkOption {
+          default = false;
+          example = true;
+          description = mdDoc ''
+            Whether to enable GitHub Actions runner.
+
+            Note: GitHub recommends using self-hosted runners with private repositories only. Learn more here:
+            [About self-hosted runners](https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners).
+          '';
+          type = types.bool;
+        };
+
+        url = mkOption {
+          type = types.str;
+          description = mdDoc ''
+            Repository to add the runner to.
+
+            Changing this option triggers a new runner registration.
+
+            IMPORTANT: If your token is org-wide (not per repository), you need to
+            provide a github org link, not a single repository, so do it like this
+            `https://github.com/nixos`, not like this
+            `https://github.com/nixos/nixpkgs`.
+            Otherwise, you are going to get a `404 NotFound`
+            from `POST https://api.github.com/actions/runner-registration`
+            in the configure script.
+          '';
+          example = "https://github.com/nixos/nixpkgs";
+        };
+
+        tokenFile = mkOption {
+          type = types.path;
+          description = mdDoc ''
+            The full path to a file which contains either
+
+            * a fine-grained personal access token (PAT),
+            * a classic PAT
+            * or a runner registration token
+
+            Changing this option or the `tokenFile`’s content triggers a new runner registration.
+
+            We suggest using the fine-grained PATs. A runner registration token is valid
+            only for 1 hour after creation, so the next time the runner configuration changes
+            this will give you hard-to-debug HTTP 404 errors in the configure step.
+
+            The file should contain exactly one line with the token without any newline.
+            (Use `echo -n '…token…' > …token file…` to make sure no newlines sneak in.)
+
+            If the file contains a PAT, the service creates a new registration token
+            on startup as needed.
+            If a registration token is given, it can be used to re-register a runner of the same
+            name but is time-limited as noted above.
+
+            For fine-grained PATs:
+
+            Give it "Read and Write access to organization/repository self hosted runners",
+            depending on whether it is organization wide or per-repository. You might have to
+            experiment a little, fine-grained PATs are a `beta` Github feature and still subject
+            to change; nonetheless they are the best option at the moment.
+
+            For classic PATs:
+
+            Make sure the PAT has a scope of `admin:org` for organization-wide registrations
+            or a scope of `repo` for a single repository.
+
+            For runner registration tokens:
+
+            Nothing special needs to be done, but updating will break after one hour,
+            so these are not recommended.
+          '';
+          example = "/run/secrets/github-runner/nixos.token";
+        };
+
+        name = mkOption {
+          type = types.nullOr types.str;
+          description = mdDoc ''
+            Name of the runner to configure. If null, defaults to the hostname.
+
+            Changing this option triggers a new runner registration.
+          '';
+          example = "nixos";
+          default = name;
+        };
+
+        runnerGroup = mkOption {
+          type = types.nullOr types.str;
+          description = mdDoc ''
+            Name of the runner group to add this runner to (defaults to the default runner group).
+
+            Changing this option triggers a new runner registration.
+          '';
+          default = null;
+        };
+
+        extraLabels = mkOption {
+          type = types.listOf types.str;
+          description = mdDoc ''
+            Extra labels in addition to the default (unless disabled through the `noDefaultLabels` option).
+
+            Changing this option triggers a new runner registration.
+          '';
+          example = literalExpression ''[ "nixos" ]'';
+          default = [ ];
+        };
+
+        noDefaultLabels = mkOption {
+          type = types.bool;
+          description = mdDoc ''
+            Disables adding the default labels. Also see the `extraLabels` option.
+
+            Changing this option triggers a new runner registration.
+          '';
+          default = false;
+        };
+
+        replace = mkOption {
+          type = types.bool;
+          description = mdDoc ''
+            Replace any existing runner with the same name.
+
+            Without this flag, registering a new runner with the same name fails.
+          '';
+          default = false;
+        };
+
+        extraPackages = mkOption {
+          type = types.listOf types.package;
+          description = mdDoc ''
+            Extra packages to add to `PATH` of the service to make them available to workflows.
+          '';
+          default = [ ];
+        };
+
+        extraEnvironment = mkOption {
+          type = types.attrs;
+          description = mdDoc ''
+            Extra environment variables to set for the runner, as an attrset.
+          '';
+          example = {
+            GIT_CONFIG = "/path/to/git/config";
+          };
+          default = { };
+        };
+
+        serviceOverrides = mkOption {
+          type = types.attrs;
+          description = mdDoc ''
+            Modify the systemd service. Can be used to, e.g., adjust the sandboxing options.
+            See {manpage}`systemd.exec(5)` for more options.
+          '';
+          example = {
+            ProtectHome = false;
+            RestrictAddressFamilies = [ "AF_PACKET" ];
+          };
+          default = { };
+        };
+
+        package = mkPackageOption pkgs "github-runner" { };
+
+        ephemeral = mkOption {
+          type = types.bool;
+          description = mdDoc ''
+            If enabled, causes the following behavior:
+
+            - Passes the `--ephemeral` flag to the runner configuration script
+            - De-registers and stops the runner with GitHub after it has processed one job
+            - On stop, systemd wipes the runtime directory (this always happens, even without using the ephemeral option)
+            - Restarts the service after its successful exit
+            - On start, wipes the state directory and configures a new runner
+
+            You should only enable this option if `tokenFile` points to a file which contains a
+            personal access token (PAT). If you're using the option with a registration token, restarting the
+            service will fail as soon as the registration token expired.
+
+            Changing this option triggers a new runner registration.
+          '';
+          default = false;
+        };
+
+        user = mkOption {
+          type = types.nullOr types.str;
+          description = mdDoc ''
+            User under which to run the service.
+
+            If this option and the `group` option is set to `null`,
+            the service runs as a dynamically allocated user.
+
+            Also see the `group` option for an overview on the effects of the `user` and `group` settings.
+          '';
+          default = null;
+          defaultText = literalExpression "username";
+        };
+
+        group = mkOption {
+          type = types.nullOr types.str;
+          description = mdDoc ''
+            Group under which to run the service.
+
+            The effect of this option depends on the value of the `user` option:
+
+            - `group == null` and `user == null`:
+              The service runs with a dynamically allocated user and group.
+            - `group == null` and `user != null`:
+              The service runs as the given user and its default group.
+            - `group != null` and `user == null`:
+              This configuration is invalid. In this case, the service would use the given group
+              but run as root implicitly. If this is really what you want, set `user = "root"` explicitly.
+          '';
+          default = null;
+          defaultText = literalExpression "groupname";
+        };
+
+        workDir = mkOption {
+          type = with types; nullOr str;
+          description = mdDoc ''
+            Working directory, available as `$GITHUB_WORKSPACE` during workflow runs
+            and used as a default for [repository checkouts](https://github.com/actions/checkout).
+            The service cleans this directory on every service start.
+
+            A value of `null` will default to the systemd `RuntimeDirectory`.
+
+            Changing this option triggers a new runner registration.
+          '';
+          default = null;
+        };
+
+        nodeRuntimes = mkOption {
+          type = with types; nonEmptyListOf (enum [ "node20" ]);
+          default = [ "node20" ];
+          description = mdDoc ''
+            List of Node.js runtimes the runner should support.
+          '';
+        };
+      };
+    }));
   };
 }
diff --git a/nixos/modules/services/continuous-integration/github-runner/service.nix b/nixos/modules/services/continuous-integration/github-runner/service.nix
index 535df7f68e07..fccdcc116a21 100644
--- a/nixos/modules/services/continuous-integration/github-runner/service.nix
+++ b/nixos/modules/services/continuous-integration/github-runner/service.nix
@@ -1,268 +1,299 @@
 { config
 , lib
 , pkgs
-
-, cfg ? config.services.github-runner
-, svcName
-
-, systemdDir ? "${svcName}/${cfg.name}"
-  # %t: Runtime directory root (usually /run); see systemd.unit(5)
-, runtimeDir ? "%t/${systemdDir}"
-  # %S: State directory root (usually /var/lib); see systemd.unit(5)
-, stateDir ? "%S/${systemdDir}"
-  # %L: Log directory root (usually /var/log); see systemd.unit(5)
-, logsDir ? "%L/${systemdDir}"
-  # Name of file stored in service state directory
-, currentConfigTokenFilename ? ".current-token"
-
 , ...
 }:
 
 with lib;
-
-let
-  workDir = if cfg.workDir == null then runtimeDir else cfg.workDir;
-  package = cfg.package.override { inherit (cfg) nodeRuntimes; };
-in
 {
-  description = "GitHub Actions runner";
+  config.assertions = flatten (
+    flip mapAttrsToList config.services.github-runners (name: cfg: map (mkIf cfg.enable) [
+      {
+        assertion = !cfg.noDefaultLabels || (cfg.extraLabels != [ ]);
+        message = "`services.github-runners.${name}`: The `extraLabels` option is mandatory if `noDefaultLabels` is set";
+      }
+      {
+        assertion = cfg.group == null || cfg.user != null;
+        message = ''`services.github-runners.${name}`: Setting `group` while leaving `user` unset runs the service as `root`. If this is really what you want, set `user = "root"` explicitly'';
+      }
+    ])
+  );
 
-  wantedBy = [ "multi-user.target" ];
-  wants = [ "network-online.target" ];
-  after = [ "network.target" "network-online.target" ];
+  config.systemd.services = flip mapAttrs' config.services.github-runners (name: cfg:
+    let
+      svcName = "github-runner-${name}";
+      systemdDir = "github-runner/${name}";
 
-  environment = {
-    HOME = workDir;
-    RUNNER_ROOT = stateDir;
-  } // cfg.extraEnvironment;
+      # %t: Runtime directory root (usually /run); see systemd.unit(5)
+      runtimeDir = "%t/${systemdDir}";
+      # %S: State directory root (usually /var/lib); see systemd.unit(5)
+      stateDir = "%S/${systemdDir}";
+      # %L: Log directory root (usually /var/log); see systemd.unit(5)
+      logsDir = "%L/${systemdDir}";
+      # Name of file stored in service state directory
+      currentConfigTokenFilename = ".current-token";
 
-  path = (with pkgs; [
-    bash
-    coreutils
-    git
-    gnutar
-    gzip
-  ]) ++ [
-    config.nix.package
-  ] ++ cfg.extraPackages;
+      workDir = if cfg.workDir == null then runtimeDir else cfg.workDir;
+      # Support old github-runner versions which don't have the `nodeRuntimes` arg yet.
+      package = cfg.package.override (old: optionalAttrs (hasAttr "nodeRuntimes" old) { inherit (cfg) nodeRuntimes; });
+    in
+    nameValuePair svcName {
+      description = "GitHub Actions runner";
 
-  serviceConfig = mkMerge [
-    {
-      ExecStart = "${package}/bin/Runner.Listener run --startuptype service";
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
+      after = [ "network.target" "network-online.target" ];
 
-      # Does the following, sequentially:
-      # - If the module configuration or the token has changed, purge the state directory,
-      #   and create the current and the new token file with the contents of the configured
-      #   token. While both files have the same content, only the later is accessible by
-      #   the service user.
-      # - Configure the runner using the new token file. When finished, delete it.
-      # - Set up the directory structure by creating the necessary symlinks.
-      ExecStartPre =
-        let
-          # Wrapper script which expects the full path of the state, working and logs
-          # directory as arguments. Overrides the respective systemd variables to provide
-          # unambiguous directory names. This becomes relevant, for example, if the
-          # caller overrides any of the StateDirectory=, RuntimeDirectory= or LogDirectory=
-          # to contain more than one directory. This causes systemd to set the respective
-          # environment variables with the path of all of the given directories, separated
-          # by a colon.
-          writeScript = name: lines: pkgs.writeShellScript "${svcName}-${name}.sh" ''
-            set -euo pipefail
+      environment = {
+        HOME = workDir;
+        RUNNER_ROOT = stateDir;
+      } // cfg.extraEnvironment;
 
-            STATE_DIRECTORY="$1"
-            WORK_DIRECTORY="$2"
-            LOGS_DIRECTORY="$3"
+      path = (with pkgs; [
+        bash
+        coreutils
+        git
+        gnutar
+        gzip
+      ]) ++ [
+        config.nix.package
+      ] ++ cfg.extraPackages;
 
-            ${lines}
-          '';
-          runnerRegistrationConfig = getAttrs [ "name" "tokenFile" "url" "runnerGroup" "extraLabels" "ephemeral" "workDir" ] cfg;
-          newConfigPath = builtins.toFile "${svcName}-config.json" (builtins.toJSON runnerRegistrationConfig);
-          currentConfigPath = "$STATE_DIRECTORY/.nixos-current-config.json";
-          newConfigTokenPath = "$STATE_DIRECTORY/.new-token";
-          currentConfigTokenPath = "$STATE_DIRECTORY/${currentConfigTokenFilename}";
+      serviceConfig = mkMerge [
+        {
+          ExecStart = "${package}/bin/Runner.Listener run --startuptype service";
 
-          runnerCredFiles = [
-            ".credentials"
-            ".credentials_rsaparams"
-            ".runner"
-          ];
-          unconfigureRunner = writeScript "unconfigure" ''
-            copy_tokens() {
-              # Copy the configured token file to the state dir and allow the service user to read the file
-              install --mode=666 ${escapeShellArg cfg.tokenFile} "${newConfigTokenPath}"
-              # Also copy current file to allow for a diff on the next start
-              install --mode=600 ${escapeShellArg cfg.tokenFile} "${currentConfigTokenPath}"
-            }
-            clean_state() {
-              find "$STATE_DIRECTORY/" -mindepth 1 -delete
-              copy_tokens
-            }
-            diff_config() {
-              changed=0
-              # Check for module config changes
-              [[ -f "${currentConfigPath}" ]] \
-                && ${pkgs.diffutils}/bin/diff -q '${newConfigPath}' "${currentConfigPath}" >/dev/null 2>&1 \
-                || changed=1
-              # Also check the content of the token file
-              [[ -f "${currentConfigTokenPath}" ]] \
-                && ${pkgs.diffutils}/bin/diff -q "${currentConfigTokenPath}" ${escapeShellArg cfg.tokenFile} >/dev/null 2>&1 \
-                || changed=1
-              # If the config has changed, remove old state and copy tokens
-              if [[ "$changed" -eq 1 ]]; then
-                echo "Config has changed, removing old runner state."
-                echo "The old runner will still appear in the GitHub Actions UI." \
-                     "You have to remove it manually."
-                clean_state
-              fi
-            }
-            if [[ "${optionalString cfg.ephemeral "1"}" ]]; then
-              # In ephemeral mode, we always want to start with a clean state
-              clean_state
-            elif [[ "$(ls -A "$STATE_DIRECTORY")" ]]; then
-              # There are state files from a previous run; diff them to decide if we need a new registration
-              diff_config
-            else
-              # The state directory is entirely empty which indicates a first start
-              copy_tokens
-            fi
-            # Always clean workDir
-            find -H "$WORK_DIRECTORY" -mindepth 1 -delete
-          '';
-          configureRunner = writeScript "configure" ''
-            if [[ -e "${newConfigTokenPath}" ]]; then
-              echo "Configuring GitHub Actions Runner"
-              args=(
-                --unattended
-                --disableupdate
-                --work "$WORK_DIRECTORY"
-                --url ${escapeShellArg cfg.url}
-                --labels ${escapeShellArg (concatStringsSep "," cfg.extraLabels)}
-                --name ${escapeShellArg cfg.name}
-                ${optionalString cfg.replace "--replace"}
-                ${optionalString (cfg.runnerGroup != null) "--runnergroup ${escapeShellArg cfg.runnerGroup}"}
-                ${optionalString cfg.ephemeral "--ephemeral"}
-              )
-              # If the token file contains a PAT (i.e., it starts with "ghp_" or "github_pat_"), we have to use the --pat option,
-              # if it is not a PAT, we assume it contains a registration token and use the --token option
-              token=$(<"${newConfigTokenPath}")
-              if [[ "$token" =~ ^ghp_* ]] || [[ "$token" =~ ^github_pat_* ]]; then
-                args+=(--pat "$token")
-              else
-                args+=(--token "$token")
-              fi
-              ${package}/bin/Runner.Listener configure "''${args[@]}"
-              # Move the automatically created _diag dir to the logs dir
-              mkdir -p  "$STATE_DIRECTORY/_diag"
-              cp    -r  "$STATE_DIRECTORY/_diag/." "$LOGS_DIRECTORY/"
-              rm    -rf "$STATE_DIRECTORY/_diag/"
-              # Cleanup token from config
-              rm "${newConfigTokenPath}"
-              # Symlink to new config
-              ln -s '${newConfigPath}' "${currentConfigPath}"
-            fi
-          '';
-          setupWorkDir = writeScript "setup-work-dirs" ''
-            # Link _diag dir
-            ln -s "$LOGS_DIRECTORY" "$WORK_DIRECTORY/_diag"
+          # Does the following, sequentially:
+          # - If the module configuration or the token has changed, purge the state directory,
+          #   and create the current and the new token file with the contents of the configured
+          #   token. While both files have the same content, only the later is accessible by
+          #   the service user.
+          # - Configure the runner using the new token file. When finished, delete it.
+          # - Set up the directory structure by creating the necessary symlinks.
+          ExecStartPre =
+            let
+              # Wrapper script which expects the full path of the state, working and logs
+              # directory as arguments. Overrides the respective systemd variables to provide
+              # unambiguous directory names. This becomes relevant, for example, if the
+              # caller overrides any of the StateDirectory=, RuntimeDirectory= or LogDirectory=
+              # to contain more than one directory. This causes systemd to set the respective
+              # environment variables with the path of all of the given directories, separated
+              # by a colon.
+              writeScript = name: lines: pkgs.writeShellScript "${svcName}-${name}.sh" ''
+                set -euo pipefail
 
-            # Link the runner credentials to the work dir
-            ln -s "$STATE_DIRECTORY"/{${lib.concatStringsSep "," runnerCredFiles}} "$WORK_DIRECTORY/"
-          '';
-        in
-        map (x: "${x} ${escapeShellArgs [ stateDir workDir logsDir ]}") [
-          "+${unconfigureRunner}" # runs as root
-          configureRunner
-          setupWorkDir
-        ];
+                STATE_DIRECTORY="$1"
+                WORK_DIRECTORY="$2"
+                LOGS_DIRECTORY="$3"
 
-      # If running in ephemeral mode, restart the service on-exit (i.e., successful de-registration of the runner)
-      # to trigger a fresh registration.
-      Restart = if cfg.ephemeral then "on-success" else "no";
-      # If the runner exits with `ReturnCode.RetryableError = 2`, always restart the service:
-      # https://github.com/actions/runner/blob/40ed7f8/src/Runner.Common/Constants.cs#L146
-      RestartForceExitStatus = [ 2 ];
+                ${lines}
+              '';
+              runnerRegistrationConfig = getAttrs [
+                "ephemeral"
+                "extraLabels"
+                "name"
+                "noDefaultLabels"
+                "runnerGroup"
+                "tokenFile"
+                "url"
+                "workDir"
+              ]
+                cfg;
+              newConfigPath = builtins.toFile "${svcName}-config.json" (builtins.toJSON runnerRegistrationConfig);
+              currentConfigPath = "$STATE_DIRECTORY/.nixos-current-config.json";
+              newConfigTokenPath = "$STATE_DIRECTORY/.new-token";
+              currentConfigTokenPath = "$STATE_DIRECTORY/${currentConfigTokenFilename}";
 
-      # Contains _diag
-      LogsDirectory = [ systemdDir ];
-      # Default RUNNER_ROOT which contains ephemeral Runner data
-      RuntimeDirectory = [ systemdDir ];
-      # Home of persistent runner data, e.g., credentials
-      StateDirectory = [ systemdDir ];
-      StateDirectoryMode = "0700";
-      WorkingDirectory = workDir;
+              runnerCredFiles = [
+                ".credentials"
+                ".credentials_rsaparams"
+                ".runner"
+              ];
+              unconfigureRunner = writeScript "unconfigure" ''
+                copy_tokens() {
+                  # Copy the configured token file to the state dir and allow the service user to read the file
+                  install --mode=666 ${escapeShellArg cfg.tokenFile} "${newConfigTokenPath}"
+                  # Also copy current file to allow for a diff on the next start
+                  install --mode=600 ${escapeShellArg cfg.tokenFile} "${currentConfigTokenPath}"
+                }
+                clean_state() {
+                  find "$STATE_DIRECTORY/" -mindepth 1 -delete
+                  copy_tokens
+                }
+                diff_config() {
+                  changed=0
+                  # Check for module config changes
+                  [[ -f "${currentConfigPath}" ]] \
+                    && ${pkgs.diffutils}/bin/diff -q '${newConfigPath}' "${currentConfigPath}" >/dev/null 2>&1 \
+                    || changed=1
+                  # Also check the content of the token file
+                  [[ -f "${currentConfigTokenPath}" ]] \
+                    && ${pkgs.diffutils}/bin/diff -q "${currentConfigTokenPath}" ${escapeShellArg cfg.tokenFile} >/dev/null 2>&1 \
+                    || changed=1
+                  # If the config has changed, remove old state and copy tokens
+                  if [[ "$changed" -eq 1 ]]; then
+                    echo "Config has changed, removing old runner state."
+                    echo "The old runner will still appear in the GitHub Actions UI." \
+                         "You have to remove it manually."
+                    clean_state
+                  fi
+                }
+                if [[ "${optionalString cfg.ephemeral "1"}" ]]; then
+                  # In ephemeral mode, we always want to start with a clean state
+                  clean_state
+                elif [[ "$(ls -A "$STATE_DIRECTORY")" ]]; then
+                  # There are state files from a previous run; diff them to decide if we need a new registration
+                  diff_config
+                else
+                  # The state directory is entirely empty which indicates a first start
+                  copy_tokens
+                fi
+                # Always clean workDir
+                find -H "$WORK_DIRECTORY" -mindepth 1 -delete
+              '';
+              configureRunner = writeScript "configure" ''
+                if [[ -e "${newConfigTokenPath}" ]]; then
+                  echo "Configuring GitHub Actions Runner"
+                  args=(
+                    --unattended
+                    --disableupdate
+                    --work "$WORK_DIRECTORY"
+                    --url ${escapeShellArg cfg.url}
+                    --labels ${escapeShellArg (concatStringsSep "," cfg.extraLabels)}
+                    ${optionalString (cfg.name != null ) "--name ${escapeShellArg cfg.name}"}
+                    ${optionalString cfg.replace "--replace"}
+                    ${optionalString (cfg.runnerGroup != null) "--runnergroup ${escapeShellArg cfg.runnerGroup}"}
+                    ${optionalString cfg.ephemeral "--ephemeral"}
+                    ${optionalString cfg.noDefaultLabels "--no-default-labels"}
+                  )
+                  # If the token file contains a PAT (i.e., it starts with "ghp_" or "github_pat_"), we have to use the --pat option,
+                  # if it is not a PAT, we assume it contains a registration token and use the --token option
+                  token=$(<"${newConfigTokenPath}")
+                  if [[ "$token" =~ ^ghp_* ]] || [[ "$token" =~ ^github_pat_* ]]; then
+                    args+=(--pat "$token")
+                  else
+                    args+=(--token "$token")
+                  fi
+                  ${package}/bin/Runner.Listener configure "''${args[@]}"
+                  # Move the automatically created _diag dir to the logs dir
+                  mkdir -p  "$STATE_DIRECTORY/_diag"
+                  cp    -r  "$STATE_DIRECTORY/_diag/." "$LOGS_DIRECTORY/"
+                  rm    -rf "$STATE_DIRECTORY/_diag/"
+                  # Cleanup token from config
+                  rm "${newConfigTokenPath}"
+                  # Symlink to new config
+                  ln -s '${newConfigPath}' "${currentConfigPath}"
+                fi
+              '';
+              setupWorkDir = writeScript "setup-work-dirs" ''
+                # Link _diag dir
+                ln -s "$LOGS_DIRECTORY" "$WORK_DIRECTORY/_diag"
 
-      InaccessiblePaths = [
-        # Token file path given in the configuration, if visible to the service
-        "-${cfg.tokenFile}"
-        # Token file in the state directory
-        "${stateDir}/${currentConfigTokenFilename}"
-      ];
+                # Link the runner credentials to the work dir
+                ln -s "$STATE_DIRECTORY"/{${lib.concatStringsSep "," runnerCredFiles}} "$WORK_DIRECTORY/"
+              '';
+            in
+            map (x: "${x} ${escapeShellArgs [ stateDir workDir logsDir ]}") [
+              "+${unconfigureRunner}" # runs as root
+              configureRunner
+              setupWorkDir
+            ];
 
-      KillSignal = "SIGINT";
+          # If running in ephemeral mode, restart the service on-exit (i.e., successful de-registration of the runner)
+          # to trigger a fresh registration.
+          Restart = if cfg.ephemeral then "on-success" else "no";
+          # If the runner exits with `ReturnCode.RetryableError = 2`, always restart the service:
+          # https://github.com/actions/runner/blob/40ed7f8/src/Runner.Common/Constants.cs#L146
+          RestartForceExitStatus = [ 2 ];
 
-      # Hardening (may overlap with DynamicUser=)
-      # The following options are only for optimizing:
-      # systemd-analyze security github-runner
-      AmbientCapabilities = mkBefore [ "" ];
-      CapabilityBoundingSet = mkBefore [ "" ];
-      # ProtectClock= adds DeviceAllow=char-rtc r
-      DeviceAllow = mkBefore [ "" ];
-      NoNewPrivileges = mkDefault true;
-      PrivateDevices = mkDefault true;
-      PrivateMounts = mkDefault true;
-      PrivateTmp = mkDefault true;
-      PrivateUsers = mkDefault true;
-      ProtectClock = mkDefault true;
-      ProtectControlGroups = mkDefault true;
-      ProtectHome = mkDefault true;
-      ProtectHostname = mkDefault true;
-      ProtectKernelLogs = mkDefault true;
-      ProtectKernelModules = mkDefault true;
-      ProtectKernelTunables = mkDefault true;
-      ProtectSystem = mkDefault "strict";
-      RemoveIPC = mkDefault true;
-      RestrictNamespaces = mkDefault true;
-      RestrictRealtime = mkDefault true;
-      RestrictSUIDSGID = mkDefault true;
-      UMask = mkDefault "0066";
-      ProtectProc = mkDefault "invisible";
-      SystemCallFilter = mkBefore [
-        "~@clock"
-        "~@cpu-emulation"
-        "~@module"
-        "~@mount"
-        "~@obsolete"
-        "~@raw-io"
-        "~@reboot"
-        "~capset"
-        "~setdomainname"
-        "~sethostname"
-      ];
-      RestrictAddressFamilies = mkBefore [ "AF_INET" "AF_INET6" "AF_UNIX" "AF_NETLINK" ];
+          # Contains _diag
+          LogsDirectory = [ systemdDir ];
+          # Default RUNNER_ROOT which contains ephemeral Runner data
+          RuntimeDirectory = [ systemdDir ];
+          # Home of persistent runner data, e.g., credentials
+          StateDirectory = [ systemdDir ];
+          StateDirectoryMode = "0700";
+          WorkingDirectory = workDir;
 
-      BindPaths = lib.optionals (cfg.workDir != null) [ cfg.workDir ];
+          InaccessiblePaths = [
+            # Token file path given in the configuration, if visible to the service
+            "-${cfg.tokenFile}"
+            # Token file in the state directory
+            "${stateDir}/${currentConfigTokenFilename}"
+          ];
+
+          KillSignal = "SIGINT";
+
+          # Hardening (may overlap with DynamicUser=)
+          # The following options are only for optimizing:
+          # systemd-analyze security github-runner
+          AmbientCapabilities = mkBefore [ "" ];
+          CapabilityBoundingSet = mkBefore [ "" ];
+          # ProtectClock= adds DeviceAllow=char-rtc r
+          DeviceAllow = mkBefore [ "" ];
+          NoNewPrivileges = mkDefault true;
+          PrivateDevices = mkDefault true;
+          PrivateMounts = mkDefault true;
+          PrivateTmp = mkDefault true;
+          PrivateUsers = mkDefault true;
+          ProtectClock = mkDefault true;
+          ProtectControlGroups = mkDefault true;
+          ProtectHome = mkDefault true;
+          ProtectHostname = mkDefault true;
+          ProtectKernelLogs = mkDefault true;
+          ProtectKernelModules = mkDefault true;
+          ProtectKernelTunables = mkDefault true;
+          ProtectSystem = mkDefault "strict";
+          RemoveIPC = mkDefault true;
+          RestrictNamespaces = mkDefault true;
+          RestrictRealtime = mkDefault true;
+          RestrictSUIDSGID = mkDefault true;
+          UMask = mkDefault "0066";
+          ProtectProc = mkDefault "invisible";
+          SystemCallFilter = mkBefore [
+            "~@clock"
+            "~@cpu-emulation"
+            "~@module"
+            "~@mount"
+            "~@obsolete"
+            "~@raw-io"
+            "~@reboot"
+            "~capset"
+            "~setdomainname"
+            "~sethostname"
+          ];
+          RestrictAddressFamilies = mkBefore [ "AF_INET" "AF_INET6" "AF_UNIX" "AF_NETLINK" ];
+
+          BindPaths = lib.optionals (cfg.workDir != null) [ cfg.workDir ];
 
-      # Needs network access
-      PrivateNetwork = mkDefault false;
-      # Cannot be true due to Node
-      MemoryDenyWriteExecute = mkDefault false;
+          # Needs network access
+          PrivateNetwork = mkDefault false;
+          # Cannot be true due to Node
+          MemoryDenyWriteExecute = mkDefault false;
 
-      # The more restrictive "pid" option makes `nix` commands in CI emit
-      # "GC Warning: Couldn't read /proc/stat"
-      # You may want to set this to "pid" if not using `nix` commands
-      ProcSubset = mkDefault "all";
-      # Coverage programs for compiled code such as `cargo-tarpaulin` disable
-      # ASLR (address space layout randomization) which requires the
-      # `personality` syscall
-      # You may want to set this to `true` if not using coverage tooling on
-      # compiled code
-      LockPersonality = mkDefault false;
+          # The more restrictive "pid" option makes `nix` commands in CI emit
+          # "GC Warning: Couldn't read /proc/stat"
+          # You may want to set this to "pid" if not using `nix` commands
+          ProcSubset = mkDefault "all";
+          # Coverage programs for compiled code such as `cargo-tarpaulin` disable
+          # ASLR (address space layout randomization) which requires the
+          # `personality` syscall
+          # You may want to set this to `true` if not using coverage tooling on
+          # compiled code
+          LockPersonality = mkDefault false;
 
-      # Note that this has some interactions with the User setting; so you may
-      # want to consult the systemd docs if using both.
-      DynamicUser = mkDefault true;
+          DynamicUser = mkDefault true;
+        }
+        (mkIf (cfg.user != null) {
+          DynamicUser = false;
+          User = cfg.user;
+        })
+        (mkIf (cfg.group != null) {
+          DynamicUser = false;
+          Group = cfg.group;
+        })
+        cfg.serviceOverrides
+      ];
     }
-    (mkIf (cfg.user != null) { User = cfg.user; })
-    cfg.serviceOverrides
-  ];
+  );
 }
diff --git a/nixos/modules/services/continuous-integration/github-runners.nix b/nixos/modules/services/continuous-integration/github-runners.nix
index 66ace9580eca..4a4608c2e4f8 100644
--- a/nixos/modules/services/continuous-integration/github-runners.nix
+++ b/nixos/modules/services/continuous-integration/github-runners.nix
@@ -1,58 +1,10 @@
-{ config
-, pkgs
-, lib
-, ...
-}@args:
-
-with lib;
-
-let
-  cfg = config.services.github-runners;
-
-in
-
+{ lib, ... }:
 {
-  options.services.github-runners = mkOption {
-    default = {};
-    type = with types; attrsOf (submodule { options = import ./github-runner/options.nix (args // {
-      # services.github-runners.${name}.name doesn't have a default; it falls back to ${name} below.
-      includeNameDefault = false;
-    }); });
-    example = {
-      runner1 = {
-        enable = true;
-        url = "https://github.com/owner/repo";
-        name = "runner1";
-        tokenFile = "/secrets/token1";
-      };
-
-      runner2 = {
-        enable = true;
-        url = "https://github.com/owner/repo";
-        name = "runner2";
-        tokenFile = "/secrets/token2";
-      };
-    };
-    description = lib.mdDoc ''
-      Multiple GitHub Runners.
-    '';
-  };
-
-  config = {
-    systemd.services = flip mapAttrs' cfg (n: v:
-      let
-        svcName = "github-runner-${n}";
-      in
-        nameValuePair svcName
-        (import ./github-runner/service.nix (args // {
-          inherit svcName;
-          cfg = v // {
-            name = if v.name != null then v.name else n;
-          };
-          systemdDir = "github-runner/${n}";
-        }))
-    );
-  };
+  imports = [
+    (lib.mkRemovedOptionModule [ "services" "github-runner" ] "Use `services.github-runners.*` instead")
+    ./github-runner/options.nix
+    ./github-runner/service.nix
+  ];
 
-  meta.maintainers = with maintainers; [ veehaitch newam ];
+  meta.maintainers = with lib.maintainers; [ veehaitch newam ];
 }
diff --git a/nixos/modules/services/hardware/fwupd.nix b/nixos/modules/services/hardware/fwupd.nix
index 6fbcbe676460..8a9e38d0547b 100644
--- a/nixos/modules/services/hardware/fwupd.nix
+++ b/nixos/modules/services/hardware/fwupd.nix
@@ -51,7 +51,9 @@ let
     # to install it because it would create a cyclic dependency between
     # the outputs. We also need to enable the remote,
     # which should not be done by default.
-    lib.optionalAttrs cfg.enableTestRemote (enableRemote cfg.package.installedTests "fwupd-tests")
+    lib.optionalAttrs
+      (cfg.daemonSettings.TestDevices or false)
+      (enableRemote cfg.package.installedTests "fwupd-tests")
   );
 
 in {
@@ -86,15 +88,6 @@ in {
         '';
       };
 
-      enableTestRemote = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Whether to enable test remote. This is used by
-          [installed tests](https://github.com/fwupd/fwupd/blob/master/data/installed-tests/README.md).
-        '';
-      };
-
       package = mkPackageOption pkgs "fwupd" { };
 
       daemonSettings = mkOption {
@@ -128,6 +121,16 @@ in {
                 or if this partition is not mounted at /boot/efi, /boot, or /efi
               '';
             };
+
+            TestDevices = mkOption {
+              internal = true;
+              type = types.bool;
+              default = false;
+              description = lib.mdDoc ''
+                Create virtual test devices and remote for validating daemon flows.
+                This is only intended for CI testing and development purposes.
+              '';
+            };
           };
         };
         default = {};
@@ -153,13 +156,13 @@ in {
     (mkRenamedOptionModule [ "services" "fwupd" "blacklistPlugins"] [ "services" "fwupd" "daemonSettings" "DisabledPlugins" ])
     (mkRenamedOptionModule [ "services" "fwupd" "disabledDevices" ] [ "services" "fwupd" "daemonSettings" "DisabledDevices" ])
     (mkRenamedOptionModule [ "services" "fwupd" "disabledPlugins" ] [ "services" "fwupd" "daemonSettings" "DisabledPlugins" ])
+    (mkRemovedOptionModule [ "services" "fwupd" "enableTestRemote" ] "This option was removed after being removed upstream. It only provided a method for testing fwupd functionality, and should not have been exposed for use outside of nix tests.")
   ];
 
   ###### implementation
   config = mkIf cfg.enable {
     # Disable test related plug-ins implicitly so that users do not have to care about them.
     services.fwupd.daemonSettings = {
-      DisabledPlugins = cfg.package.defaultDisabledPlugins;
       EspLocation = config.boot.loader.efi.efiSysMountPoint;
     };
 
diff --git a/nixos/modules/services/hardware/pcscd.nix b/nixos/modules/services/hardware/pcscd.nix
index 85accd8335f7..b5963e1d29a3 100644
--- a/nixos/modules/services/hardware/pcscd.nix
+++ b/nixos/modules/services/hardware/pcscd.nix
@@ -46,8 +46,8 @@ in
   config = mkIf config.services.pcscd.enable {
     environment.etc."reader.conf".source = cfgFile;
 
-    environment.systemPackages = [ package.out ];
-    systemd.packages = [ (getBin package) ];
+    environment.systemPackages = [ package ];
+    systemd.packages = [ package ];
 
     services.pcscd.plugins = [ pkgs.ccid ];
 
@@ -64,7 +64,7 @@ in
       # around it, we force the path to the cfgFile.
       #
       # https://github.com/NixOS/nixpkgs/issues/121088
-      serviceConfig.ExecStart = [ "" "${getBin package}/bin/pcscd -f -x -c ${cfgFile}" ];
+      serviceConfig.ExecStart = [ "" "${package}/bin/pcscd -f -x -c ${cfgFile}" ];
     };
   };
 }
diff --git a/nixos/modules/services/home-automation/home-assistant.nix b/nixos/modules/services/home-automation/home-assistant.nix
index a01628968966..3423eebe9ed6 100644
--- a/nixos/modules/services/home-automation/home-assistant.nix
+++ b/nixos/modules/services/home-automation/home-assistant.nix
@@ -52,7 +52,7 @@ let
     hasAttrByPath (splitString "." component) cfg.config
     || useComponentPlatform component
     || useExplicitComponent component
-    || builtins.elem component cfg.extraComponents;
+    || builtins.elem component (cfg.extraComponents ++ cfg.defaultIntegrations);
 
   # Final list of components passed into the package to include required dependencies
   extraComponents = filter useComponent availableComponents;
@@ -103,6 +103,45 @@ in {
       description = lib.mdDoc "The config directory, where your {file}`configuration.yaml` is located.";
     };
 
+    defaultIntegrations = mkOption {
+      type = types.listOf (types.enum availableComponents);
+      # https://github.com/home-assistant/core/blob/dev/homeassistant/bootstrap.py#L109
+      default = [
+        "application_credentials"
+        "frontend"
+        "hardware"
+        "logger"
+        "network"
+        "system_health"
+
+        # key features
+        "automation"
+        "person"
+        "scene"
+        "script"
+        "tag"
+        "zone"
+
+        # built-in helpers
+        "counter"
+        "input_boolean"
+        "input_button"
+        "input_datetime"
+        "input_number"
+        "input_select"
+        "input_text"
+        "schedule"
+        "timer"
+
+        # non-supervisor
+        "backup"
+      ];
+      readOnly = true;
+      description = ''
+        List of integrations set are always set up, unless in recovery mode.
+      '';
+    };
+
     extraComponents = mkOption {
       type = types.listOf (types.enum availableComponents);
       default = [
@@ -533,6 +572,7 @@ in {
           "inkbird"
           "improv_ble"
           "keymitt_ble"
+          "leaone-ble"
           "led_ble"
           "medcom_ble"
           "melnor"
diff --git a/nixos/modules/services/matrix/synapse.md b/nixos/modules/services/matrix/synapse.md
index f270be8c8d78..9c9c025fc5f5 100644
--- a/nixos/modules/services/matrix/synapse.md
+++ b/nixos/modules/services/matrix/synapse.md
@@ -126,8 +126,9 @@ then enable `services.matrix-synapse.settings.enable_registration = true;`.
 Otherwise, or you can generate a registration secret with
 {command}`pwgen -s 64 1` and set it with
 [](#opt-services.matrix-synapse.settings.registration_shared_secret).
-To create a new user or admin, run the following after you have set the secret
-and have rebuilt NixOS:
+To create a new user or admin from the terminal your client listener
+must be configured to use TCP sockets. Then you can run the following
+after you have set the secret and have rebuilt NixOS:
 ```ShellSession
 $ nix-shell -p matrix-synapse
 $ register_new_matrix_user -k your-registration-shared-secret http://localhost:8008
diff --git a/nixos/modules/services/matrix/synapse.nix b/nixos/modules/services/matrix/synapse.nix
index 4c1c396eac05..e3f9c7742cc7 100644
--- a/nixos/modules/services/matrix/synapse.nix
+++ b/nixos/modules/services/matrix/synapse.nix
@@ -6,8 +6,16 @@ let
   cfg = config.services.matrix-synapse;
   format = pkgs.formats.yaml { };
 
+  filterRecursiveNull = o:
+    if isAttrs o then
+      mapAttrs (_: v: filterRecursiveNull v) (filterAttrs (_: v: v != null) o)
+    else if isList o then
+      map filterRecursiveNull (filter (v: v != null) o)
+    else
+      o;
+
   # remove null values from the final configuration
-  finalSettings = lib.filterAttrsRecursive (_: v: v != null) cfg.settings;
+  finalSettings = filterRecursiveNull cfg.settings;
   configFile = format.generate "homeserver.yaml" finalSettings;
 
   usePostgresql = cfg.settings.database.name == "psycopg2";
@@ -105,6 +113,19 @@ let
         SYSLOG_IDENTIFIER = logName;
       };
     });
+
+  toIntBase8 = str:
+    lib.pipe str [
+      lib.stringToCharacters
+      (map lib.toInt)
+      (lib.foldl (acc: digit: acc * 8 + digit) 0)
+    ];
+
+  toDecimalFilePermission = value:
+    if value == null then
+      null
+    else
+      toIntBase8 value;
 in {
 
   imports = [
@@ -192,10 +213,11 @@ in {
   ];
 
   options = let
-    listenerType = workerContext: types.submodule {
+    listenerType = workerContext: types.submodule ({ config, ... }: {
       options = {
         port = mkOption {
-          type = types.port;
+          type = types.nullOr types.port;
+          default = null;
           example = 8448;
           description = lib.mdDoc ''
             The port to listen for HTTP(S) requests on.
@@ -203,11 +225,20 @@ in {
         };
 
         bind_addresses = mkOption {
-          type = types.listOf types.str;
-          default = [
+          type = types.nullOr (types.listOf types.str);
+          default = if config.path != null then null else [
             "::1"
             "127.0.0.1"
           ];
+          defaultText = literalExpression ''
+            if path != null then
+              null
+            else
+              [
+                "::1"
+                "127.0.0.1"
+              ]
+          '';
           example = literalExpression ''
             [
               "::"
@@ -219,6 +250,35 @@ in {
           '';
         };
 
+        path = mkOption {
+          type = types.nullOr types.path;
+          default = null;
+          description = ''
+            Unix domain socket path to bind this listener to.
+
+            ::: {.note}
+              This option is incompatible with {option}`bind_addresses`, {option}`port`, {option}`tls`
+              and also does not support the `metrics` and `manhole` listener {option}`type`.
+            :::
+          '';
+        };
+
+        mode = mkOption {
+          type = types.nullOr (types.strMatching "^[0,2-7]{3,4}$");
+          default = if config.path != null then "660" else null;
+          defaultText = literalExpression ''
+            if path != null then
+              "660"
+            else
+              null
+          '';
+          example = "660";
+          description = ''
+            File permissions on the UNIX domain socket.
+          '';
+          apply = toDecimalFilePermission;
+        };
+
         type = mkOption {
           type = types.enum [
             "http"
@@ -234,17 +294,30 @@ in {
         };
 
         tls = mkOption {
-          type = types.bool;
-          default = !workerContext;
+          type = types.nullOr types.bool;
+          default = if config.path != null then
+            null
+          else
+            !workerContext;
+          defaultText = ''
+            Enabled for the main instance listener, unless it is configured with a UNIX domain socket path.
+          '';
           example = false;
           description = lib.mdDoc ''
             Whether to enable TLS on the listener socket.
+
+            ::: {.note}
+              This option will be ignored for UNIX domain sockets.
+            :::
           '';
         };
 
         x_forwarded = mkOption {
           type = types.bool;
-          default = false;
+          default = config.path != null;
+          defaultText = ''
+            Enabled if the listener is configured with a UNIX domain socket path
+          '';
           example = true;
           description = lib.mdDoc ''
             Use the X-Forwarded-For (XFF) header as the client IP and not the
@@ -291,11 +364,28 @@ in {
           '';
         };
       };
-    };
+    });
   in {
     services.matrix-synapse = {
       enable = mkEnableOption (lib.mdDoc "matrix.org synapse");
 
+      enableRegistrationScript = mkOption {
+        type = types.bool;
+        default = clientListener.bind_addresses != [];
+        example = false;
+        defaultText = ''
+          Enabled if the client listener uses TCP sockets
+        '';
+        description = ''
+          Whether to install the `register_new_matrix_user` script, that
+          allows account creation on the terminal.
+
+          ::: {.note}
+            This script does not work when the client listener uses UNIX domain sockets
+          :::
+        '';
+      };
+
       serviceUnit = lib.mkOption {
         type = lib.types.str;
         readOnly = true;
@@ -616,11 +706,8 @@ in {
                   compress = false;
                 }];
               }] ++ lib.optional hasWorkers {
-                port = 9093;
-                bind_addresses = [ "127.0.0.1" ];
+                path = "/run/matrix-synapse/main_replication.sock";
                 type = "http";
-                tls = false;
-                x_forwarded = false;
                 resources = [{
                   names = [ "replication" ];
                   compress = false;
@@ -630,7 +717,7 @@ in {
                 List of ports that Synapse should listen on, their purpose and their configuration.
 
                 By default, synapse will be configured for client and federation traffic on port 8008, and
-                for worker replication traffic on port 9093. See [`services.matrix-synapse.workers`](#opt-services.matrix-synapse.workers)
+                use a UNIX domain socket for worker replication. See [`services.matrix-synapse.workers`](#opt-services.matrix-synapse.workers)
                 for more details.
               '';
             };
@@ -1006,9 +1093,15 @@ in {
             listener = lib.findFirst
               (
                 listener:
-                  listener.port == main.port
+                  (
+                    lib.hasAttr "port" main && listener.port or null == main.port
+                    || lib.hasAttr "path" main && listener.path or null == main.path
+                  )
                   && listenerSupportsResource "replication" listener
-                  && (lib.any (bind: bind == main.host || bind == "0.0.0.0" || bind == "::") listener.bind_addresses)
+                  && (
+                    lib.hasAttr "host" main &&  lib.any (bind: bind == main.host || bind == "0.0.0.0" || bind == "::") listener.bind_addresses
+                    || lib.hasAttr "path" main
+                  )
               )
               null
               cfg.settings.listeners;
@@ -1022,15 +1115,44 @@ in {
           This is done by default unless you manually configure either of those settings.
         '';
       }
-    ];
+      {
+        assertion = cfg.enableRegistrationScript -> clientListener.path == null;
+        message = ''
+          The client listener on matrix-synapse is configured to use UNIX domain sockets.
+          This configuration is incompatible with the `register_new_matrix_user` script.
+
+          Disable  `services.mastrix-synapse.enableRegistrationScript` to continue.
+        '';
+      }
+    ]
+    ++ (map (listener: {
+      assertion = (listener.path == null) != (listener.bind_addresses == null);
+      message = ''
+        Listeners require either a UNIX domain socket `path` or `bind_addresses` for a TCP socket.
+      '';
+    }) cfg.settings.listeners)
+    ++ (map (listener: {
+      assertion = listener.path != null -> (listener.bind_addresses == null && listener.port == null && listener.tls == null);
+      message = let
+        formatKeyValue = key: value: lib.optionalString (value != null) "  - ${key}=${toString value}\n";
+      in ''
+        Listener configured with UNIX domain socket (${toString listener.path}) ignores the following options:
+        ${formatKeyValue "bind_addresses" listener.bind_addresses}${formatKeyValue "port" listener.port}${formatKeyValue "tls" listener.tls}
+      '';
+    }) cfg.settings.listeners)
+    ++ (map (listener: {
+      assertion = listener.path == null || listener.type == "http";
+      message = ''
+        Listener configured with UNIX domain socket (${toString listener.path}) only supports the "http" listener type.
+      '';
+    }) cfg.settings.listeners);
 
     services.matrix-synapse.settings.redis = lib.mkIf cfg.configureRedisLocally {
       enabled = true;
       path = config.services.redis.servers.matrix-synapse.unixSocket;
     };
     services.matrix-synapse.settings.instance_map.main = lib.mkIf hasWorkers (lib.mkDefault {
-      host = "127.0.0.1";
-      port = 9093;
+      path = "/run/matrix-synapse/main_replication.sock";
     });
 
     services.matrix-synapse.serviceUnit = if hasWorkers then "matrix-synapse.target" else "matrix-synapse.service";
@@ -1086,6 +1208,8 @@ in {
             User = "matrix-synapse";
             Group = "matrix-synapse";
             WorkingDirectory = cfg.dataDir;
+            RuntimeDirectory = "matrix-synapse";
+            RuntimeDirectoryPreserve = true;
             ExecReload = "${pkgs.util-linux}/bin/kill -HUP $MAINPID";
             Restart = "on-failure";
             UMask = "0077";
@@ -1178,7 +1302,9 @@ in {
       user = "matrix-synapse";
     };
 
-    environment.systemPackages = [ registerNewMatrixUser ];
+    environment.systemPackages = lib.optionals cfg.enableRegistrationScript [
+      registerNewMatrixUser
+    ];
   };
 
   meta = {
diff --git a/nixos/modules/services/misc/gitea.nix b/nixos/modules/services/misc/gitea.nix
index d0135b2ba7ac..08feea853e47 100644
--- a/nixos/modules/services/misc/gitea.nix
+++ b/nixos/modules/services/misc/gitea.nix
@@ -681,6 +681,11 @@ in
       optional (cfg.database.password != "") "config.services.gitea.database.password will be stored as plaintext in the Nix store. Use database.passwordFile instead." ++
       optional (cfg.extraConfig != null) ''
         services.gitea.`extraConfig` is deprecated, please use services.gitea.`settings`.
+      '' ++
+      optional (lib.getName cfg.package == "forgejo") ''
+        Running forgejo via services.gitea.package is no longer supported.
+        Please use services.forgejo instead.
+        See https://nixos.org/manual/nixos/unstable/#module-forgejo for migration instructions.
       '';
 
     # Create database passwordFile default when password is configured.
diff --git a/nixos/modules/services/misc/nix-gc.nix b/nixos/modules/services/misc/nix-gc.nix
index de6bd76c7eb9..656cbad81373 100644
--- a/nixos/modules/services/misc/nix-gc.nix
+++ b/nixos/modules/services/misc/nix-gc.nix
@@ -64,8 +64,7 @@ in
         example = "--max-freed $((64 * 1024**3))";
         type = lib.types.singleLineStr;
         description = lib.mdDoc ''
-          Options given to {file}`nix-collect-garbage` when the
-          garbage collector is run automatically.
+          Options given to [`nix-collect-garbage`](https://nixos.org/manual/nix/stable/command-ref/nix-collect-garbage) when the garbage collector is run automatically.
         '';
       };
 
diff --git a/nixos/modules/services/misc/sourcehut/default.nix b/nixos/modules/services/misc/sourcehut/default.nix
index aa803d3bb693..80a6162b2168 100644
--- a/nixos/modules/services/misc/sourcehut/default.nix
+++ b/nixos/modules/services/misc/sourcehut/default.nix
@@ -1370,5 +1370,5 @@ in
   ];
 
   meta.doc = ./default.md;
-  meta.maintainers = with maintainers; [ tomberek nessdoor ];
+  meta.maintainers = with maintainers; [ tomberek nessdoor christoph-heiss ];
 }
diff --git a/nixos/modules/services/networking/cloudflared.nix b/nixos/modules/services/networking/cloudflared.nix
index 80c60fdb8013..b9556bfa60d0 100644
--- a/nixos/modules/services/networking/cloudflared.nix
+++ b/nixos/modules/services/networking/cloudflared.nix
@@ -276,9 +276,11 @@ in
             ingressesSet = filterIngressSet tunnel.ingress;
             ingressesStr = filterIngressStr tunnel.ingress;
 
-            fullConfig = {
+            fullConfig = filterConfig {
               tunnel = name;
               "credentials-file" = tunnel.credentialsFile;
+              warp-routing = filterConfig tunnel.warp-routing;
+              originRequest = filterConfig tunnel.originRequest;
               ingress =
                 (map
                   (key: {
@@ -294,6 +296,7 @@ in
                   (attrNames ingressesStr))
                 ++ [{ service = tunnel.default; }];
             };
+
             mkConfigFile = pkgs.writeText "cloudflared.yml" (builtins.toJSON fullConfig);
           in
           nameValuePair "cloudflared-tunnel-${name}" ({
@@ -322,5 +325,5 @@ in
     };
   };
 
-  meta.maintainers = with maintainers; [ bbigras ];
+  meta.maintainers = with maintainers; [ bbigras anpin ];
 }
diff --git a/nixos/modules/services/networking/dhcpcd.nix b/nixos/modules/services/networking/dhcpcd.nix
index 2b59352ac616..266a7ea1435e 100644
--- a/nixos/modules/services/networking/dhcpcd.nix
+++ b/nixos/modules/services/networking/dhcpcd.nix
@@ -219,6 +219,8 @@ in
       '';
     } ];
 
+    environment.etc."dhcpcd.conf".source = dhcpcdConf;
+
     systemd.services.dhcpcd = let
       cfgN = config.networking;
       hasDefaultGatewaySet = (cfgN.defaultGateway != null && cfgN.defaultGateway.address != "")
diff --git a/nixos/modules/services/networking/jibri/default.nix b/nixos/modules/services/networking/jibri/default.nix
index 73d11bdbee5a..dfba38896a91 100644
--- a/nixos/modules/services/networking/jibri/default.nix
+++ b/nixos/modules/services/networking/jibri/default.nix
@@ -5,12 +5,7 @@ with lib;
 let
   cfg = config.services.jibri;
 
-  # Copied from the jitsi-videobridge.nix file.
-  toHOCON = x:
-    if isAttrs x && x ? __hocon_envvar then ("\${" + x.__hocon_envvar + "}")
-    else if isAttrs x then "{${ concatStringsSep "," (mapAttrsToList (k: v: ''"${k}":${toHOCON v}'') x) }}"
-    else if isList x then "[${ concatMapStringsSep "," toHOCON x }]"
-    else builtins.toJSON x;
+  format = pkgs.formats.hocon { };
 
   # We're passing passwords in environment variables that have names generated
   # from an attribute name, which may not be a valid bash identifier.
@@ -38,13 +33,13 @@ let
         control-login = {
           domain = env.control.login.domain;
           username = env.control.login.username;
-          password.__hocon_envvar = toVarName "${name}_control";
+          password = format.lib.mkSubstitution (toVarName "${name}_control");
         };
 
         call-login = {
           domain = env.call.login.domain;
           username = env.call.login.username;
-          password.__hocon_envvar = toVarName "${name}_call";
+          password = format.lib.mkSubstitution (toVarName "${name}_call");
         };
 
         strip-from-room-domain = env.stripFromRoomDomain;
@@ -85,13 +80,13 @@ let
   };
   # Allow overriding leaves of the default config despite types.attrs not doing any merging.
   jibriConfig = recursiveUpdate defaultJibriConfig cfg.config;
-  configFile = pkgs.writeText "jibri.conf" (toHOCON { jibri = jibriConfig; });
+  configFile = format.generate "jibri.conf" { jibri = jibriConfig; };
 in
 {
   options.services.jibri = with types; {
     enable = mkEnableOption (lib.mdDoc "Jitsi BRoadcasting Infrastructure. Currently Jibri must be run on a host that is also running {option}`services.jitsi-meet.enable`, so for most use cases it will be simpler to run {option}`services.jitsi-meet.jibri.enable`");
     config = mkOption {
-      type = attrs;
+      type = format.type;
       default = { };
       description = lib.mdDoc ''
         Jibri configuration.
diff --git a/nixos/modules/services/networking/jicofo.nix b/nixos/modules/services/networking/jicofo.nix
index 0886bbe004c4..380344c8eaa1 100644
--- a/nixos/modules/services/networking/jicofo.nix
+++ b/nixos/modules/services/networking/jicofo.nix
@@ -5,14 +5,9 @@ with lib;
 let
   cfg = config.services.jicofo;
 
-  # HOCON is a JSON superset that some jitsi-meet components use for configuration
-  toHOCON = x: if isAttrs x && x ? __hocon_envvar then ("\${" + x.__hocon_envvar + "}")
-    else if isAttrs x && x ? __hocon_unquoted_string then x.__hocon_unquoted_string
-    else if isAttrs x then "{${ concatStringsSep "," (mapAttrsToList (k: v: ''"${k}":${toHOCON v}'') x) }}"
-    else if isList x then "[${ concatMapStringsSep "," toHOCON x }]"
-    else builtins.toJSON x;
-
-  configFile = pkgs.writeText "jicofo.conf" (toHOCON cfg.config);
+  format = pkgs.formats.hocon { };
+
+  configFile = format.generate "jicofo.conf" cfg.config;
 in
 {
   options.services.jicofo = with types; {
@@ -77,7 +72,7 @@ in
     };
 
     config = mkOption {
-      type = (pkgs.formats.json {}).type;
+      type = format.type;
       default = { };
       example = literalExpression ''
         {
@@ -99,7 +94,7 @@ in
             hostname = cfg.xmppHost;
             username = cfg.userName;
             domain = cfg.userDomain;
-            password = { __hocon_envvar = "JICOFO_AUTH_PASS"; };
+            password = format.lib.mkSubstitution "JICOFO_AUTH_PASS";
             xmpp-domain = if cfg.xmppDomain == null then cfg.xmppHost else cfg.xmppDomain;
           };
           service = client;
diff --git a/nixos/modules/services/networking/jitsi-videobridge.nix b/nixos/modules/services/networking/jitsi-videobridge.nix
index 37b0b1e5bf50..00ea5b9da546 100644
--- a/nixos/modules/services/networking/jitsi-videobridge.nix
+++ b/nixos/modules/services/networking/jitsi-videobridge.nix
@@ -6,16 +6,7 @@ let
   cfg = config.services.jitsi-videobridge;
   attrsToArgs = a: concatStringsSep " " (mapAttrsToList (k: v: "${k}=${toString v}") a);
 
-  # HOCON is a JSON superset that videobridge2 uses for configuration.
-  # It can substitute environment variables which we use for passwords here.
-  # https://github.com/lightbend/config/blob/master/README.md
-  #
-  # Substitution for environment variable FOO is represented as attribute set
-  # { __hocon_envvar = "FOO"; }
-  toHOCON = x: if isAttrs x && x ? __hocon_envvar then ("\${" + x.__hocon_envvar + "}")
-    else if isAttrs x then "{${ concatStringsSep "," (mapAttrsToList (k: v: ''"${k}":${toHOCON v}'') x) }}"
-    else if isList x then "[${ concatMapStringsSep "," toHOCON x }]"
-    else builtins.toJSON x;
+  format = pkgs.formats.hocon { };
 
   # We're passing passwords in environment variables that have names generated
   # from an attribute name, which may not be a valid bash identifier.
@@ -38,7 +29,7 @@ let
         hostname = xmppConfig.hostName;
         domain = xmppConfig.domain;
         username = xmppConfig.userName;
-        password = { __hocon_envvar = toVarName name; };
+        password = format.lib.mkSubstitution (toVarName name);
         muc_jids = xmppConfig.mucJids;
         muc_nickname = xmppConfig.mucNickname;
         disable_certificate_verification = xmppConfig.disableCertificateVerification;
@@ -221,7 +212,7 @@ in
         "-Dnet.java.sip.communicator.SC_HOME_DIR_LOCATION" = "/etc/jitsi";
         "-Dnet.java.sip.communicator.SC_HOME_DIR_NAME" = "videobridge";
         "-Djava.util.logging.config.file" = "/etc/jitsi/videobridge/logging.properties";
-        "-Dconfig.file" = pkgs.writeText "jvb.conf" (toHOCON jvbConfig);
+        "-Dconfig.file" = format.generate "jvb.conf" jvbConfig;
         # Mitigate CVE-2021-44228
         "-Dlog4j2.formatMsgNoLookups" = true;
       } // (mapAttrs' (k: v: nameValuePair "-D${k}" v) cfg.extraProperties);
diff --git a/nixos/modules/services/networking/murmur.nix b/nixos/modules/services/networking/murmur.nix
index 0cd80e134ace..5805f332a66f 100644
--- a/nixos/modules/services/networking/murmur.nix
+++ b/nixos/modules/services/networking/murmur.nix
@@ -326,6 +326,29 @@ in
         RuntimeDirectoryMode = "0700";
         User = "murmur";
         Group = "murmur";
+
+        # service hardening
+        AmbientCapabilities = "CAP_NET_BIND_SERVICE";
+        CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateTmp = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectSystem = "full";
+        RestrictAddressFamilies = "~AF_PACKET AF_NETLINK";
+        RestrictNamespaces = true;
+        RestrictSUIDSGID = true;
+        RestrictRealtime = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = "@system-service";
       };
     };
 
diff --git a/nixos/modules/services/networking/nftables.nix b/nixos/modules/services/networking/nftables.nix
index 424d005dc0b5..2351ebf4b707 100644
--- a/nixos/modules/services/networking/nftables.nix
+++ b/nixos/modules/services/networking/nftables.nix
@@ -185,6 +185,19 @@ in
           can be loaded using "nft -f".  The ruleset is updated atomically.
         '';
     };
+
+    networking.nftables.flattenRulesetFile = mkOption {
+      type = types.bool;
+      default = false;
+      description = lib.mdDoc ''
+        Use `builtins.readFile` rather than `include` to handle {option}`networking.nftables.rulesetFile`. It is useful when you want to apply {option}`networking.nftables.preCheckRuleset` to {option}`networking.nftables.rulesetFile`.
+
+        ::: {.note}
+        It is expected that {option}`networking.nftables.rulesetFile` can be accessed from the build sandbox.
+        :::
+      '';
+    };
+
     networking.nftables.tables = mkOption {
       type = types.attrsOf (types.submodule tableSubmodule);
 
@@ -252,8 +265,10 @@ in
     networking.nftables.flushRuleset = mkDefault (versionOlder config.system.stateVersion "23.11" || (cfg.rulesetFile != null || cfg.ruleset != ""));
     systemd.services.nftables = {
       description = "nftables firewall";
-      before = [ "network-pre.target" ];
-      wants = [ "network-pre.target" ];
+      after = [ "sysinit.target" ];
+      before = [ "network-pre.target" "shutdown.target" ];
+      conflicts = [ "shutdown.target" ];
+      wants = [ "network-pre.target" "sysinit.target" ];
       wantedBy = [ "multi-user.target" ];
       reloadIfChanged = true;
       serviceConfig = let
@@ -293,9 +308,13 @@ in
               }
             '') enabledTables)}
             ${cfg.ruleset}
-            ${lib.optionalString (cfg.rulesetFile != null) ''
-              include "${cfg.rulesetFile}"
-            ''}
+            ${if cfg.rulesetFile != null then
+              if cfg.flattenRulesetFile then
+                builtins.readFile cfg.rulesetFile
+                else ''
+                  include "${cfg.rulesetFile}"
+                ''
+              else ""}
           '';
           checkPhase = lib.optionalString cfg.checkRuleset ''
             cp $out ruleset.conf
@@ -315,6 +334,7 @@ in
         ExecStop = [ deletionsScriptVar cleanupDeletionsScript ];
         StateDirectory = "nftables";
       };
+      unitConfig.DefaultDependencies = false;
     };
   };
 }
diff --git a/nixos/modules/services/networking/pyload.nix b/nixos/modules/services/networking/pyload.nix
index f2b85499d4dd..93f8dd7d731a 100644
--- a/nixos/modules/services/networking/pyload.nix
+++ b/nixos/modules/services/networking/pyload.nix
@@ -34,6 +34,18 @@ in
         description = "Directory to store downloads.";
       };
 
+      user = mkOption {
+        type = types.str;
+        default = "pyload";
+        description = "User under which pyLoad runs, and which owns the download directory.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "pyload";
+        description = "Group under which pyLoad runs, and which owns the download directory.";
+      };
+
       credentialsFile = mkOption {
         type = with types; nullOr path;
         default = null;
@@ -52,7 +64,7 @@ in
 
   config = lib.mkIf cfg.enable {
     systemd.tmpfiles.settings.pyload = {
-      ${cfg.downloadDirectory}.d = { };
+      ${cfg.downloadDirectory}.d = { inherit (cfg) user group; };
     };
 
     systemd.services.pyload = {
@@ -80,9 +92,8 @@ in
           cfg.downloadDirectory
         ];
 
-        User = "pyload";
-        Group = "pyload";
-        DynamicUser = true;
+        User = cfg.user;
+        Group = cfg.group;
 
         EnvironmentFile = lib.optional (cfg.credentialsFile != null) cfg.credentialsFile;
 
@@ -143,5 +154,13 @@ in
         ];
       };
     };
+
+    users.users.pyload = lib.mkIf (cfg.user == "pyload") {
+      isSystemUser = true;
+      group = cfg.group;
+      home = stateDir;
+    };
+
+    users.groups.pyload = lib.mkIf (cfg.group == "pyload") { };
   };
 }
diff --git a/nixos/modules/services/web-apps/nextcloud.nix b/nixos/modules/services/web-apps/nextcloud.nix
index 8669f84b1cbb..08f90dcf59d8 100644
--- a/nixos/modules/services/web-apps/nextcloud.nix
+++ b/nixos/modules/services/web-apps/nextcloud.nix
@@ -873,9 +873,11 @@ in {
     { systemd.timers.nextcloud-cron = {
         wantedBy = [ "timers.target" ];
         after = [ "nextcloud-setup.service" ];
-        timerConfig.OnBootSec = "5m";
-        timerConfig.OnUnitActiveSec = "5m";
-        timerConfig.Unit = "nextcloud-cron.service";
+        timerConfig = {
+          OnBootSec = "5m";
+          OnUnitActiveSec = "5m";
+          Unit = "nextcloud-cron.service";
+        };
       };
 
       systemd.tmpfiles.rules = map (dir: "d ${dir} 0750 nextcloud nextcloud - -") [
@@ -992,15 +994,21 @@ in {
         nextcloud-cron = {
           after = [ "nextcloud-setup.service" ];
           environment.NEXTCLOUD_CONFIG_DIR = "${datadir}/config";
-          serviceConfig.Type = "oneshot";
-          serviceConfig.User = "nextcloud";
-          serviceConfig.ExecStart = "${phpPackage}/bin/php -f ${webroot}/cron.php";
+          serviceConfig = {
+            Type = "oneshot";
+            User = "nextcloud";
+            ExecCondition = "${lib.getExe phpPackage} -f ${webroot}/occ status -e";
+            ExecStart = "${lib.getExe phpPackage} -f ${webroot}/cron.php";
+            KillMode = "process";
+          };
         };
         nextcloud-update-plugins = mkIf cfg.autoUpdateApps.enable {
           after = [ "nextcloud-setup.service" ];
-          serviceConfig.Type = "oneshot";
-          serviceConfig.ExecStart = "${occ}/bin/nextcloud-occ app:update --all";
-          serviceConfig.User = "nextcloud";
+          serviceConfig = {
+            Type = "oneshot";
+            ExecStart = "${occ}/bin/nextcloud-occ app:update --all";
+            User = "nextcloud";
+          };
           startAt = cfg.autoUpdateApps.startAt;
         };
       };
diff --git a/nixos/modules/services/web-apps/restya-board.nix b/nixos/modules/services/web-apps/restya-board.nix
deleted file mode 100644
index 959bcbc5c9f1..000000000000
--- a/nixos/modules/services/web-apps/restya-board.nix
+++ /dev/null
@@ -1,380 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-# TODO: are these php-packages needed?
-#imagick
-#php-geoip -> php.ini: extension = geoip.so
-#expat
-
-let
-  cfg = config.services.restya-board;
-  fpm = config.services.phpfpm.pools.${poolName};
-
-  runDir = "/run/restya-board";
-
-  poolName = "restya-board";
-
-in
-
-{
-
-  ###### interface
-
-  options = {
-
-    services.restya-board = {
-
-      enable = mkEnableOption (lib.mdDoc "restya-board");
-
-      dataDir = mkOption {
-        type = types.path;
-        default = "/var/lib/restya-board";
-        description = lib.mdDoc ''
-          Data of the application.
-        '';
-      };
-
-      user = mkOption {
-        type = types.str;
-        default = "restya-board";
-        description = lib.mdDoc ''
-          User account under which the web-application runs.
-        '';
-      };
-
-      group = mkOption {
-        type = types.str;
-        default = "nginx";
-        description = lib.mdDoc ''
-          Group account under which the web-application runs.
-        '';
-      };
-
-      virtualHost = {
-        serverName = mkOption {
-          type = types.str;
-          default = "restya.board";
-          description = lib.mdDoc ''
-            Name of the nginx virtualhost to use.
-          '';
-        };
-
-        listenHost = mkOption {
-          type = types.str;
-          default = "localhost";
-          description = lib.mdDoc ''
-            Listen address for the virtualhost to use.
-          '';
-        };
-
-        listenPort = mkOption {
-          type = types.port;
-          default = 3000;
-          description = lib.mdDoc ''
-            Listen port for the virtualhost to use.
-          '';
-        };
-      };
-
-      database = {
-        host = mkOption {
-          type = types.nullOr types.str;
-          default = null;
-          description = lib.mdDoc ''
-            Host of the database. Leave 'null' to use a local PostgreSQL database.
-            A local PostgreSQL database is initialized automatically.
-          '';
-        };
-
-        port = mkOption {
-          type = types.nullOr types.int;
-          default = 5432;
-          description = lib.mdDoc ''
-            The database's port.
-          '';
-        };
-
-        name = mkOption {
-          type = types.str;
-          default = "restya_board";
-          description = lib.mdDoc ''
-            Name of the database. The database must exist.
-          '';
-        };
-
-        user = mkOption {
-          type = types.str;
-          default = "restya_board";
-          description = lib.mdDoc ''
-            The database user. The user must exist and have access to
-            the specified database.
-          '';
-        };
-
-        passwordFile = mkOption {
-          type = types.nullOr types.path;
-          default = null;
-          description = lib.mdDoc ''
-            The database user's password. 'null' if no password is set.
-          '';
-        };
-      };
-
-      email = {
-        server = mkOption {
-          type = types.nullOr types.str;
-          default = null;
-          example = "localhost";
-          description = lib.mdDoc ''
-            Hostname to send outgoing mail. Null to use the system MTA.
-          '';
-        };
-
-        port = mkOption {
-          type = types.port;
-          default = 25;
-          description = lib.mdDoc ''
-            Port used to connect to SMTP server.
-          '';
-        };
-
-        login = mkOption {
-          type = types.str;
-          default = "";
-          description = lib.mdDoc ''
-            SMTP authentication login used when sending outgoing mail.
-          '';
-        };
-
-        password = mkOption {
-          type = types.str;
-          default = "";
-          description = lib.mdDoc ''
-            SMTP authentication password used when sending outgoing mail.
-
-            ATTENTION: The password is stored world-readable in the nix-store!
-          '';
-        };
-      };
-
-      timezone = mkOption {
-        type = types.lines;
-        default = "GMT";
-        description = lib.mdDoc ''
-          Timezone the web-app runs in.
-        '';
-      };
-
-    };
-
-  };
-
-
-  ###### implementation
-
-  config = mkIf cfg.enable {
-
-    services.phpfpm.pools = {
-      ${poolName} = {
-        inherit (cfg) user group;
-
-        phpOptions = ''
-          date.timezone = "CET"
-
-          ${optionalString (cfg.email.server != null) ''
-            SMTP = ${cfg.email.server}
-            smtp_port = ${toString cfg.email.port}
-            auth_username = ${cfg.email.login}
-            auth_password = ${cfg.email.password}
-          ''}
-        '';
-        settings = mapAttrs (name: mkDefault) {
-          "listen.owner" = "nginx";
-          "listen.group" = "nginx";
-          "listen.mode" = "0600";
-          "pm" = "dynamic";
-          "pm.max_children" = 75;
-          "pm.start_servers" = 10;
-          "pm.min_spare_servers" = 5;
-          "pm.max_spare_servers" = 20;
-          "pm.max_requests" = 500;
-          "catch_workers_output" = 1;
-        };
-      };
-    };
-
-    services.nginx.enable = true;
-    services.nginx.virtualHosts.${cfg.virtualHost.serverName} = {
-      listen = [ { addr = cfg.virtualHost.listenHost; port = cfg.virtualHost.listenPort; } ];
-      serverName = cfg.virtualHost.serverName;
-      root = runDir;
-      extraConfig = ''
-        index index.html index.php;
-
-        gzip on;
-
-        gzip_comp_level 6;
-        gzip_min_length  1100;
-        gzip_buffers 16 8k;
-        gzip_proxied any;
-        gzip_types text/plain application/xml text/css text/js text/xml application/x-javascript text/javascript application/json application/xml+rss;
-
-        client_max_body_size 300M;
-
-        rewrite ^/oauth/authorize$ /server/php/authorize.php last;
-        rewrite ^/oauth_callback/([a-zA-Z0-9_\.]*)/([a-zA-Z0-9_\.]*)$ /server/php/oauth_callback.php?plugin=$1&code=$2 last;
-        rewrite ^/download/([0-9]*)/([a-zA-Z0-9_\.]*)$ /server/php/download.php?id=$1&hash=$2 last;
-        rewrite ^/ical/([0-9]*)/([0-9]*)/([a-z0-9]*).ics$ /server/php/ical.php?board_id=$1&user_id=$2&hash=$3 last;
-        rewrite ^/api/(.*)$ /server/php/R/r.php?_url=$1&$args last;
-        rewrite ^/api_explorer/api-docs/$ /client/api_explorer/api-docs/index.php last;
-      '';
-
-      locations."/".root = "${runDir}/client";
-
-      locations."~ \\.php$" = {
-        tryFiles = "$uri =404";
-        extraConfig = ''
-          include ${config.services.nginx.package}/conf/fastcgi_params;
-          fastcgi_pass    unix:${fpm.socket};
-          fastcgi_index   index.php;
-          fastcgi_param   SCRIPT_FILENAME $document_root$fastcgi_script_name;
-          fastcgi_param   PHP_VALUE "upload_max_filesize=9G \n post_max_size=9G \n max_execution_time=200 \n max_input_time=200 \n memory_limit=256M";
-        '';
-      };
-
-      locations."~* \\.(css|js|less|html|ttf|woff|jpg|jpeg|gif|png|bmp|ico)" = {
-        root = "${runDir}/client";
-        extraConfig = ''
-          if (-f $request_filename) {
-                  break;
-          }
-          rewrite ^/img/([a-zA-Z_]*)/([a-zA-Z_]*)/([a-zA-Z0-9_\.]*)$ /server/php/image.php?size=$1&model=$2&filename=$3 last;
-          add_header        Cache-Control public;
-          add_header        Cache-Control must-revalidate;
-          expires           7d;
-        '';
-      };
-    };
-
-    systemd.services.restya-board-init = {
-      description = "Restya board initialization";
-      serviceConfig.Type = "oneshot";
-      serviceConfig.RemainAfterExit = true;
-
-      wantedBy = [ "multi-user.target" ];
-      requires = lib.optional (cfg.database.host != null) "postgresql.service";
-      after = [ "network.target" ] ++ (lib.optional (cfg.database.host != null) "postgresql.service");
-
-      script = ''
-        rm -rf "${runDir}"
-        mkdir -m 750 -p "${runDir}"
-        cp -r "${pkgs.restya-board}/"* "${runDir}"
-        sed -i "s/@restya.com/@${cfg.virtualHost.serverName}/g" "${runDir}/sql/restyaboard_with_empty_data.sql"
-        rm -rf "${runDir}/media"
-        rm -rf "${runDir}/client/img"
-        chmod -R 0750 "${runDir}"
-
-        sed -i "s@^php@${config.services.phpfpm.phpPackage}/bin/php@" "${runDir}/server/php/shell/"*.sh
-
-        ${if (cfg.database.host == null) then ''
-          sed -i "s/^.*'R_DB_HOST'.*$/define('R_DB_HOST', 'localhost');/g" "${runDir}/server/php/config.inc.php"
-          sed -i "s/^.*'R_DB_PASSWORD'.*$/define('R_DB_PASSWORD', 'restya');/g" "${runDir}/server/php/config.inc.php"
-        '' else ''
-          sed -i "s/^.*'R_DB_HOST'.*$/define('R_DB_HOST', '${cfg.database.host}');/g" "${runDir}/server/php/config.inc.php"
-          sed -i "s/^.*'R_DB_PASSWORD'.*$/define('R_DB_PASSWORD', ${if cfg.database.passwordFile == null then "''" else "'$(cat ${cfg.database.passwordFile})');/g"}" "${runDir}/server/php/config.inc.php"
-        ''}
-        sed -i "s/^.*'R_DB_PORT'.*$/define('R_DB_PORT', '${toString cfg.database.port}');/g" "${runDir}/server/php/config.inc.php"
-        sed -i "s/^.*'R_DB_NAME'.*$/define('R_DB_NAME', '${cfg.database.name}');/g" "${runDir}/server/php/config.inc.php"
-        sed -i "s/^.*'R_DB_USER'.*$/define('R_DB_USER', '${cfg.database.user}');/g" "${runDir}/server/php/config.inc.php"
-
-        chmod 0400 "${runDir}/server/php/config.inc.php"
-
-        ln -sf "${cfg.dataDir}/media" "${runDir}/media"
-        ln -sf "${cfg.dataDir}/client/img" "${runDir}/client/img"
-
-        chmod g+w "${runDir}/tmp/cache"
-        chown -R "${cfg.user}":"${cfg.group}" "${runDir}"
-
-
-        mkdir -m 0750 -p "${cfg.dataDir}"
-        mkdir -m 0750 -p "${cfg.dataDir}/media"
-        mkdir -m 0750 -p "${cfg.dataDir}/client/img"
-        cp -r "${pkgs.restya-board}/media/"* "${cfg.dataDir}/media"
-        cp -r "${pkgs.restya-board}/client/img/"* "${cfg.dataDir}/client/img"
-        chown "${cfg.user}":"${cfg.group}" "${cfg.dataDir}"
-        chown -R "${cfg.user}":"${cfg.group}" "${cfg.dataDir}/media"
-        chown -R "${cfg.user}":"${cfg.group}" "${cfg.dataDir}/client/img"
-
-        ${optionalString (cfg.database.host == null) ''
-          if ! [ -e "${cfg.dataDir}/.db-initialized" ]; then
-            ${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} \
-              ${config.services.postgresql.package}/bin/psql -U ${config.services.postgresql.superUser} \
-              -c "CREATE USER ${cfg.database.user} WITH ENCRYPTED PASSWORD 'restya'"
-
-            ${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} \
-              ${config.services.postgresql.package}/bin/psql -U ${config.services.postgresql.superUser} \
-              -c "CREATE DATABASE ${cfg.database.name} OWNER ${cfg.database.user} ENCODING 'UTF8' TEMPLATE template0"
-
-            ${pkgs.sudo}/bin/sudo -u ${cfg.user} \
-              ${config.services.postgresql.package}/bin/psql -U ${cfg.database.user} \
-              -d ${cfg.database.name} -f "${runDir}/sql/restyaboard_with_empty_data.sql"
-
-            touch "${cfg.dataDir}/.db-initialized"
-          fi
-        ''}
-      '';
-    };
-
-    systemd.timers.restya-board = {
-      description = "restya-board scripts for e.g. email notification";
-      wantedBy = [ "timers.target" ];
-      after = [ "restya-board-init.service" ];
-      requires = [ "restya-board-init.service" ];
-      timerConfig = {
-        OnUnitInactiveSec = "60s";
-        Unit = "restya-board-timers.service";
-      };
-    };
-
-    systemd.services.restya-board-timers = {
-      description = "restya-board scripts for e.g. email notification";
-      serviceConfig.Type = "oneshot";
-      serviceConfig.User = cfg.user;
-
-      after = [ "restya-board-init.service" ];
-      requires = [ "restya-board-init.service" ];
-
-      script = ''
-        /bin/sh ${runDir}/server/php/shell/instant_email_notification.sh 2> /dev/null || true
-        /bin/sh ${runDir}/server/php/shell/periodic_email_notification.sh 2> /dev/null || true
-        /bin/sh ${runDir}/server/php/shell/imap.sh 2> /dev/null || true
-        /bin/sh ${runDir}/server/php/shell/webhook.sh 2> /dev/null || true
-        /bin/sh ${runDir}/server/php/shell/card_due_notification.sh 2> /dev/null || true
-      '';
-    };
-
-    users.users.restya-board = {
-      isSystemUser = true;
-      createHome = false;
-      home = runDir;
-      group  = "restya-board";
-    };
-    users.groups.restya-board = {};
-
-    services.postgresql.enable = mkIf (cfg.database.host == null) true;
-
-    services.postgresql.identMap = optionalString (cfg.database.host == null)
-      ''
-        restya-board-users restya-board restya_board
-      '';
-
-    services.postgresql.authentication = optionalString (cfg.database.host == null)
-      ''
-        local restya_board all ident map=restya-board-users
-      '';
-
-  };
-
-}
-
diff --git a/nixos/modules/services/web-apps/suwayomi-server.nix b/nixos/modules/services/web-apps/suwayomi-server.nix
index c4c1540edbee..94dbe6f99356 100644
--- a/nixos/modules/services/web-apps/suwayomi-server.nix
+++ b/nixos/modules/services/web-apps/suwayomi-server.nix
@@ -3,6 +3,8 @@
 let
   cfg = config.services.suwayomi-server;
   inherit (lib) mkOption mdDoc mkEnableOption mkIf types;
+
+  format = pkgs.formats.hocon { };
 in
 {
   options = {
@@ -48,19 +50,7 @@ in
 
       settings = mkOption {
         type = types.submodule {
-          freeformType =
-            let
-              recursiveAttrsType = with types; attrsOf (nullOr (oneOf [
-                str
-                path
-                int
-                float
-                bool
-                (listOf str)
-                (recursiveAttrsType // { description = "instances of this type recursively"; })
-              ]));
-            in
-            recursiveAttrsType;
+          freeformType = format.type;
           options = {
             server = {
               ip = mkOption {
@@ -180,38 +170,7 @@ in
 
     systemd.services.suwayomi-server =
       let
-        flattenConfig = prefix: config:
-          lib.foldl'
-            lib.mergeAttrs
-            { }
-            (lib.attrValues
-              (lib.mapAttrs
-                (k: v:
-                  if !(lib.isAttrs v)
-                  then { "${prefix}${k}" = v; }
-                  else flattenConfig "${prefix}${k}." v
-                )
-                config
-              )
-            );
-
-        #  HOCON is a JSON superset that suwayomi-server use for configuration
-        toHOCON = attr:
-          let
-            attrType = builtins.typeOf attr;
-          in
-          if builtins.elem attrType [ "string" "path" "int" "float" ]
-          then ''"${toString attr}"''
-          else if attrType == "bool"
-          then lib.boolToString attr
-          else if attrType == "list"
-          then "[\n${lib.concatMapStringsSep ",\n" toHOCON attr}\n]"
-          else # attrs, lambda, null
-            throw ''
-              [suwayomi-server]: invalid config value type '${attrType}'.
-            '';
-
-        configFile = pkgs.writeText "server.conf" (lib.pipe cfg.settings [
+        configFile = format.generate "server.conf" (lib.pipe cfg.settings [
           (settings: lib.recursiveUpdate settings {
             server.basicAuthPasswordFile = null;
             server.basicAuthPassword =
@@ -219,12 +178,8 @@ in
               then "$TACHIDESK_SERVER_BASIC_AUTH_PASSWORD"
               else null;
           })
-          (flattenConfig "")
-          (lib.filterAttrs (_: x: x != null))
-          (lib.mapAttrsToList (name: value: ''${name} = ${toHOCON value}''))
-          lib.concatLines
+          (lib.filterAttrsRecursive (_: x: x != null))
         ]);
-
       in
       {
         description = "A free and open source manga reader server that runs extensions built for Tachiyomi.";
diff --git a/nixos/modules/services/x11/desktop-managers/plasma5.nix b/nixos/modules/services/x11/desktop-managers/plasma5.nix
index 677465f55c47..0eb492ce4684 100644
--- a/nixos/modules/services/x11/desktop-managers/plasma5.nix
+++ b/nixos/modules/services/x11/desktop-managers/plasma5.nix
@@ -384,6 +384,7 @@ in
       system.userActivationScripts.plasmaSetup = activationScript;
 
       programs.firefox.nativeMessagingHosts.packages = [ pkgs.plasma5Packages.plasma-browser-integration ];
+      programs.chromium.enablePlasmaBrowserIntegration = true;
     })
 
     (mkIf (cfg.kwinrc != {}) {
diff --git a/nixos/modules/system/boot/uki.nix b/nixos/modules/system/boot/uki.nix
index 63c4e0c0e391..63a7cbc5967b 100644
--- a/nixos/modules/system/boot/uki.nix
+++ b/nixos/modules/system/boot/uki.nix
@@ -51,16 +51,16 @@ in
     else
       "nixos");
 
-    boot.uki.settings = lib.mkOptionDefault {
+    boot.uki.settings = {
       UKI = {
-        Linux = "${config.boot.kernelPackages.kernel}/${config.system.boot.loader.kernelFile}";
-        Initrd = "${config.system.build.initialRamdisk}/${config.system.boot.loader.initrdFile}";
-        Cmdline = "init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams}";
-        Stub = "${pkgs.systemd}/lib/systemd/boot/efi/linux${efiArch}.efi.stub";
-        Uname = "${config.boot.kernelPackages.kernel.modDirVersion}";
-        OSRelease = "@${config.system.build.etc}/etc/os-release";
+        Linux = lib.mkOptionDefault "${config.boot.kernelPackages.kernel}/${config.system.boot.loader.kernelFile}";
+        Initrd = lib.mkOptionDefault "${config.system.build.initialRamdisk}/${config.system.boot.loader.initrdFile}";
+        Cmdline = lib.mkOptionDefault "init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams}";
+        Stub = lib.mkOptionDefault "${pkgs.systemd}/lib/systemd/boot/efi/linux${efiArch}.efi.stub";
+        Uname = lib.mkOptionDefault "${config.boot.kernelPackages.kernel.modDirVersion}";
+        OSRelease = lib.mkOptionDefault "@${config.system.build.etc}/etc/os-release";
         # This is needed for cross compiling.
-        EFIArch = efiArch;
+        EFIArch = lib.mkOptionDefault efiArch;
       };
     };
 
diff --git a/nixos/modules/tasks/filesystems/overlayfs.nix b/nixos/modules/tasks/filesystems/overlayfs.nix
new file mode 100644
index 000000000000..e71ef9ba62e9
--- /dev/null
+++ b/nixos/modules/tasks/filesystems/overlayfs.nix
@@ -0,0 +1,144 @@
+{ config, lib, pkgs, utils, ... }:
+
+let
+  # The scripted initrd contains some magic to add the prefix to the
+  # paths just in time, so we don't add it here.
+  sysrootPrefix = fs:
+    if config.boot.initrd.systemd.enable && (utils.fsNeededForBoot fs) then
+      "/sysroot"
+    else
+      "";
+
+  # Returns a service that creates the required directories before the mount is
+  # created.
+  preMountService = _name: fs:
+    let
+      prefix = sysrootPrefix fs;
+
+      escapedMountpoint = utils.escapeSystemdPath (prefix + fs.mountPoint);
+      mountUnit = "${escapedMountpoint}.mount";
+
+      upperdir = prefix + fs.overlay.upperdir;
+      workdir = prefix + fs.overlay.workdir;
+    in
+    lib.mkIf (fs.overlay.upperdir != null)
+      {
+        "rw-${escapedMountpoint}" = {
+          requiredBy = [ mountUnit ];
+          before = [ mountUnit ];
+          unitConfig = {
+            DefaultDependencies = false;
+            RequiresMountsFor = "${upperdir} ${workdir}";
+          };
+          serviceConfig = {
+            Type = "oneshot";
+            ExecStart = "${pkgs.coreutils}/bin/mkdir -p -m 0755 ${upperdir} ${workdir}";
+          };
+        };
+      };
+
+  overlayOpts = { config, ... }: {
+
+    options.overlay = {
+
+      lowerdir = lib.mkOption {
+        type = with lib.types; nullOr (nonEmptyListOf (either str pathInStore));
+        default = null;
+        description = lib.mdDoc ''
+          The list of path(s) to the lowerdir(s).
+
+          To create a writable overlay, you MUST provide an upperdir and a
+          workdir.
+
+          You can create a read-only overlay when you provide multiple (at
+          least 2!) lowerdirs and neither an upperdir nor a workdir.
+        '';
+      };
+
+      upperdir = lib.mkOption {
+        type = lib.types.nullOr lib.types.str;
+        default = null;
+        description = lib.mdDoc ''
+          The path to the upperdir.
+
+          If this is null, a read-only overlay is created using the lowerdir.
+
+          If you set this to some value you MUST also set `workdir`.
+        '';
+      };
+
+      workdir = lib.mkOption {
+        type = lib.types.nullOr lib.types.str;
+        default = null;
+        description = lib.mdDoc ''
+          The path to the workdir.
+
+          This MUST be set if you set `upperdir`.
+        '';
+      };
+
+    };
+
+    config = lib.mkIf (config.overlay.lowerdir != null) {
+      fsType = "overlay";
+      device = lib.mkDefault "overlay";
+
+      options =
+        let
+          prefix = sysrootPrefix config;
+
+          lowerdir = map (s: prefix + s) config.overlay.lowerdir;
+          upperdir = prefix + config.overlay.upperdir;
+          workdir = prefix + config.overlay.workdir;
+        in
+        [
+          "lowerdir=${lib.concatStringsSep ":" lowerdir}"
+        ] ++ lib.optionals (config.overlay.upperdir != null) [
+          "upperdir=${upperdir}"
+          "workdir=${workdir}"
+        ] ++ (map (s: "x-systemd.requires-mounts-for=${s}") lowerdir);
+    };
+
+  };
+in
+
+{
+
+  options = {
+
+    # Merge the overlay options into the fileSystems option.
+    fileSystems = lib.mkOption {
+      type = lib.types.attrsOf (lib.types.submodule [ overlayOpts ]);
+    };
+
+  };
+
+  config =
+    let
+      overlayFileSystems = lib.filterAttrs (_name: fs: (fs.overlay.lowerdir != null)) config.fileSystems;
+      initrdFileSystems = lib.filterAttrs (_name: utils.fsNeededForBoot) overlayFileSystems;
+      userspaceFileSystems = lib.filterAttrs (_name: fs: (!utils.fsNeededForBoot fs)) overlayFileSystems;
+    in
+    {
+
+      boot.initrd.availableKernelModules = lib.mkIf (initrdFileSystems != { }) [ "overlay" ];
+
+      assertions = lib.concatLists (lib.mapAttrsToList
+        (_name: fs: [
+          {
+            assertion = (fs.overlay.upperdir == null) == (fs.overlay.workdir == null);
+            message = "You cannot define a `lowerdir` without a `workdir` and vice versa for mount point: ${fs.mountPoint}";
+          }
+          {
+            assertion = (fs.overlay.lowerdir != null && fs.overlay.upperdir == null) -> (lib.length fs.overlay.lowerdir) >= 2;
+            message = "A read-only overlay (without an `upperdir`) requires at least 2 `lowerdir`s: ${fs.mountPoint}";
+          }
+        ])
+        config.fileSystems);
+
+      boot.initrd.systemd.services = lib.mkMerge (lib.mapAttrsToList preMountService initrdFileSystems);
+      systemd.services = lib.mkMerge (lib.mapAttrsToList preMountService userspaceFileSystems);
+
+    };
+
+}
diff --git a/nixos/modules/virtualisation/amazon-image.nix b/nixos/modules/virtualisation/amazon-image.nix
index f0d9b95f81f6..c7fe1bed5159 100644
--- a/nixos/modules/virtualisation/amazon-image.nix
+++ b/nixos/modules/virtualisation/amazon-image.nix
@@ -103,4 +103,5 @@ in
     # (e.g. it depends on GTK).
     services.udisks2.enable = false;
   };
+  meta.maintainers = with maintainers; [ arianvp ];
 }
diff --git a/nixos/modules/virtualisation/amazon-init.nix b/nixos/modules/virtualisation/amazon-init.nix
index 8b98f2e32dd5..8475097df07c 100644
--- a/nixos/modules/virtualisation/amazon-init.nix
+++ b/nixos/modules/virtualisation/amazon-init.nix
@@ -84,4 +84,5 @@ in {
       };
     };
   };
+  meta.maintainers = with maintainers; [ arianvp ];
 }
diff --git a/nixos/modules/virtualisation/oci-containers.nix b/nixos/modules/virtualisation/oci-containers.nix
index 07ed08ab2f84..b6a7b1154c4a 100644
--- a/nixos/modules/virtualisation/oci-containers.nix
+++ b/nixos/modules/virtualisation/oci-containers.nix
@@ -308,9 +308,10 @@ let
     );
 
     preStop = if cfg.backend == "podman"
-      then "[ $SERVICE_RESULT = success ] || podman stop --ignore --cidfile=/run/podman-${escapedName}.ctr-id"
-      else "[ $SERVICE_RESULT = success ] || ${cfg.backend} stop ${name}";
-    postStop =  if cfg.backend == "podman"
+      then "podman stop --ignore --cidfile=/run/podman-${escapedName}.ctr-id"
+      else "${cfg.backend} stop ${name}";
+
+    postStop = if cfg.backend == "podman"
       then "podman rm -f --ignore --cidfile=/run/podman-${escapedName}.ctr-id"
       else "${cfg.backend} rm -f ${name} || true";
 
diff --git a/nixos/modules/virtualisation/qemu-vm.nix b/nixos/modules/virtualisation/qemu-vm.nix
index 3d7f3ccb62f8..55a214325118 100644
--- a/nixos/modules/virtualisation/qemu-vm.nix
+++ b/nixos/modules/virtualisation/qemu-vm.nix
@@ -701,7 +701,10 @@ in
           type = types.listOf types.str;
           default = [];
           example = [ "-vga std" ];
-          description = lib.mdDoc "Options passed to QEMU.";
+          description = lib.mdDoc ''
+            Options passed to QEMU.
+            See [QEMU User Documentation](https://www.qemu.org/docs/master/system/qemu-manpage) for a complete list.
+          '';
         };
 
       consoles = mkOption {
@@ -732,9 +735,10 @@ in
           description = lib.mdDoc ''
             Networking-related command-line options that should be passed to qemu.
             The default is to use userspace networking (SLiRP).
+            See the [QEMU Wiki on Networking](https://wiki.qemu.org/Documentation/Networking) for details.
 
             If you override this option, be advised to keep
-            ''${QEMU_NET_OPTS:+,$QEMU_NET_OPTS} (as seen in the example)
+            `''${QEMU_NET_OPTS:+,$QEMU_NET_OPTS}` (as seen in the example)
             to keep the default runtime behaviour.
           '';
         };
@@ -809,14 +813,19 @@ in
           defaultText = "!cfg.useBootLoader";
           description =
             lib.mdDoc ''
-              If enabled, the virtual machine will boot directly into the kernel instead of through a bootloader. Other relevant parameters such as the initrd are also passed to QEMU.
+              If enabled, the virtual machine will boot directly into the kernel instead of through a bootloader.
+              Read more about this feature in the [QEMU documentation on Direct Linux Boot](https://qemu-project.gitlab.io/qemu/system/linuxboot.html)
 
+              This is enabled by default.
               If you want to test netboot, consider disabling this option.
+              Enable a bootloader with {option}`virtualisation.useBootLoader` if you need.
 
-              This will not boot / reboot correctly into a system that has switched to a different configuration on disk.
+              Relevant parameters such as those set in `boot.initrd` and `boot.kernelParams` are also passed to QEMU.
+              Additional parameters can be supplied on invocation through the environment variable `$QEMU_KERNEL_PARAMS`.
+              They are added to the `-append` option, see [QEMU User Documentation](https://www.qemu.org/docs/master/system/qemu-manpage) for details
+              For example, to let QEMU use the parent terminal as the serial console, set `QEMU_KERNEL_PARAMS="console=ttyS0"`.
 
-              This is enabled by default if you don't enable bootloaders, but you can still enable a bootloader if you need.
-              Read more about this feature: <https://qemu-project.gitlab.io/qemu/system/linuxboot.html>.
+              This will not (re-)boot correctly into a system that has switched to a different configuration on disk.
             '';
         };
       initrd =
@@ -846,6 +855,8 @@ in
 
             If disabled, the kernel and initrd are directly booted,
             forgoing any bootloader.
+
+            Check the documentation on {option}`virtualisation.directBoot.enable` for details.
           '';
       };
 
@@ -1066,10 +1077,18 @@ in
         ''}
       '';
 
-    systemd.tmpfiles.rules = lib.mkIf config.boot.initrd.systemd.enable [
-      "f /etc/NIXOS 0644 root root -"
-      "d /boot 0644 root root -"
-    ];
+    systemd.tmpfiles.settings."10-qemu-vm" = lib.mkIf config.boot.initrd.systemd.enable {
+      "/etc/NIXOS".f = {
+        mode = "0644";
+        user = "root";
+        group = "root";
+      };
+      "${config.boot.loader.efi.efiSysMountPoint}".d = {
+        mode = "0644";
+        user = "root";
+        group = "root";
+      };
+    };
 
     # After booting, register the closure of the paths in
     # `virtualisation.additionalPaths' in the Nix database in the VM.  This
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index c2114098fe05..31af6ec64214 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -301,6 +301,7 @@ in {
   fenics = handleTest ./fenics.nix {};
   ferm = handleTest ./ferm.nix {};
   ferretdb = handleTest ./ferretdb.nix {};
+  filesystems-overlayfs = runTest ./filesystems-overlayfs.nix;
   firefox = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox; };
   firefox-beta = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox-beta; };
   firefox-devedition = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox-devedition; };
diff --git a/nixos/tests/filesystems-overlayfs.nix b/nixos/tests/filesystems-overlayfs.nix
new file mode 100644
index 000000000000..d7cbf640abe4
--- /dev/null
+++ b/nixos/tests/filesystems-overlayfs.nix
@@ -0,0 +1,89 @@
+{ lib, pkgs, ... }:
+
+let
+  initrdLowerdir = pkgs.runCommand "initrd-lowerdir" { } ''
+    mkdir -p $out
+    echo "initrd" > $out/initrd.txt
+  '';
+  initrdLowerdir2 = pkgs.runCommand "initrd-lowerdir-2" { } ''
+    mkdir -p $out
+    echo "initrd2" > $out/initrd2.txt
+  '';
+  userspaceLowerdir = pkgs.runCommand "userspace-lowerdir" { } ''
+    mkdir -p $out
+    echo "userspace" > $out/userspace.txt
+  '';
+  userspaceLowerdir2 = pkgs.runCommand "userspace-lowerdir-2" { } ''
+    mkdir -p $out
+    echo "userspace2" > $out/userspace2.txt
+  '';
+in
+{
+
+  name = "writable-overlays";
+
+  meta.maintainers = with lib.maintainers; [ nikstur ];
+
+  nodes.machine = { config, pkgs, ... }: {
+    boot.initrd.systemd.enable = true;
+    boot.initrd.availableKernelModules = [ "overlay" ];
+
+    virtualisation.fileSystems = {
+      "/initrd-overlay" = {
+        overlay = {
+          lowerdir = [ initrdLowerdir ];
+          upperdir = "/.rw-initrd-overlay/upper";
+          workdir = "/.rw-initrd-overlay/work";
+        };
+        neededForBoot = true;
+      };
+      "/userspace-overlay" = {
+        overlay = {
+          lowerdir = [ userspaceLowerdir ];
+          upperdir = "/.rw-userspace-overlay/upper";
+          workdir = "/.rw-userspace-overlay/work";
+        };
+      };
+      "/ro-initrd-overlay" = {
+        overlay.lowerdir = [
+          initrdLowerdir
+          initrdLowerdir2
+        ];
+        neededForBoot = true;
+      };
+      "/ro-userspace-overlay" = {
+        overlay.lowerdir = [
+          userspaceLowerdir
+          userspaceLowerdir2
+        ];
+      };
+    };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("default.target")
+
+    with subtest("Initrd overlay"):
+      machine.wait_for_file("/initrd-overlay/initrd.txt", 5)
+      machine.succeed("touch /initrd-overlay/writable.txt")
+      machine.succeed("findmnt --kernel --types overlay /initrd-overlay")
+
+    with subtest("Userspace overlay"):
+      machine.wait_for_file("/userspace-overlay/userspace.txt", 5)
+      machine.succeed("touch /userspace-overlay/writable.txt")
+      machine.succeed("findmnt --kernel --types overlay /userspace-overlay")
+
+    with subtest("Read only initrd overlay"):
+      machine.wait_for_file("/ro-initrd-overlay/initrd.txt", 5)
+      machine.wait_for_file("/ro-initrd-overlay/initrd2.txt", 5)
+      machine.fail("touch /ro-initrd-overlay/not-writable.txt")
+      machine.succeed("findmnt --kernel --types overlay /ro-initrd-overlay")
+
+    with subtest("Read only userspace overlay"):
+      machine.wait_for_file("/ro-userspace-overlay/userspace.txt", 5)
+      machine.wait_for_file("/ro-userspace-overlay/userspace2.txt", 5)
+      machine.fail("touch /ro-userspace-overlay/not-writable.txt")
+      machine.succeed("findmnt --kernel --types overlay /ro-userspace-overlay")
+  '';
+
+}
diff --git a/nixos/tests/freetube.nix b/nixos/tests/freetube.nix
index f285384b68e0..faa534938227 100644
--- a/nixos/tests/freetube.nix
+++ b/nixos/tests/freetube.nix
@@ -23,6 +23,8 @@ let
       inherit name;
       nodes = { "${name}" = machine; };
       meta.maintainers = with pkgs.lib.maintainers; [ kirillrdy ];
+      # time-out on ofborg
+      meta.broken = pkgs.stdenv.isAarch64;
       enableOCR = true;
 
       testScript = ''
diff --git a/nixos/tests/installed-tests/fwupd.nix b/nixos/tests/installed-tests/fwupd.nix
index c095a50dc836..fe4f443d7004 100644
--- a/nixos/tests/installed-tests/fwupd.nix
+++ b/nixos/tests/installed-tests/fwupd.nix
@@ -1,11 +1,12 @@
-{ pkgs, lib, makeInstalledTest, ... }:
+{ pkgs, makeInstalledTest, ... }:
 
 makeInstalledTest {
   tested = pkgs.fwupd;
 
   testConfig = {
-    services.fwupd.enable = true;
-    services.fwupd.daemonSettings.DisabledPlugins = lib.mkForce [ ]; # don't disable test plugin
-    services.fwupd.enableTestRemote = true;
+    services.fwupd = {
+      enable = true;
+      daemonSettings.TestDevices = true;
+    };
   };
 }
diff --git a/nixos/tests/oci-containers.nix b/nixos/tests/oci-containers.nix
index 205ce623d089..1f8e276204a8 100644
--- a/nixos/tests/oci-containers.nix
+++ b/nixos/tests/oci-containers.nix
@@ -24,6 +24,10 @@ let
             ports = ["8181:80"];
           };
         };
+
+        # Stop systemd from killing remaining processes if ExecStop script
+        # doesn't work, so that proper stopping can be tested.
+        systemd.services."${backend}-nginx".serviceConfig.KillSignal = "SIGCONT";
       };
     };
 
@@ -32,6 +36,7 @@ let
       ${backend}.wait_for_unit("${backend}-nginx.service")
       ${backend}.wait_for_open_port(8181)
       ${backend}.wait_until_succeeds("curl -f http://localhost:8181 | grep Hello")
+      ${backend}.succeed("systemctl stop ${backend}-nginx.service", timeout=10)
     '';
   };