summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/development/activation-script.section.md72
-rw-r--r--nixos/doc/manual/development/development.xml1
-rw-r--r--nixos/doc/manual/development/unit-handling.section.md57
-rw-r--r--nixos/doc/manual/development/what-happens-during-a-system-switch.chapter.md53
-rw-r--r--nixos/doc/manual/from_md/development/activation-script.section.xml150
-rw-r--r--nixos/doc/manual/from_md/development/unit-handling.section.xml119
-rw-r--r--nixos/doc/manual/from_md/development/what-happens-during-a-system-switch.chapter.xml122
-rw-r--r--nixos/doc/manual/from_md/installation/installing-pxe.section.xml4
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-2205.section.xml122
-rw-r--r--nixos/doc/manual/installation/installing-pxe.section.md4
-rw-r--r--nixos/doc/manual/release-notes/rl-2205.section.md41
-rw-r--r--nixos/lib/eval-config.nix14
-rw-r--r--nixos/lib/qemu-common.nix2
-rw-r--r--nixos/lib/systemd-unit-options.nix16
-rw-r--r--nixos/lib/test-driver/test_driver/machine.py13
-rw-r--r--nixos/lib/utils.nix9
-rw-r--r--nixos/modules/config/users-groups.nix36
-rw-r--r--nixos/modules/installer/tools/nixos-generate-config.pl13
-rw-r--r--nixos/modules/installer/tools/tools.nix12
-rw-r--r--nixos/modules/module-list.nix13
-rw-r--r--nixos/modules/programs/calls.nix2
-rw-r--r--nixos/modules/programs/command-not-found/command-not-found.pl40
-rw-r--r--nixos/modules/programs/dconf.nix2
-rw-r--r--nixos/modules/rename.nix7
-rw-r--r--nixos/modules/security/google_oslogin.nix9
-rw-r--r--nixos/modules/security/pam.nix12
-rw-r--r--nixos/modules/services/audio/jmusicbot.nix9
-rw-r--r--nixos/modules/services/cluster/kubernetes/scheduler.nix4
-rw-r--r--nixos/modules/services/continuous-integration/github-runner.nix1
-rw-r--r--nixos/modules/services/desktops/gnome/glib-networking.nix2
-rw-r--r--nixos/modules/services/desktops/gvfs.nix2
-rw-r--r--nixos/modules/services/desktops/pantheon/files.nix13
-rw-r--r--nixos/modules/services/hardware/ddccontrol.nix3
-rw-r--r--nixos/modules/services/hardware/udev.nix3
-rw-r--r--nixos/modules/services/hardware/undervolt.nix4
-rw-r--r--nixos/modules/services/hardware/upower.nix12
-rw-r--r--nixos/modules/services/home-automation/home-assistant.nix (renamed from nixos/modules/services/misc/home-assistant.nix)352
-rw-r--r--nixos/modules/services/matrix/mjolnir.xml4
-rw-r--r--nixos/modules/services/misc/airsonic.nix8
-rw-r--r--nixos/modules/services/misc/autorandr.nix1
-rw-r--r--nixos/modules/services/misc/gitlab.nix43
-rw-r--r--nixos/modules/services/misc/input-remapper.nix29
-rw-r--r--nixos/modules/services/misc/matrix-conduit.nix9
-rw-r--r--nixos/modules/services/misc/mediatomb.nix2
-rw-r--r--nixos/modules/services/misc/nix-daemon.nix2
-rw-r--r--nixos/modules/services/misc/packagekit.nix6
-rw-r--r--nixos/modules/services/misc/paperless-ng.nix22
-rw-r--r--nixos/modules/services/misc/plex.nix18
-rw-r--r--nixos/modules/services/misc/rmfakecloud.nix147
-rw-r--r--nixos/modules/services/network-filesystems/ceph.nix4
-rw-r--r--nixos/modules/services/network-filesystems/ipfs.nix29
-rw-r--r--nixos/modules/services/network-filesystems/moosefs.nix249
-rw-r--r--nixos/modules/services/network-filesystems/rsyncd.nix2
-rw-r--r--nixos/modules/services/networking/blocky.nix40
-rw-r--r--nixos/modules/services/networking/connman.nix4
-rw-r--r--nixos/modules/services/networking/firewall.nix6
-rw-r--r--nixos/modules/services/networking/gogoclient.nix87
-rw-r--r--nixos/modules/services/networking/hylafax/options.nix16
-rw-r--r--nixos/modules/services/networking/networkmanager.nix17
-rw-r--r--nixos/modules/services/networking/squid.nix10
-rw-r--r--nixos/modules/services/networking/syncplay.nix2
-rw-r--r--nixos/modules/services/networking/wg-netmanager.nix42
-rw-r--r--nixos/modules/services/networking/wpa_supplicant.nix10
-rw-r--r--nixos/modules/services/networking/yggdrasil.xml1
-rw-r--r--nixos/modules/services/security/cfssl.nix95
-rw-r--r--nixos/modules/services/security/haveged.nix68
-rw-r--r--nixos/modules/services/security/step-ca.nix4
-rw-r--r--nixos/modules/services/system/cloud-init.nix16
-rw-r--r--nixos/modules/services/system/self-deploy.nix5
-rw-r--r--nixos/modules/services/web-apps/bookstack.nix4
-rw-r--r--nixos/modules/services/web-apps/matomo.nix1
-rw-r--r--nixos/modules/services/web-servers/agate.nix148
-rw-r--r--nixos/modules/services/web-servers/shellinabox.nix122
-rw-r--r--nixos/modules/services/x11/desktop-managers/default.nix2
-rw-r--r--nixos/modules/services/x11/desktop-managers/pantheon.nix10
-rw-r--r--nixos/modules/services/x11/desktop-managers/pantheon.xml4
-rw-r--r--nixos/modules/services/x11/desktop-managers/retroarch.nix40
-rw-r--r--nixos/modules/services/x11/display-managers/gdm.nix3
-rw-r--r--nixos/modules/system/activation/switch-to-configuration.pl203
-rw-r--r--nixos/modules/system/activation/top-level.nix2
-rw-r--r--nixos/modules/system/boot/stage-1.nix3
-rw-r--r--nixos/modules/system/boot/systemd.nix5
-rw-r--r--nixos/modules/system/boot/tmp.nix7
-rw-r--r--nixos/modules/system/etc/etc-activation.nix12
-rw-r--r--nixos/modules/system/etc/etc.nix6
-rw-r--r--nixos/modules/system/etc/test.nix70
-rw-r--r--nixos/modules/tasks/network-interfaces.nix2
-rw-r--r--nixos/modules/testing/test-instrumentation.nix4
-rw-r--r--nixos/modules/virtualisation/amazon-image.nix2
-rw-r--r--nixos/modules/virtualisation/containerd.nix1
-rw-r--r--nixos/modules/virtualisation/docker-rootless.nix6
-rw-r--r--nixos/modules/virtualisation/fetch-instance-ssh-keys.bash36
-rw-r--r--nixos/modules/virtualisation/google-compute-config.nix135
-rw-r--r--nixos/modules/virtualisation/kvmgt.nix2
-rw-r--r--nixos/modules/virtualisation/qemu-vm.nix13
-rw-r--r--nixos/modules/virtualisation/virtualbox-guest.nix2
-rw-r--r--nixos/tests/all-tests.nix6
-rw-r--r--nixos/tests/blocky.nix34
-rw-r--r--nixos/tests/bpf.nix6
-rw-r--r--nixos/tests/cloud-init.nix27
-rw-r--r--nixos/tests/google-oslogin/default.nix4
-rw-r--r--nixos/tests/google-oslogin/server.nix4
-rwxr-xr-x[-rw-r--r--]nixos/tests/google-oslogin/server.py0
-rw-r--r--nixos/tests/haproxy.nix1
-rw-r--r--nixos/tests/home-assistant.nix102
-rw-r--r--nixos/tests/moosefs.nix89
-rw-r--r--nixos/tests/prometheus-exporters.nix2
-rw-r--r--nixos/tests/retroarch.nix49
-rw-r--r--nixos/tests/sourcehut.nix19
-rw-r--r--nixos/tests/ssh-keys.nix2
-rw-r--r--nixos/tests/sudo.nix4
-rw-r--r--nixos/tests/sway.nix29
-rw-r--r--nixos/tests/switch-test.nix283
-rw-r--r--nixos/tests/unifi.nix3
-rw-r--r--nixos/tests/web-servers/agate.nix29
-rw-r--r--nixos/tests/wine.nix2
-rw-r--r--nixos/tests/wpa_supplicant.nix19
117 files changed, 3112 insertions, 795 deletions
diff --git a/nixos/doc/manual/development/activation-script.section.md b/nixos/doc/manual/development/activation-script.section.md
new file mode 100644
index 00000000000..df683662404
--- /dev/null
+++ b/nixos/doc/manual/development/activation-script.section.md
@@ -0,0 +1,72 @@
+# Activation script {#sec-activation-script}
+
+The activation script is a bash script called to activate the new
+configuration which resides in a NixOS system in `$out/activate`. Since its
+contents depend on your system configuration, the contents may differ.
+This chapter explains how the script works in general and some common NixOS
+snippets. Please be aware that the script is executed on every boot and system
+switch, so tasks that can be performed in other places should be performed
+there (for example letting a directory of a service be created by systemd using
+mechanisms like `StateDirectory`, `CacheDirectory`, ... or if that's not
+possible using `preStart` of the service).
+
+Activation scripts are defined as snippets using
+[](#opt-system.activationScripts). They can either be a simple multiline string
+or an attribute set that can depend on other snippets. The builder for the
+activation script will take these dependencies into account and order the
+snippets accordingly. As a simple example:
+
+```nix
+system.activationScripts.my-activation-script = {
+  deps = [ "etc" ];
+  # supportsDryActivation = true;
+  text = ''
+    echo "Hallo i bims"
+  '';
+};
+```
+
+This example creates an activation script snippet that is run after the `etc`
+snippet. The special variable `supportsDryActivation` can be set so the snippet
+is also run when `nixos-rebuild dry-activate` is run. To differentiate between
+real and dry activation, the `$NIXOS_ACTION` environment variable can be
+read which is set to `dry-activate` when a dry activation is done.
+
+An activation script can write to special files instructing
+`switch-to-configuration` to restart/reload units. The script will take these
+requests into account and will incorperate the unit configuration as described
+above. This means that the activation script will "fake" a modified unit file
+and `switch-to-configuration` will act accordingly. By doing so, configuration
+like [systemd.services.\<name\>.restartIfChanged](#opt-systemd.services) is
+respected. Since the activation script is run **after** services are already
+stopped, [systemd.services.\<name\>.stopIfChanged](#opt-systemd.services)
+cannot be taken into account anymore and the unit is always restarted instead
+of being stopped and started afterwards.
+
+The files that can be written to are `/run/nixos/activation-restart-list` and
+`/run/nixos/activation-reload-list` with their respective counterparts for
+dry activation being `/run/nixos/dry-activation-restart-list` and
+`/run/nixos/dry-activation-reload-list`. Those files can contain
+newline-separated lists of unit names where duplicates are being ignored. These
+files are not create automatically and activation scripts must take the
+possiblility into account that they have to create them first.
+
+## NixOS snippets {#sec-activation-script-nixos-snippets}
+
+There are some snippets NixOS enables by default because disabling them would
+most likely break you system. This section lists a few of them and what they
+do:
+
+- `binsh` creates `/bin/sh` which points to the runtime shell
+- `etc` sets up the contents of `/etc`, this includes systemd units and
+  excludes `/etc/passwd`, `/etc/group`, and `/etc/shadow` (which are managed by
+  the `users` snippet)
+- `hostname` sets the system's hostname in the kernel (not in `/etc`)
+- `modprobe` sets the path to the `modprobe` binary for module auto-loading
+- `nix` prepares the nix store and adds a default initial channel
+- `specialfs` is responsible for mounting filesystems like `/proc` and `sys`
+- `users` creates and removes users and groups by managing `/etc/passwd`,
+  `/etc/group` and `/etc/shadow`. This also creates home directories
+- `usrbinenv` creates `/usr/bin/env`
+- `var` creates some directories in `/var` that are not service-specific
+- `wrappers` creates setuid wrappers like `ping` and `sudo`
diff --git a/nixos/doc/manual/development/development.xml b/nixos/doc/manual/development/development.xml
index 0b2ad60a878..21286cdbd2b 100644
--- a/nixos/doc/manual/development/development.xml
+++ b/nixos/doc/manual/development/development.xml
@@ -12,6 +12,7 @@
  <xi:include href="../from_md/development/sources.chapter.xml" />
  <xi:include href="../from_md/development/writing-modules.chapter.xml" />
  <xi:include href="../from_md/development/building-parts.chapter.xml" />
+ <xi:include href="../from_md/development/what-happens-during-a-system-switch.chapter.xml" />
  <xi:include href="../from_md/development/writing-documentation.chapter.xml" />
  <xi:include href="../from_md/development/building-nixos.chapter.xml" />
  <xi:include href="../from_md/development/nixos-tests.chapter.xml" />
diff --git a/nixos/doc/manual/development/unit-handling.section.md b/nixos/doc/manual/development/unit-handling.section.md
new file mode 100644
index 00000000000..d477f2c860f
--- /dev/null
+++ b/nixos/doc/manual/development/unit-handling.section.md
@@ -0,0 +1,57 @@
+# Unit handling {#sec-unit-handling}
+
+To figure out what units need to be started/stopped/restarted/reloaded, the
+script first checks the current state of the system, similar to what `systemctl
+list-units` shows. For each of the units, the script goes through the following
+checks:
+
+- Is the unit file still in the new system? If not, **stop** the service unless
+  it sets `X-StopOnRemoval` in the `[Unit]` section to `false`.
+
+- Is it a `.target` unit? If so, **start** it unless it sets
+  `RefuseManualStart` in the `[Unit]` section to `true` or `X-OnlyManualStart`
+  in the `[Unit]` section to `true`. Also **stop** the unit again unless it
+  sets `X-StopOnReconfiguration` to `false`.
+
+- Are the contents of the unit files different? They are compared by parsing
+  them and comparing their contents. If they are different but only
+  `X-Reload-Triggers` in the `[Unit]` section is changed, **reload** the unit.
+  The NixOS module system allows setting these triggers with the option
+  [systemd.services.\<name\>.reloadTriggers](#opt-systemd.services). If the
+  unit files differ in any way, the following actions are performed:
+
+  - `.path` and `.slice` units are ignored. There is no need to restart them
+    since changes in their values are applied by systemd when systemd is
+    reloaded.
+
+  - `.mount` units are **reload**ed. These mostly come from the `/etc/fstab`
+    parser.
+
+  - `.socket` units are currently ignored. This is to be fixed at a later
+    point.
+
+  - The rest of the units (mostly `.service` units) are then **reload**ed if
+    `X-ReloadIfChanged` in the `[Service]` section is set to `true` (exposed
+    via [systemd.services.\<name\>.reloadIfChanged](#opt-systemd.services)).
+
+  - If the reload flag is not set, some more flags decide if the unit is
+    skipped. These flags are `X-RestartIfChanged` in the `[Service]` section
+    (exposed via
+    [systemd.services.\<name\>.restartIfChanged](#opt-systemd.services)),
+    `RefuseManualStop` in the `[Unit]` section, and `X-OnlyManualStart` in the
+    `[Unit]` section.
+
+  - The rest of the behavior is decided whether the unit has `X-StopIfChanged`
+    in the `[Service]` section set (exposed via
+    [systemd.services.\<name\>.stopIfChanged](#opt-systemd.services)). This is
+    set to `true` by default and must be explicitly turned off if not wanted.
+    If the flag is enabled, the unit is **stop**ped and then **start**ed. If
+    not, the unit is **restart**ed. The goal of the flag is to make sure that
+    the new unit never runs in the old environment which is still in place
+    before the activation script is run.
+
+  - The last thing that is taken into account is whether the unit is a service
+    and socket-activated. Due to a bug, this is currently only done when
+    `X-StopIfChanged` is set. If the unit is socket-activated, the socket is
+    stopped and started, and the service is stopped and to be started by socket
+    activation.
diff --git a/nixos/doc/manual/development/what-happens-during-a-system-switch.chapter.md b/nixos/doc/manual/development/what-happens-during-a-system-switch.chapter.md
new file mode 100644
index 00000000000..aad82831a3c
--- /dev/null
+++ b/nixos/doc/manual/development/what-happens-during-a-system-switch.chapter.md
@@ -0,0 +1,53 @@
+# What happens during a system switch? {#sec-switching-systems}
+
+Running `nixos-rebuild switch` is one of the more common tasks under NixOS.
+This chapter explains some of the internals of this command to make it simpler
+for new module developers to configure their units correctly and to make it
+easier to understand what is happening and why for curious administrators.
+
+`nixos-rebuild`, like many deployment solutions, calls `switch-to-configuration`
+which resides in a NixOS system at `$out/bin/switch-to-configuration`. The
+script is called with the action that is to be performed like `switch`, `test`,
+`boot`. There is also the `dry-activate` action which does not really perform
+the actions but rather prints what it would do if you called it with `test`.
+This feature can be used to check what service states would be changed if the
+configuration was switched to.
+
+If the action is `switch` or `boot`, the bootloader is updated first so the
+configuration will be the next one to boot. Unless `NIXOS_NO_SYNC` is set to
+`1`, `/nix/store` is synced to disk.
+
+If the action is `switch` or `test`, the currently running system is inspected
+and the actions to switch to the new system are calculated. This process takes
+two data sources into account: `/etc/fstab` and the current systemd status.
+Mounts and swaps are read from `/etc/fstab` and the corresponding actions are
+generated. If a new mount is added, for example, the proper `.mount` unit is
+marked to be started. The current systemd state is inspected, the difference
+between the current system and the desired configuration is calculated and
+actions are generated to get to this state. There are a lot of nuances that can
+be controlled by the units which are explained here.
+
+After calculating what should be done, the actions are carried out. The order
+of actions is always the same:
+- Stop units (`systemctl stop`)
+- Run activation script (`$out/activate`)
+- See if the activation script requested more units to restart
+- Restart systemd if needed (`systemd daemon-reexec`)
+- Forget about the failed state of units (`systemctl reset-failed`)
+- Reload systemd (`systemctl daemon-reload`)
+- Reload systemd user instances (`systemctl --user daemon-reload`)
+- Set up tmpfiles (`systemd-tmpfiles --create`)
+- Reload units (`systemctl reload`)
+- Restart units (`systemctl restart`)
+- Start units (`systemctl start`)
+- Inspect what changed during these actions and print units that failed and
+  that were newly started
+
+Most of these actions are either self-explaining but some of them have to do
+with our units or the activation script. For this reason, these topics are
+explained in the next sections.
+
+```{=docbook}
+<xi:include href="unit-handling.section.xml" />
+<xi:include href="activation-script.section.xml" />
+```
diff --git a/nixos/doc/manual/from_md/development/activation-script.section.xml b/nixos/doc/manual/from_md/development/activation-script.section.xml
new file mode 100644
index 00000000000..0d9e911216e
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/activation-script.section.xml
@@ -0,0 +1,150 @@
+<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-activation-script">
+  <title>Activation script</title>
+  <para>
+    The activation script is a bash script called to activate the new
+    configuration which resides in a NixOS system in
+    <literal>$out/activate</literal>. Since its contents depend on your
+    system configuration, the contents may differ. This chapter explains
+    how the script works in general and some common NixOS snippets.
+    Please be aware that the script is executed on every boot and system
+    switch, so tasks that can be performed in other places should be
+    performed there (for example letting a directory of a service be
+    created by systemd using mechanisms like
+    <literal>StateDirectory</literal>,
+    <literal>CacheDirectory</literal>, … or if that’s not possible using
+    <literal>preStart</literal> of the service).
+  </para>
+  <para>
+    Activation scripts are defined as snippets using
+    <xref linkend="opt-system.activationScripts" />. They can either be
+    a simple multiline string or an attribute set that can depend on
+    other snippets. The builder for the activation script will take
+    these dependencies into account and order the snippets accordingly.
+    As a simple example:
+  </para>
+  <programlisting language="bash">
+system.activationScripts.my-activation-script = {
+  deps = [ &quot;etc&quot; ];
+  # supportsDryActivation = true;
+  text = ''
+    echo &quot;Hallo i bims&quot;
+  '';
+};
+</programlisting>
+  <para>
+    This example creates an activation script snippet that is run after
+    the <literal>etc</literal> snippet. The special variable
+    <literal>supportsDryActivation</literal> can be set so the snippet
+    is also run when <literal>nixos-rebuild dry-activate</literal> is
+    run. To differentiate between real and dry activation, the
+    <literal>$NIXOS_ACTION</literal> environment variable can be read
+    which is set to <literal>dry-activate</literal> when a dry
+    activation is done.
+  </para>
+  <para>
+    An activation script can write to special files instructing
+    <literal>switch-to-configuration</literal> to restart/reload units.
+    The script will take these requests into account and will
+    incorperate the unit configuration as described above. This means
+    that the activation script will <quote>fake</quote> a modified unit
+    file and <literal>switch-to-configuration</literal> will act
+    accordingly. By doing so, configuration like
+    <link linkend="opt-systemd.services">systemd.services.&lt;name&gt;.restartIfChanged</link>
+    is respected. Since the activation script is run
+    <emphasis role="strong">after</emphasis> services are already
+    stopped,
+    <link linkend="opt-systemd.services">systemd.services.&lt;name&gt;.stopIfChanged</link>
+    cannot be taken into account anymore and the unit is always
+    restarted instead of being stopped and started afterwards.
+  </para>
+  <para>
+    The files that can be written to are
+    <literal>/run/nixos/activation-restart-list</literal> and
+    <literal>/run/nixos/activation-reload-list</literal> with their
+    respective counterparts for dry activation being
+    <literal>/run/nixos/dry-activation-restart-list</literal> and
+    <literal>/run/nixos/dry-activation-reload-list</literal>. Those
+    files can contain newline-separated lists of unit names where
+    duplicates are being ignored. These files are not create
+    automatically and activation scripts must take the possiblility into
+    account that they have to create them first.
+  </para>
+  <section xml:id="sec-activation-script-nixos-snippets">
+    <title>NixOS snippets</title>
+    <para>
+      There are some snippets NixOS enables by default because disabling
+      them would most likely break you system. This section lists a few
+      of them and what they do:
+    </para>
+    <itemizedlist spacing="compact">
+      <listitem>
+        <para>
+          <literal>binsh</literal> creates <literal>/bin/sh</literal>
+          which points to the runtime shell
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>etc</literal> sets up the contents of
+          <literal>/etc</literal>, this includes systemd units and
+          excludes <literal>/etc/passwd</literal>,
+          <literal>/etc/group</literal>, and
+          <literal>/etc/shadow</literal> (which are managed by the
+          <literal>users</literal> snippet)
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>hostname</literal> sets the system’s hostname in the
+          kernel (not in <literal>/etc</literal>)
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>modprobe</literal> sets the path to the
+          <literal>modprobe</literal> binary for module auto-loading
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>nix</literal> prepares the nix store and adds a
+          default initial channel
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>specialfs</literal> is responsible for mounting
+          filesystems like <literal>/proc</literal> and
+          <literal>sys</literal>
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>users</literal> creates and removes users and groups
+          by managing <literal>/etc/passwd</literal>,
+          <literal>/etc/group</literal> and
+          <literal>/etc/shadow</literal>. This also creates home
+          directories
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>usrbinenv</literal> creates
+          <literal>/usr/bin/env</literal>
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>var</literal> creates some directories in
+          <literal>/var</literal> that are not service-specific
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>wrappers</literal> creates setuid wrappers like
+          <literal>ping</literal> and <literal>sudo</literal>
+        </para>
+      </listitem>
+    </itemizedlist>
+  </section>
+</section>
diff --git a/nixos/doc/manual/from_md/development/unit-handling.section.xml b/nixos/doc/manual/from_md/development/unit-handling.section.xml
new file mode 100644
index 00000000000..a6a654042f6
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/unit-handling.section.xml
@@ -0,0 +1,119 @@
+<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-unit-handling">
+  <title>Unit handling</title>
+  <para>
+    To figure out what units need to be
+    started/stopped/restarted/reloaded, the script first checks the
+    current state of the system, similar to what
+    <literal>systemctl list-units</literal> shows. For each of the
+    units, the script goes through the following checks:
+  </para>
+  <itemizedlist>
+    <listitem>
+      <para>
+        Is the unit file still in the new system? If not,
+        <emphasis role="strong">stop</emphasis> the service unless it
+        sets <literal>X-StopOnRemoval</literal> in the
+        <literal>[Unit]</literal> section to <literal>false</literal>.
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Is it a <literal>.target</literal> unit? If so,
+        <emphasis role="strong">start</emphasis> it unless it sets
+        <literal>RefuseManualStart</literal> in the
+        <literal>[Unit]</literal> section to <literal>true</literal> or
+        <literal>X-OnlyManualStart</literal> in the
+        <literal>[Unit]</literal> section to <literal>true</literal>.
+        Also <emphasis role="strong">stop</emphasis> the unit again
+        unless it sets <literal>X-StopOnReconfiguration</literal> to
+        <literal>false</literal>.
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Are the contents of the unit files different? They are compared
+        by parsing them and comparing their contents. If they are
+        different but only <literal>X-Reload-Triggers</literal> in the
+        <literal>[Unit]</literal> section is changed,
+        <emphasis role="strong">reload</emphasis> the unit. The NixOS
+        module system allows setting these triggers with the option
+        <link linkend="opt-systemd.services">systemd.services.&lt;name&gt;.reloadTriggers</link>.
+        If the unit files differ in any way, the following actions are
+        performed:
+      </para>
+      <itemizedlist>
+        <listitem>
+          <para>
+            <literal>.path</literal> and <literal>.slice</literal> units
+            are ignored. There is no need to restart them since changes
+            in their values are applied by systemd when systemd is
+            reloaded.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>.mount</literal> units are
+            <emphasis role="strong">reload</emphasis>ed. These mostly
+            come from the <literal>/etc/fstab</literal> parser.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>.socket</literal> units are currently ignored. This
+            is to be fixed at a later point.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            The rest of the units (mostly <literal>.service</literal>
+            units) are then <emphasis role="strong">reload</emphasis>ed
+            if <literal>X-ReloadIfChanged</literal> in the
+            <literal>[Service]</literal> section is set to
+            <literal>true</literal> (exposed via
+            <link linkend="opt-systemd.services">systemd.services.&lt;name&gt;.reloadIfChanged</link>).
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            If the reload flag is not set, some more flags decide if the
+            unit is skipped. These flags are
+            <literal>X-RestartIfChanged</literal> in the
+            <literal>[Service]</literal> section (exposed via
+            <link linkend="opt-systemd.services">systemd.services.&lt;name&gt;.restartIfChanged</link>),
+            <literal>RefuseManualStop</literal> in the
+            <literal>[Unit]</literal> section, and
+            <literal>X-OnlyManualStart</literal> in the
+            <literal>[Unit]</literal> section.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            The rest of the behavior is decided whether the unit has
+            <literal>X-StopIfChanged</literal> in the
+            <literal>[Service]</literal> section set (exposed via
+            <link linkend="opt-systemd.services">systemd.services.&lt;name&gt;.stopIfChanged</link>).
+            This is set to <literal>true</literal> by default and must
+            be explicitly turned off if not wanted. If the flag is
+            enabled, the unit is
+            <emphasis role="strong">stop</emphasis>ped and then
+            <emphasis role="strong">start</emphasis>ed. If not, the unit
+            is <emphasis role="strong">restart</emphasis>ed. The goal of
+            the flag is to make sure that the new unit never runs in the
+            old environment which is still in place before the
+            activation script is run.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            The last thing that is taken into account is whether the
+            unit is a service and socket-activated. Due to a bug, this
+            is currently only done when
+            <literal>X-StopIfChanged</literal> is set. If the unit is
+            socket-activated, the socket is stopped and started, and the
+            service is stopped and to be started by socket activation.
+          </para>
+        </listitem>
+      </itemizedlist>
+    </listitem>
+  </itemizedlist>
+</section>
diff --git a/nixos/doc/manual/from_md/development/what-happens-during-a-system-switch.chapter.xml b/nixos/doc/manual/from_md/development/what-happens-during-a-system-switch.chapter.xml
new file mode 100644
index 00000000000..66ba792ddac
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/what-happens-during-a-system-switch.chapter.xml
@@ -0,0 +1,122 @@
+<chapter xmlns="http://docbook.org/ns/docbook"  xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xi="http://www.w3.org/2001/XInclude" xml:id="sec-switching-systems">
+  <title>What happens during a system switch?</title>
+  <para>
+    Running <literal>nixos-rebuild switch</literal> is one of the more
+    common tasks under NixOS. This chapter explains some of the
+    internals of this command to make it simpler for new module
+    developers to configure their units correctly and to make it easier
+    to understand what is happening and why for curious administrators.
+  </para>
+  <para>
+    <literal>nixos-rebuild</literal>, like many deployment solutions,
+    calls <literal>switch-to-configuration</literal> which resides in a
+    NixOS system at <literal>$out/bin/switch-to-configuration</literal>.
+    The script is called with the action that is to be performed like
+    <literal>switch</literal>, <literal>test</literal>,
+    <literal>boot</literal>. There is also the
+    <literal>dry-activate</literal> action which does not really perform
+    the actions but rather prints what it would do if you called it with
+    <literal>test</literal>. This feature can be used to check what
+    service states would be changed if the configuration was switched
+    to.
+  </para>
+  <para>
+    If the action is <literal>switch</literal> or
+    <literal>boot</literal>, the bootloader is updated first so the
+    configuration will be the next one to boot. Unless
+    <literal>NIXOS_NO_SYNC</literal> is set to <literal>1</literal>,
+    <literal>/nix/store</literal> is synced to disk.
+  </para>
+  <para>
+    If the action is <literal>switch</literal> or
+    <literal>test</literal>, the currently running system is inspected
+    and the actions to switch to the new system are calculated. This
+    process takes two data sources into account:
+    <literal>/etc/fstab</literal> and the current systemd status. Mounts
+    and swaps are read from <literal>/etc/fstab</literal> and the
+    corresponding actions are generated. If a new mount is added, for
+    example, the proper <literal>.mount</literal> unit is marked to be
+    started. The current systemd state is inspected, the difference
+    between the current system and the desired configuration is
+    calculated and actions are generated to get to this state. There are
+    a lot of nuances that can be controlled by the units which are
+    explained here.
+  </para>
+  <para>
+    After calculating what should be done, the actions are carried out.
+    The order of actions is always the same:
+  </para>
+  <itemizedlist spacing="compact">
+    <listitem>
+      <para>
+        Stop units (<literal>systemctl stop</literal>)
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Run activation script (<literal>$out/activate</literal>)
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        See if the activation script requested more units to restart
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Restart systemd if needed
+        (<literal>systemd daemon-reexec</literal>)
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Forget about the failed state of units
+        (<literal>systemctl reset-failed</literal>)
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Reload systemd (<literal>systemctl daemon-reload</literal>)
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Reload systemd user instances
+        (<literal>systemctl --user daemon-reload</literal>)
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Set up tmpfiles (<literal>systemd-tmpfiles --create</literal>)
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Reload units (<literal>systemctl reload</literal>)
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Restart units (<literal>systemctl restart</literal>)
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Start units (<literal>systemctl start</literal>)
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Inspect what changed during these actions and print units that
+        failed and that were newly started
+      </para>
+    </listitem>
+  </itemizedlist>
+  <para>
+    Most of these actions are either self-explaining but some of them
+    have to do with our units or the activation script. For this reason,
+    these topics are explained in the next sections.
+  </para>
+  <xi:include href="unit-handling.section.xml" />
+  <xi:include href="activation-script.section.xml" />
+</chapter>
diff --git a/nixos/doc/manual/from_md/installation/installing-pxe.section.xml b/nixos/doc/manual/from_md/installation/installing-pxe.section.xml
index 1dd15ddacba..94172de65ea 100644
--- a/nixos/doc/manual/from_md/installation/installing-pxe.section.xml
+++ b/nixos/doc/manual/from_md/installation/installing-pxe.section.xml
@@ -7,11 +7,11 @@
   <para>
     These instructions assume that you have an existing PXE or iPXE
     infrastructure and simply want to add the NixOS installer as another
-    option. To build the necessary files from a recent version of
+    option. To build the necessary files from your current version of
     nixpkgs, you can run:
   </para>
   <programlisting>
-nix-build -A netboot.x86_64-linux nixos/release.nix
+nix-build -A netboot.x86_64-linux '&lt;nixpkgs/nixos/release.nix&gt;'
 </programlisting>
   <para>
     This will create a <literal>result</literal> directory containing: *
diff --git a/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
index 5d0a9dc76ea..7502214c86b 100644
--- a/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
+++ b/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
@@ -42,6 +42,14 @@
           upgrade notes</link>.
         </para>
       </listitem>
+      <listitem>
+        <para>
+          systemd services can now set
+          <link linkend="opt-systemd.services">systemd.services.&lt;name&gt;.reloadTriggers</link>
+          instead of <literal>reloadIfChanged</literal> for a more
+          granular distinction between reloads and restarts.
+        </para>
+      </listitem>
     </itemizedlist>
   </section>
   <section xml:id="sec-release-22.05-new-services">
@@ -116,6 +124,14 @@
       </listitem>
       <listitem>
         <para>
+          <link xlink:href="https://github.com/sezanzeb/input-remapper">input-remapper</link>,
+          an easy to use tool to change the mapping of your input device
+          buttons. Available at
+          <link xlink:href="options.html#opt-services.input-remapper.enable">services.input-remapper</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <link xlink:href="https://invoiceplane.com">InvoicePlane</link>,
           web application for managing and creating invoices. Available
           at
@@ -146,6 +162,14 @@
       </listitem>
       <listitem>
         <para>
+          <link xlink:href="https://github.com/mbrubeck/agate">agate</link>,
+          a very simple server for the Gemini hypertext protocol.
+          Available as
+          <link xlink:href="options.html#opt-services.agate.enable">services.agate</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <link xlink:href="https://github.com/JustArchiNET/ArchiSteamFarm">ArchiSteamFarm</link>,
           a C# application with primary purpose of idling Steam cards
           from multiple accounts simultaneously. Available as
@@ -170,6 +194,13 @@
       </listitem>
       <listitem>
         <para>
+          <link xlink:href="https://moosefs.com">moosefs</link>, fault
+          tolerant petabyte distributed file system. Available as
+          <link linkend="opt-services.moosefs">moosefs</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <link xlink:href="https://github.com/ThomasLeister/prosody-filer">prosody-filer</link>,
           a server for handling XMPP HTTP Upload requests. Available at
           <link linkend="opt-services.prosody-filer.enable">services.prosody-filer</link>.
@@ -207,6 +238,13 @@
           <link xlink:href="options.html#opt-services.headscale.enable">services.headscale</link>
         </para>
       </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://0xerr0r.github.io/blocky/">blocky</link>,
+          fast and lightweight DNS proxy as ad-blocker for local network
+          with many features.
+        </para>
+      </listitem>
     </itemizedlist>
   </section>
   <section xml:id="sec-release-22.05-incompatibilities">
@@ -235,6 +273,36 @@
       </listitem>
       <listitem>
         <para>
+          <literal>pkgs.ghc.withPackages</literal> as well as
+          <literal>haskellPackages.ghcWithPackages</literal> etc. now
+          needs be overridden directly, as opposed to overriding the
+          result of calling it. Additionally, the
+          <literal>withLLVM</literal> parameter has been renamed to
+          <literal>useLLVM</literal>. So instead of
+          <literal>(ghc.withPackages (p: [])).override { withLLVM = true; }</literal>,
+          one needs to use
+          <literal>(ghc.withPackages.override { useLLVM = true; }) (p: [])</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The <literal>home-assistant</literal> module now requires
+          users that don’t want their configuration to be managed
+          declaratively to set
+          <literal>services.home-assistant.config = null;</literal>.
+          This is required due to the way default settings are handled
+          with the new settings style.
+        </para>
+        <para>
+          Additionally the default list of
+          <literal>extraComponents</literal> now includes the minimal
+          dependencies to successfully complete the
+          <link xlink:href="https://www.home-assistant.io/getting-started/onboarding/">onboarding</link>
+          procedure.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <literal>pkgs.emacsPackages.orgPackages</literal> is removed
           because org elpa is deprecated. The packages in the top level
           of <literal>pkgs.emacsPackages</literal>, such as org and
@@ -252,6 +320,17 @@
       </listitem>
       <listitem>
         <para>
+          <literal>services.kubernetes.scheduler.{port,address}</literal>
+          now set <literal>--secure-port</literal> and
+          <literal>--bind-address</literal> instead of
+          <literal>--port</literal> and <literal>--address</literal>,
+          since the former have been deprecated and are no longer
+          functional in kubernetes&gt;=1.23. Ensure that you are not
+          relying on the insecure behaviour before upgrading.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           The DHCP server (<literal>services.dhcpd4</literal>,
           <literal>services.dhcpd6</literal>) has been hardened. The
           service is now using the systemd’s
@@ -312,6 +391,22 @@
       </listitem>
       <listitem>
         <para>
+          <literal>buildGoModule</literal> was updated to use
+          <literal>go_1_17</literal>, third party derivations that
+          specify &gt;= go 1.17 in the main <literal>go.mod</literal>
+          will need to regenerate their <literal>vendorSha256</literal>
+          hash.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The <literal>gnome-passwordsafe</literal> package updated to
+          <link xlink:href="https://gitlab.gnome.org/World/secrets/-/tags/6.0">version
+          6.x</link> and renamed to <literal>gnome-secrets</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           If you previously used
           <literal>/etc/docker/daemon.json</literal>, you need to
           incorporate the changes into the new option
@@ -523,6 +618,15 @@
               honors <literal>restartIfChanged</literal> and
               <literal>reloadIfChanged</literal> of the units.
             </para>
+            <itemizedlist spacing="compact">
+              <listitem>
+                <para>
+                  Preferring to reload instead of restarting can still
+                  be achieved using
+                  <literal>/run/nixos/activation-reload-list</literal>.
+                </para>
+              </listitem>
+            </itemizedlist>
           </listitem>
           <listitem>
             <para>
@@ -777,6 +881,13 @@
       </listitem>
       <listitem>
         <para>
+          <literal>nixos-generate-config</literal> now puts the dhcp
+          configuration in <literal>hardware-configuration.nix</literal>
+          instead of <literal>configuration.nix</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <literal>fetchFromSourcehut</literal> now allows fetching
           repositories recursively using <literal>fetchgit</literal> or
           <literal>fetchhg</literal> if the argument
@@ -869,6 +980,17 @@
       </listitem>
       <listitem>
         <para>
+          The option
+          <link linkend="opt-services.networking.networkmanager.enableFccUnlock">services.networking.networkmanager.enableFccUnlock</link>
+          was added to support FCC unlock procedures. Since release
+          1.18.4, the ModemManager daemon no longer automatically
+          performs the FCC unlock procedure by default. See
+          <link xlink:href="https://modemmanager.org/docs/modemmanager/fcc-unlock/">the
+          docs</link> for more details.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <literal>programs.tmux</literal> has a new option
           <literal>plugins</literal> that accepts a list of packages
           from the <literal>tmuxPlugins</literal> group. The specified
diff --git a/nixos/doc/manual/installation/installing-pxe.section.md b/nixos/doc/manual/installation/installing-pxe.section.md
index 2016a258251..4fbd6525f8c 100644
--- a/nixos/doc/manual/installation/installing-pxe.section.md
+++ b/nixos/doc/manual/installation/installing-pxe.section.md
@@ -5,11 +5,11 @@ setup.
 
 These instructions assume that you have an existing PXE or iPXE
 infrastructure and simply want to add the NixOS installer as another
-option. To build the necessary files from a recent version of nixpkgs,
+option. To build the necessary files from your current version of nixpkgs,
 you can run:
 
 ```ShellSession
-nix-build -A netboot.x86_64-linux nixos/release.nix
+nix-build -A netboot.x86_64-linux '<nixpkgs/nixos/release.nix>'
 ```
 
 This will create a `result` directory containing: \* `bzImage` -- the
diff --git a/nixos/doc/manual/release-notes/rl-2205.section.md b/nixos/doc/manual/release-notes/rl-2205.section.md
index 7846513c607..c96f898505a 100644
--- a/nixos/doc/manual/release-notes/rl-2205.section.md
+++ b/nixos/doc/manual/release-notes/rl-2205.section.md
@@ -17,6 +17,8 @@ In addition to numerous new and upgraded packages, this release has the followin
   Migrations may take a while, see the [changelog](https://docs.mattermost.com/install/self-managed-changelog.html#release-v6-3-extended-support-release)
   and [important upgrade notes](https://docs.mattermost.com/upgrade/important-upgrade-notes.html).
 
+- systemd services can now set [systemd.services.\<name\>.reloadTriggers](#opt-systemd.services) instead of `reloadIfChanged` for a more granular distinction between reloads and restarts.
+
 ## New Services {#sec-release-22.05-new-services}
 
 - [aesmd](https://github.com/intel/linux-sgx#install-the-intelr-sgx-psw), the Intel SGX Architectural Enclave Service Manager. Available as [services.aesmd](#opt-services.aesmd.enable).
@@ -37,6 +39,8 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - [PowerDNS-Admin](https://github.com/ngoduykhanh/PowerDNS-Admin), a web interface for the PowerDNS server. Available at [services.powerdns-admin](options.html#opt-services.powerdns-admin.enable).
 
+- [input-remapper](https://github.com/sezanzeb/input-remapper), an easy to use tool to change the mapping of your input device buttons. Available at [services.input-remapper](options.html#opt-services.input-remapper.enable).
+
 - [InvoicePlane](https://invoiceplane.com), web application for managing and creating invoices. Available at [services.invoiceplane](options.html#opt-services.invoiceplane.enable).
 
 - [maddy](https://maddy.email), a composable all-in-one mail server. Available as [services.maddy](options.html#opt-services.maddy.enable).
@@ -45,12 +49,17 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - [tetrd](https://tetrd.app), share your internet connection from your device to your PC and vice versa through a USB cable. Available at [services.tetrd](#opt-services.tetrd.enable).
 
+- [agate](https://github.com/mbrubeck/agate), a very simple server for the Gemini hypertext protocol. Available as [services.agate](options.html#opt-services.agate.enable).
+
 - [ArchiSteamFarm](https://github.com/JustArchiNET/ArchiSteamFarm), a C# application with primary purpose of idling Steam cards from multiple accounts simultaneously. Available as [services.archisteamfarm](options.html#opt-services.archisteamfarm.enable).
 
 - [teleport](https://goteleport.com), allows engineers and security professionals to unify access for SSH servers, Kubernetes clusters, web applications, and databases across all environments. Available at [services.teleport](#opt-services.teleport.enable).
 
 - [BaGet](https://loic-sharma.github.io/BaGet/), a lightweight NuGet and symbol server. Available at [services.baget](#opt-services.baget.enable).
 
+- [moosefs](https://moosefs.com), fault tolerant petabyte distributed file system.
+  Available as [moosefs](#opt-services.moosefs).
+
 - [prosody-filer](https://github.com/ThomasLeister/prosody-filer), a server for handling XMPP HTTP Upload requests. Available at [services.prosody-filer](#opt-services.prosody-filer.enable).
 
 - [ethercalc](https://github.com/audreyt/ethercalc), an online collaborative
@@ -62,6 +71,8 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - [headscale](https://github.com/juanfont/headscale), an Open Source implementation of the [Tailscale](https://tailscale.io) Control Server. Available as [services.headscale](options.html#opt-services.headscale.enable)
 
+- [blocky](https://0xerr0r.github.io/blocky/), fast and lightweight DNS proxy as ad-blocker for local network with many features.
+
 <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
 
 ## Backward Incompatibilities {#sec-release-22.05-incompatibilities}
@@ -78,6 +89,21 @@ In addition to numerous new and upgraded packages, this release has the followin
   instead to ensure cross compilation keeps working (or switch to
   `haskellPackages.callPackage`).
 
+- `pkgs.ghc.withPackages` as well as `haskellPackages.ghcWithPackages` etc.
+  now needs be overridden directly, as opposed to overriding the result of
+  calling it. Additionally, the `withLLVM` parameter has been renamed to
+  `useLLVM`. So instead of `(ghc.withPackages (p: [])).override { withLLVM = true; }`,
+  one needs to use `(ghc.withPackages.override { useLLVM = true; }) (p: [])`.
+
+- The `home-assistant` module now requires users that don't want their
+  configuration to be managed declaratively to set
+  `services.home-assistant.config = null;`. This is required
+  due to the way default settings are handled with the new settings style.
+
+  Additionally the default list of `extraComponents` now includes the minimal
+  dependencies to successfully complete the [onboarding](https://www.home-assistant.io/getting-started/onboarding/)
+  procedure.
+
 - `pkgs.emacsPackages.orgPackages` is removed because org elpa is deprecated.
   The packages in the top level of `pkgs.emacsPackages`, such as org and
   org-contrib, refer to the ones in `pkgs.emacsPackages.elpaPackages` and
@@ -85,6 +111,8 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - `services.kubernetes.addons.dashboard` was removed due to it being an outdated version.
 
+- `services.kubernetes.scheduler.{port,address}` now set `--secure-port` and `--bind-address` instead of `--port` and `--address`, since the former have been deprecated and are no longer functional in kubernetes>=1.23. Ensure that you are not relying on the insecure behaviour before upgrading.
+
 - The DHCP server (`services.dhcpd4`, `services.dhcpd6`) has been hardened.
   The service is now using the systemd's `DynamicUser` mechanism to run as an unprivileged dynamically-allocated user with limited capabilities.
   The dhcpd state files are now always stored in `/var/lib/dhcpd{4,6}` and the `services.dhcpd4.stateDir` and `service.dhcpd6.stateDir` options have been removed.
@@ -102,6 +130,10 @@ In addition to numerous new and upgraded packages, this release has the followin
 - The `writers.writePython2` and corresponding `writers.writePython2Bin` convenience functions to create executable Python 2 scripts in the store were removed in preparation of removal of the Python 2 interpreter.
   Scripts have to be converted to Python 3 for use with `writers.writePython3` or `writers.writePyPy2` needs to be used.
 
+- `buildGoModule` was updated to use `go_1_17`, third party derivations that specify >= go 1.17 in the main `go.mod` will need to regenerate their `vendorSha256` hash.
+
+- The `gnome-passwordsafe` package updated to [version 6.x](https://gitlab.gnome.org/World/secrets/-/tags/6.0) and renamed to `gnome-secrets`.
+
 - If you previously used `/etc/docker/daemon.json`, you need to incorporate the changes into the new option `virtualisation.docker.daemon.settings`.
 
 - Ntopng (`services.ntopng`) is updated to 5.2.1 and uses a separate Redis instance if `system.stateVersion` is at least `22.05`. Existing setups shouldn't be affected.
@@ -168,6 +200,7 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - `switch-to-configuration` (the script that is run when running `nixos-rebuild switch` for example) has been reworked
     * The interface that allows activation scripts to restart units has been streamlined. Restarting and reloading is now done by a single file `/run/nixos/activation-restart-list` that honors `restartIfChanged` and `reloadIfChanged` of the units.
+        * Preferring to reload instead of restarting can still be achieved using `/run/nixos/activation-reload-list`.
     * The script now uses a proper ini-file parser to parse systemd units. Some values are now only searched in one section instead of in the entire unit. This is only relevant for units that don't use the NixOS systemd moule.
         * `RefuseManualStop`, `X-OnlyManualStart`, `X-StopOnRemoval`, `X-StopOnReconfiguration` are only searched in the `[Unit]` section
         * `X-ReloadIfChanged`, `X-RestartIfChanged`, `X-StopIfChanged` are only searched in the `[Service]` section
@@ -263,6 +296,8 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - A new option `boot.initrd.extraModprobeConfig` has been added which can be used to configure kernel modules that are loaded in the initrd.
 
+- `nixos-generate-config` now puts the dhcp configuration in `hardware-configuration.nix` instead of `configuration.nix`.
+
 - `fetchFromSourcehut` now allows fetching repositories recursively
   using `fetchgit` or `fetchhg` if the argument `fetchSubmodules`
   is set to `true`.
@@ -290,6 +325,12 @@ In addition to numerous new and upgraded packages, this release has the followin
   Reason is that the old name has been deprecated upstream.
   Using the old option name will still work, but produce a warning.
 
+- The option
+  [services.networking.networkmanager.enableFccUnlock](#opt-services.networking.networkmanager.enableFccUnlock)
+  was added to support FCC unlock procedures. Since release 1.18.4, the ModemManager
+  daemon no longer automatically performs the FCC unlock procedure by default. See
+  [the docs](https://modemmanager.org/docs/modemmanager/fcc-unlock/) for more details.
+
 - `programs.tmux` has a new option `plugins` that accepts a list of packages from the `tmuxPlugins` group. The specified packages are added to the system and loaded by `tmux`.
 
 <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
diff --git a/nixos/lib/eval-config.nix b/nixos/lib/eval-config.nix
index e3eb88a60eb..2daaa8a1186 100644
--- a/nixos/lib/eval-config.nix
+++ b/nixos/lib/eval-config.nix
@@ -21,6 +21,7 @@ evalConfigArgs@
 , # !!! See comment about args in lib/modules.nix
   specialArgs ? {}
 , modules
+, modulesLocation ? (builtins.unsafeGetAttrPos "modules" evalConfigArgs).file or null
 , # !!! See comment about check in lib/modules.nix
   check ? true
 , prefix ? []
@@ -74,7 +75,18 @@ let
         _module.check = lib.mkDefault check;
       };
     };
-  allUserModules = modules ++ legacyModules;
+
+  allUserModules =
+    let
+      # Add the invoking file (or specified modulesLocation) as error message location
+      # for modules that don't have their own locations; presumably inline modules.
+      locatedModules =
+        if modulesLocation == null then
+          modules
+        else
+          map (lib.setDefaultModuleLocation modulesLocation) modules;
+    in
+      locatedModules ++ legacyModules;
 
   noUserModules = evalModulesMinimal ({
     inherit prefix specialArgs;
diff --git a/nixos/lib/qemu-common.nix b/nixos/lib/qemu-common.nix
index 964814e8c60..20bbe9ff5d9 100644
--- a/nixos/lib/qemu-common.nix
+++ b/nixos/lib/qemu-common.nix
@@ -22,7 +22,7 @@ rec {
         else throw "Unknown QEMU serial device for system '${pkgs.stdenv.hostPlatform.system}'";
 
   qemuBinary = qemuPkg: {
-    x86_64-linux = "${qemuPkg}/bin/qemu-kvm -cpu qemu64";
+    x86_64-linux = "${qemuPkg}/bin/qemu-kvm -cpu max";
     armv7l-linux = "${qemuPkg}/bin/qemu-system-arm -enable-kvm -machine virt -cpu host";
     aarch64-linux = "${qemuPkg}/bin/qemu-system-aarch64 -enable-kvm -machine virt,gic-version=host -cpu host";
     powerpc64le-linux = "${qemuPkg}/bin/qemu-system-ppc64 -machine powernv";
diff --git a/nixos/lib/systemd-unit-options.nix b/nixos/lib/systemd-unit-options.nix
index 520f2e982a2..8029ba0e3f6 100644
--- a/nixos/lib/systemd-unit-options.nix
+++ b/nixos/lib/systemd-unit-options.nix
@@ -201,6 +201,17 @@ in rec {
       '';
     };
 
+    reloadTriggers = mkOption {
+      default = [];
+      type = types.listOf unitOption;
+      description = ''
+        An arbitrary list of items such as derivations.  If any item
+        in the list changes between reconfigurations, the service will
+        be reloaded.  If anything but a reload trigger changes in the
+        unit file, the unit will be restarted instead.
+      '';
+    };
+
     onFailure = mkOption {
       default = [];
       type = types.listOf unitNameType;
@@ -338,6 +349,11 @@ in rec {
         configuration switch if its definition has changed.  If
         enabled, the value of <option>restartIfChanged</option> is
         ignored.
+
+        This option should not be used anymore in favor of
+        <option>reloadTriggers</option> which allows more granular
+        control of when a service is reloaded and when a service
+        is restarted.
       '';
     };
 
diff --git a/nixos/lib/test-driver/test_driver/machine.py b/nixos/lib/test-driver/test_driver/machine.py
index a41c419ebe6..569a0f3c61e 100644
--- a/nixos/lib/test-driver/test_driver/machine.py
+++ b/nixos/lib/test-driver/test_driver/machine.py
@@ -241,9 +241,15 @@ class LegacyStartCommand(StartCommand):
         cdrom: Optional[str] = None,
         usb: Optional[str] = None,
         bios: Optional[str] = None,
+        qemuBinary: Optional[str] = None,
         qemuFlags: Optional[str] = None,
     ):
-        self._cmd = "qemu-kvm -m 384"
+        if qemuBinary is not None:
+            self._cmd = qemuBinary
+        else:
+            self._cmd = "qemu-kvm"
+
+        self._cmd += " -m 384"
 
         # networking
         net_backend = "-netdev user,id=net0"
@@ -381,6 +387,7 @@ class Machine:
             cdrom=args.get("cdrom"),
             usb=args.get("usb"),
             bios=args.get("bios"),
+            qemuBinary=args.get("qemuBinary"),
             qemuFlags=args.get("qemuFlags"),
         )
 
@@ -543,11 +550,11 @@ class Machine:
 
         Should only be used during test development, not in the production test."""
         self.connect()
-        self.log("Terminal is ready (there is no prompt):")
+        self.log("Terminal is ready (there is no initial prompt):")
 
         assert self.shell
         subprocess.run(
-            ["socat", "READLINE", f"FD:{self.shell.fileno()}"],
+            ["socat", "READLINE,prompt=$ ", f"FD:{self.shell.fileno()}"],
             pass_fds=[self.shell.fileno()],
         )
 
diff --git a/nixos/lib/utils.nix b/nixos/lib/utils.nix
index bbebf8ba35a..190c4db4d49 100644
--- a/nixos/lib/utils.nix
+++ b/nixos/lib/utils.nix
@@ -149,10 +149,16 @@ rec {
       if [[ -h '${output}' ]]; then
         rm '${output}'
       fi
+
+      inherit_errexit_restore=$(shopt -p inherit_errexit)
+      shopt -s inherit_errexit
     ''
     + concatStringsSep
         "\n"
-        (imap1 (index: name: "export secret${toString index}=$(<'${secrets.${name}}')")
+        (imap1 (index: name: ''
+                  secret${toString index}=$(<'${secrets.${name}}')
+                  export secret${toString index}
+                '')
                (attrNames secrets))
     + "\n"
     + "${pkgs.jq}/bin/jq >'${output}' '"
@@ -164,6 +170,7 @@ rec {
       ' <<'EOF'
       ${builtins.toJSON set}
       EOF
+      $inherit_errexit_restore
     '';
 
   systemdUtils = {
diff --git a/nixos/modules/config/users-groups.nix b/nixos/modules/config/users-groups.nix
index 2470d8b5bfe..b0f96c754fa 100644
--- a/nixos/modules/config/users-groups.nix
+++ b/nixos/modules/config/users-groups.nix
@@ -449,16 +449,10 @@ in {
   imports = [
     (mkAliasOptionModule [ "users" "extraUsers" ] [ "users" "users" ])
     (mkAliasOptionModule [ "users" "extraGroups" ] [ "users" "groups" ])
-    (mkChangedOptionModule
-      [ "security" "initialRootPassword" ]
-      [ "users" "users" "root" "initialHashedPassword" ]
-      (cfg: if cfg.security.initialRootPassword == "!"
-            then null
-            else cfg.security.initialRootPassword))
+    (mkRenamedOptionModule ["security" "initialRootPassword"] ["users" "users" "root" "initialHashedPassword"])
   ];
 
   ###### interface
-
   options = {
 
     users.mutableUsers = mkOption {
@@ -526,6 +520,17 @@ in {
       '';
     };
 
+
+    users.allowNoPasswordLogin = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Disable checking that at least the <literal>root</literal> user or a user in the <literal>wheel</literal> group can log in using
+        a password or an SSH key.
+
+        WARNING: enabling this can lock you out of your system. Enable this only if you know what are you doing.
+      '';
+    };
   };
 
 
@@ -540,6 +545,7 @@ in {
         home = "/root";
         shell = mkDefault cfg.defaultUserShell;
         group = "root";
+        initialHashedPassword = mkDefault "!";
       };
       nobody = {
         uid = ids.uids.nobody;
@@ -616,9 +622,11 @@ in {
         # there is at least one "privileged" account that has a
         # password or an SSH authorized key. Privileged accounts are
         # root and users in the wheel group.
-        assertion = !cfg.mutableUsers ->
-          any id ((mapAttrsToList (_: cfg:
-            (cfg.name == "root"
+        # The check does not apply when users.disableLoginPossibilityAssertion
+        # The check does not apply when users.mutableUsers
+        assertion = !cfg.mutableUsers -> !cfg.allowNoPasswordLogin ->
+          any id (mapAttrsToList (name: cfg:
+            (name == "root"
              || cfg.group == "wheel"
              || elem "wheel" cfg.extraGroups)
             &&
@@ -627,12 +635,16 @@ in {
              || cfg.passwordFile != null
              || cfg.openssh.authorizedKeys.keys != []
              || cfg.openssh.authorizedKeys.keyFiles != [])
-          ) cfg.users) ++ [
+          ) cfg.users ++ [
             config.security.googleOsLogin.enable
           ]);
         message = ''
           Neither the root account nor any wheel user has a password or SSH authorized key.
-          You must set one to prevent being locked out of your system.'';
+          You must set one to prevent being locked out of your system.
+          If you really want to be locked out of your system, set users.allowNoPasswordLogin = true;
+          However you are most probably better off by setting users.mutableUsers = true; and
+          manually running passwd root to set the root password.
+          '';
       }
     ] ++ flatten (flip mapAttrsToList cfg.users (name: user:
       [
diff --git a/nixos/modules/installer/tools/nixos-generate-config.pl b/nixos/modules/installer/tools/nixos-generate-config.pl
index fe8c4fb1a6b..57aef50a0f6 100644
--- a/nixos/modules/installer/tools/nixos-generate-config.pl
+++ b/nixos/modules/installer/tools/nixos-generate-config.pl
@@ -279,7 +279,7 @@ if (`lsblk -o TYPE` =~ "lvm") {
     push @initrdKernelModules, "dm-snapshot";
 }
 
-my $virt = `systemd-detect-virt`;
+my $virt = `@detectvirt@`;
 chomp $virt;
 
 
@@ -398,7 +398,7 @@ foreach my $fs (read_file("/proc/self/mountinfo")) {
     # Maybe this is a bind-mount of a filesystem we saw earlier?
     if (defined $fsByDev{$fields[2]}) {
         # Make sure this isn't a btrfs subvolume.
-        my $msg = `btrfs subvol show $rootDir$mountPoint`;
+        my $msg = `@btrfs@ subvol show $rootDir$mountPoint`;
         if ($? != 0 || $msg =~ /ERROR:/s) {
             my $path = $fields[3]; $path = "" if $path eq "/";
             my $base = $fsByDev{$fields[2]};
@@ -436,7 +436,7 @@ EOF
 
     # Is this a btrfs filesystem?
     if ($fsType eq "btrfs") {
-        my ($status, @info) = runCommand("btrfs subvol show $rootDir$mountPoint");
+        my ($status, @info) = runCommand("@btrfs@ subvol show $rootDir$mountPoint");
         if ($status != 0 || join("", @info) =~ /ERROR:/) {
             die "Failed to retrieve subvolume info for $mountPoint\n";
         }
@@ -558,6 +558,8 @@ if (!$noFilesystems) {
     $fsAndSwap .= "swapDevices =" . multiLineList("    ", @swapDevices) . ";\n";
 }
 
+my $networkingDhcpConfig = generateNetworkingDhcpConfig();
+
 my $hwConfig = <<EOF;
 # Do not modify this file!  It was generated by ‘nixos-generate-config’
 # and may be overwritten by future invocations.  Please make changes
@@ -572,6 +574,7 @@ my $hwConfig = <<EOF;
   boot.kernelModules = [$kernelModules ];
   boot.extraModulePackages = [$modulePackages ];
 $fsAndSwap
+$networkingDhcpConfig
 ${\join "", (map { "  $_\n" } (uniq @attrs))}}
 EOF
 
@@ -580,13 +583,13 @@ sub generateNetworkingDhcpConfig {
   # The global useDHCP flag is deprecated, therefore explicitly set to false here.
   # Per-interface useDHCP will be mandatory in the future, so this generated config
   # replicates the default behaviour.
-  networking.useDHCP = false;
+  networking.useDHCP = lib.mkDefault false;
 EOF
 
     foreach my $path (glob "/sys/class/net/*") {
         my $dev = basename($path);
         if ($dev ne "lo") {
-            $config .= "  networking.interfaces.$dev.useDHCP = true;\n";
+            $config .= "  networking.interfaces.$dev.useDHCP = lib.mkDefault true;\n";
         }
     }
 
diff --git a/nixos/modules/installer/tools/tools.nix b/nixos/modules/installer/tools/tools.nix
index 2f3b0cdd48f..71aaf7f253d 100644
--- a/nixos/modules/installer/tools/tools.nix
+++ b/nixos/modules/installer/tools/tools.nix
@@ -33,8 +33,9 @@ let
   nixos-generate-config = makeProg {
     name = "nixos-generate-config";
     src = ./nixos-generate-config.pl;
-    path = lib.optionals (lib.elem "btrfs" config.boot.supportedFilesystems) [ pkgs.btrfs-progs ];
     perl = "${pkgs.perl.withPackages (p: [ p.FileSlurp ])}/bin/perl";
+    detectvirt = "${pkgs.systemd}/bin/systemd-detect-virt";
+    btrfs = "${pkgs.btrfs-progs}/bin/btrfs";
     inherit (config.system.nixos-generate-config) configuration desktopConfiguration;
     xserverEnabled = config.services.xserver.enable;
   };
@@ -133,12 +134,13 @@ in
 
       $bootLoaderConfig
         # networking.hostName = "nixos"; # Define your hostname.
+        # Pick only one of the below networking options.
         # networking.wireless.enable = true;  # Enables wireless support via wpa_supplicant.
+        # networking.networkmanager.enable = true;  # Easiest to use and most distros use this by default.
 
         # Set your time zone.
         # time.timeZone = "Europe/Amsterdam";
 
-      $networkingDhcpConfig
         # Configure network proxy if necessary
         # networking.proxy.default = "http://user:password\@proxy:port/";
         # networking.proxy.noProxy = "127.0.0.1,localhost,internal.domain";
@@ -148,6 +150,7 @@ in
         # console = {
         #   font = "Lat2-Terminus16";
         #   keyMap = "us";
+        #   useXkbConfig = true; # use xkbOptions in tty.
         # };
 
       $xserverConfig
@@ -155,7 +158,10 @@ in
       $desktopConfiguration
         # Configure keymap in X11
         # services.xserver.layout = "us";
-        # services.xserver.xkbOptions = "eurosign:e";
+        # services.xserver.xkbOptions = {
+        #   "eurosign:e";
+        #   "caps:escape" # map caps to escape.
+        # };
 
         # Enable CUPS to print documents.
         # services.printing.enable = true;
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 1e3b8f3099b..ca82ddfb586 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -356,7 +356,6 @@
   ./services/desktops/cpupower-gui.nix
   ./services/desktops/dleyna-renderer.nix
   ./services/desktops/dleyna-server.nix
-  ./services/desktops/pantheon/files.nix
   ./services/desktops/espanso.nix
   ./services/desktops/flatpak.nix
   ./services/desktops/geoclue2.nix
@@ -451,6 +450,7 @@
   ./services/hardware/undervolt.nix
   ./services/hardware/vdr.nix
   ./services/hardware/xow.nix
+  ./services/home-automation/home-assistant.nix
   ./services/logging/SystemdJournal2Gelf.nix
   ./services/logging/awstats.nix
   ./services/logging/filebeat.nix
@@ -546,8 +546,8 @@
   ./services/misc/headphones.nix
   ./services/misc/heisenbridge.nix
   ./services/misc/greenclip.nix
-  ./services/misc/home-assistant.nix
   ./services/misc/ihaskell.nix
+  ./services/misc/input-remapper.nix
   ./services/misc/irkerd.nix
   ./services/misc/jackett.nix
   ./services/misc/jellyfin.nix
@@ -596,6 +596,7 @@
   ./services/misc/redmine.nix
   ./services/misc/rippled.nix
   ./services/misc/ripple-data-api.nix
+  ./services/misc/rmfakecloud.nix
   ./services/misc/serviio.nix
   ./services/misc/safeeyes.nix
   ./services/misc/sdrplay.nix
@@ -683,6 +684,7 @@
   ./services/network-filesystems/litestream/default.nix
   ./services/network-filesystems/netatalk.nix
   ./services/network-filesystems/nfsd.nix
+  ./services/network-filesystems/moosefs.nix
   ./services/network-filesystems/openafs/client.nix
   ./services/network-filesystems/openafs/server.nix
   ./services/network-filesystems/orangefs/server.nix
@@ -716,6 +718,7 @@
   ./services/networking/bird.nix
   ./services/networking/bitlbee.nix
   ./services/networking/blockbook-frontend.nix
+  ./services/networking/blocky.nix
   ./services/networking/charybdis.nix
   ./services/networking/cjdns.nix
   ./services/networking/cntlm.nix
@@ -759,7 +762,6 @@
   ./services/networking/go-neb.nix
   ./services/networking/go-shadowsocks2.nix
   ./services/networking/gobgpd.nix
-  ./services/networking/gogoclient.nix
   ./services/networking/gvpe.nix
   ./services/networking/hans.nix
   ./services/networking/haproxy.nix
@@ -914,6 +916,7 @@
   ./services/networking/vsftpd.nix
   ./services/networking/wasabibackend.nix
   ./services/networking/websockify.nix
+  ./services/networking/wg-netmanager.nix
   ./services/networking/wg-quick.nix
   ./services/networking/wireguard.nix
   ./services/networking/wpa_supplicant.nix
@@ -1055,6 +1058,7 @@
   ./services/web-apps/wordpress.nix
   ./services/web-apps/youtrack.nix
   ./services/web-apps/zabbix.nix
+  ./services/web-servers/agate.nix
   ./services/web-servers/apache-httpd/default.nix
   ./services/web-servers/caddy/default.nix
   ./services/web-servers/darkhttpd.nix
@@ -1074,7 +1078,6 @@
   ./services/web-servers/phpfpm/default.nix
   ./services/web-servers/pomerium.nix
   ./services/web-servers/unit/default.nix
-  ./services/web-servers/shellinabox.nix
   ./services/web-servers/tomcat.nix
   ./services/web-servers/traefik.nix
   ./services/web-servers/trafficserver/default.nix
@@ -1157,7 +1160,7 @@
   ./system/boot/systemd-nspawn.nix
   ./system/boot/timesyncd.nix
   ./system/boot/tmp.nix
-  ./system/etc/etc.nix
+  ./system/etc/etc-activation.nix
   ./tasks/auto-upgrade.nix
   ./tasks/bcache.nix
   ./tasks/cpu-freq.nix
diff --git a/nixos/modules/programs/calls.nix b/nixos/modules/programs/calls.nix
index 59961625e5d..08a223b408d 100644
--- a/nixos/modules/programs/calls.nix
+++ b/nixos/modules/programs/calls.nix
@@ -14,6 +14,8 @@ in {
   };
 
   config = mkIf cfg.enable {
+    programs.dconf.enable = true;
+
     environment.systemPackages = [
       pkgs.calls
     ];
diff --git a/nixos/modules/programs/command-not-found/command-not-found.pl b/nixos/modules/programs/command-not-found/command-not-found.pl
index 220d057b7f4..72e246c81ae 100644
--- a/nixos/modules/programs/command-not-found/command-not-found.pl
+++ b/nixos/modules/programs/command-not-found/command-not-found.pl
@@ -21,11 +21,24 @@ my $res = $dbh->selectall_arrayref(
     "select package from Programs where system = ? and name = ?",
     { Slice => {} }, $system, $program);
 
-if (!defined $res || scalar @$res == 0) {
+my $len = !defined $res ? 0 : scalar @$res;
+
+if ($len == 0) {
     print STDERR "$program: command not found\n";
-} elsif (scalar @$res == 1) {
+} elsif ($len == 1) {
     my $package = @$res[0]->{package};
     if ($ENV{"NIX_AUTO_RUN"} // "") {
+        if ($ENV{"NIX_AUTO_RUN_INTERACTIVE"} // "") {
+            while (1) {
+                print STDERR "'$program' from package '$package' will be run, confirm? [yn]: ";
+                chomp(my $comfirm = <STDIN>);
+                if (lc $comfirm eq "n") {
+                    exit 0;
+                } elsif (lc $comfirm eq "y") {
+                    last;
+                }
+            }
+        }
         exec("nix-shell", "-p", $package, "--run", shell_quote("exec", @ARGV));
     } else {
         print STDERR <<EOF;
@@ -35,11 +48,30 @@ ephemeral shell by typing:
 EOF
     }
 } else {
-    print STDERR <<EOF;
+    if ($ENV{"NIX_AUTO_RUN"} // "") {
+        print STDERR "Select a package that provides '$program':\n";
+        for my $i (0 .. $len - 1) {
+            print STDERR "  [", $i + 1, "]: @$res[$i]->{package}\n";
+        }
+        my $choice = 0;
+        while (1) { # exec will break this loop
+            no warnings "numeric";
+            print STDERR "Your choice [1-${len}]: ";
+            # 0 can be invalid user input like non-number string
+            # so we start from 1
+            $choice = <STDIN> + 0;
+            if (1 <= $choice && $choice <= $len) {
+                exec("nix-shell", "-p", @$res[$choice - 1]->{package},
+                    "--run", shell_quote("exec", @ARGV));
+            }
+        }
+    } else {
+        print STDERR <<EOF;
 The program '$program' is not in your PATH. It is provided by several packages.
 You can make it available in an ephemeral shell by typing one of the following:
 EOF
-    print STDERR "  nix-shell -p $_->{package}\n" foreach @$res;
+        print STDERR "  nix-shell -p $_->{package}\n" foreach @$res;
+    }
 }
 
 exit 127;
diff --git a/nixos/modules/programs/dconf.nix b/nixos/modules/programs/dconf.nix
index 298abac8afa..265c41cbbbc 100644
--- a/nixos/modules/programs/dconf.nix
+++ b/nixos/modules/programs/dconf.nix
@@ -60,7 +60,7 @@ in
     environment.systemPackages = [ pkgs.dconf ];
 
     # Needed for unwrapped applications
-    environment.variables.GIO_EXTRA_MODULES = mkIf cfg.enable [ "${pkgs.dconf.lib}/lib/gio/modules" ];
+    environment.sessionVariables.GIO_EXTRA_MODULES = mkIf cfg.enable [ "${pkgs.dconf.lib}/lib/gio/modules" ];
   };
 
 }
diff --git a/nixos/modules/rename.nix b/nixos/modules/rename.nix
index 31538c42219..c271d504b77 100644
--- a/nixos/modules/rename.nix
+++ b/nixos/modules/rename.nix
@@ -60,6 +60,9 @@ with lib;
     (mkRemovedOptionModule [ "services" "moinmoin" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "mwlib" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "osquery" ] "The osquery module has been removed")
+    (mkRemovedOptionModule [ "services" "pantheon" "files" ] ''
+      This module was removed, please add pkgs.pantheon.elementary-files to environment.systemPackages directly.
+    '')
     (mkRemovedOptionModule [ "services" "prey" ] ''
       prey-bash-client is deprecated upstream
     '')
@@ -85,6 +88,10 @@ with lib;
       The racoon module has been removed, because the software project was abandoned upstream.
     '')
 
+    (mkRemovedOptionModule [ "services" "shellinabox" ] "The corresponding package was removed from nixpkgs.")
+
+    (mkRemovedOptionModule [ "services" "gogoclient" ] "The corresponding package was removed from nixpkgs.")
+
     # Do NOT add any option renames here, see top of the file
   ];
 }
diff --git a/nixos/modules/security/google_oslogin.nix b/nixos/modules/security/google_oslogin.nix
index c2889a0f0d1..cf416035ef6 100644
--- a/nixos/modules/security/google_oslogin.nix
+++ b/nixos/modules/security/google_oslogin.nix
@@ -5,7 +5,7 @@ with lib;
 let
 
   cfg = config.security.googleOsLogin;
-  package = pkgs.google-compute-engine-oslogin;
+  package = pkgs.google-guest-oslogin;
 
 in
 
@@ -17,7 +17,7 @@ in
       type = types.bool;
       default = false;
       description = ''
-        Whether to enable Google OS Login
+        Whether to enable Google OS Login.
 
         The OS Login package enables the following components:
         AuthorizedKeysCommand to query valid SSH keys from the user's OS Login
@@ -36,7 +36,7 @@ in
     security.pam.services.sshd = {
       makeHomeDir = true;
       googleOsLoginAccountVerification = true;
-      # disabled for now: googleOsLoginAuthentication = true;
+      googleOsLoginAuthentication = true;
     };
 
     security.sudo.extraConfig = ''
@@ -47,6 +47,9 @@ in
       "d /var/google-users.d 750 root root -"
     ];
 
+    systemd.packages = [ package ];
+    systemd.timers.google-oslogin-cache.wantedBy = [ "timers.target" ];
+
     # enable the nss module, so user lookups etc. work
     system.nssModules = [ package ];
     system.nssDatabases.passwd = [ "cache_oslogin" "oslogin" ];
diff --git a/nixos/modules/security/pam.nix b/nixos/modules/security/pam.nix
index 964cfe7040c..9f295db84fd 100644
--- a/nixos/modules/security/pam.nix
+++ b/nixos/modules/security/pam.nix
@@ -444,15 +444,15 @@ let
             account sufficient ${pam_krb5}/lib/security/pam_krb5.so
           '' +
           optionalString cfg.googleOsLoginAccountVerification ''
-            account [success=ok ignore=ignore default=die] ${pkgs.google-compute-engine-oslogin}/lib/pam_oslogin_login.so
-            account [success=ok default=ignore] ${pkgs.google-compute-engine-oslogin}/lib/pam_oslogin_admin.so
+            account [success=ok ignore=ignore default=die] ${pkgs.google-guest-oslogin}/lib/security/pam_oslogin_login.so
+            account [success=ok default=ignore] ${pkgs.google-guest-oslogin}/lib/security/pam_oslogin_admin.so
           '' +
           ''
 
             # Authentication management.
           '' +
           optionalString cfg.googleOsLoginAuthentication ''
-            auth [success=done perm_denied=bad default=ignore] ${pkgs.google-compute-engine-oslogin}/lib/pam_oslogin_login.so
+            auth [success=done perm_denied=die default=ignore] ${pkgs.google-guest-oslogin}/lib/security/pam_oslogin_login.so
           '' +
           optionalString cfg.rootOK ''
             auth sufficient pam_rootok.so
@@ -1091,11 +1091,11 @@ in
         mr ${pam_ccreds}/lib/security/pam_ccreds.so,
       '' +
       optionalString (isEnabled (cfg: cfg.googleOsLoginAccountVerification)) ''
-        mr ${pkgs.google-compute-engine-oslogin}/lib/pam_oslogin_login.so,
-        mr ${pkgs.google-compute-engine-oslogin}/lib/pam_oslogin_admin.so,
+        mr ${pkgs.google-guest-oslogin}/lib/security/pam_oslogin_login.so,
+        mr ${pkgs.google-guest-oslogin}/lib/security/pam_oslogin_admin.so,
       '' +
       optionalString (isEnabled (cfg: cfg.googleOsLoginAuthentication)) ''
-        mr ${pkgs.google-compute-engine-oslogin}/lib/pam_oslogin_login.so,
+        mr ${pkgs.google-guest-oslogin}/lib/security/pam_oslogin_login.so,
       '' +
       optionalString (config.security.pam.enableSSHAgentAuth
                      && isEnabled (cfg: cfg.sshAgentAuth)) ''
diff --git a/nixos/modules/services/audio/jmusicbot.nix b/nixos/modules/services/audio/jmusicbot.nix
index f573bd2ab8d..e0f8d461af0 100644
--- a/nixos/modules/services/audio/jmusicbot.nix
+++ b/nixos/modules/services/audio/jmusicbot.nix
@@ -9,6 +9,13 @@ in
     services.jmusicbot = {
       enable = mkEnableOption "jmusicbot, a Discord music bot that's easy to set up and run yourself";
 
+      package = mkOption {
+        type = types.package;
+        default = pkgs.jmusicbot;
+        defaultText = literalExpression "pkgs.jmusicbot";
+        description = "JMusicBot package to use";
+      };
+
       stateDir = mkOption {
         type = types.path;
         description = ''
@@ -27,7 +34,7 @@ in
       after = [ "network-online.target" ];
       description = "Discord music bot that's easy to set up and run yourself!";
       serviceConfig = mkMerge [{
-        ExecStart = "${pkgs.jmusicbot}/bin/JMusicBot";
+        ExecStart = "${cfg.package}/bin/JMusicBot";
         WorkingDirectory = cfg.stateDir;
         Restart = "always";
         RestartSec = 20;
diff --git a/nixos/modules/services/cluster/kubernetes/scheduler.nix b/nixos/modules/services/cluster/kubernetes/scheduler.nix
index 2a522f1db89..2d95528a6ea 100644
--- a/nixos/modules/services/cluster/kubernetes/scheduler.nix
+++ b/nixos/modules/services/cluster/kubernetes/scheduler.nix
@@ -66,12 +66,12 @@ in
       serviceConfig = {
         Slice = "kubernetes.slice";
         ExecStart = ''${top.package}/bin/kube-scheduler \
-          --address=${cfg.address} \
+          --bind-address=${cfg.address} \
           ${optionalString (cfg.featureGates != [])
             "--feature-gates=${concatMapStringsSep "," (feature: "${feature}=true") cfg.featureGates}"} \
           --kubeconfig=${top.lib.mkKubeConfig "kube-scheduler" cfg.kubeconfig} \
           --leader-elect=${boolToString cfg.leaderElect} \
-          --port=${toString cfg.port} \
+          --secure-port=${toString cfg.port} \
           ${optionalString (cfg.verbosity != null) "--v=${toString cfg.verbosity}"} \
           ${cfg.extraOpts}
         '';
diff --git a/nixos/modules/services/continuous-integration/github-runner.nix b/nixos/modules/services/continuous-integration/github-runner.nix
index afd85c972b5..c3bd8f99c57 100644
--- a/nixos/modules/services/continuous-integration/github-runner.nix
+++ b/nixos/modules/services/continuous-integration/github-runner.nix
@@ -208,6 +208,7 @@ in
                 token=$(< "$STATE_DIRECTORY"/${newConfigTokenFilename})
                 RUNNER_ROOT="$STATE_DIRECTORY" ${cfg.package}/bin/config.sh \
                   --unattended \
+                  --disableupdate \
                   --work "$RUNTIME_DIRECTORY" \
                   --url ${escapeShellArg cfg.url} \
                   --token "$token" \
diff --git a/nixos/modules/services/desktops/gnome/glib-networking.nix b/nixos/modules/services/desktops/gnome/glib-networking.nix
index 4288b6b5de6..1039605391a 100644
--- a/nixos/modules/services/desktops/gnome/glib-networking.nix
+++ b/nixos/modules/services/desktops/gnome/glib-networking.nix
@@ -38,7 +38,7 @@ with lib;
 
     systemd.packages = [ pkgs.glib-networking ];
 
-    environment.variables.GIO_EXTRA_MODULES = [ "${pkgs.glib-networking.out}/lib/gio/modules" ];
+    environment.sessionVariables.GIO_EXTRA_MODULES = [ "${pkgs.glib-networking.out}/lib/gio/modules" ];
 
   };
 
diff --git a/nixos/modules/services/desktops/gvfs.nix b/nixos/modules/services/desktops/gvfs.nix
index 27864fad4f2..1aa64ea37db 100644
--- a/nixos/modules/services/desktops/gvfs.nix
+++ b/nixos/modules/services/desktops/gvfs.nix
@@ -57,7 +57,7 @@ in
     services.udev.packages = [ pkgs.libmtp.out ];
 
     # Needed for unwrapped applications
-    environment.variables.GIO_EXTRA_MODULES = [ "${cfg.package}/lib/gio/modules" ];
+    environment.sessionVariables.GIO_EXTRA_MODULES = [ "${cfg.package}/lib/gio/modules" ];
 
   };
 
diff --git a/nixos/modules/services/desktops/pantheon/files.nix b/nixos/modules/services/desktops/pantheon/files.nix
deleted file mode 100644
index 8cee9f42b62..00000000000
--- a/nixos/modules/services/desktops/pantheon/files.nix
+++ /dev/null
@@ -1,13 +0,0 @@
-# pantheon files daemon.
-
-{ config, pkgs, lib, ... }:
-
-with lib;
-
-{
-
-  imports = [
-    (mkRemovedOptionModule [ "services" "pantheon" "files" "enable" ] "Use `environment.systemPackages [ pkgs.pantheon.elementary-files ];`")
-  ];
-
-}
diff --git a/nixos/modules/services/hardware/ddccontrol.nix b/nixos/modules/services/hardware/ddccontrol.nix
index 766bf12ee9f..f0b5a9c8196 100644
--- a/nixos/modules/services/hardware/ddccontrol.nix
+++ b/nixos/modules/services/hardware/ddccontrol.nix
@@ -20,6 +20,9 @@ in
   ###### implementation
 
   config = lib.mkIf cfg.enable {
+    # Load the i2c-dev module
+    boot.kernelModules = [ "i2c_dev" ];
+
     # Give users access to the "gddccontrol" tool
     environment.systemPackages = [
       pkgs.ddccontrol
diff --git a/nixos/modules/services/hardware/udev.nix b/nixos/modules/services/hardware/udev.nix
index d48b5444677..61448af2d33 100644
--- a/nixos/modules/services/hardware/udev.nix
+++ b/nixos/modules/services/hardware/udev.nix
@@ -317,7 +317,8 @@ in
       (isYes "NET")
     ];
 
-    boot.extraModprobeConfig = "options firmware_class path=${config.hardware.firmware}/lib/firmware";
+    # We don't place this into `extraModprobeConfig` so that stage-1 ramdisk doesn't bloat.
+    environment.etc."modprobe.d/firmware.conf".text = "options firmware_class path=${config.hardware.firmware}/lib/firmware";
 
     system.activationScripts.udevd =
       ''
diff --git a/nixos/modules/services/hardware/undervolt.nix b/nixos/modules/services/hardware/undervolt.nix
index 212c0227c0d..a743bbf21c8 100644
--- a/nixos/modules/services/hardware/undervolt.nix
+++ b/nixos/modules/services/hardware/undervolt.nix
@@ -164,8 +164,6 @@ in
     environment.systemPackages = [ cfg.package ];
 
     systemd.services.undervolt = {
-      path = [ pkgs.undervolt ];
-
       description = "Intel Undervolting Service";
 
       # Apply undervolt on boot, nixos generation switch and resume
@@ -175,7 +173,7 @@ in
       serviceConfig = {
         Type = "oneshot";
         Restart = "no";
-        ExecStart = "${pkgs.undervolt}/bin/undervolt ${toString cliArgs}";
+        ExecStart = "${cfg.package}/bin/undervolt ${toString cliArgs}";
       };
     };
 
diff --git a/nixos/modules/services/hardware/upower.nix b/nixos/modules/services/hardware/upower.nix
index 92c060147bf..81bf497c993 100644
--- a/nixos/modules/services/hardware/upower.nix
+++ b/nixos/modules/services/hardware/upower.nix
@@ -155,8 +155,8 @@ in
         default = 1200;
         description = ''
           When <literal>usePercentageForPolicy</literal> is
-          <literal>false</literal>, the time remaining at which UPower will
-          consider the battery low.
+          <literal>false</literal>, the time remaining in seconds at which
+          UPower will consider the battery low.
 
           If any value (of <literal>timeLow</literal>,
           <literal>timeCritical</literal> and <literal>timeAction</literal>) is
@@ -169,8 +169,8 @@ in
         default = 300;
         description = ''
           When <literal>usePercentageForPolicy</literal> is
-          <literal>false</literal>, the time remaining at which UPower will
-          consider the battery critical.
+          <literal>false</literal>, the time remaining in seconds at which
+          UPower will consider the battery critical.
 
           If any value (of <literal>timeLow</literal>,
           <literal>timeCritical</literal> and <literal>timeAction</literal>) is
@@ -183,8 +183,8 @@ in
         default = 120;
         description = ''
           When <literal>usePercentageForPolicy</literal> is
-          <literal>false</literal>, the time remaining at which UPower will
-          take action for the critical battery level.
+          <literal>false</literal>, the time remaining in seconds at which
+          UPower will take action for the critical battery level.
 
           If any value (of <literal>timeLow</literal>,
           <literal>timeCritical</literal> and <literal>timeAction</literal>) is
diff --git a/nixos/modules/services/misc/home-assistant.nix b/nixos/modules/services/home-automation/home-assistant.nix
index ac4c0222aac..f4197650ab2 100644
--- a/nixos/modules/services/misc/home-assistant.nix
+++ b/nixos/modules/services/home-automation/home-assistant.nix
@@ -4,35 +4,27 @@ with lib;
 
 let
   cfg = config.services.home-assistant;
+  format = pkgs.formats.yaml {};
 
-  # cfg.config != null can be assumed here
-  configJSON = pkgs.writeText "configuration.json"
-    (builtins.toJSON (if cfg.applyDefaultConfig then
-    (recursiveUpdate defaultConfig cfg.config) else cfg.config));
+  # Render config attribute sets to YAML
+  # Values that are null will be filtered from the output, so this is one way to have optional
+  # options shown in settings.
+  # We post-process the result to add support for YAML functions, like secrets or includes, see e.g.
+  # https://www.home-assistant.io/docs/configuration/secrets/
+  filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! elem v [ null ])) cfg.config or {};
   configFile = pkgs.runCommand "configuration.yaml" { preferLocalBuild = true; } ''
-    ${pkgs.remarshal}/bin/json2yaml -i ${configJSON} -o $out
-    # Hack to support custom yaml objects,
-    # i.e. secrets: https://www.home-assistant.io/docs/configuration/secrets/
+    cp ${format.generate "configuration.yaml" filteredConfig} $out
     sed -i -e "s/'\!\([a-z_]\+\) \(.*\)'/\!\1 \2/;s/^\!\!/\!/;" $out
   '';
+  lovelaceConfig = cfg.lovelaceConfig or {};
+  lovelaceConfigFile = format.generate "ui-lovelace.yaml" lovelaceConfig;
 
-  lovelaceConfigJSON = pkgs.writeText "ui-lovelace.json"
-    (builtins.toJSON cfg.lovelaceConfig);
-  lovelaceConfigFile = pkgs.runCommand "ui-lovelace.yaml" { preferLocalBuild = true; } ''
-    ${pkgs.remarshal}/bin/json2yaml -i ${lovelaceConfigJSON} -o $out
-  '';
-
+  # Components advertised by the home-assistant package
   availableComponents = cfg.package.availableComponents;
 
+  # Components that were added by overriding the package
   explicitComponents = cfg.package.extraComponents;
-
-  usedPlatforms = config:
-    if isAttrs config then
-      optional (config ? platform) config.platform
-      ++ concatMap usedPlatforms (attrValues config)
-    else if isList config then
-      concatMap usedPlatforms config
-    else [ ];
+  useExplicitComponent = component: elem component explicitComponents;
 
   # Given a component "platform", looks up whether it is used in the config
   # as `platform = "platform";`.
@@ -42,34 +34,46 @@ let
   #   platform = "mqtt";
   #   ...
   # } ];
-  useComponentPlatform = component: elem component (usedPlatforms cfg.config);
+  usedPlatforms = config:
+    if isAttrs config then
+      optional (config ? platform) config.platform
+      ++ concatMap usedPlatforms (attrValues config)
+    else if isList config then
+      concatMap usedPlatforms config
+    else [ ];
 
-  useExplicitComponent = component: elem component explicitComponents;
+  useComponentPlatform = component: elem component (usedPlatforms cfg.config);
 
-  # Returns whether component is used in config or explicitly passed into package
+  # Returns whether component is used in config, explicitly passed into package or
+  # configured in the module.
   useComponent = component:
     hasAttrByPath (splitString "." component) cfg.config
     || useComponentPlatform component
-    || useExplicitComponent component;
+    || useExplicitComponent component
+    || builtins.elem component cfg.extraComponents;
 
-  # List of components used in config
+  # Final list of components passed into the package to include required dependencies
   extraComponents = filter useComponent availableComponents;
 
-  package = if (cfg.autoExtraComponents && cfg.config != null)
-    then (cfg.package.override { inherit extraComponents; })
-    else cfg.package;
+  package = (cfg.package.override (oldArgs: {
+    # Respect overrides that already exist in the passed package and
+    # concat it with values passed via the module.
+    extraComponents = oldArgs.extraComponents or [] ++ extraComponents;
+    extraPackages = ps: (oldArgs.extraPackages or (_: []) ps) ++ (cfg.extraPackages ps);
+  }));
+in {
+  imports = [
+    # Migrations in NixOS 22.05
+    (mkRemovedOptionModule [ "services" "home-assistant" "applyDefaultConfig" ] "The default config was migrated into services.home-assistant.config")
+    (mkRemovedOptionModule [ "services" "home-assistant" "autoExtraComponents" ] "Components are now parsed from services.home-assistant.config unconditionally")
+    (mkRenamedOptionModule [ "services" "home-assistant" "port" ] [ "services" "home-assistant" "config" "http" "server_port" ])
+  ];
 
-  # If you are changing this, please update the description in applyDefaultConfig
-  defaultConfig = {
-    homeassistant.time_zone = config.time.timeZone;
-    http.server_port = cfg.port;
-  } // optionalAttrs (cfg.lovelaceConfig != null) {
-    lovelace.mode = "yaml";
+  meta = {
+    buildDocsInSandbox = false;
+    maintainers = teams.home-assistant.members;
   };
 
-in {
-  meta.maintainers = teams.home-assistant.members;
-
   options.services.home-assistant = {
     # Running home-assistant on NixOS is considered an installation method that is unsupported by the upstream project.
     # https://github.com/home-assistant/architecture/blob/master/adr/0012-define-supported-installation-method.md#decision
@@ -81,42 +85,166 @@ in {
       description = "The config directory, where your <filename>configuration.yaml</filename> is located.";
     };
 
-    port = mkOption {
-      default = 8123;
-      type = types.port;
-      description = "The port on which to listen.";
+    extraComponents = mkOption {
+      type = types.listOf (types.enum availableComponents);
+      default = [
+        # List of components required to complete the onboarding
+        "default_config"
+        "met"
+        "esphome"
+      ] ++ optionals (pkgs.stdenv.hostPlatform.isAarch32 || pkgs.stdenv.hostPlatform.isAarch64) [
+        # Use the platform as an indicator that we might be running on a RaspberryPi and include
+        # relevant components
+        "rpi_power"
+      ];
+      example = literalExpression ''
+        [
+          "analytics"
+          "default_config"
+          "esphome"
+          "my"
+          "shopping_list"
+          "wled"
+        ]
+      '';
+      description = ''
+        List of <link xlink:href="https://www.home-assistant.io/integrations/">components</link> that have their dependencies included in the package.
+
+        The component name can be found in the URL, for example <literal>https://www.home-assistant.io/integrations/ffmpeg/</literal> would map to <literal>ffmpeg</literal>.
+      '';
     };
 
-    applyDefaultConfig = mkOption {
-      default = true;
-      type = types.bool;
+    extraPackages = mkOption {
+      type = types.functionTo (types.listOf types.package);
+      default = _: [];
+      defaultText = literalExpression ''
+        python3Packages: with python3Packages; [];
+      '';
+      example = literalExpression ''
+        python3Packages: with python3Packages; [
+          # postgresql support
+          psycopg2
+        ];
+      '';
       description = ''
-        Setting this option enables a few configuration options for HA based on NixOS configuration (such as time zone) to avoid having to manually specify configuration we already have.
-        </para>
-        <para>
-        Currently one side effect of enabling this is that the <literal>http</literal> component will be enabled.
-        </para>
-        <para>
-        This only takes effect if <literal>config != null</literal> in order to ensure that a manually managed <filename>configuration.yaml</filename> is not overwritten.
+        List of packages to add to propagatedBuildInputs.
+
+        A popular example is <package>python3Packages.psycopg2</package>
+        for PostgreSQL support in the recorder component.
       '';
     };
 
     config = mkOption {
-      default = null;
-      # Migrate to new option types later: https://github.com/NixOS/nixpkgs/pull/75584
-      type =  with lib.types; let
-          valueType = nullOr (oneOf [
-            bool
-            int
-            float
-            str
-            (lazyAttrsOf valueType)
-            (listOf valueType)
-          ]) // {
-            description = "Yaml value";
-            emptyValue.value = {};
+      type = types.submodule {
+        freeformType = format.type;
+        options = {
+          # This is a partial selection of the most common options, so new users can quickly
+          # pick up how to match home-assistants config structure to ours. It also lets us preset
+          # config values intelligently.
+
+          homeassistant = {
+            # https://www.home-assistant.io/docs/configuration/basic/
+            name = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              example = "Home";
+              description = ''
+                Name of the location where Home Assistant is running.
+              '';
+            };
+
+            latitude = mkOption {
+              type = types.nullOr (types.either types.float types.str);
+              default = null;
+              example = 52.3;
+              description = ''
+                Latitude of your location required to calculate the time the sun rises and sets.
+              '';
+            };
+
+            longitude = mkOption {
+              type = types.nullOr (types.either types.float types.str);
+              default = null;
+              example = 4.9;
+              description = ''
+                Longitude of your location required to calculate the time the sun rises and sets.
+              '';
+            };
+
+            unit_system = mkOption {
+              type = types.nullOr (types.enum [ "metric" "imperial" ]);
+              default = null;
+              example = "metric";
+              description = ''
+                The unit system to use. This also sets temperature_unit, Celsius for Metric and Fahrenheit for Imperial.
+              '';
+            };
+
+            temperature_unit = mkOption {
+              type = types.nullOr (types.enum [ "C" "F" ]);
+              default = null;
+              example = "C";
+              description = ''
+                Override temperature unit set by unit_system. <literal>C</literal> for Celsius, <literal>F</literal> for Fahrenheit.
+              '';
+            };
+
+            time_zone = mkOption {
+              type = types.nullOr types.str;
+              default = config.time.timeZone or null;
+              defaultText = literalExpression ''
+                config.time.timeZone or null
+              '';
+              example = "Europe/Amsterdam";
+              description = ''
+                Pick your time zone from the column TZ of Wikipedia’s <link xlink:href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones">list of tz database time zones</link>.
+              '';
+            };
+          };
+
+          http = {
+            # https://www.home-assistant.io/integrations/http/
+            server_host = mkOption {
+              type = types.either types.str (types.listOf types.str);
+              default = [
+                "0.0.0.0"
+                "::"
+              ];
+              example = "::1";
+              description = ''
+                Only listen to incoming requests on specific IP/host. The default listed assumes support for IPv4 and IPv6.
+              '';
+            };
+
+            server_port = mkOption {
+              default = 8123;
+              type = types.port;
+              description = ''
+                The port on which to listen.
+              '';
+            };
           };
-        in valueType;
+
+          lovelace = {
+            # https://www.home-assistant.io/lovelace/dashboards/
+            mode = mkOption {
+              type = types.enum [ "yaml" "storage" ];
+              default = if cfg.lovelaceConfig != null
+                then "yaml"
+                else "storage";
+              defaultText = literalExpression ''
+                if cfg.lovelaceConfig != null
+                  then "yaml"
+                else "storage";
+              '';
+              example = "yaml";
+              description = ''
+                In what mode should the main Lovelace panel be, <literal>yaml</literal> or <literal>storage</literal> (UI managed).
+              '';
+            };
+          };
+        };
+      };
       example = literalExpression ''
         {
           homeassistant = {
@@ -130,15 +258,19 @@ in {
           frontend = {
             themes = "!include_dir_merge_named themes";
           };
-          http = { };
+          http = {};
           feedreader.urls = [ "https://nixos.org/blogs.xml" ];
         }
       '';
       description = ''
         Your <filename>configuration.yaml</filename> as a Nix attribute set.
-        Beware that setting this option will delete your previous <filename>configuration.yaml</filename>.
-        <link xlink:href="https://www.home-assistant.io/docs/configuration/secrets/">Secrets</link>
-        are encoded as strings as shown in the example.
+
+        YAML functions like <link xlink:href="https://www.home-assistant.io/docs/configuration/secrets/">secrets</link>
+        can be passed as a string and will be unquoted automatically.
+
+        Unless this option is explicitly set to <literal>null</literal>
+        we assume your <filename>configuration.yaml</filename> is
+        managed through this module and thereby overwritten on startup.
       '';
     };
 
@@ -147,16 +279,18 @@ in {
       type = types.bool;
       description = ''
         Whether to make <filename>configuration.yaml</filename> writable.
-        This only has an effect if <option>config</option> is set.
+
         This will allow you to edit it from Home Assistant's web interface.
+
+        This only has an effect if <option>config</option> is set.
         However, bear in mind that it will be overwritten at every start of the service.
       '';
     };
 
     lovelaceConfig = mkOption {
       default = null;
-      type = with types; nullOr attrs;
-      # from https://www.home-assistant.io/lovelace/yaml-mode/
+      type = types.nullOr format.type;
+      # from https://www.home-assistant.io/lovelace/dashboards/
       example = literalExpression ''
         {
           title = "My Awesome Home";
@@ -172,8 +306,8 @@ in {
       '';
       description = ''
         Your <filename>ui-lovelace.yaml</filename> as a Nix attribute set.
-        Setting this option will automatically add
-        <literal>lovelace.mode = "yaml";</literal> to your <option>config</option>.
+        Setting this option will automatically set <literal>lovelace.mode</literal> to <literal>yaml</literal>.
+
         Beware that setting this option will delete your previous <filename>ui-lovelace.yaml</filename>
       '';
     };
@@ -183,8 +317,10 @@ in {
       type = types.bool;
       description = ''
         Whether to make <filename>ui-lovelace.yaml</filename> writable.
-        This only has an effect if <option>lovelaceConfig</option> is set.
+
         This will allow you to edit it from Home Assistant's web interface.
+
+        This only has an effect if <option>lovelaceConfig</option> is set.
         However, bear in mind that it will be overwritten at every start of the service.
       '';
     };
@@ -201,11 +337,18 @@ in {
       type = types.package;
       example = literalExpression ''
         pkgs.home-assistant.override {
-          extraPackages = ps: with ps; [ colorlog ];
+          extraPackages = python3Packages: with python3Packages; [
+            psycopg2
+          ];
+          extraComponents = [
+            "default_config"
+            "esphome"
+            "met"
+          ];
         }
       '';
       description = ''
-        Home Assistant package to use. By default the tests are disabled, as they take a considerable amout of time to complete.
+        The Home Assistant package to use.
         Override <literal>extraPackages</literal> or <literal>extraComponents</literal> in order to add additional dependencies.
         If you specify <option>config</option> and do not set <option>autoExtraComponents</option>
         to <literal>false</literal>, overriding <literal>extraComponents</literal> will have no effect.
@@ -213,21 +356,6 @@ in {
       '';
     };
 
-    autoExtraComponents = mkOption {
-      default = true;
-      type = types.bool;
-      description = ''
-        If set to <literal>true</literal>, the components used in <literal>config</literal>
-        are set as the specified package's <literal>extraComponents</literal>.
-        This in turn adds all packaged dependencies to the derivation.
-        You might still see import errors in your log.
-        In this case, you will need to package the necessary dependencies yourself
-        or ask for someone else to package them.
-        If a dependency is packaged but not automatically added to this list,
-        you might need to specify it in <literal>extraPackages</literal>.
-      '';
-    };
-
     openFirewall = mkOption {
       default = false;
       type = types.bool;
@@ -240,18 +368,30 @@ in {
 
     systemd.services.home-assistant = {
       description = "Home Assistant";
-      after = [ "network.target" ];
-      preStart = optionalString (cfg.config != null) (if cfg.configWritable then ''
-        cp --no-preserve=mode ${configFile} "${cfg.configDir}/configuration.yaml"
-      '' else ''
-        rm -f "${cfg.configDir}/configuration.yaml"
-        ln -s ${configFile} "${cfg.configDir}/configuration.yaml"
-      '') + optionalString (cfg.lovelaceConfig != null) (if cfg.lovelaceConfigWritable then ''
-        cp --no-preserve=mode ${lovelaceConfigFile} "${cfg.configDir}/ui-lovelace.yaml"
-      '' else ''
-        rm -f "${cfg.configDir}/ui-lovelace.yaml"
-        ln -s ${lovelaceConfigFile} "${cfg.configDir}/ui-lovelace.yaml"
-      '');
+      after = [
+        "network-online.target"
+
+        # prevent races with database creation
+        "mysql.service"
+        "postgresql.service"
+      ];
+      preStart = let
+        copyConfig = if cfg.configWritable then ''
+          cp --no-preserve=mode ${configFile} "${cfg.configDir}/configuration.yaml"
+        '' else ''
+          rm -f "${cfg.configDir}/configuration.yaml"
+          ln -s ${configFile} "${cfg.configDir}/configuration.yaml"
+        '';
+        copyLovelaceConfig = if cfg.lovelaceConfigWritable then ''
+          cp --no-preserve=mode ${lovelaceConfigFile} "${cfg.configDir}/ui-lovelace.yaml"
+        '' else ''
+          rm -f "${cfg.configDir}/ui-lovelace.yaml"
+          ln -s ${lovelaceConfigFile} "${cfg.configDir}/ui-lovelace.yaml"
+        '';
+      in
+        (optionalString (cfg.config != null) copyConfig) +
+        (optionalString (cfg.lovelaceConfig != null) copyLovelaceConfig)
+      ;
       serviceConfig = let
         # List of capabilities to equip home-assistant with, depending on configured components
         capabilities = [
@@ -329,7 +469,7 @@ in {
           "zwave_js"
         ];
       in {
-        ExecStart = "${package}/bin/hass --runner --config '${cfg.configDir}'";
+        ExecStart = "${package}/bin/hass --config '${cfg.configDir}'";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
         User = "hass";
         Group = "hass";
diff --git a/nixos/modules/services/matrix/mjolnir.xml b/nixos/modules/services/matrix/mjolnir.xml
index d462ddf7b01..b07abe33979 100644
--- a/nixos/modules/services/matrix/mjolnir.xml
+++ b/nixos/modules/services/matrix/mjolnir.xml
@@ -98,7 +98,7 @@
   </para>
   <para>
    To use the Antispam Module, add <package>matrix-synapse-plugins.matrix-synapse-mjolnir-antispam</package>
-   to the Synapse plugin list and enable the <literal>mjolnir.AntiSpam</literal> module.
+   to the Synapse plugin list and enable the <literal>mjolnir.Module</literal> module.
   </para>
 <programlisting>
 {
@@ -108,7 +108,7 @@
     ];
     extraConfig = ''
       modules:
-        - module: mjolnir.AntiSpam
+        - module: mjolnir.Module
           config:
             # Prevent servers/users in the ban lists from inviting users on this
             # server to rooms. Default true.
diff --git a/nixos/modules/services/misc/airsonic.nix b/nixos/modules/services/misc/airsonic.nix
index 5a5c30a4123..2b9c6d80abb 100644
--- a/nixos/modules/services/misc/airsonic.nix
+++ b/nixos/modules/services/misc/airsonic.nix
@@ -39,9 +39,11 @@ in {
         default = "127.0.0.1";
         description = ''
           The host name or IP address on which to bind Airsonic.
-          Only relevant if you have multiple network interfaces and want
-          to make Airsonic available on only one of them. The default value
-          will bind Airsonic to all available network interfaces.
+          The default value is appropriate for first launch, when the
+          default credentials are easy to guess. It is also appropriate
+          if you intend to use the virtualhost option in the service
+          module. In other cases, you may want to change this to a
+          specific IP or 0.0.0.0 to listen on all interfaces.
         '';
       };
 
diff --git a/nixos/modules/services/misc/autorandr.nix b/nixos/modules/services/misc/autorandr.nix
index 95cee5046e8..a65c5c9d11c 100644
--- a/nixos/modules/services/misc/autorandr.nix
+++ b/nixos/modules/services/misc/autorandr.nix
@@ -43,6 +43,7 @@ in {
         ExecStart = "${pkgs.autorandr}/bin/autorandr --batch --change --default ${cfg.defaultTarget}";
         Type = "oneshot";
         RemainAfterExit = false;
+        KillMode = "process";
       };
     };
 
diff --git a/nixos/modules/services/misc/gitlab.nix b/nixos/modules/services/misc/gitlab.nix
index 6f6a9e3110a..e48444f7161 100644
--- a/nixos/modules/services/misc/gitlab.nix
+++ b/nixos/modules/services/misc/gitlab.nix
@@ -72,7 +72,7 @@ let
     redis = {
       bin = "${pkgs.redis}/bin/redis-cli";
       host = "127.0.0.1";
-      port = 6379;
+      port = config.services.redis.servers.gitlab.port;
       database = 0;
       namespace = "resque:gitlab";
     };
@@ -450,7 +450,8 @@ in {
 
       redisUrl = mkOption {
         type = types.str;
-        default = "redis://localhost:6379/";
+        default = "redis://localhost:${toString config.services.redis.servers.gitlab.port}/";
+        defaultText = literalExpression ''redis://localhost:''${toString config.services.redis.servers.gitlab.port}/'';
         description = "Redis URL for all GitLab services except gitlab-shell";
       };
 
@@ -961,7 +962,11 @@ in {
     };
 
     # Redis is required for the sidekiq queue runner.
-    services.redis.enable = mkDefault true;
+    services.redis.servers.gitlab = {
+      enable = mkDefault true;
+      port = mkDefault 31636;
+      bind = mkDefault "127.0.0.1";
+    };
 
     # We use postgres as the main data store.
     services.postgresql = optionalAttrs databaseActuallyCreateLocally {
@@ -1131,8 +1136,8 @@ in {
 
         ExecStartPre = let
           preStartFullPrivileges = ''
-            shopt -s dotglob nullglob
-            set -eu
+            set -o errexit -o pipefail -o nounset
+            shopt -s dotglob nullglob inherit_errexit
 
             chown --no-dereference '${cfg.user}':'${cfg.group}' '${cfg.statePath}'/*
             if [[ -n "$(ls -A '${cfg.statePath}'/config/)" ]]; then
@@ -1142,7 +1147,8 @@ in {
         in "+${pkgs.writeShellScript "gitlab-pre-start-full-privileges" preStartFullPrivileges}";
 
         ExecStart = pkgs.writeShellScript "gitlab-config" ''
-          set -eu
+          set -o errexit -o pipefail -o nounset
+          shopt -s inherit_errexit
 
           umask u=rwx,g=rx,o=
 
@@ -1171,7 +1177,8 @@ in {
             rm -f '${cfg.statePath}/config/database.yml'
 
             ${if cfg.databasePasswordFile != null then ''
-                export db_password="$(<'${cfg.databasePasswordFile}')"
+                db_password="$(<'${cfg.databasePasswordFile}')"
+                export db_password
 
                 if [[ -z "$db_password" ]]; then
                   >&2 echo "Database password was an empty string!"
@@ -1195,10 +1202,11 @@ in {
 
             rm -f '${cfg.statePath}/config/secrets.yml'
 
-            export secret="$(<'${cfg.secrets.secretFile}')"
-            export db="$(<'${cfg.secrets.dbFile}')"
-            export otp="$(<'${cfg.secrets.otpFile}')"
-            export jws="$(<'${cfg.secrets.jwsFile}')"
+            secret="$(<'${cfg.secrets.secretFile}')"
+            db="$(<'${cfg.secrets.dbFile}')"
+            otp="$(<'${cfg.secrets.otpFile}')"
+            jws="$(<'${cfg.secrets.jwsFile}')"
+            export secret db otp jws
             jq -n '{production: {secret_key_base: $ENV.secret,
                     otp_key_base: $ENV.otp,
                     db_key_base: $ENV.db,
@@ -1232,7 +1240,8 @@ in {
         RemainAfterExit = true;
 
         ExecStart = pkgs.writeShellScript "gitlab-db-config" ''
-          set -eu
+          set -o errexit -o pipefail -o nounset
+          shopt -s inherit_errexit
           umask u=rwx,g=rx,o=
 
           initial_root_password="$(<'${cfg.initialRootPasswordFile}')"
@@ -1245,13 +1254,13 @@ in {
     systemd.services.gitlab-sidekiq = {
       after = [
         "network.target"
-        "redis.service"
+        "redis-gitlab.service"
         "postgresql.service"
         "gitlab-config.service"
         "gitlab-db-config.service"
       ];
       bindsTo = [
-        "redis.service"
+        "redis-gitlab.service"
         "gitlab-config.service"
         "gitlab-db-config.service"
       ] ++ optional (cfg.databaseHost == "") "postgresql.service";
@@ -1366,7 +1375,7 @@ in {
 
     systemd.services.gitlab-mailroom = mkIf (gitlabConfig.production.incoming_email.enabled or false) {
       description = "GitLab incoming mail daemon";
-      after = [ "network.target" "redis.service" "gitlab-config.service" ];
+      after = [ "network.target" "redis-gitlab.service" "gitlab-config.service" ];
       bindsTo = [ "gitlab-config.service" ];
       wantedBy = [ "gitlab.target" ];
       partOf = [ "gitlab.target" ];
@@ -1387,12 +1396,12 @@ in {
       after = [
         "gitlab-workhorse.service"
         "network.target"
-        "redis.service"
+        "redis-gitlab.service"
         "gitlab-config.service"
         "gitlab-db-config.service"
       ];
       bindsTo = [
-        "redis.service"
+        "redis-gitlab.service"
         "gitlab-config.service"
         "gitlab-db-config.service"
       ] ++ optional (cfg.databaseHost == "") "postgresql.service";
diff --git a/nixos/modules/services/misc/input-remapper.nix b/nixos/modules/services/misc/input-remapper.nix
new file mode 100644
index 00000000000..c2da0d616a3
--- /dev/null
+++ b/nixos/modules/services/misc/input-remapper.nix
@@ -0,0 +1,29 @@
+{ pkgs, lib, config, ... }:
+
+with lib;
+
+let cfg = config.services.input-remapper; in
+{
+  options = {
+    services.input-remapper = {
+      enable = mkEnableOption "input-remapper, an easy to use tool to change the mapping of your input device buttons.";
+      package = mkOption {
+        type = types.package;
+        default = pkgs.input-remapper;
+        defaultText = literalExpression "pkgs.input-remapper";
+        description = ''
+          The input-remapper package to use.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    # FIXME: udev rule hangs sometimes when lots of devices connected, so let's not use it
+    # config.services.udev.packages = mapper-pkg;
+    services.dbus.packages = cfg.package;
+    systemd.packages = cfg.package;
+    environment.systemPackages = cfg.package;
+    systemd.services.input-remapper.wantedBy = [ "graphical.target" ];
+  };
+}
diff --git a/nixos/modules/services/misc/matrix-conduit.nix b/nixos/modules/services/misc/matrix-conduit.nix
index d6cd575ee9a..108f64de7aa 100644
--- a/nixos/modules/services/misc/matrix-conduit.nix
+++ b/nixos/modules/services/misc/matrix-conduit.nix
@@ -86,6 +86,15 @@ in
                 and is set to be read only.
               '';
             };
+            global.database_backend = mkOption {
+              type = types.enum [ "sqlite" "rocksdb" ];
+              default = "sqlite";
+              example = "rocksdb";
+              description = ''
+                The database backend for the service. Switching it on an existing
+                instance will require manual migration of data.
+              '';
+            };
           };
         };
         default = {};
diff --git a/nixos/modules/services/misc/mediatomb.nix b/nixos/modules/services/misc/mediatomb.nix
index ea9ffbb8677..ee5c0ef8d27 100644
--- a/nixos/modules/services/misc/mediatomb.nix
+++ b/nixos/modules/services/misc/mediatomb.nix
@@ -217,7 +217,6 @@ in {
 
       package = mkOption {
         type = types.package;
-        example = literalExpression "pkgs.mediatomb";
         default = pkgs.gerbera;
         defaultText = literalExpression "pkgs.gerbera";
         description = ''
@@ -367,6 +366,7 @@ in {
       wantedBy = [ "multi-user.target" ];
       serviceConfig.ExecStart = "${binaryCommand} --port ${toString cfg.port} ${interfaceFlag} ${configFlag} --home ${cfg.dataDir}";
       serviceConfig.User = cfg.user;
+      serviceConfig.Group = cfg.group;
     };
 
     users.groups = optionalAttrs (cfg.group == "mediatomb") {
diff --git a/nixos/modules/services/misc/nix-daemon.nix b/nixos/modules/services/misc/nix-daemon.nix
index ea57292fc4b..a401458c416 100644
--- a/nixos/modules/services/misc/nix-daemon.nix
+++ b/nixos/modules/services/misc/nix-daemon.nix
@@ -680,7 +680,7 @@ in
               (if machine.sshKey != null then machine.sshKey else "-")
               (toString machine.maxJobs)
               (toString machine.speedFactor)
-              (concatStringsSep "," machine.supportedFeatures)
+              (concatStringsSep "," (machine.supportedFeatures ++ machine.mandatoryFeatures))
               (concatStringsSep "," machine.mandatoryFeatures)
             ]
             ++ optional (isNixAtLeast "2.4pre") (if machine.publicHostKey != null then machine.publicHostKey else "-")))
diff --git a/nixos/modules/services/misc/packagekit.nix b/nixos/modules/services/misc/packagekit.nix
index 93bd206bd98..9191078ef9c 100644
--- a/nixos/modules/services/misc/packagekit.nix
+++ b/nixos/modules/services/misc/packagekit.nix
@@ -13,7 +13,7 @@ let
     (iniFmt.generate "PackageKit.conf" (recursiveUpdate
       {
         Daemon = {
-          DefaultBackend = "test_nop";
+          DefaultBackend = "nix";
           KeepCache = false;
         };
       }
@@ -35,7 +35,7 @@ let
 in
 {
   imports = [
-    (mkRemovedOptionModule [ "services" "packagekit" "backend" ] "The only backend that doesn't blow up is `test_nop`.")
+    (mkRemovedOptionModule [ "services" "packagekit" "backend" ] "Always set to Nix.")
   ];
 
   options.services.packagekit = {
@@ -62,6 +62,8 @@ in
 
     services.dbus.packages = with pkgs; [ packagekit ];
 
+    environment.systemPackages = with pkgs; [ packagekit ];
+
     systemd.packages = with pkgs; [ packagekit ];
 
     environment.etc = listToAttrs (map
diff --git a/nixos/modules/services/misc/paperless-ng.nix b/nixos/modules/services/misc/paperless-ng.nix
index db8082f072c..44efc234a2b 100644
--- a/nixos/modules/services/misc/paperless-ng.nix
+++ b/nixos/modules/services/misc/paperless-ng.nix
@@ -6,12 +6,18 @@ let
 
   defaultUser = "paperless";
 
+  hasCustomRedis = hasAttr "PAPERLESS_REDIS" cfg.extraConfig;
+
   env = {
     PAPERLESS_DATA_DIR = cfg.dataDir;
     PAPERLESS_MEDIA_ROOT = cfg.mediaDir;
     PAPERLESS_CONSUMPTION_DIR = cfg.consumptionDir;
     GUNICORN_CMD_ARGS = "--bind=${cfg.address}:${toString cfg.port}";
-  } // lib.mapAttrs (_: toString) cfg.extraConfig;
+  } // (
+    lib.mapAttrs (_: toString) cfg.extraConfig
+  ) // (optionalAttrs (!hasCustomRedis) {
+    PAPERLESS_REDIS = "unix://${config.services.redis.servers.paperless-ng.unixSocket}";
+  });
 
   manage = let
     setupEnv = lib.concatStringsSep "\n" (mapAttrsToList (name: val: "export ${name}=\"${val}\"") env);
@@ -30,7 +36,7 @@ let
       "-/etc/hosts"
       "-/etc/localtime"
       "-/run/postgresql"
-    ];
+    ] ++ (optional (!hasCustomRedis) config.services.redis.servers.paperless-ng.unixSocket);
     BindPaths = [
       cfg.consumptionDir
       cfg.dataDir
@@ -44,8 +50,7 @@ let
     NoNewPrivileges = true;
     PrivateDevices = true;
     PrivateMounts = true;
-    # Needs to connect to redis
-    # PrivateNetwork = true;
+    PrivateNetwork = true;
     PrivateTmp = true;
     PrivateUsers = true;
     ProcSubset = "pid";
@@ -65,6 +70,7 @@ let
     RestrictNamespaces = true;
     RestrictRealtime = true;
     RestrictSUIDSGID = true;
+    SupplementaryGroups = optional (!hasCustomRedis) config.services.redis.servers.paperless-ng.user;
     SystemCallArchitectures = "native";
     SystemCallFilter = [ "@system-service" "~@privileged @resources @setuid @keyring" ];
     # Does not work well with the temporary root
@@ -190,7 +196,7 @@ in
 
   config = mkIf cfg.enable {
     # Enable redis if no special url is set
-    services.redis.enable = mkIf (!hasAttr "PAPERLESS_REDIS" env) true;
+    services.redis.servers.paperless-ng.enable = mkIf (!hasCustomRedis) true;
 
     systemd.tmpfiles.rules = [
       "d '${cfg.dataDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
@@ -234,6 +240,8 @@ in
           echo "$superuserState" > "$superuserStateFile"
         fi
       '';
+    } // optionalAttrs (!hasCustomRedis) {
+      after = [ "redis-paperless-ng.service" ];
     };
 
     # Password copying can't be implemented as a privileged preStart script
@@ -248,6 +256,8 @@ in
             '${cfg.passwordFile}' '${cfg.dataDir}/superuser-password'
         '';
         Type = "oneshot";
+        # Needs to talk to mail server for automated import rules
+        PrivateNetwork = false;
       };
     };
 
@@ -279,6 +289,8 @@ in
         CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
         # gunicorn needs setuid
         SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "@setuid" ];
+        # Needs to serve web page
+        PrivateNetwork = false;
       };
       environment = env // {
         PATH = mkForce cfg.package.path;
diff --git a/nixos/modules/services/misc/plex.nix b/nixos/modules/services/misc/plex.nix
index 2ae4e80d5c3..7000d45975f 100644
--- a/nixos/modules/services/misc/plex.nix
+++ b/nixos/modules/services/misc/plex.nix
@@ -6,6 +6,10 @@ let
   cfg = config.services.plex;
 in
 {
+  imports = [
+    (mkRemovedOptionModule [ "services" "plex" "managePlugins" ] "Please omit or define the option: `services.plex.extraPlugins' instead.")
+  ];
+
   options = {
     services.plex = {
       enable = mkEnableOption "Plex Media Server";
@@ -42,16 +46,6 @@ in
         '';
       };
 
-      managePlugins = mkOption {
-        type = types.bool;
-        default = true;
-        description = ''
-          If set to true, this option will cause all of the symlinks in Plex's
-          plugin directory to be removed and symlinks for paths specified in
-          <option>extraPlugins</option> to be added.
-        '';
-      };
-
       extraPlugins = mkOption {
         type = types.listOf types.path;
         default = [];
@@ -59,9 +53,7 @@ in
           A list of paths to extra plugin bundles to install in Plex's plugin
           directory. Every time the systemd unit for Plex starts up, all of the
           symlinks in Plex's plugin directory will be cleared and this module
-          will symlink all of the paths specified here to that directory. If
-          this behavior is undesired, set <option>managePlugins</option> to
-          false.
+          will symlink all of the paths specified here to that directory.
         '';
       };
 
diff --git a/nixos/modules/services/misc/rmfakecloud.nix b/nixos/modules/services/misc/rmfakecloud.nix
new file mode 100644
index 00000000000..fe522653c21
--- /dev/null
+++ b/nixos/modules/services/misc/rmfakecloud.nix
@@ -0,0 +1,147 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.rmfakecloud;
+  serviceDataDir = "/var/lib/rmfakecloud";
+
+in {
+  options = {
+    services.rmfakecloud = {
+      enable = mkEnableOption "rmfakecloud remarkable self-hosted cloud";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.rmfakecloud;
+        defaultText = literalExpression "pkgs.rmfakecloud";
+        description = ''
+          rmfakecloud package to use.
+
+          The default does not include the web user interface.
+        '';
+      };
+
+      storageUrl = mkOption {
+        type = types.str;
+        example = "https://local.appspot.com";
+        description = ''
+          URL used by the tablet to access the rmfakecloud service.
+        '';
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 3000;
+        description = ''
+          Listening port number.
+        '';
+      };
+
+      logLevel = mkOption {
+        type = types.enum [ "info" "debug" "warn" "error" ];
+        default = "info";
+        description = ''
+          Logging level.
+        '';
+      };
+
+      extraSettings = mkOption {
+        type = with types; attrsOf str;
+        default = { };
+        example = { DATADIR = "/custom/path/for/rmfakecloud/data"; };
+        description = ''
+          Extra settings in the form of a set of key-value pairs.
+          For tokens and secrets, use `environmentFile` instead.
+
+          Available settings are listed on
+          https://ddvk.github.io/rmfakecloud/install/configuration/.
+        '';
+      };
+
+      environmentFile = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        example = "/etc/secrets/rmfakecloud.env";
+        description = ''
+          Path to an environment file loaded for the rmfakecloud service.
+
+          This can be used to securely store tokens and secrets outside of the
+          world-readable Nix store. Since this file is read by systemd, it may
+          have permission 0400 and be owned by root.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.rmfakecloud = {
+      description = "rmfakecloud remarkable self-hosted cloud";
+
+      environment = {
+        STORAGE_URL = cfg.storageUrl;
+        PORT = toString cfg.port;
+        LOGLEVEL = cfg.logLevel;
+      } // cfg.extraSettings;
+
+      preStart = ''
+        # Generate the secret key used to sign client session tokens.
+        # Replacing it invalidates the previously established sessions.
+        if [ -z "$JWT_SECRET_KEY" ] && [ ! -f jwt_secret_key ]; then
+          (umask 077; touch jwt_secret_key)
+          cat /dev/urandom | tr -cd '[:alnum:]' | head -c 48 >> jwt_secret_key
+        fi
+      '';
+
+      script = ''
+        if [ -z "$JWT_SECRET_KEY" ]; then
+          export JWT_SECRET_KEY="$(cat jwt_secret_key)"
+        fi
+
+        ${cfg.package}/bin/rmfakecloud
+      '';
+
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
+      after = [ "network-online.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        Restart = "always";
+
+        EnvironmentFile =
+          mkIf (cfg.environmentFile != null) cfg.environmentFile;
+
+        AmbientCapabilities =
+          mkIf (cfg.port < 1024) [ "CAP_NET_BIND_SERVICE" ];
+
+        DynamicUser = true;
+        PrivateDevices = true;
+        ProtectHome = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        CapabilityBoundingSet = [ "" ];
+        DevicePolicy = "closed";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        ProtectClock = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectProc = "invisible";
+        ProcSubset = "pid";
+        RemoveIPC = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        WorkingDirectory = serviceDataDir;
+        StateDirectory = baseNameOf serviceDataDir;
+        UMask = 0027;
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ pacien ];
+}
diff --git a/nixos/modules/services/network-filesystems/ceph.nix b/nixos/modules/services/network-filesystems/ceph.nix
index e313589134f..7a1444decaf 100644
--- a/nixos/modules/services/network-filesystems/ceph.nix
+++ b/nixos/modules/services/network-filesystems/ceph.nix
@@ -181,8 +181,8 @@ in
 
       rgwMimeTypesFile = mkOption {
         type = with types; nullOr path;
-        default = "${pkgs.mime-types}/etc/mime.types";
-        defaultText = literalExpression ''"''${pkgs.mime-types}/etc/mime.types"'';
+        default = "${pkgs.mailcap}/etc/mime.types";
+        defaultText = literalExpression ''"''${pkgs.mailcap}/etc/mime.types"'';
         description = ''
           Path to mime types used by radosgw.
         '';
diff --git a/nixos/modules/services/network-filesystems/ipfs.nix b/nixos/modules/services/network-filesystems/ipfs.nix
index 5482b2aaf88..b311b91b4a0 100644
--- a/nixos/modules/services/network-filesystems/ipfs.nix
+++ b/nixos/modules/services/network-filesystems/ipfs.nix
@@ -260,24 +260,17 @@ in
         ipfs --offline config Mounts.IPNS ${cfg.ipnsMountDir}
       '' + optionalString cfg.autoMigrate ''
         ${pkgs.ipfs-migrator}/bin/fs-repo-migrations -y
-      '' + concatStringsSep "\n" (collect
-        isString
-        (mapAttrsRecursive
-          (path: value:
-            # Using heredoc below so that the value is never improperly quoted
-            ''
-              read value <<EOF
-              ${builtins.toJSON value}
-              EOF
-              ipfs --offline config --json "${concatStringsSep "." path}" "$value"
-            '')
-          ({
-            Addresses.API = cfg.apiAddress;
-            Addresses.Gateway = cfg.gatewayAddress;
-            Addresses.Swarm = cfg.swarmAddress;
-          } //
-          cfg.extraConfig))
-      );
+      '' + ''
+        ipfs --offline config show \
+          | ${pkgs.jq}/bin/jq '. * $extraConfig' --argjson extraConfig ${
+              escapeShellArg (builtins.toJSON ({
+                Addresses.API = cfg.apiAddress;
+                Addresses.Gateway = cfg.gatewayAddress;
+                Addresses.Swarm = cfg.swarmAddress;
+              } // cfg.extraConfig))
+            } \
+          | ipfs --offline config replace -
+      '';
       serviceConfig = {
         ExecStart = [ "" "${cfg.package}/bin/ipfs daemon ${ipfsFlags}" ];
         User = cfg.user;
diff --git a/nixos/modules/services/network-filesystems/moosefs.nix b/nixos/modules/services/network-filesystems/moosefs.nix
new file mode 100644
index 00000000000..88b2ada37e7
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/moosefs.nix
@@ -0,0 +1,249 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.moosefs;
+
+  mfsUser = if cfg.runAsUser then "moosefs" else "root";
+
+  settingsFormat = let
+    listSep = " ";
+    allowedTypes = with types; [ bool int float str ];
+    valueToString = val:
+        if isList val then concatStringsSep listSep (map (x: valueToString x) val)
+        else if isBool val then (if val then "1" else "0")
+        else toString val;
+
+    in {
+      type = with types; let
+        valueType = oneOf ([
+          (listOf valueType)
+        ] ++ allowedTypes) // {
+          description = "Flat key-value file";
+        };
+      in attrsOf valueType;
+
+      generate = name: value:
+        pkgs.writeText name ( lib.concatStringsSep "\n" (
+          lib.mapAttrsToList (key: val: "${key} = ${valueToString val}") value ));
+    };
+
+
+  initTool = pkgs.writeShellScriptBin "mfsmaster-init" ''
+    if [ ! -e ${cfg.master.settings.DATA_PATH}/metadata.mfs ]; then
+      cp ${pkgs.moosefs}/var/mfs/metadata.mfs.empty ${cfg.master.settings.DATA_PATH}
+      chmod +w ${cfg.master.settings.DATA_PATH}/metadata.mfs.empty
+      ${pkgs.moosefs}/bin/mfsmaster -a -c ${masterCfg} start
+      ${pkgs.moosefs}/bin/mfsmaster -c ${masterCfg} stop
+      rm ${cfg.master.settings.DATA_PATH}/metadata.mfs.empty
+    fi
+  '';
+
+  # master config file
+  masterCfg = settingsFormat.generate
+    "mfsmaster.cfg" cfg.master.settings;
+
+  # metalogger config file
+  metaloggerCfg = settingsFormat.generate
+    "mfsmetalogger.cfg" cfg.metalogger.settings;
+
+  # chunkserver config file
+  chunkserverCfg = settingsFormat.generate
+    "mfschunkserver.cfg" cfg.chunkserver.settings;
+
+  # generic template for all deamons
+  systemdService = name: extraConfig: configFile: {
+    wantedBy = [ "multi-user.target" ];
+    wants = [ "network-online.target" ];
+    after = [ "network.target" "network-online.target" ];
+
+    serviceConfig = {
+      Type = "forking";
+      ExecStart  = "${pkgs.moosefs}/bin/mfs${name} -c ${configFile} start";
+      ExecStop   = "${pkgs.moosefs}/bin/mfs${name} -c ${configFile} stop";
+      ExecReload = "${pkgs.moosefs}/bin/mfs${name} -c ${configFile} reload";
+      PIDFile = "${cfg."${name}".settings.DATA_PATH}/.mfs${name}.lock";
+    } // extraConfig;
+  };
+
+in {
+  ###### interface
+
+  options = {
+    services.moosefs = {
+      masterHost = mkOption {
+        type = types.str;
+        default = null;
+        description = "IP or DNS name of master host.";
+      };
+
+      runAsUser = mkOption {
+        type = types.bool;
+        default = true;
+        example = true;
+        description = "Run daemons as user moosefs instead of root.";
+      };
+
+      client.enable = mkEnableOption "Moosefs client.";
+
+      master = {
+        enable = mkOption {
+          type = types.bool;
+          description = ''
+            Enable Moosefs master daemon.
+
+            You need to run <literal>mfsmaster-init</literal> on a freshly installed master server to
+            initialize the <literal>DATA_PATH</literal> direcory.
+          '';
+          default = false;
+        };
+
+        exports = mkOption {
+          type = with types; listOf str;
+          default = null;
+          description = "Paths to export (see mfsexports.cfg).";
+          example = [
+            "* / rw,alldirs,admin,maproot=0:0"
+            "* . rw"
+          ];
+        };
+
+        openFirewall = mkOption {
+          type = types.bool;
+          description = "Whether to automatically open the necessary ports in the firewall.";
+          default = false;
+        };
+
+        settings = mkOption {
+          type = types.submodule {
+            freeformType = settingsFormat.type;
+
+            options.DATA_PATH = mkOption {
+              type = types.str;
+              default = "/var/lib/mfs";
+              description = "Data storage directory.";
+            };
+          };
+
+          description = "Contents of config file (mfsmaster.cfg).";
+        };
+      };
+
+      metalogger = {
+        enable = mkEnableOption "Moosefs metalogger daemon.";
+
+        settings = mkOption {
+          type = types.submodule {
+            freeformType = settingsFormat.type;
+
+            options.DATA_PATH = mkOption {
+              type = types.str;
+              default = "/var/lib/mfs";
+              description = "Data storage directory";
+            };
+          };
+
+          description = "Contents of metalogger config file (mfsmetalogger.cfg).";
+        };
+      };
+
+      chunkserver = {
+        enable = mkEnableOption "Moosefs chunkserver daemon.";
+
+        openFirewall = mkOption {
+          type = types.bool;
+          description = "Whether to automatically open the necessary ports in the firewall.";
+          default = false;
+        };
+
+        hdds = mkOption {
+          type = with types; listOf str;
+          default =  null;
+          description = "Mount points to be used by chunkserver for storage (see mfshdd.cfg).";
+          example = [ "/mnt/hdd1" ];
+        };
+
+        settings = mkOption {
+          type = types.submodule {
+            freeformType = settingsFormat.type;
+
+            options.DATA_PATH = mkOption {
+              type = types.str;
+              default = "/var/lib/mfs";
+              description = "Directory for lock file.";
+            };
+          };
+
+          description = "Contents of chunkserver config file (mfschunkserver.cfg).";
+        };
+      };
+    };
+  };
+
+  ###### implementation
+
+  config =  mkIf ( cfg.client.enable || cfg.master.enable || cfg.metalogger.enable || cfg.chunkserver.enable ) {
+
+    warnings = [ ( mkIf (!cfg.runAsUser) "Running moosefs services as root is not recommended.") ];
+
+    # Service settings
+    services.moosefs = {
+      master.settings = mkIf cfg.master.enable {
+        WORKING_USER = mfsUser;
+        EXPORTS_FILENAME = toString ( pkgs.writeText "mfsexports.cfg"
+          (concatStringsSep "\n" cfg.master.exports));
+      };
+
+      metalogger.settings = mkIf cfg.metalogger.enable {
+        WORKING_USER = mfsUser;
+        MASTER_HOST = cfg.masterHost;
+      };
+
+      chunkserver.settings = mkIf cfg.chunkserver.enable {
+        WORKING_USER = mfsUser;
+        MASTER_HOST = cfg.masterHost;
+        HDD_CONF_FILENAME = toString ( pkgs.writeText "mfshdd.cfg"
+          (concatStringsSep "\n" cfg.chunkserver.hdds));
+      };
+    };
+
+    # Create system user account for daemons
+    users = mkIf ( cfg.runAsUser && ( cfg.master.enable || cfg.metalogger.enable || cfg.chunkserver.enable ) ) {
+      users.moosefs = {
+        isSystemUser = true;
+        description = "moosefs daemon user";
+        group = "moosefs";
+      };
+      groups.moosefs = {};
+    };
+
+    environment.systemPackages =
+      (lib.optional cfg.client.enable pkgs.moosefs) ++
+      (lib.optional cfg.master.enable initTool);
+
+    networking.firewall.allowedTCPPorts =
+      (lib.optionals cfg.master.openFirewall [ 9419 9420 9421 ]) ++
+      (lib.optional cfg.chunkserver.openFirewall 9422);
+
+    # Ensure storage directories exist
+    systemd.tmpfiles.rules =
+         optional cfg.master.enable "d ${cfg.master.settings.DATA_PATH} 0700 ${mfsUser} ${mfsUser}"
+      ++ optional cfg.metalogger.enable "d ${cfg.metalogger.settings.DATA_PATH} 0700 ${mfsUser} ${mfsUser}"
+      ++ optional cfg.chunkserver.enable "d ${cfg.chunkserver.settings.DATA_PATH} 0700 ${mfsUser} ${mfsUser}";
+
+    # Service definitions
+    systemd.services.mfs-master = mkIf cfg.master.enable
+    ( systemdService "master" {
+      TimeoutStartSec = 1800;
+      TimeoutStopSec = 1800;
+      Restart = "no";
+    } masterCfg );
+
+    systemd.services.mfs-metalogger = mkIf cfg.metalogger.enable
+      ( systemdService "metalogger" { Restart = "on-abnormal"; } metaloggerCfg );
+
+    systemd.services.mfs-chunkserver = mkIf cfg.chunkserver.enable
+      ( systemdService "chunkserver" { Restart = "on-abnormal"; } chunkserverCfg );
+    };
+}
diff --git a/nixos/modules/services/network-filesystems/rsyncd.nix b/nixos/modules/services/network-filesystems/rsyncd.nix
index edac86eb0e3..e72f9b54cd6 100644
--- a/nixos/modules/services/network-filesystems/rsyncd.nix
+++ b/nixos/modules/services/network-filesystems/rsyncd.nix
@@ -79,7 +79,7 @@ in {
     in {
       services.rsync = {
         enable = !cfg.socketActivated;
-        aliases = [ "rsyncd" ];
+        aliases = [ "rsyncd.service" ];
 
         description = "fast remote file copy program daemon";
         after = [ "network.target" ];
diff --git a/nixos/modules/services/networking/blocky.nix b/nixos/modules/services/networking/blocky.nix
new file mode 100644
index 00000000000..7488e05fc03
--- /dev/null
+++ b/nixos/modules/services/networking/blocky.nix
@@ -0,0 +1,40 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.blocky;
+
+  format = pkgs.formats.yaml { };
+  configFile = format.generate "config.yaml" cfg.settings;
+in
+{
+  options.services.blocky = {
+    enable = mkEnableOption "Fast and lightweight DNS proxy as ad-blocker for local network with many features";
+
+    settings = mkOption {
+      type = format.type;
+      default = { };
+      description = ''
+        Blocky configuration. Refer to
+        <link xlink:href="https://0xerr0r.github.io/blocky/configuration/"/>
+        for details on supported values.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.blocky = {
+      description = "A DNS proxy and ad-blocker for the local network";
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        DynamicUser = true;
+        ExecStart = "${pkgs.blocky}/bin/blocky --config ${configFile}";
+
+        AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
+        CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/connman.nix b/nixos/modules/services/networking/connman.nix
index 8886e7a30f1..9945dc83a27 100644
--- a/nixos/modules/services/networking/connman.nix
+++ b/nixos/modules/services/networking/connman.nix
@@ -127,7 +127,7 @@ in {
       description = "ConnMan VPN service";
       wantedBy = [ "multi-user.target" ];
       after = [ "syslog.target" ];
-      before = [ "connman" ];
+      before = [ "connman.service" ];
       serviceConfig = {
         Type = "dbus";
         BusName = "net.connman.vpn";
@@ -140,7 +140,7 @@ in {
       description = "D-BUS Service";
       serviceConfig = {
         Name = "net.connman.vpn";
-        before = [ "connman" ];
+        before = [ "connman.service" ];
         ExecStart = "${cfg.package}/sbin/connman-vpnd -n";
         User = "root";
         SystemdService = "connman-vpn.service";
diff --git a/nixos/modules/services/networking/firewall.nix b/nixos/modules/services/networking/firewall.nix
index ff023a888f2..7482e29a3fd 100644
--- a/nixos/modules/services/networking/firewall.nix
+++ b/nixos/modules/services/networking/firewall.nix
@@ -179,10 +179,6 @@ let
       ) cfg.allowedUDPPortRanges
     ) allInterfaces)}
 
-    # Accept IPv4 multicast.  Not a big security risk since
-    # probably nobody is listening anyway.
-    #iptables -A nixos-fw -d 224.0.0.0/4 -j nixos-fw-accept
-
     # Optionally respond to ICMPv4 pings.
     ${optionalString cfg.allowPing ''
       iptables -w -A nixos-fw -p icmp --icmp-type echo-request ${optionalString (cfg.pingLimit != null)
@@ -326,7 +322,7 @@ in
         type = types.package;
         default = pkgs.iptables;
         defaultText = literalExpression "pkgs.iptables";
-        example = literalExpression "pkgs.iptables-nftables-compat";
+        example = literalExpression "pkgs.iptables-legacy";
         description =
           ''
             The iptables package to use for running the firewall service."
diff --git a/nixos/modules/services/networking/gogoclient.nix b/nixos/modules/services/networking/gogoclient.nix
deleted file mode 100644
index 1205321818b..00000000000
--- a/nixos/modules/services/networking/gogoclient.nix
+++ /dev/null
@@ -1,87 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let cfg = config.services.gogoclient;
-in
-
-{
-
-  ###### interface
-
-  options = {
-    services.gogoclient = {
-      enable = mkOption {
-        default = false;
-        type =  types.bool;
-        description = ''
-          Enable the gogoCLIENT IPv6 tunnel.
-        '';
-      };
-      autorun = mkOption {
-        type = types.bool;
-        default = true;
-        description = ''
-          Whether to automatically start the tunnel.
-        '';
-      };
-
-      username = mkOption {
-        default = "";
-        type = types.str;
-        description = ''
-          Your Gateway6 login name, if any.
-        '';
-      };
-
-      password = mkOption {
-        default = "";
-        type = types.str;
-        description = ''
-          Path to a file (as a string), containing your gogoNET password, if any.
-        '';
-      };
-
-      server = mkOption {
-        type = types.str;
-        default = "anonymous.freenet6.net";
-        example = "broker.freenet6.net";
-        description = "The Gateway6 server to be used.";
-      };
-    };
-  };
-
-  ###### implementation
-
-  config = mkIf cfg.enable {
-    boot.kernelModules = [ "tun" ];
-
-    networking.enableIPv6 = true;
-
-    systemd.services.gogoclient = {
-      description = "ipv6 tunnel";
-
-      after = [ "network.target" ];
-      requires = [ "network.target" ];
-
-      unitConfig.RequiresMountsFor = "/var/lib/gogoc";
-
-      script = let authMethod = if cfg.password == "" then "anonymous" else "any"; in ''
-        mkdir -p -m 700 /var/lib/gogoc
-        cat ${pkgs.gogoclient}/share/${pkgs.gogoclient.name}/gogoc.conf.sample | \
-          ${pkgs.gnused}/bin/sed \
-            -e "s|^userid=|&${cfg.username}|" \
-            -e "s|^passwd=|&${optionalString (cfg.password != "") "$(cat ${cfg.password})"}|" \
-            -e "s|^server=.*|server=${cfg.server}|" \
-            -e "s|^auth_method=.*|auth_method=${authMethod}|" \
-            -e "s|^#log_file=|log_file=1|" > /var/lib/gogoc/gogoc.conf
-        cd /var/lib/gogoc
-        exec ${pkgs.gogoclient}/bin/gogoc -y -f /var/lib/gogoc/gogoc.conf
-      '';
-    } // optionalAttrs cfg.autorun {
-      wantedBy = [ "multi-user.target" ];
-    };
-
-  };
-
-}
diff --git a/nixos/modules/services/networking/hylafax/options.nix b/nixos/modules/services/networking/hylafax/options.nix
index 8e59c68054d..8f621b61002 100644
--- a/nixos/modules/services/networking/hylafax/options.nix
+++ b/nixos/modules/services/networking/hylafax/options.nix
@@ -3,7 +3,7 @@
 let
 
   inherit (lib.options) literalExpression mkEnableOption mkOption;
-  inherit (lib.types) bool enum ints lines attrsOf nullOr path str submodule;
+  inherit (lib.types) bool enum ints lines attrsOf nonEmptyStr nullOr path str submodule;
   inherit (lib.modules) mkDefault mkIf mkMerge;
 
   commonDescr = ''
@@ -17,8 +17,6 @@ let
     configuration to yield an operational system.
   '';
 
-  str1 = lib.types.addCheck str (s: s!="");  # non-empty string
-
   configAttrType =
     # Options in HylaFAX configuration files can be
     # booleans, strings, integers, or list thereof
@@ -37,7 +35,7 @@ let
   modemConfigOptions = { name, config, ... }: {
     options = {
       name = mkOption {
-        type = str1;
+        type = nonEmptyStr;
         example = "ttyS1";
         description = ''
           Name of modem device,
@@ -45,7 +43,7 @@ let
         '';
       };
       type = mkOption {
-        type = str1;
+        type = nonEmptyStr;
         example = "cirrus";
         description = ''
           Name of modem configuration file,
@@ -135,14 +133,14 @@ in
     };
 
     countryCode = mkOption {
-      type = nullOr str1;
+      type = nullOr nonEmptyStr;
       default = null;
       example = "49";
       description = "Country code for server and all modems.";
     };
 
     areaCode = mkOption {
-      type = nullOr str1;
+      type = nullOr nonEmptyStr;
       default = null;
       example = "30";
       description = "Area code for server and all modems.";
@@ -279,7 +277,7 @@ in
       each time the spooling area is initialized.
     '';
     faxcron.enable.frequency = mkOption {
-      type = nullOr str1;
+      type = nullOr nonEmptyStr;
       default = null;
       example = "daily";
       description = ''
@@ -319,7 +317,7 @@ in
       each time the spooling area is initialized.
     '';
     faxqclean.enable.frequency = mkOption {
-      type = nullOr str1;
+      type = nullOr nonEmptyStr;
       default = null;
       example = "daily";
       description = ''
diff --git a/nixos/modules/services/networking/networkmanager.nix b/nixos/modules/services/networking/networkmanager.nix
index 73e63e2ee99..a9801036b00 100644
--- a/nixos/modules/services/networking/networkmanager.nix
+++ b/nixos/modules/services/networking/networkmanager.nix
@@ -384,6 +384,17 @@ in {
           so you don't need to to that yourself.
         '';
       };
+
+      enableFccUnlock = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable FCC unlock procedures. Since release 1.18.4, the ModemManager daemon no longer
+          automatically performs the FCC unlock procedure by default. See
+          <link xlink:href="https://modemmanager.org/docs/modemmanager/fcc-unlock/">the docs</link>
+          for more details.
+        '';
+      };
     };
   };
 
@@ -438,7 +449,13 @@ in {
 
       "NetworkManager/VPN/nm-sstp-service.name".source =
         "${networkmanager-sstp}/lib/NetworkManager/VPN/nm-sstp-service.name";
+
       }
+      // optionalAttrs cfg.enableFccUnlock
+         {
+           "ModemManager/fcc-unlock.d".source =
+             "${pkgs.modemmanager}/share/ModemManager/fcc-unlock.available.d/*";
+         }
       // optionalAttrs (cfg.appendNameservers != [] || cfg.insertNameservers != [])
          {
            "NetworkManager/dispatcher.d/02overridedns".source = overrideNameserversScript;
diff --git a/nixos/modules/services/networking/squid.nix b/nixos/modules/services/networking/squid.nix
index 9d063b92aa1..4f3881af8bb 100644
--- a/nixos/modules/services/networking/squid.nix
+++ b/nixos/modules/services/networking/squid.nix
@@ -81,7 +81,9 @@ let
     http_access deny all
 
     # Squid normally listens to port 3128
-    http_port ${toString cfg.proxyPort}
+    http_port ${
+      optionalString (cfg.proxyAddress != null) "${cfg.proxyAddress}:"
+    }${toString cfg.proxyPort}
 
     # Leave coredumps in the first cache dir
     coredump_dir /var/cache/squid
@@ -109,6 +111,12 @@ in
         description = "Whether to run squid web proxy.";
       };
 
+      proxyAddress = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "IP address on which squid will listen.";
+      };
+
       proxyPort = mkOption {
         type = types.int;
         default = 3128;
diff --git a/nixos/modules/services/networking/syncplay.nix b/nixos/modules/services/networking/syncplay.nix
index 27a16fb2e29..b6faf2d3f77 100644
--- a/nixos/modules/services/networking/syncplay.nix
+++ b/nixos/modules/services/networking/syncplay.nix
@@ -68,7 +68,7 @@ in
     systemd.services.syncplay = {
       description = "Syncplay Service";
       wantedBy    = [ "multi-user.target" ];
-      after       = [ "network-online.target "];
+      after       = [ "network-online.target" ];
 
       serviceConfig = {
         ExecStart = "${pkgs.syncplay}/bin/syncplay-server ${escapeShellArgs cmdArgs}";
diff --git a/nixos/modules/services/networking/wg-netmanager.nix b/nixos/modules/services/networking/wg-netmanager.nix
new file mode 100644
index 00000000000..493ff7ceba9
--- /dev/null
+++ b/nixos/modules/services/networking/wg-netmanager.nix
@@ -0,0 +1,42 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.wg-netmanager;
+in
+{
+
+  options = {
+    services.wg-netmanager = {
+      enable = mkEnableOption "Wireguard network manager";
+    };
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    # NOTE: wg-netmanager runs as root
+    systemd.services.wg-netmanager = {
+      description = "Wireguard network manager";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      path = with pkgs; [ wireguard-tools iproute2 wireguard-go ];
+      serviceConfig = {
+        Type = "simple";
+        Restart = "on-failure";
+        ExecStart = "${pkgs.wg-netmanager}/bin/wg_netmanager";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        ExecStop = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+
+        ReadWritePaths = [
+          "/tmp"  # wg-netmanager creates files in /tmp before deleting them after use
+        ];
+      };
+      unitConfig =  {
+        ConditionPathExists = ["/etc/wg_netmanager/network.yaml" "/etc/wg_netmanager/peer.yaml"];
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ gin66 ];
+}
diff --git a/nixos/modules/services/networking/wpa_supplicant.nix b/nixos/modules/services/networking/wpa_supplicant.nix
index 9989b6df659..c2e1d37e28b 100644
--- a/nixos/modules/services/networking/wpa_supplicant.nix
+++ b/nixos/modules/services/networking/wpa_supplicant.nix
@@ -11,11 +11,15 @@ let
   opt = options.networking.wireless;
 
   wpa3Protocols = [ "SAE" "FT-SAE" ];
-  hasWPA3 = opts: !mutuallyExclusive opts.authProtocols wpa3Protocols;
+  hasMixedWPA = opts:
+    let
+      hasWPA3 = !mutuallyExclusive opts.authProtocols wpa3Protocols;
+      others = subtractLists wpa3Protocols opts.authProtocols;
+    in hasWPA3 && others != [];
 
   # Gives a WPA3 network higher priority
   increaseWPA3Priority = opts:
-    opts // optionalAttrs (hasWPA3 opts)
+    opts // optionalAttrs (hasMixedWPA opts)
       { priority = if opts.priority == null
                      then 1
                      else opts.priority + 1;
@@ -33,7 +37,7 @@ let
   allNetworks =
     if cfg.fallbackToWPA2
       then map increaseWPA3Priority networkList
-           ++ map mkWPA2Fallback (filter hasWPA3 networkList)
+           ++ map mkWPA2Fallback (filter hasMixedWPA networkList)
       else networkList;
 
   # Content of wpa_supplicant.conf
diff --git a/nixos/modules/services/networking/yggdrasil.xml b/nixos/modules/services/networking/yggdrasil.xml
index c012cd4a929..a341d5d8153 100644
--- a/nixos/modules/services/networking/yggdrasil.xml
+++ b/nixos/modules/services/networking/yggdrasil.xml
@@ -84,7 +84,6 @@ in {
       interface eth0
       {
         AdvSendAdvert on;
-        AdvDefaultLifetime 0;
         prefix ${prefix}::/64 {
           AdvOnLink on;
           AdvAutonomous on;
diff --git a/nixos/modules/services/security/cfssl.nix b/nixos/modules/services/security/cfssl.nix
index e5bed0a9987..6df2343b84d 100644
--- a/nixos/modules/services/security/cfssl.nix
+++ b/nixos/modules/services/security/cfssl.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, ... }:
+{ config, options, lib, pkgs, ... }:
 
 with lib;
 
@@ -11,7 +11,16 @@ in {
     dataDir = mkOption {
       default = "/var/lib/cfssl";
       type = types.path;
-      description = "Cfssl work directory.";
+      description = ''
+        The work directory for CFSSL.
+
+        <note><para>
+          If left as the default value this directory will automatically be
+          created before the CFSSL server starts, otherwise you are
+          responsible for ensuring the directory exists with appropriate
+          ownership and permissions.
+        </para></note>
+      '';
     };
 
     address = mkOption {
@@ -22,7 +31,7 @@ in {
 
     port = mkOption {
       default = 8888;
-      type = types.ints.u16;
+      type = types.port;
       description = "Port to bind.";
     };
 
@@ -147,13 +156,12 @@ in {
   };
 
   config = mkIf cfg.enable {
-    users.extraGroups.cfssl = {
+    users.groups.cfssl = {
       gid = config.ids.gids.cfssl;
     };
 
-    users.extraUsers.cfssl = {
+    users.users.cfssl = {
       description = "cfssl user";
-      createHome = true;
       home = cfg.dataDir;
       group = "cfssl";
       uid = config.ids.uids.cfssl;
@@ -164,41 +172,46 @@ in {
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" ];
 
-      serviceConfig = {
-        WorkingDirectory = cfg.dataDir;
-        StateDirectory = cfg.dataDir;
-        StateDirectoryMode = 700;
-        Restart = "always";
-        User = "cfssl";
-
-        ExecStart = with cfg; let
-          opt = n: v: optionalString (v != null) ''-${n}="${v}"'';
-        in
-          lib.concatStringsSep " \\\n" [
-            "${pkgs.cfssl}/bin/cfssl serve"
-            (opt "address" address)
-            (opt "port" (toString port))
-            (opt "ca" ca)
-            (opt "ca-key" caKey)
-            (opt "ca-bundle" caBundle)
-            (opt "int-bundle" intBundle)
-            (opt "int-dir" intDir)
-            (opt "metadata" metadata)
-            (opt "remote" remote)
-            (opt "config" configFile)
-            (opt "responder" responder)
-            (opt "responder-key" responderKey)
-            (opt "tls-key" tlsKey)
-            (opt "tls-cert" tlsCert)
-            (opt "mutual-tls-ca" mutualTlsCa)
-            (opt "mutual-tls-cn" mutualTlsCn)
-            (opt "mutual-tls-client-key" mutualTlsClientKey)
-            (opt "mutual-tls-client-cert" mutualTlsClientCert)
-            (opt "tls-remote-ca" tlsRemoteCa)
-            (opt "db-config" dbConfig)
-            (opt "loglevel" (toString logLevel))
-          ];
-      };
+      serviceConfig = lib.mkMerge [
+        {
+          WorkingDirectory = cfg.dataDir;
+          Restart = "always";
+          User = "cfssl";
+          Group = "cfssl";
+
+          ExecStart = with cfg; let
+            opt = n: v: optionalString (v != null) ''-${n}="${v}"'';
+          in
+            lib.concatStringsSep " \\\n" [
+              "${pkgs.cfssl}/bin/cfssl serve"
+              (opt "address" address)
+              (opt "port" (toString port))
+              (opt "ca" ca)
+              (opt "ca-key" caKey)
+              (opt "ca-bundle" caBundle)
+              (opt "int-bundle" intBundle)
+              (opt "int-dir" intDir)
+              (opt "metadata" metadata)
+              (opt "remote" remote)
+              (opt "config" configFile)
+              (opt "responder" responder)
+              (opt "responder-key" responderKey)
+              (opt "tls-key" tlsKey)
+              (opt "tls-cert" tlsCert)
+              (opt "mutual-tls-ca" mutualTlsCa)
+              (opt "mutual-tls-cn" mutualTlsCn)
+              (opt "mutual-tls-client-key" mutualTlsClientKey)
+              (opt "mutual-tls-client-cert" mutualTlsClientCert)
+              (opt "tls-remote-ca" tlsRemoteCa)
+              (opt "db-config" dbConfig)
+              (opt "loglevel" (toString logLevel))
+            ];
+        }
+        (mkIf (cfg.dataDir == options.services.cfssl.dataDir.default) {
+          StateDirectory = baseNameOf cfg.dataDir;
+          StateDirectoryMode = 700;
+        })
+      ];
     };
 
     services.cfssl = {
diff --git a/nixos/modules/services/security/haveged.nix b/nixos/modules/services/security/haveged.nix
index 22ece188344..57cef7e44d5 100644
--- a/nixos/modules/services/security/haveged.nix
+++ b/nixos/modules/services/security/haveged.nix
@@ -3,12 +3,10 @@
 with lib;
 
 let
-
   cfg = config.services.haveged;
 
 in
 
-
 {
 
   ###### interface
@@ -17,14 +15,11 @@ in
 
     services.haveged = {
 
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Whether to enable to haveged entropy daemon, which refills
-          /dev/random when low.
-        '';
-      };
+      enable = mkEnableOption ''
+        haveged entropy daemon, which refills /dev/random when low.
+        NOTE: does nothing on kernels newer than 5.6.
+      '';
+      # source for the note https://github.com/jirka-h/haveged/issues/57
 
       refill_threshold = mkOption {
         type = types.int;
@@ -39,29 +34,44 @@ in
 
   };
 
-
-  ###### implementation
-
   config = mkIf cfg.enable {
 
-    systemd.services.haveged =
-      { description = "Entropy Harvesting Daemon";
-        unitConfig.Documentation = "man:haveged(8)";
-        wantedBy = [ "multi-user.target" ];
-
-        path = [ pkgs.haveged ];
-
-        serviceConfig = {
-          ExecStart = "${pkgs.haveged}/bin/haveged -F -w ${toString cfg.refill_threshold} -v 1";
-          SuccessExitStatus = 143;
-          PrivateTmp = true;
-          PrivateDevices = true;
-          PrivateNetwork = true;
-          ProtectSystem = "full";
-          ProtectHome = true;
-        };
+    # https://github.com/jirka-h/haveged/blob/a4b69d65a8dfc5a9f52ff8505c7f58dcf8b9234f/contrib/Fedora/haveged.service
+    systemd.services.haveged = {
+      description = "Entropy Daemon based on the HAVEGE algorithm";
+      unitConfig = {
+        Documentation = "man:haveged(8)";
+        DefaultDependencies = false;
+        ConditionKernelVersion = "<5.6";
+      };
+      wantedBy = [ "sysinit.target" ];
+      after = [ "systemd-tmpfiles-setup-dev.service" ];
+      before = [ "sysinit.target" "shutdown.target" "systemd-journald.service" ];
+
+      serviceConfig = {
+        ExecStart = "${pkgs.haveged}/bin/haveged -w ${toString cfg.refill_threshold} --Foreground -v 1";
+        Restart = "always";
+        SuccessExitStatus = "137 143";
+        SecureBits = "noroot-locked";
+        CapabilityBoundingSet = [ "CAP_SYS_ADMIN" "CAP_SYS_CHROOT" ];
+        # We can *not* set PrivateTmp=true as it can cause an ordering cycle.
+        PrivateTmp = false;
+        PrivateDevices = true;
+        ProtectSystem = "full";
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [ "@system-service" "newuname" "~@mount" ];
+        SystemCallErrorNumber = "EPERM";
       };
 
+    };
   };
 
 }
diff --git a/nixos/modules/services/security/step-ca.nix b/nixos/modules/services/security/step-ca.nix
index 27b2ceed1a4..db7f81acd2a 100644
--- a/nixos/modules/services/security/step-ca.nix
+++ b/nixos/modules/services/security/step-ca.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, nixosTests, ... }:
+{ config, lib, pkgs, ... }:
 let
   cfg = config.services.step-ca;
   settingsFormat = (pkgs.formats.json { });
@@ -82,8 +82,6 @@ in
       });
     in
     {
-      passthru.tests.step-ca = nixosTests.step-ca;
-
       assertions =
         [
           {
diff --git a/nixos/modules/services/system/cloud-init.nix b/nixos/modules/services/system/cloud-init.nix
index eb82b738e49..8c6a6e294eb 100644
--- a/nixos/modules/services/system/cloud-init.nix
+++ b/nixos/modules/services/system/cloud-init.nix
@@ -52,11 +52,22 @@ in
         '';
       };
 
+      network.enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Allow the cloud-init service to configure network interfaces
+          through systemd-networkd.
+        '';
+      };
+
       config = mkOption {
         type = types.str;
         default = ''
           system_info:
             distro: nixos
+            network:
+              renderers: [ 'networkd' ]
           users:
              - root
 
@@ -109,9 +120,12 @@ in
 
     environment.etc."cloud/cloud.cfg".text = cfg.config;
 
+    systemd.network.enable = cfg.network.enable;
+
     systemd.services.cloud-init-local =
       { description = "Initial cloud-init job (pre-networking)";
         wantedBy = [ "multi-user.target" ];
+        before = ["systemd-networkd.service"];
         path = path;
         serviceConfig =
           { Type = "oneshot";
@@ -129,7 +143,7 @@ in
                   "sshd.service" "sshd-keygen.service" ];
         after = [ "network-online.target" "cloud-init-local.service" ];
         before = [ "sshd.service" "sshd-keygen.service" ];
-        requires = [ "network.target "];
+        requires = [ "network.target"];
         path = path;
         serviceConfig =
           { Type = "oneshot";
diff --git a/nixos/modules/services/system/self-deploy.nix b/nixos/modules/services/system/self-deploy.nix
index 33d15e08f4a..d7130a13c73 100644
--- a/nixos/modules/services/system/self-deploy.nix
+++ b/nixos/modules/services/system/self-deploy.nix
@@ -126,6 +126,8 @@ in
 
   config = lib.mkIf cfg.enable {
     systemd.services.self-deploy = {
+      inherit (cfg) startAt;
+
       wantedBy = [ "multi-user.target" ];
 
       requires = lib.mkIf (!(isPathType cfg.repository)) [ "network-online.target" ];
@@ -138,8 +140,7 @@ in
       path = with pkgs; [
         git
         nix
-        systemd
-      ];
+      ] ++ lib.optionals (cfg.switchCommand == "boot") [ systemd ];
 
       script = ''
         if [ ! -e ${repositoryDirectory} ]; then
diff --git a/nixos/modules/services/web-apps/bookstack.nix b/nixos/modules/services/web-apps/bookstack.nix
index 54eaea63b6e..64a2767fab6 100644
--- a/nixos/modules/services/web-apps/bookstack.nix
+++ b/nixos/modules/services/web-apps/bookstack.nix
@@ -385,13 +385,13 @@ in {
                 else if isString v then v
                 else if true  == v then "true"
                 else if false == v then "false"
-                else if isSecret v then v._secret
+                else if isSecret v then hashString "sha256" v._secret
                 else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}";
             };
           };
           secretPaths = lib.mapAttrsToList (_: v: v._secret) (lib.filterAttrs (_: isSecret) cfg.config);
           mkSecretReplacement = file: ''
-            replace-secret ${escapeShellArgs [ file file "${cfg.dataDir}/.env" ]}
+            replace-secret ${escapeShellArgs [ (builtins.hashString "sha256" file) file "${cfg.dataDir}/.env" ]}
           '';
           secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
           filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! elem v [ {} null ])) cfg.config;
diff --git a/nixos/modules/services/web-apps/matomo.nix b/nixos/modules/services/web-apps/matomo.nix
index 8a0ca33b51f..c6d4ed6d39d 100644
--- a/nixos/modules/services/web-apps/matomo.nix
+++ b/nixos/modules/services/web-apps/matomo.nix
@@ -192,6 +192,7 @@ in {
             # Copy config folder
             chmod g+s "${dataDir}"
             cp -r "${cfg.package}/share/config" "${dataDir}/"
+            mkdir -p "${dataDir}/misc"
             chmod -R u+rwX,g+rwX,o-rwx "${dataDir}"
 
             # check whether user setup has already been done
diff --git a/nixos/modules/services/web-servers/agate.nix b/nixos/modules/services/web-servers/agate.nix
new file mode 100644
index 00000000000..3afdb561c0b
--- /dev/null
+++ b/nixos/modules/services/web-servers/agate.nix
@@ -0,0 +1,148 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.agate;
+in
+{
+  options = {
+    services.agate = {
+      enable = mkEnableOption "Agate Server";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.agate;
+        defaultText = literalExpression "pkgs.agate";
+        description = "The package to use";
+      };
+
+      addresses = mkOption {
+        type = types.listOf types.str;
+        default = [ "0.0.0.0:1965" ];
+        description = ''
+          Addresses to listen on, IP:PORT, if you haven't disabled forwarding
+          only set IPv4.
+        '';
+      };
+
+      contentDir = mkOption {
+        default = "/var/lib/agate/content";
+        type = types.path;
+        description = "Root of the content directory.";
+      };
+
+      certificatesDir = mkOption {
+        default = "/var/lib/agate/certificates";
+        type = types.path;
+        description = "Root of the certificate directory.";
+      };
+
+      hostnames = mkOption {
+        default = [ ];
+        type = types.listOf types.str;
+        description = ''
+          Domain name of this Gemini server, enables checking hostname and port
+          in requests. (multiple occurences means basic vhosts)
+        '';
+      };
+
+      language = mkOption {
+        default = null;
+        type = types.nullOr types.str;
+        description = "RFC 4646 Language code for text/gemini documents.";
+      };
+
+      onlyTls_1_3 = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Only use TLSv1.3 (default also allows TLSv1.2).";
+      };
+
+      extraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [ "" ];
+        example = [ "--log-ip" ];
+        description = "Extra arguments to use running agate.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    # available for generating certs by hand
+    # it can be a bit arduous with openssl
+    environment.systemPackages = [ cfg.package ];
+
+    systemd.services.agate = {
+      description = "Agate";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" "network-online.target" ];
+
+      script =
+        let
+          prefixKeyList = key: list: concatMap (v: [ key v ]) list;
+          addresses = prefixKeyList "--addr" cfg.addresses;
+          hostnames = prefixKeyList "--hostname" cfg.hostnames;
+        in
+        ''
+          exec ${cfg.package}/bin/agate ${
+            escapeShellArgs (
+              [
+                "--content" "${cfg.contentDir}"
+                "--certs" "${cfg.certificatesDir}"
+              ] ++
+              addresses ++
+              (optionals (cfg.hostnames != []) hostnames) ++
+              (optionals (cfg.language != null) [ "--lang" cfg.language ]) ++
+              (optionals cfg.onlyTls_1_3 [ "--only-tls13" ]) ++
+              (optionals (cfg.extraArgs != []) cfg.extraArgs)
+            )
+          }
+        '';
+
+      serviceConfig = {
+        Restart = "always";
+        RestartSec = "5s";
+        DynamicUser = true;
+        StateDirectory = "agate";
+
+        # Security options:
+        AmbientCapabilities = "";
+        CapabilityBoundingSet = "";
+
+        # ProtectClock= adds DeviceAllow=char-rtc r
+        DeviceAllow = "";
+
+        LockPersonality = true;
+
+        PrivateTmp = true;
+        PrivateDevices = true;
+        PrivateUsers = true;
+
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+
+        RestrictNamespaces = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictRealtime = true;
+
+        SystemCallArchitectures = "native";
+        SystemCallErrorNumber = "EPERM";
+        SystemCallFilter = [
+          "@system-service"
+          "~@cpu-emulation"
+          "~@debug"
+          "~@keyring"
+          "~@memlock"
+          "~@obsolete"
+          "~@privileged"
+          "~@setuid"
+        ];
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-servers/shellinabox.nix b/nixos/modules/services/web-servers/shellinabox.nix
deleted file mode 100644
index c7c51f873eb..00000000000
--- a/nixos/modules/services/web-servers/shellinabox.nix
+++ /dev/null
@@ -1,122 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-
-  cfg = config.services.shellinabox;
-
-  # If a certificate file is specified, shellinaboxd requires
-  # a file descriptor to retrieve it
-  fd = "3";
-  createFd = optionalString (cfg.certFile != null) "${fd}<${cfg.certFile}";
-
-  # Command line arguments for the shellinabox daemon
-  args = [ "--background" ]
-   ++ optional (! cfg.enableSSL) "--disable-ssl"
-   ++ optional (cfg.certFile != null) "--cert-fd=${fd}"
-   ++ optional (cfg.certDirectory != null) "--cert=${cfg.certDirectory}"
-   ++ cfg.extraOptions;
-
-  # Command to start shellinaboxd
-  cmd = "${pkgs.shellinabox}/bin/shellinaboxd ${concatStringsSep " " args}";
-
-  # Command to start shellinaboxd if certFile is specified
-  wrappedCmd = "${pkgs.bash}/bin/bash -c 'exec ${createFd} && ${cmd}'";
-
-in
-
-{
-
-  ###### interface
-
-  options = {
-    services.shellinabox = {
-      enable = mkEnableOption "shellinabox daemon";
-
-      user = mkOption {
-        type = types.str;
-        default = "root";
-        description = ''
-          User to run shellinaboxd as. If started as root, the server drops
-          privileges by changing to nobody, unless overridden by the
-          <literal>--user</literal> option.
-        '';
-      };
-
-      enableSSL = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Whether or not to enable SSL (https) support.
-        '';
-      };
-
-      certDirectory = mkOption {
-        type = types.nullOr types.path;
-        default = null;
-        example = "/var/certs";
-        description = ''
-          The daemon will look in this directory far any certificates.
-          If the browser negotiated a Server Name Identification the daemon
-          will look for a matching certificate-SERVERNAME.pem file. If no SNI
-          handshake takes place, it will fall back on using the certificate in the
-          certificate.pem file.
-
-          If no suitable certificate is installed, shellinaboxd will attempt to
-          create a new self-signed certificate. This will only succeed if, after
-          dropping privileges, shellinaboxd has write permissions for this
-          directory.
-        '';
-      };
-
-      certFile = mkOption {
-        type = types.nullOr types.path;
-        default = null;
-        example = "/var/certificate.pem";
-        description = "Path to server SSL certificate.";
-      };
-
-      extraOptions = mkOption {
-        type = types.listOf types.str;
-        default = [ ];
-        example = [ "--port=443" "--service /:LOGIN" ];
-        description = ''
-          A list of strings to be appended to the command line arguments
-          for shellinaboxd. Please see the manual page
-          <link xlink:href="https://code.google.com/p/shellinabox/wiki/shellinaboxd_man"/>
-          for a full list of available arguments.
-        '';
-      };
-
-    };
-  };
-
-  ###### implementation
-
-  config = mkIf cfg.enable {
-
-    assertions =
-      [ { assertion = cfg.enableSSL == true
-            -> cfg.certDirectory != null || cfg.certFile != null;
-          message = "SSL is enabled for shellinabox, but no certDirectory or certFile has been specefied."; }
-        { assertion = ! (cfg.certDirectory != null && cfg.certFile != null);
-          message = "Cannot set both certDirectory and certFile for shellinabox."; }
-      ];
-
-    systemd.services.shellinaboxd = {
-      description = "Shellinabox Web Server Daemon";
-
-      wantedBy = [ "multi-user.target" ];
-      requires = [ "sshd.service" ];
-      after = [ "sshd.service" ];
-
-      serviceConfig = {
-        Type = "forking";
-        User = "${cfg.user}";
-        ExecStart = "${if cfg.certFile == null then "${cmd}" else "${wrappedCmd}"}";
-        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
-      };
-    };
-  };
-}
diff --git a/nixos/modules/services/x11/desktop-managers/default.nix b/nixos/modules/services/x11/desktop-managers/default.nix
index 6ee5b0fc54f..8247a7e381c 100644
--- a/nixos/modules/services/x11/desktop-managers/default.nix
+++ b/nixos/modules/services/x11/desktop-managers/default.nix
@@ -19,7 +19,7 @@ in
   # E.g., if Plasma 5 is enabled, it supersedes xterm.
   imports = [
     ./none.nix ./xterm.nix ./xfce.nix ./plasma5.nix ./lumina.nix
-    ./lxqt.nix ./enlightenment.nix ./gnome.nix ./kodi.nix
+    ./lxqt.nix ./enlightenment.nix ./gnome.nix ./retroarch.nix ./kodi.nix
     ./mate.nix ./pantheon.nix ./surf-display.nix ./cde.nix
     ./cinnamon.nix
   ];
diff --git a/nixos/modules/services/x11/desktop-managers/pantheon.nix b/nixos/modules/services/x11/desktop-managers/pantheon.nix
index 980a6b939d5..6a7d2a8aa6c 100644
--- a/nixos/modules/services/x11/desktop-managers/pantheon.nix
+++ b/nixos/modules/services/x11/desktop-managers/pantheon.nix
@@ -135,6 +135,7 @@ in
       services.bamf.enable = true;
       services.colord.enable = mkDefault true;
       services.fwupd.enable = mkDefault true;
+      services.packagekit.enable = mkDefault true;
       services.touchegg.enable = mkDefault true;
       services.touchegg.package = pkgs.pantheon.touchegg;
       services.tumbler.enable = mkDefault true;
@@ -224,7 +225,6 @@ in
       programs.file-roller.package = pkgs.pantheon.file-roller;
 
       # Settings from elementary-default-settings
-      environment.sessionVariables.GTK_CSD = "1";
       environment.etc."gtk-3.0/settings.ini".source = "${pkgs.pantheon.elementary-default-settings}/etc/gtk-3.0/settings.ini";
 
       xdg.portal.extraPortals = with pkgs.pantheon; [
@@ -273,7 +273,7 @@ in
     })
 
     (mkIf serviceCfg.apps.enable {
-      environment.systemPackages = (with pkgs.pantheon; pkgs.gnome.removePackagesByName [
+      environment.systemPackages = with pkgs.pantheon; pkgs.gnome.removePackagesByName ([
         elementary-calculator
         elementary-calendar
         elementary-camera
@@ -287,7 +287,11 @@ in
         elementary-terminal
         elementary-videos
         epiphany
-      ] config.environment.pantheon.excludePackages);
+      ] ++ lib.optionals config.services.flatpak.enable [
+        # Only install appcenter if flatpak is enabled before
+        # https://github.com/NixOS/nixpkgs/issues/15932 is resolved.
+        appcenter
+      ]) config.environment.pantheon.excludePackages;
 
       # needed by screenshot
       fonts.fonts = [
diff --git a/nixos/modules/services/x11/desktop-managers/pantheon.xml b/nixos/modules/services/x11/desktop-managers/pantheon.xml
index fe0a1c49622..202909d398f 100644
--- a/nixos/modules/services/x11/desktop-managers/pantheon.xml
+++ b/nixos/modules/services/x11/desktop-managers/pantheon.xml
@@ -105,10 +105,10 @@ switchboard-with-plugs.override {
     </term>
     <listitem>
      <para>
-      AppCenter has been available since 20.03, but it is of little use. This is because there is no functioning PackageKit backend for Nix 2.0. Starting from 21.11, the Flatpak backend should work so you can install some Flatpak applications using it. See this <link xlink:href="https://github.com/NixOS/nixpkgs/issues/70214">issue</link>.
+      AppCenter has been available since 20.03. Starting from 21.11, the Flatpak backend should work so you can install some Flatpak applications using it. However, due to missing appstream metadata, the Packagekit backend does not function currently. See this <link xlink:href="https://github.com/NixOS/nixpkgs/issues/15932">issue</link>.
      </para>
      <para>
-      To use AppCenter on NixOS, add <literal>pantheon.appcenter</literal> to <xref linkend="opt-environment.systemPackages" />, <link linkend="module-services-flatpak">enable Flatpak support</link> and optionally add the <literal>appcenter</literal> Flatpak remote:
+      If you are using Pantheon, AppCenter should be installed by default if you have <link linkend="module-services-flatpak">Flatpak support</link> enabled. If you also wish to add the <literal>appcenter</literal> Flatpak remote:
      </para>
 <screen>
 <prompt>$ </prompt>flatpak remote-add --if-not-exists appcenter https://flatpak.elementary.io/repo.flatpakrepo
diff --git a/nixos/modules/services/x11/desktop-managers/retroarch.nix b/nixos/modules/services/x11/desktop-managers/retroarch.nix
new file mode 100644
index 00000000000..d471673d452
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/retroarch.nix
@@ -0,0 +1,40 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.services.xserver.desktopManager.retroarch;
+
+in {
+  options.services.xserver.desktopManager.retroarch = {
+    enable = mkEnableOption "RetroArch";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.retroarch;
+      defaultText = literalExpression "pkgs.retroarch";
+      example = literalExpression "pkgs.retroarch-full";
+      description = "RetroArch package to use.";
+    };
+
+    extraArgs = mkOption {
+      type = types.listOf types.str;
+      default = [ ];
+      example = [ "--verbose" "--host" ];
+      description = "Extra arguments to pass to RetroArch.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.xserver.desktopManager.session = [{
+      name = "RetroArch";
+      start = ''
+        ${cfg.package}/bin/retroarch -f ${escapeShellArgs cfg.extraArgs} &
+        waitPID=$!
+      '';
+    }];
+
+    environment.systemPackages = [ cfg.package ];
+  };
+
+  meta.maintainers = with maintainers; [ j0hax ];
+}
diff --git a/nixos/modules/services/x11/display-managers/gdm.nix b/nixos/modules/services/x11/display-managers/gdm.nix
index f9d4d3d6836..b1dc6643be8 100644
--- a/nixos/modules/services/x11/display-managers/gdm.nix
+++ b/nixos/modules/services/x11/display-managers/gdm.nix
@@ -140,7 +140,8 @@ in
         environment = {
           GDM_X_SERVER_EXTRA_ARGS = toString
             (filter (arg: arg != "-terminate") cfg.xserverArgs);
-          XDG_DATA_DIRS = "${cfg.sessionData.desktops}/share/";
+          # GDM is needed for gnome-login.session
+          XDG_DATA_DIRS = "${gdm}/share:${cfg.sessionData.desktops}/share";
         } // optionalAttrs (xSessionWrapper != null) {
           # Make GDM use this wrapper before running the session, which runs the
           # configured setupCommands. This relies on a patched GDM which supports
diff --git a/nixos/modules/system/activation/switch-to-configuration.pl b/nixos/modules/system/activation/switch-to-configuration.pl
index 1fe346114e4..a8fe14c58f0 100644
--- a/nixos/modules/system/activation/switch-to-configuration.pl
+++ b/nixos/modules/system/activation/switch-to-configuration.pl
@@ -5,7 +5,7 @@ use warnings;
 use Config::IniFiles;
 use File::Path qw(make_path);
 use File::Basename;
-use File::Slurp;
+use File::Slurp qw(read_file write_file edit_file);
 use Net::DBus;
 use Sys::Syslog qw(:standard :macros);
 use Cwd 'abs_path';
@@ -20,12 +20,19 @@ my $restartListFile = "/run/nixos/restart-list";
 my $reloadListFile = "/run/nixos/reload-list";
 
 # Parse restart/reload requests by the activation script.
-# Activation scripts may write newline-separated units to this
+# Activation scripts may write newline-separated units to the restart
 # file and switch-to-configuration will handle them. While
 # `stopIfChanged = true` is ignored, switch-to-configuration will
 # handle `restartIfChanged = false` and `reloadIfChanged = true`.
+# This is the same as specifying a restart trigger in the NixOS module.
+#
+# The reload file asks the script to reload a unit. This is the same as
+# specifying a reload trigger in the NixOS module and can be ignored if
+# the unit is restarted in this activation.
 my $restartByActivationFile = "/run/nixos/activation-restart-list";
+my $reloadByActivationFile = "/run/nixos/activation-reload-list";
 my $dryRestartByActivationFile = "/run/nixos/dry-activation-restart-list";
+my $dryReloadByActivationFile = "/run/nixos/dry-activation-reload-list";
 
 make_path("/run/nixos", { mode => oct(755) });
 
@@ -131,6 +138,10 @@ sub parseSystemdIni {
 
     # Copy over all sections
     foreach my $sectionName (keys %fileContents) {
+        if ($sectionName eq "Install") {
+            # Skip the [Install] section because it has no relevant keys for us
+            next;
+        }
         # Copy over all keys
         foreach my $iniKey (keys %{$fileContents{$sectionName}}) {
             # Ensure the value is an array so it's easier to work with
@@ -162,13 +173,18 @@ sub parseSystemdIni {
 #
 # If a directory with the same basename ending in .d exists next to the unit file, it will be
 # assumed to contain override files which will be parsed as well and handled properly.
-sub parseUnit {
-    my ($unitPath) = @_;
+sub parse_unit {
+    my ($unit_path) = @_;
 
     # Parse the main unit and all overrides
-    my %unitData;
-    parseSystemdIni(\%unitData, $_) for glob("${unitPath}{,.d/*.conf}");
-    return %unitData;
+    my %unit_data;
+    # Replace \ with \\ so glob() still works with units that have a \ in them
+    # Valid characters in unit names are ASCII letters, digits, ":", "-", "_", ".", and "\"
+    $unit_path =~ s/\\/\\\\/gmsx;
+    foreach (glob "${unit_path}{,.d/*.conf}") {
+        parseSystemdIni(\%unit_data, "$_")
+    }
+    return %unit_data;
 }
 
 # Checks whether a specified boolean in a systemd unit is true
@@ -192,16 +208,96 @@ sub recordUnit {
     write_file($fn, { append => 1 }, "$unit\n") if $action ne "dry-activate";
 }
 
-# As a fingerprint for determining whether a unit has changed, we use
-# its absolute path. If it has an override file, we append *its*
-# absolute path as well.
-sub fingerprintUnit {
-    my ($s) = @_;
-    return abs_path($s) . (-f "${s}.d/overrides.conf" ? " " . abs_path "${s}.d/overrides.conf" : "");
+# The opposite of recordUnit, removes a unit name from a file
+sub unrecord_unit {
+    my ($fn, $unit) = @_;
+    edit_file { s/^$unit\n//msx } $fn if $action ne "dry-activate";
+}
+
+# Compare the contents of two unit files and return whether the unit
+# needs to be restarted or reloaded. If the units differ, the service
+# is restarted unless the only difference is `X-Reload-Triggers` in the
+# `Unit` section. If this is the only modification, the unit is reloaded
+# instead of restarted.
+# Returns:
+# - 0 if the units are equal
+# - 1 if the units are different and a restart action is required
+# - 2 if the units are different and a reload action is required
+sub compare_units {
+    my ($old_unit, $new_unit) = @_;
+    my $ret = 0;
+
+    my $comp_array = sub {
+      my ($a, $b) = @_;
+      return join("\0", @{$a}) eq join("\0", @{$b});
+    };
+
+    # Comparison hash for the sections
+    my %section_cmp = map { $_ => 1 } keys %{$new_unit};
+    # Iterate over the sections
+    foreach my $section_name (keys %{$old_unit}) {
+        # Missing section in the new unit?
+        if (not exists $section_cmp{$section_name}) {
+            if ($section_name eq 'Unit' and %{$old_unit->{'Unit'}} == 1 and defined(%{$old_unit->{'Unit'}}{'X-Reload-Triggers'})) {
+                # If a new [Unit] section was removed that only contained X-Reload-Triggers,
+                # do nothing.
+                next;
+            } else {
+                return 1;
+            }
+        }
+        delete $section_cmp{$section_name};
+        # Comparison hash for the section contents
+        my %ini_cmp = map { $_ => 1 } keys %{$new_unit->{$section_name}};
+        # Iterate over the keys of the section
+        foreach my $ini_key (keys %{$old_unit->{$section_name}}) {
+            delete $ini_cmp{$ini_key};
+            my @old_value = @{$old_unit->{$section_name}{$ini_key}};
+            # If the key is missing in the new unit, they are different...
+            if (not $new_unit->{$section_name}{$ini_key}) {
+                # ... unless the key that is now missing was the reload trigger
+                if ($section_name eq 'Unit' and $ini_key eq 'X-Reload-Triggers') {
+                    next;
+                }
+                return 1;
+            }
+            my @new_value = @{$new_unit->{$section_name}{$ini_key}};
+            # If the contents are different, the units are different
+            if (not $comp_array->(\@old_value, \@new_value)) {
+                # Check if only the reload triggers changed
+                if ($section_name eq 'Unit' and $ini_key eq 'X-Reload-Triggers') {
+                    $ret = 2;
+                } else {
+                    return 1;
+                }
+            }
+        }
+        # A key was introduced that was missing in the old unit
+        if (%ini_cmp) {
+            if ($section_name eq 'Unit' and %ini_cmp == 1 and defined($ini_cmp{'X-Reload-Triggers'})) {
+                # If the newly introduced key was the reload triggers, reload the unit
+                $ret = 2;
+            } else {
+                return 1;
+            }
+        };
+    }
+    # A section was introduced that was missing in the old unit
+    if (%section_cmp) {
+        if (%section_cmp == 1 and defined($section_cmp{'Unit'}) and %{$new_unit->{'Unit'}} == 1 and defined(%{$new_unit->{'Unit'}}{'X-Reload-Triggers'})) {
+            # If a new [Unit] section was introduced that only contains X-Reload-Triggers,
+            # reload instead of restarting
+            $ret = 2;
+        } else {
+            return 1;
+        }
+    }
+
+    return $ret;
 }
 
 sub handleModifiedUnit {
-    my ($unit, $baseName, $newUnitFile, $activePrev, $unitsToStop, $unitsToStart, $unitsToReload, $unitsToRestart, $unitsToSkip) = @_;
+    my ($unit, $baseName, $newUnitFile, $newUnitInfo, $activePrev, $unitsToStop, $unitsToStart, $unitsToReload, $unitsToRestart, $unitsToSkip) = @_;
 
     if ($unit eq "sysinit.target" || $unit eq "basic.target" || $unit eq "multi-user.target" || $unit eq "graphical.target" || $unit =~ /\.path$/ || $unit =~ /\.slice$/) {
         # Do nothing.  These cannot be restarted directly.
@@ -219,8 +315,8 @@ sub handleModifiedUnit {
         # Revert of the attempt: https://github.com/NixOS/nixpkgs/pull/147609
         # More details: https://github.com/NixOS/nixpkgs/issues/74899#issuecomment-981142430
     } else {
-        my %unitInfo = parseUnit($newUnitFile);
-        if (parseSystemdBool(\%unitInfo, "Service", "X-ReloadIfChanged", 0)) {
+        my %unitInfo = $newUnitInfo ? %{$newUnitInfo} : parse_unit($newUnitFile);
+        if (parseSystemdBool(\%unitInfo, "Service", "X-ReloadIfChanged", 0) and not $unitsToRestart->{$unit} and not $unitsToStop->{$unit}) {
             $unitsToReload->{$unit} = 1;
             recordUnit($reloadListFile, $unit);
         }
@@ -234,6 +330,11 @@ sub handleModifiedUnit {
                 # stopped and started.
                 $unitsToRestart->{$unit} = 1;
                 recordUnit($restartListFile, $unit);
+                # Remove from units to reload so we don't restart and reload
+                if ($unitsToReload->{$unit}) {
+                    delete $unitsToReload->{$unit};
+                    unrecord_unit($reloadListFile, $unit);
+                }
             } else {
                 # If this unit is socket-activated, then stop the
                 # socket unit(s) as well, and restart the
@@ -254,6 +355,11 @@ sub handleModifiedUnit {
                                 recordUnit($startListFile, $socket);
                                 $socketActivated = 1;
                             }
+                            # Remove from units to reload so we don't restart and reload
+                            if ($unitsToReload->{$unit}) {
+                                delete $unitsToReload->{$unit};
+                                unrecord_unit($reloadListFile, $unit);
+                            }
                         }
                     }
                 }
@@ -268,6 +374,11 @@ sub handleModifiedUnit {
                 }
 
                 $unitsToStop->{$unit} = 1;
+                # Remove from units to reload so we don't restart and reload
+                if ($unitsToReload->{$unit}) {
+                    delete $unitsToReload->{$unit};
+                    unrecord_unit($reloadListFile, $unit);
+                }
             }
         }
     }
@@ -306,12 +417,12 @@ while (my ($unit, $state) = each %{$activePrev}) {
 
     if (-e $prevUnitFile && ($state->{state} eq "active" || $state->{state} eq "activating")) {
         if (! -e $newUnitFile || abs_path($newUnitFile) eq "/dev/null") {
-            my %unitInfo = parseUnit($prevUnitFile);
+            my %unitInfo = parse_unit($prevUnitFile);
             $unitsToStop{$unit} = 1 if parseSystemdBool(\%unitInfo, "Unit", "X-StopOnRemoval", 1);
         }
 
         elsif ($unit =~ /\.target$/) {
-            my %unitInfo = parseUnit($newUnitFile);
+            my %unitInfo = parse_unit($newUnitFile);
 
             # Cause all active target units to be restarted below.
             # This should start most changed units we stop here as
@@ -344,8 +455,16 @@ while (my ($unit, $state) = each %{$activePrev}) {
             }
         }
 
-        elsif (fingerprintUnit($prevUnitFile) ne fingerprintUnit($newUnitFile)) {
-            handleModifiedUnit($unit, $baseName, $newUnitFile, $activePrev, \%unitsToStop, \%unitsToStart, \%unitsToReload, \%unitsToRestart, \%unitsToSkip);
+        else {
+            my %old_unit_info = parse_unit($prevUnitFile);
+            my %new_unit_info = parse_unit($newUnitFile);
+            my $diff = compare_units(\%old_unit_info, \%new_unit_info);
+            if ($diff eq 1) {
+                handleModifiedUnit($unit, $baseName, $newUnitFile, \%new_unit_info, $activePrev, \%unitsToStop, \%unitsToStart, \%unitsToReload, \%unitsToRestart, \%unitsToSkip);
+            } elsif ($diff eq 2 and not $unitsToRestart{$unit}) {
+                $unitsToReload{$unit} = 1;
+                recordUnit($reloadListFile, $unit);
+            }
         }
     }
 }
@@ -361,17 +480,6 @@ sub pathToUnitName {
     return $escaped;
 }
 
-sub unique {
-    my %seen;
-    my @res;
-    foreach my $name (@_) {
-        next if $seen{$name};
-        $seen{$name} = 1;
-        push @res, $name;
-    }
-    return @res;
-}
-
 # Compare the previous and new fstab to figure out which filesystems
 # need a remount or need to be unmounted.  New filesystems are mounted
 # automatically by starting local-fs.target.  FIXME: might be nicer if
@@ -407,8 +515,12 @@ foreach my $device (keys %$prevSwaps) {
         # "systemctl stop" here because systemd has lots of alias
         # units that prevent a stop from actually calling
         # "swapoff".
-        print STDERR "stopping swap device: $device\n";
-        system("@utillinux@/sbin/swapoff", $device);
+        if ($action ne "dry-activate") {
+            print STDERR "would stop swap device: $device\n";
+        } else {
+            print STDERR "stopping swap device: $device\n";
+            system("@utillinux@/sbin/swapoff", $device);
+        }
     }
     # FIXME: update swap options (i.e. its priority).
 }
@@ -469,10 +581,20 @@ if ($action eq "dry-activate") {
             next;
         }
 
-        handleModifiedUnit($unit, $baseName, $newUnitFile, $activePrev, \%unitsToRestart, \%unitsToRestart, \%unitsToReload, \%unitsToRestart, \%unitsToSkip);
+        handleModifiedUnit($unit, $baseName, $newUnitFile, undef, $activePrev, \%unitsToRestart, \%unitsToRestart, \%unitsToReload, \%unitsToRestart, \%unitsToSkip);
     }
     unlink($dryRestartByActivationFile);
 
+    foreach (split('\n', read_file($dryReloadByActivationFile, err_mode => 'quiet') // "")) {
+        my $unit = $_;
+
+        if (defined($activePrev->{$unit}) and not $unitsToRestart{$unit} and not $unitsToStop{$unit}) {
+            $unitsToReload{$unit} = 1;
+            recordUnit($reloadListFile, $unit);
+        }
+    }
+    unlink($dryReloadByActivationFile);
+
     print STDERR "would restart systemd\n" if $restartSystemd;
     print STDERR "would reload the following units: ", join(", ", sort(keys %unitsToReload)), "\n"
         if scalar(keys %unitsToReload) > 0;
@@ -525,11 +647,22 @@ foreach (split('\n', read_file($restartByActivationFile, err_mode => 'quiet') //
         next;
     }
 
-    handleModifiedUnit($unit, $baseName, $newUnitFile, $activePrev, \%unitsToRestart, \%unitsToRestart, \%unitsToReload, \%unitsToRestart, \%unitsToSkip);
+    handleModifiedUnit($unit, $baseName, $newUnitFile, undef, $activePrev, \%unitsToRestart, \%unitsToRestart, \%unitsToReload, \%unitsToRestart, \%unitsToSkip);
 }
 # We can remove the file now because it has been propagated to the other restart/reload files
 unlink($restartByActivationFile);
 
+foreach (split('\n', read_file($reloadByActivationFile, err_mode => 'quiet') // "")) {
+    my $unit = $_;
+
+    if (defined($activePrev->{$unit}) and not $unitsToRestart{$unit} and not $unitsToStop{$unit}) {
+        $unitsToReload{$unit} = 1;
+        recordUnit($reloadListFile, $unit);
+    }
+}
+# We can remove the file now because it has been propagated to the other reload file
+unlink($reloadByActivationFile);
+
 # Restart systemd if necessary. Note that this is done using the
 # current version of systemd, just in case the new one has trouble
 # communicating with the running pid 1.
diff --git a/nixos/modules/system/activation/top-level.nix b/nixos/modules/system/activation/top-level.nix
index 9e6ca75b9da..b8aeee8c11b 100644
--- a/nixos/modules/system/activation/top-level.nix
+++ b/nixos/modules/system/activation/top-level.nix
@@ -117,7 +117,7 @@ let
     configurationName = config.boot.loader.grub.configurationName;
 
     # Needed by switch-to-configuration.
-    perl = pkgs.perl.withPackages (p: with p; [ FileSlurp NetDBus XMLParser XMLTwig ConfigIniFiles ]);
+    perl = pkgs.perl.withPackages (p: with p; [ ConfigIniFiles FileSlurp NetDBus ]);
   };
 
   # Handle assertions and warnings
diff --git a/nixos/modules/system/boot/stage-1.nix b/nixos/modules/system/boot/stage-1.nix
index 9c684fbada2..1575c0257d1 100644
--- a/nixos/modules/system/boot/stage-1.nix
+++ b/nixos/modules/system/boot/stage-1.nix
@@ -350,6 +350,9 @@ let
             '';
           symlink = "/etc/modprobe.d/ubuntu.conf";
         }
+        { object = config.environment.etc."modprobe.d/nixos.conf".source;
+          symlink = "/etc/modprobe.d/nixos.conf";
+        }
         { object = pkgs.kmod-debian-aliases;
           symlink = "/etc/modprobe.d/debian.conf";
         }
diff --git a/nixos/modules/system/boot/systemd.nix b/nixos/modules/system/boot/systemd.nix
index 9dcf9eb769f..1f2dd618698 100644
--- a/nixos/modules/system/boot/systemd.nix
+++ b/nixos/modules/system/boot/systemd.nix
@@ -243,6 +243,8 @@ let
           { Requisite = toString config.requisite; }
         // optionalAttrs (config.restartTriggers != [])
           { X-Restart-Triggers = toString config.restartTriggers; }
+        // optionalAttrs (config.reloadTriggers != [])
+          { X-Reload-Triggers = toString config.reloadTriggers; }
         // optionalAttrs (config.description != "") {
           Description = config.description; }
         // optionalAttrs (config.documentation != []) {
@@ -917,6 +919,9 @@ in
               (optional hasDeprecated
                 "Service '${name}.service' uses the attribute 'StartLimitInterval' in the Service section, which is deprecated. See https://github.com/NixOS/nixpkgs/issues/45786."
               )
+              (optional (service.reloadIfChanged && service.reloadTriggers != [])
+                "Service '${name}.service' has both 'reloadIfChanged' and 'reloadTriggers' set. This is probably not what you want, because 'reloadTriggers' behave the same whay as 'restartTriggers' if 'reloadIfChanged' is set."
+              )
             ]
         )
         cfg.services
diff --git a/nixos/modules/system/boot/tmp.nix b/nixos/modules/system/boot/tmp.nix
index 6edafd6695b..cf6d19eb5f0 100644
--- a/nixos/modules/system/boot/tmp.nix
+++ b/nixos/modules/system/boot/tmp.nix
@@ -48,7 +48,12 @@ in
         what = "tmpfs";
         where = "/tmp";
         type = "tmpfs";
-        mountConfig.Options = [ "mode=1777" "strictatime" "rw" "nosuid" "nodev" "size=${toString cfg.tmpOnTmpfsSize}" ];
+        mountConfig.Options = concatStringsSep "," [ "mode=1777"
+                                                     "strictatime"
+                                                     "rw"
+                                                     "nosuid"
+                                                     "nodev"
+                                                     "size=${toString cfg.tmpOnTmpfsSize}" ];
       }
     ];
 
diff --git a/nixos/modules/system/etc/etc-activation.nix b/nixos/modules/system/etc/etc-activation.nix
new file mode 100644
index 00000000000..78010495018
--- /dev/null
+++ b/nixos/modules/system/etc/etc-activation.nix
@@ -0,0 +1,12 @@
+{ config, lib, ... }:
+let
+  inherit (lib) stringAfter;
+in {
+
+  imports = [ ./etc.nix ];
+
+  config = {
+    system.activationScripts.etc =
+      stringAfter [ "users" "groups" ] config.system.build.etcActivationCommands;
+  };
+}
diff --git a/nixos/modules/system/etc/etc.nix b/nixos/modules/system/etc/etc.nix
index 6cc8c341e6d..ed552fecec5 100644
--- a/nixos/modules/system/etc/etc.nix
+++ b/nixos/modules/system/etc/etc.nix
@@ -66,6 +66,8 @@ in
 
 {
 
+  imports = [ ../build.nix ];
+
   ###### interface
 
   options = {
@@ -188,14 +190,12 @@ in
   config = {
 
     system.build.etc = etc;
-
-    system.activationScripts.etc = stringAfter [ "users" "groups" ]
+    system.build.etcActivationCommands =
       ''
         # Set up the statically computed bits of /etc.
         echo "setting up /etc..."
         ${pkgs.perl.withPackages (p: [ p.FileSlurp ])}/bin/perl ${./setup-etc.pl} ${etc}/etc
       '';
-
   };
 
 }
diff --git a/nixos/modules/system/etc/test.nix b/nixos/modules/system/etc/test.nix
new file mode 100644
index 00000000000..5e43b155038
--- /dev/null
+++ b/nixos/modules/system/etc/test.nix
@@ -0,0 +1,70 @@
+{ lib
+, coreutils
+, fakechroot
+, fakeroot
+, evalMinimalConfig
+, pkgsModule
+, runCommand
+, util-linux
+, vmTools
+, writeText
+}:
+let
+  node = evalMinimalConfig ({ config, ... }: {
+    imports = [ pkgsModule ../etc/etc.nix ];
+    environment.etc."passwd" = {
+      text = passwdText;
+    };
+    environment.etc."hosts" = {
+      text = hostsText;
+      mode = "0751";
+    };
+  });
+  passwdText = ''
+    root:x:0:0:System administrator:/root:/run/current-system/sw/bin/bash
+  '';
+  hostsText = ''
+    127.0.0.1 localhost
+    ::1 localhost
+    # testing...
+  '';
+in
+lib.recurseIntoAttrs {
+  test-etc-vm =
+    vmTools.runInLinuxVM (runCommand "test-etc-vm" { } ''
+      mkdir -p /etc
+      ${node.config.system.build.etcActivationCommands}
+      set -x
+      [[ -L /etc/passwd ]]
+      diff /etc/passwd ${writeText "expected-passwd" passwdText}
+      [[ 751 = $(stat --format %a /etc/hosts) ]]
+      diff /etc/hosts ${writeText "expected-hosts" hostsText}
+      set +x
+      touch $out
+    '');
+
+  # fakeroot is behaving weird
+  test-etc-fakeroot =
+    runCommand "test-etc"
+      {
+        nativeBuildInputs = [
+          fakeroot
+          fakechroot
+          # for chroot
+          coreutils
+          # fakechroot needs getopt, which is provided by util-linux
+          util-linux
+        ];
+        fakeRootCommands = ''
+          mkdir -p /etc
+          ${node.config.system.build.etcActivationCommands}
+          diff /etc/hosts ${writeText "expected-hosts" hostsText}
+          touch $out
+        '';
+      } ''
+      mkdir fake-root
+      export FAKECHROOT_EXCLUDE_PATH=/dev:/proc:/sys:${builtins.storeDir}:$out
+      fakechroot fakeroot chroot $PWD/fake-root bash -c 'source $stdenv/setup; eval "$fakeRootCommands"'
+    '';
+
+}
diff --git a/nixos/modules/tasks/network-interfaces.nix b/nixos/modules/tasks/network-interfaces.nix
index 1dac405ac30..5c91993771e 100644
--- a/nixos/modules/tasks/network-interfaces.nix
+++ b/nixos/modules/tasks/network-interfaces.nix
@@ -305,7 +305,7 @@ let
         enable = mkOption {
           type = types.bool;
           default = false;
-          description = "Wether to enable wol on this interface.";
+          description = "Whether to enable wol on this interface.";
         };
       };
     };
diff --git a/nixos/modules/testing/test-instrumentation.nix b/nixos/modules/testing/test-instrumentation.nix
index a7011be7e04..01447e6ada8 100644
--- a/nixos/modules/testing/test-instrumentation.nix
+++ b/nixos/modules/testing/test-instrumentation.nix
@@ -109,6 +109,10 @@ in
       # Allow very slow start
       DefaultTimeoutStartSec=300
     '';
+    systemd.user.extraConfig = ''
+      # Allow very slow start
+      DefaultTimeoutStartSec=300
+    '';
 
     boot.consoleLogLevel = 7;
 
diff --git a/nixos/modules/virtualisation/amazon-image.nix b/nixos/modules/virtualisation/amazon-image.nix
index fe248a94488..bd7077ff7ea 100644
--- a/nixos/modules/virtualisation/amazon-image.nix
+++ b/nixos/modules/virtualisation/amazon-image.nix
@@ -155,7 +155,7 @@ in
     systemd.services."serial-getty@ttyS0".enable = true;
 
     # Creates symlinks for block device names.
-    services.udev.packages = [ pkgs.ec2-utils ];
+    services.udev.packages = [ pkgs.amazon-ec2-utils ];
 
     # Force getting the hostname from EC2.
     networking.hostName = mkDefault "";
diff --git a/nixos/modules/virtualisation/containerd.nix b/nixos/modules/virtualisation/containerd.nix
index 898a66e7b04..ea89a994b17 100644
--- a/nixos/modules/virtualisation/containerd.nix
+++ b/nixos/modules/virtualisation/containerd.nix
@@ -53,6 +53,7 @@ in
     virtualisation.containerd = {
       args.config = toString containerdConfigChecked;
       settings = {
+        version = 2;
         plugins."io.containerd.grpc.v1.cri" = {
          containerd.snapshotter =
            lib.mkIf config.boot.zfs.enabled (lib.mkOptionDefault "zfs");
diff --git a/nixos/modules/virtualisation/docker-rootless.nix b/nixos/modules/virtualisation/docker-rootless.nix
index 0e7f0503142..d371f67ecdc 100644
--- a/nixos/modules/virtualisation/docker-rootless.nix
+++ b/nixos/modules/virtualisation/docker-rootless.nix
@@ -76,7 +76,11 @@ in
       # needs newuidmap from pkgs.shadow
       path = [ "/run/wrappers" ];
       environment = proxy_env;
-      unitConfig.StartLimitInterval = "60s";
+      unitConfig = {
+        # docker-rootless doesn't support running as root.
+        ConditionUser = "!root";
+        StartLimitInterval = "60s";
+      };
       serviceConfig = {
         Type = "notify";
         ExecStart = "${cfg.package}/bin/dockerd-rootless --config-file=${daemonSettingsFile}";
diff --git a/nixos/modules/virtualisation/fetch-instance-ssh-keys.bash b/nixos/modules/virtualisation/fetch-instance-ssh-keys.bash
deleted file mode 100644
index 4a860196111..00000000000
--- a/nixos/modules/virtualisation/fetch-instance-ssh-keys.bash
+++ /dev/null
@@ -1,36 +0,0 @@
-#!/usr/bin/env bash
-
-set -euo pipefail
-
-WGET() {
-    wget --retry-connrefused -t 15 --waitretry=10 --header='Metadata-Flavor: Google' "$@"
-}
-
-# When dealing with cryptographic keys, we want to keep things private.
-umask 077
-mkdir -p /root/.ssh
-
-echo "Fetching authorized keys..."
-WGET -O /tmp/auth_keys http://metadata.google.internal/computeMetadata/v1/instance/attributes/sshKeys
-
-# Read keys one by one, split in case Google decided
-# to append metadata (it does sometimes) and add to
-# authorized_keys if not already present.
-touch /root/.ssh/authorized_keys
-while IFS='' read -r line || [[ -n "$line" ]]; do
-    keyLine=$(echo -n "$line" | cut -d ':' -f2)
-    IFS=' ' read -r -a array <<<"$keyLine"
-    if [[ ${#array[@]} -ge 3 ]]; then
-        echo "${array[@]:0:3}" >>/tmp/new_keys
-        echo "Added ${array[*]:2} to authorized_keys"
-    fi
-done </tmp/auth_keys
-mv /tmp/new_keys /root/.ssh/authorized_keys
-chmod 600 /root/.ssh/authorized_keys
-
-echo "Fetching host keys..."
-WGET -O /tmp/ssh_host_ed25519_key http://metadata.google.internal/computeMetadata/v1/instance/attributes/ssh_host_ed25519_key
-WGET -O /tmp/ssh_host_ed25519_key.pub http://metadata.google.internal/computeMetadata/v1/instance/attributes/ssh_host_ed25519_key_pub
-mv -f /tmp/ssh_host_ed25519_key* /etc/ssh/
-chmod 600 /etc/ssh/ssh_host_ed25519_key
-chmod 644 /etc/ssh/ssh_host_ed25519_key.pub
diff --git a/nixos/modules/virtualisation/google-compute-config.nix b/nixos/modules/virtualisation/google-compute-config.nix
index cff48d20b2b..44d2a589511 100644
--- a/nixos/modules/virtualisation/google-compute-config.nix
+++ b/nixos/modules/virtualisation/google-compute-config.nix
@@ -1,8 +1,5 @@
 { config, lib, pkgs, ... }:
 with lib;
-let
-  gce = pkgs.google-compute-engine;
-in
 {
   imports = [
     ../profiles/headless.nix
@@ -40,7 +37,8 @@ in
   security.googleOsLogin.enable = true;
 
   # Use GCE udev rules for dynamic disk volumes
-  services.udev.packages = [ gce ];
+  services.udev.packages = [ pkgs.google-guest-configs ];
+  services.udev.path = [ pkgs.google-guest-configs ];
 
   # Force getting the hostname from Google Compute.
   networking.hostName = mkDefault "";
@@ -48,12 +46,6 @@ in
   # Always include cryptsetup so that NixOps can use it.
   environment.systemPackages = [ pkgs.cryptsetup ];
 
-  # Make sure GCE image does not replace host key that NixOps sets
-  environment.etc."default/instance_configs.cfg".text = lib.mkDefault ''
-    [InstanceSetup]
-    set_host_keys = false
-  '';
-
   # Rely on GCP's firewall instead
   networking.firewall.enable = mkDefault false;
 
@@ -69,105 +61,42 @@ in
   # GC has 1460 MTU
   networking.interfaces.eth0.mtu = 1460;
 
-  # Used by NixOps
-  systemd.services.fetch-instance-ssh-keys = {
-    description = "Fetch host keys and authorized_keys for root user";
-
-    wantedBy = [ "sshd.service" ];
-    before = [ "sshd.service" ];
-    after = [ "network-online.target" ];
-    wants = [ "network-online.target" ];
-    path = [ pkgs.wget ];
-
-    serviceConfig = {
-      Type = "oneshot";
-      ExecStart = pkgs.runCommand "fetch-instance-ssh-keys" { } ''
-        cp ${./fetch-instance-ssh-keys.bash} $out
-        chmod +x $out
-        ${pkgs.shfmt}/bin/shfmt -i 4 -d $out
-        ${pkgs.shellcheck}/bin/shellcheck $out
-        patchShebangs $out
-      '';
-      PrivateTmp = true;
-      StandardError = "journal+console";
-      StandardOutput = "journal+console";
-    };
+  systemd.packages = [ pkgs.google-guest-agent ];
+  systemd.services.google-guest-agent = {
+    wantedBy = [ "multi-user.target" ];
+    restartTriggers = [ config.environment.etc."default/instance_configs.cfg".source ];
+    path = lib.optional config.users.mutableUsers pkgs.shadow;
   };
+  systemd.services.google-startup-scripts.wantedBy = [ "multi-user.target" ];
+  systemd.services.google-shutdown-scripts.wantedBy = [ "multi-user.target" ];
 
-  systemd.services.google-instance-setup = {
-    description = "Google Compute Engine Instance Setup";
-    after = [ "network-online.target" "network.target" "rsyslog.service" ];
-    before = [ "sshd.service" ];
-    path = with pkgs; [ coreutils ethtool openssh ];
-    serviceConfig = {
-      ExecStart = "${gce}/bin/google_instance_setup";
-      StandardOutput="journal+console";
-      Type = "oneshot";
-    };
-    wantedBy = [ "sshd.service" "multi-user.target" ];
-  };
+  security.sudo.extraRules = mkIf config.users.mutableUsers [
+    { groups = [ "google-sudoers" ]; commands = [ { command = "ALL"; options = [ "NOPASSWD" ]; } ]; }
+  ];
 
-  systemd.services.google-network-daemon = {
-    description = "Google Compute Engine Network Daemon";
-    after = [ "network-online.target" "network.target" "google-instance-setup.service" ];
-    path = with pkgs; [ iproute2 ];
-    serviceConfig = {
-      ExecStart = "${gce}/bin/google_network_daemon";
-      StandardOutput="journal+console";
-      Type="simple";
-    };
-    wantedBy = [ "multi-user.target" ];
-  };
+  users.groups.google-sudoers = mkIf config.users.mutableUsers { };
 
-  systemd.services.google-clock-skew-daemon = {
-    description = "Google Compute Engine Clock Skew Daemon";
-    after = [ "network.target" "google-instance-setup.service" "google-network-daemon.service" ];
-    serviceConfig = {
-      ExecStart = "${gce}/bin/google_clock_skew_daemon";
-      StandardOutput="journal+console";
-      Type = "simple";
-    };
-    wantedBy = ["multi-user.target"];
-  };
+  boot.extraModprobeConfig = lib.readFile "${pkgs.google-guest-configs}/etc/modprobe.d/gce-blacklist.conf";
 
+  environment.etc."sysctl.d/60-gce-network-security.conf".source = "${pkgs.google-guest-configs}/etc/sysctl.d/60-gce-network-security.conf";
 
-  systemd.services.google-shutdown-scripts = {
-    description = "Google Compute Engine Shutdown Scripts";
-    after = [
-      "network-online.target"
-      "network.target"
-      "rsyslog.service"
-      "google-instance-setup.service"
-      "google-network-daemon.service"
-    ];
-    serviceConfig = {
-      ExecStart = "${pkgs.coreutils}/bin/true";
-      ExecStop = "${gce}/bin/google_metadata_script_runner --script-type shutdown";
-      RemainAfterExit = true;
-      StandardOutput="journal+console";
-      TimeoutStopSec = "0";
-      Type = "oneshot";
-    };
-    wantedBy = [ "multi-user.target" ];
-  };
+  environment.etc."default/instance_configs.cfg".text = ''
+    [Accounts]
+    useradd_cmd = useradd -m -s /run/current-system/sw/bin/bash -p * {user}
 
-  systemd.services.google-startup-scripts = {
-    description = "Google Compute Engine Startup Scripts";
-    after = [
-      "network-online.target"
-      "network.target"
-      "rsyslog.service"
-      "google-instance-setup.service"
-      "google-network-daemon.service"
-    ];
-    serviceConfig = {
-      ExecStart = "${gce}/bin/google_metadata_script_runner --script-type startup";
-      KillMode = "process";
-      StandardOutput = "journal+console";
-      Type = "oneshot";
-    };
-    wantedBy = [ "multi-user.target" ];
-  };
+    [Daemons]
+    accounts_daemon = ${boolToString config.users.mutableUsers}
 
-  environment.etc."sysctl.d/11-gce-network-security.conf".source = "${gce}/sysctl.d/11-gce-network-security.conf";
+    [InstanceSetup]
+    # Make sure GCE image does not replace host key that NixOps sets.
+    set_host_keys = false
+
+    [MetadataScripts]
+    default_shell = ${pkgs.stdenv.shell}
+
+    [NetworkInterfaces]
+    dhclient_script = ${pkgs.google-guest-configs}/bin/google-dhclient-script
+    # We set up network interfaces declaratively.
+    setup = false
+  '';
 }
diff --git a/nixos/modules/virtualisation/kvmgt.nix b/nixos/modules/virtualisation/kvmgt.nix
index 72bd2c24e56..5e7a73bec90 100644
--- a/nixos/modules/virtualisation/kvmgt.nix
+++ b/nixos/modules/virtualisation/kvmgt.nix
@@ -82,5 +82,5 @@ in {
     };
   };
 
-  meta.maintainers = with maintainers; [ ];
+  meta.maintainers = with maintainers; [ patryk27 ];
 }
diff --git a/nixos/modules/virtualisation/qemu-vm.nix b/nixos/modules/virtualisation/qemu-vm.nix
index 29e3aa024df..51438935894 100644
--- a/nixos/modules/virtualisation/qemu-vm.nix
+++ b/nixos/modules/virtualisation/qemu-vm.nix
@@ -632,6 +632,15 @@ in
             Enable the Qemu guest agent.
           '';
         };
+
+      virtioKeyboard =
+        mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Enable the virtio-keyboard device.
+          '';
+        };
     };
 
     virtualisation.useNixStoreImage =
@@ -835,7 +844,9 @@ in
 
     # FIXME: Consolidate this one day.
     virtualisation.qemu.options = mkMerge [
-      [ "-device virtio-keyboard" ]
+      (mkIf cfg.qemu.virtioKeyboard [
+        "-device virtio-keyboard"
+      ])
       (mkIf pkgs.stdenv.hostPlatform.isx86 [
         "-usb" "-device usb-tablet,bus=usb-bus.0"
       ])
diff --git a/nixos/modules/virtualisation/virtualbox-guest.nix b/nixos/modules/virtualisation/virtualbox-guest.nix
index f702fb4e525..7b55b3b9759 100644
--- a/nixos/modules/virtualisation/virtualbox-guest.nix
+++ b/nixos/modules/virtualisation/virtualbox-guest.nix
@@ -68,7 +68,7 @@ in
         SUBSYSTEM=="misc", KERNEL=="vboxguest", TAG+="systemd"
       '';
   } (mkIf cfg.x11 {
-    services.xserver.videoDrivers = mkOverride 50 [ "vmware" "virtualbox" "modesetting" ];
+    services.xserver.videoDrivers = [ "vmware" "virtualbox" "modesetting" ];
 
     services.xserver.config =
       ''
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index b75cbf4d10b..a0beaf98de5 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -32,6 +32,7 @@ in
   acme = handleTest ./acme.nix {};
   adguardhome = handleTest ./adguardhome.nix {};
   aesmd = handleTest ./aesmd.nix {};
+  agate = handleTest ./web-servers/agate.nix {};
   agda = handleTest ./agda.nix {};
   airsonic = handleTest ./airsonic.nix {};
   amazon-init-shell = handleTest ./amazon-init-shell.nix {};
@@ -51,6 +52,7 @@ in
   bitcoind = handleTest ./bitcoind.nix {};
   bittorrent = handleTest ./bittorrent.nix {};
   blockbook-frontend = handleTest ./blockbook-frontend.nix {};
+  blocky = handleTest ./blocky.nix {};
   boot = handleTestOn ["x86_64-linux" "aarch64-linux"] ./boot.nix {};
   boot-stage1 = handleTest ./boot-stage1.nix {};
   borgbackup = handleTest ./borgbackup.nix {};
@@ -141,6 +143,7 @@ in
   env = handleTest ./env.nix {};
   ergo = handleTest ./ergo.nix {};
   ergochat = handleTest ./ergochat.nix {};
+  etc = pkgs.callPackage ../modules/system/etc/test.nix { inherit evalMinimalConfig; };
   etcd = handleTestOn ["x86_64-linux"] ./etcd.nix {};
   etcd-cluster = handleTestOn ["x86_64-linux"] ./etcd-cluster.nix {};
   etebase-server = handleTest ./etebase-server.nix {};
@@ -237,7 +240,6 @@ in
   jibri = handleTest ./jibri.nix {};
   jirafeau = handleTest ./jirafeau.nix {};
   jitsi-meet = handleTest ./jitsi-meet.nix {};
-  k3s = handleTest ./k3s.nix {};
   k3s-single-node = handleTest ./k3s-single-node.nix {};
   k3s-single-node-docker = handleTest ./k3s-single-node-docker.nix {};
   kafka = handleTest ./kafka.nix {};
@@ -305,6 +307,7 @@ in
   moodle = handleTest ./moodle.nix {};
   morty = handleTest ./morty.nix {};
   mosquitto = handleTest ./mosquitto.nix {};
+  moosefs = handleTest ./moosefs.nix {};
   mpd = handleTest ./mpd.nix {};
   mpv = handleTest ./mpv.nix {};
   mumble = handleTest ./mumble.nix {};
@@ -440,6 +443,7 @@ in
   resolv = handleTest ./resolv.nix {};
   restartByActivationScript = handleTest ./restart-by-activation-script.nix {};
   restic = handleTest ./restic.nix {};
+  retroarch = handleTest ./retroarch.nix {};
   riak = handleTest ./riak.nix {};
   robustirc-bridge = handleTest ./robustirc-bridge.nix {};
   roundcube = handleTest ./roundcube.nix {};
diff --git a/nixos/tests/blocky.nix b/nixos/tests/blocky.nix
new file mode 100644
index 00000000000..18e7f45e1c7
--- /dev/null
+++ b/nixos/tests/blocky.nix
@@ -0,0 +1,34 @@
+import ./make-test-python.nix {
+  name = "blocky";
+
+  nodes = {
+    server = { pkgs, ... }: {
+      environment.systemPackages = [ pkgs.dnsutils ];
+      services.blocky = {
+        enable = true;
+
+        settings = {
+          customDNS = {
+            mapping = {
+              "printer.lan" = "192.168.178.3,2001:0db8:85a3:08d3:1319:8a2e:0370:7344";
+            };
+          };
+          upstream = {
+            default = [ "8.8.8.8" "1.1.1.1" ];
+          };
+          port = 53;
+          httpPort = 5000;
+          logLevel = "info";
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    with subtest("Service test"):
+        server.wait_for_unit("blocky.service")
+        server.wait_for_open_port(53)
+        server.wait_for_open_port(5000)
+        server.succeed("dig @127.0.0.1 +short -x 192.168.178.3 | grep -qF printer.lan")
+  '';
+}
diff --git a/nixos/tests/bpf.nix b/nixos/tests/bpf.nix
index 233c7dab1ee..e479cd05792 100644
--- a/nixos/tests/bpf.nix
+++ b/nixos/tests/bpf.nix
@@ -18,8 +18,12 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     # simple BEGIN probe (user probe on bpftrace itself)
     print(machine.succeed("bpftrace -e 'BEGIN { print(\"ok\"); exit(); }'"))
     # tracepoint
-    print(machine.succeed("bpftrace -e 'tracepoint:syscalls:sys_enter_* { print(probe); exit(); }'"))
+    print(machine.succeed("bpftrace -e 'tracepoint:syscalls:sys_enter_* { print(probe); exit() }'"))
     # kprobe
     print(machine.succeed("bpftrace -e 'kprobe:schedule { print(probe); exit() }'"))
+    # BTF
+    print(machine.succeed("bpftrace -e 'kprobe:schedule { "
+        "    printf(\"tgid: %d\", ((struct task_struct*) curtask)->tgid); exit() "
+        "}'"))
   '';
 })
diff --git a/nixos/tests/cloud-init.nix b/nixos/tests/cloud-init.nix
index e06cbd056a3..3f191ff5616 100644
--- a/nixos/tests/cloud-init.nix
+++ b/nixos/tests/cloud-init.nix
@@ -35,6 +35,24 @@ let
       public-keys:
         - "${snakeOilPublicKey}"
       EOF
+
+      cat << EOF > $out/iso/network-config
+      version: 1
+      config:
+          - type: physical
+            name: eth0
+            mac_address: '52:54:00:12:34:56'
+            subnets:
+            - type: static
+              address: '12.34.56.78'
+              netmask: '255.255.255.0'
+              gateway: '12.34.56.9'
+          - type: nameserver
+            address:
+            - '8.8.8.8'
+            search:
+            - 'example.com'
+      EOF
       ${pkgs.cdrkit}/bin/genisoimage -volid cidata -joliet -rock -o $out/metadata.iso $out/iso
       '';
   };
@@ -46,9 +64,13 @@ in makeTest {
   machine = { ... }:
   {
     virtualisation.qemu.options = [ "-cdrom" "${metadataDrive}/metadata.iso" ];
-    services.cloud-init.enable = true;
+    services.cloud-init = {
+      enable = true;
+      network.enable = true;
+    };
     services.openssh.enable = true;
     networking.hostName = "";
+    networking.useDHCP = false;
   };
   testScript = ''
     # To wait until cloud-init terminates its run
@@ -80,5 +102,8 @@ in makeTest {
         ).strip()
         == "test"
     )
+
+    assert "default via 12.34.56.9 dev eth0 proto static" in unnamed.succeed("ip route")
+    assert "12.34.56.0/24 dev eth0 proto kernel scope link src 12.34.56.78" in unnamed.succeed("ip route")
   '';
 }
diff --git a/nixos/tests/google-oslogin/default.nix b/nixos/tests/google-oslogin/default.nix
index dea660ed05a..72c87d7153b 100644
--- a/nixos/tests/google-oslogin/default.nix
+++ b/nixos/tests/google-oslogin/default.nix
@@ -31,10 +31,10 @@ in {
 
     # mockserver should return a non-expired ssh key for both mockuser and mockadmin
     server.succeed(
-        f'${pkgs.google-compute-engine-oslogin}/bin/google_authorized_keys {MOCKUSER} | grep -q "${snakeOilPublicKey}"'
+        f'${pkgs.google-guest-oslogin}/bin/google_authorized_keys {MOCKUSER} | grep -q "${snakeOilPublicKey}"'
     )
     server.succeed(
-        f'${pkgs.google-compute-engine-oslogin}/bin/google_authorized_keys {MOCKADMIN} | grep -q "${snakeOilPublicKey}"'
+        f'${pkgs.google-guest-oslogin}/bin/google_authorized_keys {MOCKADMIN} | grep -q "${snakeOilPublicKey}"'
     )
 
     # install snakeoil ssh key on the client, and provision .ssh/config file
diff --git a/nixos/tests/google-oslogin/server.nix b/nixos/tests/google-oslogin/server.nix
index a0a3144ae69..faf5e847d7e 100644
--- a/nixos/tests/google-oslogin/server.nix
+++ b/nixos/tests/google-oslogin/server.nix
@@ -23,7 +23,5 @@ in {
   security.googleOsLogin.enable = true;
 
   # Mock google service
-  networking.extraHosts = ''
-    127.0.0.1 metadata.google.internal
-  '';
+  networking.interfaces.lo.ipv4.addresses = [ { address = "169.254.169.254"; prefixLength = 32; } ];
 }
diff --git a/nixos/tests/google-oslogin/server.py b/nixos/tests/google-oslogin/server.py
index 5ea9bbd2c96..5ea9bbd2c96 100644..100755
--- a/nixos/tests/google-oslogin/server.py
+++ b/nixos/tests/google-oslogin/server.py
diff --git a/nixos/tests/haproxy.nix b/nixos/tests/haproxy.nix
index 2c3878131b6..b6ff4102fe6 100644
--- a/nixos/tests/haproxy.nix
+++ b/nixos/tests/haproxy.nix
@@ -16,7 +16,6 @@ import ./make-test-python.nix ({ pkgs, ...}: {
           frontend http
             bind *:80
             mode http
-            option http-use-htx
             http-request use-service prometheus-exporter if { path /metrics }
             use_backend http_server
         '';
diff --git a/nixos/tests/home-assistant.nix b/nixos/tests/home-assistant.nix
index 5b1c07c92da..31f8a4bcc19 100644
--- a/nixos/tests/home-assistant.nix
+++ b/nixos/tests/home-assistant.nix
@@ -10,6 +10,7 @@ in {
 
   nodes.hass = { pkgs, ... }: {
     environment.systemPackages = with pkgs; [ mosquitto ];
+
     services.mosquitto = {
       enable = true;
       listeners = [ {
@@ -21,14 +22,42 @@ in {
         };
       } ];
     };
+
+    services.postgresql = {
+      enable = true;
+      ensureDatabases = [ "hass" ];
+      ensureUsers = [{
+        name = "hass";
+        ensurePermissions = {
+          "DATABASE hass" = "ALL PRIVILEGES";
+        };
+      }];
+    };
+
     services.home-assistant = {
-      inherit configDir;
       enable = true;
+      inherit configDir;
+
+      # tests loading components by overriding the package
       package = (pkgs.home-assistant.override {
+        extraPackages = ps: with ps; [
+          colorama
+        ];
         extraComponents = [ "zha" ];
       }).overrideAttrs (oldAttrs: {
         doInstallCheck = false;
       });
+
+      # tests loading components from the module
+      extraComponents = [
+        "wake_on_lan"
+      ];
+
+      # test extra package passing from the module
+      extraPackages = python3Packages: with python3Packages; [
+        psycopg2
+      ];
+
       config = {
         homeassistant = {
           name = "Home";
@@ -37,34 +66,58 @@ in {
           longitude = "0.0";
           elevation = 0;
         };
+
+        # configure the recorder component to use the postgresql db
+        recorder.db_url = "postgresql://@/hass";
+
+        # we can't load default_config, because the updater requires
+        # network access and would cause an error, so load frontend
+        # here explicitly.
+        # https://www.home-assistant.io/integrations/frontend/
         frontend = {};
+
+        # configure an mqtt broker connection
+        # https://www.home-assistant.io/integrations/mqtt
         mqtt = {
           broker = "127.0.0.1";
           username = mqttUsername;
           password = mqttPassword;
         };
-        binary_sensor = [{
+
+        # create a mqtt sensor that syncs state with its mqtt topic
+        # https://www.home-assistant.io/integrations/sensor.mqtt/
+        binary_sensor = [ {
           platform = "mqtt";
           state_topic = "home-assistant/test";
           payload_on = "let_there_be_light";
           payload_off = "off";
-        }];
-        wake_on_lan = {};
-        switch = [{
+        } ];
+
+        # set up a wake-on-lan switch to test capset capability required
+        # for the ping suid wrapper
+        # https://www.home-assistant.io/integrations/wake_on_lan/
+        switch = [ {
           platform = "wake_on_lan";
           mac = "00:11:22:33:44:55";
           host = "127.0.0.1";
-        }];
-        # tests component-based capability assignment (CAP_NET_BIND_SERVICE)
+        } ];
+
+        # test component-based capability assignment (CAP_NET_BIND_SERVICE)
+        # https://www.home-assistant.io/integrations/emulated_hue/
         emulated_hue = {
           host_ip = "127.0.0.1";
           listen_port = 80;
         };
+
+        # show mqtt interaction in the log
+        # https://www.home-assistant.io/integrations/logger/
         logger = {
           default = "info";
           logs."homeassistant.components.mqtt" = "debug";
         };
       };
+
+      # configure the sample lovelace dashboard
       lovelaceConfig = {
         title = "My Awesome Home";
         views = [{
@@ -81,34 +134,57 @@ in {
   };
 
   testScript = ''
+    import re
+
     start_all()
+
+    # Parse the package path out of the systemd unit, as we cannot
+    # access the final package, that is overriden inside the module,
+    # by any other means.
+    pattern = re.compile(r"path=(?P<path>[\/a-z0-9-.]+)\/bin\/hass")
+    response = hass.execute("systemctl show -p ExecStart home-assistant.service")[1]
+    match = pattern.search(response)
+    package = match.group('path')
+
     hass.wait_for_unit("home-assistant.service")
+
     with subtest("Check that YAML configuration file is in place"):
         hass.succeed("test -L ${configDir}/configuration.yaml")
-    with subtest("lovelace config is copied because lovelaceConfigWritable = true"):
+
+    with subtest("Check the lovelace config is copied because lovelaceConfigWritable = true"):
         hass.succeed("test -f ${configDir}/ui-lovelace.yaml")
+
+    with subtest("Check extraComponents and extraPackages are considered from the package"):
+        hass.succeed(f"grep -q 'colorama' {package}/extra_packages")
+        hass.succeed(f"grep -q 'zha' {package}/extra_components")
+
+    with subtest("Check extraComponents and extraPackages are considered from the module"):
+        hass.succeed(f"grep -q 'psycopg2' {package}/extra_packages")
+        hass.succeed(f"grep -q 'wake_on_lan' {package}/extra_components")
+
     with subtest("Check that Home Assistant's web interface and API can be reached"):
+        hass.wait_until_succeeds("journalctl -u home-assistant.service | grep -q 'Home Assistant initialized in'")
         hass.wait_for_open_port(8123)
         hass.succeed("curl --fail http://localhost:8123/lovelace")
+
     with subtest("Toggle a binary sensor using MQTT"):
         hass.wait_for_open_port(1883)
         hass.succeed(
             "mosquitto_pub -V mqttv5 -t home-assistant/test -u ${mqttUsername} -P '${mqttPassword}' -m let_there_be_light"
         )
+
     with subtest("Check that capabilities are passed for emulated_hue to bind to port 80"):
         hass.wait_for_open_port(80)
         hass.succeed("curl --fail http://localhost:80/description.xml")
+
     with subtest("Check extra components are considered in systemd unit hardening"):
         hass.succeed("systemctl show -p DeviceAllow home-assistant.service | grep -q char-ttyUSB")
+
     with subtest("Print log to ease debugging"):
         output_log = hass.succeed("cat ${configDir}/home-assistant.log")
         print("\n### home-assistant.log ###\n")
         print(output_log + "\n")
 
-    # wait for home-assistant to fully boot
-    hass.sleep(30)
-    hass.wait_for_unit("home-assistant.service")
-
     with subtest("Check that no errors were logged"):
         assert "ERROR" not in output_log
 
@@ -117,7 +193,7 @@ in {
         assert "let_there_be_light" in output_log
 
     with subtest("Check systemd unit hardening"):
-        hass.log(hass.succeed("systemctl show home-assistant.service"))
+        hass.log(hass.succeed("systemctl cat home-assistant.service"))
         hass.log(hass.succeed("systemd-analyze security home-assistant.service"))
   '';
 })
diff --git a/nixos/tests/moosefs.nix b/nixos/tests/moosefs.nix
new file mode 100644
index 00000000000..0dc08748b82
--- /dev/null
+++ b/nixos/tests/moosefs.nix
@@ -0,0 +1,89 @@
+import ./make-test-python.nix ({ pkgs, ... } :
+
+let
+  master = { pkgs, ... } : {
+    # data base is stored in memory
+    # server crashes with default memory size
+    virtualisation.memorySize = 1024;
+
+    services.moosefs.master = {
+      enable = true;
+      openFirewall = true;
+      exports = [
+        "* / rw,alldirs,admin,maproot=0:0"
+        "* . rw"
+      ];
+    };
+  };
+
+  chunkserver = { pkgs, ... } : {
+    virtualisation.emptyDiskImages = [ 4096 ];
+    boot.initrd.postDeviceCommands = ''
+      ${pkgs.e2fsprogs}/bin/mkfs.ext4 -L data /dev/vdb
+    '';
+
+    fileSystems = pkgs.lib.mkVMOverride {
+      "/data" = {
+        device = "/dev/disk/by-label/data";
+        fsType = "ext4";
+      };
+    };
+
+    services.moosefs = {
+      masterHost = "master";
+      chunkserver = {
+        openFirewall = true;
+        enable = true;
+        hdds = [ "~/data" ];
+      };
+    };
+  };
+
+  metalogger = { pkgs, ... } : {
+    services.moosefs = {
+      masterHost = "master";
+      metalogger.enable = true;
+    };
+  };
+
+  client = { pkgs, ... } : {
+    services.moosefs.client.enable = true;
+  };
+
+in {
+  name = "moosefs";
+
+  nodes= {
+    inherit master;
+    inherit metalogger;
+    chunkserver1 = chunkserver;
+    chunkserver2 = chunkserver;
+    client1 = client;
+    client2 = client;
+  };
+
+  testScript = ''
+    # prepare master server
+    master.start()
+    master.wait_for_unit("multi-user.target")
+    master.succeed("mfsmaster-init")
+    master.succeed("systemctl restart mfs-master")
+    master.wait_for_unit("mfs-master.service")
+
+    metalogger.wait_for_unit("mfs-metalogger.service")
+
+    for chunkserver in [chunkserver1, chunkserver2]:
+        chunkserver.wait_for_unit("multi-user.target")
+        chunkserver.succeed("chown moosefs:moosefs /data")
+        chunkserver.succeed("systemctl restart mfs-chunkserver")
+        chunkserver.wait_for_unit("mfs-chunkserver.service")
+
+    for client in [client1, client2]:
+        client.wait_for_unit("multi-user.target")
+        client.succeed("mkdir /moosefs")
+        client.succeed("mount -t moosefs master:/ /moosefs")
+
+    client1.succeed("echo test > /moosefs/file")
+    client2.succeed("grep test /moosefs/file")
+  '';
+})
diff --git a/nixos/tests/prometheus-exporters.nix b/nixos/tests/prometheus-exporters.nix
index 036c037e426..323d79c5b86 100644
--- a/nixos/tests/prometheus-exporters.nix
+++ b/nixos/tests/prometheus-exporters.nix
@@ -672,7 +672,7 @@ let
             basicAuth.nextcloud-exporter = "snakeoilpw";
             locations."/" = {
               root = "${pkgs.prometheus-nextcloud-exporter.src}/serverinfo/testdata";
-              tryFiles = "/negative-space.xml =404";
+              tryFiles = "/negative-space.json =404";
             };
           };
         };
diff --git a/nixos/tests/retroarch.nix b/nixos/tests/retroarch.nix
new file mode 100644
index 00000000000..4c96f9eabc8
--- /dev/null
+++ b/nixos/tests/retroarch.nix
@@ -0,0 +1,49 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+  {
+    name = "retroarch";
+    meta = with pkgs.lib.maintainers; { maintainers = [ j0hax ]; };
+
+    machine = { ... }:
+
+      {
+        imports = [ ./common/user-account.nix ];
+        services.xserver.enable = true;
+        services.xserver.desktopManager.retroarch = {
+          enable = true;
+          package = pkgs.retroarchFull;
+        };
+        services.xserver.displayManager = {
+          sddm.enable = true;
+          defaultSession = "RetroArch";
+          autoLogin = {
+            enable = true;
+            user = "alice";
+          };
+        };
+      };
+
+    testScript = { nodes, ... }:
+      let
+        user = nodes.machine.config.users.users.alice;
+        xdo = "${pkgs.xdotool}/bin/xdotool";
+      in ''
+        with subtest("Wait for login"):
+            start_all()
+            machine.wait_for_file("${user.home}/.Xauthority")
+            machine.succeed("xauth merge ${user.home}/.Xauthority")
+
+        with subtest("Check RetroArch started"):
+            machine.wait_until_succeeds("pgrep retroarch")
+            machine.wait_for_window("^RetroArch ")
+
+        with subtest("Check configuration created"):
+            machine.wait_for_file("${user.home}/.config/retroarch/retroarch.cfg")
+
+        with subtest("Wait to get a screenshot"):
+            machine.execute(
+                "${xdo} key Alt+F1 sleep 10"
+            )
+            machine.screenshot("screen")
+      '';
+  })
diff --git a/nixos/tests/sourcehut.nix b/nixos/tests/sourcehut.nix
index d1536c59322..55757e35f9b 100644
--- a/nixos/tests/sourcehut.nix
+++ b/nixos/tests/sourcehut.nix
@@ -125,13 +125,18 @@ in
     virtualisation.memorySize = 2 * 1024;
     networking.domain = domain;
     networking.extraHosts = ''
-      ${config.networking.primaryIPAddress} meta.${domain}
       ${config.networking.primaryIPAddress} builds.${domain}
+      ${config.networking.primaryIPAddress} git.${domain}
+      ${config.networking.primaryIPAddress} meta.${domain}
     '';
 
     services.sourcehut = {
       enable = true;
-      services = [ "meta" "builds" ];
+      services = [
+        "builds"
+        "git"
+        "meta"
+      ];
       nginx.enable = true;
       nginx.virtualHost = {
         forceSSL = true;
@@ -148,6 +153,8 @@ in
         #enableWorker = true;
         inherit images;
       };
+      git.enable = true;
+
       settings."sr.ht" = {
         global-domain = config.networking.domain;
         service-key = pkgs.writeText "service-key" "8b327279b77e32a3620e2fc9aabce491cc46e7d821fd6713b2a2e650ce114d01";
@@ -157,6 +164,10 @@ in
         oauth-client-secret = pkgs.writeText "buildsrht-oauth-client-secret" "2260e9c4d9b8dcedcef642860e0504bc";
         oauth-client-id = "299db9f9c2013170";
       };
+      settings."git.sr.ht" = {
+        oauth-client-secret = pkgs.writeText "gitsrht-oauth-client-secret" "3597288dc2c716e567db5384f493b09d";
+        oauth-client-id = "d07cb713d920702e";
+      };
       settings.webhooks.private-key = pkgs.writeText "webhook-key" "Ra3IjxgFiwG9jxgp4WALQIZw/BMYt30xWiOsqD0J7EA=";
     };
 
@@ -193,5 +204,9 @@ in
     machine.wait_for_open_port(5002)
     machine.succeed("curl -sL http://localhost:5002 | grep builds.${domain}")
     #machine.wait_for_unit("buildsrht-worker.service")
+
+    # Testing gitsrht
+    machine.wait_for_unit("gitsrht.service")
+    machine.succeed("curl -sL http://git.${domain} | grep git.${domain}")
   '';
 })
diff --git a/nixos/tests/ssh-keys.nix b/nixos/tests/ssh-keys.nix
index 07d422196ef..df9ff38a3b2 100644
--- a/nixos/tests/ssh-keys.nix
+++ b/nixos/tests/ssh-keys.nix
@@ -10,6 +10,6 @@ pkgs:
   snakeOilPublicKey = pkgs.lib.concatStrings [
     "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHA"
     "yNTYAAABBBChdA2BmwcG49OrQN33f/sj+OHL5sJhwVl2Qim0vkUJQCry1zFpKTa"
-    "9ZcDMiWaEhoAR6FGoaGI04ff7CS+1yybQ= sakeoil"
+    "9ZcDMiWaEhoAR6FGoaGI04ff7CS+1yybQ= snakeoil"
   ];
 }
diff --git a/nixos/tests/sudo.nix b/nixos/tests/sudo.nix
index ae9362ca70d..661fe9989e7 100644
--- a/nixos/tests/sudo.nix
+++ b/nixos/tests/sudo.nix
@@ -28,6 +28,10 @@ in
           enable = true;
           wheelNeedsPassword = false;
 
+          extraConfig = ''
+            Defaults lecture="never"
+          '';
+
           extraRules = [
             # SUDOERS SYNTAX CHECK (Test whether the module produces a valid output;
             # errors being detected by the visudo checks.
diff --git a/nixos/tests/sway.nix b/nixos/tests/sway.nix
index 43b8c847304..1e9e146c4b6 100644
--- a/nixos/tests/sway.nix
+++ b/nixos/tests/sway.nix
@@ -1,6 +1,4 @@
-import ./make-test-python.nix ({ pkgs, lib, ... }:
-
-{
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
   name = "sway";
   meta = {
     maintainers = with lib.maintainers; [ primeos synthetica ];
@@ -70,6 +68,14 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
   enableOCR = true;
 
   testScript = { nodes, ... }: ''
+    import shlex
+
+    def swaymsg(command: str, succeed=True):
+        with machine.nested(f"sending swaymsg {command!r}" + " (allowed to fail)" * (not succeed)):
+          (machine.succeed if succeed else machine.execute)(
+            f"su - alice -c {shlex.quote('swaymsg -- ' + command)}"
+          )
+
     start_all()
     machine.wait_for_unit("multi-user.target")
 
@@ -81,9 +87,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
     machine.wait_for_file("/tmp/sway-ipc.sock")
 
     # Test XWayland (foot does not support X):
-    machine.succeed(
-        "su - alice -c 'swaymsg exec WINIT_UNIX_BACKEND=x11 WAYLAND_DISPLAY=invalid alacritty'"
-    )
+    swaymsg("exec WINIT_UNIX_BACKEND=x11 WAYLAND_DISPLAY=invalid alacritty")
     machine.wait_for_text("alice@machine")
     machine.send_chars("test-x11\n")
     machine.wait_for_file("/tmp/test-x11-exit-ok")
@@ -107,9 +111,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
 
     # Test gpg-agent starting pinentry-gnome3 via D-Bus (tests if
     # $WAYLAND_DISPLAY is correctly imported into the D-Bus user env):
-    machine.succeed(
-        "su - alice -c 'swaymsg -- exec gpg --no-tty --yes --quick-generate-key test'"
-    )
+    swaymsg("exec gpg --no-tty --yes --quick-generate-key test")
     machine.wait_until_succeeds("pgrep --exact gpg")
     machine.wait_for_text("Passphrase")
     machine.screenshot("gpg_pinentry")
@@ -121,8 +123,15 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
     machine.wait_for_text("You pressed the exit shortcut.")
     machine.screenshot("sway_exit")
 
+    swaymsg("exec swaylock")
+    machine.wait_until_succeeds("pgrep -x swaylock")
+    machine.sleep(3)
+    machine.send_chars("${nodes.machine.config.users.users.alice.password}")
+    machine.send_key("ret")
+    machine.wait_until_fails("pgrep -x swaylock")
+
     # Exit Sway and verify process exit status 0:
-    machine.succeed("su - alice -c 'swaymsg exit || true'")
+    swaymsg("exit", succeed=False)
     machine.wait_until_fails("pgrep -x sway")
     machine.wait_for_file("/tmp/sway-exit-ok")
   '';
diff --git a/nixos/tests/switch-test.nix b/nixos/tests/switch-test.nix
index 8e425f0f877..4f297b6521d 100644
--- a/nixos/tests/switch-test.nix
+++ b/nixos/tests/switch-test.nix
@@ -18,6 +18,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
               Type = "oneshot";
               RemainAfterExit = true;
               ExecStart = "${pkgs.coreutils}/bin/true";
+              ExecReload = "${pkgs.coreutils}/bin/true";
             };
           };
         };
@@ -70,6 +71,97 @@ import ./make-test-python.nix ({ pkgs, ...} : {
           };
         };
 
+        simpleServiceWithExtraSection.configuration = {
+          imports = [ simpleServiceNostop.configuration ];
+          systemd.packages = [ (pkgs.writeTextFile {
+            name = "systemd-extra-section";
+            destination = "/etc/systemd/system/test.service";
+            text = ''
+              [X-Test]
+              X-Test-Value=a
+            '';
+          }) ];
+        };
+
+        simpleServiceWithExtraSectionOtherName.configuration = {
+          imports = [ simpleServiceNostop.configuration ];
+          systemd.packages = [ (pkgs.writeTextFile {
+            name = "systemd-extra-section";
+            destination = "/etc/systemd/system/test.service";
+            text = ''
+              [X-Test2]
+              X-Test-Value=a
+            '';
+          }) ];
+        };
+
+        simpleServiceWithInstallSection.configuration = {
+          imports = [ simpleServiceNostop.configuration ];
+          systemd.packages = [ (pkgs.writeTextFile {
+            name = "systemd-extra-section";
+            destination = "/etc/systemd/system/test.service";
+            text = ''
+              [Install]
+              WantedBy=multi-user.target
+            '';
+          }) ];
+        };
+
+        simpleServiceWithExtraKey.configuration = {
+          imports = [ simpleServiceNostop.configuration ];
+          systemd.services.test.serviceConfig."X-Test" = "test";
+        };
+
+        simpleServiceWithExtraKeyOtherValue.configuration = {
+          imports = [ simpleServiceNostop.configuration ];
+          systemd.services.test.serviceConfig."X-Test" = "test2";
+        };
+
+        simpleServiceWithExtraKeyOtherName.configuration = {
+          imports = [ simpleServiceNostop.configuration ];
+          systemd.services.test.serviceConfig."X-Test2" = "test";
+        };
+
+        simpleServiceReloadTrigger.configuration = {
+          imports = [ simpleServiceNostop.configuration ];
+          systemd.services.test.reloadTriggers = [ "/dev/null" ];
+        };
+
+        simpleServiceReloadTriggerModified.configuration = {
+          imports = [ simpleServiceNostop.configuration ];
+          systemd.services.test.reloadTriggers = [ "/dev/zero" ];
+        };
+
+        simpleServiceReloadTriggerModifiedAndSomethingElse.configuration = {
+          imports = [ simpleServiceNostop.configuration ];
+          systemd.services.test = {
+            reloadTriggers = [ "/dev/zero" ];
+            serviceConfig."X-Test" = "test";
+          };
+        };
+
+        simpleServiceReloadTriggerModifiedSomethingElse.configuration = {
+          imports = [ simpleServiceNostop.configuration ];
+          systemd.services.test.serviceConfig."X-Test" = "test";
+        };
+
+        unitWithBackslash.configuration = {
+          systemd.services."escaped\\x2ddash" = {
+            wantedBy = [ "multi-user.target" ];
+            serviceConfig = {
+              Type = "oneshot";
+              RemainAfterExit = true;
+              ExecStart = "${pkgs.coreutils}/bin/true";
+              ExecReload = "${pkgs.coreutils}/bin/true";
+            };
+          };
+        };
+
+        unitWithBackslashModified.configuration = {
+          imports = [ unitWithBackslash.configuration ];
+          systemd.services."escaped\\x2ddash".serviceConfig.X-Test = "test";
+        };
+
         restart-and-reload-by-activation-script.configuration = {
           systemd.services = rec {
             simple-service = {
@@ -93,6 +185,17 @@ import ./make-test-python.nix ({ pkgs, ...} : {
             no-restart-service = simple-service // {
               restartIfChanged = false;
             };
+
+            reload-triggers = simple-service // {
+              wantedBy = [ "multi-user.target" ];
+            };
+
+            reload-triggers-and-restart-by-as = simple-service;
+
+            reload-triggers-and-restart = simple-service // {
+              stopIfChanged = false; # easier to check for this
+              wantedBy = [ "multi-user.target" ];
+            };
           };
 
           system.activationScripts.restart-and-reload-test = {
@@ -101,19 +204,33 @@ import ./make-test-python.nix ({ pkgs, ...} : {
             text = ''
               if [ "$NIXOS_ACTION" = dry-activate ]; then
                 f=/run/nixos/dry-activation-restart-list
+                g=/run/nixos/dry-activation-reload-list
               else
                 f=/run/nixos/activation-restart-list
+                g=/run/nixos/activation-reload-list
               fi
               cat <<EOF >> "$f"
               simple-service.service
               simple-restart-service.service
               simple-reload-service.service
               no-restart-service.service
+              reload-triggers-and-restart-by-as.service
+              EOF
+
+              cat <<EOF >> "$g"
+              reload-triggers.service
+              reload-triggers-and-restart-by-as.service
+              reload-triggers-and-restart.service
               EOF
             '';
           };
         };
 
+        restart-and-reload-by-activation-script-modified.configuration = {
+          imports = [ restart-and-reload-by-activation-script.configuration ];
+          systemd.services.reload-triggers-and-restart.serviceConfig.X-Modified = "test";
+        };
+
         mount.configuration = {
           systemd.mounts = [
             {
@@ -241,6 +358,8 @@ import ./make-test-python.nix ({ pkgs, ...} : {
             raise Exception(f"Unexpected string '{needle}' was found")
 
 
+    machine.wait_for_unit("multi-user.target")
+
     machine.succeed(
         "${stderrRunner} ${originalSystem}/bin/switch-to-configuration test"
     )
@@ -331,6 +450,25 @@ import ./make-test-python.nix ({ pkgs, ...} : {
         assert_lacks(out, "as well:")
         assert_contains(out, "would start the following units: test.service\n")
 
+        # Ensure \ works in unit names
+        out = switch_to_specialisation("${machine}", "unitWithBackslash")
+        assert_contains(out, "stopping the following units: test.service\n")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_contains(out, "the following new units were started: escaped\\x2ddash.service\n")
+        assert_lacks(out, "as well:")
+
+        out = switch_to_specialisation("${machine}", "unitWithBackslashModified")
+        assert_contains(out, "stopping the following units: escaped\\x2ddash.service\n")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_contains(out, "\nstarting the following units: escaped\\x2ddash.service\n")
+        assert_lacks(out, "the following new units were started:")
+        assert_lacks(out, "as well:")
+
     with subtest("failing units"):
         # Let the simple service fail
         switch_to_specialisation("${machine}", "simpleServiceModified")
@@ -379,6 +517,130 @@ import ./make-test-python.nix ({ pkgs, ...} : {
         assert_contains(out, "Main PID:")  # output of systemctl
         assert_lacks(out, "as well:")
 
+    with subtest("unit file parser"):
+        # Switch to a well-known state
+        switch_to_specialisation("${machine}", "simpleServiceNostop")
+
+        # Add a section
+        out = switch_to_specialisation("${machine}", "simpleServiceWithExtraSection")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_contains(out, "\nrestarting the following units: test.service\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        assert_lacks(out, "as well:")
+
+        # Rename it
+        out = switch_to_specialisation("${machine}", "simpleServiceWithExtraSectionOtherName")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_contains(out, "\nrestarting the following units: test.service\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        assert_lacks(out, "as well:")
+
+        # Remove it
+        out = switch_to_specialisation("${machine}", "simpleServiceNostop")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_contains(out, "\nrestarting the following units: test.service\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        assert_lacks(out, "as well:")
+
+        # [Install] section is ignored
+        out = switch_to_specialisation("${machine}", "simpleServiceWithInstallSection")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        assert_lacks(out, "as well:")
+
+        # Add a key
+        out = switch_to_specialisation("${machine}", "simpleServiceWithExtraKey")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_contains(out, "\nrestarting the following units: test.service\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        assert_lacks(out, "as well:")
+
+        # Change its value
+        out = switch_to_specialisation("${machine}", "simpleServiceWithExtraKeyOtherValue")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_contains(out, "\nrestarting the following units: test.service\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        assert_lacks(out, "as well:")
+
+        # Rename it
+        out = switch_to_specialisation("${machine}", "simpleServiceWithExtraKeyOtherName")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_contains(out, "\nrestarting the following units: test.service\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        assert_lacks(out, "as well:")
+
+        # Remove it
+        out = switch_to_specialisation("${machine}", "simpleServiceNostop")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_contains(out, "\nrestarting the following units: test.service\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        assert_lacks(out, "as well:")
+
+        # Add a reload trigger
+        out = switch_to_specialisation("${machine}", "simpleServiceReloadTrigger")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_contains(out, "reloading the following units: test.service\n")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        assert_lacks(out, "as well:")
+
+        # Modify the reload trigger
+        out = switch_to_specialisation("${machine}", "simpleServiceReloadTriggerModified")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_contains(out, "reloading the following units: test.service\n")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        assert_lacks(out, "as well:")
+
+        # Modify the reload trigger and something else
+        out = switch_to_specialisation("${machine}", "simpleServiceReloadTriggerModifiedAndSomethingElse")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_contains(out, "\nrestarting the following units: test.service\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        assert_lacks(out, "as well:")
+
+        # Remove the reload trigger
+        out = switch_to_specialisation("${machine}", "simpleServiceReloadTriggerModifiedSomethingElse")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        assert_lacks(out, "as well:")
+
     with subtest("restart and reload by activation script"):
         switch_to_specialisation("${machine}", "simpleServiceNorestart")
         out = switch_to_specialisation("${machine}", "restart-and-reload-by-activation-script")
@@ -386,23 +648,32 @@ import ./make-test-python.nix ({ pkgs, ...} : {
         assert_lacks(out, "NOT restarting the following changed units:")
         assert_lacks(out, "reloading the following units:")
         assert_lacks(out, "restarting the following units:")
-        assert_contains(out, "\nstarting the following units: no-restart-service.service, simple-reload-service.service, simple-restart-service.service, simple-service.service\n")
+        assert_contains(out, "\nstarting the following units: no-restart-service.service, reload-triggers-and-restart-by-as.service, simple-reload-service.service, simple-restart-service.service, simple-service.service\n")
         assert_lacks(out, "as well:")
         # Switch to the same system where the example services get restarted
-        # by the activation script
+        # and reloaded by the activation script
         out = switch_to_specialisation("${machine}", "restart-and-reload-by-activation-script")
         assert_lacks(out, "stopping the following units:")
         assert_lacks(out, "NOT restarting the following changed units:")
-        assert_contains(out, "reloading the following units: simple-reload-service.service\n")
-        assert_contains(out, "restarting the following units: simple-restart-service.service, simple-service.service\n")
+        assert_contains(out, "reloading the following units: reload-triggers-and-restart.service, reload-triggers.service, simple-reload-service.service\n")
+        assert_contains(out, "restarting the following units: reload-triggers-and-restart-by-as.service, simple-restart-service.service, simple-service.service\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "as well:")
+        # Switch to the same system and see if the service gets restarted when it's modified
+        # while the fact that it's supposed to be reloaded by the activation script is ignored.
+        out = switch_to_specialisation("${machine}", "restart-and-reload-by-activation-script-modified")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_contains(out, "reloading the following units: reload-triggers.service, simple-reload-service.service\n")
+        assert_contains(out, "restarting the following units: reload-triggers-and-restart-by-as.service, reload-triggers-and-restart.service, simple-restart-service.service, simple-service.service\n")
         assert_lacks(out, "\nstarting the following units:")
         assert_lacks(out, "as well:")
         # The same, but in dry mode
         out = switch_to_specialisation("${machine}", "restart-and-reload-by-activation-script", action="dry-activate")
         assert_lacks(out, "would stop the following units:")
         assert_lacks(out, "would NOT stop the following changed units:")
-        assert_contains(out, "would reload the following units: simple-reload-service.service\n")
-        assert_contains(out, "would restart the following units: simple-restart-service.service, simple-service.service\n")
+        assert_contains(out, "would reload the following units: reload-triggers.service, simple-reload-service.service\n")
+        assert_contains(out, "would restart the following units: reload-triggers-and-restart-by-as.service, reload-triggers-and-restart.service, simple-restart-service.service, simple-service.service\n")
         assert_lacks(out, "\nwould start the following units:")
         assert_lacks(out, "as well:")
 
diff --git a/nixos/tests/unifi.nix b/nixos/tests/unifi.nix
index 34284811abf..9dc7e5d04bd 100644
--- a/nixos/tests/unifi.nix
+++ b/nixos/tests/unifi.nix
@@ -12,7 +12,7 @@ let
   makeAppTest = unifi: makeTest {
     name = "unifi-controller-${unifi.version}";
     meta = with pkgs.lib.maintainers; {
-      maintainers = [ zhaofengli ];
+      maintainers = [ patryk27 zhaofengli ];
     };
 
     nodes.server = {
@@ -32,4 +32,5 @@ in with pkgs; {
   unifiLTS = makeAppTest unifiLTS;
   unifi5 = makeAppTest unifi5;
   unifi6 = makeAppTest unifi6;
+  unifi7 = makeAppTest unifi7;
 }
diff --git a/nixos/tests/web-servers/agate.nix b/nixos/tests/web-servers/agate.nix
new file mode 100644
index 00000000000..e364e134cfd
--- /dev/null
+++ b/nixos/tests/web-servers/agate.nix
@@ -0,0 +1,29 @@
+import ../make-test-python.nix (
+  { pkgs, lib, ... }:
+  {
+    name = "agate";
+    meta = with lib.maintainers; { maintainers = [ jk ]; };
+
+    nodes = {
+      geminiserver = { pkgs, ... }: {
+        services.agate = {
+          enable = true;
+          hostnames = [ "localhost" ];
+          contentDir = pkgs.writeTextDir "index.gmi" ''
+            # Hello NixOS!
+          '';
+        };
+      };
+    };
+
+    testScript = { nodes, ... }: ''
+      geminiserver.wait_for_unit("agate")
+      geminiserver.wait_for_open_port(1965)
+
+      with subtest("check is serving over gemini"):
+        response = geminiserver.succeed("${pkgs.gmni}/bin/gmni -j once -i -N gemini://localhost:1965")
+        print(response)
+        assert "Hello NixOS!" in response
+    '';
+  }
+)
diff --git a/nixos/tests/wine.nix b/nixos/tests/wine.nix
index 18ad759b551..cc449864c76 100644
--- a/nixos/tests/wine.nix
+++ b/nixos/tests/wine.nix
@@ -17,7 +17,7 @@ let
 
       machine = { pkgs, ... }: {
         environment.systemPackages = [ pkgs."${packageSet}"."${variant}" ];
-        virtualisation.diskSize = "800";
+        virtualisation.diskSize = 800;
       };
 
       testScript = ''
diff --git a/nixos/tests/wpa_supplicant.nix b/nixos/tests/wpa_supplicant.nix
index 1d669d5016a..40d934b8e1d 100644
--- a/nixos/tests/wpa_supplicant.nix
+++ b/nixos/tests/wpa_supplicant.nix
@@ -27,8 +27,19 @@ import ./make-test-python.nix ({ pkgs, lib, ...}:
       enable = lib.mkOverride 0 true;
       userControlled.enable = true;
       interfaces = [ "wlan1" ];
+      fallbackToWPA2 = true;
 
       networks = {
+        # test WPA2 fallback
+        mixed-wpa = {
+          psk = "password";
+          authProtocols = [ "WPA-PSK" "SAE" ];
+        };
+        sae-only = {
+          psk = "password";
+          authProtocols = [ "SAE" ];
+        };
+
         # test network
         nixos-test.psk = "@PSK_NIXOS_TEST@";
 
@@ -64,8 +75,12 @@ import ./make-test-python.nix ({ pkgs, lib, ...}:
           machine.succeed(f"grep -q @PSK_MISSING@ {config_file}")
           machine.succeed(f"grep -q P@ssowrdWithSome@tSymbol {config_file}")
 
-          # save file for manual inspection
-          machine.copy_from_vm(config_file)
+      with subtest("WPA2 fallbacks have been generated"):
+          assert int(machine.succeed(f"grep -c sae-only {config_file}")) == 1
+          assert int(machine.succeed(f"grep -c mixed-wpa {config_file}")) == 2
+
+      # save file for manual inspection
+      machine.copy_from_vm(config_file)
 
       with subtest("Daemon is running and accepting connections"):
           machine.wait_for_unit("wpa_supplicant-wlan1.service")