summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/configuration/luks-file-systems.xml34
-rw-r--r--nixos/doc/manual/configuration/network-manager.xml16
-rw-r--r--nixos/doc/manual/configuration/x-windows.xml9
-rw-r--r--nixos/doc/manual/configuration/xfce.xml23
-rwxr-xr-xnixos/doc/manual/development/releases.xml6
-rw-r--r--nixos/doc/manual/man-nixos-install.xml2
-rw-r--r--nixos/doc/manual/man-nixos-option.xml21
-rw-r--r--nixos/doc/manual/release-notes/rl-2003.xml163
-rw-r--r--nixos/lib/qemu-flags.nix4
-rw-r--r--nixos/lib/test-driver/test-driver.py2
-rw-r--r--nixos/lib/testing/jquery-ui.nix4
-rw-r--r--nixos/modules/config/ldap.nix3
-rw-r--r--nixos/modules/config/resolvconf.nix10
-rw-r--r--nixos/modules/hardware/openrazer.nix2
-rw-r--r--nixos/modules/hardware/tuxedo-keyboard.nix35
-rw-r--r--nixos/modules/installer/cd-dvd/installation-cd-graphical-plasma5-new-kernel.nix (renamed from nixos/modules/installer/cd-dvd/installation-cd-graphical-kde-new-kernel.nix)2
-rw-r--r--nixos/modules/installer/cd-dvd/installation-cd-graphical-plasma5.nix (renamed from nixos/modules/installer/cd-dvd/installation-cd-graphical-kde.nix)0
-rw-r--r--nixos/modules/installer/tools/nixos-option/nixos-option.cc251
-rw-r--r--nixos/modules/installer/tools/nixos-rebuild.sh2
-rw-r--r--nixos/modules/misc/locate.nix7
-rw-r--r--nixos/modules/misc/version.nix5
-rw-r--r--nixos/modules/module-list.nix9
-rw-r--r--nixos/modules/programs/geary.nix20
-rw-r--r--nixos/modules/programs/gnupg.nix2
-rw-r--r--nixos/modules/programs/sway.nix3
-rw-r--r--nixos/modules/rename.nix8
-rw-r--r--nixos/modules/security/duosec.nix16
-rw-r--r--nixos/modules/services/amqp/rabbitmq.nix8
-rw-r--r--nixos/modules/services/databases/victoriametrics.nix70
-rw-r--r--nixos/modules/services/desktops/gnome3/at-spi2-core.nix3
-rw-r--r--nixos/modules/services/development/jupyter/default.nix6
-rw-r--r--nixos/modules/services/hardware/irqbalance.nix14
-rw-r--r--nixos/modules/services/mail/mailman.nix277
-rw-r--r--nixos/modules/services/mail/roundcube.nix79
-rw-r--r--nixos/modules/services/mail/spamassassin.nix23
-rw-r--r--nixos/modules/services/misc/freeswitch.nix103
-rw-r--r--nixos/modules/services/misc/home-assistant.nix1
-rw-r--r--nixos/modules/services/monitoring/nagios.nix2
-rw-r--r--nixos/modules/services/monitoring/prometheus/alertmanager.nix21
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/postfix.nix2
-rw-r--r--nixos/modules/services/monitoring/prometheus/xmpp-alerts.nix47
-rw-r--r--nixos/modules/services/network-filesystems/kbfs.nix90
-rw-r--r--nixos/modules/services/networking/bitlbee.nix3
-rw-r--r--nixos/modules/services/networking/dhcpcd.nix29
-rw-r--r--nixos/modules/services/networking/dnscrypt-proxy.nix328
-rw-r--r--nixos/modules/services/networking/dnscrypt-proxy.xml66
-rw-r--r--nixos/modules/services/networking/dnscrypt-proxy2.nix61
-rw-r--r--nixos/modules/services/networking/keybase.nix11
-rw-r--r--nixos/modules/services/networking/knot.nix2
-rw-r--r--nixos/modules/services/networking/kresd.nix34
-rw-r--r--nixos/modules/services/networking/nsd.nix4
-rw-r--r--nixos/modules/services/networking/unifi.nix15
-rw-r--r--nixos/modules/services/search/solr.nix12
-rw-r--r--nixos/modules/services/security/bitwarden_rs/default.nix44
-rw-r--r--nixos/modules/services/security/fail2ban.nix306
-rw-r--r--nixos/modules/services/security/sshguard.nix13
-rw-r--r--nixos/modules/services/security/vault.nix1
-rw-r--r--nixos/modules/services/web-apps/dokuwiki.nix272
-rw-r--r--nixos/modules/services/web-apps/limesurvey.nix2
-rw-r--r--nixos/modules/services/web-apps/mediawiki.nix2
-rw-r--r--nixos/modules/services/web-apps/moodle.nix2
-rw-r--r--nixos/modules/services/web-apps/wordpress.nix2
-rw-r--r--nixos/modules/services/web-apps/zabbix.nix2
-rw-r--r--nixos/modules/services/web-servers/apache-httpd/default.nix238
-rw-r--r--nixos/modules/services/web-servers/apache-httpd/location-options.nix54
-rw-r--r--nixos/modules/services/web-servers/apache-httpd/vhost-options.nix (renamed from nixos/modules/services/web-servers/apache-httpd/per-server-options.nix)33
-rw-r--r--nixos/modules/services/web-servers/unit/default.nix6
-rw-r--r--nixos/modules/services/x11/desktop-managers/default.nix26
-rw-r--r--nixos/modules/services/x11/desktop-managers/gnome3.nix2
-rw-r--r--nixos/modules/services/x11/desktop-managers/xfce.nix11
-rw-r--r--nixos/modules/services/x11/xserver.nix3
-rw-r--r--nixos/modules/system/boot/loader/grub/grub.nix2
-rw-r--r--nixos/modules/system/boot/loader/grub/install-grub.pl3
-rw-r--r--nixos/modules/system/boot/luksroot.nix79
-rw-r--r--nixos/modules/system/boot/networkd.nix28
-rw-r--r--nixos/modules/system/boot/systemd.nix15
-rw-r--r--nixos/modules/tasks/network-interfaces.nix63
-rw-r--r--nixos/modules/virtualisation/amazon-init.nix11
-rw-r--r--nixos/modules/virtualisation/docker-containers.nix55
-rw-r--r--nixos/modules/virtualisation/lxd.nix44
-rw-r--r--nixos/release-combined.nix2
-rw-r--r--nixos/release.nix9
-rw-r--r--nixos/tests/all-tests.nix10
-rw-r--r--nixos/tests/blivet.nix87
-rw-r--r--nixos/tests/buildbot.nix130
-rw-r--r--nixos/tests/chromium.nix2
-rw-r--r--nixos/tests/common/auto.nix (renamed from nixos/modules/services/x11/display-managers/auto.nix)4
-rw-r--r--nixos/tests/common/ec2.nix4
-rw-r--r--nixos/tests/common/x11.nix9
-rw-r--r--nixos/tests/dnscrypt-proxy2.nix (renamed from nixos/tests/dnscrypt-proxy.nix)23
-rw-r--r--nixos/tests/docker-containers.nix9
-rw-r--r--nixos/tests/docker-tools.nix3
-rw-r--r--nixos/tests/dokuwiki.nix29
-rw-r--r--nixos/tests/ec2.nix45
-rw-r--r--nixos/tests/freeswitch.nix29
-rw-r--r--nixos/tests/gnome3.nix64
-rw-r--r--nixos/tests/graphite.nix31
-rw-r--r--nixos/tests/i3wm.nix2
-rw-r--r--nixos/tests/ihatemoney.nix59
-rw-r--r--nixos/tests/installer.nix2
-rw-r--r--nixos/tests/keymap.nix114
-rw-r--r--nixos/tests/limesurvey.nix29
-rw-r--r--nixos/tests/misc.nix164
-rw-r--r--nixos/tests/networking-proxy.nix108
-rw-r--r--nixos/tests/networking.nix4
-rw-r--r--nixos/tests/openstack-image.nix8
-rw-r--r--nixos/tests/proxy.nix143
-rw-r--r--nixos/tests/riak.nix25
-rw-r--r--nixos/tests/signal-desktop.nix2
-rw-r--r--nixos/tests/solr.nix101
-rw-r--r--nixos/tests/systemd-networkd-vrf.nix221
-rw-r--r--nixos/tests/systemd.nix95
-rw-r--r--nixos/tests/victoriametrics.nix31
-rw-r--r--nixos/tests/virtualbox.nix2
-rw-r--r--nixos/tests/xautolock.nix2
-rw-r--r--nixos/tests/xfce.nix14
-rw-r--r--nixos/tests/xmonad.nix2
-rw-r--r--nixos/tests/xrdp.nix2
-rw-r--r--nixos/tests/xss-lock.nix4
-rw-r--r--nixos/tests/yabar.nix2
120 files changed, 3195 insertions, 1720 deletions
diff --git a/nixos/doc/manual/configuration/luks-file-systems.xml b/nixos/doc/manual/configuration/luks-file-systems.xml
index 8a2b107e0ee8a..d3007843d68bd 100644
--- a/nixos/doc/manual/configuration/luks-file-systems.xml
+++ b/nixos/doc/manual/configuration/luks-file-systems.xml
@@ -37,4 +37,38 @@ Enter passphrase for /dev/disk/by-uuid/3f6b0024-3a44-4fde-a43a-767b872abe5d: ***
   on an encrypted partition, it is necessary to add the following grub option:
 <programlisting><xref linkend="opt-boot.loader.grub.enableCryptodisk"/> = true;</programlisting>
  </para>
+  <section xml:id="sec-luks-file-systems-fido2">
+  <title>FIDO2</title>
+
+  <para>
+   NixOS also supports unlocking your LUKS-Encrypted file system using a FIDO2 compatible token. In the following example, we will create a new FIDO2 credential
+   and add it as a new key to our existing device <filename>/dev/sda2</filename>:
+
+   <screen>
+# export FIDO2_LABEL="/dev/sda2 @ $HOSTNAME"
+# fido2luks credential "$FIDO2_LABEL"
+f1d00200108b9d6e849a8b388da457688e3dd653b4e53770012d8f28e5d3b269865038c346802f36f3da7278b13ad6a3bb6a1452e24ebeeaa24ba40eef559b1b287d2a2f80b7
+
+# fido2luks -i add-key /dev/sda2 f1d00200108b9d6e849a8b388da457688e3dd653b4e53770012d8f28e5d3b269865038c346802f36f3da7278b13ad6a3bb6a1452e24ebeeaa24ba40eef559b1b287d2a2f80b7
+Password:
+Password (again):
+Old password:
+Old password (again):
+Added to key to device /dev/sda2, slot: 2
+</screen>
+
+  To ensure that this file system is decrypted using the FIDO2 compatible key, add the following to <filename>configuration.nix</filename>:
+<programlisting>
+<link linkend="opt-boot.initrd.luks.fido2Support">boot.initrd.luks.fido2Support</link> = true;
+<link linkend="opt-boot.initrd.luks.devices._name__.fido2.credential">boot.initrd.luks.devices."/dev/sda2".fido2.credential</link> = "f1d00200108b9d6e849a8b388da457688e3dd653b4e53770012d8f28e5d3b269865038c346802f36f3da7278b13ad6a3bb6a1452e24ebeeaa24ba40eef559b1b287d2a2f80b7";
+</programlisting>
+
+  You can also use the FIDO2 passwordless setup, but for security reasons, you might want to enable it only when your device is PIN protected, such as <link xlink:href="https://trezor.io/">Trezor</link>.
+
+<programlisting>
+<link linkend="opt-boot.initrd.luks.devices._name__.fido2.passwordLess">boot.initrd.luks.devices."/dev/sda2".fido2.passwordLess</link> = true;
+</programlisting>
+  </para>
+ </section>
+
 </section>
diff --git a/nixos/doc/manual/configuration/network-manager.xml b/nixos/doc/manual/configuration/network-manager.xml
index d103ee2497839..3953e0ffe851a 100644
--- a/nixos/doc/manual/configuration/network-manager.xml
+++ b/nixos/doc/manual/configuration/network-manager.xml
@@ -28,17 +28,21 @@
   <command>nmtui</command> (curses-based terminal user interface). See their
   manual pages for details on their usage. Some desktop environments (GNOME,
   KDE) have their own configuration tools for NetworkManager. On XFCE, there is
-  no configuration tool for NetworkManager by default: by adding
-  <code>networkmanagerapplet</code> to the list of system packages, the
-  graphical applet will be installed and will launch automatically when XFCE is
-  starting (and will show in the status tray).
+  no configuration tool for NetworkManager by default: by enabling <xref linkend="opt-programs.nm-applet.enable"/>, the
+  graphical applet will be installed and will launch automatically when the graphical session is started.
  </para>
 
  <note>
   <para>
    <code>networking.networkmanager</code> and <code>networking.wireless</code>
-   (WPA Supplicant) cannot be enabled at the same time: you can still connect
-   to the wireless networks using NetworkManager.
+   (WPA Supplicant) can be used together if desired. To do this you need to instruct
+   NetworkManager to ignore those interfaces like:
+<programlisting>
+<xref linkend="opt-networking.networkmanager.unmanaged"/> = [
+   "*" "except:type:wwan" "except:type:gsm"
+];
+</programlisting>
+   Refer to the option description for the exact syntax and references to external documentation.
   </para>
  </note>
 </section>
diff --git a/nixos/doc/manual/configuration/x-windows.xml b/nixos/doc/manual/configuration/x-windows.xml
index 55ad9fe6e6530..06dd7c8bfb949 100644
--- a/nixos/doc/manual/configuration/x-windows.xml
+++ b/nixos/doc/manual/configuration/x-windows.xml
@@ -85,11 +85,14 @@
 <programlisting>
 <xref linkend="opt-services.xserver.displayManager.defaultSession"/> = "none+i3";
 </programlisting>
-  And, finally, to enable auto-login for a user <literal>johndoe</literal>:
+  Every display manager in NixOS supports auto-login, here is an example
+  using lightdm for a user <literal>alice</literal>:
 <programlisting>
-<xref linkend="opt-services.xserver.displayManager.auto.enable"/> = true;
-<xref linkend="opt-services.xserver.displayManager.auto.user"/> = "johndoe";
+<xref linkend="opt-services.xserver.displayManager.lightdm.enable"/> = true;
+<xref linkend="opt-services.xserver.displayManager.lightdm.autoLogin.enable"/> = true;
+<xref linkend="opt-services.xserver.displayManager.lightdm.autoLogin.user"/> = "alice";
 </programlisting>
+  The options are named identically for all other display managers.
   </para>
  </simplesect>
  <simplesect xml:id="sec-x11-graphics-cards-nvidia">
diff --git a/nixos/doc/manual/configuration/xfce.xml b/nixos/doc/manual/configuration/xfce.xml
index 7d2862f8b31ff..a81a327c09b68 100644
--- a/nixos/doc/manual/configuration/xfce.xml
+++ b/nixos/doc/manual/configuration/xfce.xml
@@ -28,25 +28,14 @@
  <para>
   Some Xfce programs are not installed automatically. To install them manually
   (system wide), put them into your
-  <xref linkend="opt-environment.systemPackages"/>.
+  <xref linkend="opt-environment.systemPackages"/> from <literal>pkgs.xfce</literal>.
  </para>
- <simplesect xml:id="sec-xfce-thunar-volumes">
-  <title>Thunar Volume Support</title>
+ <simplesect xml:id="sec-xfce-thunar-plugins">
+  <title>Thunar Plugins</title>
   <para>
-   To enable <emphasis>Thunar</emphasis> volume support, put
-<programlisting>
-<xref linkend="opt-services.xserver.desktopManager.xfce.enable"/> = true;
-</programlisting>
-   into your <emphasis>configuration.nix</emphasis>.
-  </para>
- </simplesect>
- <simplesect xml:id="sec-xfce-polkit">
-  <title>Polkit Authentication Agent</title>
-  <para>
-   There is no authentication agent automatically installed alongside Xfce. To
-   allow mounting of local (non-removable) filesystems, you will need to
-   install one. Installing <emphasis>polkit_gnome</emphasis>, a rebuild, logout
-   and login did the trick.
+    If you'd like to add extra plugins to Thunar, add them to
+    <xref linkend="opt-services.xserver.desktopManager.xfce.thunarPlugins"/>.
+    You shouldn't just add them to <xref linkend="opt-environment.systemPackages"/>.
   </para>
  </simplesect>
  <simplesect xml:id="sec-xfce-troubleshooting">
diff --git a/nixos/doc/manual/development/releases.xml b/nixos/doc/manual/development/releases.xml
index 9371af9984d1d..a22a0a3707b4d 100755
--- a/nixos/doc/manual/development/releases.xml
+++ b/nixos/doc/manual/development/releases.xml
@@ -187,7 +187,7 @@
     </listitem>
     <listitem>
      <para>
-      Update "Chapter 4. Upgrading NixOS" section of the manual to match 
+      Update "Chapter 4. Upgrading NixOS" section of the manual to match
       new stable release version.
      </para>
     </listitem>
@@ -237,6 +237,10 @@
    experience.
   </para>
   <para>
+   Release managers for the current NixOS release are tracked by GitHub team
+   <link xlink:href="https://github.com/orgs/NixOS/teams/nixos-release-managers/members"><literal>@NixOS/nixos-release-managers</literal></link>.
+  </para>
+  <para>
    A release manager's role and responsibilities are:
   </para>
   <itemizedlist>
diff --git a/nixos/doc/manual/man-nixos-install.xml b/nixos/doc/manual/man-nixos-install.xml
index 0752c397182f5..9255ce763efee 100644
--- a/nixos/doc/manual/man-nixos-install.xml
+++ b/nixos/doc/manual/man-nixos-install.xml
@@ -210,7 +210,7 @@
       The closure must be an appropriately configured NixOS system, with boot
       loader and partition configuration that fits the target host. Such a
       closure is typically obtained with a command such as <command>nix-build
-      -I nixos-config=./configuration.nix '&lt;nixos&gt;' -A system
+      -I nixos-config=./configuration.nix '&lt;nixpkgs/nixos&gt;' -A system
       --no-out-link</command>
      </para>
     </listitem>
diff --git a/nixos/doc/manual/man-nixos-option.xml b/nixos/doc/manual/man-nixos-option.xml
index b82f31256099c..b921386d0df01 100644
--- a/nixos/doc/manual/man-nixos-option.xml
+++ b/nixos/doc/manual/man-nixos-option.xml
@@ -14,12 +14,16 @@
  <refsynopsisdiv>
   <cmdsynopsis>
    <command>nixos-option</command>
+
    <arg>
-    <option>-I</option> <replaceable>path</replaceable>
+    <group choice='req'>
+     <arg choice='plain'><option>-r</option></arg>
+     <arg choice='plain'><option>--recursive</option></arg>
+    </group>
    </arg>
 
    <arg>
-    <option>--all</option>
+    <option>-I</option> <replaceable>path</replaceable>
    </arg>
 
    <arg>
@@ -46,23 +50,22 @@
   </para>
   <variablelist>
    <varlistentry>
-    <term>
-     <option>-I</option> <replaceable>path</replaceable>
-    </term>
+    <term><option>-r</option></term>
+    <term><option>--recursive</option></term>
     <listitem>
      <para>
-      This option is passed to the underlying
-      <command>nix-instantiate</command> invocation.
+      Print all the values at or below the specified path recursively.
      </para>
     </listitem>
    </varlistentry>
    <varlistentry>
     <term>
-     <option>--all</option>
+     <option>-I</option> <replaceable>path</replaceable>
     </term>
     <listitem>
      <para>
-      Print the values of all options.
+      This option is passed to the underlying
+      <command>nix-instantiate</command> invocation.
      </para>
     </listitem>
    </varlistentry>
diff --git a/nixos/doc/manual/release-notes/rl-2003.xml b/nixos/doc/manual/release-notes/rl-2003.xml
index 1eef4f08c4fdb..d21ac882f275e 100644
--- a/nixos/doc/manual/release-notes/rl-2003.xml
+++ b/nixos/doc/manual/release-notes/rl-2003.xml
@@ -25,6 +25,13 @@
    </listitem>
    <listitem>
     <para>
+     Linux kernel is updated to branch 5.4 by default (from 4.19).
+     Users of Intel GPUs may prefer to explicitly set branch to 4.19 to avoid some regressions.
+     <programlisting>boot.kernelPackages = pkgs.linuxPackages_4_19;</programlisting>
+    </para>
+   </listitem>
+   <listitem>
+    <para>
      Postgresql for NixOS service now defaults to v11.
     </para>
    </listitem>
@@ -52,7 +59,7 @@
    <listitem>
     <para>
       <command>nixos-option</command> has been rewritten in C++, speeding it up, improving correctness,
-      and adding a <option>--all</option> option which prints all options and their values.
+      and adding a <option>-r</option> option which prints all options and their values recursively.
     </para>
    </listitem>
    <listitem>
@@ -96,6 +103,13 @@ services.xserver.displayManager.defaultSession = "xfce+icewm";
     via <option>services.upower</option>.
     </para>
    </listitem>
+   <listitem>
+    <para>
+     To use Geary you should enable <xref linkend="opt-programs.geary.enable"/> instead of
+     just adding it to <xref linkend="opt-environment.systemPackages"/>.
+     It was created so Geary could function properly outside of GNOME.
+    </para>
+   </listitem>
   </itemizedlist>
 
  </section>
@@ -126,7 +140,7 @@ services.xserver.displayManager.defaultSession = "xfce+icewm";
    <listitem>
     <para>
      The <literal>dynamicHosts</literal> option has been removed from the
-     <link linkend="opt-networking.networkmanager.enable">networkd</link>
+     <link linkend="opt-networking.networkmanager.enable">NetworkManager</link>
      module. Allowing (multiple) regular users to override host entries
      affecting the whole system opens up a huge attack vector.
      There seem to be very rare cases where this might be useful.
@@ -445,6 +459,145 @@ users.users.me =
       </listitem>
     </itemizedlist>
    </listitem>
+   <listitem>
+    <para>
+     The <literal>citrix_workspace_19_3_0</literal> package has been removed as
+     it will be EOLed within the lifespan of 20.03. For further information,
+     please refer to the <link xlink:href="https://www.citrix.com/de-de/support/product-lifecycle/milestones/receiver.html">support and maintenance information</link> from upstream.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The <literal>gcc5</literal> and <literal>gfortran5</literal> packages have been removed.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The <option>services.xserver.displayManager.auto</option> module has been removed.
+     It was only intended for use in internal NixOS tests, and gave the false impression
+     of it being a special display manager when it's actually LightDM.
+     Please use the <xref linkend="opt-services.xserver.displayManager.lightdm.autoLogin"/> options instead,
+     or any other display manager in NixOS as they all support auto-login. If you used this module specifically
+     because it permitted root auto-login you can override the lightdm-autologin pam module like:
+<programlisting>
+<link xlink:href="#opt-security.pam.services._name__.text">security.pam.services.lightdm-autologin.text</link> = lib.mkForce ''
+    auth     requisite pam_nologin.so
+    auth     required  pam_succeed_if.so quiet
+    auth     required  pam_permit.so
+
+    account  include   lightdm
+
+    password include   lightdm
+
+    session  include   lightdm
+'';
+</programlisting>
+     The difference is the:
+<programlisting>
+auth required pam_succeed_if.so quiet
+</programlisting>
+     line, where default it's:
+<programlisting>
+auth required pam_succeed_if.so uid >= 1000 quiet
+</programlisting>
+     not permitting users with uid's below 1000 (like root).
+     All other display managers in NixOS are configured like this.
+    </para>
+   </listitem>
+   <listitem>
+     <para>
+       There have been lots of improvements to the Mailman module.  As
+       a result,
+     </para>
+     <itemizedlist>
+       <listitem>
+         <para>
+           The <option>services.mailman.hyperkittyBaseUrl</option>
+           option has been renamed to <xref
+           linkend="opt-services.mailman.hyperkitty.baseUrl"/>.
+         </para>
+       </listitem>
+       <listitem>
+         <para>
+           The <option>services.mailman.hyperkittyApiKey</option>
+           option has been removed.  This is because having an option
+           for the Hyperkitty API key meant that the API key would be
+           stored in the world-readable Nix store, which was a
+           security vulnerability.  A new Hyperkitty API key will be
+           generated the first time the new Hyperkitty service is run,
+           and it will then be persisted outside of the Nix store.  To
+           continue using Hyperkitty, you must set <xref
+           linkend="opt-services.mailman.hyperkitty.enable"/> to
+           <literal>true</literal>.
+         </para>
+       </listitem>
+       <listitem>
+         <para>
+           Additionally, some Postfix configuration must now be set
+           manually instead of automatically by the Mailman module:
+<programlisting>
+<xref linkend="opt-services.postfix.relayDomains"/> = [ "hash:/var/lib/mailman/data/postfix_domains" ];
+<xref linkend="opt-services.postfix.config"/>.transport_maps = [ "hash:/var/lib/mailman/data/postfix_lmtp" ];
+<xref linkend="opt-services.postfix.config"/>.local_recipient_maps = [ "hash:/var/lib/mailman/data/postfix_lmtp" ];
+</programlisting>
+           This is because some users may want to include other values
+           in these lists as well, and this was not possible if they
+           were set automatically by the Mailman module.  It would not
+           have been possible to just concatenate values from multiple
+           modules each setting the values they needed, because the
+           order of elements in the list is significant.
+         </para>
+       </listitem>
+     </itemizedlist>
+   </listitem>
+   <listitem>
+    <para>The LLVM versions 3.5, 3.9 and 4 (including the corresponding CLang versions) have been dropped.</para>
+   </listitem>
+   <listitem>
+    <para>
+     The <option>networking.interfaces.*.preferTempAddress</option> option has
+     been replaced by <option>networking.interfaces.*.tempAddress</option>.
+     The new option allows better control of the IPv6 temporary addresses,
+     including completely disabling them for interfaces where they are not
+     needed.
+    </para>
+   </listitem>
+   <listitem>
+     <para>
+       Rspamd was updated to version 2.2. Read
+       <link xlink:href="https://rspamd.com/doc/migration.html#migration-to-rspamd-20">
+       the upstream migration notes</link> carefully. Please be especially
+       aware that some modules were removed and the default Bayes backend is
+       now Redis.
+     </para>
+   </listitem>
+   <listitem>
+    <para>
+     The <literal>*psu</literal> versions of <package>oraclejdk8</package> have been removed
+     as they aren't provided by upstream anymore.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The <option>services.dnscrypt-proxy</option> module has been removed
+     as it used the deprecated version of dnscrypt-proxy. We've added
+     <xref linkend="opt-services.dnscrypt-proxy2.enable"/> to use the supported version.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     <literal>qesteidutil</literal> has been deprecated in favor of <literal>qdigidoc</literal>.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     <package>sqldeveloper_18</package> has been removed as it's not maintained anymore,
+     <package>sqldeveloper</package> has been updated to version <literal>19.4</literal>.
+     Please note that this means that this means that the <package>oraclejdk</package> is now
+     required. For further information please read the
+     <link xlink:href="https://www.oracle.com/technetwork/developer-tools/sql-developer/downloads/sqldev-relnotes-194-5908846.html">release notes</link>.
+    </para>
+   </listitem>
   </itemizedlist>
  </section>
 
@@ -485,6 +638,12 @@ users.users.me =
        now uses the short rather than full version string.
      </para>
    </listitem>
+    <listitem>
+    <para>
+    It is now possible to unlock LUKS-Encrypted file systems using a FIDO2 token
+    via <option>boot.initrd.luks.fido2Support</option>.
+    </para>
+   </listitem>
   </itemizedlist>
  </section>
 </section>
diff --git a/nixos/lib/qemu-flags.nix b/nixos/lib/qemu-flags.nix
index 774f66b4804e0..859d9e975fec7 100644
--- a/nixos/lib/qemu-flags.nix
+++ b/nixos/lib/qemu-flags.nix
@@ -17,9 +17,9 @@ in
         else throw "Unknown QEMU serial device for system '${pkgs.stdenv.hostPlatform.system}'";
 
   qemuBinary = qemuPkg: {
-    x86_64-linux = "${qemuPkg}/bin/qemu-kvm -cpu kvm64";
+    x86_64-linux = "${qemuPkg}/bin/qemu-kvm -cpu host";
     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";
-    x86_64-darwin = "${qemuPkg}/bin/qemu-kvm -cpu kvm64";
+    x86_64-darwin = "${qemuPkg}/bin/qemu-kvm -cpu host";
   }.${pkgs.stdenv.hostPlatform.system} or "${qemuPkg}/bin/qemu-kvm";
 }
diff --git a/nixos/lib/test-driver/test-driver.py b/nixos/lib/test-driver/test-driver.py
index cf204a2619f58..75f80df53f21c 100644
--- a/nixos/lib/test-driver/test-driver.py
+++ b/nixos/lib/test-driver/test-driver.py
@@ -395,7 +395,7 @@ class Machine:
         status_code_pattern = re.compile(r"(.*)\|\!EOF\s+(\d+)")
 
         while True:
-            chunk = self.shell.recv(4096).decode()
+            chunk = self.shell.recv(4096).decode(errors="ignore")
             match = status_code_pattern.match(chunk)
             if match:
                 output += match[1]
diff --git a/nixos/lib/testing/jquery-ui.nix b/nixos/lib/testing/jquery-ui.nix
index e65107a3c2fbc..abd59da2d285e 100644
--- a/nixos/lib/testing/jquery-ui.nix
+++ b/nixos/lib/testing/jquery-ui.nix
@@ -4,7 +4,7 @@ stdenv.mkDerivation rec {
   name = "jquery-ui-1.11.4";
 
   src = fetchurl {
-    url = "http://jqueryui.com/resources/download/${name}.zip";
+    url = "https://jqueryui.com/resources/download/${name}.zip";
     sha256 = "0ciyaj1acg08g8hpzqx6whayq206fvf4whksz2pjgxlv207lqgjh";
   };
 
@@ -17,7 +17,7 @@ stdenv.mkDerivation rec {
     '';
 
   meta = {
-    homepage = http://jqueryui.com/;
+    homepage = https://jqueryui.com/;
     description = "A library of JavaScript widgets and effects";
     platforms = stdenv.lib.platforms.all;
   };
diff --git a/nixos/modules/config/ldap.nix b/nixos/modules/config/ldap.nix
index 9c8e9d1493714..b554f197dc4ba 100644
--- a/nixos/modules/config/ldap.nix
+++ b/nixos/modules/config/ldap.nix
@@ -28,8 +28,6 @@ let
   };
 
   nslcdConfig = writeText "nslcd.conf" ''
-    uid nslcd
-    gid nslcd
     uri ${cfg.server}
     base ${cfg.base}
     timelimit ${toString cfg.timeLimit}
@@ -282,6 +280,7 @@ in
           Group = "nslcd";
           RuntimeDirectory = [ "nslcd" ];
           PIDFile = "/run/nslcd/nslcd.pid";
+          AmbientCapabilities = "CAP_SYS_RESOURCE";
         };
       };
 
diff --git a/nixos/modules/config/resolvconf.nix b/nixos/modules/config/resolvconf.nix
index 7d2f252a88863..cc202bca6c4e4 100644
--- a/nixos/modules/config/resolvconf.nix
+++ b/nixos/modules/config/resolvconf.nix
@@ -38,6 +38,7 @@ in
     (mkRenamedOptionModule [ "networking" "dnsExtensionMechanism" ] [ "networking" "resolvconf" "dnsExtensionMechanism" ])
     (mkRenamedOptionModule [ "networking" "extraResolvconfConf" ] [ "networking" "resolvconf" "extraConfig" ])
     (mkRenamedOptionModule [ "networking" "resolvconfOptions" ] [ "networking" "resolvconf" "extraOptions" ])
+    (mkRemovedOptionModule [ "networking" "resolvconf" "useHostResolvConf" ] "This option was never used for anything anyways")
   ];
 
   options = {
@@ -53,15 +54,6 @@ in
         '';
       };
 
-      useHostResolvConf = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          In containers, whether to use the
-          <filename>resolv.conf</filename> supplied by the host.
-        '';
-      };
-
       dnsSingleRequest = lib.mkOption {
         type = types.bool;
         default = false;
diff --git a/nixos/modules/hardware/openrazer.nix b/nixos/modules/hardware/openrazer.nix
index 883db7f2f4f19..b5c3d67441422 100644
--- a/nixos/modules/hardware/openrazer.nix
+++ b/nixos/modules/hardware/openrazer.nix
@@ -49,7 +49,7 @@ in
 {
   options = {
     hardware.openrazer = {
-      enable = mkEnableOption "OpenRazer drivers and userspace daemon.";
+      enable = mkEnableOption "OpenRazer drivers and userspace daemon";
 
       verboseLogging = mkOption {
         type = types.bool;
diff --git a/nixos/modules/hardware/tuxedo-keyboard.nix b/nixos/modules/hardware/tuxedo-keyboard.nix
new file mode 100644
index 0000000000000..898eed2449355
--- /dev/null
+++ b/nixos/modules/hardware/tuxedo-keyboard.nix
@@ -0,0 +1,35 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let 
+  cfg = config.hardware.tuxedo-keyboard;
+  tuxedo-keyboard = config.boot.kernelPackages.tuxedo-keyboard;
+in
+  {
+    options.hardware.tuxedo-keyboard = {
+      enable = mkEnableOption ''
+          Enables the tuxedo-keyboard driver.
+
+          To configure the driver, pass the options to the <option>boot.kernelParams</option> configuration.
+          There are several parameters you can change. It's best to check at the source code description which options are supported.
+          You can find all the supported parameters at: <link xlink:href="https://github.com/tuxedocomputers/tuxedo-keyboard#kernelparam" />
+
+          In order to use the <literal>custom</literal> lighting with the maximumg brightness and a color of <literal>0xff0a0a</literal> one would put pass <option>boot.kernelParams</option> like this:
+
+          <programlisting>
+          boot.kernelParams = [
+           "tuxedo_keyboard.mode=0"
+           "tuxedo_keyboard.brightness=255"
+           "tuxedo_keyboard.color_left=0xff0a0a"
+          ];
+          </programlisting>
+      '';
+    };
+
+    config = mkIf cfg.enable 
+    {
+      boot.kernelModules = ["tuxedo_keyboard"];
+      boot.extraModulePackages = [ tuxedo-keyboard ];
+    };
+  }
diff --git a/nixos/modules/installer/cd-dvd/installation-cd-graphical-kde-new-kernel.nix b/nixos/modules/installer/cd-dvd/installation-cd-graphical-plasma5-new-kernel.nix
index 3336d512cfd86..d98325a99ac2a 100644
--- a/nixos/modules/installer/cd-dvd/installation-cd-graphical-kde-new-kernel.nix
+++ b/nixos/modules/installer/cd-dvd/installation-cd-graphical-plasma5-new-kernel.nix
@@ -1,7 +1,7 @@
 { pkgs, ... }:
 
 {
-  imports = [ ./installation-cd-graphical-kde.nix ];
+  imports = [ ./installation-cd-graphical-plasma5.nix ];
 
   boot.kernelPackages = pkgs.linuxPackages_latest;
 }
diff --git a/nixos/modules/installer/cd-dvd/installation-cd-graphical-kde.nix b/nixos/modules/installer/cd-dvd/installation-cd-graphical-plasma5.nix
index e00d3f7535b2f..e00d3f7535b2f 100644
--- a/nixos/modules/installer/cd-dvd/installation-cd-graphical-kde.nix
+++ b/nixos/modules/installer/cd-dvd/installation-cd-graphical-plasma5.nix
diff --git a/nixos/modules/installer/tools/nixos-option/nixos-option.cc b/nixos/modules/installer/tools/nixos-option/nixos-option.cc
index 9b92dc829cd16..1a7b07a74f8ac 100644
--- a/nixos/modules/installer/tools/nixos-option/nixos-option.cc
+++ b/nixos/modules/installer/tools/nixos-option/nixos-option.cc
@@ -131,12 +131,12 @@ bool isOption(Context & ctx, const Value & v)
     if (v.type != tAttrs) {
         return false;
     }
-    const auto & atualType = v.attrs->find(ctx.underscoreType);
-    if (atualType == v.attrs->end()) {
+    const auto & actualType = v.attrs->find(ctx.underscoreType);
+    if (actualType == v.attrs->end()) {
         return false;
     }
     try {
-        Value evaluatedType = evaluateValue(ctx, *atualType->value);
+        Value evaluatedType = evaluateValue(ctx, *actualType->value);
         if (evaluatedType.type != tString) {
             return false;
         }
@@ -197,9 +197,107 @@ void recurse(const std::function<bool(const std::string & path, std::variant<Val
     }
 }
 
-// Calls f on all the option names
-void mapOptions(const std::function<void(const std::string & path)> & f, Context & ctx, Value root)
+bool optionTypeIs(Context & ctx, Value & v, const std::string & soughtType)
 {
+    try {
+        const auto & typeLookup = v.attrs->find(ctx.state.sType);
+        if (typeLookup == v.attrs->end()) {
+            return false;
+        }
+        Value type = evaluateValue(ctx, *typeLookup->value);
+        if (type.type != tAttrs) {
+            return false;
+        }
+        const auto & nameLookup = type.attrs->find(ctx.state.sName);
+        if (nameLookup == type.attrs->end()) {
+            return false;
+        }
+        Value name = evaluateValue(ctx, *nameLookup->value);
+        if (name.type != tString) {
+            return false;
+        }
+        return name.string.s == soughtType;
+    } catch (Error &) {
+        return false;
+    }
+}
+
+bool isAggregateOptionType(Context & ctx, Value & v)
+{
+    return optionTypeIs(ctx, v, "attrsOf") || optionTypeIs(ctx, v, "listOf") || optionTypeIs(ctx, v, "loaOf");
+}
+
+MakeError(OptionPathError, EvalError);
+
+Value getSubOptions(Context & ctx, Value & option)
+{
+    Value getSubOptions = evaluateValue(ctx, *findAlongAttrPath(ctx.state, "type.getSubOptions", ctx.autoArgs, option));
+    if (getSubOptions.type != tLambda) {
+        throw OptionPathError("Option's type.getSubOptions isn't a function");
+    }
+    Value emptyString{};
+    nix::mkString(emptyString, "");
+    Value v;
+    ctx.state.callFunction(getSubOptions, emptyString, v, nix::Pos{});
+    return v;
+}
+
+// Carefully walk an option path, looking for sub-options when a path walks past
+// an option value.
+struct FindAlongOptionPathRet
+{
+    Value option;
+    std::string path;
+};
+FindAlongOptionPathRet findAlongOptionPath(Context & ctx, const std::string & path)
+{
+    Strings tokens = parseAttrPath(path);
+    Value v = ctx.optionsRoot;
+    std::string processedPath;
+    for (auto i = tokens.begin(); i != tokens.end(); i++) {
+        const auto & attr = *i;
+        try {
+            bool lastAttribute = std::next(i) == tokens.end();
+            v = evaluateValue(ctx, v);
+            if (attr.empty()) {
+                throw OptionPathError("empty attribute name");
+            }
+            if (isOption(ctx, v) && optionTypeIs(ctx, v, "submodule")) {
+                v = getSubOptions(ctx, v);
+            }
+            if (isOption(ctx, v) && isAggregateOptionType(ctx, v)) {
+                auto subOptions = getSubOptions(ctx, v);
+                if (lastAttribute && subOptions.attrs->empty()) {
+                    break;
+                }
+                v = subOptions;
+                // Note that we've consumed attr, but didn't actually use it.  This is the path component that's looked
+                // up in the list or attribute set that doesn't name an option -- the "root" in "users.users.root.name".
+            } else if (v.type != tAttrs) {
+                throw OptionPathError("Value is %s while a set was expected", showType(v));
+            } else {
+                const auto & next = v.attrs->find(ctx.state.symbols.create(attr));
+                if (next == v.attrs->end()) {
+                    throw OptionPathError("Attribute not found", attr, path);
+                }
+                v = *next->value;
+            }
+            processedPath = appendPath(processedPath, attr);
+        } catch (OptionPathError & e) {
+            throw OptionPathError("At '%s' in path '%s': %s", attr, path, e.msg());
+        }
+    }
+    return {v, processedPath};
+}
+
+// Calls f on all the option names at or below the option described by `path`.
+// Note that "the option described by `path`" is not trivial -- if path describes a value inside an aggregate
+// option (such as users.users.root), the *option* described by that path is one path component shorter
+// (eg: users.users), which results in f being called on sibling-paths (eg: users.users.nixbld1).  If f
+// doesn't want these, it must do its own filtering.
+void mapOptions(const std::function<void(const std::string & path)> & f, Context & ctx, const std::string & path)
+{
+    auto root = findAlongOptionPath(ctx, path);
     recurse(
         [f, &ctx](const std::string & path, std::variant<Value, std::exception_ptr> v) {
             bool isOpt = std::holds_alternative<std::exception_ptr>(v) || isOption(ctx, std::get<Value>(v));
@@ -208,7 +306,7 @@ void mapOptions(const std::function<void(const std::string & path)> & f, Context
             }
             return !isOpt;
         },
-        ctx, root, "");
+        ctx, root.option, root.path);
 }
 
 // Calls f on all the config values inside one option.
@@ -294,9 +392,11 @@ void printAttrs(Context & ctx, Out & out, Value & v, const std::string & path)
     Out attrsOut(out, "{", "}", v.attrs->size());
     for (const auto & a : v.attrs->lexicographicOrder()) {
         std::string name = a->name;
-        attrsOut << name << " = ";
-        printValue(ctx, attrsOut, *a->value, appendPath(path, name));
-        attrsOut << ";" << Out::sep;
+        if (!forbiddenRecursionName(name)) {
+            attrsOut << name << " = ";
+            printValue(ctx, attrsOut, *a->value, appendPath(path, name));
+            attrsOut << ";" << Out::sep;
+        }
     }
 }
 
@@ -380,17 +480,26 @@ void printConfigValue(Context & ctx, Out & out, const std::string & path, std::v
     out << ";\n";
 }
 
-void printAll(Context & ctx, Out & out)
+// Replace with std::starts_with when C++20 is available
+bool starts_with(const std::string & s, const std::string & prefix)
+{
+    return s.size() >= prefix.size() &&
+           std::equal(s.begin(), std::next(s.begin(), prefix.size()), prefix.begin(), prefix.end());
+}
+
+void printRecursive(Context & ctx, Out & out, const std::string & path)
 {
     mapOptions(
-        [&ctx, &out](const std::string & optionPath) {
+        [&ctx, &out, &path](const std::string & optionPath) {
             mapConfigValuesInOption(
-                [&ctx, &out](const std::string & configPath, std::variant<Value, std::exception_ptr> v) {
-                    printConfigValue(ctx, out, configPath, v);
+                [&ctx, &out, &path](const std::string & configPath, std::variant<Value, std::exception_ptr> v) {
+                    if (starts_with(configPath, path)) {
+                        printConfigValue(ctx, out, configPath, v);
+                    }
                 },
                 optionPath, ctx);
         },
-        ctx, ctx.optionsRoot);
+        ctx, path);
 }
 
 void printAttr(Context & ctx, Out & out, const std::string & path, Value & root)
@@ -450,95 +559,17 @@ void printListing(Out & out, Value & v)
     }
 }
 
-bool optionTypeIs(Context & ctx, Value & v, const std::string & soughtType)
-{
-    try {
-        const auto & typeLookup = v.attrs->find(ctx.state.sType);
-        if (typeLookup == v.attrs->end()) {
-            return false;
-        }
-        Value type = evaluateValue(ctx, *typeLookup->value);
-        if (type.type != tAttrs) {
-            return false;
-        }
-        const auto & nameLookup = type.attrs->find(ctx.state.sName);
-        if (nameLookup == type.attrs->end()) {
-            return false;
-        }
-        Value name = evaluateValue(ctx, *nameLookup->value);
-        if (name.type != tString) {
-            return false;
-        }
-        return name.string.s == soughtType;
-    } catch (Error &) {
-        return false;
-    }
-}
-
-bool isAggregateOptionType(Context & ctx, Value & v)
-{
-    return optionTypeIs(ctx, v, "attrsOf") || optionTypeIs(ctx, v, "listOf") || optionTypeIs(ctx, v, "loaOf");
-}
-
-MakeError(OptionPathError, EvalError);
-
-Value getSubOptions(Context & ctx, Value & option)
-{
-    Value getSubOptions = evaluateValue(ctx, *findAlongAttrPath(ctx.state, "type.getSubOptions", ctx.autoArgs, option));
-    if (getSubOptions.type != tLambda) {
-        throw OptionPathError("Option's type.getSubOptions isn't a function");
-    }
-    Value emptyString{};
-    nix::mkString(emptyString, "");
-    Value v;
-    ctx.state.callFunction(getSubOptions, emptyString, v, nix::Pos{});
-    return v;
-}
-
-// Carefully walk an option path, looking for sub-options when a path walks past
-// an option value.
-Value findAlongOptionPath(Context & ctx, const std::string & path)
-{
-    Strings tokens = parseAttrPath(path);
-    Value v = ctx.optionsRoot;
-    for (auto i = tokens.begin(); i != tokens.end(); i++) {
-        const auto & attr = *i;
-        try {
-            bool lastAttribute = std::next(i) == tokens.end();
-            v = evaluateValue(ctx, v);
-            if (attr.empty()) {
-                throw OptionPathError("empty attribute name");
-            }
-            if (isOption(ctx, v) && optionTypeIs(ctx, v, "submodule")) {
-                v = getSubOptions(ctx, v);
-            }
-            if (isOption(ctx, v) && isAggregateOptionType(ctx, v) && !lastAttribute) {
-                v = getSubOptions(ctx, v);
-                // Note that we've consumed attr, but didn't actually use it.  This is the path component that's looked
-                // up in the list or attribute set that doesn't name an option -- the "root" in "users.users.root.name".
-            } else if (v.type != tAttrs) {
-                throw OptionPathError("Value is %s while a set was expected", showType(v));
-            } else {
-                const auto & next = v.attrs->find(ctx.state.symbols.create(attr));
-                if (next == v.attrs->end()) {
-                    throw OptionPathError("Attribute not found", attr, path);
-                }
-                v = *next->value;
-            }
-        } catch (OptionPathError & e) {
-            throw OptionPathError("At '%s' in path '%s': %s", attr, path, e.msg());
-        }
-    }
-    return v;
-}
-
 void printOne(Context & ctx, Out & out, const std::string & path)
 {
     try {
-        Value option = findAlongOptionPath(ctx, path);
+        auto result = findAlongOptionPath(ctx, path);
+        Value & option = result.option;
         option = evaluateValue(ctx, option);
+        if (path != result.path) {
+            out << "Note: showing " << result.path << " instead of " << path << "\n";
+        }
         if (isOption(ctx, option)) {
-            printOption(ctx, out, path, option);
+            printOption(ctx, out, result.path, option);
         } else {
             printListing(out, option);
         }
@@ -552,7 +583,7 @@ void printOne(Context & ctx, Out & out, const std::string & path)
 
 int main(int argc, char ** argv)
 {
-    bool all = false;
+    bool recursive = false;
     std::string path = ".";
     std::string optionsExpr = "(import <nixpkgs/nixos> {}).options";
     std::string configExpr = "(import <nixpkgs/nixos> {}).config";
@@ -568,8 +599,8 @@ int main(int argc, char ** argv)
             nix::showManPage("nixos-option");
         } else if (*arg == "--version") {
             nix::printVersion("nixos-option");
-        } else if (*arg == "--all") {
-            all = true;
+        } else if (*arg == "-r" || *arg == "--recursive") {
+            recursive = true;
         } else if (*arg == "--path") {
             path = nix::getArg(*arg, arg, end);
         } else if (*arg == "--options_expr") {
@@ -598,18 +629,12 @@ int main(int argc, char ** argv)
     Context ctx{*state, *myArgs.getAutoArgs(*state), optionsRoot, configRoot};
     Out out(std::cout);
 
-    if (all) {
-        if (!args.empty()) {
-            throw UsageError("--all cannot be used with arguments");
-        }
-        printAll(ctx, out);
-    } else {
-        if (args.empty()) {
-            printOne(ctx, out, "");
-        }
-        for (const auto & arg : args) {
-            printOne(ctx, out, arg);
-        }
+    auto print = recursive ? printRecursive : printOne;
+    if (args.empty()) {
+        print(ctx, out, "");
+    }
+    for (const auto & arg : args) {
+        print(ctx, out, arg);
     }
 
     ctx.state.printStats();
diff --git a/nixos/modules/installer/tools/nixos-rebuild.sh b/nixos/modules/installer/tools/nixos-rebuild.sh
index 61b4af1102739..7db323d38e680 100644
--- a/nixos/modules/installer/tools/nixos-rebuild.sh
+++ b/nixos/modules/installer/tools/nixos-rebuild.sh
@@ -91,9 +91,7 @@ while [ "$#" -gt 0 ]; do
         shift 1
         ;;
       --use-remote-sudo)
-        # note the trailing space
         maybeSudo=(sudo --)
-        shift 1
         ;;
       *)
         echo "$0: unknown option \`$i'"
diff --git a/nixos/modules/misc/locate.nix b/nixos/modules/misc/locate.nix
index 552535c253e61..dc668796c7886 100644
--- a/nixos/modules/misc/locate.nix
+++ b/nixos/modules/misc/locate.nix
@@ -131,13 +131,6 @@ in {
             ++ optional (isFindutils && cfg.pruneNames != []) "findutils locate does not support pruning by directory component"
             ++ optional (isFindutils && cfg.pruneBindMounts) "findutils locate does not support skipping bind mounts";
 
-    # directory creation needs to be separated from main service
-    # because ReadWritePaths fails when the directory doesn't already exist
-    systemd.tmpfiles.rules =
-      let dir = dirOf cfg.output; in
-      mkIf (dir != "/var/cache")
-        [ "d ${dir} 0755 root root -" ];
-
     systemd.services.update-locatedb =
       { description = "Update Locate Database";
         path = mkIf (!isMLocate) [ pkgs.su ];
diff --git a/nixos/modules/misc/version.nix b/nixos/modules/misc/version.nix
index ddbd3963cc57a..8a85035ceb7ce 100644
--- a/nixos/modules/misc/version.nix
+++ b/nixos/modules/misc/version.nix
@@ -6,6 +6,7 @@ let
   cfg = config.system.nixos;
 
   gitRepo      = "${toString pkgs.path}/.git";
+  gitRepoValid = lib.pathIsGitRepo gitRepo;
   gitCommitId  = lib.substring 0 7 (commitIdFromGitRepo gitRepo);
 in
 
@@ -91,8 +92,8 @@ in
       # These defaults are set here rather than up there so that
       # changing them would not rebuild the manual
       version = mkDefault (cfg.release + cfg.versionSuffix);
-      revision      = mkIf (pathExists gitRepo) (mkDefault            gitCommitId);
-      versionSuffix = mkIf (pathExists gitRepo) (mkDefault (".git." + gitCommitId));
+      revision      = mkIf gitRepoValid (mkDefault            gitCommitId);
+      versionSuffix = mkIf gitRepoValid (mkDefault (".git." + gitCommitId));
     };
 
     # Generate /etc/os-release.  See
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index eadf1d2d89b3c..fb5331f11abd6 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -62,6 +62,7 @@
   ./hardware/printers.nix
   ./hardware/raid/hpsa.nix
   ./hardware/steam-hardware.nix
+  ./hardware/tuxedo-keyboard.nix
   ./hardware/usb-wwan.nix
   ./hardware/onlykey.nix
   ./hardware/video/amdgpu.nix
@@ -116,6 +117,7 @@
   ./programs/fish.nix
   ./programs/freetds.nix
   ./programs/fuse.nix
+  ./programs/geary.nix
   ./programs/gnome-disks.nix
   ./programs/gnome-documents.nix
   ./programs/gnome-terminal.nix
@@ -279,6 +281,7 @@
   ./services/databases/riak.nix
   ./services/databases/riak-cs.nix
   ./services/databases/stanchion.nix
+  ./services/databases/victoriametrics.nix
   ./services/databases/virtuoso.nix
   ./services/desktops/accountsservice.nix
   ./services/desktops/bamf.nix
@@ -425,6 +428,7 @@
   ./services/misc/exhibitor.nix
   ./services/misc/felix.nix
   ./services/misc/folding-at-home.nix
+  ./services/misc/freeswitch.nix
   ./services/misc/fstrim.nix
   ./services/misc/gammu-smsd.nix
   ./services/misc/geoip-updater.nix
@@ -525,6 +529,7 @@
   ./services/monitoring/prometheus/alertmanager.nix
   ./services/monitoring/prometheus/exporters.nix
   ./services/monitoring/prometheus/pushgateway.nix
+  ./services/monitoring/prometheus/xmpp-alerts.nix
   ./services/monitoring/riemann.nix
   ./services/monitoring/riemann-dash.nix
   ./services/monitoring/riemann-tools.nix
@@ -586,7 +591,7 @@
   ./services/networking/dhcpd.nix
   ./services/networking/dnscache.nix
   ./services/networking/dnschain.nix
-  ./services/networking/dnscrypt-proxy.nix
+  ./services/networking/dnscrypt-proxy2.nix
   ./services/networking/dnscrypt-wrapper.nix
   ./services/networking/dnsdist.nix
   ./services/networking/dnsmasq.nix
@@ -805,6 +810,7 @@
   ./services/web-apps/codimd.nix
   ./services/web-apps/cryptpad.nix
   ./services/web-apps/documize.nix
+  ./services/web-apps/dokuwiki.nix
   ./services/web-apps/frab.nix
   ./services/web-apps/gotify-server.nix
   ./services/web-apps/icingaweb2/icingaweb2.nix
@@ -862,7 +868,6 @@
   ./services/x11/unclutter.nix
   ./services/x11/unclutter-xfixes.nix
   ./services/x11/desktop-managers/default.nix
-  ./services/x11/display-managers/auto.nix
   ./services/x11/display-managers/default.nix
   ./services/x11/display-managers/gdm.nix
   ./services/x11/display-managers/lightdm.nix
diff --git a/nixos/modules/programs/geary.nix b/nixos/modules/programs/geary.nix
new file mode 100644
index 0000000000000..01803bc411e54
--- /dev/null
+++ b/nixos/modules/programs/geary.nix
@@ -0,0 +1,20 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.geary;
+
+in {
+  options = {
+    programs.geary.enable = mkEnableOption "Geary, a Mail client for GNOME 3";
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.gnome3.geary ];
+    programs.dconf.enable = true;
+    services.gnome3.gnome-keyring.enable = true;
+    services.gnome3.gnome-online-accounts.enable = true;
+  };
+}
+
diff --git a/nixos/modules/programs/gnupg.nix b/nixos/modules/programs/gnupg.nix
index 2d262d9065796..7a3cb588ee719 100644
--- a/nixos/modules/programs/gnupg.nix
+++ b/nixos/modules/programs/gnupg.nix
@@ -96,7 +96,7 @@ in
     # This overrides the systemd user unit shipped with the gnupg package
     systemd.user.services.gpg-agent = mkIf (cfg.agent.pinentryFlavor != null) {
       serviceConfig.ExecStart = [ "" ''
-        ${pkgs.gnupg}/bin/gpg-agent --supervised \
+        ${cfg.package}/bin/gpg-agent --supervised \
           --pinentry-program ${pkgs.pinentry.${cfg.agent.pinentryFlavor}}/bin/pinentry
       '' ];
     };
diff --git a/nixos/modules/programs/sway.nix b/nixos/modules/programs/sway.nix
index 33e252be45f8f..7e646f8737d67 100644
--- a/nixos/modules/programs/sway.nix
+++ b/nixos/modules/programs/sway.nix
@@ -87,7 +87,8 @@ in {
       type = with types; listOf package;
       default = with pkgs; [
         swaylock swayidle
-        xwayland rxvt_unicode dmenu
+        xwayland alacritty dmenu
+        rxvt_unicode # For backward compatibility (old default terminal)
       ];
       defaultText = literalExample ''
         with pkgs; [ swaylock swayidle xwayland rxvt_unicode dmenu ];
diff --git a/nixos/modules/rename.nix b/nixos/modules/rename.nix
index 26de8a18d9227..3b1b1b8bb55c8 100644
--- a/nixos/modules/rename.nix
+++ b/nixos/modules/rename.nix
@@ -34,6 +34,14 @@ with lib;
       as the underlying package isn't being maintained. Working alternatives are
       libinput and synaptics.
     '')
+    (mkRemovedOptionModule [ "services" "xserver" "displayManager" "auto" ] ''
+      The services.xserver.displayManager.auto module has been removed
+      because it was only intended for use in internal NixOS tests, and gave the
+      false impression of it being a special display manager when it's actually
+      LightDM. Please use the services.xserver.displayManager.lightdm.autoLogin options
+      instead, or any other display manager in NixOS as they all support auto-login.
+    '')
+    (mkRemovedOptionModule [ "services" "dnscrypt-proxy" ] "Use services.dnscrypt-proxy2 instead")
 
     # Do NOT add any option renames here, see top of the file
   ];
diff --git a/nixos/modules/security/duosec.nix b/nixos/modules/security/duosec.nix
index 78a82b7154e75..c686a6861d0fc 100644
--- a/nixos/modules/security/duosec.nix
+++ b/nixos/modules/security/duosec.nix
@@ -12,7 +12,7 @@ let
     ikey=${cfg.ikey}
     skey=${cfg.skey}
     host=${cfg.host}
-    ${optionalString (cfg.group != "") ("group="+cfg.group)}
+    ${optionalString (cfg.groups != "") ("groups="+cfg.groups)}
     failmode=${cfg.failmode}
     pushinfo=${boolToStr cfg.pushinfo}
     autopush=${boolToStr cfg.autopush}
@@ -42,6 +42,10 @@ let
   };
 in
 {
+  imports = [
+    (mkRenamedOptionModule [ "security" "duosec" "group" ] [ "security" "duosec" "groups" ])
+  ];
+
   options = {
     security.duosec = {
       ssh.enable = mkOption {
@@ -71,10 +75,16 @@ in
         description = "Duo API hostname.";
       };
 
-      group = mkOption {
+      groups = mkOption {
         type = types.str;
         default = "";
-        description = "Use Duo authentication for users only in this group.";
+        example = "users,!wheel,!*admin guests";
+        description = ''
+          If specified, Duo authentication is required only for users
+          whose primary group or supplementary group list matches one
+          of the space-separated pattern lists. Refer to
+          <link xlink:href="https://duo.com/docs/duounix"/> for details.
+        '';
       };
 
       failmode = mkOption {
diff --git a/nixos/modules/services/amqp/rabbitmq.nix b/nixos/modules/services/amqp/rabbitmq.nix
index 35fb49f709a69..f80d6b3f1ba56 100644
--- a/nixos/modules/services/amqp/rabbitmq.nix
+++ b/nixos/modules/services/amqp/rabbitmq.nix
@@ -98,8 +98,8 @@ in {
           will be merged into these options by RabbitMQ at runtime to
           form the final configuration.
 
-          See http://www.rabbitmq.com/configure.html#config-items
-          For the distinct formats, see http://www.rabbitmq.com/configure.html#config-file-formats
+          See https://www.rabbitmq.com/configure.html#config-items
+          For the distinct formats, see https://www.rabbitmq.com/configure.html#config-file-formats
         '';
       };
 
@@ -116,8 +116,8 @@ in {
           The contents of this option will be merged into the <literal>configItems</literal>
           by RabbitMQ at runtime to form the final configuration.
 
-          See the second table on http://www.rabbitmq.com/configure.html#config-items
-          For the distinct formats, see http://www.rabbitmq.com/configure.html#config-file-formats
+          See the second table on https://www.rabbitmq.com/configure.html#config-items
+          For the distinct formats, see https://www.rabbitmq.com/configure.html#config-file-formats
         '';
       };
 
diff --git a/nixos/modules/services/databases/victoriametrics.nix b/nixos/modules/services/databases/victoriametrics.nix
new file mode 100644
index 0000000000000..cb6bf8508fb65
--- /dev/null
+++ b/nixos/modules/services/databases/victoriametrics.nix
@@ -0,0 +1,70 @@
+{ config, pkgs, lib, ... }:
+let cfg = config.services.victoriametrics; in
+{
+  options.services.victoriametrics = with lib; {
+    enable = mkEnableOption "victoriametrics";
+    package = mkOption {
+      type = types.package;
+      default = pkgs.victoriametrics;
+      defaultText = "pkgs.victoriametrics";
+      description = ''
+        The VictoriaMetrics distribution to use.
+      '';
+    };
+    listenAddress = mkOption {
+      default = ":8428";
+      type = types.str;
+      description = ''
+        The listen address for the http interface.
+      '';
+    };
+    retentionPeriod = mkOption {
+      type = types.int;
+      default = 1;
+      description = ''
+        Retention period in months.
+      '';
+    };
+    extraOptions = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+        Extra options to pass to VictoriaMetrics. See the README: <link
+        xlink:href="https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/README.md" />
+        or <command>victoriametrics -help</command> for more
+        information.
+      '';
+    };
+  };
+  config = lib.mkIf cfg.enable {
+    systemd.services.victoriametrics = {
+      description = "VictoriaMetrics time series database";
+      after = [ "network.target" ];
+      serviceConfig = {
+        Restart = "on-failure";
+        RestartSec = 1;
+        StartLimitBurst = 5;
+        StateDirectory = "victoriametrics";
+        DynamicUser = true;
+        ExecStart = ''
+          ${cfg.package}/bin/victoria-metrics \
+              -storageDataPath=/var/lib/victoriametrics \
+              -httpListenAddr ${cfg.listenAddress}
+              -retentionPeriod ${toString cfg.retentionPeriod}
+              ${lib.escapeShellArgs cfg.extraOptions}
+        '';
+      };
+      wantedBy = [ "multi-user.target" ];
+
+      postStart =
+        let
+          bindAddr = (lib.optionalString (lib.hasPrefix ":" cfg.listenAddress) "127.0.0.1") + cfg.listenAddress;
+        in
+        lib.mkBefore ''
+          until ${lib.getBin pkgs.curl}/bin/curl -s -o /dev/null http://${bindAddr}/ping; do
+            sleep 1;
+          done
+        '';
+    };
+  };
+}
diff --git a/nixos/modules/services/desktops/gnome3/at-spi2-core.nix b/nixos/modules/services/desktops/gnome3/at-spi2-core.nix
index cca98c43dc7a2..8fa108c4f9df4 100644
--- a/nixos/modules/services/desktops/gnome3/at-spi2-core.nix
+++ b/nixos/modules/services/desktops/gnome3/at-spi2-core.nix
@@ -18,6 +18,9 @@ with lib;
         description = ''
           Whether to enable at-spi2-core, a service for the Assistive Technologies
           available on the GNOME platform.
+
+          Enable this if you get the error or warning
+          <literal>The name org.a11y.Bus was not provided by any .service files</literal>.
         '';
       };
 
diff --git a/nixos/modules/services/development/jupyter/default.nix b/nixos/modules/services/development/jupyter/default.nix
index f20860af6e128..e598b0186450b 100644
--- a/nixos/modules/services/development/jupyter/default.nix
+++ b/nixos/modules/services/development/jupyter/default.nix
@@ -118,15 +118,15 @@ in {
           in {
             displayName = "Python 3 for machine learning";
             argv = [
-              "$ {env.interpreter}"
+              "''${env.interpreter}"
               "-m"
               "ipykernel_launcher"
               "-f"
               "{connection_file}"
             ];
             language = "python";
-            logo32 = "$ {env.sitePackages}/ipykernel/resources/logo-32x32.png";
-            logo64 = "$ {env.sitePackages}/ipykernel/resources/logo-64x64.png";
+            logo32 = "''${env.sitePackages}/ipykernel/resources/logo-32x32.png";
+            logo64 = "''${env.sitePackages}/ipykernel/resources/logo-64x64.png";
           };
         }
       '';
diff --git a/nixos/modules/services/hardware/irqbalance.nix b/nixos/modules/services/hardware/irqbalance.nix
index b139154432cf9..c79e0eb83ecea 100644
--- a/nixos/modules/services/hardware/irqbalance.nix
+++ b/nixos/modules/services/hardware/irqbalance.nix
@@ -13,18 +13,12 @@ in
 
   config = mkIf cfg.enable {
 
-    systemd.services = {
-      irqbalance = {
-        description = "irqbalance daemon";
-        path = [ pkgs.irqbalance ];
-        serviceConfig =
-          { ExecStart = "${pkgs.irqbalance}/bin/irqbalance --foreground"; };
-        wantedBy = [ "multi-user.target" ];
-      };
-    };
-
     environment.systemPackages = [ pkgs.irqbalance ];
 
+    systemd.services.irqbalance.wantedBy = ["multi-user.target"];
+
+    systemd.packages = [ pkgs.irqbalance ];
+
   };
 
 }
diff --git a/nixos/modules/services/mail/mailman.nix b/nixos/modules/services/mail/mailman.nix
index e917209f3d1f5..43dc185cdd777 100644
--- a/nixos/modules/services/mail/mailman.nix
+++ b/nixos/modules/services/mail/mailman.nix
@@ -6,37 +6,18 @@ let
 
   cfg = config.services.mailman;
 
-  mailmanPyEnv = pkgs.python3.withPackages (ps: with ps; [mailman mailman-hyperkitty]);
-
-  mailmanExe = with pkgs; stdenv.mkDerivation {
-    name = "mailman-" + python3Packages.mailman.version;
-    buildInputs = [makeWrapper];
-    unpackPhase = ":";
-    installPhase = ''
-      mkdir -p $out/bin
-      makeWrapper ${mailmanPyEnv}/bin/mailman $out/bin/mailman \
-        --set MAILMAN_CONFIG_FILE /etc/mailman.cfg
-   '';
-  };
-
-  mailmanWeb = pkgs.python3Packages.mailman-web.override {
-    serverEMail = cfg.siteOwner;
-    archiverKey = cfg.hyperkittyApiKey;
-    allowedHosts = cfg.webHosts;
-  };
-
-  mailmanWebPyEnv = pkgs.python3.withPackages (x: with x; [mailman-web]);
-
-  mailmanWebExe = with pkgs; stdenv.mkDerivation {
-    inherit (mailmanWeb) name;
-    buildInputs = [makeWrapper];
-    unpackPhase = ":";
-    installPhase = ''
-      mkdir -p $out/bin
-      makeWrapper ${mailmanWebPyEnv}/bin/django-admin $out/bin/mailman-web \
-        --set DJANGO_SETTINGS_MODULE settings
-    '';
-  };
+  # This deliberately doesn't use recursiveUpdate so users can
+  # override the defaults.
+  settings = {
+    DEFAULT_FROM_EMAIL = cfg.siteOwner;
+    SERVER_EMAIL = cfg.siteOwner;
+    ALLOWED_HOSTS = [ "localhost" "127.0.0.1" ] ++ cfg.webHosts;
+    COMPRESS_OFFLINE = true;
+    STATIC_ROOT = "/var/lib/mailman-web/static";
+    MEDIA_ROOT = "/var/lib/mailman-web/media";
+  } // cfg.webSettings;
+
+  settingsJSON = pkgs.writeText "settings.json" (builtins.toJSON settings);
 
   mailmanCfg = ''
     [mailman]
@@ -53,30 +34,42 @@ let
     etc_dir: /etc
     ext_dir: $etc_dir/mailman.d
     pid_file: /run/mailman/master.pid
-  '' + optionalString (cfg.hyperkittyApiKey != null) ''
+  '' + optionalString cfg.hyperkitty.enable ''
+
     [archiver.hyperkitty]
     class: mailman_hyperkitty.Archiver
     enable: yes
-    configuration: ${pkgs.writeText "mailman-hyperkitty.cfg" mailmanHyperkittyCfg}
+    configuration: /var/lib/mailman/mailman-hyperkitty.cfg
   '';
 
-  mailmanHyperkittyCfg = ''
+  mailmanHyperkittyCfg = pkgs.writeText "mailman-hyperkitty.cfg" ''
     [general]
     # This is your HyperKitty installation, preferably on the localhost. This
     # address will be used by Mailman to forward incoming emails to HyperKitty
     # for archiving. It does not need to be publicly available, in fact it's
     # better if it is not.
-    base_url: ${cfg.hyperkittyBaseUrl}
+    base_url: ${cfg.hyperkitty.baseUrl}
 
     # Shared API key, must be the identical to the value in HyperKitty's
     # settings.
-    api_key: ${cfg.hyperkittyApiKey}
+    api_key: @API_KEY@
   '';
 
 in {
 
   ###### interface
 
+  imports = [
+    (mkRenamedOptionModule [ "services" "mailman" "hyperkittyBaseUrl" ]
+      [ "services" "mailman" "hyperkitty" "baseUrl" ])
+
+    (mkRemovedOptionModule [ "services" "mailman" "hyperkittyApiKey" ] ''
+      The Hyperkitty API key is now generated on first run, and not
+      stored in the world-readable Nix store.  To continue using
+      Hyperkitty, you must set services.mailman.hyperkitty.enable = true.
+    '')
+  ];
+
   options = {
 
     services.mailman = {
@@ -87,9 +80,17 @@ in {
         description = "Enable Mailman on this host. Requires an active Postfix installation.";
       };
 
+      package = mkOption {
+        type = types.package;
+        default = pkgs.mailman;
+        defaultText = "pkgs.mailman";
+        example = "pkgs.mailman.override { archivers = []; }";
+        description = "Mailman package to use";
+      };
+
       siteOwner = mkOption {
         type = types.str;
-        default = "postmaster@example.org";
+        example = "postmaster@example.org";
         description = ''
           Certain messages that must be delivered to a human, but which can't
           be delivered to a list owner (e.g. a bounce from a list owner), will
@@ -99,12 +100,13 @@ in {
 
       webRoot = mkOption {
         type = types.path;
-        default = "${mailmanWeb}/${pkgs.python3.sitePackages}";
-        defaultText = "pkgs.python3Packages.mailman-web";
+        default = "${pkgs.mailman-web}/${pkgs.python3.sitePackages}";
+        defaultText = "\${pkgs.mailman-web}/\${pkgs.python3.sitePackages}";
         description = ''
           The web root for the Hyperkity + Postorius apps provided by Mailman.
           This variable can be set, of course, but it mainly exists so that site
-          admins can refer to it in their own hand-written httpd configuration files.
+          admins can refer to it in their own hand-written web server
+          configuration files.
         '';
       };
 
@@ -120,26 +122,35 @@ in {
         '';
       };
 
-      hyperkittyBaseUrl = mkOption {
+      webUser = mkOption {
         type = types.str;
-        default = "http://localhost/hyperkitty/";
+        default = config.services.httpd.user;
         description = ''
-          Where can Mailman connect to Hyperkitty's internal API, preferably on
-          localhost?
+          User to run mailman-web as
         '';
       };
 
-      hyperkittyApiKey = mkOption {
-        type = types.nullOr types.str;
-        default = null;
+      webSettings = mkOption {
+        type = types.attrs;
+        default = {};
         description = ''
-          The shared secret used to authenticate Mailman's internal
-          communication with Hyperkitty. Must be set to enable support for the
-          Hyperkitty archiver. Note that this secret is going to be visible to
-          all local users in the Nix store.
+          Overrides for the default mailman-web Django settings.
         '';
       };
 
+      hyperkitty = {
+        enable = mkEnableOption "the Hyperkitty archiver for Mailman";
+
+        baseUrl = mkOption {
+          type = types.str;
+          default = "http://localhost/hyperkitty/";
+          description = ''
+            Where can Mailman connect to Hyperkitty's internal API, preferably on
+            localhost?
+          '';
+        };
+      };
+
     };
   };
 
@@ -147,25 +158,58 @@ in {
 
   config = mkIf cfg.enable {
 
-    assertions = [
-      { assertion = cfg.enable -> config.services.postfix.enable;
+    assertions = let
+      inherit (config.services) postfix;
+
+      requirePostfixHash = optionPath: dataFile:
+        with lib;
+        let
+          expected = "hash:/var/lib/mailman/data/${dataFile}";
+          value = attrByPath optionPath [] postfix;
+        in
+          { assertion = postfix.enable -> isList value && elem expected value;
+            message = ''
+              services.postfix.${concatStringsSep "." optionPath} must contain
+              "${expected}".
+              See <https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html>.
+            '';
+          };
+    in [
+      { assertion = postfix.enable;
         message = "Mailman requires Postfix";
       }
+      (requirePostfixHash [ "relayDomains" ] "postfix_domains")
+      (requirePostfixHash [ "config" "transport_maps" ] "postfix_lmtp")
+      (requirePostfixHash [ "config" "local_recipient_maps" ] "postfix_lmtp")
     ];
 
     users.users.mailman = { description = "GNU Mailman"; isSystemUser = true; };
 
-    environment = {
-      systemPackages = [ mailmanExe mailmanWebExe pkgs.sassc ];
-      etc."mailman.cfg".text = mailmanCfg;
-    };
+    environment.etc."mailman.cfg".text = mailmanCfg;
+
+    environment.etc."mailman3/settings.py".text = ''
+      import os
+
+      # Required by mailman_web.settings, but will be overridden when
+      # settings_local.json is loaded.
+      os.environ["SECRET_KEY"] = ""
+
+      from mailman_web.settings import *
+
+      import json
+
+      with open('${settingsJSON}') as f:
+          globals().update(json.load(f))
+
+      with open('/var/lib/mailman-web/settings_local.json') as f:
+          globals().update(json.load(f))
+    '';
+
+    environment.systemPackages = [ cfg.package ] ++ (with pkgs; [ mailman-web ]);
 
     services.postfix = {
-      relayDomains = [ "hash:/var/lib/mailman/data/postfix_domains" ];
       recipientDelimiter = "+";         # bake recipient addresses in mail envelopes via VERP
       config = {
-        transport_maps = [ "hash:/var/lib/mailman/data/postfix_lmtp" ];
-        local_recipient_maps = [ "hash:/var/lib/mailman/data/postfix_lmtp" ];
         owner_request_special = "no";   # Mailman handles -owner addresses on its own
       };
     };
@@ -173,34 +217,71 @@ in {
     systemd.services.mailman = {
       description = "GNU Mailman Master Process";
       after = [ "network.target" ];
+      restartTriggers = [ config.environment.etc."mailman.cfg".source ];
       wantedBy = [ "multi-user.target" ];
       serviceConfig = {
-        ExecStart = "${mailmanExe}/bin/mailman start";
-        ExecStop = "${mailmanExe}/bin/mailman stop";
+        ExecStart = "${cfg.package}/bin/mailman start";
+        ExecStop = "${cfg.package}/bin/mailman stop";
         User = "mailman";
         Type = "forking";
-        StateDirectory = "mailman";
-        StateDirectoryMode = "0700";
         RuntimeDirectory = "mailman";
         PIDFile = "/run/mailman/master.pid";
       };
     };
 
+    systemd.services.mailman-settings = {
+      description = "Generate settings files (including secrets) for Mailman";
+      before = [ "mailman.service" "mailman-web.service" "hyperkitty.service" "httpd.service" "uwsgi.service" ];
+      requiredBy = [ "mailman.service" "mailman-web.service" "hyperkitty.service" "httpd.service" "uwsgi.service" ];
+      path = with pkgs; [ jq ];
+      script = ''
+        mailmanDir=/var/lib/mailman
+        mailmanWebDir=/var/lib/mailman-web
+
+        mailmanCfg=$mailmanDir/mailman-hyperkitty.cfg
+        mailmanWebCfg=$mailmanWebDir/settings_local.json
+
+        install -m 0700 -o mailman -g nogroup -d $mailmanDir
+        install -m 0700 -o ${cfg.webUser} -g nogroup -d $mailmanWebDir
+
+        if [ ! -e $mailmanWebCfg ]; then
+            hyperkittyApiKey=$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 64)
+            secretKey=$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 64)
+
+            mailmanWebCfgTmp=$(mktemp)
+            jq -n '.MAILMAN_ARCHIVER_KEY=$archiver_key | .SECRET_KEY=$secret_key' \
+                --arg archiver_key "$hyperkittyApiKey" \
+                --arg secret_key "$secretKey" \
+                >"$mailmanWebCfgTmp"
+            chown ${cfg.webUser} "$mailmanWebCfgTmp"
+            mv -n "$mailmanWebCfgTmp" $mailmanWebCfg
+        fi
+
+        hyperkittyApiKey="$(jq -r .MAILMAN_ARCHIVER_KEY $mailmanWebCfg)"
+        mailmanCfgTmp=$(mktemp)
+        sed "s/@API_KEY@/$hyperkittyApiKey/g" ${mailmanHyperkittyCfg} >"$mailmanCfgTmp"
+        chown mailman "$mailmanCfgTmp"
+        mv "$mailmanCfgTmp" $mailmanCfg
+      '';
+      serviceConfig = {
+        Type = "oneshot";
+      };
+    };
+
     systemd.services.mailman-web = {
       description = "Init Postorius DB";
-      before = [ "httpd.service" ];
-      requiredBy = [ "httpd.service" ];
+      before = [ "httpd.service" "uwsgi.service" ];
+      requiredBy = [ "httpd.service" "uwsgi.service" ];
+      restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
       script = ''
-        ${mailmanWebExe}/bin/mailman-web migrate
+        ${pkgs.mailman-web}/bin/mailman-web migrate
         rm -rf static
-        ${mailmanWebExe}/bin/mailman-web collectstatic
-        ${mailmanWebExe}/bin/mailman-web compress
+        ${pkgs.mailman-web}/bin/mailman-web collectstatic
+        ${pkgs.mailman-web}/bin/mailman-web compress
       '';
       serviceConfig = {
-        User = config.services.httpd.user;
+        User = cfg.webUser;
         Type = "oneshot";
-        StateDirectory = "mailman-web";
-        StateDirectoryMode = "0700";
         WorkingDirectory = "/var/lib/mailman-web";
       };
     };
@@ -208,86 +289,94 @@ in {
     systemd.services.mailman-daily = {
       description = "Trigger daily Mailman events";
       startAt = "daily";
+      restartTriggers = [ config.environment.etc."mailman.cfg".source ];
       serviceConfig = {
-        ExecStart = "${mailmanExe}/bin/mailman digests --send";
+        ExecStart = "${cfg.package}/bin/mailman digests --send";
         User = "mailman";
       };
     };
 
     systemd.services.hyperkitty = {
-      enable = cfg.hyperkittyApiKey != null;
+      inherit (cfg.hyperkitty) enable;
       description = "GNU Hyperkitty QCluster Process";
       after = [ "network.target" ];
+      restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
       wantedBy = [ "mailman.service" "multi-user.target" ];
       serviceConfig = {
-        ExecStart = "${mailmanWebExe}/bin/mailman-web qcluster";
-        User = config.services.httpd.user;
+        ExecStart = "${pkgs.mailman-web}/bin/mailman-web qcluster";
+        User = cfg.webUser;
         WorkingDirectory = "/var/lib/mailman-web";
       };
     };
 
     systemd.services.hyperkitty-minutely = {
-      enable = cfg.hyperkittyApiKey != null;
+      inherit (cfg.hyperkitty) enable;
       description = "Trigger minutely Hyperkitty events";
       startAt = "minutely";
+      restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
       serviceConfig = {
-        ExecStart = "${mailmanWebExe}/bin/mailman-web runjobs minutely";
-        User = config.services.httpd.user;
+        ExecStart = "${pkgs.mailman-web}/bin/mailman-web runjobs minutely";
+        User = cfg.webUser;
         WorkingDirectory = "/var/lib/mailman-web";
       };
     };
 
     systemd.services.hyperkitty-quarter-hourly = {
-      enable = cfg.hyperkittyApiKey != null;
+      inherit (cfg.hyperkitty) enable;
       description = "Trigger quarter-hourly Hyperkitty events";
       startAt = "*:00/15";
+      restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
       serviceConfig = {
-        ExecStart = "${mailmanWebExe}/bin/mailman-web runjobs quarter_hourly";
-        User = config.services.httpd.user;
+        ExecStart = "${pkgs.mailman-web}/bin/mailman-web runjobs quarter_hourly";
+        User = cfg.webUser;
         WorkingDirectory = "/var/lib/mailman-web";
       };
     };
 
     systemd.services.hyperkitty-hourly = {
-      enable = cfg.hyperkittyApiKey != null;
+      inherit (cfg.hyperkitty) enable;
       description = "Trigger hourly Hyperkitty events";
       startAt = "hourly";
+      restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
       serviceConfig = {
-        ExecStart = "${mailmanWebExe}/bin/mailman-web runjobs hourly";
-        User = config.services.httpd.user;
+        ExecStart = "${pkgs.mailman-web}/bin/mailman-web runjobs hourly";
+        User = cfg.webUser;
         WorkingDirectory = "/var/lib/mailman-web";
       };
     };
 
     systemd.services.hyperkitty-daily = {
-      enable = cfg.hyperkittyApiKey != null;
+      inherit (cfg.hyperkitty) enable;
       description = "Trigger daily Hyperkitty events";
       startAt = "daily";
+      restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
       serviceConfig = {
-        ExecStart = "${mailmanWebExe}/bin/mailman-web runjobs daily";
-        User = config.services.httpd.user;
+        ExecStart = "${pkgs.mailman-web}/bin/mailman-web runjobs daily";
+        User = cfg.webUser;
         WorkingDirectory = "/var/lib/mailman-web";
       };
     };
 
     systemd.services.hyperkitty-weekly = {
-      enable = cfg.hyperkittyApiKey != null;
+      inherit (cfg.hyperkitty) enable;
       description = "Trigger weekly Hyperkitty events";
       startAt = "weekly";
+      restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
       serviceConfig = {
-        ExecStart = "${mailmanWebExe}/bin/mailman-web runjobs weekly";
-        User = config.services.httpd.user;
+        ExecStart = "${pkgs.mailman-web}/bin/mailman-web runjobs weekly";
+        User = cfg.webUser;
         WorkingDirectory = "/var/lib/mailman-web";
       };
     };
 
     systemd.services.hyperkitty-yearly = {
-      enable = cfg.hyperkittyApiKey != null;
+      inherit (cfg.hyperkitty) enable;
       description = "Trigger yearly Hyperkitty events";
       startAt = "yearly";
+      restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
       serviceConfig = {
-        ExecStart = "${mailmanWebExe}/bin/mailman-web runjobs yearly";
-        User = config.services.httpd.user;
+        ExecStart = "${pkgs.mailman-web}/bin/mailman-web runjobs yearly";
+        User = cfg.webUser;
         WorkingDirectory = "/var/lib/mailman-web";
       };
     };
diff --git a/nixos/modules/services/mail/roundcube.nix b/nixos/modules/services/mail/roundcube.nix
index 36dda619ad063..0bb0eaedad500 100644
--- a/nixos/modules/services/mail/roundcube.nix
+++ b/nixos/modules/services/mail/roundcube.nix
@@ -5,6 +5,8 @@ with lib;
 let
   cfg = config.services.roundcube;
   fpm = config.services.phpfpm.pools.roundcube;
+  localDB = cfg.database.host == "localhost";
+  user = cfg.database.username;
 in
 {
   options.services.roundcube = {
@@ -44,7 +46,10 @@ in
       username = mkOption {
         type = types.str;
         default = "roundcube";
-        description = "Username for the postgresql connection";
+        description = ''
+          Username for the postgresql connection.
+          If <literal>database.host</literal> is set to <literal>localhost</literal>, a unix user and group of the same name will be created as well.
+        '';
       };
       host = mkOption {
         type = types.str;
@@ -58,7 +63,12 @@ in
       };
       password = mkOption {
         type = types.str;
-        description = "Password for the postgresql connection";
+        description = "Password for the postgresql connection. Do not use: the password will be stored world readable in the store; use <literal>passwordFile</literal> instead.";
+        default = "";
+      };
+      passwordFile = mkOption {
+        type = types.str;
+        description = "Password file for the postgresql connection. Must be readable by user <literal>nginx</literal>. Ignored if <literal>database.host</literal> is set to <literal>localhost</literal>, as peer authentication will be used.";
       };
       dbname = mkOption {
         type = types.str;
@@ -83,14 +93,22 @@ in
   };
 
   config = mkIf cfg.enable {
+    # backward compatibility: if password is set but not passwordFile, make one.
+    services.roundcube.database.passwordFile = mkIf (!localDB && cfg.database.password != "") (mkDefault ("${pkgs.writeText "roundcube-password" cfg.database.password}"));
+    warnings = lib.optional (!localDB && cfg.database.password != "") "services.roundcube.database.password is deprecated and insecure; use services.roundcube.database.passwordFile instead";
+
     environment.etc."roundcube/config.inc.php".text = ''
       <?php
 
+      ${lib.optionalString (!localDB) "$password = file_get_contents('${cfg.database.passwordFile}');"}
+
       $config = array();
-      $config['db_dsnw'] = 'pgsql://${cfg.database.username}:${cfg.database.password}@${cfg.database.host}/${cfg.database.dbname}';
+      $config['db_dsnw'] = 'pgsql://${cfg.database.username}${lib.optionalString (!localDB) ":' . $password . '"}@${if localDB then "unix(/run/postgresql)" else cfg.database.host}/${cfg.database.dbname}';
       $config['log_driver'] = 'syslog';
       $config['max_message_size'] = '25M';
       $config['plugins'] = [${concatMapStringsSep "," (p: "'${p}'") cfg.plugins}];
+      $config['des_key'] = file_get_contents('/var/lib/roundcube/des_key');
+      $config['mime_types'] = '${pkgs.nginx}/conf/mime.types';
       ${cfg.extraConfig}
     '';
 
@@ -116,12 +134,26 @@ in
       };
     };
 
-    services.postgresql = mkIf (cfg.database.host == "localhost") {
+    services.postgresql = mkIf localDB {
       enable = true;
+      ensureDatabases = [ cfg.database.dbname ];
+      ensureUsers = [ {
+        name = cfg.database.username;
+        ensurePermissions = {
+          "DATABASE ${cfg.database.username}" = "ALL PRIVILEGES";
+        };
+      } ];
+    };
+
+    users.users.${user} = mkIf localDB {
+      group = user;
+      isSystemUser = true;
+      createHome = false;
     };
+    users.groups.${user} = mkIf localDB {};
 
     services.phpfpm.pools.roundcube = {
-      user = "nginx";
+      user = if localDB then user else "nginx";
       phpOptions = ''
         error_log = 'stderr'
         log_errors = on
@@ -143,9 +175,7 @@ in
     };
     systemd.services.phpfpm-roundcube.after = [ "roundcube-setup.service" ];
 
-    systemd.services.roundcube-setup = let
-      pgSuperUser = config.services.postgresql.superUser;
-    in mkMerge [
+    systemd.services.roundcube-setup = mkMerge [
       (mkIf (cfg.database.host == "localhost") {
         requires = [ "postgresql.service" ];
         after = [ "postgresql.service" ];
@@ -153,22 +183,31 @@ in
       })
       {
         wantedBy = [ "multi-user.target" ];
-        script = ''
-          mkdir -p /var/lib/roundcube
-          if [ ! -f /var/lib/roundcube/db-created ]; then
-            if [ "${cfg.database.host}" = "localhost" ]; then
-              ${pkgs.sudo}/bin/sudo -u ${pgSuperUser} psql postgres -c "create role ${cfg.database.username} with login password '${cfg.database.password}'";
-              ${pkgs.sudo}/bin/sudo -u ${pgSuperUser} psql postgres -c "create database ${cfg.database.dbname} with owner ${cfg.database.username}";
-            fi
-            PGPASSWORD="${cfg.database.password}" ${pkgs.postgresql}/bin/psql -U ${cfg.database.username} \
-              -f ${cfg.package}/SQL/postgres.initial.sql \
-              -h ${cfg.database.host} ${cfg.database.dbname}
-            touch /var/lib/roundcube/db-created
+        script = let
+          psql = "${lib.optionalString (!localDB) "PGPASSFILE=${cfg.database.passwordFile}"} ${pkgs.postgresql}/bin/psql ${lib.optionalString (!localDB) "-h ${cfg.database.host} -U ${cfg.database.username} "} ${cfg.database.dbname}";
+        in
+        ''
+          version="$(${psql} -t <<< "select value from system where name = 'roundcube-version';" || true)"
+          if ! (grep -E '[a-zA-Z0-9]' <<< "$version"); then
+            ${psql} -f ${cfg.package}/SQL/postgres.initial.sql
+          fi
+
+          if [ ! -f /var/lib/roundcube/des_key ]; then
+            base64 /dev/urandom | head -c 24 > /var/lib/roundcube/des_key;
+            # we need to log out everyone in case change the des_key
+            # from the default when upgrading from nixos 19.09
+            ${psql} <<< 'TRUNCATE TABLE session;'
           fi
 
           ${pkgs.php}/bin/php ${cfg.package}/bin/update.sh
         '';
-        serviceConfig.Type = "oneshot";
+        serviceConfig = {
+          Type = "oneshot";
+          StateDirectory = "roundcube";
+          User = if localDB then user else "nginx";
+          # so that the des_key is not world readable
+          StateDirectoryMode = "0700";
+        };
       }
     ];
   };
diff --git a/nixos/modules/services/mail/spamassassin.nix b/nixos/modules/services/mail/spamassassin.nix
index 75442c7cdb5e1..2d5fb40fad357 100644
--- a/nixos/modules/services/mail/spamassassin.nix
+++ b/nixos/modules/services/mail/spamassassin.nix
@@ -6,15 +6,6 @@ let
   cfg = config.services.spamassassin;
   spamassassin-local-cf = pkgs.writeText "local.cf" cfg.config;
 
-  spamdEnv = pkgs.buildEnv {
-    name = "spamd-env";
-    paths = [];
-    postBuild = ''
-      ln -sf ${spamassassin-init-pre} $out/init.pre
-      ln -sf ${spamassassin-local-cf} $out/local.cf
-    '';
-  };
-
 in
 
 {
@@ -120,13 +111,11 @@ in
   };
 
   config = mkIf cfg.enable {
+    environment.etc."mail/spamassassin/init.pre".source = cfg.initPreConf;
+    environment.etc."mail/spamassassin/local.cf".source = spamassassin-local-cf;
 
     # Allow users to run 'spamc'.
-
-    environment = {
-      etc.spamassassin.source = spamdEnv;
-      systemPackages = [ pkgs.spamassassin ];
-    };
+    environment.systemPackages = [ pkgs.spamassassin ];
 
     users.users.spamd = {
       description = "Spam Assassin Daemon";
@@ -141,7 +130,7 @@ in
     systemd.services.sa-update = {
       script = ''
         set +e
-        ${pkgs.su}/bin/su -s "${pkgs.bash}/bin/bash" -c "${pkgs.spamassassin}/bin/sa-update --gpghomedir=/var/lib/spamassassin/sa-update-keys/ --siteconfigpath=${spamdEnv}/" spamd
+        ${pkgs.su}/bin/su -s "${pkgs.bash}/bin/bash" -c "${pkgs.spamassassin}/bin/sa-update --gpghomedir=/var/lib/spamassassin/sa-update-keys/" spamd
 
         v=$?
         set -e
@@ -172,7 +161,7 @@ in
       after = [ "network.target" ];
 
       serviceConfig = {
-        ExecStart = "${pkgs.spamassassin}/bin/spamd ${optionalString cfg.debug "-D"} --username=spamd --groupname=spamd --siteconfigpath=${spamdEnv} --virtual-config-dir=/var/lib/spamassassin/user-%u --allow-tell --pidfile=/run/spamd.pid";
+        ExecStart = "${pkgs.spamassassin}/bin/spamd ${optionalString cfg.debug "-D"} --username=spamd --groupname=spamd --virtual-config-dir=/var/lib/spamassassin/user-%u --allow-tell --pidfile=/run/spamd.pid";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
       };
 
@@ -183,7 +172,7 @@ in
         mkdir -p /var/lib/spamassassin
         chown spamd:spamd /var/lib/spamassassin -R
         set +e
-        ${pkgs.su}/bin/su -s "${pkgs.bash}/bin/bash" -c "${pkgs.spamassassin}/bin/sa-update --gpghomedir=/var/lib/spamassassin/sa-update-keys/ --siteconfigpath=${spamdEnv}/" spamd
+        ${pkgs.su}/bin/su -s "${pkgs.bash}/bin/bash" -c "${pkgs.spamassassin}/bin/sa-update --gpghomedir=/var/lib/spamassassin/sa-update-keys/" spamd
         v=$?
         set -e
         if [ $v -gt 1 ]; then
diff --git a/nixos/modules/services/misc/freeswitch.nix b/nixos/modules/services/misc/freeswitch.nix
new file mode 100644
index 0000000000000..0de5ba428110a
--- /dev/null
+++ b/nixos/modules/services/misc/freeswitch.nix
@@ -0,0 +1,103 @@
+{ config, lib, pkgs, ...}:
+with lib;
+let
+  cfg = config.services.freeswitch;
+  pkg = cfg.package;
+  configDirectory = pkgs.runCommand "freeswitch-config-d" { } ''
+    mkdir -p $out
+    cp -rT ${cfg.configTemplate} $out
+    chmod -R +w $out
+    ${concatStringsSep "\n" (mapAttrsToList (fileName: filePath: ''
+      mkdir -p $out/$(dirname ${fileName})
+      cp ${filePath} $out/${fileName}
+    '') cfg.configDir)}
+  '';
+  configPath = if cfg.enableReload
+    then "/etc/freeswitch"
+    else configDirectory;
+in {
+  options = {
+    services.freeswitch = {
+      enable = mkEnableOption "FreeSWITCH";
+      enableReload = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Issue the <literal>reloadxml</literal> command to FreeSWITCH when configuration directory changes (instead of restart).
+          See <link xlink:href="https://freeswitch.org/confluence/display/FREESWITCH/Reloading">FreeSWITCH documentation</link> for more info.
+          The configuration directory is exposed at <filename>/etc/freeswitch</filename>.
+          See also <literal>systemd.services.*.restartIfChanged</literal>.
+        '';
+      };
+      configTemplate = mkOption {
+        type = types.path;
+        default = "${config.services.freeswitch.package}/share/freeswitch/conf/vanilla";
+        defaultText = literalExample "\${config.services.freeswitch.package}/share/freeswitch/conf/vanilla";
+        example = literalExample "\${config.services.freeswitch.package}/share/freeswitch/conf/minimal";
+        description = ''
+          Configuration template to use.
+          See available templates in <link xlink:href="https://github.com/signalwire/freeswitch/tree/master/conf">FreeSWITCH repository</link>.
+          You can also set your own configuration directory.
+        '';
+      };
+      configDir = mkOption {
+        type = with types; attrsOf path;
+        default = { };
+        example = literalExample ''
+          {
+            "freeswitch.xml" = ./freeswitch.xml;
+            "dialplan/default.xml" = pkgs.writeText "dialplan-default.xml" '''
+              [xml lines]
+            ''';
+          }
+        '';
+        description = ''
+          Override file in FreeSWITCH config template directory.
+          Each top-level attribute denotes a file path in the configuration directory, its value is the file path.
+          See <link xlink:href="https://freeswitch.org/confluence/display/FREESWITCH/Default+Configuration">FreeSWITCH documentation</link> for more info.
+          Also check available templates in <link xlink:href="https://github.com/signalwire/freeswitch/tree/master/conf">FreeSWITCH repository</link>.
+        '';
+      };
+      package = mkOption {
+        type = types.package;
+        default = pkgs.freeswitch;
+        defaultText = literalExample "pkgs.freeswitch";
+        example = literalExample "pkgs.freeswitch";
+        description = ''
+          FreeSWITCH package.
+        '';
+      };
+    };
+  };
+  config = mkIf cfg.enable {
+    environment.etc.freeswitch = mkIf cfg.enableReload {
+      source = configDirectory;
+    };
+    systemd.services.freeswitch-config-reload = mkIf cfg.enableReload {
+      before = [ "freeswitch.service" ];
+      wantedBy = [ "multi-user.target" ];
+      restartTriggers = [ configDirectory ];
+      serviceConfig = {
+        ExecStart = "${pkgs.systemd}/bin/systemctl try-reload-or-restart freeswitch.service";
+        RemainAfterExit = true;
+        Type = "oneshot";
+      };
+    };
+    systemd.services.freeswitch = {
+      description = "Free and open-source application server for real-time communication";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        DynamicUser = true;
+        StateDirectory = "freeswitch";
+        ExecStart = "${pkg}/bin/freeswitch -nf \\
+          -mod ${pkg}/lib/freeswitch/mod \\
+          -conf ${configPath} \\
+          -base /var/lib/freeswitch";
+        ExecReload = "${pkg}/bin/fs_cli -x reloadxml";
+        Restart = "always";
+        RestartSec = "5s";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/home-assistant.nix b/nixos/modules/services/misc/home-assistant.nix
index cc113ca2d0c1a..d63f38e93b8e1 100644
--- a/nixos/modules/services/misc/home-assistant.nix
+++ b/nixos/modules/services/misc/home-assistant.nix
@@ -251,6 +251,7 @@ in {
       home = cfg.configDir;
       createHome = true;
       group = "hass";
+      extraGroups = [ "dialout" ];
       uid = config.ids.uids.hass;
     };
 
diff --git a/nixos/modules/services/monitoring/nagios.nix b/nixos/modules/services/monitoring/nagios.nix
index 3ca79dddaf57a..9ac6869068f2b 100644
--- a/nixos/modules/services/monitoring/nagios.nix
+++ b/nixos/modules/services/monitoring/nagios.nix
@@ -154,7 +154,7 @@ in
       };
 
       virtualHost = mkOption {
-        type = types.submodule (import ../web-servers/apache-httpd/per-server-options.nix);
+        type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
         example = literalExample ''
           { hostName = "example.org";
             adminAddr = "webmaster@example.org";
diff --git a/nixos/modules/services/monitoring/prometheus/alertmanager.nix b/nixos/modules/services/monitoring/prometheus/alertmanager.nix
index 9af6b1d94f374..4534d150885eb 100644
--- a/nixos/modules/services/monitoring/prometheus/alertmanager.nix
+++ b/nixos/modules/services/monitoring/prometheus/alertmanager.nix
@@ -18,7 +18,7 @@ let
     in checkedConfig yml;
 
   cmdlineArgs = cfg.extraFlags ++ [
-    "--config.file ${alertmanagerYml}"
+    "--config.file /tmp/alert-manager-substituted.yaml"
     "--web.listen-address ${cfg.listenAddress}:${toString cfg.port}"
     "--log.level ${cfg.logLevel}"
     ] ++ (optional (cfg.webExternalUrl != null)
@@ -127,6 +127,18 @@ in {
           Extra commandline options when launching the Alertmanager.
         '';
       };
+
+      environmentFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/root/alertmanager.env";
+        description = ''
+          File to load as environment file. Environment variables
+          from this file will be interpolated into the config file
+          using envsubst with this syntax:
+          <literal>$ENVIRONMENT ''${VARIABLE}</literal>
+        '';
+      };
     };
   };
 
@@ -144,9 +156,14 @@ in {
       systemd.services.alertmanager = {
         wantedBy = [ "multi-user.target" ];
         after    = [ "network.target" ];
+        preStart = ''
+           ${lib.getBin pkgs.envsubst}/bin/envsubst -o "/tmp/alert-manager-substituted.yaml" \
+                                                    -i "${alertmanagerYml}"
+        '';
         serviceConfig = {
           Restart  = "always";
-          DynamicUser = true;
+          DynamicUser = true; # implies PrivateTmp
+          EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
           WorkingDirectory = "/tmp";
           ExecStart = "${cfg.package}/bin/alertmanager" +
             optionalString (length cmdlineArgs != 0) (" \\\n  " +
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/postfix.nix b/nixos/modules/services/monitoring/prometheus/exporters/postfix.nix
index f40819e826b0d..d50564717eaf7 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/postfix.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/postfix.nix
@@ -74,7 +74,7 @@ in
                                           then "--systemd.slice ${cfg.systemd.slice}"
                                           else "--systemd.unit ${cfg.systemd.unit}")
           ++ optional (cfg.systemd.enable && (cfg.systemd.journalPath != null))
-                       "--systemd.jounal_path ${cfg.systemd.journalPath}"
+                       "--systemd.journal_path ${cfg.systemd.journalPath}"
           ++ optional (!cfg.systemd.enable) "--postfix.logfile_path ${cfg.logfilePath}")}
       '';
     };
diff --git a/nixos/modules/services/monitoring/prometheus/xmpp-alerts.nix b/nixos/modules/services/monitoring/prometheus/xmpp-alerts.nix
new file mode 100644
index 0000000000000..44b15cb2034c2
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/xmpp-alerts.nix
@@ -0,0 +1,47 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.xmpp-alerts;
+
+  configFile = pkgs.writeText "prometheus-xmpp-alerts.yml" (builtins.toJSON cfg.configuration);
+
+in
+
+{
+  options.services.prometheus.xmpp-alerts = {
+
+    enable = mkEnableOption "XMPP Web hook service for Alertmanager";
+
+    configuration = mkOption {
+      type = types.attrs;
+      description = "Configuration as attribute set which will be converted to YAML";
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.prometheus-xmpp-alerts = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      wants = [ "network-online.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.prometheus-xmpp-alerts}/bin/prometheus-xmpp-alerts --config ${configFile}";
+        Restart = "on-failure";
+        DynamicUser = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectHome = true;
+        ProtectSystem = "strict";
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        NoNewPrivileges = true;
+        SystemCallArchitectures = "native";
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        SystemCallFilter = [ "@system-service" ];
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/network-filesystems/kbfs.nix b/nixos/modules/services/network-filesystems/kbfs.nix
index 263b70d04a56d..a43ac656f6676 100644
--- a/nixos/modules/services/network-filesystems/kbfs.nix
+++ b/nixos/modules/services/network-filesystems/kbfs.nix
@@ -1,6 +1,7 @@
 { config, lib, pkgs, ... }:
 with lib;
 let
+  inherit (config.security) wrapperDir;
   cfg = config.services.kbfs;
 
 in {
@@ -17,6 +18,16 @@ in {
         description = "Whether to mount the Keybase filesystem.";
       };
 
+      enableRedirector = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the Keybase root redirector service, allowing
+          any user to access KBFS files via <literal>/keybase</literal>,
+          which will show different contents depending on the requester.
+        '';
+      };
+
       mountPoint = mkOption {
         type = types.str;
         default = "%h/keybase";
@@ -41,26 +52,67 @@ in {
 
   ###### implementation
 
-  config = mkIf cfg.enable {
-
-    systemd.user.services.kbfs = {
-      description = "Keybase File System";
-      requires = [ "keybase.service" ];
-      after = [ "keybase.service" ];
-      path = [ "/run/wrappers" ];
-      unitConfig.ConditionUser = "!@system";
-      serviceConfig = {
-        ExecStartPre = "${pkgs.coreutils}/bin/mkdir -p ${cfg.mountPoint}";
-        ExecStart = "${pkgs.kbfs}/bin/kbfsfuse ${toString cfg.extraFlags} ${cfg.mountPoint}";
-        ExecStopPost = "/run/wrappers/bin/fusermount -u ${cfg.mountPoint}";
-        Restart = "on-failure";
-        PrivateTmp = true;
+  config = mkIf cfg.enable (mkMerge [
+    {
+      # Upstream: https://github.com/keybase/client/blob/master/packaging/linux/systemd/kbfs.service
+      systemd.user.services.kbfs = {
+        description = "Keybase File System";
+
+        # Note that the "Requires" directive will cause a unit to be restarted whenever its dependency is restarted.
+        # Do not issue a hard dependency on keybase, because kbfs can reconnect to a restarted service.
+        # Do not issue a hard dependency on keybase-redirector, because it's ok if it fails (e.g., if it is disabled).
+        wants = [ "keybase.service" ] ++ optional cfg.enableRedirector "keybase-redirector.service";
+        path = [ "/run/wrappers" ];
+        unitConfig.ConditionUser = "!@system";
+
+        serviceConfig = {
+          Type = "notify";
+          # Keybase notifies from a forked process
+          EnvironmentFile = [
+            "-%E/keybase/keybase.autogen.env"
+            "-%E/keybase/keybase.env"
+          ];
+          ExecStartPre = [
+            "${pkgs.coreutils}/bin/mkdir -p \"${cfg.mountPoint}\""
+            "-${wrapperDir}/fusermount -uz \"${cfg.mountPoint}\""
+          ];
+          ExecStart = "${pkgs.kbfs}/bin/kbfsfuse ${toString cfg.extraFlags} \"${cfg.mountPoint}\"";
+          ExecStop = "${wrapperDir}/fusermount -uz \"${cfg.mountPoint}\"";
+          Restart = "on-failure";
+          PrivateTmp = true;
+        };
+        wantedBy = [ "default.target" ];
       };
-      wantedBy = [ "default.target" ];
-    };
 
-    services.keybase.enable = true;
+      services.keybase.enable = true;
 
-    environment.systemPackages = [ pkgs.kbfs ];
-  };
+      environment.systemPackages = [ pkgs.kbfs ];
+    }
+
+    (mkIf cfg.enableRedirector {
+      security.wrappers."keybase-redirector".source = "${pkgs.kbfs}/bin/redirector";
+
+      systemd.tmpfiles.rules = [ "d /keybase 0755 root root 0" ];
+
+      # Upstream: https://github.com/keybase/client/blob/master/packaging/linux/systemd/keybase-redirector.service
+      systemd.user.services.keybase-redirector = {
+        description = "Keybase Root Redirector for KBFS";
+        wants = [ "keybase.service" ];
+        unitConfig.ConditionUser = "!@system";
+
+        serviceConfig = {
+          EnvironmentFile = [
+            "-%E/keybase/keybase.autogen.env"
+            "-%E/keybase/keybase.env"
+          ];
+          # Note: The /keybase mount point is not currently configurable upstream.
+          ExecStart = "${wrapperDir}/keybase-redirector /keybase";
+          Restart = "on-failure";
+          PrivateTmp = true;
+        };
+
+        wantedBy = [ "default.target" ];
+      };
+    })
+  ]);
 }
diff --git a/nixos/modules/services/networking/bitlbee.nix b/nixos/modules/services/networking/bitlbee.nix
index 54fe70f7ccc02..01a16698384ab 100644
--- a/nixos/modules/services/networking/bitlbee.nix
+++ b/nixos/modules/services/networking/bitlbee.nix
@@ -168,8 +168,7 @@ in
         createHome = true;
       };
 
-      users.groups = singleton {
-        name = "bitlbee";
+      users.groups.bitlbee = {
         gid = config.ids.gids.bitlbee;
       };
 
diff --git a/nixos/modules/services/networking/dhcpcd.nix b/nixos/modules/services/networking/dhcpcd.nix
index 6fbc014db718b..f476b147a5761 100644
--- a/nixos/modules/services/networking/dhcpcd.nix
+++ b/nixos/modules/services/networking/dhcpcd.nix
@@ -59,6 +59,16 @@ let
       # Use the list of allowed interfaces if specified
       ${optionalString (allowInterfaces != null) "allowinterfaces ${toString allowInterfaces}"}
 
+      # Immediately fork to background if specified, otherwise wait for IP address to be assigned
+      ${{
+        background = "background";
+        any = "waitip";
+        ipv4 = "waitip 4";
+        ipv6 = "waitip 6";
+        both = "waitip 4\nwaitip 6";
+        if-carrier-up = "";
+      }.${cfg.wait}}
+
       ${cfg.extraConfig}
     '';
 
@@ -146,6 +156,21 @@ in
       '';
     };
 
+    networking.dhcpcd.wait = mkOption {
+      type = types.enum [ "background" "any" "ipv4" "ipv6" "both" "if-carrier-up" ];
+      default = "any";
+      description = ''
+        This option specifies when the dhcpcd service will fork to background.
+        If set to "background", dhcpcd will fork to background immediately.
+        If set to "ipv4" or "ipv6", dhcpcd will wait for the corresponding IP
+        address to be assigned. If set to "any", dhcpcd will wait for any type
+        (IPv4 or IPv6) to be assigned. If set to "both", dhcpcd will wait for
+        both an IPv4 and an IPv6 address before forking.
+        The option "if-carrier-up" is equivalent to "any" if either ethernet
+        is plugged nor WiFi is powered, and to "background" otherwise.
+      '';
+    };
+
   };
 
 
@@ -165,6 +190,8 @@ in
         before = [ "network-online.target" ];
         after = [ "systemd-udev-settle.service" ];
 
+        restartTriggers = [ exitHook ];
+
         # Stopping dhcpcd during a reconfiguration is undesirable
         # because it brings down the network interfaces configured by
         # dhcpcd.  So do a "systemctl restart" instead.
@@ -177,7 +204,7 @@ in
         serviceConfig =
           { Type = "forking";
             PIDFile = "/run/dhcpcd.pid";
-            ExecStart = "@${dhcpcd}/sbin/dhcpcd dhcpcd -w --quiet ${optionalString cfg.persistent "--persistent"} --config ${dhcpcdConf}";
+            ExecStart = "@${dhcpcd}/sbin/dhcpcd dhcpcd --quiet ${optionalString cfg.persistent "--persistent"} --config ${dhcpcdConf}";
             ExecReload = "${dhcpcd}/sbin/dhcpcd --rebind";
             Restart = "always";
           };
diff --git a/nixos/modules/services/networking/dnscrypt-proxy.nix b/nixos/modules/services/networking/dnscrypt-proxy.nix
deleted file mode 100644
index 8edcf925dbfa1..0000000000000
--- a/nixos/modules/services/networking/dnscrypt-proxy.nix
+++ /dev/null
@@ -1,328 +0,0 @@
-{ config, lib, pkgs, ... }:
-with lib;
-
-let
-  cfg = config.services.dnscrypt-proxy;
-
-  stateDirectory = "/var/lib/dnscrypt-proxy";
-
-  # The minisign public key used to sign the upstream resolver list.
-  # This is somewhat more flexible than preloading the key as an
-  # embedded string.
-  upstreamResolverListPubKey = pkgs.fetchurl {
-    url = https://raw.githubusercontent.com/dyne/dnscrypt-proxy/master/minisign.pub;
-    sha256 = "18lnp8qr6ghfc2sd46nn1rhcpr324fqlvgsp4zaigw396cd7vnnh";
-  };
-
-  # Internal flag indicating whether the upstream resolver list is used.
-  useUpstreamResolverList = cfg.customResolver == null;
-
-  # The final local address.
-  localAddress = "${cfg.localAddress}:${toString cfg.localPort}";
-
-  # The final resolvers list path.
-  resolverList = "${stateDirectory}/dnscrypt-resolvers.csv";
-
-  # Build daemon command line
-
-  resolverArgs =
-    if (cfg.customResolver == null)
-      then
-        [ "-L ${resolverList}"
-          "-R ${cfg.resolverName}"
-        ]
-      else with cfg.customResolver;
-        [ "-N ${name}"
-          "-k ${key}"
-          "-r ${address}:${toString port}"
-        ];
-
-  daemonArgs =
-       [ "-a ${localAddress}" ]
-    ++ resolverArgs
-    ++ cfg.extraArgs;
-in
-
-{
-  meta = {
-    maintainers = with maintainers; [ joachifm ];
-    doc = ./dnscrypt-proxy.xml;
-  };
-
-  options = {
-    # Before adding another option, consider whether it could
-    # equally well be passed via extraArgs.
-
-    services.dnscrypt-proxy = {
-      enable = mkOption {
-        default = false;
-        type = types.bool;
-        description = "Whether to enable the DNSCrypt client proxy";
-      };
-
-      localAddress = mkOption {
-        default = "127.0.0.1";
-        type = types.str;
-        description = ''
-          Listen for DNS queries to relay on this address. The only reason to
-          change this from its default value is to proxy queries on behalf
-          of other machines (typically on the local network).
-        '';
-      };
-
-      localPort = mkOption {
-        default = 53;
-        type = types.int;
-        description = ''
-          Listen for DNS queries to relay on this port. The default value
-          assumes that the DNSCrypt proxy should relay DNS queries directly.
-          When running as a forwarder for another DNS client, set this option
-          to a different value; otherwise leave the default.
-        '';
-      };
-
-      resolverName = mkOption {
-        default = "random";
-        example = "dnscrypt.eu-nl";
-        type = types.nullOr types.str;
-        description = ''
-          The name of the DNSCrypt resolver to use, taken from
-          <filename>${resolverList}</filename>.  The default is to
-          pick a random non-logging resolver that supports DNSSEC.
-        '';
-      };
-
-      customResolver = mkOption {
-        default = null;
-        description = ''
-          Use an unlisted resolver (e.g., a private DNSCrypt provider). For
-          advanced users only. If specified, this option takes precedence.
-        '';
-        type = types.nullOr (types.submodule ({ ... }: { options = {
-          address = mkOption {
-            type = types.str;
-            description = "IP address";
-            example = "208.67.220.220";
-          };
-
-          port = mkOption {
-            type = types.int;
-            description = "Port";
-            default = 443;
-          };
-
-          name = mkOption {
-            type = types.str;
-            description = "Fully qualified domain name";
-            example = "2.dnscrypt-cert.example.com";
-          };
-
-          key = mkOption {
-            type = types.str;
-            description = "Public key";
-            example = "B735:1140:206F:225D:3E2B:D822:D7FD:691E:A1C3:3CC8:D666:8D0C:BE04:BFAB:CA43:FB79";
-          };
-        }; }));
-      };
-
-      extraArgs = mkOption {
-        default = [];
-        type = types.listOf types.str;
-        description = ''
-          Additional command-line arguments passed verbatim to the daemon.
-          See <citerefentry><refentrytitle>dnscrypt-proxy</refentrytitle>
-          <manvolnum>8</manvolnum></citerefentry> for details.
-        '';
-        example = [ "-X libdcplugin_example_cache.so,--min-ttl=60" ];
-      };
-    };
-  };
-
-  config = mkIf cfg.enable (mkMerge [{
-    assertions = [
-      { assertion = (cfg.customResolver != null) || (cfg.resolverName != null);
-        message   = "please configure upstream DNSCrypt resolver";
-      }
-    ];
-
-    # make man 8 dnscrypt-proxy work
-    environment.systemPackages = [ pkgs.dnscrypt-proxy ];
-
-    users.users.dnscrypt-proxy = {
-      description = "dnscrypt-proxy daemon user";
-      isSystemUser = true;
-      group = "dnscrypt-proxy";
-    };
-    users.groups.dnscrypt-proxy = {};
-
-    systemd.sockets.dnscrypt-proxy = {
-      description = "dnscrypt-proxy listening socket";
-      documentation = [ "man:dnscrypt-proxy(8)" ];
-
-      wantedBy = [ "sockets.target" ];
-
-      socketConfig = {
-        ListenStream = localAddress;
-        ListenDatagram = localAddress;
-      };
-    };
-
-    systemd.services.dnscrypt-proxy = {
-      description = "dnscrypt-proxy daemon";
-      documentation = [ "man:dnscrypt-proxy(8)" ];
-
-      before = [ "nss-lookup.target" ];
-      after = [ "network.target" ];
-      requires = [ "dnscrypt-proxy.socket "];
-
-      serviceConfig = {
-        NonBlocking = "true";
-        ExecStart = "${pkgs.dnscrypt-proxy}/bin/dnscrypt-proxy ${toString daemonArgs}";
-        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
-
-        User = "dnscrypt-proxy";
-
-        PrivateTmp = true;
-        PrivateDevices = true;
-        ProtectHome = true;
-      };
-    };
-    }
-
-    (mkIf config.security.apparmor.enable {
-    systemd.services.dnscrypt-proxy.after = [ "apparmor.service" ];
-
-    security.apparmor.profiles = singleton (pkgs.writeText "apparmor-dnscrypt-proxy" ''
-      ${pkgs.dnscrypt-proxy}/bin/dnscrypt-proxy {
-        /dev/null rw,
-        /dev/random r,
-        /dev/urandom r,
-
-        /etc/passwd r,
-        /etc/group r,
-        ${config.environment.etc."nsswitch.conf".source} r,
-
-        ${getLib pkgs.glibc}/lib/*.so mr,
-        ${pkgs.tzdata}/share/zoneinfo/** r,
-
-        network inet stream,
-        network inet6 stream,
-        network inet dgram,
-        network inet6 dgram,
-
-        ${getLib pkgs.dnscrypt-proxy}/lib/dnscrypt-proxy/libdcplugin*.so mr,
-
-        ${getLib pkgs.gcc.cc}/lib/libssp.so.* mr,
-        ${getLib pkgs.libsodium}/lib/libsodium.so.* mr,
-        ${getLib pkgs.systemd}/lib/libsystemd.so.* mr,
-        ${getLib pkgs.utillinuxMinimal.out}/lib/libmount.so.* mr,
-        ${getLib pkgs.utillinuxMinimal.out}/lib/libblkid.so.* mr,
-        ${getLib pkgs.utillinuxMinimal.out}/lib/libuuid.so.* mr,
-        ${getLib pkgs.xz}/lib/liblzma.so.* mr,
-        ${getLib pkgs.libgcrypt}/lib/libgcrypt.so.* mr,
-        ${getLib pkgs.libgpgerror}/lib/libgpg-error.so.* mr,
-        ${getLib pkgs.libcap}/lib/libcap.so.* mr,
-        ${getLib pkgs.lz4}/lib/liblz4.so.* mr,
-        ${getLib pkgs.attr}/lib/libattr.so.* mr, # */
-
-        ${resolverList} r,
-
-        /run/systemd/notify rw,
-      }
-    '');
-    })
-
-    (mkIf useUpstreamResolverList {
-    systemd.services.init-dnscrypt-proxy-statedir = {
-      description = "Initialize dnscrypt-proxy state directory";
-
-      wantedBy = [ "dnscrypt-proxy.service" ];
-      before = [ "dnscrypt-proxy.service" ];
-
-      script = ''
-        mkdir -pv ${stateDirectory}
-        chown -c dnscrypt-proxy:dnscrypt-proxy ${stateDirectory}
-        cp -uv \
-          ${pkgs.dnscrypt-proxy}/share/dnscrypt-proxy/dnscrypt-resolvers.csv \
-          ${stateDirectory}
-      '';
-
-      serviceConfig = {
-        Type = "oneshot";
-        RemainAfterExit = true;
-      };
-    };
-
-    systemd.services.update-dnscrypt-resolvers = {
-      description = "Update list of DNSCrypt resolvers";
-
-      requires = [ "init-dnscrypt-proxy-statedir.service" ];
-      after = [ "init-dnscrypt-proxy-statedir.service" ];
-
-      path = with pkgs; [ curl diffutils dnscrypt-proxy minisign ];
-      script = ''
-        cd ${stateDirectory}
-        domain=raw.githubusercontent.com
-        get="curl -fSs --resolve $domain:443:$(hostip -r 8.8.8.8 $domain | head -1)"
-        $get -o dnscrypt-resolvers.csv.tmp \
-          https://$domain/dyne/dnscrypt-proxy/master/dnscrypt-resolvers.csv
-        $get -o dnscrypt-resolvers.csv.minisig.tmp \
-          https://$domain/dyne/dnscrypt-proxy/master/dnscrypt-resolvers.csv.minisig
-        mv dnscrypt-resolvers.csv.minisig{.tmp,}
-        if ! minisign -q -V -p ${upstreamResolverListPubKey} \
-          -m dnscrypt-resolvers.csv.tmp -x dnscrypt-resolvers.csv.minisig ; then
-          echo "failed to verify resolver list!" >&2
-          exit 1
-        fi
-        [[ -f dnscrypt-resolvers.csv ]] && mv dnscrypt-resolvers.csv{,.old}
-        mv dnscrypt-resolvers.csv{.tmp,}
-        if cmp dnscrypt-resolvers.csv{,.old} ; then
-          echo "no change"
-        else
-          echo "resolver list updated"
-        fi
-      '';
-
-      serviceConfig = {
-        PrivateTmp = true;
-        PrivateDevices = true;
-        ProtectHome = true;
-        ProtectSystem = "strict";
-        ReadWritePaths = "${dirOf stateDirectory} ${stateDirectory}";
-        SystemCallFilter = "~@mount";
-      };
-    };
-
-    systemd.timers.update-dnscrypt-resolvers = {
-      wantedBy = [ "timers.target" ];
-      timerConfig = {
-        OnBootSec = "5min";
-        OnUnitActiveSec = "6h";
-      };
-    };
-    })
-    ]);
-
-  imports = [
-    (mkRenamedOptionModule [ "services" "dnscrypt-proxy" "port" ] [ "services" "dnscrypt-proxy" "localPort" ])
-
-    (mkChangedOptionModule
-      [ "services" "dnscrypt-proxy" "tcpOnly" ]
-      [ "services" "dnscrypt-proxy" "extraArgs" ]
-      (config:
-        let val = getAttrFromPath [ "services" "dnscrypt-proxy" "tcpOnly" ] config; in
-        optional val "-T"))
-
-    (mkChangedOptionModule
-      [ "services" "dnscrypt-proxy" "ephemeralKeys" ]
-      [ "services" "dnscrypt-proxy" "extraArgs" ]
-      (config:
-        let val = getAttrFromPath [ "services" "dnscrypt-proxy" "ephemeralKeys" ] config; in
-        optional val "-E"))
-
-    (mkRemovedOptionModule [ "services" "dnscrypt-proxy" "resolverList" ] ''
-      The current resolver listing from upstream is always used
-      unless a custom resolver is specified.
-    '')
-  ];
-}
diff --git a/nixos/modules/services/networking/dnscrypt-proxy.xml b/nixos/modules/services/networking/dnscrypt-proxy.xml
deleted file mode 100644
index afc7880392a1a..0000000000000
--- a/nixos/modules/services/networking/dnscrypt-proxy.xml
+++ /dev/null
@@ -1,66 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="sec-dnscrypt-proxy">
- <title>DNSCrypt client proxy</title>
- <para>
-  The DNSCrypt client proxy relays DNS queries to a DNSCrypt enabled upstream
-  resolver. The traffic between the client and the upstream resolver is
-  encrypted and authenticated, mitigating the risk of MITM attacks, DNS
-  poisoning attacks, and third-party snooping (assuming the upstream is
-  trustworthy).
- </para>
- <sect1 xml:id="sec-dnscrypt-proxy-configuration">
-  <title>Basic configuration</title>
-
-  <para>
-   To enable the client proxy, set
-<programlisting>
-<xref linkend="opt-services.dnscrypt-proxy.enable"/> = true;
-</programlisting>
-  </para>
-
-  <para>
-   Enabling the client proxy does not alter the system nameserver; to relay
-   local queries, prepend <literal>127.0.0.1</literal> to
-   <option>networking.nameservers</option>.
-  </para>
- </sect1>
- <sect1 xml:id="sec-dnscrypt-proxy-forwarder">
-  <title>As a forwarder for another DNS client</title>
-
-  <para>
-   To run the DNSCrypt proxy client as a forwarder for another DNS client,
-   change the default proxy listening port to a non-standard value and point
-   the other client to it:
-<programlisting>
-<xref linkend="opt-services.dnscrypt-proxy.localPort"/> = 43;
-</programlisting>
-  </para>
-
-  <sect2 xml:id="sec-dnscrypt-proxy-forwarder-dsnmasq">
-   <title>dnsmasq</title>
-   <para>
-<programlisting>
-{
-  <xref linkend="opt-services.dnsmasq.enable"/> = true;
-  <xref linkend="opt-services.dnsmasq.servers"/> = [ "127.0.0.1#43" ];
-}
-</programlisting>
-   </para>
-  </sect2>
-
-  <sect2 xml:id="sec-dnscrypt-proxy-forwarder-unbound">
-   <title>unbound</title>
-   <para>
-<programlisting>
-{
-  <xref linkend="opt-services.unbound.enable"/> = true;
-  <xref linkend="opt-services.unbound.forwardAddresses"/> = [ "127.0.0.1@43" ];
-}
-</programlisting>
-   </para>
-  </sect2>
- </sect1>
-</chapter>
diff --git a/nixos/modules/services/networking/dnscrypt-proxy2.nix b/nixos/modules/services/networking/dnscrypt-proxy2.nix
new file mode 100644
index 0000000000000..e48eb729103be
--- /dev/null
+++ b/nixos/modules/services/networking/dnscrypt-proxy2.nix
@@ -0,0 +1,61 @@
+{ config, lib, pkgs, ... }: with lib;
+
+let
+  cfg = config.services.dnscrypt-proxy2;
+in
+
+{
+  options.services.dnscrypt-proxy2 = {
+    enable = mkEnableOption "dnscrypt-proxy2";
+
+    settings = mkOption {
+      description = ''
+        Attrset that is converted and passed as TOML config file.
+        For available params, see: <link xlink:href="https://github.com/DNSCrypt/dnscrypt-proxy/blob/master/dnscrypt-proxy/example-dnscrypt-proxy.toml"/>
+      '';
+      example = literalExample ''
+        {
+          sources.public-resolvers = {
+            urls = [ "https://download.dnscrypt.info/resolvers-list/v2/public-resolvers.md" ];
+            cache_file = "public-resolvers.md";
+            minisign_key = "RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3";
+            refresh_delay = 72;
+          };
+        }
+      '';
+      type = types.attrs;
+      default = {};
+    };
+
+    configFile = mkOption {
+      description = ''
+        Path to TOML config file. See: <link xlink:href="https://github.com/DNSCrypt/dnscrypt-proxy/blob/master/dnscrypt-proxy/example-dnscrypt-proxy.toml"/>
+        If this option is set, it will override any configuration done in options.services.dnscrypt-proxy2.settings.
+      '';
+      example = "/etc/dnscrypt-proxy/dnscrypt-proxy.toml";
+      type = types.path;
+      default = pkgs.runCommand "dnscrypt-proxy.toml" {
+        json = builtins.toJSON cfg.settings;
+        passAsFile = [ "json" ];
+      } ''
+        ${pkgs.remarshal}/bin/json2toml < $jsonPath > $out
+      '';
+      defaultText = literalExample "TOML file generated from services.dnscrypt-proxy2.settings";
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    networking.nameservers = lib.mkDefault [ "127.0.0.1" ];
+
+    systemd.services.dnscrypt-proxy2 = {
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        AmbientCapabilities = "CAP_NET_BIND_SERVICE";
+        DynamicUser = true;
+        ExecStart = "${pkgs.dnscrypt-proxy2}/bin/dnscrypt-proxy -config ${cfg.configFile}";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/keybase.nix b/nixos/modules/services/networking/keybase.nix
index 85f52be8a6ac9..495102cb7eeee 100644
--- a/nixos/modules/services/networking/keybase.nix
+++ b/nixos/modules/services/networking/keybase.nix
@@ -24,13 +24,18 @@ in {
 
   config = mkIf cfg.enable {
 
+    # Upstream: https://github.com/keybase/client/blob/master/packaging/linux/systemd/keybase.service
     systemd.user.services.keybase = {
       description = "Keybase service";
       unitConfig.ConditionUser = "!@system";
+      environment.KEYBASE_SERVICE_TYPE = "systemd";
       serviceConfig = {
-        ExecStart = ''
-          ${pkgs.keybase}/bin/keybase service --auto-forked
-        '';
+        Type = "notify";
+        EnvironmentFile = [
+          "-%E/keybase/keybase.autogen.env"
+          "-%E/keybase/keybase.env"
+        ];
+        ExecStart = "${pkgs.keybase}/bin/keybase service";
         Restart = "on-failure";
         PrivateTmp = true;
       };
diff --git a/nixos/modules/services/networking/knot.nix b/nixos/modules/services/networking/knot.nix
index 1cc1dd3f2f62b..47364ecb84640 100644
--- a/nixos/modules/services/networking/knot.nix
+++ b/nixos/modules/services/networking/knot.nix
@@ -56,6 +56,7 @@ in {
       package = mkOption {
         type = types.package;
         default = pkgs.knot-dns;
+        defaultText = "pkgs.knot-dns";
         description = ''
           Which Knot DNS package to use
         '';
@@ -92,4 +93,3 @@ in {
     environment.systemPackages = [ knot-cli-wrappers ];
   };
 }
-
diff --git a/nixos/modules/services/networking/kresd.nix b/nixos/modules/services/networking/kresd.nix
index 5eb50a13ca9ab..bb941e93e150f 100644
--- a/nixos/modules/services/networking/kresd.nix
+++ b/nixos/modules/services/networking/kresd.nix
@@ -5,12 +5,15 @@ with lib;
 let
 
   cfg = config.services.kresd;
-  package = pkgs.knot-resolver;
+  configFile = pkgs.writeText "kresd.conf" ''
+    ${optionalString (cfg.listenDoH != []) "modules.load('http')"}
+    ${cfg.extraConfig};
+  '';
 
-  configFile = pkgs.writeText "kresd.conf" cfg.extraConfig;
-in
-
-{
+  package = pkgs.knot-resolver.override {
+    extraFeatures = cfg.listenDoH != [];
+  };
+in {
   meta.maintainers = [ maintainers.vcunat /* upstream developer */ ];
 
   imports = [
@@ -67,6 +70,15 @@ in
         For detailed syntax see ListenStream in man systemd.socket.
       '';
     };
+    listenDoH = mkOption {
+      type = with types; listOf str;
+      default = [];
+      example = [ "198.51.100.1:443" "[2001:db8::1]:443" "443" ];
+      description = ''
+        Addresses and ports on which kresd should provide DNS over HTTPS (see RFC 7858).
+        For detailed syntax see ListenStream in man systemd.socket.
+      '';
+    };
     # TODO: perhaps options for more common stuff like cache size or forwarding
   };
 
@@ -104,6 +116,18 @@ in
       };
     };
 
+    systemd.sockets.kresd-doh = mkIf (cfg.listenDoH != []) rec {
+      wantedBy = [ "sockets.target" ];
+      before = wantedBy;
+      partOf = [ "kresd.socket" ];
+      listenStreams = cfg.listenDoH;
+      socketConfig = {
+        FileDescriptorName = "doh";
+        FreeBind = true;
+        Service = "kresd.service";
+      };
+    };
+
     systemd.sockets.kresd-control = rec {
       wantedBy = [ "sockets.target" ];
       before = wantedBy;
diff --git a/nixos/modules/services/networking/nsd.nix b/nixos/modules/services/networking/nsd.nix
index 344396638a6cf..429580e5c6c48 100644
--- a/nixos/modules/services/networking/nsd.nix
+++ b/nixos/modules/services/networking/nsd.nix
@@ -244,7 +244,7 @@ let
       };
 
       data = mkOption {
-        type = types.str;
+        type = types.lines;
         default = "";
         example = "";
         description = ''
@@ -484,7 +484,7 @@ in
     };
 
     extraConfig = mkOption {
-      type = types.str;
+      type = types.lines;
       default = "";
       description = ''
         Extra nsd config.
diff --git a/nixos/modules/services/networking/unifi.nix b/nixos/modules/services/networking/unifi.nix
index c922ba15960fd..4bdfa8143dce0 100644
--- a/nixos/modules/services/networking/unifi.nix
+++ b/nixos/modules/services/networking/unifi.nix
@@ -147,8 +147,10 @@ in
       }) mountPoints;
 
     systemd.tmpfiles.rules = [
-      "e '${stateDir}' 0700 unifi - - -"
+      "d '${stateDir}' 0700 unifi - - -"
       "d '${stateDir}/data' 0700 unifi - - -"
+      "d '${stateDir}/webapps' 0700 unifi - - -"
+      "L+ '${stateDir}/webapps/ROOT' - - - - ${cfg.unifiPackage}/webapps/ROOT"
     ];
 
     systemd.services.unifi = {
@@ -161,17 +163,6 @@ in
       # This a HACK to fix missing dependencies of dynamic libs extracted from jars
       environment.LD_LIBRARY_PATH = with pkgs.stdenv; "${cc.cc.lib}/lib";
 
-      preStart = ''
-        # Create the volatile webapps
-        rm -rf "${stateDir}/webapps"
-        mkdir -p "${stateDir}/webapps"
-        ln -s "${cfg.unifiPackage}/webapps/ROOT" "${stateDir}/webapps/ROOT"
-      '';
-
-      postStop = ''
-        rm -rf "${stateDir}/webapps"
-      '';
-
       serviceConfig = {
         Type = "simple";
         ExecStart = "${(removeSuffix "\n" cmd)} start";
diff --git a/nixos/modules/services/search/solr.nix b/nixos/modules/services/search/solr.nix
index b2176225493e4..a8615a20a1cf2 100644
--- a/nixos/modules/services/search/solr.nix
+++ b/nixos/modules/services/search/solr.nix
@@ -13,19 +13,11 @@ in
     services.solr = {
       enable = mkEnableOption "Solr";
 
-      # default to the 8.x series not forcing major version upgrade of those on the 7.x series
       package = mkOption {
         type = types.package;
-        default = if versionAtLeast config.system.stateVersion "19.09"
-          then pkgs.solr_8
-          else pkgs.solr_7
-        ;
+        default = pkgs.solr;
         defaultText = "pkgs.solr";
-        description = ''
-          Which Solr package to use. This defaults to version 7.x if
-          <literal>system.stateVersion &lt; 19.09</literal> and version 8.x
-          otherwise.
-        '';
+        description = "Which Solr package to use.";
       };
 
       port = mkOption {
diff --git a/nixos/modules/services/security/bitwarden_rs/default.nix b/nixos/modules/services/security/bitwarden_rs/default.nix
index d1817db075550..a63be0ee766e9 100644
--- a/nixos/modules/services/security/bitwarden_rs/default.nix
+++ b/nixos/modules/services/security/bitwarden_rs/default.nix
@@ -18,15 +18,33 @@ let
         else key + toUpper x) "" parts;
     in if builtins.match "[A-Z0-9_]+" name != null then name else partsToEnvVar parts;
 
-  configFile = pkgs.writeText "bitwarden_rs.env" (concatMapStrings (s: s + "\n") (
-    (concatLists (mapAttrsToList (name: value:
-      if value != null then [ "${nameToEnvVar name}=${if isBool value then boolToString value else toString value}" ] else []
-    ) cfg.config))));
+  # Due to the different naming schemes allowed for config keys,
+  # we can only check for values consistently after converting them to their corresponding environment variable name.
+  configEnv =
+    let
+      configEnv = listToAttrs (concatLists (mapAttrsToList (name: value:
+        if value != null then [ (nameValuePair (nameToEnvVar name) (if isBool value then boolToString value else toString value)) ] else []
+      ) cfg.config));
+    in { DATA_FOLDER = "/var/lib/bitwarden_rs"; } // optionalAttrs (!(configEnv ? WEB_VAULT_ENABLED) || configEnv.WEB_VAULT_ENABLED == "true") {
+      WEB_VAULT_FOLDER = "${pkgs.bitwarden_rs-vault}/share/bitwarden_rs/vault";
+    } // configEnv;
+
+  configFile = pkgs.writeText "bitwarden_rs.env" (concatStrings (mapAttrsToList (name: value: "${name}=${value}\n") configEnv));
+
+  bitwarden_rs = pkgs.bitwarden_rs.override { inherit (cfg) dbBackend; };
 
 in {
   options.services.bitwarden_rs = with types; {
     enable = mkEnableOption "bitwarden_rs";
 
+    dbBackend = mkOption {
+      type = enum [ "sqlite" "mysql" "postgresql" ];
+      default = "sqlite";
+      description = ''
+        Which database backend bitwarden_rs will be using.
+      '';
+    };
+
     backupDir = mkOption {
       type = nullOr str;
       default = null;
@@ -56,23 +74,20 @@ in {
         even though foo2 would have been converted to FOO_2.
         This allows working around any potential future conflicting naming conventions.
 
-        Based on the attributes passed to this config option a environment file will be generated
+        Based on the attributes passed to this config option an environment file will be generated
         that is passed to bitwarden_rs's systemd service.
 
         The available configuration options can be found in
-        <link xlink:href="https://github.com/dani-garcia/bitwarden_rs/blob/1.8.0/.env.template">the environment template file</link>.
+        <link xlink:href="https://github.com/dani-garcia/bitwarden_rs/blob/${bitwarden_rs.version}/.env.template">the environment template file</link>.
       '';
-      apply = config: optionalAttrs config.webVaultEnabled {
-        webVaultFolder = "${pkgs.bitwarden_rs-vault}/share/bitwarden_rs/vault";
-      } // config;
     };
   };
 
   config = mkIf cfg.enable {
-    services.bitwarden_rs.config = {
-      dataFolder = "/var/lib/bitwarden_rs";
-      webVaultEnabled = mkDefault true;
-    };
+    assertions = [ {
+      assertion = cfg.backupDir != null -> cfg.dbBackend == "sqlite";
+      message = "Backups for database backends other than sqlite will need customization";
+    } ];
 
     users.users.bitwarden_rs = {
       inherit group;
@@ -87,7 +102,7 @@ in {
         User = user;
         Group = group;
         EnvironmentFile = configFile;
-        ExecStart = "${pkgs.bitwarden_rs}/bin/bitwarden_rs";
+        ExecStart = "${bitwarden_rs}/bin/bitwarden_rs";
         LimitNOFILE = "1048576";
         LimitNPROC = "64";
         PrivateTmp = "true";
@@ -109,6 +124,7 @@ in {
       path = with pkgs; [ sqlite ];
       serviceConfig = {
         SyslogIdentifier = "backup-bitwarden_rs";
+        Type = "oneshot";
         User = mkDefault user;
         Group = mkDefault group;
         ExecStart = "${pkgs.bash}/bin/bash ${./backup.sh}";
diff --git a/nixos/modules/services/security/fail2ban.nix b/nixos/modules/services/security/fail2ban.nix
index 716ae7a2d2f4c..cb748c93d24ed 100644
--- a/nixos/modules/services/security/fail2ban.nix
+++ b/nixos/modules/services/security/fail2ban.nix
@@ -6,15 +6,32 @@ let
 
   cfg = config.services.fail2ban;
 
-  fail2banConf = pkgs.writeText "fail2ban.conf" cfg.daemonConfig;
+  fail2banConf = pkgs.writeText "fail2ban.local" cfg.daemonConfig;
 
-  jailConf = pkgs.writeText "jail.conf"
-    (concatStringsSep "\n" (attrValues (flip mapAttrs cfg.jails (name: def:
+  jailConf = pkgs.writeText "jail.local" ''
+    [INCLUDES]
+
+    before = paths-nixos.conf
+
+    ${concatStringsSep "\n" (attrValues (flip mapAttrs cfg.jails (name: def:
       optionalString (def != "")
         ''
           [${name}]
           ${def}
-        ''))));
+        '')))}
+  '';
+
+  pathsConf = pkgs.writeText "paths-nixos.conf" ''
+    # NixOS
+
+    [INCLUDES]
+
+    before = paths-common.conf
+
+    after  = paths-overrides.local
+
+    [DEFAULT]
+  '';
 
 in
 
@@ -31,21 +48,135 @@ in
         description = "Whether to enable the fail2ban service.";
       };
 
+      package = mkOption {
+        default = pkgs.fail2ban;
+        type = types.package;
+        example = "pkgs.fail2ban_0_11";
+        description = "The fail2ban package to use for running the fail2ban service.";
+      };
+
+      packageFirewall = mkOption {
+        default = pkgs.iptables;
+        type = types.package;
+        example = "pkgs.nftables";
+        description = "The firewall package used by fail2ban service.";
+      };
+
+      banaction = mkOption {
+        default = "iptables-multiport";
+        type = types.str;
+        example = "nftables-multiport";
+        description = ''
+          Default banning action (e.g. iptables, iptables-new, iptables-multiport,
+          shorewall, etc) It is used to define action_* variables. Can be overridden
+          globally or per section within jail.local file
+        '';
+      };
+
+      banaction-allports = mkOption {
+        default = "iptables-allport";
+        type = types.str;
+        example = "nftables-allport";
+        description = ''
+          Default banning action (e.g. iptables, iptables-new, iptables-multiport,
+          shorewall, etc) It is used to define action_* variables. Can be overridden
+          globally or per section within jail.local file
+        '';
+      };
+
+      bantime-increment.enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Allows to use database for searching of previously banned ip's to increase
+          a default ban time using special formula, default it is banTime * 1, 2, 4, 8, 16, 32...
+        '';
+      };
+
+      bantime-increment.rndtime = mkOption {
+        default = "4m";
+        type = types.str;
+        example = "8m";
+        description = ''
+          "bantime-increment.rndtime" is the max number of seconds using for mixing with random time
+          to prevent "clever" botnets calculate exact time IP can be unbanned again
+        '';
+      };
+
+      bantime-increment.maxtime = mkOption {
+        default = "10h";
+        type = types.str;
+        example = "48h";
+        description = ''
+          "bantime-increment.maxtime" is the max number of seconds using the ban time can reach (don't grows further)
+        '';
+      };
+
+      bantime-increment.factor = mkOption {
+        default = "1";
+        type = types.str;
+        example = "4";
+        description = ''
+          "bantime-increment.factor" is a coefficient to calculate exponent growing of the formula or common multiplier,
+          default value of factor is 1 and with default value of formula, the ban time grows by 1, 2, 4, 8, 16 ...
+        '';
+      };
+
+      bantime-increment.formula = mkOption {
+        default = "ban.Time * (1<<(ban.Count if ban.Count<20 else 20)) * banFactor";
+        type = types.str;
+        example = "ban.Time * math.exp(float(ban.Count+1)*banFactor)/math.exp(1*banFactor)";
+        description = ''
+          "bantime-increment.formula" used by default to calculate next value of ban time, default value bellow,
+          the same ban time growing will be reached by multipliers 1, 2, 4, 8, 16, 32...
+        '';
+      };
+
+      bantime-increment.multipliers = mkOption {
+        default = "1 2 4 8 16 32 64";
+        type = types.str;
+        example = "2 4 16 128";
+        description = ''
+          "bantime-increment.multipliers" used to calculate next value of ban time instead of formula, coresponding
+          previously ban count and given "bantime.factor" (for multipliers default is 1);
+          following example grows ban time by 1, 2, 4, 8, 16 ... and if last ban count greater as multipliers count,
+          always used last multiplier (64 in example), for factor '1' and original ban time 600 - 10.6 hours
+        '';
+      };
+
+      bantime-increment.overalljails = mkOption {
+        default = false;
+        type = types.bool;
+        example = true;
+        description = ''
+          "bantime-increment.overalljails"  (if true) specifies the search of IP in the database will be executed
+          cross over all jails, if false (dafault), only current jail of the ban IP will be searched
+        '';
+      };
+
+      ignoreIP = mkOption {
+        default = [ ];
+        type = types.listOf types.str;
+        example = [ "192.168.0.0/16" "2001:DB8::42" ];
+        description = ''
+          "ignoreIP" can be a list of IP addresses, CIDR masks or DNS hosts. Fail2ban will not ban a host which
+          matches an address in this list. Several addresses can be defined using space (and/or comma) separator.
+        '';
+      };
+
       daemonConfig = mkOption {
-        default =
-          ''
-            [Definition]
-            loglevel  = INFO
-            logtarget = SYSLOG
-            socket    = /run/fail2ban/fail2ban.sock
-            pidfile   = /run/fail2ban/fail2ban.pid
-          '';
+        default = ''
+          [Definition]
+          logtarget = SYSLOG
+          socket    = /run/fail2ban/fail2ban.sock
+          pidfile   = /run/fail2ban/fail2ban.pid
+          dbfile    = /var/lib/fail2ban/fail2ban.sqlite3
+        '';
         type = types.lines;
-        description =
-          ''
-            The contents of Fail2ban's main configuration file.  It's
-            generally not necessary to change it.
-          '';
+        description = ''
+          The contents of Fail2ban's main configuration file.  It's
+          generally not necessary to change it.
+       '';
       };
 
       jails = mkOption {
@@ -65,88 +196,107 @@ in
           }
         '';
         type = types.attrsOf types.lines;
-        description =
-          ''
-            The configuration of each Fail2ban “jail”.  A jail
-            consists of an action (such as blocking a port using
-            <command>iptables</command>) that is triggered when a
-            filter applied to a log file triggers more than a certain
-            number of times in a certain time period.  Actions are
-            defined in <filename>/etc/fail2ban/action.d</filename>,
-            while filters are defined in
-            <filename>/etc/fail2ban/filter.d</filename>.
-          '';
+        description = ''
+          The configuration of each Fail2ban “jail”.  A jail
+          consists of an action (such as blocking a port using
+          <command>iptables</command>) that is triggered when a
+          filter applied to a log file triggers more than a certain
+          number of times in a certain time period.  Actions are
+          defined in <filename>/etc/fail2ban/action.d</filename>,
+          while filters are defined in
+          <filename>/etc/fail2ban/filter.d</filename>.
+        '';
       };
 
     };
 
   };
 
-
   ###### implementation
 
   config = mkIf cfg.enable {
 
-    environment.systemPackages = [ pkgs.fail2ban ];
+    environment.systemPackages = [ cfg.package ];
 
-    environment.etc."fail2ban/fail2ban.conf".source = fail2banConf;
-    environment.etc."fail2ban/jail.conf".source = jailConf;
-    environment.etc."fail2ban/action.d".source = "${pkgs.fail2ban}/etc/fail2ban/action.d/*.conf";
-    environment.etc."fail2ban/filter.d".source = "${pkgs.fail2ban}/etc/fail2ban/filter.d/*.conf";
-
-    systemd.services.fail2ban =
-      { description = "Fail2ban Intrusion Prevention System";
+    environment.etc = {
+      "fail2ban/fail2ban.local".source = fail2banConf;
+      "fail2ban/jail.local".source = jailConf;
+      "fail2ban/fail2ban.conf".source = "${cfg.package}/etc/fail2ban/fail2ban.conf";
+      "fail2ban/jail.conf".source = "${cfg.package}/etc/fail2ban/jail.conf";
+      "fail2ban/paths-common.conf".source = "${cfg.package}/etc/fail2ban/paths-common.conf";
+      "fail2ban/paths-nixos.conf".source = pathsConf;
+      "fail2ban/action.d".source = "${cfg.package}/etc/fail2ban/action.d/*.conf";
+      "fail2ban/filter.d".source = "${cfg.package}/etc/fail2ban/filter.d/*.conf";
+    };
 
-        wantedBy = [ "multi-user.target" ];
-        after = [ "network.target" ];
-        partOf = optional config.networking.firewall.enable "firewall.service";
+    systemd.services.fail2ban = {
+      description = "Fail2ban Intrusion Prevention System";
 
-        restartTriggers = [ fail2banConf jailConf ];
-        path = [ pkgs.fail2ban pkgs.iptables pkgs.iproute ];
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      partOf = optional config.networking.firewall.enable "firewall.service";
 
-        preStart =
-          ''
-            mkdir -p /var/lib/fail2ban
-          '';
+      restartTriggers = [ fail2banConf jailConf pathsConf ];
+      reloadIfChanged = true;
 
-        unitConfig.Documentation = "man:fail2ban(1)";
+      path = [ cfg.package cfg.packageFirewall pkgs.iproute ];
 
-        serviceConfig =
-          { Type = "forking";
-            ExecStart = "${pkgs.fail2ban}/bin/fail2ban-client -x start";
-            ExecStop = "${pkgs.fail2ban}/bin/fail2ban-client stop";
-            ExecReload = "${pkgs.fail2ban}/bin/fail2ban-client reload";
-            PIDFile = "/run/fail2ban/fail2ban.pid";
-            Restart = "always";
+      unitConfig.Documentation = "man:fail2ban(1)";
 
-            ReadOnlyDirectories = "/";
-            ReadWriteDirectories = "/run/fail2ban /var/tmp /var/lib";
-            PrivateTmp = "true";
-            RuntimeDirectory = "fail2ban";
-            CapabilityBoundingSet = "CAP_DAC_READ_SEARCH CAP_NET_ADMIN CAP_NET_RAW";
-          };
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/fail2ban-server -xf start";
+        ExecStop = "${cfg.package}/bin/fail2ban-server stop";
+        ExecReload = "${cfg.package}/bin/fail2ban-server reload";
+        Type = "simple";
+        Restart = "on-failure";
+        PIDFile = "/run/fail2ban/fail2ban.pid";
+        # Capabilities
+        CapabilityBoundingSet = [ "CAP_AUDIT_READ" "CAP_DAC_READ_SEARCH" "CAP_NET_ADMIN" "CAP_NET_RAW" ];
+        # Security
+        NoNewPrivileges = true;
+        # Directory
+        RuntimeDirectory = "fail2ban";
+        RuntimeDirectoryMode = "0750";
+        StateDirectory = "fail2ban";
+        StateDirectoryMode = "0750";
+        LogsDirectory = "fail2ban";
+        LogsDirectoryMode = "0750";
+        # Sandboxing
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectHostname = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
       };
+    };
 
     # Add some reasonable default jails.  The special "DEFAULT" jail
     # sets default values for all other jails.
-    services.fail2ban.jails.DEFAULT =
-      ''
-        ignoreip = 127.0.0.1/8
-        bantime  = 600
-        findtime = 600
-        maxretry = 3
-        backend  = systemd
-        enabled  = true
-       '';
-
+    services.fail2ban.jails.DEFAULT = ''
+      ${optionalString cfg.bantime-increment.enable ''
+        # Bantime incremental
+        bantime.increment    = ${if cfg.bantime-increment.enable then "true" else "false"}
+        bantime.maxtime      = ${cfg.bantime-increment.maxtime}
+        bantime.factor       = ${cfg.bantime-increment.factor}
+        bantime.formula      = ${cfg.bantime-increment.formula}
+        bantime.multipliers  = ${cfg.bantime-increment.multipliers}
+        bantime.overalljails = ${if cfg.bantime-increment.overalljails then "true" else "false"}
+      ''}
+      # Miscellaneous options
+      ignoreip    = 127.0.0.1/8 ${optionalString config.networking.enableIPv6 "::1"} ${concatStringsSep " " cfg.ignoreIP}
+      maxretry    = 3
+      backend     = systemd
+      # Actions
+      banaction   = ${cfg.banaction}
+      banaction_allports = ${cfg.banaction-allports}
+    '';
     # Block SSH if there are too many failing connection attempts.
-    services.fail2ban.jails.ssh-iptables =
-      ''
-        filter   = sshd
-        action   = iptables-multiport[name=SSH, port="${concatMapStringsSep "," (p: toString p) config.services.openssh.ports}", protocol=tcp]
-        maxretry = 5
-      '';
-
+    services.fail2ban.jails.sshd = mkDefault ''
+      enabled = true
+      port    = ${concatMapStringsSep "," (p: toString p) config.services.openssh.ports}
+    '';
   };
-
 }
diff --git a/nixos/modules/services/security/sshguard.nix b/nixos/modules/services/security/sshguard.nix
index 4a174564dd2ca..e7a9cefdef30a 100644
--- a/nixos/modules/services/security/sshguard.nix
+++ b/nixos/modules/services/security/sshguard.nix
@@ -92,8 +92,11 @@ in {
         "-o cat"
         "-n1"
       ] ++ (map (name: "-t ${escapeShellArg name}") cfg.services));
+      backend = if config.networking.nftables.enable
+        then "sshg-fw-nft-sets"
+        else "sshg-fw-ipset";
     in ''
-      BACKEND="${pkgs.sshguard}/libexec/sshg-fw-ipset"
+      BACKEND="${pkgs.sshguard}/libexec/${backend}"
       LOGREADER="LANG=C ${pkgs.systemd}/bin/journalctl ${args}"
     '';
 
@@ -104,7 +107,9 @@ in {
       after = [ "network.target" ];
       partOf = optional config.networking.firewall.enable "firewall.service";
 
-      path = with pkgs; [ iptables ipset iproute systemd ];
+      path = with pkgs; if config.networking.nftables.enable
+        then [ nftables iproute systemd ]
+        else [ iptables ipset iproute systemd ];
 
       # The sshguard ipsets must exist before we invoke
       # iptables. sshguard creates the ipsets after startup if
@@ -112,14 +117,14 @@ in {
       # the iptables rules because postStart races with the creation
       # of the ipsets. So instead, we create both the ipsets and
       # firewall rules before sshguard starts.
-      preStart = ''
+      preStart = optionalString config.networking.firewall.enable ''
         ${pkgs.ipset}/bin/ipset -quiet create -exist sshguard4 hash:net family inet
         ${pkgs.ipset}/bin/ipset -quiet create -exist sshguard6 hash:net family inet6
         ${pkgs.iptables}/bin/iptables  -I INPUT -m set --match-set sshguard4 src -j DROP
         ${pkgs.iptables}/bin/ip6tables -I INPUT -m set --match-set sshguard6 src -j DROP
       '';
 
-      postStop = ''
+      postStop = optionalString config.networking.firewall.enable ''
         ${pkgs.iptables}/bin/iptables  -D INPUT -m set --match-set sshguard4 src -j DROP
         ${pkgs.iptables}/bin/ip6tables -D INPUT -m set --match-set sshguard6 src -j DROP
         ${pkgs.ipset}/bin/ipset -quiet destroy sshguard4
diff --git a/nixos/modules/services/security/vault.nix b/nixos/modules/services/security/vault.nix
index b0ab8fadcbec9..6a8a3a93327eb 100644
--- a/nixos/modules/services/security/vault.nix
+++ b/nixos/modules/services/security/vault.nix
@@ -135,6 +135,7 @@ in
         User = "vault";
         Group = "vault";
         ExecStart = "${cfg.package}/bin/vault server -config ${configFile}";
+        ExecReload = "${pkgs.coreutils}/bin/kill -SIGHUP $MAINPID";
         PrivateDevices = true;
         PrivateTmp = true;
         ProtectSystem = "full";
diff --git a/nixos/modules/services/web-apps/dokuwiki.nix b/nixos/modules/services/web-apps/dokuwiki.nix
new file mode 100644
index 0000000000000..07af7aa0dfec7
--- /dev/null
+++ b/nixos/modules/services/web-apps/dokuwiki.nix
@@ -0,0 +1,272 @@
+{ config, lib, pkgs, ... }:
+
+let
+
+  inherit (lib) mkEnableOption mkForce mkIf mkMerge mkOption optionalAttrs recursiveUpdate types;
+
+  cfg = config.services.dokuwiki;
+
+  user = config.services.nginx.user;
+  group = config.services.nginx.group;
+
+  dokuwikiAclAuthConfig = pkgs.writeText "acl.auth.php" ''
+    # acl.auth.php
+    # <?php exit()?>
+    #
+    # Access Control Lists
+    #
+    ${toString cfg.acl}
+  '';
+
+  dokuwikiLocalConfig = pkgs.writeText "local.php" ''
+    <?php
+    $conf['savedir'] = '${cfg.stateDir}';
+    $conf['superuser'] = '${toString cfg.superUser}';
+    $conf['useacl'] = '${toString cfg.aclUse}';
+    ${toString cfg.extraConfig}
+  '';
+
+  dokuwikiPluginsLocalConfig = pkgs.writeText "plugins.local.php" ''
+    <?php
+    ${cfg.pluginsConfig}
+  '';
+
+in
+{
+  options.services.dokuwiki = {
+    enable = mkEnableOption "DokuWiki web application.";
+
+    hostName = mkOption {
+      type = types.str;
+      default = "localhost";
+      description = "FQDN for the instance.";
+    };
+
+    stateDir = mkOption {
+      type = types.path;
+      default = "/var/lib/dokuwiki/data";
+      description = "Location of the dokuwiki state directory.";
+    };
+
+    acl = mkOption {
+      type = types.nullOr types.lines;
+      default = null;
+      example = "*               @ALL               8";
+      description = ''
+        Access Control Lists: see <link xlink:href="https://www.dokuwiki.org/acl"/>
+        Mutually exclusive with services.dokuwiki.aclFile
+        Set this to a value other than null to take precedence over aclFile option.
+      '';
+    };
+
+    aclFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        Location of the dokuwiki acl rules. Mutually exclusive with services.dokuwiki.acl
+        Mutually exclusive with services.dokuwiki.acl which is preferred.
+        Consult documentation <link xlink:href="https://www.dokuwiki.org/acl"/> for further instructions.
+        Example: <link xlink:href="https://github.com/splitbrain/dokuwiki/blob/master/conf/acl.auth.php.dist"/>
+      '';
+    };
+
+    aclUse = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Necessary for users to log in into the system.
+        Also limits anonymous users. When disabled,
+        everyone is able to create and edit content.
+      '';
+    };
+
+    pluginsConfig = mkOption {
+      type = types.lines;
+      default = ''
+        $plugins['authad'] = 0;
+        $plugins['authldap'] = 0;
+        $plugins['authmysql'] = 0;
+        $plugins['authpgsql'] = 0;
+      '';
+      description = ''
+        List of the dokuwiki (un)loaded plugins.
+      '';
+    };
+
+    superUser = mkOption {
+      type = types.nullOr types.str;
+      default = "@admin";
+      description = ''
+        You can set either a username, a list of usernames (“admin1,admin2”), 
+        or the name of a group by prepending an @ char to the groupname
+        Consult documentation <link xlink:href="https://www.dokuwiki.org/config:superuser"/> for further instructions.
+      '';
+    };
+
+    usersFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        Location of the dokuwiki users file. List of users. Format:
+        login:passwordhash:Real Name:email:groups,comma,separated 
+        Create passwordHash easily by using:$ mkpasswd -5 password `pwgen 8 1`
+        Example: <link xlink:href="https://github.com/splitbrain/dokuwiki/blob/master/conf/users.auth.php.dist"/>
+        '';
+    };
+
+    extraConfig = mkOption {
+      type = types.nullOr types.lines;
+      default = null;
+      example = ''
+        $conf['title'] = 'My Wiki';
+        $conf['userewrite'] = 1;
+      '';
+      description = ''
+        DokuWiki configuration. Refer to
+        <link xlink:href="https://www.dokuwiki.org/config"/>
+        for details on supported values.
+      '';
+    };
+
+    poolConfig = mkOption {
+      type = with types; attrsOf (oneOf [ str int bool ]);
+      default = {
+        "pm" = "dynamic";
+        "pm.max_children" = 32;
+        "pm.start_servers" = 2;
+        "pm.min_spare_servers" = 2;
+        "pm.max_spare_servers" = 4;
+        "pm.max_requests" = 500;
+      };
+      description = ''
+        Options for the dokuwiki PHP pool. See the documentation on <literal>php-fpm.conf</literal>
+        for details on configuration directives.
+      '';
+    };
+
+    nginx = mkOption {
+      type = types.submodule (
+        recursiveUpdate
+          (import ../web-servers/nginx/vhost-options.nix { inherit config lib; })
+          {
+            # Enable encryption by default,
+            options.forceSSL.default = true;
+            options.enableACME.default = true;
+          }
+      );
+      default = {forceSSL = true; enableACME = true;};
+      example = {
+        serverAliases = [
+          "wiki.\${config.networking.domain}"
+        ];
+        enableACME = false;
+      };
+      description = ''
+        With this option, you can customize the nginx virtualHost which already has sensible defaults for DokuWiki.
+      '';
+    };
+  };
+
+  # implementation
+
+  config = mkIf cfg.enable {
+
+    warnings = mkIf (cfg.superUser == null) ["Not setting services.dokuwiki.superUser will impair your ability to administer DokuWiki"];
+
+    assertions = [ 
+      {
+        assertion = cfg.aclUse -> (cfg.acl != null || cfg.aclFile != null);
+        message = "Either services.dokuwiki.acl or services.dokuwiki.aclFile is mandatory when aclUse is true";
+      }
+      {
+        assertion = cfg.usersFile != null -> cfg.aclUse != false;
+        message = "services.dokuwiki.aclUse must be true when usersFile is not null";
+      }
+    ];
+
+    services.phpfpm.pools.dokuwiki = {
+      inherit user;
+      inherit group;
+      phpEnv = {        
+        DOKUWIKI_LOCAL_CONFIG = "${dokuwikiLocalConfig}";
+        DOKUWIKI_PLUGINS_LOCAL_CONFIG = "${dokuwikiPluginsLocalConfig}";
+      } //optionalAttrs (cfg.usersFile != null) {
+        DOKUWIKI_USERS_AUTH_CONFIG = "${cfg.usersFile}";
+      } //optionalAttrs (cfg.aclUse) {
+        DOKUWIKI_ACL_AUTH_CONFIG = if (cfg.acl != null) then "${dokuwikiAclAuthConfig}" else "${toString cfg.aclFile}";
+      };
+      
+      settings = {
+        "listen.mode" = "0660";
+        "listen.owner" = user;
+        "listen.group" = group;
+      } // cfg.poolConfig;
+    };
+
+    services.nginx = {
+      enable = true;
+      
+       virtualHosts = {
+        ${cfg.hostName} = mkMerge [ cfg.nginx {
+          root = mkForce "${pkgs.dokuwiki}/share/dokuwiki/";
+          extraConfig = "fastcgi_param HTTPS on;";
+
+          locations."~ /(conf/|bin/|inc/|install.php)" = {
+            extraConfig = "deny all;";
+          };
+
+          locations."~ ^/data/" = {
+            root = "${cfg.stateDir}";
+            extraConfig = "internal;";
+          };
+
+          locations."~ ^/lib.*\.(js|css|gif|png|ico|jpg|jpeg)$" = {
+            extraConfig = "expires 365d;";
+          };
+
+          locations."/" = {
+            priority = 1;
+            index = "doku.php";
+            extraConfig = ''try_files $uri $uri/ @dokuwiki;'';
+          };
+
+          locations."@dokuwiki" = {
+            extraConfig = ''
+              # rewrites "doku.php/" out of the URLs if you set the userwrite setting to .htaccess in dokuwiki config page
+              rewrite ^/_media/(.*) /lib/exe/fetch.php?media=$1 last;
+              rewrite ^/_detail/(.*) /lib/exe/detail.php?media=$1 last;
+              rewrite ^/_export/([^/]+)/(.*) /doku.php?do=export_$1&id=$2 last;
+              rewrite ^/(.*) /doku.php?id=$1&$args last;
+            '';
+          };
+
+          locations."~ \.php$" = {
+            extraConfig = ''
+              try_files $uri $uri/ /doku.php;
+              include ${pkgs.nginx}/conf/fastcgi_params;
+              fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+              fastcgi_param REDIRECT_STATUS 200;
+              fastcgi_pass unix:${config.services.phpfpm.pools.dokuwiki.socket};
+              fastcgi_param HTTPS on;
+            '';
+          };
+        }];
+      };
+
+    };
+
+    systemd.tmpfiles.rules = [
+      "d ${cfg.stateDir}/attic 0750 ${user} ${group} - -"
+      "d ${cfg.stateDir}/cache 0750 ${user} ${group} - -"
+      "d ${cfg.stateDir}/index 0750 ${user} ${group} - -"
+      "d ${cfg.stateDir}/locks 0750 ${user} ${group} - -"
+      "d ${cfg.stateDir}/media 0750 ${user} ${group} - -"
+      "d ${cfg.stateDir}/media_attic 0750 ${user} ${group} - -"
+      "d ${cfg.stateDir}/media_meta 0750 ${user} ${group} - -"
+      "d ${cfg.stateDir}/meta 0750 ${user} ${group} - -"
+      "d ${cfg.stateDir}/pages 0750 ${user} ${group} - -"
+      "d ${cfg.stateDir}/tmp 0750 ${user} ${group} - -"
+    ];
+
+  };
+}
diff --git a/nixos/modules/services/web-apps/limesurvey.nix b/nixos/modules/services/web-apps/limesurvey.nix
index e00a47191c6f9..56265e80957ed 100644
--- a/nixos/modules/services/web-apps/limesurvey.nix
+++ b/nixos/modules/services/web-apps/limesurvey.nix
@@ -100,7 +100,7 @@ in
     };
 
     virtualHost = mkOption {
-      type = types.submodule (import ../web-servers/apache-httpd/per-server-options.nix);
+      type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
       example = literalExample ''
         {
           hostName = "survey.example.org";
diff --git a/nixos/modules/services/web-apps/mediawiki.nix b/nixos/modules/services/web-apps/mediawiki.nix
index 8a109b39bb579..e9ed53857d811 100644
--- a/nixos/modules/services/web-apps/mediawiki.nix
+++ b/nixos/modules/services/web-apps/mediawiki.nix
@@ -290,7 +290,7 @@ in
       };
 
       virtualHost = mkOption {
-        type = types.submodule (import ../web-servers/apache-httpd/per-server-options.nix);
+        type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
         example = literalExample ''
           {
             hostName = "mediawiki.example.org";
diff --git a/nixos/modules/services/web-apps/moodle.nix b/nixos/modules/services/web-apps/moodle.nix
index 595d070d940ab..1196780cf6ef9 100644
--- a/nixos/modules/services/web-apps/moodle.nix
+++ b/nixos/modules/services/web-apps/moodle.nix
@@ -140,7 +140,7 @@ in
     };
 
     virtualHost = mkOption {
-      type = types.submodule (import ../web-servers/apache-httpd/per-server-options.nix);
+      type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
       example = literalExample ''
         {
           hostName = "moodle.example.org";
diff --git a/nixos/modules/services/web-apps/wordpress.nix b/nixos/modules/services/web-apps/wordpress.nix
index ad4f39fbf52c4..c48a44097372e 100644
--- a/nixos/modules/services/web-apps/wordpress.nix
+++ b/nixos/modules/services/web-apps/wordpress.nix
@@ -209,7 +209,7 @@ let
         };
 
         virtualHost = mkOption {
-          type = types.submodule (import ../web-servers/apache-httpd/per-server-options.nix);
+          type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
           example = literalExample ''
             {
               adminAddr = "webmaster@example.org";
diff --git a/nixos/modules/services/web-apps/zabbix.nix b/nixos/modules/services/web-apps/zabbix.nix
index ee8447810c6de..0071951283478 100644
--- a/nixos/modules/services/web-apps/zabbix.nix
+++ b/nixos/modules/services/web-apps/zabbix.nix
@@ -113,7 +113,7 @@ in
       };
 
       virtualHost = mkOption {
-        type = types.submodule (import ../web-servers/apache-httpd/per-server-options.nix);
+        type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
         example = literalExample ''
           {
             hostName = "zabbix.example.org";
diff --git a/nixos/modules/services/web-servers/apache-httpd/default.nix b/nixos/modules/services/web-servers/apache-httpd/default.nix
index fd17e4b54f0f2..3200a26364f68 100644
--- a/nixos/modules/services/web-servers/apache-httpd/default.nix
+++ b/nixos/modules/services/web-servers/apache-httpd/default.nix
@@ -4,21 +4,21 @@ with lib;
 
 let
 
-  mainCfg = config.services.httpd;
+  cfg = config.services.httpd;
 
   runtimeDir = "/run/httpd";
 
-  httpd = mainCfg.package.out;
+  pkg = cfg.package.out;
 
-  httpdConf = mainCfg.configFile;
+  httpdConf = cfg.configFile;
 
-  php = mainCfg.phpPackage.override { apacheHttpd = httpd.dev; /* otherwise it only gets .out */ };
+  php = cfg.phpPackage.override { apacheHttpd = pkg.dev; /* otherwise it only gets .out */ };
 
   phpMajorVersion = lib.versions.major (lib.getVersion php);
 
-  mod_perl = pkgs.apacheHttpdPackages.mod_perl.override { apacheHttpd = httpd; };
+  mod_perl = pkgs.apacheHttpdPackages.mod_perl.override { apacheHttpd = pkg; };
 
-  vhosts = attrValues mainCfg.virtualHosts;
+  vhosts = attrValues cfg.virtualHosts;
 
   mkListenInfo = hostOpts:
     if hostOpts.listen != [] then hostOpts.listen
@@ -41,23 +41,18 @@ let
       "mime" "autoindex" "negotiation" "dir"
       "alias" "rewrite"
       "unixd" "slotmem_shm" "socache_shmcb"
-      "mpm_${mainCfg.multiProcessingModule}"
+      "mpm_${cfg.multiProcessingModule}"
     ]
-    ++ (if mainCfg.multiProcessingModule == "prefork" then [ "cgi" ] else [ "cgid" ])
+    ++ (if cfg.multiProcessingModule == "prefork" then [ "cgi" ] else [ "cgid" ])
     ++ optional enableSSL "ssl"
     ++ optional enableUserDir "userdir"
-    ++ optional mainCfg.enableMellon { name = "auth_mellon"; path = "${pkgs.apacheHttpdPackages.mod_auth_mellon}/modules/mod_auth_mellon.so"; }
-    ++ optional mainCfg.enablePHP { name = "php${phpMajorVersion}"; path = "${php}/modules/libphp${phpMajorVersion}.so"; }
-    ++ optional mainCfg.enablePerl { name = "perl"; path = "${mod_perl}/modules/mod_perl.so"; }
-    ++ mainCfg.extraModules;
+    ++ optional cfg.enableMellon { name = "auth_mellon"; path = "${pkgs.apacheHttpdPackages.mod_auth_mellon}/modules/mod_auth_mellon.so"; }
+    ++ optional cfg.enablePHP { name = "php${phpMajorVersion}"; path = "${php}/modules/libphp${phpMajorVersion}.so"; }
+    ++ optional cfg.enablePerl { name = "perl"; path = "${mod_perl}/modules/mod_perl.so"; }
+    ++ cfg.extraModules;
 
-
-  allDenied = "Require all denied";
-  allGranted = "Require all granted";
-
-
-  loggingConf = (if mainCfg.logFormat != "none" then ''
-    ErrorLog ${mainCfg.logDir}/error.log
+  loggingConf = (if cfg.logFormat != "none" then ''
+    ErrorLog ${cfg.logDir}/error.log
 
     LogLevel notice
 
@@ -66,7 +61,7 @@ let
     LogFormat "%{Referer}i -> %U" referer
     LogFormat "%{User-agent}i" agent
 
-    CustomLog ${mainCfg.logDir}/access.log ${mainCfg.logFormat}
+    CustomLog ${cfg.logDir}/access.log ${cfg.logFormat}
   '' else ''
     ErrorLog /dev/null
   '');
@@ -88,34 +83,36 @@ let
 
 
   sslConf = ''
-    SSLSessionCache shmcb:${runtimeDir}/ssl_scache(512000)
+    <IfModule mod_ssl.c>
+        SSLSessionCache shmcb:${runtimeDir}/ssl_scache(512000)
 
-    Mutex posixsem
+        Mutex posixsem
 
-    SSLRandomSeed startup builtin
-    SSLRandomSeed connect builtin
+        SSLRandomSeed startup builtin
+        SSLRandomSeed connect builtin
 
-    SSLProtocol ${mainCfg.sslProtocols}
-    SSLCipherSuite ${mainCfg.sslCiphers}
-    SSLHonorCipherOrder on
+        SSLProtocol ${cfg.sslProtocols}
+        SSLCipherSuite ${cfg.sslCiphers}
+        SSLHonorCipherOrder on
+    </IfModule>
   '';
 
 
   mimeConf = ''
-    TypesConfig ${httpd}/conf/mime.types
+    TypesConfig ${pkg}/conf/mime.types
 
     AddType application/x-x509-ca-cert .crt
     AddType application/x-pkcs7-crl    .crl
     AddType application/x-httpd-php    .php .phtml
 
     <IfModule mod_mime_magic.c>
-        MIMEMagicFile ${httpd}/conf/magic
+        MIMEMagicFile ${pkg}/conf/magic
     </IfModule>
   '';
 
   mkVHostConf = hostOpts:
     let
-      adminAddr = if hostOpts.adminAddr != null then hostOpts.adminAddr else mainCfg.adminAddr;
+      adminAddr = if hostOpts.adminAddr != null then hostOpts.adminAddr else cfg.adminAddr;
       listen = filter (listen: !listen.ssl) (mkListenInfo hostOpts);
       listenSSL = filter (listen: listen.ssl) (mkListenInfo hostOpts);
 
@@ -179,11 +176,33 @@ let
         then hostOpts.documentRoot
         else pkgs.runCommand "empty" { preferLocalBuild = true; } "mkdir -p $out"
       ;
+
+      mkLocations = locations: concatStringsSep "\n" (map (config: ''
+        <Location ${config.location}>
+          ${optionalString (config.proxyPass != null) ''
+            <IfModule mod_proxy.c>
+                ProxyPass ${config.proxyPass}
+                ProxyPassReverse ${config.proxyPass}
+            </IfModule>
+          ''}
+          ${optionalString (config.index != null) ''
+            <IfModule mod_dir.c>
+                DirectoryIndex ${config.index}
+            </IfModule>
+          ''}
+          ${optionalString (config.alias != null) ''
+            <IfModule mod_alias.c>
+                Alias "${config.alias}"
+            </IfModule>
+          ''}
+          ${config.extraConfig}
+        </Location>
+      '') (sortProperties (mapAttrsToList (k: v: v // { location = k; }) locations)));
     in
       ''
-        ${optionalString mainCfg.logPerVirtualHost ''
-          ErrorLog ${mainCfg.logDir}/error-${hostOpts.hostName}.log
-          CustomLog ${mainCfg.logDir}/access-${hostOpts.hostName}.log ${hostOpts.logFormat}
+        ${optionalString cfg.logPerVirtualHost ''
+          ErrorLog ${cfg.logDir}/error-${hostOpts.hostName}.log
+          CustomLog ${cfg.logDir}/access-${hostOpts.hostName}.log ${hostOpts.logFormat}
         ''}
 
         ${optionalString (hostOpts.robotsEntries != "") ''
@@ -195,7 +214,7 @@ let
         <Directory "${documentRoot}">
             Options Indexes FollowSymLinks
             AllowOverride None
-            ${allGranted}
+            Require all granted
         </Directory>
 
         ${optionalString hostOpts.enableUserDir ''
@@ -218,23 +237,18 @@ let
         ''}
 
         ${
-          let makeFileConf = elem: ''
-                Alias ${elem.urlPath} ${elem.file}
-              '';
-          in concatMapStrings makeFileConf hostOpts.servedFiles
-        }
-        ${
           let makeDirConf = elem: ''
                 Alias ${elem.urlPath} ${elem.dir}/
                 <Directory ${elem.dir}>
                     Options +Indexes
-                    ${allGranted}
+                    Require all granted
                     AllowOverride All
                 </Directory>
               '';
           in concatMapStrings makeDirConf hostOpts.servedDirs
         }
 
+        ${mkLocations hostOpts.locations}
         ${hostOpts.extraConfig}
       ''
   ;
@@ -242,20 +256,20 @@ let
 
   confFile = pkgs.writeText "httpd.conf" ''
 
-    ServerRoot ${httpd}
+    ServerRoot ${pkg}
     ServerName ${config.networking.hostName}
     DefaultRuntimeDir ${runtimeDir}/runtime
 
     PidFile ${runtimeDir}/httpd.pid
 
-    ${optionalString (mainCfg.multiProcessingModule != "prefork") ''
+    ${optionalString (cfg.multiProcessingModule != "prefork") ''
       # mod_cgid requires this.
       ScriptSock ${runtimeDir}/cgisock
     ''}
 
     <IfModule prefork.c>
-        MaxClients           ${toString mainCfg.maxClients}
-        MaxRequestsPerChild  ${toString mainCfg.maxRequestsPerChild}
+        MaxClients           ${toString cfg.maxClients}
+        MaxRequestsPerChild  ${toString cfg.maxRequestsPerChild}
     </IfModule>
 
     ${let
@@ -264,12 +278,12 @@ let
       in concatStringsSep "\n" uniqueListen
     }
 
-    User ${mainCfg.user}
-    Group ${mainCfg.group}
+    User ${cfg.user}
+    Group ${cfg.group}
 
     ${let
         mkModule = module:
-          if isString module then { name = module; path = "${httpd}/modules/mod_${module}.so"; }
+          if isString module then { name = module; path = "${pkg}/modules/mod_${module}.so"; }
           else if isAttrs module then { inherit (module) name path; }
           else throw "Expecting either a string or attribute set including a name and path.";
       in
@@ -279,37 +293,37 @@ let
     AddHandler type-map var
 
     <Files ~ "^\.ht">
-        ${allDenied}
+        Require all denied
     </Files>
 
     ${mimeConf}
     ${loggingConf}
     ${browserHacks}
 
-    Include ${httpd}/conf/extra/httpd-default.conf
-    Include ${httpd}/conf/extra/httpd-autoindex.conf
-    Include ${httpd}/conf/extra/httpd-multilang-errordoc.conf
-    Include ${httpd}/conf/extra/httpd-languages.conf
+    Include ${pkg}/conf/extra/httpd-default.conf
+    Include ${pkg}/conf/extra/httpd-autoindex.conf
+    Include ${pkg}/conf/extra/httpd-multilang-errordoc.conf
+    Include ${pkg}/conf/extra/httpd-languages.conf
 
     TraceEnable off
 
-    ${if enableSSL then sslConf else ""}
+    ${sslConf}
 
     # Fascist default - deny access to everything.
     <Directory />
         Options FollowSymLinks
         AllowOverride None
-        ${allDenied}
+        Require all denied
     </Directory>
 
     # But do allow access to files in the store so that we don't have
     # to generate <Directory> clauses for every generated file that we
     # want to serve.
     <Directory /nix/store>
-        ${allGranted}
+        Require all granted
     </Directory>
 
-    ${mainCfg.extraConfig}
+    ${cfg.extraConfig}
 
     ${concatMapStringsSep "\n" mkVHostConf vhosts}
   '';
@@ -317,7 +331,7 @@ let
   # Generate the PHP configuration file.  Should probably be factored
   # out into a separate module.
   phpIni = pkgs.runCommand "php.ini"
-    { options = mainCfg.phpOptions;
+    { options = cfg.phpOptions;
       preferLocalBuild = true;
     }
     ''
@@ -350,17 +364,13 @@ in
     (mkRemovedOptionModule [ "services" "httpd" "sslServerKey" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
   ];
 
-  ###### interface
+  # interface
 
   options = {
 
     services.httpd = {
 
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = "Whether to enable the Apache HTTP Server.";
-      };
+      enable = mkEnableOption "the Apache HTTP Server";
 
       package = mkOption {
         type = types.package;
@@ -387,7 +397,7 @@ in
         default = "";
         description = ''
           Configuration lines appended to the generated Apache
-          configuration file. Note that this mechanism may not work
+          configuration file. Note that this mechanism will not work
           when <option>configFile</option> is overridden.
         '';
       };
@@ -402,7 +412,7 @@ in
           ]
         '';
         description = ''
-          Additional Apache modules to be used.  These can be
+          Additional Apache modules to be used. These can be
           specified as a string in the case of modules distributed
           with Apache, or as an attribute set specifying the
           <varname>name</varname> and <varname>path</varname> of the
@@ -441,8 +451,7 @@ in
         type = types.str;
         default = "wwwrun";
         description = ''
-          User account under which httpd runs.  The account is created
-          automatically if it doesn't exist.
+          User account under which httpd runs.
         '';
       };
 
@@ -450,8 +459,7 @@ in
         type = types.str;
         default = "wwwrun";
         description = ''
-          Group under which httpd runs.  The account is created
-          automatically if it doesn't exist.
+          Group under which httpd runs.
         '';
       };
 
@@ -459,15 +467,15 @@ in
         type = types.path;
         default = "/var/log/httpd";
         description = ''
-          Directory for Apache's log files.  It is created automatically.
+          Directory for Apache's log files. It is created automatically.
         '';
       };
 
       virtualHosts = mkOption {
-        type = with types; attrsOf (submodule (import ./per-server-options.nix));
+        type = with types; attrsOf (submodule (import ./vhost-options.nix));
         default = {
           localhost = {
-            documentRoot = "${httpd}/htdocs";
+            documentRoot = "${pkg}/htdocs";
           };
         };
         example = literalExample ''
@@ -523,17 +531,18 @@ in
           ''
             date.timezone = "CET"
           '';
-        description =
-          "Options appended to the PHP configuration file <filename>php.ini</filename>.";
+        description = ''
+          Options appended to the PHP configuration file <filename>php.ini</filename>.
+        '';
       };
 
       multiProcessingModule = mkOption {
-        type = types.str;
+        type = types.enum [ "event" "prefork" "worker" ];
         default = "prefork";
         example = "worker";
         description =
           ''
-            Multi-processing module to be used by Apache.  Available
+            Multi-processing module to be used by Apache. Available
             modules are <literal>prefork</literal> (the default;
             handles each request in a separate child process),
             <literal>worker</literal> (hybrid approach that starts a
@@ -555,8 +564,9 @@ in
         type = types.int;
         default = 0;
         example = 500;
-        description =
-          "Maximum number of httpd requests answered per httpd child (prefork), 0 means unlimited";
+        description = ''
+          Maximum number of httpd requests answered per httpd child (prefork), 0 means unlimited.
+        '';
       };
 
       sslCiphers = mkOption {
@@ -575,10 +585,9 @@ in
 
   };
 
+  # implementation
 
-  ###### implementation
-
-  config = mkIf config.services.httpd.enable {
+  config = mkIf cfg.enable {
 
     assertions = [
       {
@@ -606,28 +615,33 @@ in
       }
     ];
 
-    users.users = optionalAttrs (mainCfg.user == "wwwrun") {
+    warnings =
+      mapAttrsToList (name: hostOpts: ''
+        Using config.services.httpd.virtualHosts."${name}".servedFiles is deprecated and will become unsupported in a future release. Your configuration will continue to work as is but please migrate your configuration to config.services.httpd.virtualHosts."${name}".locations before the 20.09 release of NixOS.
+      '') (filterAttrs (name: hostOpts: hostOpts.servedFiles != []) cfg.virtualHosts);
+
+    users.users = optionalAttrs (cfg.user == "wwwrun") {
       wwwrun = {
-        group = mainCfg.group;
+        group = cfg.group;
         description = "Apache httpd user";
         uid = config.ids.uids.wwwrun;
       };
     };
 
-    users.groups = optionalAttrs (mainCfg.group == "wwwrun") {
+    users.groups = optionalAttrs (cfg.group == "wwwrun") {
       wwwrun.gid = config.ids.gids.wwwrun;
     };
 
     security.acme.certs = mapAttrs (name: hostOpts: {
-      user = mainCfg.user;
-      group = mkDefault mainCfg.group;
-      email = if hostOpts.adminAddr != null then hostOpts.adminAddr else mainCfg.adminAddr;
+      user = cfg.user;
+      group = mkDefault cfg.group;
+      email = if hostOpts.adminAddr != null then hostOpts.adminAddr else cfg.adminAddr;
       webroot = hostOpts.acmeRoot;
       extraDomains = genAttrs hostOpts.serverAliases (alias: null);
       postRun = "systemctl reload httpd.service";
-    }) (filterAttrs (name: hostOpts: hostOpts.enableACME) mainCfg.virtualHosts);
+    }) (filterAttrs (name: hostOpts: hostOpts.enableACME) cfg.virtualHosts);
 
-    environment.systemPackages = [httpd];
+    environment.systemPackages = [ pkg ];
 
     # required for "apachectl configtest"
     environment.etc."httpd/httpd.conf".source = httpdConf;
@@ -667,6 +681,15 @@ in
       "access_compat"
     ];
 
+    systemd.tmpfiles.rules =
+      let
+        svc = config.systemd.services.httpd.serviceConfig;
+      in
+        [
+          "d '${cfg.logDir}' 0700 ${svc.User} ${svc.Group}"
+          "Z '${cfg.logDir}' - ${svc.User} ${svc.Group}"
+        ];
+
     systemd.services.httpd =
       let
         vhostsACME = filter (hostOpts: hostOpts.enableACME) vhosts;
@@ -678,35 +701,36 @@ in
         after = [ "network.target" "fs.target" ] ++ map (hostOpts: "acme-selfsigned-${hostOpts.hostName}.service") vhostsACME;
 
         path =
-          [ httpd pkgs.coreutils pkgs.gnugrep ]
-          ++ optional mainCfg.enablePHP pkgs.system-sendmail; # Needed for PHP's mail() function.
+          [ pkg pkgs.coreutils pkgs.gnugrep ]
+          ++ optional cfg.enablePHP pkgs.system-sendmail; # Needed for PHP's mail() function.
 
         environment =
-          optionalAttrs mainCfg.enablePHP { PHPRC = phpIni; }
-          // optionalAttrs mainCfg.enableMellon { LD_LIBRARY_PATH  = "${pkgs.xmlsec}/lib"; };
+          optionalAttrs cfg.enablePHP { PHPRC = phpIni; }
+          // optionalAttrs cfg.enableMellon { LD_LIBRARY_PATH  = "${pkgs.xmlsec}/lib"; };
 
         preStart =
           ''
-            mkdir -m 0700 -p ${mainCfg.logDir}
-
             # Get rid of old semaphores.  These tend to accumulate across
             # server restarts, eventually preventing it from restarting
             # successfully.
-            for i in $(${pkgs.utillinux}/bin/ipcs -s | grep ' ${mainCfg.user} ' | cut -f2 -d ' '); do
+            for i in $(${pkgs.utillinux}/bin/ipcs -s | grep ' ${cfg.user} ' | cut -f2 -d ' '); do
                 ${pkgs.utillinux}/bin/ipcrm -s $i
             done
           '';
 
-        serviceConfig.ExecStart = "@${httpd}/bin/httpd httpd -f ${httpdConf}";
-        serviceConfig.ExecStop = "${httpd}/bin/httpd -f ${httpdConf} -k graceful-stop";
-        serviceConfig.ExecReload = "${httpd}/bin/httpd -f ${httpdConf} -k graceful";
-        serviceConfig.Group = mainCfg.group;
-        serviceConfig.Type = "forking";
-        serviceConfig.PIDFile = "${runtimeDir}/httpd.pid";
-        serviceConfig.Restart = "always";
-        serviceConfig.RestartSec = "5s";
-        serviceConfig.RuntimeDirectory = "httpd httpd/runtime";
-        serviceConfig.RuntimeDirectoryMode = "0750";
+        serviceConfig = {
+          ExecStart = "@${pkg}/bin/httpd httpd -f ${httpdConf}";
+          ExecStop = "${pkg}/bin/httpd -f ${httpdConf} -k graceful-stop";
+          ExecReload = "${pkg}/bin/httpd -f ${httpdConf} -k graceful";
+          User = "root";
+          Group = cfg.group;
+          Type = "forking";
+          PIDFile = "${runtimeDir}/httpd.pid";
+          Restart = "always";
+          RestartSec = "5s";
+          RuntimeDirectory = "httpd httpd/runtime";
+          RuntimeDirectoryMode = "0750";
+        };
       };
 
   };
diff --git a/nixos/modules/services/web-servers/apache-httpd/location-options.nix b/nixos/modules/services/web-servers/apache-httpd/location-options.nix
new file mode 100644
index 0000000000000..8ea88f94f973f
--- /dev/null
+++ b/nixos/modules/services/web-servers/apache-httpd/location-options.nix
@@ -0,0 +1,54 @@
+{ config, lib, name, ... }:
+let
+  inherit (lib) mkOption types;
+in
+{
+  options = {
+
+    proxyPass = mkOption {
+      type = with types; nullOr str;
+      default = null;
+      example = "http://www.example.org/";
+      description = ''
+        Sets up a simple reverse proxy as described by <link xlink:href="https://httpd.apache.org/docs/2.4/howto/reverse_proxy.html#simple" />.
+      '';
+    };
+
+    index = mkOption {
+      type = with types; nullOr str;
+      default = null;
+      example = "index.php index.html";
+      description = ''
+        Adds DirectoryIndex directive. See <link xlink:href="https://httpd.apache.org/docs/2.4/mod/mod_dir.html#directoryindex" />.
+      '';
+    };
+
+    alias = mkOption {
+      type = with types; nullOr path;
+      default = null;
+      example = "/your/alias/directory";
+      description = ''
+        Alias directory for requests. See <link xlink:href="https://httpd.apache.org/docs/2.4/mod/mod_alias.html#alias" />.
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        These lines go to the end of the location verbatim.
+      '';
+    };
+
+    priority = mkOption {
+      type = types.int;
+      default = 1000;
+      description = ''
+        Order of this location block in relation to the others in the vhost.
+        The semantics are the same as with `lib.mkOrder`. Smaller values have
+        a greater priority.
+      '';
+    };
+
+  };
+}
diff --git a/nixos/modules/services/web-servers/apache-httpd/per-server-options.nix b/nixos/modules/services/web-servers/apache-httpd/vhost-options.nix
index f2e92cda05f60..f34f8b4acdf7e 100644
--- a/nixos/modules/services/web-servers/apache-httpd/per-server-options.nix
+++ b/nixos/modules/services/web-servers/apache-httpd/vhost-options.nix
@@ -1,6 +1,6 @@
 { config, lib, name, ... }:
 let
-  inherit (lib) mkOption types;
+  inherit (lib) literalExample mkOption nameValuePair types;
 in
 {
   options = {
@@ -175,6 +175,12 @@ in
       ];
       description = ''
         This option provides a simple way to serve individual, static files.
+
+        <note><para>
+          This option has been deprecated and will be removed in a future
+          version of NixOS. You can achieve the same result by making use of
+          the <literal>locations.&lt;name&gt;.alias</literal> option.
+        </para></note>
       '';
     };
 
@@ -231,5 +237,30 @@ in
       '';
     };
 
+    locations = mkOption {
+      type = with types; attrsOf (submodule (import ./location-options.nix));
+      default = {};
+      example = literalExample ''
+        {
+          "/" = {
+            proxyPass = "http://localhost:3000";
+          };
+          "/foo/bar.png" = {
+            alias = "/home/eelco/some-file.png";
+          };
+        };
+      '';
+      description = ''
+        Declarative location config. See <link
+        xlink:href="https://httpd.apache.org/docs/2.4/mod/core.html#location"/> for details.
+      '';
+    };
+
+  };
+
+  config = {
+
+    locations = builtins.listToAttrs (map (elem: nameValuePair elem.urlPath { alias = elem.file; }) config.servedFiles);
+
   };
 }
diff --git a/nixos/modules/services/web-servers/unit/default.nix b/nixos/modules/services/web-servers/unit/default.nix
index b0b837cd1929e..f8a18954fc99b 100644
--- a/nixos/modules/services/web-servers/unit/default.nix
+++ b/nixos/modules/services/web-servers/unit/default.nix
@@ -130,8 +130,10 @@ in {
     };
 
     users.users = optionalAttrs (cfg.user == "unit") {
-      unit.group = cfg.group;
-      isSystemUser = true;
+      unit = {
+        group = cfg.group;
+        isSystemUser = true;
+      };
     };
 
     users.groups = optionalAttrs (cfg.group == "unit") {
diff --git a/nixos/modules/services/x11/desktop-managers/default.nix b/nixos/modules/services/x11/desktop-managers/default.nix
index 970fa620c6b6d..ea6aac9f6c92f 100644
--- a/nixos/modules/services/x11/desktop-managers/default.nix
+++ b/nixos/modules/services/x11/desktop-managers/default.nix
@@ -68,21 +68,15 @@ in
           scripts before forwarding the value to the
           <varname>displayManager</varname>.
         '';
-        apply = list: {
-          list = map (d: d // {
-            manage = "desktop";
-            start = d.start
-            + optionalString (needBGCond d) ''
-              if [ -e $HOME/.background-image ]; then
-                ${pkgs.feh}/bin/feh --bg-${cfg.wallpaper.mode} ${optionalString cfg.wallpaper.combineScreens "--no-xinerama"} $HOME/.background-image
-              else
-                # Use a solid black background as fallback
-                ${pkgs.xorg.xsetroot}/bin/xsetroot -solid black
-              fi
-            '';
-          }) list;
-          needBGPackages = [] != filter needBGCond list;
-        };
+        apply = map (d: d // {
+          manage = "desktop";
+          start = d.start
+          + optionalString (needBGCond d) ''
+            if [ -e $HOME/.background-image ]; then
+              ${pkgs.feh}/bin/feh --bg-${cfg.wallpaper.mode} ${optionalString cfg.wallpaper.combineScreens "--no-xinerama"} $HOME/.background-image
+            fi
+          '';
+        });
       };
 
       default = mkOption {
@@ -100,5 +94,5 @@ in
 
   };
 
-  config.services.xserver.displayManager.session = cfg.session.list;
+  config.services.xserver.displayManager.session = cfg.session;
 }
diff --git a/nixos/modules/services/x11/desktop-managers/gnome3.nix b/nixos/modules/services/x11/desktop-managers/gnome3.nix
index ba9906072b3f3..5756cf14ed948 100644
--- a/nixos/modules/services/x11/desktop-managers/gnome3.nix
+++ b/nixos/modules/services/x11/desktop-managers/gnome3.nix
@@ -334,7 +334,6 @@ in
         cheese
         eog
         epiphany
-        geary
         gedit
         gnome-calculator
         gnome-calendar
@@ -361,6 +360,7 @@ in
       # Enable default programs
       programs.evince.enable = mkDefault true;
       programs.file-roller.enable = mkDefault true;
+      programs.geary.enable = mkDefault true;
       programs.gnome-disks.enable = mkDefault true;
       programs.gnome-terminal.enable = mkDefault true;
       programs.seahorse.enable = mkDefault true;
diff --git a/nixos/modules/services/x11/desktop-managers/xfce.nix b/nixos/modules/services/x11/desktop-managers/xfce.nix
index a08b1947f65ba..21f59074f3ae8 100644
--- a/nixos/modules/services/x11/desktop-managers/xfce.nix
+++ b/nixos/modules/services/x11/desktop-managers/xfce.nix
@@ -127,14 +127,9 @@ in
       "/share/gtksourceview-4.0"
     ];
 
-    services.xserver.desktopManager.session = [{
-      name = "xfce";
-      bgSupport = true;
-      start = ''
-        ${pkgs.runtimeShell} ${pkgs.xfce.xfce4-session.xinitrc} &
-        waitPID=$!
-      '';
-    }];
+    services.xserver.displayManager.sessionPackages = [
+      pkgs.xfce.xfce4-session
+    ];
 
     services.xserver.updateDbusEnvironment = true;
     services.xserver.gdk-pixbuf.modulePackages = [ pkgs.librsvg ];
diff --git a/nixos/modules/services/x11/xserver.nix b/nixos/modules/services/x11/xserver.nix
index 7029919170aaf..7f0de96d2084a 100644
--- a/nixos/modules/services/x11/xserver.nix
+++ b/nixos/modules/services/x11/xserver.nix
@@ -556,8 +556,7 @@ in
 
     services.xserver.displayManager.lightdm.enable =
       let dmconf = cfg.displayManager;
-          default = !( dmconf.auto.enable
-                    || dmconf.gdm.enable
+          default = !(dmconf.gdm.enable
                     || dmconf.sddm.enable
                     || dmconf.xpra.enable );
       in mkIf (default) true;
diff --git a/nixos/modules/system/boot/loader/grub/grub.nix b/nixos/modules/system/boot/loader/grub/grub.nix
index 9a4db84f7b731..26c1197bf975f 100644
--- a/nixos/modules/system/boot/loader/grub/grub.nix
+++ b/nixos/modules/system/boot/loader/grub/grub.nix
@@ -630,7 +630,7 @@ in
 
       boot.loader.grub.extraPrepareConfig =
         concatStrings (mapAttrsToList (n: v: ''
-          ${pkgs.coreutils}/bin/cp -pf "${v}" "/boot/${n}"
+          ${pkgs.coreutils}/bin/cp -pf "${v}" "@bootPath@/${n}"
         '') config.boot.loader.grub.extraFiles);
 
       assertions = [
diff --git a/nixos/modules/system/boot/loader/grub/install-grub.pl b/nixos/modules/system/boot/loader/grub/install-grub.pl
index a09c5dc476180..ca0fb0248e0e9 100644
--- a/nixos/modules/system/boot/loader/grub/install-grub.pl
+++ b/nixos/modules/system/boot/loader/grub/install-grub.pl
@@ -475,6 +475,9 @@ if ($grubVersion == 2) {
     }
 }
 
+# extraPrepareConfig could refer to @bootPath@, which we have to substitute
+$extraPrepareConfig =~ s/\@bootPath\@/$bootPath/g;
+
 # Run extraPrepareConfig in sh
 if ($extraPrepareConfig ne "") {
   system((get("shell"), "-c", $extraPrepareConfig));
diff --git a/nixos/modules/system/boot/luksroot.nix b/nixos/modules/system/boot/luksroot.nix
index 0bb8396a44fc8..31f1e22cda32c 100644
--- a/nixos/modules/system/boot/luksroot.nix
+++ b/nixos/modules/system/boot/luksroot.nix
@@ -4,6 +4,7 @@ with lib;
 
 let
   luks = config.boot.initrd.luks;
+  kernelPackages = config.boot.kernelPackages;
 
   commonFunctions = ''
     die() {
@@ -139,7 +140,7 @@ let
     umount /crypt-ramfs 2>/dev/null
   '';
 
-  openCommand = name': { name, device, header, keyFile, keyFileSize, keyFileOffset, allowDiscards, yubikey, gpgCard, fallbackToPassword, ... }: assert name' == name;
+  openCommand = name': { name, device, header, keyFile, keyFileSize, keyFileOffset, allowDiscards, yubikey, gpgCard, fido2, fallbackToPassword, ... }: assert name' == name;
   let
     csopen   = "cryptsetup luksOpen ${device} ${name} ${optionalString allowDiscards "--allow-discards"} ${optionalString (header != null) "--header=${header}"}";
     cschange = "cryptsetup luksChangeKey ${device} ${optionalString (header != null) "--header=${header}"}";
@@ -387,7 +388,31 @@ let
     }
     ''}
 
-    ${if (luks.yubikeySupport && (yubikey != null)) || (luks.gpgSupport && (gpgCard != null)) then ''
+    ${optionalString (luks.fido2Support && (fido2.credential != null)) ''
+
+    open_with_hardware() {
+      local passsphrase
+
+        ${if fido2.passwordLess then ''
+          export passphrase=""
+        '' else ''
+          read -rsp "FIDO2 salt for ${device}: " passphrase
+          echo
+        ''}
+        ${optionalString (lib.versionOlder kernelPackages.kernel.version "5.4") ''
+          echo "On systems with Linux Kernel < 5.4, it might take a while to initialize the CRNG, you might want to use linuxPackages_latest."
+          echo "Please move your mouse to create needed randomness."
+        ''}
+          echo "Waiting for your FIDO2 device..."
+          fido2luks -i open ${device} ${name} ${fido2.credential} --await-dev ${toString fido2.gracePeriod} --salt string:$passphrase
+        if [ $? -ne 0 ]; then
+          echo "No FIDO2 key found, falling back to normal open procedure"
+          open_normally
+        fi
+    }
+    ''}
+
+    ${if (luks.yubikeySupport && (yubikey != null)) || (luks.gpgSupport && (gpgCard != null)) || (luks.fido2Support && (fido2.credential != null)) then ''
     open_with_hardware
     '' else ''
     open_normally
@@ -608,6 +633,31 @@ in
             });
           };
 
+          fido2 = {
+            credential = mkOption {
+              default = null;
+              example = "f1d00200d8dc783f7fb1e10ace8da27f8312d72692abfca2f7e4960a73f48e82e1f7571f6ebfcee9fb434f9886ccc8fcc52a6614d8d2";
+              type = types.str;
+              description = "The FIDO2 credential ID.";
+            };
+
+            gracePeriod = mkOption {
+              default = 10;
+              type = types.int;
+              description = "Time in seconds to wait for the FIDO2 key.";
+            };
+
+            passwordLess = mkOption {
+              default = false;
+              type = types.bool;
+              description = ''
+                Defines whatever to use an empty string as a default salt.
+
+                Enable only when your device is PIN protected, such as <link xlink:href="https://trezor.io/">Trezor</link>.
+              '';
+            };
+          };
+
           yubikey = mkOption {
             default = null;
             description = ''
@@ -706,6 +756,15 @@ in
             and a Yubikey to work with this feature.
           '';
     };
+
+    boot.initrd.luks.fido2Support = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Enables support for authenticating with FIDO2 devices.
+      '';
+    };
+
   };
 
   config = mkIf (luks.devices != {} || luks.forceLuksSupportInInitrd) {
@@ -714,6 +773,14 @@ in
       [ { assertion = !(luks.gpgSupport && luks.yubikeySupport);
           message = "Yubikey and GPG Card may not be used at the same time.";
         }
+
+        { assertion = !(luks.gpgSupport && luks.fido2Support);
+          message = "FIDO2 and GPG Card may not be used at the same time.";
+        }
+
+        { assertion = !(luks.fido2Support && luks.yubikeySupport);
+          message = "FIDO2 and Yubikey may not be used at the same time.";
+        }
       ];
 
     # actually, sbp2 driver is the one enabling the DMA attack, but this needs to be tested
@@ -753,6 +820,11 @@ in
         chmod +x $out/bin/openssl-wrap
       ''}
 
+      ${optionalString luks.fido2Support ''
+        copy_bin_and_libs ${pkgs.fido2luks}/bin/fido2luks
+      ''}
+
+
       ${optionalString luks.gpgSupport ''
         copy_bin_and_libs ${pkgs.gnupg}/bin/gpg
         copy_bin_and_libs ${pkgs.gnupg}/bin/gpg-agent
@@ -783,6 +855,9 @@ in
         $out/bin/gpg-agent --version
         $out/bin/scdaemon --version
       ''}
+      ${optionalString luks.fido2Support ''
+        $out/bin/fido2luks --version
+      ''}
     '';
 
     boot.initrd.preFailCommands = postCommands;
diff --git a/nixos/modules/system/boot/networkd.nix b/nixos/modules/system/boot/networkd.nix
index 56a9d6b11380b..a77dbc609f462 100644
--- a/nixos/modules/system/boot/networkd.nix
+++ b/nixos/modules/system/boot/networkd.nix
@@ -55,6 +55,11 @@ let
     (assertMacAddress "MACAddress")
   ];
 
+  checkVRF = checkUnitConfig "VRF" [
+    (assertOnlyFields [ "Table" ])
+    (assertMinimum "Table" 0)
+  ];
+
   # NOTE The PrivateKey directive is missing on purpose here, please
   # do not add it to this list. The nix store is world-readable let's
   # refrain ourselves from providing a footgun.
@@ -349,6 +354,21 @@ let
       '';
     };
 
+    vrfConfig = mkOption {
+      default = {};
+      example = { Table = 2342; };
+      type = types.addCheck (types.attrsOf unitOption) checkVRF;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[VRF]</literal> section of the unit. See
+        <citerefentry><refentrytitle>systemd.netdev</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+        A detailed explanation about how VRFs work can be found in the
+        <link xlink:href="https://www.kernel.org/doc/Documentation/networking/vrf.txt">kernel
+        docs</link>.
+      '';
+    };
+
     wireguardConfig = mkOption {
       default = {};
       example = {
@@ -845,6 +865,11 @@ let
             ${attrsToSection def.xfrmConfig}
 
           ''}
+          ${optionalString (def.vrfConfig != { }) ''
+            [VRF]
+            ${attrsToSection def.vrfConfig}
+
+          ''}
           ${optionalString (def.wireguardConfig != { }) ''
             [WireGuard]
             ${attrsToSection def.wireguardConfig}
@@ -947,9 +972,10 @@ in
     systemd.network.units = mkOption {
       description = "Definition of networkd units.";
       default = {};
+      internal = true;
       type = with types; attrsOf (submodule (
         { name, config, ... }:
-        { options = concreteUnitOptions;
+        { options = mapAttrs (_: x: x // { internal = true; }) concreteUnitOptions;
           config = {
             unit = mkDefault (makeUnit name config);
           };
diff --git a/nixos/modules/system/boot/systemd.nix b/nixos/modules/system/boot/systemd.nix
index c438bb216e705..2a5b3608311ad 100644
--- a/nixos/modules/system/boot/systemd.nix
+++ b/nixos/modules/system/boot/systemd.nix
@@ -697,6 +697,16 @@ in
       '';
     };
 
+    systemd.sleep.extraConfig = mkOption {
+      default = "";
+      type = types.lines;
+      example = "HibernateDelaySec=1h";
+      description = ''
+        Extra config options for systemd sleep state logic.
+        See sleep.conf.d(5) man page for available options.
+      '';
+    };
+
     systemd.user.extraConfig = mkOption {
       default = "";
       type = types.lines;
@@ -863,17 +873,22 @@ in
 
       "systemd/sleep.conf".text = ''
         [Sleep]
+        ${config.systemd.sleep.extraConfig}
       '';
 
       # install provided sysctl snippets
       "sysctl.d/50-coredump.conf".source = "${systemd}/example/sysctl.d/50-coredump.conf";
       "sysctl.d/50-default.conf".source = "${systemd}/example/sysctl.d/50-default.conf";
 
+      "tmpfiles.d/home.conf".source = "${systemd}/example/tmpfiles.d/home.conf";
       "tmpfiles.d/journal-nocow.conf".source = "${systemd}/example/tmpfiles.d/journal-nocow.conf";
+      "tmpfiles.d/portables.conf".source = "${systemd}/example/tmpfiles.d/portables.conf";
       "tmpfiles.d/static-nodes-permissions.conf".source = "${systemd}/example/tmpfiles.d/static-nodes-permissions.conf";
       "tmpfiles.d/systemd.conf".source = "${systemd}/example/tmpfiles.d/systemd.conf";
+      "tmpfiles.d/systemd-nologin.conf".source = "${systemd}/example/tmpfiles.d/systemd-nologin.conf";
       "tmpfiles.d/systemd-nspawn.conf".source = "${systemd}/example/tmpfiles.d/systemd-nspawn.conf";
       "tmpfiles.d/systemd-tmp.conf".source = "${systemd}/example/tmpfiles.d/systemd-tmp.conf";
+      "tmpfiles.d/tmp.conf".source = "${systemd}/example/tmpfiles.d/tmp.conf";
       "tmpfiles.d/var.conf".source = "${systemd}/example/tmpfiles.d/var.conf";
       "tmpfiles.d/x11.conf".source = "${systemd}/example/tmpfiles.d/x11.conf";
 
diff --git a/nixos/modules/tasks/network-interfaces.nix b/nixos/modules/tasks/network-interfaces.nix
index 31e2ed1cd1eae..cef9c38c2e304 100644
--- a/nixos/modules/tasks/network-interfaces.nix
+++ b/nixos/modules/tasks/network-interfaces.nix
@@ -143,13 +143,34 @@ let
         description = "Name of the interface.";
       };
 
-      preferTempAddress = mkOption {
-        type = types.bool;
-        default = cfg.enableIPv6;
-        defaultText = literalExample "config.networking.enableIPv6";
+      tempAddress = mkOption {
+        type = types.enum [ "default" "enabled" "disabled" ];
+        default = if cfg.enableIPv6 then "default" else "disabled";
+        defaultText = literalExample ''if cfg.enableIPv6 then "default" else "disabled"'';
         description = ''
-          When using SLAAC prefer a temporary (IPv6) address over the EUI-64
-          address for originating connections. This is used to reduce tracking.
+          When IPv6 is enabled with SLAAC, this option controls the use of
+          temporary address (aka privacy extensions). This is used to reduce tracking.
+          The three possible values are:
+
+          <itemizedlist>
+           <listitem>
+            <para>
+             <literal>"default"</literal> to generate temporary addresses and use
+             them by default;
+            </para>
+           </listitem>
+           <listitem>
+            <para>
+             <literal>"enabled"</literal> to generate temporary addresses but keep
+             using the standard EUI-64 ones by default;
+            </para>
+           </listitem>
+           <listitem>
+            <para>
+             <literal>"disabled"</literal> to completely disable temporary addresses.
+            </para>
+           </listitem>
+          </itemizedlist>
         '';
       };
 
@@ -287,6 +308,11 @@ let
       let
         defined = x: x != "_mkMergedOptionModule";
       in [
+        (mkChangedOptionModule [ "preferTempAddress" ] [ "tempAddress" ]
+         (config:
+          let bool = getAttrFromPath [ "preferTempAddress" ] config;
+          in if bool then "default" else "enabled"
+        ))
         (mkRenamedOptionModule [ "ip4" ] [ "ipv4" "addresses"])
         (mkRenamedOptionModule [ "ip6" ] [ "ipv6" "addresses"])
         (mkRemovedOptionModule [ "subnetMask" ] ''
@@ -945,7 +971,7 @@ in
           The networking.interfaces."${i.name}" must not have any defined ips when it is a slave.
         '';
       })) ++ (forEach interfaces (i: {
-        assertion = i.preferTempAddress -> cfg.enableIPv6;
+        assertion = i.tempAddress != "disabled" -> cfg.enableIPv6;
         message = ''
           Temporary addresses are only needed when IPv6 is enabled.
         '';
@@ -973,8 +999,11 @@ in
       "net.ipv6.conf.all.forwarding" = mkDefault (any (i: i.proxyARP) interfaces);
     } // listToAttrs (flip concatMap (filter (i: i.proxyARP) interfaces)
         (i: forEach [ "4" "6" ] (v: nameValuePair "net.ipv${v}.conf.${replaceChars ["."] ["/"] i.name}.proxy_arp" true)))
-      // listToAttrs (forEach (filter (i: i.preferTempAddress) interfaces)
-        (i: nameValuePair "net.ipv6.conf.${replaceChars ["."] ["/"] i.name}.use_tempaddr" 2));
+      // listToAttrs (forEach interfaces
+        (i: let
+          opt = i.tempAddress;
+          val = { disabled = 0; enabled = 1; default = 2; }.${opt};
+         in nameValuePair "net.ipv6.conf.${replaceChars ["."] ["/"] i.name}.use_tempaddr" val));
 
     # Capabilities won't work unless we have at-least a 4.3 Linux
     # kernel because we need the ambient capability
@@ -1103,10 +1132,18 @@ in
       (pkgs.writeTextFile rec {
         name = "ipv6-privacy-extensions.rules";
         destination = "/etc/udev/rules.d/99-${name}";
-        text = concatMapStrings (i: ''
-          # enable IPv6 privacy addresses but prefer EUI-64 addresses for ${i.name}
-          ACTION=="add", SUBSYSTEM=="net", RUN+="${pkgs.procps}/bin/sysctl net.ipv6.conf.${replaceChars ["."] ["/"] i.name}.use_tempaddr=1"
-        '') (filter (i: !i.preferTempAddress) interfaces);
+        text = concatMapStrings (i:
+          let
+            opt = i.tempAddress;
+            val = if opt == "disabled" then 0 else 1;
+            msg = if opt == "disabled"
+                  then "completely disable IPv6 privacy addresses"
+                  else "enable IPv6 privacy addresses but prefer EUI-64 addresses";
+          in
+          ''
+            # override to ${msg} for ${i.name}
+            ACTION=="add", SUBSYSTEM=="net", RUN+="${pkgs.procps}/bin/sysctl net.ipv6.conf.${replaceChars ["."] ["/"] i.name}.use_tempaddr=${toString val}"
+          '') (filter (i: i.tempAddress != "default") interfaces);
       })
     ] ++ lib.optional (cfg.wlanInterfaces != {})
       (pkgs.writeTextFile {
diff --git a/nixos/modules/virtualisation/amazon-init.nix b/nixos/modules/virtualisation/amazon-init.nix
index 8032b2c6d7ca4..8c12e0e49bf5b 100644
--- a/nixos/modules/virtualisation/amazon-init.nix
+++ b/nixos/modules/virtualisation/amazon-init.nix
@@ -7,8 +7,8 @@ let
     echo "attempting to fetch configuration from EC2 user data..."
 
     export HOME=/root
-    export PATH=${pkgs.lib.makeBinPath [ config.nix.package pkgs.systemd pkgs.gnugrep pkgs.gnused config.system.build.nixos-rebuild]}:$PATH
-    export NIX_PATH=/nix/var/nix/profiles/per-user/root/channels/nixos:nixos-config=/etc/nixos/configuration.nix:/nix/var/nix/profiles/per-user/root/channels
+    export PATH=${pkgs.lib.makeBinPath [ config.nix.package pkgs.systemd pkgs.gnugrep pkgs.git pkgs.gnutar pkgs.gzip pkgs.gnused config.system.build.nixos-rebuild]}:$PATH
+    export NIX_PATH=nixpkgs=/nix/var/nix/profiles/per-user/root/channels/nixos:nixos-config=/etc/nixos/configuration.nix:/nix/var/nix/profiles/per-user/root/channels
 
     userData=/etc/ec2-metadata/user-data
 
@@ -18,9 +18,9 @@ let
       # that as the channel.
       if sed '/^\(#\|SSH_HOST_.*\)/d' < "$userData" | grep -q '\S'; then
         channels="$(grep '^###' "$userData" | sed 's|###\s*||')"
-        printf "%s" "$channels" | while read channel; do
+        while IFS= read -r channel; do
           echo "writing channel: $channel"
-        done
+        done < <(printf "%s\n" "$channels")
 
         if [[ -n "$channels" ]]; then
           printf "%s" "$channels" > /root/.nix-channels
@@ -48,7 +48,7 @@ in {
     wantedBy = [ "multi-user.target" ];
     after = [ "multi-user.target" ];
     requires = [ "network-online.target" ];
- 
+
     restartIfChanged = false;
     unitConfig.X-StopOnRemoval = false;
 
@@ -58,4 +58,3 @@ in {
     };
   };
 }
-
diff --git a/nixos/modules/virtualisation/docker-containers.nix b/nixos/modules/virtualisation/docker-containers.nix
index 760cb9122a2f5..3a2eb97d1bf1b 100644
--- a/nixos/modules/virtualisation/docker-containers.nix
+++ b/nixos/modules/virtualisation/docker-containers.nix
@@ -10,11 +10,24 @@ let
       options = {
 
         image = mkOption {
-          type = types.str;
+          type = with types; str;
           description = "Docker image to run.";
           example = "library/hello-world";
         };
 
+        imageFile = mkOption {
+          type = with types; nullOr package;
+          default = null;
+          description = ''
+            Path to an image file to load instead of pulling from a registry.
+            If defined, do not pull from registry.
+
+            You still need to set the <literal>image</literal> attribute, as it
+            will be used as the image name for docker to start a container.
+          '';
+          example = literalExample "pkgs.dockerTools.buildDockerImage {...};";
+        };
+
         cmd = mkOption {
           type =  with types; listOf str;
           default = [];
@@ -153,6 +166,24 @@ let
           example = "/var/lib/hello_world";
         };
 
+        dependsOn = mkOption {
+          type = with types; listOf str;
+          default = [];
+          description = ''
+            Define which other containers this one depends on. They will be added to both After and Requires for the unit.
+
+            Use the same name as the attribute under <literal>services.docker-containers</literal>.
+          '';
+          example = literalExample ''
+            services.docker-containers = {
+              node1 = {};
+              node2 = {
+                dependsOn = [ "node1" ];
+              }
+            }
+          '';
+        };
+
         extraDockerOptions = mkOption {
           type = with types; listOf str;
           default = [];
@@ -164,15 +195,18 @@ let
       };
     };
 
-  mkService = name: container: {
+  mkService = name: container: let
+    mkAfter = map (x: "docker-${x}.service") container.dependsOn;
+  in rec {
     wantedBy = [ "multi-user.target" ];
-    after = [ "docker.service" "docker.socket" ];
-    requires = [ "docker.service" "docker.socket" ];
+    after = [ "docker.service" "docker.socket" ] ++ mkAfter;
+    requires = after;
+
     serviceConfig = {
       ExecStart = concatStringsSep " \\\n  " ([
         "${pkgs.docker}/bin/docker run"
         "--rm"
-        "--name=%n"
+        "--name=${name}"
         "--log-driver=${container.log-driver}"
       ] ++ optional (container.entrypoint != null)
         "--entrypoint=${escapeShellArg container.entrypoint}"
@@ -185,9 +219,14 @@ let
         ++ [container.image]
         ++ map escapeShellArg container.cmd
       );
-      ExecStartPre = "-${pkgs.docker}/bin/docker rm -f %n";
-      ExecStop = ''${pkgs.bash}/bin/sh -c "[ $SERVICE_RESULT = success ] || ${pkgs.docker}/bin/docker stop %n"'';
-      ExecStopPost = "-${pkgs.docker}/bin/docker rm -f %n";
+
+      ExecStartPre = ["-${pkgs.docker}/bin/docker rm -f ${name}"
+                      "-${pkgs.docker}/bin/docker image prune -f"] ++
+        (optional (container.imageFile != null)
+                ["${pkgs.docker}/bin/docker load -i ${container.imageFile}"]);
+
+      ExecStop = ''${pkgs.bash}/bin/sh -c "[ $SERVICE_RESULT = success ] || ${pkgs.docker}/bin/docker stop ${name}"'';
+      ExecStopPost = "-${pkgs.docker}/bin/docker rm -f ${name}";
 
       ### There is no generalized way of supporting `reload` for docker
       ### containers. Some containers may respond well to SIGHUP sent to their
diff --git a/nixos/modules/virtualisation/lxd.nix b/nixos/modules/virtualisation/lxd.nix
index b4934a86cf56c..de48d3a780e27 100644
--- a/nixos/modules/virtualisation/lxd.nix
+++ b/nixos/modules/virtualisation/lxd.nix
@@ -7,6 +7,7 @@ with lib;
 let
 
   cfg = config.virtualisation.lxd;
+  zfsCfg = config.boot.zfs;
 
 in
 
@@ -26,11 +27,40 @@ in
           <command>lxc</command> command line tool, among others.
         '';
       };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.lxd;
+        defaultText = "pkgs.lxd";
+        description = ''
+          The LXD package to use.
+        '';
+      };
+
+      lxcPackage = mkOption {
+        type = types.package;
+        default = pkgs.lxc;
+        defaultText = "pkgs.lxc";
+        description = ''
+          The LXC package to use with LXD (required for AppArmor profiles).
+        '';
+      };
+
+      zfsPackage = mkOption {
+        type = types.package;
+        default = with pkgs; if zfsCfg.enableUnstable then zfsUnstable else zfs;
+        defaultText = "pkgs.zfs";
+        description = ''
+          The ZFS package to use with LXD.
+        '';
+      };
+
       zfsSupport = mkOption {
         type = types.bool;
         default = false;
         description = ''
-          enables lxd to use zfs as a storage for containers.
+          Enables lxd to use zfs as a storage for containers.
+
           This option is enabled by default if a zfs pool is configured
           with nixos.
         '';
@@ -54,15 +84,15 @@ in
 
   config = mkIf cfg.enable {
 
-    environment.systemPackages = [ pkgs.lxd ];
+    environment.systemPackages = [ cfg.package ];
 
     security.apparmor = {
       enable = true;
       profiles = [
-        "${pkgs.lxc}/etc/apparmor.d/usr.bin.lxc-start"
-        "${pkgs.lxc}/etc/apparmor.d/lxc-containers"
+        "${cfg.lxcPackage}/etc/apparmor.d/usr.bin.lxc-start"
+        "${cfg.lxcPackage}/etc/apparmor.d/lxc-containers"
       ];
-      packages = [ pkgs.lxc ];
+      packages = [ cfg.lxcPackage ];
     };
 
     systemd.services.lxd = {
@@ -71,14 +101,14 @@ in
       wantedBy = [ "multi-user.target" ];
       after = [ "systemd-udev-settle.service" ];
 
-      path = lib.optional cfg.zfsSupport pkgs.zfs;
+      path = lib.optional cfg.zfsSupport cfg.zfsPackage;
 
       preStart = ''
         mkdir -m 0755 -p /var/lib/lxc/rootfs
       '';
 
       serviceConfig = {
-        ExecStart = "@${pkgs.lxd.bin}/bin/lxd lxd --group lxd";
+        ExecStart = "@${cfg.package.bin}/bin/lxd lxd --group lxd";
         Type = "simple";
         KillMode = "process"; # when stopping, leave the containers alone
         LimitMEMLOCK = "infinity";
diff --git a/nixos/release-combined.nix b/nixos/release-combined.nix
index ca9c6f9a7f911..b46731863cab7 100644
--- a/nixos/release-combined.nix
+++ b/nixos/release-combined.nix
@@ -54,7 +54,7 @@ in rec {
         (all nixos.dummy)
         (all nixos.manual)
 
-        nixos.iso_graphical.x86_64-linux or []
+        nixos.iso_plasma5.x86_64-linux or []
         nixos.iso_minimal.aarch64-linux or []
         nixos.iso_minimal.i686-linux or []
         nixos.iso_minimal.x86_64-linux or []
diff --git a/nixos/release.nix b/nixos/release.nix
index f40b5fa9bd7f7..512ba7143977a 100644
--- a/nixos/release.nix
+++ b/nixos/release.nix
@@ -149,9 +149,9 @@ in rec {
     inherit system;
   });
 
-  iso_graphical = forMatchingSystems [ "x86_64-linux" ] (system: makeIso {
-    module = ./modules/installer/cd-dvd/installation-cd-graphical-kde.nix;
-    type = "graphical";
+  iso_plasma5 = forMatchingSystems [ "x86_64-linux" ] (system: makeIso {
+    module = ./modules/installer/cd-dvd/installation-cd-graphical-plasma5.nix;
+    type = "plasma5";
     inherit system;
   });
 
@@ -209,7 +209,8 @@ in rec {
     hydraJob ((import lib/eval-config.nix {
       inherit system;
       modules =
-        [ versionModule
+        [ configuration
+          versionModule
           ./maintainers/scripts/ec2/amazon-image.nix
         ];
     }).config.system.build.amazonImage)
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index eb69457fb7e9b..17f36265c51a4 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -32,7 +32,6 @@ in
   bees = handleTest ./bees.nix {};
   bind = handleTest ./bind.nix {};
   bittorrent = handleTest ./bittorrent.nix {};
-  #blivet = handleTest ./blivet.nix {};   # broken since 2017-07024
   buildkite-agent = handleTest ./buildkite-agent.nix {};
   boot = handleTestOn ["x86_64-linux"] ./boot.nix {}; # syslinux is unsupported on aarch64
   boot-stage1 = handleTest ./boot-stage1.nix {};
@@ -66,7 +65,7 @@ in
   couchdb = handleTest ./couchdb.nix {};
   deluge = handleTest ./deluge.nix {};
   dhparams = handleTest ./dhparams.nix {};
-  dnscrypt-proxy = handleTestOn ["x86_64-linux"] ./dnscrypt-proxy.nix {};
+  dnscrypt-proxy2 = handleTestOn ["x86_64-linux"] ./dnscrypt-proxy2.nix {};
   docker = handleTestOn ["x86_64-linux"] ./docker.nix {};
   docker-containers = handleTestOn ["x86_64-linux"] ./docker-containers.nix {};
   docker-edge = handleTestOn ["x86_64-linux"] ./docker-edge.nix {};
@@ -75,6 +74,7 @@ in
   docker-tools = handleTestOn ["x86_64-linux"] ./docker-tools.nix {};
   docker-tools-overlay = handleTestOn ["x86_64-linux"] ./docker-tools-overlay.nix {};
   documize = handleTest ./documize.nix {};
+  dokuwiki = handleTest ./dokuwiki.nix {};
   dovecot = handleTest ./dovecot.nix {};
   # ec2-config doesn't work in a sandbox as the simulated ec2 instance needs network access
   #ec2-config = (handleTestOn ["x86_64-linux"] ./ec2.nix {}).boot-ec2-config or {};
@@ -93,6 +93,7 @@ in
   flannel = handleTestOn ["x86_64-linux"] ./flannel.nix {};
   fluentd = handleTest ./fluentd.nix {};
   fontconfig-default-fonts = handleTest ./fontconfig-default-fonts.nix {};
+  freeswitch = handleTest ./freeswitch.nix {};
   fsck = handleTest ./fsck.nix {};
   gotify-server = handleTest ./gotify-server.nix {};
   gitea = handleTest ./gitea.nix {};
@@ -212,8 +213,7 @@ in
   openldap = handleTest ./openldap.nix {};
   opensmtpd = handleTest ./opensmtpd.nix {};
   openssh = handleTest ./openssh.nix {};
-  # openstack-image-userdata doesn't work in a sandbox as the simulated openstack instance needs network access
-  #openstack-image-userdata = (handleTestOn ["x86_64-linux"] ./openstack-image.nix {}).userdata or {};
+  openstack-image-userdata = (handleTestOn ["x86_64-linux"] ./openstack-image.nix {}).userdata or {};
   openstack-image-metadata = (handleTestOn ["x86_64-linux"] ./openstack-image.nix {}).metadata or {};
   orangefs = handleTest ./orangefs.nix {};
   os-prober = handleTestOn ["x86_64-linux"] ./os-prober.nix {};
@@ -274,6 +274,7 @@ in
   systemd-analyze = handleTest ./systemd-analyze.nix {};
   systemd-confinement = handleTest ./systemd-confinement.nix {};
   systemd-timesyncd = handleTest ./systemd-timesyncd.nix {};
+  systemd-networkd-vrf = handleTest ./systemd-networkd-vrf.nix {};
   systemd-networkd-wireguard = handleTest ./systemd-networkd-wireguard.nix {};
   systemd-nspawn = handleTest ./systemd-nspawn.nix {};
   pdns-recursor = handleTest ./pdns-recursor.nix {};
@@ -292,6 +293,7 @@ in
   upnp = handleTest ./upnp.nix {};
   uwsgi = handleTest ./uwsgi.nix {};
   vault = handleTest ./vault.nix {};
+  victoriametrics = handleTest ./victoriametrics.nix {};
   virtualbox = handleTestOn ["x86_64-linux"] ./virtualbox.nix {};
   wireguard = handleTest ./wireguard {};
   wireguard-generated = handleTest ./wireguard/generated.nix {};
diff --git a/nixos/tests/blivet.nix b/nixos/tests/blivet.nix
deleted file mode 100644
index 2adc2ee1eeea3..0000000000000
--- a/nixos/tests/blivet.nix
+++ /dev/null
@@ -1,87 +0,0 @@
-import ./make-test.nix ({ pkgs, ... }: with pkgs.python2Packages; rec {
-  name = "blivet";
-  meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ aszlig ];
-  };
-
-  machine = {
-    environment.systemPackages = [ pkgs.python blivet mock ];
-    boot.supportedFilesystems = [ "btrfs" "jfs" "reiserfs" "xfs" ];
-    virtualisation.memorySize = 768;
-  };
-
-  debugBlivet       = false;
-  debugProgramCalls = false;
-
-  pythonTestRunner = pkgs.writeText "run-blivet-tests.py" ''
-    import sys
-    import logging
-
-    from unittest import TestLoader
-    from unittest.runner import TextTestRunner
-
-    ${pkgs.lib.optionalString debugProgramCalls ''
-      blivet_program_log = logging.getLogger("program")
-      blivet_program_log.setLevel(logging.DEBUG)
-      blivet_program_log.addHandler(logging.StreamHandler(sys.stderr))
-    ''}
-
-    ${pkgs.lib.optionalString debugBlivet ''
-      blivet_log = logging.getLogger("blivet")
-      blivet_log.setLevel(logging.DEBUG)
-      blivet_log.addHandler(logging.StreamHandler(sys.stderr))
-    ''}
-
-    runner = TextTestRunner(verbosity=2, failfast=False, buffer=False)
-    result = runner.run(TestLoader().discover('tests/', pattern='*_test.py'))
-    sys.exit(not result.wasSuccessful())
-  '';
-
-  blivetTest = pkgs.writeScript "blivet-test.sh" ''
-    #!${pkgs.stdenv.shell} -e
-
-    # Use the hosts temporary directory, because we have a tmpfs within the VM
-    # and we don't want to increase the memory size of the VM for no reason.
-    mkdir -p /tmp/xchg/bigtmp
-    TMPDIR=/tmp/xchg/bigtmp
-    export TMPDIR
-
-    cp -Rd "${blivet.src}/tests" .
-
-    # Skip SELinux tests
-    rm -f tests/formats_test/selinux_test.py
-
-    # Race conditions in growing/shrinking during resync
-    rm -f tests/devicelibs_test/mdraid_*
-
-    # Deactivate small BTRFS device test, because it fails with newer btrfsprogs
-    sed -i -e '/^class *BTRFSAsRootTestCase3(/,/^[^ ]/ {
-      /^class *BTRFSAsRootTestCase3(/d
-      /^$/d
-      /^ /d
-    }' tests/devicelibs_test/btrfs_test.py
-
-    # How on earth can these tests ever work even upstream? O_o
-    sed -i -e '/def testDiskChunk[12]/,/^ *[^ ]/{n; s/^ */&return # /}' \
-      tests/partitioning_test.py
-
-    # fix hardcoded temporary directory
-    sed -i \
-      -e '1i import tempfile' \
-      -e 's|_STORE_FILE_PATH = .*|_STORE_FILE_PATH = tempfile.gettempdir()|' \
-      -e 's|DEFAULT_STORE_SIZE = .*|DEFAULT_STORE_SIZE = 409600|' \
-      tests/loopbackedtestcase.py
-
-    PYTHONPATH=".:$(< "${pkgs.stdenv.mkDerivation {
-      name = "blivet-pythonpath";
-      buildInputs = [ blivet mock ];
-      buildCommand = "echo \"$PYTHONPATH\" > \"$out\"";
-    }}")" python "${pythonTestRunner}"
-  '';
-
-  testScript = ''
-    $machine->waitForUnit("multi-user.target");
-    $machine->succeed("${blivetTest}");
-    $machine->execute("rm -rf /tmp/xchg/bigtmp");
-  '';
-})
diff --git a/nixos/tests/buildbot.nix b/nixos/tests/buildbot.nix
index f5c8c4863b6f1..5655a34a8b513 100644
--- a/nixos/tests/buildbot.nix
+++ b/nixos/tests/buildbot.nix
@@ -1,12 +1,11 @@
+# Test ensures buildbot master comes up correctly and workers can connect
+
 { system ? builtins.currentSystem,
   config ? {},
   pkgs ? import ../.. { inherit system config; }
 }:
 
-with import ../lib/testing.nix { inherit system pkgs; };
-
-# Test ensures buildbot master comes up correctly and workers can connect
-makeTest {
+import ./make-test-python.nix {
   name = "buildbot";
 
   nodes = {
@@ -39,75 +38,76 @@ makeTest {
       services.openssh.enable = true;
       networking.firewall.allowedTCPPorts = [ 22 9418 ];
       environment.systemPackages = with pkgs; [ git ];
+      systemd.services.git-daemon = {
+        description   = "Git daemon for the test";
+        wantedBy      = [ "multi-user.target" ];
+        after         = [ "network.target" ];
+
+        serviceConfig.Restart = "always";
+        path = with pkgs; [ coreutils git openssh ];
+        environment = { HOME = "/root"; };
+        preStart = ''
+          git config --global user.name 'Nobody Fakeuser'
+          git config --global user.email 'nobody\@fakerepo.com'
+          rm -rvf /srv/repos/fakerepo.git /tmp/fakerepo
+          mkdir -pv /srv/repos/fakerepo ~/.ssh
+          ssh-keyscan -H gitrepo > ~/.ssh/known_hosts
+          cat ~/.ssh/known_hosts
+
+          mkdir -p /src/repos/fakerepo
+          cd /srv/repos/fakerepo
+          rm -rf *
+          git init
+          echo -e '#!/bin/sh\necho fakerepo' > fakerepo.sh
+          cat fakerepo.sh
+          touch .git/git-daemon-export-ok
+          git add fakerepo.sh .git/git-daemon-export-ok
+          git commit -m fakerepo
+        '';
+        script = ''
+          git daemon --verbose --export-all --base-path=/srv/repos --reuseaddr
+        '';
+      };
     };
   };
 
   testScript = ''
-    #Start up and populate fake repo
-    $gitrepo->waitForUnit("multi-user.target");
-    print($gitrepo->execute(" \
-      git config --global user.name 'Nobody Fakeuser' && \
-      git config --global user.email 'nobody\@fakerepo.com' && \
-      rm -rvf /srv/repos/fakerepo.git /tmp/fakerepo && \
-      mkdir -pv /srv/repos/fakerepo ~/.ssh && \
-      ssh-keyscan -H gitrepo > ~/.ssh/known_hosts && \
-      cat ~/.ssh/known_hosts && \
-      cd /srv/repos/fakerepo && \
-      git init && \
-      echo -e '#!/bin/sh\necho fakerepo' > fakerepo.sh && \
-      cat fakerepo.sh && \
-      touch .git/git-daemon-export-ok && \
-      git add fakerepo.sh .git/git-daemon-export-ok && \
-      git commit -m fakerepo && \
-      git daemon --verbose --export-all --base-path=/srv/repos --reuseaddr & \
-    "));
-
-    # Test gitrepo
-    $bbmaster->waitForUnit("network-online.target");
-    #$bbmaster->execute("nc -z gitrepo 9418");
-    print($bbmaster->execute(" \
-      rm -rfv /tmp/fakerepo && \
-      git clone git://gitrepo/fakerepo /tmp/fakerepo && \
-      pwd && \
-      ls -la && \
-      ls -la /tmp/fakerepo \
-    "));
-
-    # Test start master and connect worker
-    $bbmaster->waitForUnit("buildbot-master.service");
-    $bbmaster->waitUntilSucceeds("curl -s --head http://bbmaster:8010") =~ /200 OK/;
-    $bbworker->waitForUnit("network-online.target");
-    $bbworker->execute("nc -z bbmaster 8010");
-    $bbworker->execute("nc -z bbmaster 9989");
-    $bbworker->waitForUnit("buildbot-worker.service");
-    print($bbworker->execute("ls -la /home/bbworker/worker"));
-
+    gitrepo.wait_for_unit("git-daemon.service")
+    gitrepo.wait_for_unit("multi-user.target")
 
-    # Test stop buildbot master and worker
-    print($bbmaster->execute(" \
-      systemctl -l --no-pager status buildbot-master && \
-      systemctl stop buildbot-master \
-    "));
-    $bbworker->fail("nc -z bbmaster 8010");
-    $bbworker->fail("nc -z bbmaster 9989");
-    print($bbworker->execute(" \
-      systemctl -l --no-pager status buildbot-worker && \
-      systemctl stop buildbot-worker && \
-      ls -la /home/bbworker/worker \
-    "));
+    with subtest("Repo is accessible via git daemon"):
+        bbmaster.wait_for_unit("network-online.target")
+        bbmaster.succeed("rm -rfv /tmp/fakerepo")
+        bbmaster.succeed("git clone git://gitrepo/fakerepo /tmp/fakerepo")
 
+    with subtest("Master service and worker successfully connect"):
+        bbmaster.wait_for_unit("buildbot-master.service")
+        bbmaster.wait_until_succeeds("curl --fail -s --head http://bbmaster:8010")
+        bbworker.wait_for_unit("network-online.target")
+        bbworker.succeed("nc -z bbmaster 8010")
+        bbworker.succeed("nc -z bbmaster 9989")
+        bbworker.wait_for_unit("buildbot-worker.service")
 
-    # Test buildbot daemon mode
-    $bbmaster->execute("buildbot create-master /tmp");
-    $bbmaster->execute("mv -fv /tmp/master.cfg.sample /tmp/master.cfg");
-    $bbmaster->execute("sed -i 's/8010/8011/' /tmp/master.cfg");
-    $bbmaster->execute("buildbot start /tmp");
-    $bbworker->execute("nc -z bbmaster 8011");
-    $bbworker->waitUntilSucceeds("curl -s --head http://bbmaster:8011") =~ /200 OK/;
-    $bbmaster->execute("buildbot stop /tmp");
-    $bbworker->fail("nc -z bbmaster 8011");
+    with subtest("Stop buildbot worker"):
+        bbmaster.succeed("systemctl -l --no-pager status buildbot-master")
+        bbmaster.succeed("systemctl stop buildbot-master")
+        bbworker.fail("nc -z bbmaster 8010")
+        bbworker.fail("nc -z bbmaster 9989")
+        bbworker.succeed("systemctl -l --no-pager status buildbot-worker")
+        bbworker.succeed("systemctl stop buildbot-worker")
 
+    with subtest("Buildbot daemon mode works"):
+        bbmaster.succeed(
+            "buildbot create-master /tmp",
+            "mv -fv /tmp/master.cfg.sample /tmp/master.cfg",
+            "sed -i 's/8010/8011/' /tmp/master.cfg",
+            "buildbot start /tmp",
+            "nc -z bbmaster 8011",
+        )
+        bbworker.wait_until_succeeds("curl --fail -s --head http://bbmaster:8011")
+        bbmaster.wait_until_succeeds("buildbot stop /tmp")
+        bbworker.fail("nc -z bbmaster 8011")
   '';
 
   meta.maintainers = with pkgs.stdenv.lib.maintainers; [ nand0p ];
-}
+} {}
diff --git a/nixos/tests/chromium.nix b/nixos/tests/chromium.nix
index a5531d112e3c6..3844255bd8af9 100644
--- a/nixos/tests/chromium.nix
+++ b/nixos/tests/chromium.nix
@@ -23,7 +23,7 @@ mapAttrs (channel: chromiumPkg: makeTest rec {
 
   machine.imports = [ ./common/user-account.nix ./common/x11.nix ];
   machine.virtualisation.memorySize = 2047;
-  machine.services.xserver.displayManager.auto.user = "alice";
+  machine.test-support.displayManager.auto.user = "alice";
   machine.environment.systemPackages = [ chromiumPkg ];
 
   startupHTML = pkgs.writeText "chromium-startup.html" ''
diff --git a/nixos/modules/services/x11/display-managers/auto.nix b/nixos/tests/common/auto.nix
index 1068a344e0cfb..2c21a8d516737 100644
--- a/nixos/modules/services/x11/display-managers/auto.nix
+++ b/nixos/tests/common/auto.nix
@@ -5,7 +5,7 @@ with lib;
 let
 
   dmcfg = config.services.xserver.displayManager;
-  cfg = dmcfg.auto;
+  cfg = config.test-support.displayManager.auto;
 
 in
 
@@ -15,7 +15,7 @@ in
 
   options = {
 
-    services.xserver.displayManager.auto = {
+    test-support.displayManager.auto = {
 
       enable = mkOption {
         default = false;
diff --git a/nixos/tests/common/ec2.nix b/nixos/tests/common/ec2.nix
index 1e69b63191a70..ba087bb600905 100644
--- a/nixos/tests/common/ec2.nix
+++ b/nixos/tests/common/ec2.nix
@@ -25,7 +25,7 @@ with pkgs.lib;
           my $imageDir = ($ENV{'TMPDIR'} // "/tmp") . "/vm-state-machine";
           mkdir $imageDir, 0700;
           my $diskImage = "$imageDir/machine.qcow2";
-          system("qemu-img create -f qcow2 -o backing_file=${image}/nixos.qcow2 $diskImage") == 0 or die;
+          system("qemu-img create -f qcow2 -o backing_file=${image} $diskImage") == 0 or die;
           system("qemu-img resize $diskImage 10G") == 0 or die;
 
           # Note: we use net=169.0.0.0/8 rather than
@@ -35,7 +35,7 @@ with pkgs.lib;
           # again when it deletes link-local addresses.) Ideally we'd
           # turn off the DHCP server, but qemu does not have an option
           # to do that.
-          my $startCommand = "qemu-kvm -m 768";
+          my $startCommand = "qemu-kvm -m 1024";
           $startCommand .= " -device virtio-net-pci,netdev=vlan0";
           $startCommand .= " -netdev 'user,id=vlan0,net=169.0.0.0/8,guestfwd=tcp:169.254.169.254:80-cmd:${pkgs.micro-httpd}/bin/micro_httpd ${metaData}'";
           $startCommand .= " -drive file=$diskImage,if=virtio,werror=report";
diff --git a/nixos/tests/common/x11.nix b/nixos/tests/common/x11.nix
index 5ad0ac20fac85..0d76a0e972ff5 100644
--- a/nixos/tests/common/x11.nix
+++ b/nixos/tests/common/x11.nix
@@ -1,9 +1,14 @@
 { lib, ... }:
 
-{ services.xserver.enable = true;
+{
+  imports = [
+    ./auto.nix
+  ];
+
+  services.xserver.enable = true;
 
   # Automatically log in.
-  services.xserver.displayManager.auto.enable = true;
+  test-support.displayManager.auto.enable = true;
 
   # Use IceWM as the window manager.
   # Don't use a desktop manager.
diff --git a/nixos/tests/dnscrypt-proxy.nix b/nixos/tests/dnscrypt-proxy2.nix
index 98153d5c90470..b614d912a9f4e 100644
--- a/nixos/tests/dnscrypt-proxy.nix
+++ b/nixos/tests/dnscrypt-proxy2.nix
@@ -1,5 +1,5 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
-  name = "dnscrypt-proxy";
+  name = "dnscrypt-proxy2";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ joachifm ];
   };
@@ -13,9 +13,16 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     {
       security.apparmor.enable = true;
 
-      services.dnscrypt-proxy.enable = true;
-      services.dnscrypt-proxy.localPort = localProxyPort;
-      services.dnscrypt-proxy.extraArgs = [ "-X libdcplugin_example.so" ];
+      services.dnscrypt-proxy2.enable = true;
+      services.dnscrypt-proxy2.settings = {
+        listen_addresses = [ "127.0.0.1:${toString localProxyPort}" ];
+        sources.public-resolvers = {
+          urls = [ "https://download.dnscrypt.info/resolvers-list/v2/public-resolvers.md" ];
+          cache_file = "public-resolvers.md";
+          minisign_key = "RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3";
+          refresh_delay = 72;
+        };
+      };
 
       services.dnsmasq.enable = true;
       services.dnsmasq.servers = [ "127.0.0.1#${toString localProxyPort}" ];
@@ -24,12 +31,6 @@ import ./make-test-python.nix ({ pkgs, ... }: {
 
   testScript = ''
     client.wait_for_unit("dnsmasq")
-
-    # The daemon is socket activated; sending a single ping should activate it.
-    client.fail("systemctl is-active dnscrypt-proxy")
-    client.execute(
-        "${pkgs.iputils}/bin/ping -c1 example.com"
-    )
-    client.wait_until_succeeds("systemctl is-active dnscrypt-proxy")
+    client.wait_for_unit("dnscrypt-proxy2")
   '';
 })
diff --git a/nixos/tests/docker-containers.nix b/nixos/tests/docker-containers.nix
index 9725527352020..9be9bfa80ce0c 100644
--- a/nixos/tests/docker-containers.nix
+++ b/nixos/tests/docker-containers.nix
@@ -1,9 +1,11 @@
 # Test Docker containers as systemd units
 
-import ./make-test.nix ({ pkgs, lib, ... }: {
+import ./make-test.nix ({ pkgs, lib, ... }:
+
+{
   name = "docker-containers";
   meta = {
-    maintainers = with lib.maintainers; [ benley ];
+    maintainers = with lib.maintainers; [ benley mkaito ];
   };
 
   nodes = {
@@ -11,10 +13,9 @@ import ./make-test.nix ({ pkgs, lib, ... }: {
       {
         virtualisation.docker.enable = true;
 
-        virtualisation.dockerPreloader.images = [ pkgs.dockerTools.examples.nginx ];
-
         docker-containers.nginx = {
           image = "nginx-container";
+          imageFile = pkgs.dockerTools.examples.nginx;
           ports = ["8181:80"];
         };
       };
diff --git a/nixos/tests/docker-tools.nix b/nixos/tests/docker-tools.nix
index 9ab1a71f3314a..07fac5336803a 100644
--- a/nixos/tests/docker-tools.nix
+++ b/nixos/tests/docker-tools.nix
@@ -80,5 +80,8 @@ import ./make-test.nix ({ pkgs, ... }: {
       # This is to be sure the order of layers of the parent image is preserved
       $docker->succeed("docker run --rm  ${pkgs.dockerTools.examples.layersOrder.imageName} cat /tmp/layer2 | grep -q layer2");
       $docker->succeed("docker run --rm  ${pkgs.dockerTools.examples.layersOrder.imageName} cat /tmp/layer3 | grep -q layer3");
+
+      # Ensure image with only 2 layers can be loaded
+      $docker->succeed("docker load --input='${pkgs.dockerTools.examples.two-layered-image}'");
     '';
 })
diff --git a/nixos/tests/dokuwiki.nix b/nixos/tests/dokuwiki.nix
new file mode 100644
index 0000000000000..38bde10f47edc
--- /dev/null
+++ b/nixos/tests/dokuwiki.nix
@@ -0,0 +1,29 @@
+import ./make-test-python.nix ({ lib, ... }:
+
+with lib;
+
+{
+  name = "dokuwiki";
+  meta.maintainers = with maintainers; [ maintainers."1000101" ];
+
+  nodes.machine =
+    { pkgs, ... }:
+    { services.dokuwiki = {
+        enable = true;
+        acl = " ";
+        superUser = null;
+        nginx = {
+          forceSSL = false;
+          enableACME = false;
+        };
+      }; 
+    };
+
+  testScript = ''
+    machine.start()
+    machine.wait_for_unit("phpfpm-dokuwiki.service")
+    machine.wait_for_unit("nginx.service")
+    machine.wait_for_open_port(80)
+    machine.succeed("curl -sSfL http://localhost/ | grep 'DokuWiki'")
+  '';
+})
diff --git a/nixos/tests/ec2.nix b/nixos/tests/ec2.nix
index c649ce852dad5..6aeeb17ba31ad 100644
--- a/nixos/tests/ec2.nix
+++ b/nixos/tests/ec2.nix
@@ -9,7 +9,7 @@ with pkgs.lib;
 with import common/ec2.nix { inherit makeTest pkgs; };
 
 let
-  image =
+  imageCfg =
     (import ../lib/eval-config.nix {
       inherit system;
       modules = [
@@ -26,20 +26,32 @@ let
             '';
 
           # Needed by nixos-rebuild due to the lack of network
-          # access. Mostly copied from
-          # modules/profiles/installation-device.nix.
+          # access. Determined by trial and error.
           system.extraDependencies =
-            with pkgs; [
-              stdenv busybox perlPackages.ArchiveCpio unionfs-fuse mkinitcpio-nfs-utils
-
-              # These are used in the configure-from-userdata tests for EC2. Httpd and valgrind are requested
-              # directly by the configuration we set, and libxslt.bin is used indirectly as a build dependency
-              # of the derivation for dbus configuration files.
-              apacheHttpd valgrind.doc libxslt.bin
-            ];
+            with pkgs; (
+              [
+                # Needed for a nixos-rebuild.
+                busybox
+                stdenv
+                stdenvNoCC
+                mkinitcpio-nfs-utils
+                unionfs-fuse
+                cloud-utils
+                desktop-file-utils
+                texinfo
+                libxslt.bin
+                xorg.lndir
+
+                # These are used in the configure-from-userdata tests
+                # for EC2. Httpd and valgrind are requested by the
+                # configuration.
+                apacheHttpd apacheHttpd.doc apacheHttpd.man valgrind.doc
+              ]
+            );
         }
       ];
-    }).config.system.build.amazonImage;
+    }).config;
+  image = "${imageCfg.system.build.amazonImage}/${imageCfg.amazonImage.name}.vhd";
 
   sshKeys = import ./ssh-keys.nix pkgs;
   snakeOilPrivateKey = sshKeys.snakeOilPrivateKey.text;
@@ -110,16 +122,23 @@ in {
           text = "whoa";
         };
 
+        networking.hostName = "ec2-test-vm"; # required by services.httpd
+
         services.httpd = {
           enable = true;
           adminAddr = "test@example.org";
-          virtualHosts.localhost.documentRoot = "${pkgs.valgrind.doc}/share/doc/valgrind/html";
+          virtualHosts.localhost.documentRoot = "''${pkgs.valgrind.doc}/share/doc/valgrind/html";
         };
         networking.firewall.allowedTCPPorts = [ 80 ];
       }
     '';
     script = ''
       $machine->start;
+
+      # amazon-init must succeed. if it fails, make the test fail
+      # immediately instead of timing out in waitForFile.
+      $machine->waitForUnit('amazon-init.service');
+
       $machine->waitForFile("/etc/testFile");
       $machine->succeed("cat /etc/testFile | grep -q 'whoa'");
 
diff --git a/nixos/tests/freeswitch.nix b/nixos/tests/freeswitch.nix
new file mode 100644
index 0000000000000..349d0e7bc6f0d
--- /dev/null
+++ b/nixos/tests/freeswitch.nix
@@ -0,0 +1,29 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "freeswitch";
+  meta = with pkgs.stdenv.lib.maintainers; {
+    maintainers = [ misuzu ];
+  };
+  nodes = {
+    node0 = { config, lib, ... }: {
+      networking.useDHCP = false;
+      networking.interfaces.eth1 = {
+        ipv4.addresses = [
+          {
+            address = "192.168.0.1";
+            prefixLength = 24;
+          }
+        ];
+      };
+      services.freeswitch = {
+        enable = true;
+        enableReload = true;
+        configTemplate = "${config.services.freeswitch.package}/share/freeswitch/conf/minimal";
+      };
+    };
+  };
+  testScript = ''
+    node0.wait_for_unit("freeswitch.service")
+    # Wait for SIP port to be open
+    node0.wait_for_open_port("5060")
+  '';
+})
diff --git a/nixos/tests/gnome3.nix b/nixos/tests/gnome3.nix
index ab363efb6a197..486c146d8dc39 100644
--- a/nixos/tests/gnome3.nix
+++ b/nixos/tests/gnome3.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ...} : {
+import ./make-test-python.nix ({ pkgs, ...} : {
   name = "gnome3";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = pkgs.gnome3.maintainers;
@@ -24,41 +24,53 @@ import ./make-test.nix ({ pkgs, ...} : {
       virtualisation.memorySize = 1024;
     };
 
-  testScript = let
+  testScript = { nodes, ... }: let
     # Keep line widths somewhat managable
-    bus = "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus";
+    user = nodes.machine.config.users.users.alice;
+    uid = toString user.uid;
+    bus = "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/${uid}/bus";
     gdbus = "${bus} gdbus";
+    su = command: "su - ${user.name} -c '${command}'";
+
     # Call javascript in gnome shell, returns a tuple (success, output), where
     # `success` is true if the dbus call was successful and output is what the
     # javascript evaluates to.
     eval = "call --session -d org.gnome.Shell -o /org/gnome/Shell -m org.gnome.Shell.Eval";
-    # False when startup is done
-    startingUp = "${gdbus} ${eval} Main.layoutManager._startingUp";
-    # Hopefully gnome-terminal's wm class
-    wmClass = "${gdbus} ${eval} global.display.focus_window.wm_class";
-  in ''
-      # wait for gdm to start
-      $machine->waitForUnit("display-manager.service");
 
-      # wait for alice to be logged in
-      $machine->waitForUnit("default.target","alice");
-
-      # Check that logging in has given the user ownership of devices.
-      $machine->succeed("getfacl -p /dev/snd/timer | grep -q alice");
+    # False when startup is done
+    startingUp = su "${gdbus} ${eval} Main.layoutManager._startingUp";
 
-      # Wait for the wayland server
-      $machine->waitForFile("/run/user/1000/wayland-0");
+    # Start gnome-terminal
+    gnomeTerminalCommand = su "${bus} gnome-terminal";
 
-      # Wait for gnome shell, correct output should be "(true, 'false')"
-      $machine->waitUntilSucceeds("su - alice -c '${startingUp} | grep -q true,..false'");
+    # Hopefully gnome-terminal's wm class
+    wmClass = su "${gdbus} ${eval} global.display.focus_window.wm_class";
+  in ''
+      with subtest("Login to GNOME with GDM"):
+          # wait for gdm to start
+          machine.wait_for_unit("display-manager.service")
+          # wait for the wayland server
+          machine.wait_for_file("/run/user/${uid}/wayland-0")
+          # wait for alice to be logged in
+          machine.wait_for_unit("default.target", "${user.name}")
+          # check that logging in has given the user ownership of devices
+          assert "alice" in machine.succeed("getfacl -p /dev/snd/timer")
 
-      # open a terminal
-      $machine->succeed("su - alice -c '${bus} gnome-terminal'");
-      # and check it's there
-      $machine->waitUntilSucceeds("su - alice -c '${wmClass} | grep -q gnome-terminal-server'");
+      with subtest("Wait for GNOME Shell"):
+          # correct output should be (true, 'false')
+          machine.wait_until_succeeds(
+              "${startingUp} | grep -q 'true,..false'"
+          )
 
-      # wait to get a nice screenshot
-      $machine->sleep(20);
-      $machine->screenshot("screen");
+      with subtest("Open Gnome Terminal"):
+          machine.succeed(
+              "${gnomeTerminalCommand}"
+          )
+          # correct output should be (true, '"gnome-terminal-server"')
+          machine.wait_until_succeeds(
+              "${wmClass} | grep -q 'gnome-terminal-server'"
+          )
+          machine.sleep(20)
+          machine.screenshot("screen")
     '';
 })
diff --git a/nixos/tests/graphite.nix b/nixos/tests/graphite.nix
index 27a87bdbb9f29..ba3c73bb878d7 100644
--- a/nixos/tests/graphite.nix
+++ b/nixos/tests/graphite.nix
@@ -1,6 +1,11 @@
-import ./make-test.nix ({ pkgs, ... } :
+import ./make-test-python.nix ({ pkgs, ... } :
 {
   name = "graphite";
+  meta = {
+    # Fails on dependency `python-2.7-Twisted`'s test suite
+    # complaining `ImportError: No module named zope.interface`.
+    broken = true;
+  };
   nodes = {
     one =
       { ... }: {
@@ -22,20 +27,20 @@ import ./make-test.nix ({ pkgs, ... } :
   };
 
   testScript = ''
-    startAll;
-    $one->waitForUnit("default.target");
-    $one->waitForUnit("graphiteWeb.service");
-    $one->waitForUnit("graphiteApi.service");
-    $one->waitForUnit("graphitePager.service");
-    $one->waitForUnit("graphite-beacon.service");
-    $one->waitForUnit("carbonCache.service");
-    $one->waitForUnit("seyren.service");
+    start_all()
+    one.wait_for_unit("default.target")
+    one.wait_for_unit("graphiteWeb.service")
+    one.wait_for_unit("graphiteApi.service")
+    one.wait_for_unit("graphitePager.service")
+    one.wait_for_unit("graphite-beacon.service")
+    one.wait_for_unit("carbonCache.service")
+    one.wait_for_unit("seyren.service")
     # The services above are of type "simple". systemd considers them active immediately
     # even if they're still in preStart (which takes quite long for graphiteWeb).
     # Wait for ports to open so we're sure the services are up and listening.
-    $one->waitForOpenPort(8080);
-    $one->waitForOpenPort(2003);
-    $one->succeed("echo \"foo 1 `date +%s`\" | nc -N localhost 2003");
-    $one->waitUntilSucceeds("curl 'http://localhost:8080/metrics/find/?query=foo&format=treejson' --silent | grep foo >&2");
+    one.wait_for_open_port(8080)
+    one.wait_for_open_port(2003)
+    one.succeed('echo "foo 1 `date +%s`" | nc -N localhost 2003')
+    one.wait_until_succeeds("curl 'http://localhost:8080/metrics/find/?query=foo&format=treejson' --silent | grep foo >&2")
   '';
 })
diff --git a/nixos/tests/i3wm.nix b/nixos/tests/i3wm.nix
index 126178d118790..b527aa706ad21 100644
--- a/nixos/tests/i3wm.nix
+++ b/nixos/tests/i3wm.nix
@@ -6,7 +6,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
 
   machine = { lib, ... }: {
     imports = [ ./common/x11.nix ./common/user-account.nix ];
-    services.xserver.displayManager.auto.user = "alice";
+    test-support.displayManager.auto.user = "alice";
     services.xserver.displayManager.defaultSession = lib.mkForce "none+i3";
     services.xserver.windowManager.i3.enable = true;
   };
diff --git a/nixos/tests/ihatemoney.nix b/nixos/tests/ihatemoney.nix
index 14db17fe5e676..7df0ea0b691fc 100644
--- a/nixos/tests/ihatemoney.nix
+++ b/nixos/tests/ihatemoney.nix
@@ -1,13 +1,5 @@
-{ system ? builtins.currentSystem
-, config ? {}
-, pkgs ? import ../.. { inherit system config; }
-}:
-
 let
-  inherit (import ../lib/testing.nix { inherit system pkgs; }) makeTest;
-in
-map (
-  backend: makeTest {
+  f = backend: import ./make-test-python.nix ({ pkgs, ... }: {
     name = "ihatemoney-${backend}";
     machine = { lib, ... }: {
       services.ihatemoney = {
@@ -30,23 +22,34 @@ map (
       };
     };
     testScript = ''
-      $machine->waitForOpenPort(8000);
-      $machine->waitForUnit("uwsgi.service");
-      my $return = $machine->succeed("curl -X POST http://localhost:8000/api/projects -d 'name=yay&id=yay&password=yay&contact_email=yay\@example.com'");
-      die "wrong project id $return" unless "\"yay\"\n" eq $return;
-      my $timestamp = $machine->succeed("stat --printf %Y /var/lib/ihatemoney/secret_key");
-      my $owner = $machine->succeed("stat --printf %U:%G /var/lib/ihatemoney/secret_key");
-      die "wrong ownership for the secret key: $owner, is uwsgi running as the right user ?" unless $owner eq "ihatemoney:ihatemoney";
-      $machine->shutdown();
-      $machine->start();
-      $machine->waitForOpenPort(8000);
-      $machine->waitForUnit("uwsgi.service");
-      # check that the database is really persistent
-      print $machine->succeed("curl --basic -u yay:yay http://localhost:8000/api/projects/yay");
-      # check that the secret key is really persistent
-      my $timestamp2 = $machine->succeed("stat --printf %Y /var/lib/ihatemoney/secret_key");
-      die unless $timestamp eq $timestamp2;
-      $machine->succeed("curl http://localhost:8000 | grep ihatemoney");
+      machine.wait_for_open_port(8000)
+      machine.wait_for_unit("uwsgi.service")
+
+      assert '"yay"' in machine.succeed(
+          "curl -X POST http://localhost:8000/api/projects -d 'name=yay&id=yay&password=yay&contact_email=yay\@example.com'"
+      )
+      owner, timestamp = machine.succeed(
+          "stat --printf %U:%G___%Y /var/lib/ihatemoney/secret_key"
+      ).split("___")
+      assert "ihatemoney:ihatemoney" == owner
+
+      with subtest("Restart machine and service"):
+          machine.shutdown()
+          machine.start()
+          machine.wait_for_open_port(8000)
+          machine.wait_for_unit("uwsgi.service")
+
+      with subtest("check that the database is really persistent"):
+          machine.succeed("curl --basic -u yay:yay http://localhost:8000/api/projects/yay")
+
+      with subtest("check that the secret key is really persistent"):
+          timestamp2 = machine.succeed("stat --printf %Y /var/lib/ihatemoney/secret_key")
+          assert timestamp == timestamp2
+
+      assert "ihatemoney" in machine.succeed("curl http://localhost:8000")
     '';
-  }
-) [ "sqlite" "postgresql" ]
+  });
+in {
+  ihatemoney-sqlite = f "sqlite";
+  ihatemoney-postgresql = f "postgresql";
+}
diff --git a/nixos/tests/installer.nix b/nixos/tests/installer.nix
index eb1f4f192dd11..024445b01b6c0 100644
--- a/nixos/tests/installer.nix
+++ b/nixos/tests/installer.nix
@@ -74,7 +74,7 @@ let
       # FIXME don't duplicate the -enable-kvm etc. flags here yet again!
       qemuFlags =
         (if system == "x86_64-linux" then "-m 768 " else "-m 512 ") +
-        (optionalString (system == "x86_64-linux") "-cpu kvm64 ") +
+        (optionalString (system == "x86_64-linux") "-cpu host ") +
         (optionalString (system == "aarch64-linux") "-enable-kvm -machine virt,gic-version=host -cpu host ");
 
       hdFlags = ''hda => "vm-state-machine/machine.qcow2", hdaInterface => "${iface}", ''
diff --git a/nixos/tests/keymap.nix b/nixos/tests/keymap.nix
index 2b4c1ab7b0529..09d5d2a6c9e14 100644
--- a/nixos/tests/keymap.nix
+++ b/nixos/tests/keymap.nix
@@ -3,14 +3,13 @@
   pkgs ? import ../.. { inherit system config; }
 }:
 
-with import ../lib/testing.nix { inherit system pkgs; };
+with import ../lib/testing-python.nix { inherit system pkgs; };
 
 let
   readyFile  = "/tmp/readerReady";
   resultFile = "/tmp/readerResult";
 
   testReader = pkgs.writeScript "test-input-reader" ''
-    #!${pkgs.stdenv.shell}
     rm -f ${resultFile} ${resultFile}.tmp
     logger "testReader: START: Waiting for $1 characters, expecting '$2'."
     touch ${readyFile}
@@ -27,56 +26,75 @@ let
   '';
 
 
-  mkKeyboardTest = layout: { extraConfig ? {}, tests }: with pkgs.lib; let
-    combinedTests = foldAttrs (acc: val: acc ++ val) [] (builtins.attrValues tests);
-    perlStr = val: "'${escape ["'" "\\"] val}'";
-    lq = length combinedTests.qwerty;
-    le = length combinedTests.expect;
-    msg = "length mismatch between qwerty (${toString lq}) and expect (${toString le}) lists!";
-    send   = concatMapStringsSep ", " perlStr combinedTests.qwerty;
-    expect = if (lq == le) then concatStrings combinedTests.expect else throw msg;
-
-  in makeTest {
+  mkKeyboardTest = layout: { extraConfig ? {}, tests }: with pkgs.lib; makeTest {
     name = "keymap-${layout}";
 
+    machine.console.keyMap = mkOverride 900 layout;
     machine.services.xserver.desktopManager.xterm.enable = false;
-    machine.i18n.consoleKeyMap = mkOverride 900 layout;
     machine.services.xserver.layout = mkOverride 900 layout;
     machine.imports = [ ./common/x11.nix extraConfig ];
 
     testScript = ''
-
-      sub mkTest ($$) {
-        my ($desc, $cmd) = @_;
-
-        subtest $desc, sub {
-          # prepare and start testReader
-          $machine->execute("rm -f ${readyFile} ${resultFile}");
-          $machine->succeed("$cmd ${testReader} ${toString le} ".q(${escapeShellArg expect} & ));
-
-          if ($desc eq "Xorg keymap") {
-            # make sure the xterm window is open and has focus
-            $machine->waitForWindow(qr/testterm/);
-            $machine->waitUntilSucceeds("${pkgs.xdotool}/bin/xdotool search --sync --onlyvisible --class testterm windowfocus --sync");
-          }
-
-          # wait for reader to be ready
-          $machine->waitForFile("${readyFile}");
-          $machine->sleep(1);
-
-          # send all keys
-          foreach ((${send})) { $machine->sendKeys($_); };
-
-          # wait for result and check
-          $machine->waitForFile("${resultFile}");
-          $machine->succeed("grep -q 'PASS:' ${resultFile}");
-        };
-      };
-
-      $machine->waitForX;
-
-      mkTest "VT keymap", "openvt -sw --";
-      mkTest "Xorg keymap", "DISPLAY=:0 xterm -title testterm -class testterm -fullscreen -e";
+      import json
+      import shlex
+
+
+      def run_test_case(cmd, xorg_keymap, test_case_name, inputs, expected):
+          with subtest(test_case_name):
+              assert len(inputs) == len(expected)
+              machine.execute("rm -f ${readyFile} ${resultFile}")
+
+              # set up process that expects all the keys to be entered
+              machine.succeed(
+                  "{} {} {} {} &".format(
+                      cmd,
+                      "${testReader}",
+                      len(inputs),
+                      shlex.quote("".join(expected)),
+                  )
+              )
+
+              if xorg_keymap:
+                  # make sure the xterm window is open and has focus
+                  machine.wait_for_window("testterm")
+                  machine.wait_until_succeeds(
+                      "${pkgs.xdotool}/bin/xdotool search --sync --onlyvisible "
+                      "--class testterm windowfocus --sync"
+                  )
+
+              # wait for reader to be ready
+              machine.wait_for_file("${readyFile}")
+              machine.sleep(1)
+
+              # send all keys
+              for key in inputs:
+                  machine.send_key(key)
+
+              # wait for result and check
+              machine.wait_for_file("${resultFile}")
+              machine.succeed("grep -q 'PASS:' ${resultFile}")
+
+
+      with open("${pkgs.writeText "tests.json" (builtins.toJSON tests)}") as json_file:
+          tests = json.load(json_file)
+
+      keymap_environments = {
+          "VT Keymap": "openvt -sw --",
+          "Xorg Keymap": "DISPLAY=:0 xterm -title testterm -class testterm -fullscreen -e",
+      }
+
+      machine.wait_for_x()
+
+      for keymap_env_name, command in keymap_environments.items():
+          with subtest(keymap_env_name):
+              for test_case_name, test_data in tests.items():
+                  run_test_case(
+                      command,
+                      False,
+                      test_case_name,
+                      test_data["qwerty"],
+                      test_data["expect"],
+                  )
     '';
   };
 
@@ -89,7 +107,7 @@ in pkgs.lib.mapAttrs mkKeyboardTest {
       altgr.expect = [ "~"       "#"       "{"       "["       "|"       ];
     };
 
-    extraConfig.i18n.consoleKeyMap = "azerty/fr";
+    extraConfig.console.keyMap = "azerty/fr";
     extraConfig.services.xserver.layout = "fr";
   };
 
@@ -99,7 +117,7 @@ in pkgs.lib.mapAttrs mkKeyboardTest {
       homerow.expect = [ "a" "r" "s" "t" "n" "e" "i" "o"         ];
     };
 
-    extraConfig.i18n.consoleKeyMap = "colemak/colemak";
+    extraConfig.console.keyMap = "colemak/colemak";
     extraConfig.services.xserver.layout = "us";
     extraConfig.services.xserver.xkbVariant = "colemak";
   };
@@ -151,7 +169,7 @@ in pkgs.lib.mapAttrs mkKeyboardTest {
       altgr.expect = [ "@" "|"    "{" "[" "]" "}" ];
     };
 
-    extraConfig.i18n.consoleKeyMap = "de";
+    extraConfig.console.keyMap = "de";
     extraConfig.services.xserver.layout = "de";
   };
 }
diff --git a/nixos/tests/limesurvey.nix b/nixos/tests/limesurvey.nix
index ad66ada106b71..7228fcb833155 100644
--- a/nixos/tests/limesurvey.nix
+++ b/nixos/tests/limesurvey.nix
@@ -1,21 +1,26 @@
-import ./make-test.nix ({ pkgs, ... }: {
+import ./make-test-python.nix ({ pkgs, ... }: {
   name = "limesurvey";
   meta.maintainers = [ pkgs.stdenv.lib.maintainers.aanderse ];
 
-  machine =
-    { ... }:
-    { services.limesurvey.enable = true;
-      services.limesurvey.virtualHost.hostName = "example.local";
-      services.limesurvey.virtualHost.adminAddr = "root@example.local";
-
-      # limesurvey won't work without a dot in the hostname
-      networking.hosts."127.0.0.1" = [ "example.local" ];
+  machine = { ... }: {
+    services.limesurvey = {
+      enable = true;
+      virtualHost = {
+        hostName = "example.local";
+        adminAddr = "root@example.local";
+      };
     };
 
+    # limesurvey won't work without a dot in the hostname
+    networking.hosts."127.0.0.1" = [ "example.local" ];
+  };
+
   testScript = ''
-    startAll;
+    start_all()
 
-    $machine->waitForUnit('phpfpm-limesurvey.service');
-    $machine->succeed('curl http://example.local/') =~ /The following surveys are available/ or die;
+    machine.wait_for_unit("phpfpm-limesurvey.service")
+    assert "The following surveys are available" in machine.succeed(
+        "curl http://example.local/"
+    )
   '';
 })
diff --git a/nixos/tests/misc.nix b/nixos/tests/misc.nix
index ca28bc31cf1c2..17260ce640676 100644
--- a/nixos/tests/misc.nix
+++ b/nixos/tests/misc.nix
@@ -1,6 +1,6 @@
 # Miscellaneous small tests that don't warrant their own VM run.
 
-import ./make-test.nix ({ pkgs, ...} : rec {
+import ./make-test-python.nix ({ pkgs, ...} : rec {
   name = "misc";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ eelco ];
@@ -34,109 +34,97 @@ import ./make-test.nix ({ pkgs, ...} : rec {
 
   testScript =
     ''
-      subtest "nix-db", sub {
-          my $json = $machine->succeed("nix path-info --json ${foo}");
-          $json =~ /"narHash":"sha256:0afw0d9j1hvwiz066z93jiddc33nxg6i6qyp26vnqyglpyfivlq5"/ or die "narHash not set";
-          $json =~ /"narSize":128/ or die "narSize not set";
-      };
+      import json
 
-      subtest "nixos-version", sub {
-          $machine->succeed("[ `nixos-version | wc -w` = 2 ]");
-      };
 
-      subtest "nixos-rebuild", sub {
-          $machine->succeed("nixos-rebuild --help | grep 'NixOS module' ");
-      };
+      def get_path_info(path):
+          result = machine.succeed(f"nix path-info --json {path}")
+          parsed = json.loads(result)
+          return parsed
 
-      # Sanity check for uid/gid assignment.
-      subtest "users-groups", sub {
-          $machine->succeed("[ `id -u messagebus` = 4 ]");
-          $machine->succeed("[ `id -g messagebus` = 4 ]");
-          $machine->succeed("[ `getent group users` = 'users:x:100:' ]");
-      };
 
-      # Regression test for GMP aborts on QEMU.
-      subtest "gmp", sub {
-          $machine->succeed("expr 1 + 2");
-      };
+      with subtest("nix-db"):
+          info = get_path_info("${foo}")
 
-      # Test that the swap file got created.
-      subtest "swapfile", sub {
-          $machine->waitForUnit("root-swapfile.swap");
-          $machine->succeed("ls -l /root/swapfile | grep 134217728");
-      };
+          if (
+              info[0]["narHash"]
+              != "sha256:0afw0d9j1hvwiz066z93jiddc33nxg6i6qyp26vnqyglpyfivlq5"
+          ):
+              raise Exception("narHash not set")
 
-      # Test whether kernel.poweroff_cmd is set.
-      subtest "poweroff_cmd", sub {
-          $machine->succeed("[ -x \"\$(cat /proc/sys/kernel/poweroff_cmd)\" ]")
-      };
+          if info[0]["narSize"] != 128:
+              raise Exception("narSize not set")
 
-      # Test whether the blkio controller is properly enabled.
-      subtest "blkio-cgroup", sub {
-          $machine->succeed("[ -n \"\$(cat /sys/fs/cgroup/blkio/blkio.sectors)\" ]")
-      };
+      with subtest("nixos-version"):
+          machine.succeed("[ `nixos-version | wc -w` = 2 ]")
 
-      # Test whether we have a reboot record in wtmp.
-      subtest "reboot-wtmp", sub {
-          $machine->shutdown;
-          $machine->waitForUnit('multi-user.target');
-          $machine->succeed("last | grep reboot >&2");
-      };
+      with subtest("nixos-rebuild"):
+          assert "NixOS module" in machine.succeed("nixos-rebuild --help")
 
-      # Test whether we can override environment variables.
-      subtest "override-env-var", sub {
-          $machine->succeed('[ "$EDITOR" = emacs ]');
-      };
+      with subtest("Sanity check for uid/gid assignment"):
+          assert "4" == machine.succeed("id -u messagebus").strip()
+          assert "4" == machine.succeed("id -g messagebus").strip()
+          assert "users:x:100:" == machine.succeed("getent group users").strip()
 
-      # Test whether hostname (and by extension nss_myhostname) works.
-      subtest "hostname", sub {
-          $machine->succeed('[ "`hostname`" = machine ]');
-          #$machine->succeed('[ "`hostname -s`" = machine ]');
-      };
+      with subtest("Regression test for GMP aborts on QEMU."):
+          machine.succeed("expr 1 + 2")
 
-      # Test whether systemd-udevd automatically loads modules for our hardware.
-      $machine->succeed("systemctl start systemd-udev-settle.service");
-      subtest "udev-auto-load", sub {
-          $machine->waitForUnit('systemd-udev-settle.service');
-          $machine->succeed('lsmod | grep mousedev');
-      };
+      with subtest("the swap file got created"):
+          machine.wait_for_unit("root-swapfile.swap")
+          machine.succeed("ls -l /root/swapfile | grep 134217728")
 
-      # Test whether systemd-tmpfiles-clean works.
-      subtest "tmpfiles", sub {
-          $machine->succeed('touch /tmp/foo');
-          $machine->succeed('systemctl start systemd-tmpfiles-clean');
-          $machine->succeed('[ -e /tmp/foo ]');
-          $machine->succeed('date -s "@$(($(date +%s) + 1000000))"'); # move into the future
-          $machine->succeed('systemctl start systemd-tmpfiles-clean');
-          $machine->fail('[ -e /tmp/foo ]');
-      };
+      with subtest("whether kernel.poweroff_cmd is set"):
+          machine.succeed('[ -x "$(cat /proc/sys/kernel/poweroff_cmd)" ]')
 
-      # Test whether automounting works.
-      subtest "automount", sub {
-          $machine->fail("grep '/tmp2 tmpfs' /proc/mounts");
-          $machine->succeed("touch /tmp2/x");
-          $machine->succeed("grep '/tmp2 tmpfs' /proc/mounts");
-      };
+      with subtest("whether the blkio controller is properly enabled"):
+          machine.succeed("[ -e /sys/fs/cgroup/blkio/blkio.reset_stats ]")
 
-      subtest "shell-vars", sub {
-          $machine->succeed('[ -n "$NIX_PATH" ]');
-      };
+      with subtest("whether we have a reboot record in wtmp"):
+          machine.shutdown
+          machine.wait_for_unit("multi-user.target")
+          machine.succeed("last | grep reboot >&2")
 
-      subtest "nix-db", sub {
-          $machine->succeed("nix-store -qR /run/current-system | grep nixos-");
-      };
+      with subtest("whether we can override environment variables"):
+          machine.succeed('[ "$EDITOR" = emacs ]')
 
-      # Test sysctl
-      subtest "sysctl", sub {
-          $machine->waitForUnit("systemd-sysctl.service");
-          $machine->succeed('[ `sysctl -ne vm.swappiness` = 1 ]');
-          $machine->execute('sysctl vm.swappiness=60');
-          $machine->succeed('[ `sysctl -ne vm.swappiness` = 60 ]');
-      };
+      with subtest("whether hostname (and by extension nss_myhostname) works"):
+          assert "machine" == machine.succeed("hostname").strip()
+          assert "machine" == machine.succeed("hostname -s").strip()
 
-      # Test boot parameters
-      subtest "bootparam", sub {
-          $machine->succeed('grep -Fq vsyscall=emulate /proc/cmdline');
-      };
+      with subtest("whether systemd-udevd automatically loads modules for our hardware"):
+          machine.succeed("systemctl start systemd-udev-settle.service")
+          machine.wait_for_unit("systemd-udev-settle.service")
+          assert "mousedev" in machine.succeed("lsmod")
+
+      with subtest("whether systemd-tmpfiles-clean works"):
+          machine.succeed(
+              "touch /tmp/foo", "systemctl start systemd-tmpfiles-clean", "[ -e /tmp/foo ]"
+          )
+          # move into the future
+          machine.succeed(
+              'date -s "@$(($(date +%s) + 1000000))"',
+              "systemctl start systemd-tmpfiles-clean",
+          )
+          machine.fail("[ -e /tmp/foo ]")
+
+      with subtest("whether automounting works"):
+          machine.fail("grep '/tmp2 tmpfs' /proc/mounts")
+          machine.succeed("touch /tmp2/x")
+          machine.succeed("grep '/tmp2 tmpfs' /proc/mounts")
+
+      with subtest("shell-vars"):
+          machine.succeed('[ -n "$NIX_PATH" ]')
+
+      with subtest("nix-db"):
+          machine.succeed("nix-store -qR /run/current-system | grep nixos-")
+
+      with subtest("Test sysctl"):
+          machine.wait_for_unit("systemd-sysctl.service")
+          assert "1" == machine.succeed("sysctl -ne vm.swappiness").strip()
+          machine.execute("sysctl vm.swappiness=60")
+          assert "60" == machine.succeed("sysctl -ne vm.swappiness").strip()
+
+      with subtest("Test boot parameters"):
+          assert "vsyscall=emulate" in machine.succeed("cat /proc/cmdline")
     '';
 })
diff --git a/nixos/tests/networking-proxy.nix b/nixos/tests/networking-proxy.nix
index ab908c96e5eea..bae9c66ed61a2 100644
--- a/nixos/tests/networking-proxy.nix
+++ b/nixos/tests/networking-proxy.nix
@@ -10,7 +10,7 @@ let default-config = {
 
         virtualisation.memorySize = 128;
       };
-in import ./make-test.nix ({ pkgs, ...} : {
+in import ./make-test-python.nix ({ pkgs, ...} : {
   name = "networking-proxy";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [  ];
@@ -66,46 +66,70 @@ in import ./make-test.nix ({ pkgs, ...} : {
 
   testScript =
     ''
-      startAll;
-
-      # no proxy at all
-      print $machine->execute("env | grep -i proxy");
-      print $machine->execute("su - alice -c 'env | grep -i proxy'");
-      $machine->mustFail("env | grep -i proxy");
-      $machine->mustFail("su - alice -c 'env | grep -i proxy'");
-
-      # Use a default proxy option
-      print $machine2->execute("env | grep -i proxy");
-      print $machine2->execute("su - alice -c 'env | grep -i proxy'");
-      $machine2->mustSucceed("env | grep -i proxy");
-      $machine2->mustSucceed("su - alice -c 'env | grep -i proxy'");
-
-      # explicitly set each proxy option
-      print $machine3->execute("env | grep -i proxy");
-      print $machine3->execute("su - alice -c 'env | grep -i proxy'");
-      $machine3->mustSucceed("env | grep -i http_proxy | grep 123");
-      $machine3->mustSucceed("env | grep -i https_proxy | grep 456");
-      $machine3->mustSucceed("env | grep -i rsync_proxy | grep 789");
-      $machine3->mustSucceed("env | grep -i ftp_proxy | grep 101112");
-      $machine3->mustSucceed("env | grep -i no_proxy | grep 131415");
-      $machine3->mustSucceed("su - alice -c 'env | grep -i http_proxy | grep 123'");
-      $machine3->mustSucceed("su - alice -c 'env | grep -i https_proxy | grep 456'");
-      $machine3->mustSucceed("su - alice -c 'env | grep -i rsync_proxy | grep 789'");
-      $machine3->mustSucceed("su - alice -c 'env | grep -i ftp_proxy | grep 101112'");
-      $machine3->mustSucceed("su - alice -c 'env | grep -i no_proxy | grep 131415'");
-
-      # set default proxy option + some other specifics
-      print $machine4->execute("env | grep -i proxy");
-      print $machine4->execute("su - alice -c 'env | grep -i proxy'");
-      $machine4->mustSucceed("env | grep -i http_proxy | grep 000");
-      $machine4->mustSucceed("env | grep -i https_proxy | grep 000");
-      $machine4->mustSucceed("env | grep -i rsync_proxy | grep 123");
-      $machine4->mustSucceed("env | grep -i ftp_proxy | grep 000");
-      $machine4->mustSucceed("env | grep -i no_proxy | grep 131415");
-      $machine4->mustSucceed("su - alice -c 'env | grep -i http_proxy | grep 000'");
-      $machine4->mustSucceed("su - alice -c 'env | grep -i https_proxy | grep 000'");
-      $machine4->mustSucceed("su - alice -c 'env | grep -i rsync_proxy | grep 123'");
-      $machine4->mustSucceed("su - alice -c 'env | grep -i ftp_proxy | grep 000'");
-      $machine4->mustSucceed("su - alice -c 'env | grep -i no_proxy | grep 131415'");
+      from typing import Dict, Optional
+
+
+      def get_machine_env(machine: Machine, user: Optional[str] = None) -> Dict[str, str]:
+          """
+          Gets the environment from a given machine, and returns it as a
+          dictionary in the form:
+              {"lowercase_var_name": "value"}
+
+          Duplicate environment variables with the same name
+          (e.g. "foo" and "FOO") are handled in an undefined manner.
+          """
+          if user is not None:
+              env = machine.succeed("su - {} -c 'env -0'".format(user))
+          else:
+              env = machine.succeed("env -0")
+          ret = {}
+          for line in env.split("\0"):
+              if "=" not in line:
+                  continue
+
+              key, val = line.split("=", 1)
+              ret[key.lower()] = val
+          return ret
+
+
+      start_all()
+
+      with subtest("no proxy"):
+          assert "proxy" not in machine.succeed("env").lower()
+          assert "proxy" not in machine.succeed("su - alice -c env").lower()
+
+      with subtest("default proxy"):
+          assert "proxy" in machine2.succeed("env").lower()
+          assert "proxy" in machine2.succeed("su - alice -c env").lower()
+
+      with subtest("explicitly-set proxy"):
+          env = get_machine_env(machine3)
+          assert "123" in env["http_proxy"]
+          assert "456" in env["https_proxy"]
+          assert "789" in env["rsync_proxy"]
+          assert "101112" in env["ftp_proxy"]
+          assert "131415" in env["no_proxy"]
+
+          env = get_machine_env(machine3, "alice")
+          assert "123" in env["http_proxy"]
+          assert "456" in env["https_proxy"]
+          assert "789" in env["rsync_proxy"]
+          assert "101112" in env["ftp_proxy"]
+          assert "131415" in env["no_proxy"]
+
+      with subtest("default proxy + some other specifics"):
+          env = get_machine_env(machine4)
+          assert "000" in env["http_proxy"]
+          assert "000" in env["https_proxy"]
+          assert "123" in env["rsync_proxy"]
+          assert "000" in env["ftp_proxy"]
+          assert "131415" in env["no_proxy"]
+
+          env = get_machine_env(machine4, "alice")
+          assert "000" in env["http_proxy"]
+          assert "000" in env["https_proxy"]
+          assert "123" in env["rsync_proxy"]
+          assert "000" in env["ftp_proxy"]
+          assert "131415" in env["no_proxy"]
     '';
 })
diff --git a/nixos/tests/networking.nix b/nixos/tests/networking.nix
index 9448a104073f7..933a4451af924 100644
--- a/nixos/tests/networking.nix
+++ b/nixos/tests/networking.nix
@@ -533,7 +533,7 @@ let
           useNetworkd = networkd;
           useDHCP = false;
           interfaces.eth1 = {
-            preferTempAddress = true;
+            tempAddress = "default";
             ipv4.addresses = mkOverride 0 [ ];
             ipv6.addresses = mkOverride 0 [ ];
             useDHCP = true;
@@ -546,7 +546,7 @@ let
           useNetworkd = networkd;
           useDHCP = false;
           interfaces.eth1 = {
-            preferTempAddress = false;
+            tempAddress = "enabled";
             ipv4.addresses = mkOverride 0 [ ];
             ipv6.addresses = mkOverride 0 [ ];
             useDHCP = true;
diff --git a/nixos/tests/openstack-image.nix b/nixos/tests/openstack-image.nix
index d0225016ab762..97c9137fe1d67 100644
--- a/nixos/tests/openstack-image.nix
+++ b/nixos/tests/openstack-image.nix
@@ -16,8 +16,14 @@ let
         ../maintainers/scripts/openstack/openstack-image.nix
         ../modules/testing/test-instrumentation.nix
         ../modules/profiles/qemu-guest.nix
+        {
+          # Needed by nixos-rebuild due to lack of network access.
+          system.extraDependencies = with pkgs; [
+            stdenv
+          ];
+        }
       ];
-    }).config.system.build.openstackImage;
+    }).config.system.build.openstackImage + "/nixos.qcow2";
 
   sshKeys = import ./ssh-keys.nix pkgs;
   snakeOilPrivateKey = sshKeys.snakeOilPrivateKey.text;
diff --git a/nixos/tests/proxy.nix b/nixos/tests/proxy.nix
index 3859d429c21b9..6a14a9af59aec 100644
--- a/nixos/tests/proxy.nix
+++ b/nixos/tests/proxy.nix
@@ -1,97 +1,90 @@
-import ./make-test.nix ({ pkgs, ...} :
+import ./make-test-python.nix ({ pkgs, ...} :
 
 let
-
-  backend =
-    { pkgs, ... }:
-
-    { services.httpd.enable = true;
-      services.httpd.adminAddr = "foo@example.org";
-      services.httpd.virtualHosts.localhost.documentRoot = "${pkgs.valgrind.doc}/share/doc/valgrind/html";
-      networking.firewall.allowedTCPPorts = [ 80 ];
+  backend = { pkgs, ... }: {
+    services.httpd = {
+      enable = true;
+      adminAddr = "foo@example.org";
+      virtualHosts.localhost.documentRoot = "${pkgs.valgrind.doc}/share/doc/valgrind/html";
     };
-
-in
-
-{
+    networking.firewall.allowedTCPPorts = [ 80 ];
+  };
+in {
   name = "proxy";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ eelco ];
   };
 
-  nodes =
-    { proxy =
-        { nodes, ... }:
-
-        { services.httpd.enable = true;
-          services.httpd.adminAddr = "bar@example.org";
-          services.httpd.extraModules = [ "proxy_balancer" "lbmethod_byrequests" ];
-          services.httpd.extraConfig = ''
-            ExtendedStatus on
+  nodes = {
+    proxy = { nodes, ... }: {
+      services.httpd = {
+        enable = true;
+        adminAddr = "bar@example.org";
+        extraModules = [ "proxy_balancer" "lbmethod_byrequests" ];
+        extraConfig = ''
+          ExtendedStatus on
+        '';
+        virtualHosts.localhost = {
+          extraConfig = ''
+            <Location /server-status>
+              Require all granted
+              SetHandler server-status
+            </Location>
+
+            <Proxy balancer://cluster>
+              Require all granted
+              BalancerMember http://${nodes.backend1.config.networking.hostName} retry=0
+              BalancerMember http://${nodes.backend2.config.networking.hostName} retry=0
+            </Proxy>
+
+            ProxyStatus       full
+            ProxyPass         /server-status !
+            ProxyPass         /       balancer://cluster/
+            ProxyPassReverse  /       balancer://cluster/
+
+            # For testing; don't want to wait forever for dead backend servers.
+            ProxyTimeout      5
           '';
-          services.httpd.virtualHosts.localhost = {
-            extraConfig = ''
-              <Location /server-status>
-                Require all granted
-                SetHandler server-status
-              </Location>
-
-              <Proxy balancer://cluster>
-                Require all granted
-                BalancerMember http://${nodes.backend1.config.networking.hostName} retry=0
-                BalancerMember http://${nodes.backend2.config.networking.hostName} retry=0
-              </Proxy>
-
-              ProxyStatus       full
-              ProxyPass         /server-status !
-              ProxyPass         /       balancer://cluster/
-              ProxyPassReverse  /       balancer://cluster/
-
-              # For testing; don't want to wait forever for dead backend servers.
-              ProxyTimeout      5
-            '';
-          };
-
-          networking.firewall.allowedTCPPorts = [ 80 ];
         };
-
-      backend1 = backend;
-      backend2 = backend;
-
-      client = { ... }: { };
+      };
+      networking.firewall.allowedTCPPorts = [ 80 ];
     };
 
-  testScript =
-    ''
-      startAll;
+    backend1 = backend;
+    backend2 = backend;
+
+    client = { ... }: { };
+  };
 
-      $proxy->waitForUnit("httpd");
-      $backend1->waitForUnit("httpd");
-      $backend2->waitForUnit("httpd");
-      $client->waitForUnit("network.target");
+  testScript = ''
+    start_all()
 
-      # With the back-ends up, the proxy should work.
-      $client->succeed("curl --fail http://proxy/");
+    proxy.wait_for_unit("httpd")
+    backend1.wait_for_unit("httpd")
+    backend2.wait_for_unit("httpd")
+    client.wait_for_unit("network.target")
 
-      $client->succeed("curl --fail http://proxy/server-status");
+    # With the back-ends up, the proxy should work.
+    client.succeed("curl --fail http://proxy/")
 
-      # Block the first back-end.
-      $backend1->block;
+    client.succeed("curl --fail http://proxy/server-status")
 
-      # The proxy should still work.
-      $client->succeed("curl --fail http://proxy/");
+    # Block the first back-end.
+    backend1.block()
 
-      $client->succeed("curl --fail http://proxy/");
+    # The proxy should still work.
+    client.succeed("curl --fail http://proxy/")
+    client.succeed("curl --fail http://proxy/")
 
-      # Block the second back-end.
-      $backend2->block;
+    # Block the second back-end.
+    backend2.block()
 
-      # Now the proxy should fail as well.
-      $client->fail("curl --fail http://proxy/");
+    # Now the proxy should fail as well.
+    client.fail("curl --fail http://proxy/")
 
-      # But if the second back-end comes back, the proxy should start
-      # working again.
-      $backend2->unblock;
-      $client->succeed("curl --fail http://proxy/");
-    '';
+    # But if the second back-end comes back, the proxy should start
+    # working again.
+    backend2.unblock()
+    client.succeed("curl --fail http://proxy/")
+  '';
 })
diff --git a/nixos/tests/riak.nix b/nixos/tests/riak.nix
index 68a9b7315b350..6915779e7e9c2 100644
--- a/nixos/tests/riak.nix
+++ b/nixos/tests/riak.nix
@@ -1,21 +1,18 @@
-import ./make-test.nix {
+import ./make-test-python.nix ({ lib, pkgs, ... }: {
   name = "riak";
+  meta = with lib.maintainers; {
+    maintainers = [ filalex77 ];
+  };
 
-  nodes = {
-    master =
-      { pkgs, ... }:
-
-      {
-        services.riak.enable = true;
-        services.riak.package = pkgs.riak;
-      };
+  machine = {
+    services.riak.enable = true;
+    services.riak.package = pkgs.riak;
   };
 
   testScript = ''
-    startAll;
+    machine.start()
 
-    $master->waitForUnit("riak");
-    $master->sleep(20); # Hopefully this is long enough!!
-    $master->succeed("riak ping 2>&1");
+    machine.wait_for_unit("riak")
+    machine.wait_until_succeeds("riak ping 2>&1")
   '';
-}
+})
diff --git a/nixos/tests/signal-desktop.nix b/nixos/tests/signal-desktop.nix
index c746d46dc5505..ae141fe116de8 100644
--- a/nixos/tests/signal-desktop.nix
+++ b/nixos/tests/signal-desktop.nix
@@ -15,7 +15,7 @@ import ./make-test-python.nix ({ pkgs, ...} :
     ];
 
     services.xserver.enable = true;
-    services.xserver.displayManager.auto.user = "alice";
+    test-support.displayManager.auto.user = "alice";
     environment.systemPackages = [ pkgs.signal-desktop ];
   };
 
diff --git a/nixos/tests/solr.nix b/nixos/tests/solr.nix
index 2108e851bc595..23e1a960fb371 100644
--- a/nixos/tests/solr.nix
+++ b/nixos/tests/solr.nix
@@ -1,65 +1,48 @@
-{ system ? builtins.currentSystem,
-  config ? {},
-  pkgs ? import ../.. { inherit system config; }
-}:
+import ./make-test.nix ({ pkgs, ... }:
 
-with import ../lib/testing.nix { inherit system pkgs; };
-with pkgs.lib;
-
-let
-  solrTest = package: makeTest {
-    machine =
-      { config, pkgs, ... }:
-      {
-        # Ensure the virtual machine has enough memory for Solr to avoid the following error:
-        #
-        #   OpenJDK 64-Bit Server VM warning:
-        #     INFO: os::commit_memory(0x00000000e8000000, 402653184, 0)
-        #     failed; error='Cannot allocate memory' (errno=12)
-        #
-        #   There is insufficient memory for the Java Runtime Environment to continue.
-        #   Native memory allocation (mmap) failed to map 402653184 bytes for committing reserved memory.
-        virtualisation.memorySize = 2000;
+{
+  name = "solr";
+  meta.maintainers = [ pkgs.stdenv.lib.maintainers.aanderse ];
 
-        services.solr.enable = true;
-        services.solr.package = package;
-      };
+  machine =
+    { config, pkgs, ... }:
+    {
+      # Ensure the virtual machine has enough memory for Solr to avoid the following error:
+      #
+      #   OpenJDK 64-Bit Server VM warning:
+      #     INFO: os::commit_memory(0x00000000e8000000, 402653184, 0)
+      #     failed; error='Cannot allocate memory' (errno=12)
+      #
+      #   There is insufficient memory for the Java Runtime Environment to continue.
+      #   Native memory allocation (mmap) failed to map 402653184 bytes for committing reserved memory.
+      virtualisation.memorySize = 2000;
 
-    testScript = ''
-      startAll;
+      services.solr.enable = true;
+    };
 
-      $machine->waitForUnit('solr.service');
-      $machine->waitForOpenPort('8983');
-      $machine->succeed('curl --fail http://localhost:8983/solr/');
+  testScript = ''
+    startAll;
 
-      # adapted from pkgs.solr/examples/films/README.txt
-      $machine->succeed('sudo -u solr solr create -c films');
-      $machine->succeed(q(curl http://localhost:8983/solr/films/schema -X POST -H 'Content-type:application/json' --data-binary '{
-        "add-field" : {
-          "name":"name",
-          "type":"text_general",
-          "multiValued":false,
-          "stored":true
-        },
-        "add-field" : {
-          "name":"initial_release_date",
-          "type":"pdate",
-          "stored":true
-        }
-      }')) =~ /"status":0/ or die;
-      $machine->succeed('sudo -u solr post -c films ${pkgs.solr}/example/films/films.json');
-      $machine->succeed('curl http://localhost:8983/solr/films/query?q=name:batman') =~ /"name":"Batman Begins"/ or die;
-    '';
-  };
-in
-{
-  solr_7 = solrTest pkgs.solr_7 // {
-    name = "solr_7";
-    meta.maintainers = [ lib.maintainers.aanderse ];
-  };
+    $machine->waitForUnit('solr.service');
+    $machine->waitForOpenPort('8983');
+    $machine->succeed('curl --fail http://localhost:8983/solr/');
 
-  solr_8 = solrTest pkgs.solr_8 // {
-    name = "solr_8";
-    meta.maintainers = [ lib.maintainers.aanderse ];
-  };
-}
+    # adapted from pkgs.solr/examples/films/README.txt
+    $machine->succeed('sudo -u solr solr create -c films');
+    $machine->succeed(q(curl http://localhost:8983/solr/films/schema -X POST -H 'Content-type:application/json' --data-binary '{
+      "add-field" : {
+        "name":"name",
+        "type":"text_general",
+        "multiValued":false,
+        "stored":true
+      },
+      "add-field" : {
+        "name":"initial_release_date",
+        "type":"pdate",
+        "stored":true
+      }
+    }')) =~ /"status":0/ or die;
+    $machine->succeed('sudo -u solr post -c films ${pkgs.solr}/example/films/films.json');
+    $machine->succeed('curl http://localhost:8983/solr/films/query?q=name:batman') =~ /"name":"Batman Begins"/ or die;
+  '';
+})
diff --git a/nixos/tests/systemd-networkd-vrf.nix b/nixos/tests/systemd-networkd-vrf.nix
new file mode 100644
index 0000000000000..5bc824531e82e
--- /dev/null
+++ b/nixos/tests/systemd-networkd-vrf.nix
@@ -0,0 +1,221 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: let
+  inherit (import ./ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey;
+in {
+  name = "systemd-networkd-vrf";
+  meta.maintainers = with lib.maintainers; [ ma27 ];
+
+  nodes = {
+    client = { pkgs, ... }: {
+      virtualisation.vlans = [ 1 2 ];
+
+      networking = {
+        useDHCP = false;
+        useNetworkd = true;
+        firewall.checkReversePath = "loose";
+      };
+
+      systemd.network = {
+        enable = true;
+
+        netdevs."10-vrf1" = {
+          netdevConfig = {
+            Kind = "vrf";
+            Name = "vrf1";
+            MTUBytes = "1300";
+          };
+          vrfConfig.Table = 23;
+        };
+        netdevs."10-vrf2" = {
+          netdevConfig = {
+            Kind = "vrf";
+            Name = "vrf2";
+            MTUBytes = "1300";
+          };
+          vrfConfig.Table = 42;
+        };
+
+        networks."10-vrf1" = {
+          matchConfig.Name = "vrf1";
+          networkConfig.IPForward = "yes";
+          routes = [
+            { routeConfig = { Destination = "192.168.1.2"; Metric = "100"; }; }
+          ];
+        };
+        networks."10-vrf2" = {
+          matchConfig.Name = "vrf2";
+          networkConfig.IPForward = "yes";
+          routes = [
+            { routeConfig = { Destination = "192.168.2.3"; Metric = "100"; }; }
+          ];
+        };
+
+        networks."10-eth1" = {
+          matchConfig.Name = "eth1";
+          linkConfig.RequiredForOnline = "no";
+          networkConfig = {
+            VRF = "vrf1";
+            Address = "192.168.1.1";
+            IPForward = "yes";
+          };
+        };
+        networks."10-eth2" = {
+          matchConfig.Name = "eth2";
+          linkConfig.RequiredForOnline = "no";
+          networkConfig = {
+            VRF = "vrf2";
+            Address = "192.168.2.1";
+            IPForward = "yes";
+          };
+        };
+      };
+    };
+
+    node1 = { pkgs, ... }: {
+      virtualisation.vlans = [ 1 ];
+      networking = {
+        useDHCP = false;
+        useNetworkd = true;
+      };
+
+      services.openssh.enable = true;
+      users.users.root.openssh.authorizedKeys.keys = [ snakeOilPublicKey ];
+
+      systemd.network = {
+        enable = true;
+
+        networks."10-eth1" = {
+          matchConfig.Name = "eth1";
+          linkConfig.RequiredForOnline = "no";
+          networkConfig = {
+            Address = "192.168.1.2";
+            IPForward = "yes";
+          };
+        };
+      };
+    };
+
+    node2 = { pkgs, ... }: {
+      virtualisation.vlans = [ 2 ];
+      networking = {
+        useDHCP = false;
+        useNetworkd = true;
+      };
+
+      systemd.network = {
+        enable = true;
+
+        networks."10-eth2" = {
+          matchConfig.Name = "eth2";
+          linkConfig.RequiredForOnline = "no";
+          networkConfig = {
+            Address = "192.168.2.3";
+            IPForward = "yes";
+          };
+        };
+      };
+    };
+
+    node3 = { pkgs, ... }: {
+      virtualisation.vlans = [ 2 ];
+      networking = {
+        useDHCP = false;
+        useNetworkd = true;
+      };
+
+      systemd.network = {
+        enable = true;
+
+        networks."10-eth2" = {
+          matchConfig.Name = "eth2";
+          linkConfig.RequiredForOnline = "no";
+          networkConfig = {
+            Address = "192.168.2.4";
+            IPForward = "yes";
+          };
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    def compare_tables(expected, actual):
+        assert (
+            expected == actual
+        ), """
+        Routing tables don't match!
+        Expected:
+          {}
+        Actual:
+          {}
+        """.format(
+            expected, actual
+        )
+
+
+    start_all()
+
+    client.wait_for_unit("network.target")
+    node1.wait_for_unit("network.target")
+    node2.wait_for_unit("network.target")
+    node3.wait_for_unit("network.target")
+
+    client_ipv4_table = """
+    192.168.1.2 dev vrf1 proto static metric 100 
+    192.168.2.3 dev vrf2 proto static metric 100
+    """.strip()
+    vrf1_table = """
+    broadcast 192.168.1.0 dev eth1 proto kernel scope link src 192.168.1.1 
+    192.168.1.0/24 dev eth1 proto kernel scope link src 192.168.1.1 
+    local 192.168.1.1 dev eth1 proto kernel scope host src 192.168.1.1 
+    broadcast 192.168.1.255 dev eth1 proto kernel scope link src 192.168.1.1
+    """.strip()
+    vrf2_table = """
+    broadcast 192.168.2.0 dev eth2 proto kernel scope link src 192.168.2.1 
+    192.168.2.0/24 dev eth2 proto kernel scope link src 192.168.2.1 
+    local 192.168.2.1 dev eth2 proto kernel scope host src 192.168.2.1 
+    broadcast 192.168.2.255 dev eth2 proto kernel scope link src 192.168.2.1
+    """.strip()
+
+    # Check that networkd properly configures the main routing table
+    # and the routing tables for the VRF.
+    with subtest("check vrf routing tables"):
+        compare_tables(
+            client_ipv4_table, client.succeed("ip -4 route list | head -n2").strip()
+        )
+        compare_tables(
+            vrf1_table, client.succeed("ip -4 route list table 23 | head -n4").strip()
+        )
+        compare_tables(
+            vrf2_table, client.succeed("ip -4 route list table 42 | head -n4").strip()
+        )
+
+    # Ensure that other nodes are reachable via ICMP through the VRF.
+    with subtest("icmp through vrf works"):
+        client.succeed("ping -c5 192.168.1.2")
+        client.succeed("ping -c5 192.168.2.3")
+
+    # Test whether SSH through a VRF IP is possible.
+    # (Note: this seems to be an issue on Linux 5.x, so I decided to add this to
+    # ensure that we catch this when updating the default kernel).
+    with subtest("tcp traffic through vrf works"):
+        node1.wait_for_open_port(22)
+        client.succeed(
+            "cat ${snakeOilPrivateKey} > privkey.snakeoil"
+        )
+        client.succeed("chmod 600 privkey.snakeoil")
+        client.succeed(
+            "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil root@192.168.1.2 true"
+        )
+
+    # Only configured routes through the VRF from the main routing table should
+    # work. Additional IPs are only reachable when binding to the vrf interface.
+    with subtest("only routes from main routing table work by default"):
+        client.fail("ping -c5 192.168.2.4")
+        client.succeed("ping -I vrf2 -c5 192.168.2.4")
+
+    client.shutdown()
+    node1.shutdown()
+    node2.shutdown()
+    node3.shutdown()
+  '';
+})
diff --git a/nixos/tests/systemd.nix b/nixos/tests/systemd.nix
index 4b71b4d67597d..8028145939bb5 100644
--- a/nixos/tests/systemd.nix
+++ b/nixos/tests/systemd.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ... }: {
+import ./make-test-python.nix ({ pkgs, ... }: {
   name = "systemd";
 
   machine = { lib, ... }: {
@@ -19,7 +19,7 @@ import ./make-test.nix ({ pkgs, ... }: {
     systemd.extraConfig = "DefaultEnvironment=\"XXX_SYSTEM=foo\"";
     systemd.user.extraConfig = "DefaultEnvironment=\"XXX_USER=bar\"";
     services.journald.extraConfig = "Storage=volatile";
-    services.xserver.displayManager.auto.user = "alice";
+    test-support.displayManager.auto.user = "alice";
 
     systemd.shutdown.test = pkgs.writeScript "test.shutdown" ''
       #!${pkgs.stdenv.shell}
@@ -53,50 +53,69 @@ import ./make-test.nix ({ pkgs, ... }: {
   };
 
   testScript = ''
-    $machine->waitForX;
+    import re
+    import subprocess
+
+    machine.wait_for_x()
     # wait for user services
-    $machine->waitForUnit("default.target","alice");
+    machine.wait_for_unit("default.target", "alice")
 
     # Regression test for https://github.com/NixOS/nixpkgs/issues/35415
-    subtest "configuration files are recognized by systemd", sub {
-      $machine->succeed('test -e /system_conf_read');
-      $machine->succeed('test -e /home/alice/user_conf_read');
-      $machine->succeed('test -z $(ls -1 /var/log/journal)');
-    };
+    with subtest("configuration files are recognized by systemd"):
+        machine.succeed("test -e /system_conf_read")
+        machine.succeed("test -e /home/alice/user_conf_read")
+        machine.succeed("test -z $(ls -1 /var/log/journal)")
 
     # Regression test for https://github.com/NixOS/nixpkgs/issues/50273
-    subtest "DynamicUser actually allocates a user", sub {
-        $machine->succeed('systemd-run --pty --property=Type=oneshot --property=DynamicUser=yes --property=User=iamatest whoami | grep iamatest');
-    };
+    with subtest("DynamicUser actually allocates a user"):
+        assert "iamatest" in machine.succeed(
+            "systemd-run --pty --property=Type=oneshot --property=DynamicUser=yes --property=User=iamatest whoami"
+        )
 
     # Regression test for https://github.com/NixOS/nixpkgs/issues/35268
-    subtest "file system with x-initrd.mount is not unmounted", sub {
-      $machine->succeed('mountpoint -q /test-x-initrd-mount');
-      $machine->shutdown;
-      system('qemu-img', 'convert', '-O', 'raw',
-             'vm-state-machine/empty2.qcow2', 'x-initrd-mount.raw');
-      my $extinfo = `${pkgs.e2fsprogs}/bin/dumpe2fs x-initrd-mount.raw`;
-      die "File system was not cleanly unmounted: $extinfo"
-        unless $extinfo =~ /^Filesystem state: *clean$/m;
-    };
+    with subtest("file system with x-initrd.mount is not unmounted"):
+        machine.succeed("mountpoint -q /test-x-initrd-mount")
+        machine.shutdown()
 
-    subtest "systemd-shutdown works", sub {
-      $machine->shutdown;
-      $machine->waitForUnit('multi-user.target');
-      $machine->succeed('test -e /tmp/shared/shutdown-test');
-    };
+        subprocess.check_call(
+            [
+                "qemu-img",
+                "convert",
+                "-O",
+                "raw",
+                "vm-state-machine/empty0.qcow2",
+                "x-initrd-mount.raw",
+            ]
+        )
+        extinfo = subprocess.check_output(
+            [
+                "${pkgs.e2fsprogs}/bin/dumpe2fs",
+                "x-initrd-mount.raw",
+            ]
+        ).decode("utf-8")
+        assert (
+            re.search(r"^Filesystem state: *clean$", extinfo, re.MULTILINE) is not None
+        ), ("File system was not cleanly unmounted: " + extinfo)
+
+    with subtest("systemd-shutdown works"):
+        machine.shutdown()
+        machine.wait_for_unit("multi-user.target")
+        machine.succeed("test -e /tmp/shared/shutdown-test")
+
+    # Test settings from /etc/sysctl.d/50-default.conf are applied
+    with subtest("systemd sysctl settings are applied"):
+        machine.wait_for_unit("multi-user.target")
+        assert "fq_codel" in machine.succeed("sysctl net.core.default_qdisc")
+
+    # Test cgroup accounting is enabled
+    with subtest("systemd cgroup accounting is enabled"):
+        machine.wait_for_unit("multi-user.target")
+        assert "yes" in machine.succeed(
+            "systemctl show testservice1.service -p IOAccounting"
+        )
 
-   # Test settings from /etc/sysctl.d/50-default.conf are applied
-   subtest "systemd sysctl settings are applied", sub {
-     $machine->waitForUnit('multi-user.target');
-     $machine->succeed('sysctl net.core.default_qdisc | grep -q "fq_codel"');
-   };
-
-   # Test cgroup accounting is enabled
-   subtest "systemd cgroup accounting is enabled", sub {
-     $machine->waitForUnit('multi-user.target');
-     $machine->succeed('systemctl show testservice1.service -p IOAccounting | grep -q "yes"');
-     $machine->succeed('systemctl status testservice1.service | grep -q "CPU:"');
-   };
+        retcode, output = machine.execute("systemctl status testservice1.service")
+        assert retcode in [0, 3]  # https://bugs.freedesktop.org/show_bug.cgi?id=77507
+        assert "CPU:" in output
   '';
 })
diff --git a/nixos/tests/victoriametrics.nix b/nixos/tests/victoriametrics.nix
new file mode 100644
index 0000000000000..73ef8b7286153
--- /dev/null
+++ b/nixos/tests/victoriametrics.nix
@@ -0,0 +1,31 @@
+# This test runs influxdb and checks if influxdb is up and running
+
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "victoriametrics";
+  meta = with pkgs.stdenv.lib.maintainers; {
+    maintainers = [ yorickvp ];
+  };
+
+  nodes = {
+    one = { ... }: {
+      services.victoriametrics.enable = true;
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    one.wait_for_unit("victoriametrics.service")
+
+    # write some points and run simple query
+    out = one.succeed(
+        "curl -d 'measurement,tag1=value1,tag2=value2 field1=123,field2=1.23' -X POST 'http://localhost:8428/write'"
+    )
+    cmd = """curl -s -G 'http://localhost:8428/api/v1/export' -d 'match={__name__!=""}'"""
+    # data takes a while to appear
+    one.wait_until_succeeds(f"[[ $({cmd} | wc -l) -ne 0 ]]")
+    out = one.succeed(cmd)
+    assert '"values":[123]' in out
+    assert '"values":[1.23]' in out
+  '';
+})
diff --git a/nixos/tests/virtualbox.nix b/nixos/tests/virtualbox.nix
index 32637d2c1efe2..f03dc1cc41384 100644
--- a/nixos/tests/virtualbox.nix
+++ b/nixos/tests/virtualbox.nix
@@ -356,7 +356,7 @@ let
       virtualisation.qemu.options =
         if useKvmNestedVirt then ["-cpu" "kvm64,vmx=on"] else [];
       virtualisation.virtualbox.host.enable = true;
-      services.xserver.displayManager.auto.user = "alice";
+      test-support.displayManager.auto.user = "alice";
       users.users.alice.extraGroups = let
         inherit (config.virtualisation.virtualbox.host) enableHardening;
       in lib.mkIf enableHardening (lib.singleton "vboxusers");
diff --git a/nixos/tests/xautolock.nix b/nixos/tests/xautolock.nix
index 10e92b40e9562..4a8d3f4cebf7c 100644
--- a/nixos/tests/xautolock.nix
+++ b/nixos/tests/xautolock.nix
@@ -9,7 +9,7 @@ with lib;
   nodes.machine = {
     imports = [ ./common/x11.nix ./common/user-account.nix ];
 
-    services.xserver.displayManager.auto.user = "bob";
+    test-support.displayManager.auto.user = "bob";
     services.xserver.xautolock.enable = true;
     services.xserver.xautolock.time = 1;
   };
diff --git a/nixos/tests/xfce.nix b/nixos/tests/xfce.nix
index 3ea96b383631d..99065669661a0 100644
--- a/nixos/tests/xfce.nix
+++ b/nixos/tests/xfce.nix
@@ -4,12 +4,20 @@ import ./make-test-python.nix ({ pkgs, ...} : {
   machine =
     { pkgs, ... }:
 
-    { imports = [ ./common/user-account.nix ];
+    {
+      imports = [
+        ./common/user-account.nix
+      ];
 
       services.xserver.enable = true;
 
-      services.xserver.displayManager.auto.enable = true;
-      services.xserver.displayManager.auto.user = "alice";
+      services.xserver.displayManager.lightdm = {
+        enable = true;
+        autoLogin = {
+          enable = true;
+          user = "alice";
+        };
+      };
 
       services.xserver.desktopManager.xfce.enable = true;
 
diff --git a/nixos/tests/xmonad.nix b/nixos/tests/xmonad.nix
index ef711f8dcf6ae..56baae8b9d3cd 100644
--- a/nixos/tests/xmonad.nix
+++ b/nixos/tests/xmonad.nix
@@ -6,7 +6,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
 
   machine = { pkgs, ... }: {
     imports = [ ./common/x11.nix ./common/user-account.nix ];
-    services.xserver.displayManager.auto.user = "alice";
+    test-support.displayManager.auto.user = "alice";
     services.xserver.displayManager.defaultSession = "none+xmonad";
     services.xserver.windowManager.xmonad = {
       enable = true;
diff --git a/nixos/tests/xrdp.nix b/nixos/tests/xrdp.nix
index 1aceeffb955d4..6d7f2b9249ffa 100644
--- a/nixos/tests/xrdp.nix
+++ b/nixos/tests/xrdp.nix
@@ -14,7 +14,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
 
     client = { pkgs, ... }: {
       imports = [ ./common/x11.nix ./common/user-account.nix ];
-      services.xserver.displayManager.auto.user = "alice";
+      test-support.displayManager.auto.user = "alice";
       environment.systemPackages = [ pkgs.freerdp ];
       services.xrdp.enable = true;
       services.xrdp.defaultWindowManager = "${pkgs.icewm}/bin/icewm";
diff --git a/nixos/tests/xss-lock.nix b/nixos/tests/xss-lock.nix
index 3a7dea07d53a5..b77bbbbb3c4e7 100644
--- a/nixos/tests/xss-lock.nix
+++ b/nixos/tests/xss-lock.nix
@@ -10,12 +10,12 @@ with lib;
     simple = {
       imports = [ ./common/x11.nix ./common/user-account.nix ];
       programs.xss-lock.enable = true;
-      services.xserver.displayManager.auto.user = "alice";
+      test-support.displayManager.auto.user = "alice";
     };
 
     custom_lockcmd = { pkgs, ... }: {
       imports = [ ./common/x11.nix ./common/user-account.nix ];
-      services.xserver.displayManager.auto.user = "alice";
+      test-support.displayManager.auto.user = "alice";
 
       programs.xss-lock = {
         enable = true;
diff --git a/nixos/tests/yabar.nix b/nixos/tests/yabar.nix
index 9108004d4df9d..b374ef2968074 100644
--- a/nixos/tests/yabar.nix
+++ b/nixos/tests/yabar.nix
@@ -11,7 +11,7 @@ with lib;
   machine = {
     imports = [ ./common/x11.nix ./common/user-account.nix ];
 
-    services.xserver.displayManager.auto.user = "bob";
+    test-support.displayManager.auto.user = "bob";
 
     programs.yabar.enable = true;
     programs.yabar.bars = {