summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
authorLucas Savva <lucas@m1cr0man.com>2020-02-09 16:31:07 +0000
committerLucas Savva <lucas@m1cr0man.com>2020-02-09 16:31:07 +0000
commit75fa8027ebbfaa31e67bf2e931b8b3d428494692 (patch)
tree3b6939b1bfc774c78ac47b3befe690a70c3ea582 /nixos
parentd8e697b4fcfd929d05221ac3e67b9c04ac69df86 (diff)
parenta8f3903ba5ac2899d059b7586f1f047df23b25b5 (diff)
downloadnixpkgs-75fa8027ebbfaa31e67bf2e931b8b3d428494692.tar
nixpkgs-75fa8027ebbfaa31e67bf2e931b8b3d428494692.tar.gz
nixpkgs-75fa8027ebbfaa31e67bf2e931b8b3d428494692.tar.bz2
nixpkgs-75fa8027ebbfaa31e67bf2e931b8b3d428494692.tar.lz
nixpkgs-75fa8027ebbfaa31e67bf2e931b8b3d428494692.tar.xz
nixpkgs-75fa8027ebbfaa31e67bf2e931b8b3d428494692.tar.zst
nixpkgs-75fa8027ebbfaa31e67bf2e931b8b3d428494692.zip
nixos/acme: Update release note, remove redundant requires
Merge remote-tracking branch 'remotes/upstream/master'
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/configuration/declarative-packages.xml6
-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/installation/upgrading.xml13
-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/man-pages.xml2
-rw-r--r--nixos/doc/manual/release-notes/rl-2003.xml225
-rw-r--r--nixos/lib/qemu-flags.nix4
-rw-r--r--nixos/lib/test-driver/test-driver.py71
-rw-r--r--nixos/lib/testing-python.nix2
-rw-r--r--nixos/lib/testing/jquery-ui.nix4
-rw-r--r--nixos/modules/config/ldap.nix3
-rw-r--r--nixos/modules/config/pulseaudio.nix3
-rw-r--r--nixos/modules/config/resolvconf.nix10
-rw-r--r--nixos/modules/hardware/opengl.nix6
-rw-r--r--nixos/modules/hardware/openrazer.nix2
-rw-r--r--nixos/modules/hardware/tuxedo-keyboard.nix35
-rw-r--r--nixos/modules/hardware/usb-wwan.nix13
-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/cd-dvd/iso-image.nix14
-rw-r--r--nixos/modules/installer/netboot/netboot.nix14
-rw-r--r--nixos/modules/installer/tools/nixos-option/nixos-option.cc251
-rw-r--r--nixos/modules/installer/tools/nixos-rebuild.sh18
-rw-r--r--nixos/modules/misc/ids.nix4
-rw-r--r--nixos/modules/misc/locate.nix7
-rw-r--r--nixos/modules/misc/version.nix5
-rw-r--r--nixos/modules/module-list.nix16
-rw-r--r--nixos/modules/programs/bash-my-aws.nix25
-rw-r--r--nixos/modules/programs/bash/bash.nix13
-rw-r--r--nixos/modules/programs/geary.nix20
-rw-r--r--nixos/modules/programs/gnupg.nix2
-rw-r--r--nixos/modules/programs/liboping.nix22
-rw-r--r--nixos/modules/programs/sway.nix3
-rw-r--r--nixos/modules/programs/tmux.nix8
-rw-r--r--nixos/modules/programs/traceroute.nix26
-rw-r--r--nixos/modules/programs/way-cooler.nix78
-rw-r--r--nixos/modules/programs/zsh/zsh.nix66
-rw-r--r--nixos/modules/rename.nix15
-rw-r--r--nixos/modules/security/duosec.nix16
-rw-r--r--nixos/modules/services/amqp/rabbitmq.nix13
-rw-r--r--nixos/modules/services/backup/restic.nix56
-rw-r--r--nixos/modules/services/cluster/kubernetes/pki.nix10
-rw-r--r--nixos/modules/services/continuous-integration/buildkite-agent.nix98
-rw-r--r--nixos/modules/services/continuous-integration/hydra/default.nix4
-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/fwupd.nix5
-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/gitea.nix2
-rw-r--r--nixos/modules/services/misc/home-assistant.nix1
-rw-r--r--nixos/modules/services/misc/paperless.nix14
-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/corerad.nix46
-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.nix144
-rw-r--r--nixos/modules/services/networking/matterbridge.nix2
-rw-r--r--nixos/modules/services/networking/ndppd.nix20
-rw-r--r--nixos/modules/services/networking/nsd.nix4
-rw-r--r--nixos/modules/services/networking/syncthing.nix18
-rw-r--r--nixos/modules/services/networking/unifi.nix15
-rw-r--r--nixos/modules/services/networking/wireguard.nix2
-rw-r--r--nixos/modules/services/networking/wpa_supplicant.nix7
-rw-r--r--nixos/modules/services/networking/xandikos.nix148
-rw-r--r--nixos/modules/services/networking/zerotierone.nix10
-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/torrent/transmission.nix16
-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.nix245
-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)42
-rw-r--r--nixos/modules/services/web-servers/nginx/gitweb.nix53
-rw-r--r--nixos/modules/services/web-servers/unit/default.nix8
-rw-r--r--nixos/modules/services/x11/desktop-managers/default.nix26
-rw-r--r--nixos/modules/services/x11/desktop-managers/gnome3.nix14
-rw-r--r--nixos/modules/services/x11/desktop-managers/plasma5.nix3
-rw-r--r--nixos/modules/services/x11/display-managers/default.nix6
-rw-r--r--nixos/modules/services/x11/display-managers/gdm.nix4
-rw-r--r--nixos/modules/services/x11/hardware/multitouch.nix94
-rw-r--r--nixos/modules/services/x11/unclutter.nix7
-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.nix68
-rw-r--r--nixos/modules/system/boot/stage-1-init.sh13
-rw-r--r--nixos/modules/system/boot/systemd-lib.nix8
-rw-r--r--nixos/modules/system/boot/systemd.nix32
-rw-r--r--nixos/modules/tasks/filesystems.nix8
-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.nix58
-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/acme.nix1
-rw-r--r--nixos/tests/all-tests.nix15
-rw-r--r--nixos/tests/bittorrent.nix155
-rw-r--r--nixos/tests/blivet.nix87
-rw-r--r--nixos/tests/buildbot.nix130
-rw-r--r--nixos/tests/buildkite-agent.nix36
-rw-r--r--nixos/tests/certmgr.nix12
-rw-r--r--nixos/tests/chromium.nix367
-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/corerad.nix70
-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/elk.nix8
-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/installed-tests/fwupd.nix4
-rw-r--r--nixos/tests/installer.nix1027
-rw-r--r--nixos/tests/keymap.nix114
-rw-r--r--nixos/tests/limesurvey.nix29
-rw-r--r--nixos/tests/lorri/default.nix6
-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/printing.nix165
-rw-r--r--nixos/tests/proxy.nix143
-rw-r--r--nixos/tests/restic.nix63
-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/xandikos.nix70
-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
-rw-r--r--nixos/tests/zfs.nix75
173 files changed, 5322 insertions, 2977 deletions
diff --git a/nixos/doc/manual/configuration/declarative-packages.xml b/nixos/doc/manual/configuration/declarative-packages.xml
index 5fb3bcb9f8f..cd84d1951d2 100644
--- a/nixos/doc/manual/configuration/declarative-packages.xml
+++ b/nixos/doc/manual/configuration/declarative-packages.xml
@@ -19,6 +19,12 @@
   <command>nixos-rebuild switch</command>.
  </para>
 
+ <note>
+  <para>
+   Some packages require additional global configuration such as D-Bus or systemd service registration so adding them to <xref linkend="opt-environment.systemPackages"/> might not be sufficient. You are advised to check the <link xlink:href="#ch-options">list of options</link> whether a NixOS module for the package does not exist.
+  </para>
+ </note>
+
  <para>
   You can get a list of the available packages as follows:
 <screen>
diff --git a/nixos/doc/manual/configuration/luks-file-systems.xml b/nixos/doc/manual/configuration/luks-file-systems.xml
index 8a2b107e0ee..d3007843d68 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 d103ee24978..3953e0ffe85 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 55ad9fe6e65..06dd7c8bfb9 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 7d2862f8b31..a81a327c09b 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 9371af9984d..a22a0a3707b 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/installation/upgrading.xml b/nixos/doc/manual/installation/upgrading.xml
index 8d3f35b7c26..92864cf2557 100644
--- a/nixos/doc/manual/installation/upgrading.xml
+++ b/nixos/doc/manual/installation/upgrading.xml
@@ -120,12 +120,17 @@ nixos https://nixos.org/channels/nixos-unstable
    to <filename>configuration.nix</filename>:
 <programlisting>
 <xref linkend="opt-system.autoUpgrade.enable"/> = true;
+<xref linkend="opt-system.autoUpgrade.allowReboot"/> = true;
 </programlisting>
    This enables a periodically executed systemd service named
-   <literal>nixos-upgrade.service</literal>. It runs <command>nixos-rebuild
-   switch --upgrade</command> to upgrade NixOS to the latest version in the
-   current channel. (To see when the service runs, see <command>systemctl
-   list-timers</command>.) You can also specify a channel explicitly, e.g.
+   <literal>nixos-upgrade.service</literal>. If the <literal>allowReboot</literal>
+   option is <literal>false</literal>, it runs <command>nixos-rebuild switch
+   --upgrade</command> to upgrade NixOS to the latest version in the current
+   channel. (To see when the service runs, see <command>systemctl list-timers</command>.)
+   If <literal>allowReboot</literal> is <literal>true</literal>, then the
+   system will automatically reboot if the new generation contains a different
+   kernel, initrd or kernel modules.
+   You can also specify a channel explicitly, e.g.
 <programlisting>
 <xref linkend="opt-system.autoUpgrade.channel"/> = https://nixos.org/channels/nixos-19.09;
 </programlisting>
diff --git a/nixos/doc/manual/man-nixos-install.xml b/nixos/doc/manual/man-nixos-install.xml
index 0752c397182..9255ce763ef 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 b82f3125609..b921386d0df 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/man-pages.xml b/nixos/doc/manual/man-pages.xml
index f5a1dd2d69f..49acfe7330b 100644
--- a/nixos/doc/manual/man-pages.xml
+++ b/nixos/doc/manual/man-pages.xml
@@ -6,7 +6,7 @@
   <author><personname><firstname>Eelco</firstname><surname>Dolstra</surname></personname>
    <contrib>Author</contrib>
   </author>
-  <copyright><year>2007-2019</year><holder>Eelco Dolstra</holder>
+  <copyright><year>2007-2020</year><holder>Eelco Dolstra</holder>
   </copyright>
  </info>
  <xi:include href="man-configuration.xml" />
diff --git a/nixos/doc/manual/release-notes/rl-2003.xml b/nixos/doc/manual/release-notes/rl-2003.xml
index 37ac4ec0288..106612d0595 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.
@@ -170,6 +184,12 @@ services.xserver.displayManager.defaultSession = "xfce+icewm";
    </listitem>
    <listitem>
     <para>
+     The Way Cooler wayland compositor has been removed, as the project has been officially canceled.
+     There are no more <literal>way-cooler</literal> attribute and <literal>programs.way-cooler</literal> options.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
       The BEAM package set has been deleted. You will only find there the different interpreters.
       You should now use the different build tools coming with the languages with sandbox mode disabled.
     </para>
@@ -228,6 +248,23 @@ services.xserver.displayManager.defaultSession = "xfce+icewm";
    </listitem>
    <listitem>
     <para>
+     The <literal>roundcube</literal> module has been hardened.
+     <itemizedlist>
+      <listitem>
+       <para>
+        The password of the database is not written world readable in the store any more. If <literal>database.host</literal> is set to <literal>localhost</literal>, then a unix user of the same name as the database will be created and PostreSQL peer authentication will be used, removing the need for a password. Otherwise, a password is still needed and can be provided with the new option <literal>database.passwordFile</literal>, which should be set to the path of a file containing the password and readable by the user <literal>nginx</literal> only. The <literal>database.password</literal> option is insecure and deprecated. Usage of this option will print a warning.
+       </para>
+      </listitem>
+      <listitem>
+       <para>
+        A random <literal>des_key</literal> is set by default in the configuration of roundcube, instead of using the hardcoded and insecure default. To ensure a clean migration, all users will be logged out when you upgrade to this release.
+       </para>
+      </listitem>
+     </itemizedlist>
+    </para>
+   </listitem>
+   <listitem>
+    <para>
      The packages <literal>openobex</literal> and <literal>obexftp</literal>
      are no longer installed when enabling Bluetooth via
      <option>hardware.bluetooth.enable</option>.
@@ -401,6 +438,183 @@ users.users.me =
      the type to <literal>either path (submodule ...)</literal>.
     </para>
    </listitem>
+   <listitem>
+    <para>
+      The <link linkend="opt-services.buildkite-agent.enable">Buildkite Agent</link>
+      module and corresponding packages have been updated to 3.x.
+      While doing so, the following options have been changed:
+    </para>
+    <itemizedlist>
+      <listitem>
+       <para>
+         <literal>services.buildkite-agent.meta-data</literal> has been renamed to
+         <link linkend="opt-services.buildkite-agent.tags">services.buildkite-agent.tags</link>,
+         to match upstreams naming for 3.x.
+         Its type has also changed - it now accepts an attrset of strings.
+       </para>
+      </listitem>
+      <listitem>
+       <para>
+         The<literal>services.buildkite-agent.openssh.publicKeyPath</literal> option
+         has been removed, as it's not necessary to deploy public keys to clone private
+         repositories.
+       </para>
+      </listitem>
+      <listitem>
+       <para>
+         <literal>services.buildkite-agent.openssh.privateKeyPath</literal>
+         has been renamed to
+         <link linkend="opt-services.buildkite-agent.privateSshKeyPath">buildkite-agent.privateSshKeyPath</link>,
+         as the whole <literal>openssh</literal> now only contained that single option.
+       </para>
+      </listitem>
+      <listitem>
+       <para>
+         <link linkend="opt-services.buildkite-agent.shell">services.buildkite-agent.shell</link>
+         has been introduced, allowing to specify a custom shell to be used.
+       </para>
+      </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>
 
@@ -452,9 +666,14 @@ users.users.me =
      As well as this, the options <literal>security.acme.acceptTerms</literal> and either
      <literal>security.acme.email</literal> or <literal>security.acme.certs.&lt;name&gt;.email</literal>
      must be set in order to use the ACME module.
-     Certificates will be regenerated from new on the next renewal date. The credentials for simp-le are
+     Certificates will be regenerated anew on the next renewal date. The credentials for simp-le are
      preserved and thus it is possible to roll back to previous versions without breaking certificate
      generation.
+   </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>
diff --git a/nixos/lib/qemu-flags.nix b/nixos/lib/qemu-flags.nix
index 774f66b4804..859d9e975fe 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 7e575189209..2b8dffec7d5 100644
--- a/nixos/lib/test-driver/test-driver.py
+++ b/nixos/lib/test-driver/test-driver.py
@@ -1,13 +1,17 @@
 #! /somewhere/python3
 from contextlib import contextmanager, _GeneratorContextManager
+from queue import Queue, Empty
+from typing import Tuple, Any, Callable, Dict, Iterator, Optional, List
 from xml.sax.saxutils import XMLGenerator
 import _thread
 import atexit
+import base64
 import os
+import pathlib
 import ptpython.repl
 import pty
-from queue import Queue, Empty
 import re
+import shlex
 import shutil
 import socket
 import subprocess
@@ -15,9 +19,6 @@ import sys
 import tempfile
 import time
 import unicodedata
-from typing import Tuple, Any, Callable, Dict, Iterator, Optional, List
-import shlex
-import pathlib
 
 CHAR_TO_KEY = {
     "A": "shift-a",
@@ -84,7 +85,7 @@ CHAR_TO_KEY = {
 
 # Forward references
 nr_tests: int
-nr_succeeded: int
+failed_tests: list
 log: "Logger"
 machines: "List[Machine]"
 
@@ -221,7 +222,7 @@ class Machine:
             return path
 
         self.state_dir = create_dir("vm-state-{}".format(self.name))
-        self.shared_dir = create_dir("{}/xchg".format(self.state_dir))
+        self.shared_dir = create_dir("shared-xchg")
 
         self.booted = False
         self.connected = False
@@ -395,7 +396,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]
@@ -566,6 +567,41 @@ class Machine:
             if ret.returncode != 0:
                 raise Exception("Cannot convert screenshot")
 
+    def copy_from_host_via_shell(self, source: str, target: str) -> None:
+        """Copy a file from the host into the guest by piping it over the
+        shell into the destination file. Works without host-guest shared folder.
+        Prefer copy_from_host for whenever possible.
+        """
+        with open(source, "rb") as fh:
+            content_b64 = base64.b64encode(fh.read()).decode()
+            self.succeed(
+                f"mkdir -p $(dirname {target})",
+                f"echo -n {content_b64} | base64 -d > {target}",
+            )
+
+    def copy_from_host(self, source: str, target: str) -> None:
+        """Copy a file from the host into the guest via the `shared_dir` shared
+        among all the VMs (using a temporary directory).
+        """
+        host_src = pathlib.Path(source)
+        vm_target = pathlib.Path(target)
+        with tempfile.TemporaryDirectory(dir=self.shared_dir) as shared_td:
+            shared_temp = pathlib.Path(shared_td)
+            host_intermediate = shared_temp / host_src.name
+            vm_shared_temp = pathlib.Path("/tmp/shared") / shared_temp.name
+            vm_intermediate = vm_shared_temp / host_src.name
+
+            self.succeed(make_command(["mkdir", "-p", vm_shared_temp]))
+            if host_src.is_dir():
+                shutil.copytree(host_src, host_intermediate)
+            else:
+                shutil.copy(host_src, host_intermediate)
+            self.succeed("sync")
+            self.succeed(make_command(["mkdir", "-p", vm_target.parent]))
+            self.succeed(make_command(["cp", "-r", vm_intermediate, vm_target]))
+        # Make sure the cleanup is synced into VM
+        self.succeed("sync")
+
     def copy_from_vm(self, source: str, target_dir: str = "") -> None:
         """Copy a file from the VM (specified by an in-VM source path) to a path
         relative to `$out`. The file is copied via the `shared_dir` shared among
@@ -576,7 +612,7 @@ class Machine:
         vm_src = pathlib.Path(source)
         with tempfile.TemporaryDirectory(dir=self.shared_dir) as shared_td:
             shared_temp = pathlib.Path(shared_td)
-            vm_shared_temp = pathlib.Path("/tmp/xchg") / shared_temp.name
+            vm_shared_temp = pathlib.Path("/tmp/shared") / shared_temp.name
             vm_intermediate = vm_shared_temp / vm_src.name
             intermediate = shared_temp / vm_src.name
             # Copy the file to the shared directory inside VM
@@ -704,7 +740,8 @@ class Machine:
 
         def process_serial_output() -> None:
             for _line in self.process.stdout:
-                line = _line.decode("unicode_escape").replace("\r", "").rstrip()
+                # Ignore undecodable bytes that may occur in boot menus
+                line = _line.decode(errors="ignore").replace("\r", "").rstrip()
                 eprint("{} # {}".format(self.name, line))
                 self.logger.enqueue({"msg": line, "machine": self.name})
 
@@ -841,23 +878,31 @@ def run_tests() -> None:
             machine.execute("sync")
 
     if nr_tests != 0:
+        nr_succeeded = nr_tests - len(failed_tests)
         eprint("{} out of {} tests succeeded".format(nr_succeeded, nr_tests))
-        if nr_tests > nr_succeeded:
+        if len(failed_tests) > 0:
+            eprint(
+                "The following tests have failed:\n - {}".format(
+                    "\n - ".join(failed_tests)
+                )
+            )
             sys.exit(1)
 
 
 @contextmanager
 def subtest(name: str) -> Iterator[None]:
     global nr_tests
-    global nr_succeeded
+    global failed_tests
 
     with log.nested(name):
         nr_tests += 1
         try:
             yield
-            nr_succeeded += 1
             return True
         except Exception as e:
+            failed_tests.append(
+                'Test "{}" failed with error: "{}"'.format(name, str(e))
+            )
             log.log("error: {}".format(str(e)))
 
     return False
@@ -879,7 +924,7 @@ if __name__ == "__main__":
     exec("\n".join(machine_eval))
 
     nr_tests = 0
-    nr_succeeded = 0
+    failed_tests = []
 
     @atexit.register
     def clean_up() -> None:
diff --git a/nixos/lib/testing-python.nix b/nixos/lib/testing-python.nix
index 3d09be3b6cd..a7f6d792651 100644
--- a/nixos/lib/testing-python.nix
+++ b/nixos/lib/testing-python.nix
@@ -155,7 +155,7 @@ in rec {
             --add-flags "''${vms[*]}" \
             ${lib.optionalString enableOCR
               "--prefix PATH : '${ocrProg}/bin:${imagemagick_tiff}/bin'"} \
-            --run "export testScript=\"\$(cat $out/test-script)\"" \
+            --run "export testScript=\"\$(${coreutils}/bin/cat $out/test-script)\"" \
             --set VLANS '${toString vlans}'
           ln -s ${testDriver}/bin/nixos-test-driver $out/bin/nixos-run-vms
           wrapProgram $out/bin/nixos-run-vms \
diff --git a/nixos/lib/testing/jquery-ui.nix b/nixos/lib/testing/jquery-ui.nix
index e65107a3c2f..abd59da2d28 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 9c8e9d14937..b554f197dc4 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/pulseaudio.nix b/nixos/modules/config/pulseaudio.nix
index 048bbb30c73..408d0a9c33f 100644
--- a/nixos/modules/config/pulseaudio.nix
+++ b/nixos/modules/config/pulseaudio.nix
@@ -248,6 +248,9 @@ in {
       security.rtkit.enable = true;
 
       systemd.packages = [ overriddenPackage ];
+
+      # PulseAudio is packaged with udev rules to handle various audio device quirks
+      services.udev.packages = [ overriddenPackage ];
     })
 
     (mkIf (cfg.extraModules != []) {
diff --git a/nixos/modules/config/resolvconf.nix b/nixos/modules/config/resolvconf.nix
index 7d2f252a888..cc202bca6c4 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/opengl.nix b/nixos/modules/hardware/opengl.nix
index 89dc5008df5..28cddea8b79 100644
--- a/nixos/modules/hardware/opengl.nix
+++ b/nixos/modules/hardware/opengl.nix
@@ -43,11 +43,11 @@ in
         description = ''
           Whether to enable OpenGL drivers. This is needed to enable
           OpenGL support in X11 systems, as well as for Wayland compositors
-          like sway, way-cooler and Weston. It is enabled by default
+          like sway and Weston. It is enabled by default
           by the corresponding modules, so you do not usually have to
           set it yourself, only if there is no module for your wayland
-          compositor of choice. See services.xserver.enable,
-          programs.sway.enable, and programs.way-cooler.enable.
+          compositor of choice. See services.xserver.enable and
+          programs.sway.enable.
         '';
         type = types.bool;
         default = false;
diff --git a/nixos/modules/hardware/openrazer.nix b/nixos/modules/hardware/openrazer.nix
index 883db7f2f4f..b5c3d674414 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 00000000000..898eed24493
--- /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/hardware/usb-wwan.nix b/nixos/modules/hardware/usb-wwan.nix
index 2d20421586a..679a6c6497c 100644
--- a/nixos/modules/hardware/usb-wwan.nix
+++ b/nixos/modules/hardware/usb-wwan.nix
@@ -21,6 +21,19 @@ with lib;
   ###### implementation
 
   config = mkIf config.hardware.usbWwan.enable {
+    # Attaches device specific handlers.
     services.udev.packages = with pkgs; [ usb-modeswitch-data ];
+
+    # Triggered by udev, usb-modeswitch creates systemd services via a
+    # template unit in the usb-modeswitch package.
+    systemd.packages = with pkgs; [ usb-modeswitch ];
+
+    # The systemd service requires the usb-modeswitch-data. The
+    # usb-modeswitch package intends to discover this via the
+    # filesystem at /usr/share/usb_modeswitch, and merge it with user
+    # configuration in /etc/usb_modeswitch.d. Configuring the correct
+    # path in the package is difficult, as it would cause a cyclic
+    # dependency.
+    environment.etc."usb_modeswitch.d".source = "${pkgs.usb-modeswitch-data}/share/usb_modeswitch";
   };
 }
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 3336d512cfd..d98325a99ac 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 e00d3f7535b..e00d3f7535b 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/cd-dvd/iso-image.nix b/nixos/modules/installer/cd-dvd/iso-image.nix
index 11319e5f4f8..4558b4dc955 100644
--- a/nixos/modules/installer/cd-dvd/iso-image.nix
+++ b/nixos/modules/installer/cd-dvd/iso-image.nix
@@ -569,14 +569,18 @@ in
       };
 
     fileSystems."/nix/store" =
-      { fsType = "unionfs-fuse";
-        device = "unionfs";
-        options = [ "allow_other" "cow" "nonempty" "chroot=/mnt-root" "max_files=32768" "hide_meta_files" "dirs=/nix/.rw-store=rw:/nix/.ro-store=ro" ];
+      { fsType = "overlay";
+        device = "overlay";
+        options = [
+          "lowerdir=/nix/.ro-store"
+          "upperdir=/nix/.rw-store/store"
+          "workdir=/nix/.rw-store/work"
+        ];
       };
 
-    boot.initrd.availableKernelModules = [ "squashfs" "iso9660" "uas" ];
+    boot.initrd.availableKernelModules = [ "squashfs" "iso9660" "uas" "overlay" ];
 
-    boot.initrd.kernelModules = [ "loop" ];
+    boot.initrd.kernelModules = [ "loop" "overlay" ];
 
     # Closures to be copied to the Nix store on the CD, namely the init
     # script and the top-level system configuration directory.
diff --git a/nixos/modules/installer/netboot/netboot.nix b/nixos/modules/installer/netboot/netboot.nix
index 5146858cccf..95eba86bcb6 100644
--- a/nixos/modules/installer/netboot/netboot.nix
+++ b/nixos/modules/installer/netboot/netboot.nix
@@ -50,14 +50,18 @@ with lib;
       };
 
     fileSystems."/nix/store" =
-      { fsType = "unionfs-fuse";
-        device = "unionfs";
-        options = [ "allow_other" "cow" "nonempty" "chroot=/mnt-root" "max_files=32768" "hide_meta_files" "dirs=/nix/.rw-store=rw:/nix/.ro-store=ro" ];
+      { fsType = "overlay";
+        device = "overlay";
+        options = [
+          "lowerdir=/nix/.ro-store"
+          "upperdir=/nix/.rw-store/store"
+          "workdir=/nix/.rw-store/work"
+        ];
       };
 
-    boot.initrd.availableKernelModules = [ "squashfs" ];
+    boot.initrd.availableKernelModules = [ "squashfs" "overlay" ];
 
-    boot.initrd.kernelModules = [ "loop" ];
+    boot.initrd.kernelModules = [ "loop" "overlay" ];
 
     # Closures to be copied to the Nix store, namely the init
     # script and the top-level system configuration directory.
diff --git a/nixos/modules/installer/tools/nixos-option/nixos-option.cc b/nixos/modules/installer/tools/nixos-option/nixos-option.cc
index 9b92dc829cd..1a7b07a74f8 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 c53dc1000c4..7db323d38e6 100644
--- a/nixos/modules/installer/tools/nixos-rebuild.sh
+++ b/nixos/modules/installer/tools/nixos-rebuild.sh
@@ -22,7 +22,7 @@ repair=
 profile=/nix/var/nix/profiles/system
 buildHost=
 targetHost=
-maybeSudo=
+maybeSudo=()
 
 while [ "$#" -gt 0 ]; do
     i="$1"; shift 1
@@ -91,9 +91,7 @@ while [ "$#" -gt 0 ]; do
         shift 1
         ;;
       --use-remote-sudo)
-        # note the trailing space
-        maybeSudo="sudo "
-        shift 1
+        maybeSudo=(sudo --)
         ;;
       *)
         echo "$0: unknown option \`$i'"
@@ -102,6 +100,10 @@ while [ "$#" -gt 0 ]; do
     esac
 done
 
+if [ -n "$SUDO_USER" ]; then
+    maybeSudo=(sudo --)
+fi
+
 if [ -z "$buildHost" -a -n "$targetHost" ]; then
     buildHost="$targetHost"
 fi
@@ -116,17 +118,17 @@ buildHostCmd() {
     if [ -z "$buildHost" ]; then
         "$@"
     elif [ -n "$remoteNix" ]; then
-        ssh $SSHOPTS "$buildHost" env PATH="$remoteNix:$PATH" "$maybeSudo$@"
+        ssh $SSHOPTS "$buildHost" env PATH="$remoteNix:$PATH" "${maybeSudo[@]}" "$@"
     else
-        ssh $SSHOPTS "$buildHost" "$maybeSudo$@"
+        ssh $SSHOPTS "$buildHost" "${maybeSudo[@]}" "$@"
     fi
 }
 
 targetHostCmd() {
     if [ -z "$targetHost" ]; then
-        "$@"
+        "${maybeSudo[@]}" "$@"
     else
-        ssh $SSHOPTS "$targetHost" "$maybeSudo$@"
+        ssh $SSHOPTS "$targetHost" "${maybeSudo[@]}" "$@"
     fi
 }
 
diff --git a/nixos/modules/misc/ids.nix b/nixos/modules/misc/ids.nix
index bedd87a368e..979cdc5d4ad 100644
--- a/nixos/modules/misc/ids.nix
+++ b/nixos/modules/misc/ids.nix
@@ -299,7 +299,7 @@ in
       couchpotato = 267;
       gogs = 268;
       pdns-recursor = 269;
-      kresd = 270;
+      #kresd = 270; # switched to "knot-resolver" with dynamic ID
       rpc = 271;
       geoip = 272;
       fcron = 273;
@@ -600,7 +600,7 @@ in
       headphones = 266;
       couchpotato = 267;
       gogs = 268;
-      kresd = 270;
+      #kresd = 270; # switched to "knot-resolver" with dynamic ID
       #rpc = 271; # unused
       #geoip = 272; # unused
       fcron = 273;
diff --git a/nixos/modules/misc/locate.nix b/nixos/modules/misc/locate.nix
index 552535c253e..dc668796c78 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 b85614771ee..8a85035ceb7 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 (pathIsDirectory gitRepo) (mkDefault            gitCommitId);
-      versionSuffix = mkIf (pathIsDirectory 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 a6c1d7c5d66..878b77969af 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
@@ -97,6 +98,7 @@
   ./programs/autojump.nix
   ./programs/bandwhich.nix
   ./programs/bash/bash.nix
+  ./programs/bash-my-aws.nix
   ./programs/bcc.nix
   ./programs/browserpass.nix
   ./programs/captive-browser.nix
@@ -116,6 +118,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
@@ -127,6 +130,7 @@
   ./programs/java.nix
   ./programs/kbdlight.nix
   ./programs/less.nix
+  ./programs/liboping.nix
   ./programs/light.nix
   ./programs/mosh.nix
   ./programs/mininet.nix
@@ -152,13 +156,13 @@
   ./programs/system-config-printer.nix
   ./programs/thefuck.nix
   ./programs/tmux.nix
+  ./programs/traceroute.nix
   ./programs/tsm-client.nix
   ./programs/udevil.nix
   ./programs/usbtop.nix
   ./programs/venus.nix
   ./programs/vim.nix
   ./programs/wavemon.nix
-  ./programs/way-cooler.nix
   ./programs/waybar.nix
   ./programs/wireshark.nix
   ./programs/x2goserver.nix
@@ -278,6 +282,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
@@ -424,6 +429,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
@@ -524,6 +530,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
@@ -577,6 +584,7 @@
   ./services/networking/connman.nix
   ./services/networking/consul.nix
   ./services/networking/coredns.nix
+  ./services/networking/corerad.nix
   ./services/networking/coturn.nix
   ./services/networking/dante.nix
   ./services/networking/ddclient.nix
@@ -584,7 +592,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
@@ -735,6 +743,7 @@
   ./services/networking/wicd.nix
   ./services/networking/wireguard.nix
   ./services/networking/wpa_supplicant.nix
+  ./services/networking/xandikos.nix
   ./services/networking/xinetd.nix
   ./services/networking/xl2tpd.nix
   ./services/networking/xrdp.nix
@@ -802,6 +811,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
@@ -859,7 +869,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
@@ -869,7 +878,6 @@
   ./services/x11/display-managers/xpra.nix
   ./services/x11/fractalart.nix
   ./services/x11/hardware/libinput.nix
-  ./services/x11/hardware/multitouch.nix
   ./services/x11/hardware/synaptics.nix
   ./services/x11/hardware/wacom.nix
   ./services/x11/hardware/digimend.nix
diff --git a/nixos/modules/programs/bash-my-aws.nix b/nixos/modules/programs/bash-my-aws.nix
new file mode 100644
index 00000000000..15e429a7549
--- /dev/null
+++ b/nixos/modules/programs/bash-my-aws.nix
@@ -0,0 +1,25 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  prg = config.programs;
+  cfg = prg.bash-my-aws;
+
+  initScript = ''
+    eval $(${pkgs.bash-my-aws}/bin/bma-init)
+  '';
+in
+  {
+    options = {
+      programs.bash-my-aws = {
+        enable = mkEnableOption "bash-my-aws";
+      };
+    };
+
+    config = mkIf cfg.enable {
+      environment.systemPackages = with pkgs; [ bash-my-aws ];
+
+      programs.bash.interactiveShellInit = initScript;
+    };
+  }
diff --git a/nixos/modules/programs/bash/bash.nix b/nixos/modules/programs/bash/bash.nix
index 366c07c0a35..be964ce7f3f 100644
--- a/nixos/modules/programs/bash/bash.nix
+++ b/nixos/modules/programs/bash/bash.nix
@@ -32,6 +32,10 @@ let
     fi
   '';
 
+  lsColors = optionalString cfg.enableLsColors ''
+    eval "$(${pkgs.coreutils}/bin/dircolors -b)"
+  '';
+
   bashAliases = concatStringsSep "\n" (
     mapAttrsFlatten (k: v: "alias ${k}=${escapeShellArg v}")
       (filterAttrs (k: v: v != null) cfg.shellAliases)
@@ -127,6 +131,14 @@ in
         type = types.bool;
       };
 
+      enableLsColors = mkOption {
+        default = true;
+        description = ''
+          Enable extra colors in directory listings.
+        '';
+        type = types.bool;
+      };
+
     };
 
   };
@@ -156,6 +168,7 @@ in
 
         ${cfg.promptInit}
         ${bashCompletion}
+        ${lsColors}
         ${bashAliases}
 
         ${cfge.interactiveShellInit}
diff --git a/nixos/modules/programs/geary.nix b/nixos/modules/programs/geary.nix
new file mode 100644
index 00000000000..01803bc411e
--- /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 2d262d90657..7a3cb588ee7 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/liboping.nix b/nixos/modules/programs/liboping.nix
new file mode 100644
index 00000000000..4e4c235ccde
--- /dev/null
+++ b/nixos/modules/programs/liboping.nix
@@ -0,0 +1,22 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.liboping;
+in {
+  options.programs.liboping = {
+    enable = mkEnableOption "liboping";
+  };
+  config = mkIf cfg.enable {
+    environment.systemPackages = with pkgs; [ liboping ];
+    security.wrappers = mkMerge (map (
+      exec: {
+        "${exec}" = {
+          source = "${pkgs.liboping}/bin/${exec}";
+          capabilities = "cap_net_raw+p";
+        };
+      }
+    ) [ "oping" "noping" ]);
+  };
+}
diff --git a/nixos/modules/programs/sway.nix b/nixos/modules/programs/sway.nix
index 33e252be45f..7e646f8737d 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/programs/tmux.nix b/nixos/modules/programs/tmux.nix
index ed077e3daa7..c39908751d2 100644
--- a/nixos/modules/programs/tmux.nix
+++ b/nixos/modules/programs/tmux.nix
@@ -52,7 +52,7 @@ let
     set  -s escape-time       ${toString cfg.escapeTime}
     set  -g history-limit     ${toString cfg.historyLimit}
 
-    ${cfg.extraTmuxConf}
+    ${cfg.extraConfig}
   '';
 
 in {
@@ -102,7 +102,7 @@ in {
         description = "Time in milliseconds for which tmux waits after an escape is input.";
       };
 
-      extraTmuxConf = mkOption {
+      extraConfig = mkOption {
         default = "";
         description = ''
           Additional contents of /etc/tmux.conf
@@ -181,4 +181,8 @@ in {
       };
     };
   };
+
+  imports = [
+    (lib.mkRenamedOptionModule [ "programs" "tmux" "extraTmuxConf" ] [ "programs" "tmux" "extraConfig" ])
+  ];
 }
diff --git a/nixos/modules/programs/traceroute.nix b/nixos/modules/programs/traceroute.nix
new file mode 100644
index 00000000000..4eb0be3f0e0
--- /dev/null
+++ b/nixos/modules/programs/traceroute.nix
@@ -0,0 +1,26 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.traceroute;
+in {
+  options = {
+    programs.traceroute = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to configure a setcap wrapper for traceroute.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    security.wrappers.traceroute = {
+      source = "${pkgs.traceroute}/bin/traceroute";
+      capabilities = "cap_net_raw+p";
+    };
+  };
+}
diff --git a/nixos/modules/programs/way-cooler.nix b/nixos/modules/programs/way-cooler.nix
deleted file mode 100644
index f27bd42bd76..00000000000
--- a/nixos/modules/programs/way-cooler.nix
+++ /dev/null
@@ -1,78 +0,0 @@
-{ config, pkgs, lib, ... }:
-
-with lib;
-
-let
-  cfg = config.programs.way-cooler;
-  way-cooler = pkgs.way-cooler;
-
-  wcWrapped = pkgs.writeShellScriptBin "way-cooler" ''
-    ${cfg.extraSessionCommands}
-    exec ${pkgs.dbus}/bin/dbus-run-session ${way-cooler}/bin/way-cooler
-  '';
-  wcJoined = pkgs.symlinkJoin {
-    name = "way-cooler-wrapped";
-    paths = [ wcWrapped way-cooler ];
-  };
-  configFile = readFile "${way-cooler}/etc/way-cooler/init.lua";
-  spawnBar = ''
-    util.program.spawn_at_startup("lemonbar");
-  '';
-in
-{
-  options.programs.way-cooler = {
-    enable = mkEnableOption "way-cooler";
-
-    extraSessionCommands = mkOption {
-      default     = "";
-      type        = types.lines;
-      example = ''
-        export XKB_DEFAULT_LAYOUT=us,de
-        export XKB_DEFAULT_VARIANT=,nodeadkeys
-        export XKB_DEFAULT_OPTIONS=grp:caps_toggle,
-      '';
-      description = ''
-        Shell commands executed just before way-cooler is started.
-      '';
-    };
-
-    extraPackages = mkOption {
-      type = with types; listOf package;
-      default = with pkgs; [
-        westonLite xwayland dmenu
-      ];
-      example = literalExample ''
-        with pkgs; [
-          westonLite xwayland dmenu
-        ]
-      '';
-      description = ''
-        Extra packages to be installed system wide.
-      '';
-    };
-
-    enableBar = mkOption {
-      type = types.bool;
-      default = true;
-      description = ''
-        Whether to enable an unofficial bar.
-      '';
-    };
-  };
-
-  config = mkIf cfg.enable {
-    environment.systemPackages = [ wcJoined ] ++ cfg.extraPackages;
-
-    security.pam.services.wc-lock = {};
-    environment.etc."way-cooler/init.lua".text = ''
-      ${configFile}
-      ${optionalString cfg.enableBar spawnBar}
-    '';
-
-    hardware.opengl.enable = mkDefault true;
-    fonts.enableDefaultFonts = mkDefault true;
-    programs.dconf.enable = mkDefault true;
-  };
-
-  meta.maintainers = with maintainers; [ gnidorah ];
-}
diff --git a/nixos/modules/programs/zsh/zsh.nix b/nixos/modules/programs/zsh/zsh.nix
index c66c29ed45f..4fbdba47b1d 100644
--- a/nixos/modules/programs/zsh/zsh.nix
+++ b/nixos/modules/programs/zsh/zsh.nix
@@ -15,6 +15,24 @@ let
       (filterAttrs (k: v: v != null) cfg.shellAliases)
   );
 
+  zshStartupNotes = ''
+    # Note that generated /etc/zprofile and /etc/zshrc files do a lot of
+    # non-standard setup to make zsh usable with no configuration by default.
+    #
+    # Which means that unless you explicitly meticulously override everything
+    # generated, interactions between your ~/.zshrc and these files are likely
+    # to be rather surprising.
+    #
+    # Note however, that you can disable loading of the generated /etc/zprofile
+    # and /etc/zshrc (you can't disable loading of /etc/zshenv, but it is
+    # designed to not set anything surprising) by setting `no_global_rcs` option
+    # in ~/.zshenv:
+    #
+    #   echo setopt no_global_rcs >> ~/.zshenv
+    #
+    # See "STARTUP/SHUTDOWN FILES" section of zsh(1) for more info.
+  '';
+
 in
 
 {
@@ -69,6 +87,10 @@ in
 
       promptInit = mkOption {
         default = ''
+          # Note that to manually override this in ~/.zshrc you should run `prompt off`
+          # before setting your PS1 and etc. Otherwise this will likely to interact with
+          # your ~/.zshrc configuration in unexpected ways as the default prompt sets
+          # a lot of different prompt variables.
           autoload -U promptinit && promptinit && prompt walters && setopt prompt_sp
         '';
         description = ''
@@ -100,7 +122,8 @@ in
         ];
         example = [ "EXTENDED_HISTORY" "RM_STAR_WAIT" ];
         description = ''
-          Configure zsh options.
+          Configure zsh options. See
+          <citerefentry><refentrytitle>zshoptions</refentrytitle><manvolnum>1</manvolnum></citerefentry>.
         '';
       };
 
@@ -147,6 +170,14 @@ in
             . ${config.system.build.setEnvironment}
         fi
 
+        HELPDIR="${pkgs.zsh}/share/zsh/$ZSH_VERSION/help"
+
+        # Tell zsh how to find installed completions.
+        for p in ''${(z)NIX_PROFILES}; do
+            fpath+=($p/share/zsh/site-functions $p/share/zsh/$ZSH_VERSION/functions $p/share/zsh/vendor-completions)
+        done
+
+        # Setup custom shell init stuff.
         ${cfge.shellInit}
 
         ${cfg.shellInit}
@@ -161,11 +192,14 @@ in
       ''
         # /etc/zprofile: DO NOT EDIT -- this file has been generated automatically.
         # This file is read for login shells.
+        #
+        ${zshStartupNotes}
 
         # Only execute this file once per shell.
         if [ -n "$__ETC_ZPROFILE_SOURCED" ]; then return; fi
         __ETC_ZPROFILE_SOURCED=1
 
+        # Setup custom login shell init stuff.
         ${cfge.loginShellInit}
 
         ${cfg.loginShellInit}
@@ -180,38 +214,44 @@ in
       ''
         # /etc/zshrc: DO NOT EDIT -- this file has been generated automatically.
         # This file is read for interactive shells.
+        #
+        ${zshStartupNotes}
 
         # Only execute this file once per shell.
         if [ -n "$__ETC_ZSHRC_SOURCED" -o -n "$NOSYSZSHRC" ]; then return; fi
         __ETC_ZSHRC_SOURCED=1
 
-        . /etc/zinputrc
+        ${optionalString (cfg.setOptions != []) ''
+          # Set zsh options.
+          setopt ${concatStringsSep " " cfg.setOptions}
+        ''}
 
-        # Don't export these, otherwise other shells (bash) will try to use same histfile
+        # Setup command line history.
+        # Don't export these, otherwise other shells (bash) will try to use same HISTFILE.
         SAVEHIST=${toString cfg.histSize}
         HISTSIZE=${toString cfg.histSize}
         HISTFILE=${cfg.histFile}
 
-        HELPDIR="${pkgs.zsh}/share/zsh/$ZSH_VERSION/help"
-
-        # Tell zsh how to find installed completions
-        for p in ''${(z)NIX_PROFILES}; do
-            fpath+=($p/share/zsh/site-functions $p/share/zsh/$ZSH_VERSION/functions $p/share/zsh/vendor-completions)
-        done
+        # Configure sane keyboard defaults.
+        . /etc/zinputrc
 
-        ${optionalString cfg.enableGlobalCompInit "autoload -U compinit && compinit"}
+        ${optionalString cfg.enableGlobalCompInit ''
+          # Enable autocompletion.
+          autoload -U compinit && compinit
+        ''}
 
+        # Setup custom interactive shell init stuff.
         ${cfge.interactiveShellInit}
 
         ${cfg.interactiveShellInit}
 
-        ${optionalString (cfg.setOptions != []) "setopt ${concatStringsSep " " cfg.setOptions}"}
-
+        # Setup aliases.
         ${zshAliases}
 
+        # Setup prompt.
         ${cfg.promptInit}
 
-        # Need to disable features to support TRAMP
+        # Disable some features to support TRAMP.
         if [ "$TERM" = dumb ]; then
             unsetopt zle prompt_cr prompt_subst
             unset RPS1 RPROMPT
diff --git a/nixos/modules/rename.nix b/nixos/modules/rename.nix
index 7109ab5a109..3b1b1b8bb55 100644
--- a/nixos/modules/rename.nix
+++ b/nixos/modules/rename.nix
@@ -27,6 +27,21 @@ with lib;
     (mkRemovedOptionModule [ "services.osquery" ] "The osquery module has been removed")
     (mkRemovedOptionModule [ "services.fourStore" ] "The fourStore module has been removed")
     (mkRemovedOptionModule [ "services.fourStoreEndpoint" ] "The fourStoreEndpoint module has been removed")
+    (mkRemovedOptionModule [ "programs" "way-cooler" ] ("way-cooler is abandoned by its author: " +
+      "https://way-cooler.org/blog/2020/01/09/way-cooler-post-mortem.html"))
+    (mkRemovedOptionModule [ "services" "xserver" "multitouch" ] ''
+      services.xserver.multitouch (which uses xf86_input_mtrack) has been removed
+      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 78a82b7154e..c686a6861d0 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 697732426cc..f80d6b3f1ba 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
         '';
       };
 
@@ -165,7 +165,10 @@ in {
       after = [ "network.target" "epmd.socket" ];
       wants = [ "network.target" "epmd.socket" ];
 
-      path = [ cfg.package pkgs.procps ];
+      path = [
+        cfg.package
+        pkgs.coreutils # mkdir/chown/chmod for preStart
+      ];
 
       environment = {
         RABBITMQ_MNESIA_BASE = "${cfg.dataDir}/mnesia";
diff --git a/nixos/modules/services/backup/restic.nix b/nixos/modules/services/backup/restic.nix
index 7e8e91e4b9c..2388f1d6ca1 100644
--- a/nixos/modules/services/backup/restic.nix
+++ b/nixos/modules/services/backup/restic.nix
@@ -103,6 +103,34 @@ in
             Create the repository if it doesn't exist.
           '';
         };
+
+        pruneOpts = mkOption {
+          type = types.listOf types.str;
+          default = [];
+          description = ''
+            A list of options (--keep-* et al.) for 'restic forget
+            --prune', to automatically prune old snapshots.  The
+            'forget' command is run *after* the 'backup' command, so
+            keep that in mind when constructing the --keep-* options.
+          '';
+          example = [
+            "--keep-daily 7"
+            "--keep-weekly 5"
+            "--keep-monthly 12"
+            "--keep-yearly 75"
+          ];
+        };
+
+        dynamicFilesFrom = mkOption {
+          type = with types; nullOr str;
+          default = null;
+          description = ''
+            A script that produces a list of files to back up.  The
+            results of this command are given to the '--files-from'
+            option.
+          '';
+          example = "find /home/matt/git -type d -name .git";
+        };
       };
     }));
     default = {};
@@ -134,25 +162,41 @@ in
         let
           extraOptions = concatMapStrings (arg: " -o ${arg}") backup.extraOptions;
           resticCmd = "${pkgs.restic}/bin/restic${extraOptions}";
+          filesFromTmpFile = "/run/restic-backups-${name}/includes";
+          backupPaths = if (backup.dynamicFilesFrom == null)
+                        then concatStringsSep " " backup.paths
+                        else "--files-from ${filesFromTmpFile}";
+          pruneCmd = optionals (builtins.length backup.pruneOpts > 0) [
+            ( resticCmd + " forget --prune " + (concatStringsSep " " backup.pruneOpts) )
+            ( resticCmd + " check" )
+          ];
         in nameValuePair "restic-backups-${name}" ({
           environment = {
             RESTIC_PASSWORD_FILE = backup.passwordFile;
             RESTIC_REPOSITORY = backup.repository;
           };
-          path = with pkgs; [
-            openssh
-          ];
+          path = [ pkgs.openssh ];
           restartIfChanged = false;
           serviceConfig = {
             Type = "oneshot";
-            ExecStart = "${resticCmd} backup ${concatStringsSep " " backup.extraBackupArgs} ${concatStringsSep " " backup.paths}";
+            ExecStart = [ "${resticCmd} backup ${concatStringsSep " " backup.extraBackupArgs} ${backupPaths}" ] ++ pruneCmd;
             User = backup.user;
+            RuntimeDirectory = "restic-backups-${name}";
           } // optionalAttrs (backup.s3CredentialsFile != null) {
             EnvironmentFile = backup.s3CredentialsFile;
           };
-        } // optionalAttrs backup.initialize {
+        } // optionalAttrs (backup.initialize || backup.dynamicFilesFrom != null) {
           preStart = ''
-            ${resticCmd} snapshots || ${resticCmd} init
+            ${optionalString (backup.initialize) ''
+              ${resticCmd} snapshots || ${resticCmd} init
+            ''}
+            ${optionalString (backup.dynamicFilesFrom != null) ''
+              ${pkgs.writeScript "dynamicFilesFromScript" backup.dynamicFilesFrom} > ${filesFromTmpFile}
+            ''}
+          '';
+        } // optionalAttrs (backup.dynamicFilesFrom != null) {
+          postStart = ''
+            rm ${filesFromTmpFile}
           '';
         })
       ) config.services.restic.backups;
diff --git a/nixos/modules/services/cluster/kubernetes/pki.nix b/nixos/modules/services/cluster/kubernetes/pki.nix
index 733479e24c9..4275563f1a3 100644
--- a/nixos/modules/services/cluster/kubernetes/pki.nix
+++ b/nixos/modules/services/cluster/kubernetes/pki.nix
@@ -20,6 +20,7 @@ let
         size = 2048;
     };
     CN = top.masterAddress;
+    hosts = cfg.cfsslAPIExtraSANs;
   });
 
   cfsslAPITokenBaseName = "apitoken.secret";
@@ -66,6 +67,15 @@ in
       type = bool;
     };
 
+    cfsslAPIExtraSANs = mkOption {
+      description = ''
+        Extra x509 Subject Alternative Names to be added to the cfssl API webserver TLS cert.
+      '';
+      default = [];
+      example = [ "subdomain.example.com" ];
+      type = listOf str;
+    };
+
     genCfsslAPIToken = mkOption {
       description = ''
         Whether to automatically generate cfssl API-token secret,
diff --git a/nixos/modules/services/continuous-integration/buildkite-agent.nix b/nixos/modules/services/continuous-integration/buildkite-agent.nix
index 32f361454bc..58bce654941 100644
--- a/nixos/modules/services/continuous-integration/buildkite-agent.nix
+++ b/nixos/modules/services/continuous-integration/buildkite-agent.nix
@@ -50,8 +50,8 @@ in
       };
 
       runtimePackages = mkOption {
-        default = [ pkgs.bash pkgs.nix ];
-        defaultText = "[ pkgs.bash pkgs.nix ]";
+        default = [ pkgs.bash pkgs.gnutar pkgs.gzip pkgs.git pkgs.nix ];
+        defaultText = "[ pkgs.bash pkgs.gnutar pkgs.gzip pkgs.git pkgs.nix ]";
         description = "Add programs to the buildkite-agent environment";
         type = types.listOf types.package;
       };
@@ -74,13 +74,12 @@ in
         '';
       };
 
-      meta-data = mkOption {
-        type = types.str;
-        default = "";
-        example = "queue=default,docker=true,ruby2=true";
+      tags = mkOption {
+        type = types.attrsOf types.str;
+        default = {};
+        example = { queue = "default"; docker = "true"; ruby2 ="true"; };
         description = ''
-          Meta data for the agent. This is a comma-separated list of
-          <code>key=value</code> pairs.
+          Tags for the agent.
         '';
       };
 
@@ -93,26 +92,20 @@ in
         '';
       };
 
-      openssh =
-        { privateKeyPath = mkOption {
-            type = types.path;
-            description = ''
-              Private agent key.
+      privateSshKeyPath = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        ## maximum care is taken so that secrets (ssh keys and the CI token)
+        ## don't end up in the Nix store.
+        apply = final: if final == null then null else toString final;
 
-              A run-time path to the key file, which is supposed to be provisioned
-              outside of Nix store.
-            '';
-          };
-          publicKeyPath = mkOption {
-            type = types.path;
-            description = ''
-              Public agent key.
-
-              A run-time path to the key file, which is supposed to be provisioned
-              outside of Nix store.
-            '';
-          };
-        };
+        description = ''
+          OpenSSH private key
+
+          A run-time path to the key file, which is supposed to be provisioned
+          outside of Nix store.
+        '';
+      };
 
       hooks = mkHookOptions [
         { name = "checkout";
@@ -181,18 +174,26 @@ in
           instead.
         '';
       };
+
+      shell = mkOption {
+        type = types.str;
+        default = "${pkgs.bash}/bin/bash -e -c";
+        description = ''
+          Command that buildkite-agent 3 will execute when it spawns a shell.
+        '';
+      };
     };
   };
 
   config = mkIf config.services.buildkite-agent.enable {
-    users.users.buildkite-agent =
-      { name = "buildkite-agent";
-        home = cfg.dataDir;
-        createHome = true;
-        description = "Buildkite agent user";
-        extraGroups = [ "keys" ];
-        isSystemUser = true;
-      };
+    users.users.buildkite-agent = {
+      name = "buildkite-agent";
+      home = cfg.dataDir;
+      createHome = true;
+      description = "Buildkite agent user";
+      extraGroups = [ "keys" ];
+      isSystemUser = true;
+    };
 
     environment.systemPackages = [ cfg.package ];
 
@@ -210,17 +211,18 @@ in
         ##     don't end up in the Nix store.
         preStart = let
           sshDir = "${cfg.dataDir}/.ssh";
+          tagStr = lib.concatStringsSep "," (lib.mapAttrsToList (name: value: "${name}=${value}") cfg.tags);
         in
-          ''
+          optionalString (cfg.privateSshKeyPath != null) ''
             mkdir -m 0700 -p "${sshDir}"
-            cp -f "${toString cfg.openssh.privateKeyPath}" "${sshDir}/id_rsa"
-            cp -f "${toString cfg.openssh.publicKeyPath}"  "${sshDir}/id_rsa.pub"
-            chmod 600 "${sshDir}"/id_rsa*
-
+            cp -f "${toString cfg.privateSshKeyPath}" "${sshDir}/id_rsa"
+            chmod 600 "${sshDir}"/id_rsa
+          '' + ''
             cat > "${cfg.dataDir}/buildkite-agent.cfg" <<EOF
             token="$(cat ${toString cfg.tokenPath})"
             name="${cfg.name}"
-            meta-data="${cfg.meta-data}"
+            shell="${cfg.shell}"
+            tags="${tagStr}"
             build-path="${cfg.dataDir}/builds"
             hooks-path="${cfg.hooksPath}"
             ${cfg.extraConfig}
@@ -228,11 +230,14 @@ in
           '';
 
         serviceConfig =
-          { ExecStart = "${pkgs.buildkite-agent}/bin/buildkite-agent start --config /var/lib/buildkite-agent/buildkite-agent.cfg";
+          { ExecStart = "${cfg.package}/bin/buildkite-agent start --config /var/lib/buildkite-agent/buildkite-agent.cfg";
             User = "buildkite-agent";
             RestartSec = 5;
             Restart = "on-failure";
             TimeoutSec = 10;
+            # set a long timeout to give buildkite-agent a chance to finish current builds
+            TimeoutStopSec = "2 min";
+            KillMode = "mixed";
           };
       };
 
@@ -246,8 +251,11 @@ in
     ];
   };
   imports = [
-    (mkRenamedOptionModule [ "services" "buildkite-agent" "token" ]                [ "services" "buildkite-agent" "tokenPath" ])
-    (mkRenamedOptionModule [ "services" "buildkite-agent" "openssh" "privateKey" ] [ "services" "buildkite-agent" "openssh" "privateKeyPath" ])
-    (mkRenamedOptionModule [ "services" "buildkite-agent" "openssh" "publicKey" ]  [ "services" "buildkite-agent" "openssh" "publicKeyPath" ])
+    (mkRenamedOptionModule [ "services" "buildkite-agent" "token" ]                    [ "services" "buildkite-agent" "tokenPath" ])
+    (mkRenamedOptionModule [ "services" "buildkite-agent" "openssh" "privateKey" ]     [ "services" "buildkite-agent" "privateSshKeyPath" ])
+    (mkRenamedOptionModule [ "services" "buildkite-agent" "openssh" "privateKeyPath" ] [ "services" "buildkite-agent" "privateSshKeyPath" ])
+    (mkRemovedOptionModule [ "services" "buildkite-agent" "openssh" "publicKey" ]      "SSH public keys aren't necessary to clone private repos.")
+    (mkRemovedOptionModule [ "services" "buildkite-agent" "openssh" "publicKeyPath" ]  "SSH public keys aren't necessary to clone private repos.")
+    (mkRenamedOptionModule [ "services" "buildkite-agent" "meta-data"]                 [ "services" "buildkite-agent" "tags" ])
   ];
 }
diff --git a/nixos/modules/services/continuous-integration/hydra/default.nix b/nixos/modules/services/continuous-integration/hydra/default.nix
index 30c5550f71c..8b56207590a 100644
--- a/nixos/modules/services/continuous-integration/hydra/default.nix
+++ b/nixos/modules/services/continuous-integration/hydra/default.nix
@@ -167,7 +167,7 @@ in
 
       buildMachinesFiles = mkOption {
         type = types.listOf types.path;
-        default = [ "/etc/nix/machines" ];
+        default = optional (config.nix.buildMachines != []) "/etc/nix/machines";
         example = [ "/etc/nix/machines" "/var/lib/hydra/provisioner/machines" ];
         description = "List of files containing build machines.";
       };
@@ -333,7 +333,7 @@ in
           IN_SYSTEMD = "1"; # to get log severity levels
         };
         serviceConfig =
-          { ExecStart = "@${cfg.package}/bin/hydra-queue-runner hydra-queue-runner -v --option build-use-substitutes ${boolToString cfg.useSubstitutes}";
+          { ExecStart = "@${cfg.package}/bin/hydra-queue-runner hydra-queue-runner -v";
             ExecStopPost = "${cfg.package}/bin/hydra-queue-runner --unlock";
             User = "hydra-queue-runner";
             Restart = "always";
diff --git a/nixos/modules/services/databases/victoriametrics.nix b/nixos/modules/services/databases/victoriametrics.nix
new file mode 100644
index 00000000000..cb6bf8508fb
--- /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 cca98c43dc7..8fa108c4f9d 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 f20860af6e1..e598b018645 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/fwupd.nix b/nixos/modules/services/hardware/fwupd.nix
index 51877970a8b..e586af25c2b 100644
--- a/nixos/modules/services/hardware/fwupd.nix
+++ b/nixos/modules/services/hardware/fwupd.nix
@@ -53,7 +53,7 @@ in {
 
       blacklistPlugins = mkOption {
         type = types.listOf types.str;
-        default = [ "test" ];
+        default = [];
         example = [ "udev" ];
         description = ''
           Allow blacklisting specific plugins
@@ -91,6 +91,9 @@ in {
 
   ###### implementation
   config = mkIf cfg.enable {
+    # Disable test related plug-ins implicitly so that users do not have to care about them.
+    services.fwupd.blacklistPlugins = cfg.package.defaultBlacklistedPlugins;
+
     environment.systemPackages = [ cfg.package ];
 
     environment.etc = {
diff --git a/nixos/modules/services/hardware/irqbalance.nix b/nixos/modules/services/hardware/irqbalance.nix
index b139154432c..c79e0eb83ec 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 e917209f3d1..43dc185cdd7 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 36dda619ad0..0bb0eaedad5 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 75442c7cdb5..2d5fb40fad3 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 00000000000..0de5ba42811
--- /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/gitea.nix b/nixos/modules/services/misc/gitea.nix
index 258476dd9fe..38910a5a005 100644
--- a/nixos/modules/services/misc/gitea.nix
+++ b/nixos/modules/services/misc/gitea.nix
@@ -364,7 +364,7 @@ in
           ''}
           sed -e "s,#secretkey#,$KEY,g" \
               -e "s,#dbpass#,$DBPASS,g" \
-              -e "s,#jwtsecet#,$JWTSECET,g" \
+              -e "s,#jwtsecret#,$JWTSECRET,g" \
               -e "s,#mailerpass#,$MAILERPASSWORD,g" \
               -i ${runConfig}
           chmod 640 ${runConfig} ${secretKey} ${jwtSecret}
diff --git a/nixos/modules/services/misc/home-assistant.nix b/nixos/modules/services/misc/home-assistant.nix
index cc113ca2d0c..d63f38e93b8 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/misc/paperless.nix b/nixos/modules/services/misc/paperless.nix
index 3985dc0b303..bfaf760fb83 100644
--- a/nixos/modules/services/misc/paperless.nix
+++ b/nixos/modules/services/misc/paperless.nix
@@ -123,9 +123,9 @@ in
   config = mkIf cfg.enable {
 
     systemd.tmpfiles.rules = [
-      "d '${cfg.dataDir}' - ${cfg.user} ${cfg.user} - -"
+      "d '${cfg.dataDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
     ] ++ (optional cfg.consumptionDirIsPublic
-      "d '${cfg.consumptionDir}' 777 ${cfg.user} ${cfg.user} - -"
+      "d '${cfg.consumptionDir}' 777 - - - -"
       # If the consumption dir is not created here, it's automatically created by
       # 'manage' with the default permissions.
     );
@@ -169,17 +169,15 @@ in
     };
 
     users = optionalAttrs (cfg.user == defaultUser) {
-      users = [{
-        name = defaultUser;
+      users.${defaultUser} = {
         group = defaultUser;
         uid = config.ids.uids.paperless;
         home = cfg.dataDir;
-      }];
+      };
 
-      groups = [{
-        name = defaultUser;
+      groups.${defaultUser} = {
         gid = config.ids.gids.paperless;
-      }];
+      };
     };
   };
 }
diff --git a/nixos/modules/services/monitoring/nagios.nix b/nixos/modules/services/monitoring/nagios.nix
index 3ca79dddaf5..9ac6869068f 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 9af6b1d94f3..4534d150885 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 f40819e826b..d50564717ea 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 00000000000..44b15cb2034
--- /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 263b70d04a5..a43ac656f66 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 54fe70f7ccc..01a16698384 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/corerad.nix b/nixos/modules/services/networking/corerad.nix
new file mode 100644
index 00000000000..1a2c4aec665
--- /dev/null
+++ b/nixos/modules/services/networking/corerad.nix
@@ -0,0 +1,46 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.corerad;
+in {
+  meta = {
+    maintainers = with maintainers; [ mdlayher ];
+  };
+
+  options.services.corerad = {
+    enable = mkEnableOption "CoreRAD IPv6 NDP RA daemon";
+
+    configFile = mkOption {
+      type = types.path;
+      example = literalExample "\"\${pkgs.corerad}/etc/corerad/corerad.toml\"";
+      description = "Path to CoreRAD TOML configuration file.";
+    };
+
+    package = mkOption {
+      default = pkgs.corerad;
+      defaultText = literalExample "pkgs.corerad";
+      type = types.package;
+      description = "CoreRAD package to use.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.corerad = {
+      description = "CoreRAD IPv6 NDP RA daemon";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        LimitNPROC = 512;
+        LimitNOFILE = 1048576;
+        CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_RAW";
+        AmbientCapabilities = "CAP_NET_ADMIN CAP_NET_RAW";
+        NoNewPrivileges = true;
+        DynamicUser = true;
+        ExecStart = "${getBin cfg.package}/bin/corerad -c=${cfg.configFile}";
+        Restart = "on-failure";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/dhcpcd.nix b/nixos/modules/services/networking/dhcpcd.nix
index 6fbc014db71..f476b147a57 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 8edcf925dbf..00000000000
--- 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 afc7880392a..00000000000
--- 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 00000000000..e48eb729103
--- /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 85f52be8a6a..495102cb7ee 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 1cc1dd3f2f6..47364ecb846 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 5eb50a13ca9..a2f91a4200b 100644
--- a/nixos/modules/services/networking/kresd.nix
+++ b/nixos/modules/services/networking/kresd.nix
@@ -3,14 +3,39 @@
 with lib;
 
 let
-
   cfg = config.services.kresd;
-  package = pkgs.knot-resolver;
 
-  configFile = pkgs.writeText "kresd.conf" cfg.extraConfig;
-in
+  # Convert systemd-style address specification to kresd config line(s).
+  # On Nix level we don't attempt to precisely validate the address specifications.
+  mkListen = kind: addr: let
+    al_v4 = builtins.match "([0-9.]\+):([0-9]\+)" addr;
+    al_v6 = builtins.match "\\[(.\+)]:([0-9]\+)" addr;
+    al_portOnly = builtins.match "()([0-9]\+)" addr;
+    al = findFirst (a: a != null)
+      (throw "services.kresd.*: incorrect address specification '${addr}'")
+      [ al_v4 al_v6 al_portOnly ];
+    port = last al;
+    addrSpec = if al_portOnly == null then "'${head al}'" else "{'::', '127.0.0.1'}";
+    in # freebind is set for compatibility with earlier kresd services;
+       # it could be configurable, for example.
+      ''
+        net.listen(${addrSpec}, ${port}, { kind = '${kind}', freebind = true })
+      '';
 
-{
+  configFile = pkgs.writeText "kresd.conf" (
+    optionalString (cfg.listenDoH != []) ''
+      modules.load('http')
+    ''
+    + concatMapStrings (mkListen "dns") cfg.listenPlain
+    + concatMapStrings (mkListen "tls") cfg.listenTLS
+    + concatMapStrings (mkListen "doh") cfg.listenDoH
+    + cfg.extraConfig
+  );
+
+  package = pkgs.knot-resolver.override {
+    extraFeatures = cfg.listenDoH != [];
+  };
+in {
   meta.maintainers = [ maintainers.vcunat /* upstream developer */ ];
 
   imports = [
@@ -22,6 +47,7 @@ in
           value
       )
     )
+    (mkRemovedOptionModule [ "services" "kresd" "cacheDir" ] "Please use (bind-)mounting instead.")
   ];
 
   ###### interface
@@ -32,8 +58,8 @@ in
       description = ''
         Whether to enable knot-resolver domain name server.
         DNSSEC validation is turned on by default.
-        You can run <literal>sudo nc -U /run/kresd/control</literal>
-        and give commands interactively to kresd.
+        You can run <literal>sudo nc -U /run/knot-resolver/control/1</literal>
+        and give commands interactively to kresd@1.service.
       '';
     };
     extraConfig = mkOption {
@@ -43,16 +69,10 @@ in
         Extra lines to be added verbatim to the generated configuration file.
       '';
     };
-    cacheDir = mkOption {
-      type = types.path;
-      default = "/var/cache/kresd";
-      description = ''
-        Directory for caches.  They are intended to survive reboots.
-      '';
-    };
     listenPlain = mkOption {
       type = with types; listOf str;
       default = [ "[::1]:53" "127.0.0.1:53" ];
+      example = [ "53" ];
       description = ''
         What addresses and ports the server should listen on.
         For detailed syntax see ListenStream in man systemd.socket.
@@ -67,75 +87,59 @@ 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 8484).
+        For detailed syntax see ListenStream in man systemd.socket.
+      '';
+    };
+    instances = mkOption {
+      type = types.ints.unsigned;
+      default = 1;
+      description = ''
+        The number of instances to start.  They will be called kresd@{1,2,...}.service.
+        Knot Resolver uses no threads, so this is the way to scale.
+        You can dynamically start/stop them at will, so this is just system default.
+      '';
+    };
     # TODO: perhaps options for more common stuff like cache size or forwarding
   };
 
   ###### implementation
   config = mkIf cfg.enable {
-    environment.etc."kresd.conf".source = configFile; # not required
+    environment.etc."knot-resolver/kresd.conf".source = configFile; # not required
 
-    users.users.kresd =
-      { uid = config.ids.uids.kresd;
-        group = "kresd";
+    users.users.knot-resolver =
+      { isSystemUser = true;
+        group = "knot-resolver";
         description = "Knot-resolver daemon user";
       };
-    users.groups.kresd.gid = config.ids.gids.kresd;
+    users.groups.knot-resolver.gid = null;
 
-    systemd.sockets.kresd = rec {
-      wantedBy = [ "sockets.target" ];
-      before = wantedBy;
-      listenStreams = cfg.listenPlain;
-      socketConfig = {
-        ListenDatagram = listenStreams;
-        FreeBind = true;
-        FileDescriptorName = "dns";
-      };
-    };
+    systemd.packages = [ package ]; # the units are patched inside the package a bit
 
-    systemd.sockets.kresd-tls = mkIf (cfg.listenTLS != []) rec {
-      wantedBy = [ "sockets.target" ];
-      before = wantedBy;
-      partOf = [ "kresd.socket" ];
-      listenStreams = cfg.listenTLS;
-      socketConfig = {
-        FileDescriptorName = "tls";
-        FreeBind = true;
-        Service = "kresd.service";
-      };
+    systemd.targets.kresd = { # configure units started by default
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "kres-cache-gc.service" ]
+        ++ map (i: "kresd@${toString i}.service") (range 1 cfg.instances);
     };
-
-    systemd.sockets.kresd-control = rec {
-      wantedBy = [ "sockets.target" ];
-      before = wantedBy;
-      partOf = [ "kresd.socket" ];
-      listenStreams = [ "/run/kresd/control" ];
-      socketConfig = {
-        FileDescriptorName = "control";
-        Service = "kresd.service";
-        SocketMode = "0660"; # only root user/group may connect and control kresd
-      };
+    systemd.services."kresd@".serviceConfig = {
+      ExecStart = "${package}/bin/kresd --noninteractive "
+        + "-c ${package}/lib/knot-resolver/distro-preconfig.lua -c ${configFile}";
+      # Ensure correct ownership in case UID or GID changes.
+      CacheDirectory = "knot-resolver";
+      CacheDirectoryMode = "0750";
     };
 
-    systemd.tmpfiles.rules = [ "d '${cfg.cacheDir}' 0770 kresd kresd - -" ];
+    environment.etc."tmpfiles.d/knot-resolver.conf".source =
+      "${package}/lib/tmpfiles.d/knot-resolver.conf";
 
-    systemd.services.kresd = {
-      description = "Knot-resolver daemon";
-
-      serviceConfig = {
-        User = "kresd";
-        Type = "notify";
-        WorkingDirectory = cfg.cacheDir;
-        Restart = "on-failure";
-        Sockets = [ "kresd.socket" "kresd-control.socket" ]
-          ++ optional (cfg.listenTLS != []) "kresd-tls.socket";
-      };
-
-      # Trust anchor goes from dns-root-data by default.
-      script = ''
-        exec '${package}/bin/kresd' --config '${configFile}' --forks=1
-      '';
-
-      requires = [ "kresd.socket" ];
-    };
+    # Try cleaning up the previously default location of cache file.
+    # Note that /var/cache/* should always be safe to remove.
+    # TODO: remove later, probably between 20.09 and 21.03
+    systemd.tmpfiles.rules = [ "R /var/cache/kresd" ];
   };
 }
diff --git a/nixos/modules/services/networking/matterbridge.nix b/nixos/modules/services/networking/matterbridge.nix
index bad35133459..b8b4f37c84a 100644
--- a/nixos/modules/services/networking/matterbridge.nix
+++ b/nixos/modules/services/networking/matterbridge.nix
@@ -111,7 +111,7 @@ in
       serviceConfig = {
         User = cfg.user;
         Group = cfg.group;
-        ExecStart = "${pkgs.matterbridge.bin}/bin/matterbridge -conf ${matterbridgeConfToml}";
+        ExecStart = "${pkgs.matterbridge}/bin/matterbridge -conf ${matterbridgeConfToml}";
         Restart = "always";
         RestartSec = "10";
       };
diff --git a/nixos/modules/services/networking/ndppd.nix b/nixos/modules/services/networking/ndppd.nix
index 92088623517..e015f76f622 100644
--- a/nixos/modules/services/networking/ndppd.nix
+++ b/nixos/modules/services/networking/ndppd.nix
@@ -161,7 +161,25 @@ in {
       documentation = [ "man:ndppd(1)" "man:ndppd.conf(5)" ];
       after = [ "network-pre.target" ];
       wantedBy = [ "multi-user.target" ];
-      serviceConfig.ExecStart = "${pkgs.ndppd}/bin/ndppd -c ${ndppdConf}";
+      serviceConfig = {
+        ExecStart = "${pkgs.ndppd}/bin/ndppd -c ${ndppdConf}";
+
+        # Sandboxing
+        CapabilityBoundingSet = "CAP_NET_RAW CAP_NET_ADMIN";
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        RestrictAddressFamilies = "AF_INET6 AF_PACKET AF_NETLINK";
+        RestrictNamespaces = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+      };
     };
   };
 }
diff --git a/nixos/modules/services/networking/nsd.nix b/nixos/modules/services/networking/nsd.nix
index 344396638a6..429580e5c6c 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/syncthing.nix b/nixos/modules/services/networking/syncthing.nix
index 47b10e408c0..5b3eb6f04b4 100644
--- a/nixos/modules/services/networking/syncthing.nix
+++ b/nixos/modules/services/networking/syncthing.nix
@@ -484,6 +484,24 @@ in {
               -gui-address=${cfg.guiAddress} \
               -home=${cfg.configDir}
           '';
+          MemoryDenyWriteExecute = true;
+          NoNewPrivileges = true;
+          PrivateDevices = true;
+          PrivateMounts = true;
+          PrivateTmp = true;
+          PrivateUsers = true;
+          ProtectControlGroups = true;
+          ProtectHostname = true;
+          ProtectKernelModules = true;
+          ProtectKernelTunables = true;
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          RestrictSUIDSGID = true;
+          CapabilityBoundingSet = [
+            "~CAP_SYS_PTRACE" "~CAP_SYS_ADMIN"
+            "~CAP_SETGID" "~CAP_SETUID" "~CAP_SETPCAP"
+            "~CAP_SYS_TIME" "~CAP_KILL"
+          ];
         };
       };
       syncthing-init = mkIf (
diff --git a/nixos/modules/services/networking/unifi.nix b/nixos/modules/services/networking/unifi.nix
index c922ba15960..4bdfa8143dc 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/networking/wireguard.nix b/nixos/modules/services/networking/wireguard.nix
index 980961225c9..7785861a730 100644
--- a/nixos/modules/services/networking/wireguard.nix
+++ b/nixos/modules/services/networking/wireguard.nix
@@ -151,7 +151,7 @@ let
       publicKey = mkOption {
         example = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
         type = types.str;
-        description = "The base64 public key the peer.";
+        description = "The base64 public key of the peer.";
       };
 
       presharedKey = mkOption {
diff --git a/nixos/modules/services/networking/wpa_supplicant.nix b/nixos/modules/services/networking/wpa_supplicant.nix
index 8f05c3949fb..de0f11595a9 100644
--- a/nixos/modules/services/networking/wpa_supplicant.nix
+++ b/nixos/modules/services/networking/wpa_supplicant.nix
@@ -233,6 +233,7 @@ in {
       path = [ pkgs.wpa_supplicant ];
 
       script = ''
+        iface_args="-s -u -D${cfg.driver} -c ${configFile}"
         ${if ifaces == [] then ''
           for i in $(cd /sys/class/net && echo *); do
             DEVTYPE=
@@ -240,14 +241,14 @@ in {
             if [ -e "$UEVENT_PATH" ]; then
               source "$UEVENT_PATH"
               if [ "$DEVTYPE" = "wlan" -o -e /sys/class/net/$i/wireless ]; then
-                ifaces="$ifaces''${ifaces:+ -N} -i$i"
+                args+="''${args:+ -N} -i$i $iface_args"
               fi
             fi
           done
         '' else ''
-          ifaces="${concatStringsSep " -N " (map (i: "-i${i}") ifaces)}"
+          args="${concatMapStringsSep " -N " (i: "-i${i} $iface_args") ifaces}"
         ''}
-        exec wpa_supplicant -s -u -D${cfg.driver} -c ${configFile} $ifaces
+        exec wpa_supplicant $args
       '';
     };
 
diff --git a/nixos/modules/services/networking/xandikos.nix b/nixos/modules/services/networking/xandikos.nix
new file mode 100644
index 00000000000..87c029156b9
--- /dev/null
+++ b/nixos/modules/services/networking/xandikos.nix
@@ -0,0 +1,148 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xandikos;
+in
+{
+
+  options = {
+    services.xandikos = {
+      enable = mkEnableOption "Xandikos CalDAV and CardDAV server";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.xandikos;
+        defaultText = "pkgs.xandikos";
+        description = "The Xandikos package to use.";
+      };
+
+      address = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = ''
+          The IP address on which Xandikos will listen.
+          By default listens on localhost.
+        '';
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 8080;
+        description = "The port of the Xandikos web application";
+      };
+
+      routePrefix = mkOption {
+        type = types.str;
+        default = "/";
+        description = ''
+          Path to Xandikos.
+          Useful when Xandikos is behind a reverse proxy.
+        '';
+      };
+
+      extraOptions = mkOption {
+        default = [];
+        type = types.listOf types.str;
+        example = literalExample ''
+          [ "--autocreate"
+            "--defaults"
+            "--current-user-principal user"
+            "--dump-dav-xml"
+          ]
+        '';
+        description = ''
+          Extra command line arguments to pass to xandikos.
+        '';
+      };
+
+      nginx = mkOption {
+        default = {};
+        description = ''
+          Configuration for nginx reverse proxy.
+        '';
+
+        type = types.submodule {
+          options = {
+            enable = mkOption {
+              type = types.bool;
+              default = false;
+              description = ''
+                Configure the nginx reverse proxy settings.
+              '';
+            };
+
+            hostName = mkOption {
+              type = types.str;
+              description = ''
+                The hostname use to setup the virtualhost configuration
+              '';
+            };
+          };
+        };
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable (
+    mkMerge [
+      {
+        meta.maintainers = [ lib.maintainers."0x4A6F" ];
+
+        systemd.services.xandikos = {
+          description = "A Simple Calendar and Contact Server";
+          after = [ "network.target" ];
+          wantedBy = [ "multi-user.target" ];
+
+          serviceConfig = {
+            User = "xandikos";
+            Group = "xandikos";
+            DynamicUser = "yes";
+            RuntimeDirectory = "xandikos";
+            StateDirectory = "xandikos";
+            StateDirectoryMode = "0700";
+            PrivateDevices = true;
+            # Sandboxing
+            CapabilityBoundingSet = "CAP_NET_RAW CAP_NET_ADMIN";
+            ProtectSystem = "strict";
+            ProtectHome = true;
+            PrivateTmp = true;
+            ProtectKernelTunables = true;
+            ProtectKernelModules = true;
+            ProtectControlGroups = true;
+            RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX AF_PACKET AF_NETLINK";
+            RestrictNamespaces = true;
+            LockPersonality = true;
+            MemoryDenyWriteExecute = true;
+            RestrictRealtime = true;
+            RestrictSUIDSGID = true;
+            ExecStart = ''
+              ${cfg.package}/bin/xandikos \
+                --directory /var/lib/xandikos \
+                --listen_address ${cfg.address} \
+                --port ${toString cfg.port} \
+                --route-prefix ${cfg.routePrefix} \
+                ${lib.concatStringsSep " " cfg.extraOptions}
+            '';
+          };
+        };
+      }
+
+      (
+        mkIf cfg.nginx.enable {
+          services.nginx = {
+            enable = true;
+            virtualHosts."${cfg.nginx.hostName}" = {
+              locations."/" = {
+                proxyPass = "http://${cfg.address}:${toString cfg.port}/";
+              };
+            };
+          };
+        }
+      )
+    ]
+  );
+}
diff --git a/nixos/modules/services/networking/zerotierone.nix b/nixos/modules/services/networking/zerotierone.nix
index 764af3846fe..069e15a909b 100644
--- a/nixos/modules/services/networking/zerotierone.nix
+++ b/nixos/modules/services/networking/zerotierone.nix
@@ -38,10 +38,13 @@ in
   config = mkIf cfg.enable {
     systemd.services.zerotierone = {
       description = "ZeroTierOne";
-      path = [ cfg.package ];
-      bindsTo = [ "network-online.target" ];
-      after = [ "network-online.target" ];
+
       wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      wants = [ "network-online.target" ];
+
+      path = [ cfg.package ];
+
       preStart = ''
         mkdir -p /var/lib/zerotier-one/networks.d
         chmod 700 /var/lib/zerotier-one
@@ -53,6 +56,7 @@ in
         ExecStart = "${cfg.package}/bin/zerotier-one -p${toString cfg.port}";
         Restart = "always";
         KillMode = "process";
+        TimeoutStopSec = 5;
       };
     };
 
diff --git a/nixos/modules/services/search/solr.nix b/nixos/modules/services/search/solr.nix
index b2176225493..a8615a20a1c 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 d1817db0755..a63be0ee766 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 716ae7a2d2f..cb748c93d24 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 4a174564dd2..e7a9cefdef3 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 b0ab8fadcbe..6a8a3a93327 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/torrent/transmission.nix b/nixos/modules/services/torrent/transmission.nix
index aa1acdf7d20..5ba72e8d773 100644
--- a/nixos/modules/services/torrent/transmission.nix
+++ b/nixos/modules/services/torrent/transmission.nix
@@ -129,19 +129,23 @@ in
     # It's useful to have transmission in path, e.g. for remote control
     environment.systemPackages = [ pkgs.transmission ];
 
-    users.users = optionalAttrs (cfg.user == "transmission") (singleton
-      { name = "transmission";
+    users.users = optionalAttrs (cfg.user == "transmission") ({
+      transmission = {
+        name = "transmission";
         group = cfg.group;
         uid = config.ids.uids.transmission;
         description = "Transmission BitTorrent user";
         home = homeDir;
         createHome = true;
-      });
+      };
+    });
 
-    users.groups = optionalAttrs (cfg.group == "transmission") (singleton
-      { name = "transmission";
+    users.groups = optionalAttrs (cfg.group == "transmission") ({
+      transmission = {
+        name = "transmission";
         gid = config.ids.gids.transmission;
-      });
+      };
+    });
 
     # AppArmor profile
     security.apparmor.profiles = mkIf apparmor [
diff --git a/nixos/modules/services/web-apps/dokuwiki.nix b/nixos/modules/services/web-apps/dokuwiki.nix
new file mode 100644
index 00000000000..07af7aa0dfe
--- /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 e00a47191c6..56265e80957 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 8a109b39bb5..e9ed53857d8 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 595d070d940..1196780cf6e 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 ad4f39fbf52..c48a4409737 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 ee8447810c6..00719512834 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 4460f89ec5c..832c8b30ee9 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
@@ -29,8 +29,8 @@ let
 
   listenInfo = unique (concatMap mkListenInfo vhosts);
 
+  enableHttp2 = any (vhost: vhost.http2) vhosts;
   enableSSL = any (listen: listen.ssl) listenInfo;
-
   enableUserDir = any (vhost: vhost.enableUserDir) vhosts;
 
   # NOTE: generally speaking order of modules is very important
@@ -41,23 +41,19 @@ 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 enableHttp2 "http2"
     ++ 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;
-
-
-  allDenied = "Require all denied";
-  allGranted = "Require all granted";
-
+    ++ 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;
 
-  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 +62,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 +84,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);
 
@@ -167,6 +165,7 @@ let
             SSLCertificateFile ${sslServerCert}
             SSLCertificateKeyFile ${sslServerKey}
             ${optionalString (sslServerChain != null) "SSLCertificateChainFile ${sslServerChain}"}
+            ${optionalString hostOpts.http2 "Protocols h2 h2c http/1.1"}
             ${acmeChallenge}
             ${mkVHostCommonConf hostOpts}
         </VirtualHost>
@@ -179,11 +178,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 +216,7 @@ let
         <Directory "${documentRoot}">
             Options Indexes FollowSymLinks
             AllowOverride None
-            ${allGranted}
+            Require all granted
         </Directory>
 
         ${optionalString hostOpts.enableUserDir ''
@@ -218,23 +239,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 +258,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 +280,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 +295,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 +333,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 +366,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 +399,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 +414,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 +453,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 +461,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 +469,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 +533,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 +566,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 +587,9 @@ in
 
   };
 
+  # implementation
 
-  ###### implementation
-
-  config = mkIf config.services.httpd.enable {
+  config = mkIf cfg.enable {
 
     assertions = [
       {
@@ -606,28 +617,36 @@ 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 = [ pkg ];
 
-    environment.systemPackages = [httpd];
+    # required for "apachectl configtest"
+    environment.etc."httpd/httpd.conf".source = httpdConf;
 
     services.httpd.phpOptions =
       ''
@@ -664,6 +683,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;
@@ -675,35 +703,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 00000000000..8ea88f94f97
--- /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 f2e92cda05f..263980add8b 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 = {
@@ -135,6 +135,15 @@ in
       description = "Path to server SSL chain file.";
     };
 
+    http2 = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable HTTP 2. HTTP/2 is supported in all multi-processing modules that come with httpd. <emphasis>However, if you use the prefork mpm, there will
+        be severe restrictions.</emphasis> Refer to <link xlink:href="https://httpd.apache.org/docs/2.4/howto/http2.html#mpm-config"/> for details.
+      '';
+    };
+
     adminAddr = mkOption {
       type = types.nullOr types.str;
       default = null;
@@ -175,6 +184,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 +246,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/nginx/gitweb.nix b/nixos/modules/services/web-servers/nginx/gitweb.nix
index 272fd148018..f7fb07bb797 100644
--- a/nixos/modules/services/web-servers/nginx/gitweb.nix
+++ b/nixos/modules/services/web-servers/nginx/gitweb.nix
@@ -3,8 +3,9 @@
 with lib;
 
 let
-  cfg = config.services.gitweb;
-  package = pkgs.gitweb.override (optionalAttrs cfg.gitwebTheme {
+  cfg = config.services.nginx.gitweb;
+  gitwebConfig = config.services.gitweb;
+  package = pkgs.gitweb.override (optionalAttrs gitwebConfig.gitwebTheme {
     gitwebTheme = true;
   });
 
@@ -17,13 +18,45 @@ in
       default = false;
       type = types.bool;
       description = ''
-        If true, enable gitweb in nginx. Access it at http://yourserver/gitweb
+        If true, enable gitweb in nginx.
+      '';
+    };
+
+    location = mkOption {
+      default = "/gitweb";
+      type = types.str;
+      description = ''
+        Location to serve gitweb on.
+      '';
+    };
+
+    user = mkOption {
+      default = "nginx";
+      type = types.str;
+      description = ''
+        Existing user that the CGI process will belong to. (Default almost surely will do.)
+      '';
+    };
+
+    group = mkOption {
+      default = "nginx";
+      type = types.str;
+      description = ''
+        Group that the CGI process will belong to. (Set to <literal>config.services.gitolite.group</literal> if you are using gitolite.)
+      '';
+    };
+
+    virtualHost = mkOption {
+      default = "_";
+      type = types.str;
+      description = ''
+        VirtualHost to serve gitweb on. Default is catch-all.
       '';
     };
 
   };
 
-  config = mkIf config.services.nginx.gitweb.enable {
+  config = mkIf cfg.enable {
 
     systemd.services.gitweb = {
       description = "GitWeb service";
@@ -32,22 +65,22 @@ in
         FCGI_SOCKET_PATH = "/run/gitweb/gitweb.sock";
       };
       serviceConfig = {
-        User = "nginx";
-        Group = "nginx";
+        User = cfg.user;
+        Group = cfg.group;
         RuntimeDirectory = [ "gitweb" ];
       };
       wantedBy = [ "multi-user.target" ];
     };
 
     services.nginx = {
-      virtualHosts.default = {
-        locations."/gitweb/static/" = {
+      virtualHosts.${cfg.virtualHost} = {
+        locations."${cfg.location}/static/" = {
           alias = "${package}/static/";
         };
-        locations."/gitweb/" = {
+        locations."${cfg.location}/" = {
           extraConfig = ''
             include ${pkgs.nginx}/conf/fastcgi_params;
-            fastcgi_param GITWEB_CONFIG ${cfg.gitwebConfigFile};
+            fastcgi_param GITWEB_CONFIG ${gitwebConfig.gitwebConfigFile};
             fastcgi_pass unix:/run/gitweb/gitweb.sock;
           '';
         };
diff --git a/nixos/modules/services/web-servers/unit/default.nix b/nixos/modules/services/web-servers/unit/default.nix
index 2303dfa9540..f8a18954fc9 100644
--- a/nixos/modules/services/web-servers/unit/default.nix
+++ b/nixos/modules/services/web-servers/unit/default.nix
@@ -111,7 +111,7 @@ in {
         AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" "CAP_SETGID" "CAP_SETUID" ];
         # Security
         NoNewPrivileges = true;
-        # Sanboxing
+        # Sandboxing
         ProtectSystem = "full";
         ProtectHome = true;
         RuntimeDirectory = "unit";
@@ -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 970fa620c6b..ea6aac9f6c9 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 6d9bd284bc7..5756cf14ed9 100644
--- a/nixos/modules/services/x11/desktop-managers/gnome3.nix
+++ b/nixos/modules/services/x11/desktop-managers/gnome3.nix
@@ -144,7 +144,7 @@ in
       services.gnome3.core-shell.enable = true;
       services.gnome3.core-utilities.enable = mkDefault true;
 
-      services.xserver.displayManager.sessionPackages = [ pkgs.gnome3.gnome-session ];
+      services.xserver.displayManager.sessionPackages = [ pkgs.gnome3.gnome-session.sessions ];
 
       environment.extraInit = ''
         ${concatMapStrings (p: ''
@@ -249,11 +249,17 @@ in
       services.system-config-printer.enable = (mkIf config.services.printing.enable (mkDefault true));
       services.telepathy.enable = mkDefault true;
 
-      systemd.packages = with pkgs.gnome3; [ vino gnome-session ];
+      systemd.packages = with pkgs.gnome3; [
+        gnome-session
+        gnome-shell
+        vino
+      ];
 
       services.avahi.enable = mkDefault true;
 
-      xdg.portal.extraPortals = [ pkgs.gnome3.gnome-shell ];
+      xdg.portal.extraPortals = [
+        pkgs.gnome3.gnome-shell
+      ];
 
       services.geoclue2.enable = mkDefault true;
       services.geoclue2.enableDemoAgent = false; # GNOME has its own geoclue agent
@@ -328,7 +334,6 @@ in
         cheese
         eog
         epiphany
-        geary
         gedit
         gnome-calculator
         gnome-calendar
@@ -355,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/plasma5.nix b/nixos/modules/services/x11/desktop-managers/plasma5.nix
index 2538858ac0f..bd0a2f3481f 100644
--- a/nixos/modules/services/x11/desktop-managers/plasma5.nix
+++ b/nixos/modules/services/x11/desktop-managers/plasma5.nix
@@ -60,7 +60,7 @@ in
                   -e '/^toolBarFont=/ s/,Regular$//'
           fi
 
-          exec "${getBin plasma5.plasma-workspace}/bin/startkde"
+          exec "${getBin plasma5.plasma-workspace}/bin/startplasma-x11"
         '';
       };
 
@@ -137,6 +137,7 @@ in
           libkscreen
           libksysguard
           milou
+          plasma-browser-integration
           plasma-integration
           polkit-kde-agent
           systemsettings
diff --git a/nixos/modules/services/x11/display-managers/default.nix b/nixos/modules/services/x11/display-managers/default.nix
index 1efd0739376..821886e5fda 100644
--- a/nixos/modules/services/x11/display-managers/default.nix
+++ b/nixos/modules/services/x11/display-managers/default.nix
@@ -141,9 +141,11 @@ let
     '';
 
   dmDefault = cfg.desktopManager.default;
+  # fallback default for cases when only default wm is set
+  dmFallbackDefault = if dmDefault != null then dmDefault else "none";
   wmDefault = cfg.windowManager.default;
 
-  defaultSessionFromLegacyOptions = concatStringsSep "+" (filter (s: s != null) ([ dmDefault ] ++ optional (wmDefault != "none") wmDefault));
+  defaultSessionFromLegacyOptions = dmFallbackDefault + optionalString (wmDefault != null && wmDefault != "none") "+${wmDefault}";
 
 in
 
@@ -358,7 +360,7 @@ in
             { c = wmDefault; t = "- services.xserver.windowManager.default"; }
             ]))}
           Please use
-            services.xserver.displayManager.defaultSession = "${concatStringsSep "+" (filter (s: s != null) [ dmDefault wmDefault ])}";
+            services.xserver.displayManager.defaultSession = "${defaultSessionFromLegacyOptions}";
           instead.
         ''
       ];
diff --git a/nixos/modules/services/x11/display-managers/gdm.nix b/nixos/modules/services/x11/display-managers/gdm.nix
index 2f8c8cc9013..325023f4121 100644
--- a/nixos/modules/services/x11/display-managers/gdm.nix
+++ b/nixos/modules/services/x11/display-managers/gdm.nix
@@ -174,6 +174,10 @@ in
       "f /run/gdm/.config/gnome-initial-setup-done 0711 gdm gdm - yes"
     ];
 
+    # Otherwise GDM will not be able to start correctly and display Wayland sessions
+    systemd.packages = with pkgs.gnome3; [ gnome-session gnome-shell ];
+    environment.systemPackages = [ pkgs.gnome3.adwaita-icon-theme ];
+
     systemd.services.display-manager.wants = [
       # Because sd_login_monitor_new requires /run/systemd/machines
       "systemd-machined.service"
diff --git a/nixos/modules/services/x11/hardware/multitouch.nix b/nixos/modules/services/x11/hardware/multitouch.nix
deleted file mode 100644
index c03bb3b494f..00000000000
--- a/nixos/modules/services/x11/hardware/multitouch.nix
+++ /dev/null
@@ -1,94 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let cfg = config.services.xserver.multitouch;
-    disabledTapConfig = ''
-      Option "MaxTapTime" "0"
-      Option "MaxTapMove" "0"
-      Option "TapButton1" "0"
-      Option "TapButton2" "0"
-      Option "TapButton3" "0"
-    '';
-in {
-
-  options = {
-
-    services.xserver.multitouch = {
-
-      enable = mkOption {
-        default = false;
-        description = "Whether to enable multitouch touchpad support.";
-      };
-
-      invertScroll = mkOption {
-        default = false;
-        type = types.bool;
-        description = "Whether to invert scrolling direction à la OSX Lion";
-      };
-
-      ignorePalm = mkOption {
-        default = false;
-        type = types.bool;
-        description = "Whether to ignore touches detected as being the palm (i.e when typing)";
-      };
-
-      tapButtons = mkOption {
-        type = types.bool;
-        default = true;
-        description = "Whether to enable tap buttons.";
-      };
-
-      buttonsMap = mkOption {
-        type = types.listOf types.int;
-        default = [3 2 0];
-        example = [1 3 2];
-        description = "Remap touchpad buttons.";
-        apply = map toString;
-      };
-
-      additionalOptions = mkOption {
-        type = types.str;
-        default = "";
-        example = ''
-          Option "ScaleDistance" "50"
-          Option "RotateDistance" "60"
-        '';
-        description = ''
-          Additional options for mtrack touchpad driver.
-        '';
-      };
-
-    };
-
-  };
-
-  config = mkIf cfg.enable {
-
-    services.xserver.modules = [ pkgs.xf86_input_mtrack ];
-
-    services.xserver.config =
-      ''
-        # Automatically enable the multitouch driver
-        Section "InputClass"
-          MatchIsTouchpad "on"
-          Identifier "Touchpads"
-          Driver "mtrack"
-          Option "IgnorePalm" "${boolToString cfg.ignorePalm}"
-          Option "ClickFinger1" "${builtins.elemAt cfg.buttonsMap 0}"
-          Option "ClickFinger2" "${builtins.elemAt cfg.buttonsMap 1}"
-          Option "ClickFinger3" "${builtins.elemAt cfg.buttonsMap 2}"
-          ${optionalString (!cfg.tapButtons) disabledTapConfig}
-          ${optionalString cfg.invertScroll ''
-            Option "ScrollUpButton" "5"
-            Option "ScrollDownButton" "4"
-            Option "ScrollLeftButton" "7"
-            Option "ScrollRightButton" "6"
-          ''}
-          ${cfg.additionalOptions}
-        EndSection
-      '';
-
-  };
-
-}
diff --git a/nixos/modules/services/x11/unclutter.nix b/nixos/modules/services/x11/unclutter.nix
index 2478aaabb79..c0868604a68 100644
--- a/nixos/modules/services/x11/unclutter.nix
+++ b/nixos/modules/services/x11/unclutter.nix
@@ -32,7 +32,7 @@ in {
       default = 1;
     };
 
-    threeshold = mkOption {
+    threshold = mkOption {
       description = "Minimum number of pixels considered cursor movement";
       type = types.int;
       default = 1;
@@ -72,6 +72,11 @@ in {
     };
   };
 
+  imports = [
+    (mkRenamedOptionModule [ "services" "unclutter" "threeshold" ]
+                           [ "services"  "unclutter" "threshold" ])
+  ];
+
   meta.maintainers = with lib.maintainers; [ rnhmjoj ];
 
 }
diff --git a/nixos/modules/services/x11/xserver.nix b/nixos/modules/services/x11/xserver.nix
index 7029919170a..7f0de96d208 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 9a4db84f7b7..26c1197bf97 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 a09c5dc4761..ca0fb0248e0 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 0bb8396a44f..31f1e22cda3 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 3e289a63139..a77dbc609f4 100644
--- a/nixos/modules/system/boot/networkd.nix
+++ b/nixos/modules/system/boot/networkd.nix
@@ -49,12 +49,17 @@ let
     (assertValueOneOf "Kind" [
       "bond" "bridge" "dummy" "gre" "gretap" "ip6gre" "ip6tnl" "ip6gretap" "ipip"
       "ipvlan" "macvlan" "macvtap" "sit" "tap" "tun" "veth" "vlan" "vti" "vti6"
-      "vxlan" "geneve" "vrf" "vcan" "vxcan" "wireguard" "netdevsim"
+      "vxlan" "geneve" "vrf" "vcan" "vxcan" "wireguard" "netdevsim" "xfrm"
     ])
     (assertByteFormat "MTUBytes")
     (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.
@@ -172,6 +177,14 @@ let
     (assertValueOneOf "AllSlavesActive" boolValues)
   ];
 
+  checkXfrm = checkUnitConfig "Xfrm" [
+    (assertOnlyFields [
+      "InterfaceId" "Independent"
+    ])
+    (assertRange "InterfaceId" 1 4294967295)
+    (assertValueOneOf "Independent" boolValues)
+  ];
+
   checkNetwork = checkUnitConfig "Network" [
     (assertOnlyFields [
       "Description" "DHCP" "DHCPServer" "LinkLocalAddressing" "IPv4LLRoute"
@@ -182,7 +195,7 @@ let
       "IPv6HopLimit" "IPv4ProxyARP" "IPv6ProxyNDP" "IPv6ProxyNDPAddress"
       "IPv6PrefixDelegation" "IPv6MTUBytes" "Bridge" "Bond" "VRF" "VLAN"
       "IPVLAN" "MACVLAN" "VXLAN" "Tunnel" "ActiveSlave" "PrimarySlave"
-      "ConfigureWithoutCarrier"
+      "ConfigureWithoutCarrier" "Xfrm"
     ])
     # Note: For DHCP the values both, none, v4, v6 are deprecated
     (assertValueOneOf "DHCP" ["yes" "no" "ipv4" "ipv6" "both" "none" "v4" "v6"])
@@ -341,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 = {
@@ -477,6 +505,18 @@ let
       '';
     };
 
+    xfrmConfig = mkOption {
+      default = {};
+      example = { InterfaceId = 1; };
+      type = types.addCheck (types.attrsOf unitOption) checkXfrm;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[Xfrm]</literal> section of the unit.  See
+        <citerefentry><refentrytitle>systemd.netdev</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
   };
 
   addressOptions = {
@@ -712,6 +752,16 @@ let
       '';
     };
 
+    xfrm = mkOption {
+      default = [ ];
+      type = types.listOf types.str;
+      description = ''
+        A list of xfrm interfaces to be added to the network section of the
+        unit.  See <citerefentry><refentrytitle>systemd.network</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
     addresses = mkOption {
       default = [ ];
       type = with types; listOf (submodule addressOptions);
@@ -810,6 +860,16 @@ let
             ${attrsToSection def.bondConfig}
 
           ''}
+          ${optionalString (def.xfrmConfig != { }) ''
+            [Xfrm]
+            ${attrsToSection def.xfrmConfig}
+
+          ''}
+          ${optionalString (def.vrfConfig != { }) ''
+            [VRF]
+            ${attrsToSection def.vrfConfig}
+
+          ''}
           ${optionalString (def.wireguardConfig != { }) ''
             [WireGuard]
             ${attrsToSection def.wireguardConfig}
@@ -847,6 +907,7 @@ let
           ${concatStringsSep "\n" (map (s: "MACVLAN=${s}") def.macvlan)}
           ${concatStringsSep "\n" (map (s: "VXLAN=${s}") def.vxlan)}
           ${concatStringsSep "\n" (map (s: "Tunnel=${s}") def.tunnel)}
+          ${concatStringsSep "\n" (map (s: "Xfrm=${s}") def.xfrm)}
 
           ${optionalString (def.dhcpConfig != { }) ''
             [DHCP]
@@ -911,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/stage-1-init.sh b/nixos/modules/system/boot/stage-1-init.sh
index f520bf54ad1..8736613c3d2 100644
--- a/nixos/modules/system/boot/stage-1-init.sh
+++ b/nixos/modules/system/boot/stage-1-init.sh
@@ -334,8 +334,10 @@ mountFS() {
 
     # Filter out x- options, which busybox doesn't do yet.
     local optionsFiltered="$(IFS=,; for i in $options; do if [ "${i:0:2}" != "x-" ]; then echo -n $i,; fi; done)"
+    # Prefix (lower|upper|work)dir with /mnt-root (overlayfs)
+    local optionsPrefixed="$( echo "$optionsFiltered" | sed -E 's#\<(lowerdir|upperdir|workdir)=#\1=/mnt-root#g' )"
 
-    echo "$device /mnt-root$mountPoint $fsType $optionsFiltered" >> /etc/fstab
+    echo "$device /mnt-root$mountPoint $fsType $optionsPrefixed" >> /etc/fstab
 
     checkFS "$device" "$fsType"
 
@@ -354,10 +356,11 @@ mountFS() {
             ;;
     esac
 
-    # Create backing directories for unionfs-fuse.
-    if [ "$fsType" = unionfs-fuse ]; then
-        for i in $(IFS=:; echo ${options##*,dirs=}); do
-            mkdir -m 0700 -p /mnt-root"${i%=*}"
+    # Create backing directories for overlayfs
+    if [ "$fsType" = overlay ]; then
+        for i in upper work; do
+             dir="$( echo "$optionsPrefixed" | grep -o "${i}dir=[^,]*" )"
+             mkdir -m 0700 -p "${dir##*=}"
         done
     fi
 
diff --git a/nixos/modules/system/boot/systemd-lib.nix b/nixos/modules/system/boot/systemd-lib.nix
index 28ad4f121bb..fd1a5b9f62c 100644
--- a/nixos/modules/system/boot/systemd-lib.nix
+++ b/nixos/modules/system/boot/systemd-lib.nix
@@ -147,7 +147,13 @@ in rec {
       done
 
       # Symlink all units provided listed in systemd.packages.
-      for i in ${toString cfg.packages}; do
+      packages="${toString cfg.packages}"
+
+      # Filter duplicate directories
+      declare -A unique_packages
+      for k in $packages ; do unique_packages[$k]=1 ; done
+
+      for i in ''${!unique_packages[@]}; do
         for fn in $i/etc/systemd/${type}/* $i/lib/systemd/${type}/*; do
           if ! [[ "$fn" =~ .wants$ ]]; then
             if [[ -d "$fn" ]]; then
diff --git a/nixos/modules/system/boot/systemd.nix b/nixos/modules/system/boot/systemd.nix
index c438bb216e7..cdc9d237939 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;
@@ -776,6 +786,18 @@ in
       '';
     };
 
+    systemd.suppressedSystemUnits = mkOption {
+      default = [ ];
+      type = types.listOf types.str;
+      example = [ "systemd-backlight@.service" ];
+      description = ''
+        A list of units to suppress when generating system systemd configuration directory. This has
+        priority over upstream units, <option>systemd.units</option>, and
+        <option>systemd.additionalUpstreamSystemUnits</option>. The main purpose of this is to
+        suppress a upstream systemd unit with any modifications made to it by other NixOS modules.
+      '';
+    };
+
   };
 
 
@@ -808,8 +830,11 @@ in
         done
         ${concatStrings (mapAttrsToList (exec: target: "ln -s ${target} $out/${exec};\n") links)}
       '';
+
+      enabledUpstreamSystemUnits = filter (n: ! elem n cfg.suppressedSystemUnits) upstreamSystemUnits;
+      enabledUnits = filterAttrs (n: v: ! elem n cfg.suppressedSystemUnits) cfg.units;
     in ({
-      "systemd/system".source = generateUnits "system" cfg.units upstreamSystemUnits upstreamSystemWants;
+      "systemd/system".source = generateUnits "system" enabledUnits enabledUpstreamSystemUnits upstreamSystemWants;
 
       "systemd/user".source = generateUnits "user" cfg.user.units upstreamUserUnits [];
 
@@ -863,17 +888,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/filesystems.nix b/nixos/modules/tasks/filesystems.nix
index 688c77cb22d..965a1c9eb1a 100644
--- a/nixos/modules/tasks/filesystems.nix
+++ b/nixos/modules/tasks/filesystems.nix
@@ -304,6 +304,10 @@ in
 
       in listToAttrs (map formatDevice (filter (fs: fs.autoFormat) fileSystems));
 
+    systemd.tmpfiles.rules = [
+      "Z /run/keys 0750 root ${toString config.ids.gids.keys}"
+    ];
+
     # Sync mount options with systemd's src/core/mount-setup.c: mount_table.
     boot.specialFileSystems = {
       "/proc" = { fsType = "proc"; options = [ "nosuid" "noexec" "nodev" ]; };
@@ -312,8 +316,8 @@ in
       "/dev/shm" = { fsType = "tmpfs"; options = [ "nosuid" "nodev" "strictatime" "mode=1777" "size=${config.boot.devShmSize}" ]; };
       "/dev/pts" = { fsType = "devpts"; options = [ "nosuid" "noexec" "mode=620" "ptmxmode=0666" "gid=${toString config.ids.gids.tty}" ]; };
 
-      # To hold secrets that shouldn't be written to disk (generally used for NixOps, harmless elsewhere)
-      "/run/keys" = { fsType = "ramfs"; options = [ "nosuid" "nodev" "mode=750" "gid=${toString config.ids.gids.keys}" ]; };
+      # To hold secrets that shouldn't be written to disk
+      "/run/keys" = { fsType = "ramfs"; options = [ "nosuid" "nodev" "mode=750" ]; };
     } // optionalAttrs (!config.boot.isContainer) {
       # systemd-nspawn populates /sys by itself, and remounting it causes all
       # kinds of weird issues (most noticeably, waiting for host disk device
diff --git a/nixos/modules/tasks/network-interfaces.nix b/nixos/modules/tasks/network-interfaces.nix
index 31e2ed1cd1e..cef9c38c2e3 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 8032b2c6d7c..8c12e0e49bf 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 760cb9122a2..216ba2c733f 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 = [];
@@ -26,7 +39,7 @@ let
 
         entrypoint = mkOption {
           type = with types; nullOr str;
-          description = "Overwrite the default entrypoint of the image.";
+          description = "Override the default entrypoint of the image.";
           default = null;
           example = "/bin/my-app";
         };
@@ -132,7 +145,7 @@ let
 
             Note that this is a list of <literal>"src:dst"</literal> strings to
             allow for <literal>src</literal> to refer to
-            <literal>/nix/store</literal> paths, which would difficult with an
+            <literal>/nix/store</literal> paths, which would be difficult with an
             attribute set.  There are also a variety of mount options available
             as a third field; please refer to the
             <link xlink:href="https://docs.docker.com/engine/reference/run/#volume-shared-filesystems">
@@ -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,13 @@ 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}"] ++
+        (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 b4934a86cf5..de48d3a780e 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 ca9c6f9a7f9..b46731863ca 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 f40b5fa9bd7..512ba714397 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/acme.nix b/nixos/tests/acme.nix
index 617dafee0dc..732f41a4905 100644
--- a/nixos/tests/acme.nix
+++ b/nixos/tests/acme.nix
@@ -29,7 +29,6 @@ in import ./make-test-python.nix {
       systemd.services.pebble-challtestsrv = {
         enable = true;
         description = "Pebble ACME challenge test server";
-        requires = [ ];
         wantedBy = [ "network.target" ];
         serviceConfig = {
           ExecStart = "${pkgs.pebble}/bin/pebble-challtestsrv -dns01 ':53' -defaultIPv6 '' -defaultIPv4 '${nodes.webserver.config.networking.primaryIPAddress}'";
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index fe9c4df1416..bdac56169fd 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -32,7 +32,7 @@ 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 {};
   borgbackup = handleTest ./borgbackup.nix {};
@@ -61,10 +61,11 @@ in
   containers-portforward = handleTest ./containers-portforward.nix {};
   containers-restart_networking = handleTest ./containers-restart_networking.nix {};
   containers-tmpfs = handleTest ./containers-tmpfs.nix {};
+  corerad = handleTest ./corerad.nix {};
   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 {};
@@ -73,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 {};
@@ -91,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 {};
@@ -210,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 {};
@@ -246,6 +248,7 @@ in
   radicale = handleTest ./radicale.nix {};
   redis = handleTest ./redis.nix {};
   redmine = handleTest ./redmine.nix {};
+  restic = handleTest ./restic.nix {};
   roundcube = handleTest ./roundcube.nix {};
   rspamd = handleTest ./rspamd.nix {};
   rss2email = handleTest ./rss2email.nix {};
@@ -272,6 +275,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 {};
@@ -290,11 +294,13 @@ 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 {};
   wireguard-namespaces = handleTest ./wireguard/namespaces.nix {};
   wordpress = handleTest ./wordpress.nix {};
+  xandikos = handleTest ./xandikos.nix {};
   xautolock = handleTest ./xautolock.nix {};
   xfce = handleTest ./xfce.nix {};
   xmonad = handleTest ./xmonad.nix {};
@@ -302,6 +308,7 @@ in
   xss-lock = handleTest ./xss-lock.nix {};
   yabar = handleTest ./yabar.nix {};
   yggdrasil = handleTest ./yggdrasil.nix {};
+  zfs = handleTest ./zfs.nix {};
   zsh-history = handleTest ./zsh-history.nix {};
   zookeeper = handleTest ./zookeeper.nix {};
 }
diff --git a/nixos/tests/bittorrent.nix b/nixos/tests/bittorrent.nix
index e5be652c711..0a97d5556a2 100644
--- a/nixos/tests/bittorrent.nix
+++ b/nixos/tests/bittorrent.nix
@@ -18,6 +18,17 @@ let
   externalRouterAddress = "80.100.100.1";
   externalClient2Address = "80.100.100.2";
   externalTrackerAddress = "80.100.100.3";
+
+  transmissionConfig = { ... }: {
+    environment.systemPackages = [ pkgs.transmission ];
+    services.transmission = {
+      enable = true;
+      settings = {
+        dht-enabled = false;
+        message-level = 3;
+      };
+    };
+  };
 in
 
 {
@@ -26,88 +37,79 @@ in
     maintainers = [ domenkozar eelco rob bobvanderlinden ];
   };
 
-  nodes =
-    { tracker =
-        { pkgs, ... }:
-        { environment.systemPackages = [ pkgs.transmission ];
-
-          virtualisation.vlans = [ 1 ];
-          networking.interfaces.eth1.ipv4.addresses = [
-            { address = externalTrackerAddress; prefixLength = 24; }
-          ];
-
-          # We need Apache on the tracker to serve the torrents.
-          services.httpd.enable = true;
-          services.httpd.adminAddr = "foo@example.org";
-          services.httpd.documentRoot = "/tmp";
-
-          networking.firewall.enable = false;
-
-          services.opentracker.enable = true;
-
-          services.transmission.enable = true;
-          services.transmission.settings.dht-enabled = false;
-          services.transmission.settings.port-forwaring-enabled = false;
-        };
-
-      router =
-        { pkgs, nodes, ... }:
-        { virtualisation.vlans = [ 1 2 ];
-          networking.nat.enable = true;
-          networking.nat.internalInterfaces = [ "eth2" ];
-          networking.nat.externalInterface = "eth1";
-          networking.firewall.enable = true;
-          networking.firewall.trustedInterfaces = [ "eth2" ];
-          networking.interfaces.eth0.ipv4.addresses = [];
-          networking.interfaces.eth1.ipv4.addresses = [
-            { address = externalRouterAddress; prefixLength = 24; }
-          ];
-          networking.interfaces.eth2.ipv4.addresses = [
-            { address = internalRouterAddress; prefixLength = 24; }
-          ];
-          services.miniupnpd = {
-            enable = true;
-            externalInterface = "eth1";
-            internalIPs = [ "eth2" ];
-            appendConfig = ''
-              ext_ip=${externalRouterAddress}
-            '';
+  nodes = {
+    tracker = { pkgs, ... }: {
+      imports = [ transmissionConfig ];
+
+      virtualisation.vlans = [ 1 ];
+      networking.firewall.enable = false;
+      networking.interfaces.eth1.ipv4.addresses = [
+        { address = externalTrackerAddress; prefixLength = 24; }
+      ];
+
+      # We need Apache on the tracker to serve the torrents.
+      services.httpd = {
+        enable = true;
+        virtualHosts = {
+          "torrentserver.org" = {
+            adminAddr = "foo@example.org";
+            documentRoot = "/tmp";
           };
         };
+      };
+      services.opentracker.enable = true;
+    };
 
-      client1 =
-        { pkgs, nodes, ... }:
-        { environment.systemPackages = [ pkgs.transmission pkgs.miniupnpc ];
-          virtualisation.vlans = [ 2 ];
-          networking.interfaces.eth0.ipv4.addresses = [];
-          networking.interfaces.eth1.ipv4.addresses = [
-            { address = internalClient1Address; prefixLength = 24; }
-          ];
-          networking.defaultGateway = internalRouterAddress;
-          networking.firewall.enable = false;
-          services.transmission.enable = true;
-          services.transmission.settings.dht-enabled = false;
-          services.transmission.settings.message-level = 3;
-        };
+    router = { pkgs, nodes, ... }: {
+      virtualisation.vlans = [ 1 2 ];
+      networking.nat.enable = true;
+      networking.nat.internalInterfaces = [ "eth2" ];
+      networking.nat.externalInterface = "eth1";
+      networking.firewall.enable = true;
+      networking.firewall.trustedInterfaces = [ "eth2" ];
+      networking.interfaces.eth0.ipv4.addresses = [];
+      networking.interfaces.eth1.ipv4.addresses = [
+        { address = externalRouterAddress; prefixLength = 24; }
+      ];
+      networking.interfaces.eth2.ipv4.addresses = [
+        { address = internalRouterAddress; prefixLength = 24; }
+      ];
+      services.miniupnpd = {
+        enable = true;
+        externalInterface = "eth1";
+        internalIPs = [ "eth2" ];
+        appendConfig = ''
+          ext_ip=${externalRouterAddress}
+        '';
+      };
+    };
 
-      client2 =
-        { pkgs, ... }:
-        { environment.systemPackages = [ pkgs.transmission ];
-          virtualisation.vlans = [ 1 ];
-          networking.interfaces.eth0.ipv4.addresses = [];
-          networking.interfaces.eth1.ipv4.addresses = [
-            { address = externalClient2Address; prefixLength = 24; }
-          ];
-          networking.firewall.enable = false;
-          services.transmission.enable = true;
-          services.transmission.settings.dht-enabled = false;
-          services.transmission.settings.port-forwaring-enabled = false;
-        };
+    client1 = { pkgs, nodes, ... }: {
+      imports = [ transmissionConfig ];
+      environment.systemPackages = [ pkgs.miniupnpc ];
+
+      virtualisation.vlans = [ 2 ];
+      networking.interfaces.eth0.ipv4.addresses = [];
+      networking.interfaces.eth1.ipv4.addresses = [
+        { address = internalClient1Address; prefixLength = 24; }
+      ];
+      networking.defaultGateway = internalRouterAddress;
+      networking.firewall.enable = false;
     };
 
-  testScript =
-    { nodes, ... }:
-    ''
+    client2 = { pkgs, ... }: {
+      imports = [ transmissionConfig ];
+
+      virtualisation.vlans = [ 1 ];
+      networking.interfaces.eth0.ipv4.addresses = [];
+      networking.interfaces.eth1.ipv4.addresses = [
+        { address = externalClient2Address; prefixLength = 24; }
+      ];
+      networking.firewall.enable = false;
+    };
+  };
+
+  testScript = { nodes, ... }: ''
       start_all()
 
       # Wait for network and miniupnpd.
@@ -159,5 +161,4 @@ in
           "cmp /tmp/test.tar.bz2 ${file}"
       )
     '';
-
 })
diff --git a/nixos/tests/blivet.nix b/nixos/tests/blivet.nix
deleted file mode 100644
index 2adc2ee1eee..00000000000
--- 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 f5c8c4863b6..5655a34a8b5 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/buildkite-agent.nix b/nixos/tests/buildkite-agent.nix
new file mode 100644
index 00000000000..3c824c9aedf
--- /dev/null
+++ b/nixos/tests/buildkite-agent.nix
@@ -0,0 +1,36 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+{
+  name = "buildkite-agent";
+  meta = with pkgs.stdenv.lib.maintainers; {
+    maintainers = [ flokli ];
+  };
+
+  nodes = {
+    node1 = { pkgs, ... }: {
+      services.buildkite-agent = {
+        enable = true;
+        privateSshKeyPath = (import ./ssh-keys.nix pkgs).snakeOilPrivateKey;
+        tokenPath = (pkgs.writeText "my-token" "5678");
+      };
+    };
+    # don't configure ssh key, run as a separate user
+    node2 = { pkgs, ...}: {
+      services.buildkite-agent = {
+        enable = true;
+        tokenPath = (pkgs.writeText "my-token" "1234");
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+    # we can't wait on the unit to start up, as we obviously can't connect to buildkite,
+    # but we can look whether files are set up correctly
+
+    node1.wait_for_file("/var/lib/buildkite-agent/buildkite-agent.cfg")
+    node1.wait_for_file("/var/lib/buildkite-agent/.ssh/id_rsa")
+
+    node2.wait_for_file("/var/lib/buildkite-agent/buildkite-agent.cfg")
+  '';
+})
diff --git a/nixos/tests/certmgr.nix b/nixos/tests/certmgr.nix
index cb69f35e862..ef32f54400e 100644
--- a/nixos/tests/certmgr.nix
+++ b/nixos/tests/certmgr.nix
@@ -9,8 +9,8 @@ let
     inherit action;
     authority = {
       file = {
-        group = "nobody";
-        owner = "nobody";
+        group = "nginx";
+        owner = "nginx";
         path = "/tmp/${host}-ca.pem";
       };
       label = "www_ca";
@@ -18,14 +18,14 @@ let
       remote = "localhost:8888";
     };
     certificate = {
-      group = "nobody";
-      owner = "nobody";
+      group = "nginx";
+      owner = "nginx";
       path = "/tmp/${host}-cert.pem";
     };
     private_key = {
-      group = "nobody";
+      group = "nginx";
       mode = "0600";
-      owner = "nobody";
+      owner = "nginx";
       path = "/tmp/${host}-key.pem";
     };
     request = {
diff --git a/nixos/tests/chromium.nix b/nixos/tests/chromium.nix
index a5531d112e3..fc5d3a5c52f 100644
--- a/nixos/tests/chromium.nix
+++ b/nixos/tests/chromium.nix
@@ -8,7 +8,7 @@
   }
 }:
 
-with import ../lib/testing.nix { inherit system pkgs; };
+with import ../lib/testing-python.nix { inherit system pkgs; };
 with pkgs.lib;
 
 mapAttrs (channel: chromiumPkg: makeTest rec {
@@ -21,9 +21,11 @@ mapAttrs (channel: chromiumPkg: makeTest rec {
 
   enableOCR = true;
 
+  user = "alice";
+
   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 = user;
   machine.environment.systemPackages = [ chromiumPkg ];
 
   startupHTML = pkgs.writeText "chromium-startup.html" ''
@@ -47,155 +49,218 @@ mapAttrs (channel: chromiumPkg: makeTest rec {
       xdoScript = pkgs.writeText "${name}.xdo" text;
     in "${pkgs.xdotool}/bin/xdotool '${xdoScript}'";
   in ''
+    import shlex
+    from contextlib import contextmanager, _GeneratorContextManager
+
+
     # Run as user alice
-    sub ru ($) {
-      my $esc = $_[0] =~ s/'/'\\${"'"}'/gr;
-      return "su - alice -c '$esc'";
-    }
-
-    sub createNewWin {
-      $machine->nest("creating a new Chromium window", sub {
-        $machine->execute(ru "${xdo "new-window" ''
-          search --onlyvisible --name "startup done"
-          windowfocus --sync
-          windowactivate --sync
-        ''}");
-        $machine->execute(ru "${xdo "new-window" ''
-          key Ctrl+n
-        ''}");
-      });
-    }
-
-    sub closeWin {
-      Machine::retry sub {
-        $machine->execute(ru "${xdo "close-window" ''
-          search --onlyvisible --name "new tab"
-          windowfocus --sync
-          windowactivate --sync
-        ''}");
-        $machine->execute(ru "${xdo "close-window" ''
-          key Ctrl+w
-        ''}");
-        for (1..20) {
-          my ($status, $out) = $machine->execute(ru "${xdo "wait-for-close" ''
-            search --onlyvisible --name "new tab"
-          ''}");
-          return 1 if $status != 0;
-          $machine->sleep(1);
-        }
-      }
-    }
-
-    sub waitForNewWin {
-      my $ret = 0;
-      $machine->nest("waiting for new Chromium window to appear", sub {
-        for (1..20) {
-          my ($status, $out) = $machine->execute(ru "${xdo "wait-for-window" ''
-            search --onlyvisible --name "new tab"
-            windowfocus --sync
-            windowactivate --sync
-          ''}");
-          if ($status == 0) {
-            $ret = 1;
-
-            # XXX: Somehow Chromium is not accepting keystrokes for a few
-            # seconds after a new window has appeared, so let's wait a while.
-            $machine->sleep(10);
-
-            last;
-          }
-          $machine->sleep(1);
-        }
-      });
-      return $ret;
-    }
-
-    sub createAndWaitForNewWin {
-      for (1..3) {
-        createNewWin;
-        return 1 if waitForNewWin;
-      }
-      die "new window didn't appear within 60 seconds";
-    }
-
-    sub testNewWin {
-      my ($desc, $code) = @_;
-      createAndWaitForNewWin;
-      subtest($desc, $code);
-      closeWin;
-    }
-
-    $machine->waitForX;
-
-    my $url = "file://${startupHTML}";
-    $machine->execute(ru "ulimit -c unlimited; chromium \"$url\" & disown");
-    $machine->waitForText(qr/startup done/);
-    $machine->waitUntilSucceeds(ru "${xdo "check-startup" ''
-      search --sync --onlyvisible --name "startup done"
-      # close first start help popup
-      key -delay 1000 Escape
-      windowfocus --sync
-      windowactivate --sync
-    ''}");
-
-    createAndWaitForNewWin;
-    $machine->screenshot("empty_windows");
-    closeWin;
-
-    $machine->screenshot("startup_done");
-
-    testNewWin "check sandbox", sub {
-      $machine->succeed(ru "${xdo "type-url" ''
-        search --sync --onlyvisible --name "new tab"
-        windowfocus --sync
-        type --delay 1000 "chrome://sandbox"
-      ''}");
-
-      $machine->succeed(ru "${xdo "submit-url" ''
-        search --sync --onlyvisible --name "new tab"
-        windowfocus --sync
-        key --delay 1000 Return
-      ''}");
-
-      $machine->screenshot("sandbox_info");
-
-      $machine->succeed(ru "${xdo "find-window" ''
-        search --sync --onlyvisible --name "sandbox status"
-        windowfocus --sync
-      ''}");
-      $machine->succeed(ru "${xdo "copy-sandbox-info" ''
-        key --delay 1000 Ctrl+a Ctrl+c
-      ''}");
-
-      my $clipboard = $machine->succeed(ru "${pkgs.xclip}/bin/xclip -o");
-      die "sandbox not working properly: $clipboard"
-      unless $clipboard =~ /layer 1 sandbox.*namespace/mi
-          && $clipboard =~ /pid namespaces.*yes/mi
-          && $clipboard =~ /network namespaces.*yes/mi
-          && $clipboard =~ /seccomp.*sandbox.*yes/mi
-          && $clipboard =~ /you are adequately sandboxed/mi;
-
-      $machine->sleep(1);
-      $machine->succeed(ru "${xdo "find-window-after-copy" ''
-        search --onlyvisible --name "sandbox status"
-      ''}");
-
-      my $clipboard = $machine->succeed(ru "echo void | ${pkgs.xclip}/bin/xclip -i");
-      $machine->succeed(ru "${xdo "copy-sandbox-info" ''
-        key --delay 1000 Ctrl+a Ctrl+c
-      ''}");
-
-      my $clipboard = $machine->succeed(ru "${pkgs.xclip}/bin/xclip -o");
-      die "copying twice in a row does not work properly: $clipboard"
-      unless $clipboard =~ /layer 1 sandbox.*namespace/mi
-          && $clipboard =~ /pid namespaces.*yes/mi
-          && $clipboard =~ /network namespaces.*yes/mi
-          && $clipboard =~ /seccomp.*sandbox.*yes/mi
-          && $clipboard =~ /you are adequately sandboxed/mi;
-
-      $machine->screenshot("afer_copy_from_chromium");
-    };
-
-    $machine->shutdown;
+    def ru(cmd):
+        return "su - ${user} -c " + shlex.quote(cmd)
+
+
+    def create_new_win():
+        with machine.nested("Creating a new Chromium window"):
+            machine.execute(
+                ru(
+                    "${xdo "new-window" ''
+                      search --onlyvisible --name "startup done"
+                      windowfocus --sync
+                      windowactivate --sync
+                    ''}"
+                )
+            )
+            machine.execute(
+                ru(
+                    "${xdo "new-window" ''
+                      key Ctrl+n
+                    ''}"
+                )
+            )
+
+
+    def close_win():
+        def try_close(_):
+            machine.execute(
+                ru(
+                    "${xdo "close-window" ''
+                      search --onlyvisible --name "new tab"
+                      windowfocus --sync
+                      windowactivate --sync
+                    ''}"
+                )
+            )
+            machine.execute(
+                ru(
+                    "${xdo "close-window" ''
+                      key Ctrl+w
+                    ''}"
+                )
+            )
+            for _ in range(1, 20):
+                status, out = machine.execute(
+                    ru(
+                        "${xdo "wait-for-close" ''
+                          search --onlyvisible --name "new tab"
+                        ''}"
+                    )
+                )
+                if status != 0:
+                    return True
+                machine.sleep(1)
+                return False
+
+        retry(try_close)
+
+
+    def wait_for_new_win():
+        ret = False
+        with machine.nested("Waiting for new Chromium window to appear"):
+            for _ in range(1, 20):
+                status, out = machine.execute(
+                    ru(
+                        "${xdo "wait-for-window" ''
+                          search --onlyvisible --name "new tab"
+                          windowfocus --sync
+                          windowactivate --sync
+                        ''}"
+                    )
+                )
+                if status == 0:
+                    ret = True
+                    machine.sleep(10)
+                    break
+                machine.sleep(1)
+        return ret
+
+
+    def create_and_wait_for_new_win():
+        for _ in range(1, 3):
+            create_new_win()
+            if wait_for_new_win():
+                return True
+        assert False, "new window did not appear within 60 seconds"
+
+
+    @contextmanager
+    def test_new_win(description):
+        create_and_wait_for_new_win()
+        with machine.nested(description):
+            yield
+        close_win()
+
+
+    machine.wait_for_x()
+
+    url = "file://${startupHTML}"
+    machine.succeed(ru(f'ulimit -c unlimited; chromium "{url}" & disown'))
+    machine.wait_for_text("startup done")
+    machine.wait_until_succeeds(
+        ru(
+            "${xdo "check-startup" ''
+              search --sync --onlyvisible --name "startup done"
+              # close first start help popup
+              key -delay 1000 Escape
+              windowfocus --sync
+              windowactivate --sync
+            ''}"
+        )
+    )
+
+    create_and_wait_for_new_win()
+    machine.screenshot("empty_windows")
+    close_win()
+
+    machine.screenshot("startup_done")
+
+    with test_new_win("check sandbox"):
+        machine.succeed(
+            ru(
+                "${xdo "type-url" ''
+                  search --sync --onlyvisible --name "new tab"
+                  windowfocus --sync
+                  type --delay 1000 "chrome://sandbox"
+                ''}"
+            )
+        )
+
+        machine.succeed(
+            ru(
+                "${xdo "submit-url" ''
+                  search --sync --onlyvisible --name "new tab"
+                  windowfocus --sync
+                  key --delay 1000 Return
+                ''}"
+            )
+        )
+
+        machine.screenshot("sandbox_info")
+
+        machine.succeed(
+            ru(
+                "${xdo "find-window" ''
+                  search --sync --onlyvisible --name "sandbox status"
+                  windowfocus --sync
+                ''}"
+            )
+        )
+        machine.succeed(
+            ru(
+                "${xdo "copy-sandbox-info" ''
+                  key --delay 1000 Ctrl+a Ctrl+c
+                ''}"
+            )
+        )
+
+        clipboard = machine.succeed(
+            ru("${pkgs.xclip}/bin/xclip -o")
+        )
+
+        filters = [
+            "layer 1 sandbox.*namespace",
+            "pid namespaces.*yes",
+            "network namespaces.*yes",
+            "seccomp.*sandbox.*yes",
+            "you are adequately sandboxed",
+        ]
+        if not all(
+            re.search(filter, clipboard, flags=re.DOTALL | re.IGNORECASE)
+            for filter in filters
+        ):
+            assert False, f"sandbox not working properly: {clipboard}"
+
+        machine.sleep(1)
+        machine.succeed(
+            ru(
+                "${xdo "find-window-after-copy" ''
+                  search --onlyvisible --name "sandbox status"
+                ''}"
+            )
+        )
+
+        clipboard = machine.succeed(
+            ru(
+                "echo void | ${pkgs.xclip}/bin/xclip -i"
+            )
+        )
+        machine.succeed(
+            ru(
+                "${xdo "copy-sandbox-info" ''
+                  key --delay 1000 Ctrl+a Ctrl+c
+                ''}"
+            )
+        )
+
+        clipboard = machine.succeed(
+            ru("${pkgs.xclip}/bin/xclip -o")
+        )
+        if not all(
+            re.search(filter, clipboard, flags=re.DOTALL | re.IGNORECASE)
+            for filter in filters
+        ):
+            assert False, f"copying twice in a row does not work properly: {clipboard}"
+
+        machine.screenshot("after_copy_from_chromium")
+
+    machine.shutdown()
   '';
 }) channelMap
diff --git a/nixos/modules/services/x11/display-managers/auto.nix b/nixos/tests/common/auto.nix
index 1068a344e0c..2c21a8d5167 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 1e69b63191a..ba087bb6009 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 5ad0ac20fac..0d76a0e972f 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/corerad.nix b/nixos/tests/corerad.nix
new file mode 100644
index 00000000000..950c9abc899
--- /dev/null
+++ b/nixos/tests/corerad.nix
@@ -0,0 +1,70 @@
+import ./make-test-python.nix (
+  {
+    nodes = {
+      router = {config, pkgs, ...}: { 
+        config = {
+          # This machines simulates a router with IPv6 forwarding and a static IPv6 address.
+          boot.kernel.sysctl = {
+            "net.ipv6.conf.all.forwarding" = true;
+          };
+          networking.interfaces.eth1 = {
+            ipv6.addresses = [ { address = "fd00:dead:beef:dead::1"; prefixLength = 64; } ];
+          };
+          services.corerad = {
+            enable = true;
+            # Serve router advertisements to the client machine with prefix information matching
+            # any IPv6 /64 prefixes configured on this interface.
+            configFile = pkgs.writeText "corerad.toml" ''
+              [[interfaces]]
+              name = "eth1"
+              send_advertisements = true
+                [[interfaces.prefix]]
+                prefix = "::/64"
+            '';
+          };
+        };
+      };
+      client = {config, pkgs, ...}: {
+        # Use IPv6 SLAAC from router advertisements, and install rdisc6 so we can
+        # trigger one immediately.
+        config = {
+          boot.kernel.sysctl = {
+            "net.ipv6.conf.all.autoconf" = true;
+          };
+          environment.systemPackages = with pkgs; [
+            ndisc6
+          ];
+        };
+      };
+    };
+
+    testScript = ''
+      start_all()
+
+      with subtest("Wait for CoreRAD and network ready"):
+          # Ensure networking is online and CoreRAD is ready.
+          router.wait_for_unit("network-online.target")
+          client.wait_for_unit("network-online.target")
+          router.wait_for_unit("corerad.service")
+
+          # Ensure the client can reach the router.
+          client.wait_until_succeeds("ping -c 1 fd00:dead:beef:dead::1")
+
+      with subtest("Verify SLAAC on client"):
+          # Trigger a router solicitation and verify a SLAAC address is assigned from
+          # the prefix configured on the router.
+          client.wait_until_succeeds("rdisc6 -1 -r 10 eth1")
+          client.wait_until_succeeds(
+              "ip -6 addr show dev eth1 | grep -q 'fd00:dead:beef:dead:'"
+          )
+
+          addrs = client.succeed("ip -6 addr show dev eth1")
+
+          assert (
+              "fd00:dead:beef:dead:" in addrs
+          ), "SLAAC prefix was not found in client addresses after router advertisement"
+          assert (
+              "/64 scope global temporary" in addrs
+          ), "SLAAC temporary address was not configured on client after router advertisement"
+    '';
+  })
diff --git a/nixos/tests/dnscrypt-proxy.nix b/nixos/tests/dnscrypt-proxy2.nix
index 98153d5c904..b614d912a9f 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 97255273520..9be9bfa80ce 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 9ab1a71f331..07fac533680 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 00000000000..38bde10f47e
--- /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 c649ce852da..6aeeb17ba31 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/elk.nix b/nixos/tests/elk.nix
index 80db0967d40..d3dc6dde135 100644
--- a/nixos/tests/elk.nix
+++ b/nixos/tests/elk.nix
@@ -10,8 +10,7 @@ let
   esUrl = "http://localhost:9200";
 
   mkElkTest = name : elk :
-   let elasticsearchGe7 = builtins.compareVersions elk.elasticsearch.version "7" >= 0;
-   in import ./make-test-python.nix ({
+    import ./make-test-python.nix ({
     inherit name;
     meta = with pkgs.stdenv.lib.maintainers; {
       maintainers = [ eelco offline basvandijk ];
@@ -91,8 +90,7 @@ let
               };
 
               elasticsearch-curator = {
-                # The current version of curator (5.6) doesn't support elasticsearch >= 7.0.0.
-                enable = !elasticsearchGe7;
+                enable = true;
                 actionYAML = ''
                 ---
                 actions:
@@ -173,7 +171,7 @@ let
           one.wait_until_succeeds(
               total_hits("Supercalifragilisticexpialidocious") + " | grep -v 0"
           )
-    '' + pkgs.lib.optionalString (!elasticsearchGe7) ''
+
       with subtest("Elasticsearch-curator works"):
           one.systemctl("stop logstash")
           one.systemctl("start elasticsearch-curator")
diff --git a/nixos/tests/freeswitch.nix b/nixos/tests/freeswitch.nix
new file mode 100644
index 00000000000..349d0e7bc6f
--- /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 ab363efb6a1..486c146d8dc 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 27a87bdbb9f..ba3c73bb878 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 126178d1187..b527aa706ad 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 14db17fe5e6..7df0ea0b691 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/installed-tests/fwupd.nix b/nixos/tests/installed-tests/fwupd.nix
index b9f761e9958..6a0ceb57dda 100644
--- a/nixos/tests/installed-tests/fwupd.nix
+++ b/nixos/tests/installed-tests/fwupd.nix
@@ -1,11 +1,11 @@
-{ pkgs, makeInstalledTest, ... }:
+{ pkgs, lib, makeInstalledTest, ... }:
 
 makeInstalledTest {
   tested = pkgs.fwupd;
 
   testConfig = {
     services.fwupd.enable = true;
-    services.fwupd.blacklistPlugins = []; # don't blacklist test plugin
+    services.fwupd.blacklistPlugins = lib.mkForce []; # don't blacklist test plugin
     services.fwupd.enableTestRemote = true;
     virtualisation.memorySize = 768;
   };
diff --git a/nixos/tests/installer.nix b/nixos/tests/installer.nix
index eb1f4f192dd..983861911e0 100644
--- a/nixos/tests/installer.nix
+++ b/nixos/tests/installer.nix
@@ -3,7 +3,7 @@
   pkgs ? import ../.. { inherit system config; }
 }:
 
-with import ../lib/testing.nix { inherit system pkgs; };
+with import ../lib/testing-python.nix { inherit system pkgs; };
 with pkgs.lib;
 
 let
@@ -67,161 +67,193 @@ let
                   , grubIdentifier, preBootCommands, extraConfig
                   , testCloneConfig
                   }:
-    let
-      iface = if grubVersion == 1 then "ide" else "virtio";
-      isEfi = bootLoader == "systemd-boot" || (bootLoader == "grub" && grubUseEfi);
-
-      # 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 == "aarch64-linux") "-enable-kvm -machine virt,gic-version=host -cpu host ");
-
-      hdFlags = ''hda => "vm-state-machine/machine.qcow2", hdaInterface => "${iface}", ''
-        + optionalString isEfi (if pkgs.stdenv.isAarch64
-            then ''bios => "${pkgs.OVMF.fd}/FV/QEMU_EFI.fd", ''
-            else ''bios => "${pkgs.OVMF.fd}/FV/OVMF.fd", '');
+    let iface = if grubVersion == 1 then "ide" else "virtio";
+        isEfi = bootLoader == "systemd-boot" || (bootLoader == "grub" && grubUseEfi);
+        bios  = if pkgs.stdenv.isAarch64 then "QEMU_EFI.fd" else "OVMF.fd";
     in if !isEfi && !(pkgs.stdenv.isi686 || pkgs.stdenv.isx86_64) then
       throw "Non-EFI boot methods are only supported on i686 / x86_64"
     else ''
+      def assemble_qemu_flags():
+          flags = "-cpu host"
+          ${if system == "x86_64-linux"
+            then ''flags += " -m 768"''
+            else ''flags += " -m 512 -enable-kvm -machine virt,gic-version=host"''
+          }
+          return flags
 
-      $machine->start;
 
-      # Make sure that we get a login prompt etc.
-      $machine->succeed("echo hello");
-      #$machine->waitForUnit('getty@tty2');
-      #$machine->waitForUnit("rogue");
-      $machine->waitForUnit("nixos-manual");
+      qemu_flags = {"qemuFlags": assemble_qemu_flags()}
 
-      # Wait for hard disks to appear in /dev
-      $machine->succeed("udevadm settle");
+      hd_flags = {
+          "hdaInterface": "${iface}",
+          "hda": "vm-state-machine/machine.qcow2",
+      }
+      ${optionalString isEfi ''
+        hd_flags.update(
+            bios="${pkgs.OVMF.fd}/FV/${bios}"
+        )''
+      }
+      default_flags = {**hd_flags, **qemu_flags}
 
-      # Partition the disk.
-      ${createPartitions}
 
-      # Create the NixOS configuration.
-      $machine->succeed("nixos-generate-config --root /mnt");
+      def create_machine_named(name):
+          return create_machine({**default_flags, "name": "boot-after-install"})
 
-      $machine->succeed("cat /mnt/etc/nixos/hardware-configuration.nix >&2");
 
-      $machine->copyFileFromHost(
-          "${ makeConfig { inherit bootLoader grubVersion grubDevice grubIdentifier grubUseEfi extraConfig; } }",
-          "/mnt/etc/nixos/configuration.nix");
+      machine.start()
 
-      # Perform the installation.
-      $machine->succeed("nixos-install < /dev/null >&2");
+      with subtest("Assert readiness of login prompt"):
+          machine.succeed("echo hello")
+          machine.wait_for_unit("nixos-manual")
 
-      # Do it again to make sure it's idempotent.
-      $machine->succeed("nixos-install < /dev/null >&2");
+      with subtest("Wait for hard disks to appear in /dev"):
+          machine.succeed("udevadm settle")
 
-      $machine->succeed("umount /mnt/boot || true");
-      $machine->succeed("umount /mnt");
-      $machine->succeed("sync");
+      ${createPartitions}
 
-      $machine->shutdown;
+      with subtest("Create the NixOS configuration"):
+          machine.succeed("nixos-generate-config --root /mnt")
+          machine.succeed("cat /mnt/etc/nixos/hardware-configuration.nix >&2")
+          machine.copy_from_host(
+              "${ makeConfig {
+                    inherit bootLoader grubVersion grubDevice grubIdentifier
+                            grubUseEfi extraConfig;
+                  }
+              }",
+              "/mnt/etc/nixos/configuration.nix",
+          )
+
+      with subtest("Perform the installation"):
+          machine.succeed("nixos-install < /dev/null >&2")
+
+      with subtest("Do it again to make sure it's idempotent"):
+          machine.succeed("nixos-install < /dev/null >&2")
+
+      with subtest("Shutdown system after installation"):
+          machine.succeed("umount /mnt/boot || true")
+          machine.succeed("umount /mnt")
+          machine.succeed("sync")
+          machine.shutdown()
 
       # Now see if we can boot the installation.
-      $machine = createMachine({ ${hdFlags} qemuFlags => "${qemuFlags}", name => "boot-after-install" });
+      machine = create_machine_named("boot-after-install")
 
       # For example to enter LUKS passphrase.
       ${preBootCommands}
 
-      # Did /boot get mounted?
-      $machine->waitForUnit("local-fs.target");
-
-      ${if bootLoader == "grub" then
-          ''$machine->succeed("test -e /boot/grub");''
-        else
-          ''$machine->succeed("test -e /boot/loader/loader.conf");''
-      }
-
-      # Check whether /root has correct permissions.
-      $machine->succeed("stat -c '%a' /root") =~ /700/ or die;
-
-      # Did the swap device get activated?
-      # uncomment once https://bugs.freedesktop.org/show_bug.cgi?id=86930 is resolved
-      $machine->waitForUnit("swap.target");
-      $machine->succeed("cat /proc/swaps | grep -q /dev");
-
-      # Check that the store is in good shape
-      $machine->succeed("nix-store --verify --check-contents >&2");
-
-      # Check whether the channel works.
-      $machine->succeed("nix-env -iA nixos.procps >&2");
-      $machine->succeed("type -tP ps | tee /dev/stderr") =~ /.nix-profile/
-          or die "nix-env failed";
-
-      # Check that the daemon works, and that non-root users can run builds (this will build a new profile generation through the daemon)
-      $machine->succeed("su alice -l -c 'nix-env -iA nixos.procps' >&2");
-
-      # We need a writable Nix store on next boot.
-      $machine->copyFileFromHost(
-          "${ makeConfig { inherit bootLoader grubVersion grubDevice grubIdentifier grubUseEfi extraConfig; forceGrubReinstallCount = 1; } }",
-          "/etc/nixos/configuration.nix");
-
-      # Check whether nixos-rebuild works.
-      $machine->succeed("nixos-rebuild switch >&2");
-
-      # Test nixos-option.
-      $machine->succeed("nixos-option boot.initrd.kernelModules | grep virtio_console");
-      $machine->succeed("nixos-option boot.initrd.kernelModules | grep 'List of modules'");
-      $machine->succeed("nixos-option boot.initrd.kernelModules | grep qemu-guest.nix");
-
-      $machine->shutdown;
+      with subtest("Assert that /boot get mounted"):
+          machine.wait_for_unit("local-fs.target")
+          ${if bootLoader == "grub"
+              then ''machine.succeed("test -e /boot/grub")''
+              else ''machine.succeed("test -e /boot/loader/loader.conf")''
+          }
+
+      with subtest("Check whether /root has correct permissions"):
+          assert "700" in machine.succeed("stat -c '%a' /root")
+
+      with subtest("Assert swap device got activated"):
+          # uncomment once https://bugs.freedesktop.org/show_bug.cgi?id=86930 is resolved
+          machine.wait_for_unit("swap.target")
+          machine.succeed("cat /proc/swaps | grep -q /dev")
+
+      with subtest("Check that the store is in good shape"):
+          machine.succeed("nix-store --verify --check-contents >&2")
+
+      with subtest("Check whether the channel works"):
+          machine.succeed("nix-env -iA nixos.procps >&2")
+          assert ".nix-profile" in machine.succeed("type -tP ps | tee /dev/stderr")
+
+      with subtest(
+          "Check that the daemon works, and that non-root users can run builds "
+          "(this will build a new profile generation through the daemon)"
+      ):
+          machine.succeed("su alice -l -c 'nix-env -iA nixos.procps' >&2")
+
+      with subtest("Configure system with writable Nix store on next boot"):
+          # we're not using copy_from_host here because the installer image
+          # doesn't know about the host-guest sharing mechanism.
+          machine.copy_from_host_via_shell(
+              "${ makeConfig {
+                    inherit bootLoader grubVersion grubDevice grubIdentifier
+                            grubUseEfi extraConfig;
+                    forceGrubReinstallCount = 1;
+                  }
+              }",
+              "/etc/nixos/configuration.nix",
+          )
+
+      with subtest("Check whether nixos-rebuild works"):
+          machine.succeed("nixos-rebuild switch >&2")
+
+      with subtest("Test nixos-option"):
+          kernel_modules = machine.succeed("nixos-option boot.initrd.kernelModules")
+          assert "virtio_console" in kernel_modules
+          assert "List of modules" in kernel_modules
+          assert "qemu-guest.nix" in kernel_modules
+
+      machine.shutdown()
 
       # Check whether a writable store build works
-      $machine = createMachine({ ${hdFlags} qemuFlags => "${qemuFlags}", name => "rebuild-switch" });
+      machine = create_machine_named("rebuild-switch")
       ${preBootCommands}
-      $machine->waitForUnit("multi-user.target");
-      $machine->copyFileFromHost(
-          "${ makeConfig { inherit bootLoader grubVersion grubDevice grubIdentifier grubUseEfi extraConfig; forceGrubReinstallCount = 2; } }",
-          "/etc/nixos/configuration.nix");
-      $machine->succeed("nixos-rebuild boot >&2");
-      $machine->shutdown;
+      machine.wait_for_unit("multi-user.target")
+
+      # we're not using copy_from_host here because the installer image
+      # doesn't know about the host-guest sharing mechanism.
+      machine.copy_from_host_via_shell(
+          "${ makeConfig {
+                inherit bootLoader grubVersion grubDevice grubIdentifier
+                grubUseEfi extraConfig;
+                forceGrubReinstallCount = 2;
+              }
+          }",
+          "/etc/nixos/configuration.nix",
+      )
+      machine.succeed("nixos-rebuild boot >&2")
+      machine.shutdown()
 
       # And just to be sure, check that the machine still boots after
       # "nixos-rebuild switch".
-      $machine = createMachine({ ${hdFlags} qemuFlags => "${qemuFlags}", "boot-after-rebuild-switch" });
+      machine = create_machine_named("boot-after-rebuild-switch")
       ${preBootCommands}
-      $machine->waitForUnit("network.target");
-      $machine->shutdown;
+      machine.wait_for_unit("network.target")
+      machine.shutdown()
 
       # Tests for validating clone configuration entries in grub menu
-      ${optionalString testCloneConfig ''
-        # Reboot Machine
-        $machine = createMachine({ ${hdFlags} qemuFlags => "${qemuFlags}", name => "clone-default-config" });
-        ${preBootCommands}
-        $machine->waitForUnit("multi-user.target");
-
-        # Booted configuration name should be Home
-        # This is not the name that shows in the grub menu.
-        # The default configuration is always shown as "Default"
-        $machine->succeed("cat /run/booted-system/configuration-name >&2");
-        $machine->succeed("cat /run/booted-system/configuration-name | grep Home");
+    ''
+    + optionalString testCloneConfig ''
+      # Reboot Machine
+      machine = create_machine_named("clone-default-config")
+      ${preBootCommands}
+      machine.wait_for_unit("multi-user.target")
 
-        # We should find **not** a file named /etc/gitconfig
-        $machine->fail("test -e /etc/gitconfig");
+      with subtest("Booted configuration name should be 'Home'"):
+          # This is not the name that shows in the grub menu.
+          # The default configuration is always shown as "Default"
+          machine.succeed("cat /run/booted-system/configuration-name >&2")
+          assert "Home" in machine.succeed("cat /run/booted-system/configuration-name")
 
-        # Set grub to boot the second configuration
-        $machine->succeed("grub-reboot 1");
+      with subtest("We should **not** find a file named /etc/gitconfig"):
+          machine.fail("test -e /etc/gitconfig")
 
-        $machine->shutdown;
+      with subtest("Set grub to boot the second configuration"):
+          machine.succeed("grub-reboot 1")
 
-        # Reboot Machine
-        $machine = createMachine({ ${hdFlags} qemuFlags => "${qemuFlags}", name => "clone-alternate-config" });
-        ${preBootCommands}
+      machine.shutdown()
 
-        $machine->waitForUnit("multi-user.target");
-        # Booted configuration name should be Work
-        $machine->succeed("cat /run/booted-system/configuration-name >&2");
-        $machine->succeed("cat /run/booted-system/configuration-name | grep Work");
+      # Reboot Machine
+      machine = create_machine_named("clone-alternate-config")
+      ${preBootCommands}
 
-        # We should find a file named /etc/gitconfig
-        $machine->succeed("test -e /etc/gitconfig");
+      machine.wait_for_unit("multi-user.target")
+      with subtest("Booted configuration name should be Work"):
+          machine.succeed("cat /run/booted-system/configuration-name >&2")
+          assert "Work" in machine.succeed("cat /run/booted-system/configuration-name")
 
-        $machine->shutdown;
-      ''}
+      with subtest("We should find a file named /etc/gitconfig"):
+          machine.succeed("test -e /etc/gitconfig")
 
+      machine.shutdown()
     '';
 
 
@@ -243,63 +275,63 @@ let
       nodes = {
 
         # The configuration of the machine used to run "nixos-install".
-        machine =
-          { pkgs, ... }:
-
-          { imports =
-              [ ../modules/profiles/installation-device.nix
-                ../modules/profiles/base.nix
-                extraInstallerConfig
-              ];
-
-            virtualisation.diskSize = 8 * 1024;
-            virtualisation.memorySize = 1024;
-
-            # Use a small /dev/vdb as the root disk for the
-            # installer. This ensures the target disk (/dev/vda) is
-            # the same during and after installation.
-            virtualisation.emptyDiskImages = [ 512 ];
-            virtualisation.bootDevice =
-              if grubVersion == 1 then "/dev/sdb" else "/dev/vdb";
-            virtualisation.qemu.diskInterface =
-              if grubVersion == 1 then "scsi" else "virtio";
-
-            boot.loader.systemd-boot.enable = mkIf (bootLoader == "systemd-boot") true;
-
-            hardware.enableAllFirmware = mkForce false;
-
-            # The test cannot access the network, so any packages we
-            # need must be included in the VM.
-            system.extraDependencies = with pkgs;
-              [ sudo
-                libxml2.bin
-                libxslt.bin
-                desktop-file-utils
-                docbook5
-                docbook_xsl_ns
-                unionfs-fuse
-                ntp
-                nixos-artwork.wallpapers.simple-dark-gray-bottom
-                perlPackages.XMLLibXML
-                perlPackages.ListCompare
-                shared-mime-info
-                texinfo
-                xorg.lndir
-
-                # add curl so that rather than seeing the test attempt to download
-                # curl's tarball, we see what it's trying to download
-                curl
-              ]
-              ++ optional (bootLoader == "grub" && grubVersion == 1) pkgs.grub
-              ++ optionals (bootLoader == "grub" && grubVersion == 2) [ pkgs.grub2 pkgs.grub2_efi ];
-
-            nix.binaryCaches = mkForce [ ];
-            nix.extraOptions =
-              ''
-                hashed-mirrors =
-                connect-timeout = 1
-              '';
-          };
+        machine = { pkgs, ... }: {
+          imports = [
+            ../modules/profiles/installation-device.nix
+            ../modules/profiles/base.nix
+            extraInstallerConfig
+          ];
+
+          virtualisation.diskSize = 8 * 1024;
+          virtualisation.memorySize = 1024;
+
+          # Use a small /dev/vdb as the root disk for the
+          # installer. This ensures the target disk (/dev/vda) is
+          # the same during and after installation.
+          virtualisation.emptyDiskImages = [ 512 ];
+          virtualisation.bootDevice =
+            if grubVersion == 1 then "/dev/sdb" else "/dev/vdb";
+          virtualisation.qemu.diskInterface =
+            if grubVersion == 1 then "scsi" else "virtio";
+
+          boot.loader.systemd-boot.enable = mkIf (bootLoader == "systemd-boot") true;
+
+          hardware.enableAllFirmware = mkForce false;
+
+          # The test cannot access the network, so any packages we
+          # need must be included in the VM.
+          system.extraDependencies = with pkgs; [
+            desktop-file-utils
+            docbook5
+            docbook_xsl_ns
+            libxml2.bin
+            libxslt.bin
+            nixos-artwork.wallpapers.simple-dark-gray-bottom
+            ntp
+            perlPackages.ListCompare
+            perlPackages.XMLLibXML
+            shared-mime-info
+            sudo
+            texinfo
+            unionfs-fuse
+            xorg.lndir
+
+            # add curl so that rather than seeing the test attempt to download
+            # curl's tarball, we see what it's trying to download
+            curl
+          ]
+          ++ optional (bootLoader == "grub" && grubVersion == 1) pkgs.grub
+          ++ optionals (bootLoader == "grub" && grubVersion == 2) [
+            pkgs.grub2
+            pkgs.grub2_efi
+          ];
+
+          nix.binaryCaches = mkForce [ ];
+          nix.extraOptions = ''
+            hashed-mirrors =
+            connect-timeout = 1
+          '';
+        };
 
       };
 
@@ -310,13 +342,13 @@ let
       };
     };
 
-    makeLuksRootTest = name: luksFormatOpts: makeInstallerTest name
-      { createPartitions = ''
-          $machine->succeed(
+    makeLuksRootTest = name: luksFormatOpts: makeInstallerTest name {
+      createPartitions = ''
+        machine.succeed(
             "flock /dev/vda parted --script /dev/vda -- mklabel msdos"
-            . " mkpart primary ext2 1M 50MB" # /boot
-            . " mkpart primary linux-swap 50M 1024M"
-            . " mkpart primary 1024M -1s", # LUKS
+            + " mkpart primary ext2 1M 50MB"  # /boot
+            + " mkpart primary linux-swap 50M 1024M"
+            + " mkpart primary 1024M -1s",  # LUKS
             "udevadm settle",
             "mkswap /dev/vda2 -L swap",
             "swapon -L swap",
@@ -328,77 +360,74 @@ let
             "mkfs.ext3 -L boot /dev/vda1",
             "mkdir -p /mnt/boot",
             "mount LABEL=boot /mnt/boot",
-          );
-        '';
-        extraConfig = ''
-          boot.kernelParams = lib.mkAfter [ "console=tty0" ];
-        '';
-        enableOCR = true;
-        preBootCommands = ''
-          $machine->start;
-          $machine->waitForText(qr/Passphrase for/);
-          $machine->sendChars("supersecret\n");
-        '';
-      };
+        )
+      '';
+      extraConfig = ''
+        boot.kernelParams = lib.mkAfter [ "console=tty0" ];
+      '';
+      enableOCR = true;
+      preBootCommands = ''
+        machine.start()
+        machine.wait_for_text("Passphrase for")
+        machine.send_chars("supersecret\n")
+      '';
+    };
 
   # The (almost) simplest partitioning scheme: a swap partition and
   # one big filesystem partition.
-  simple-test-config = { createPartitions =
-       ''
-         $machine->succeed(
-             "flock /dev/vda parted --script /dev/vda -- mklabel msdos"
-             . " mkpart primary linux-swap 1M 1024M"
-             . " mkpart primary ext2 1024M -1s",
-             "udevadm settle",
-             "mkswap /dev/vda1 -L swap",
-             "swapon -L swap",
-             "mkfs.ext3 -L nixos /dev/vda2",
-             "mount LABEL=nixos /mnt",
-         );
-       '';
-   };
-
-  simple-uefi-grub-config =
-    { createPartitions =
-        ''
-          $machine->succeed(
-              "flock /dev/vda parted --script /dev/vda -- mklabel gpt"
-              . " mkpart ESP fat32 1M 50MiB" # /boot
-              . " set 1 boot on"
-              . " mkpart primary linux-swap 50MiB 1024MiB"
-              . " mkpart primary ext2 1024MiB -1MiB", # /
-              "udevadm settle",
-              "mkswap /dev/vda2 -L swap",
-              "swapon -L swap",
-              "mkfs.ext3 -L nixos /dev/vda3",
-              "mount LABEL=nixos /mnt",
-              "mkfs.vfat -n BOOT /dev/vda1",
-              "mkdir -p /mnt/boot",
-              "mount LABEL=BOOT /mnt/boot",
-          );
-        '';
-        bootLoader = "grub";
-        grubUseEfi = true;
-    };
+  simple-test-config = {
+    createPartitions = ''
+      machine.succeed(
+          "flock /dev/vda parted --script /dev/vda -- mklabel msdos"
+          + " mkpart primary linux-swap 1M 1024M"
+          + " mkpart primary ext2 1024M -1s",
+          "udevadm settle",
+          "mkswap /dev/vda1 -L swap",
+          "swapon -L swap",
+          "mkfs.ext3 -L nixos /dev/vda2",
+          "mount LABEL=nixos /mnt",
+      )
+    '';
+  };
+
+  simple-uefi-grub-config = {
+    createPartitions = ''
+      machine.succeed(
+          "flock /dev/vda parted --script /dev/vda -- mklabel gpt"
+          + " mkpart ESP fat32 1M 50MiB"  # /boot
+          + " set 1 boot on"
+          + " mkpart primary linux-swap 50MiB 1024MiB"
+          + " mkpart primary ext2 1024MiB -1MiB",  # /
+          "udevadm settle",
+          "mkswap /dev/vda2 -L swap",
+          "swapon -L swap",
+          "mkfs.ext3 -L nixos /dev/vda3",
+          "mount LABEL=nixos /mnt",
+          "mkfs.vfat -n BOOT /dev/vda1",
+          "mkdir -p /mnt/boot",
+          "mount LABEL=BOOT /mnt/boot",
+      )
+    '';
+    bootLoader = "grub";
+    grubUseEfi = true;
+  };
 
-  clone-test-extraconfig = { extraConfig =
-         ''
-         environment.systemPackages = [ pkgs.grub2 ];
-         boot.loader.grub.configurationName = "Home";
-         nesting.clone = [
-         {
-           boot.loader.grub.configurationName = lib.mkForce "Work";
-
-           environment.etc = {
-             "gitconfig".text = "
-               [core]
-                 gitproxy = none for work.com
-                 ";
-           };
-         }
-         ];
-         '';
-       testCloneConfig = true;
+  clone-test-extraconfig = {
+    extraConfig = ''
+      environment.systemPackages = [ pkgs.grub2 ];
+      boot.loader.grub.configurationName = "Home";
+      nesting.clone = [ {
+        boot.loader.grub.configurationName = lib.mkForce "Work";
+
+        environment.etc = {
+          "gitconfig".text = "
+            [core]
+              gitproxy = none for work.com
+              ";
+        };
+      } ];
+    '';
+    testCloneConfig = true;
   };
 
 
@@ -415,27 +444,26 @@ in {
   simpleClone = makeInstallerTest "simpleClone" (simple-test-config // clone-test-extraconfig);
 
   # Simple GPT/UEFI configuration using systemd-boot with 3 partitions: ESP, swap & root filesystem
-  simpleUefiSystemdBoot = makeInstallerTest "simpleUefiSystemdBoot"
-    { createPartitions =
-        ''
-          $machine->succeed(
-              "flock /dev/vda parted --script /dev/vda -- mklabel gpt"
-              . " mkpart ESP fat32 1M 50MiB" # /boot
-              . " set 1 boot on"
-              . " mkpart primary linux-swap 50MiB 1024MiB"
-              . " mkpart primary ext2 1024MiB -1MiB", # /
-              "udevadm settle",
-              "mkswap /dev/vda2 -L swap",
-              "swapon -L swap",
-              "mkfs.ext3 -L nixos /dev/vda3",
-              "mount LABEL=nixos /mnt",
-              "mkfs.vfat -n BOOT /dev/vda1",
-              "mkdir -p /mnt/boot",
-              "mount LABEL=BOOT /mnt/boot",
-          );
-        '';
-        bootLoader = "systemd-boot";
-    };
+  simpleUefiSystemdBoot = makeInstallerTest "simpleUefiSystemdBoot" {
+    createPartitions = ''
+      machine.succeed(
+          "flock /dev/vda parted --script /dev/vda -- mklabel gpt"
+          + " mkpart ESP fat32 1M 50MiB"  # /boot
+          + " set 1 boot on"
+          + " mkpart primary linux-swap 50MiB 1024MiB"
+          + " mkpart primary ext2 1024MiB -1MiB",  # /
+          "udevadm settle",
+          "mkswap /dev/vda2 -L swap",
+          "swapon -L swap",
+          "mkfs.ext3 -L nixos /dev/vda3",
+          "mount LABEL=nixos /mnt",
+          "mkfs.vfat -n BOOT /dev/vda1",
+          "mkdir -p /mnt/boot",
+          "mount LABEL=BOOT /mnt/boot",
+      )
+    '';
+    bootLoader = "systemd-boot";
+  };
 
   simpleUefiGrub = makeInstallerTest "simpleUefiGrub" simple-uefi-grub-config;
 
@@ -443,107 +471,99 @@ in {
   simpleUefiGrubClone = makeInstallerTest "simpleUefiGrubClone" (simple-uefi-grub-config // clone-test-extraconfig);
 
   # Same as the previous, but now with a separate /boot partition.
-  separateBoot = makeInstallerTest "separateBoot"
-    { createPartitions =
-        ''
-          $machine->succeed(
-              "flock /dev/vda parted --script /dev/vda -- mklabel msdos"
-              . " mkpart primary ext2 1M 50MB" # /boot
-              . " mkpart primary linux-swap 50MB 1024M"
-              . " mkpart primary ext2 1024M -1s", # /
-              "udevadm settle",
-              "mkswap /dev/vda2 -L swap",
-              "swapon -L swap",
-              "mkfs.ext3 -L nixos /dev/vda3",
-              "mount LABEL=nixos /mnt",
-              "mkfs.ext3 -L boot /dev/vda1",
-              "mkdir -p /mnt/boot",
-              "mount LABEL=boot /mnt/boot",
-          );
-        '';
-    };
+  separateBoot = makeInstallerTest "separateBoot" {
+    createPartitions = ''
+      machine.succeed(
+          "flock /dev/vda parted --script /dev/vda -- mklabel msdos"
+          + " mkpart primary ext2 1M 50MB"  # /boot
+          + " mkpart primary linux-swap 50MB 1024M"
+          + " mkpart primary ext2 1024M -1s",  # /
+          "udevadm settle",
+          "mkswap /dev/vda2 -L swap",
+          "swapon -L swap",
+          "mkfs.ext3 -L nixos /dev/vda3",
+          "mount LABEL=nixos /mnt",
+          "mkfs.ext3 -L boot /dev/vda1",
+          "mkdir -p /mnt/boot",
+          "mount LABEL=boot /mnt/boot",
+      )
+    '';
+  };
 
   # Same as the previous, but with fat32 /boot.
-  separateBootFat = makeInstallerTest "separateBootFat"
-    { createPartitions =
-        ''
-          $machine->succeed(
-              "flock /dev/vda parted --script /dev/vda -- mklabel msdos"
-              . " mkpart primary ext2 1M 50MB" # /boot
-              . " mkpart primary linux-swap 50MB 1024M"
-              . " mkpart primary ext2 1024M -1s", # /
-              "udevadm settle",
-              "mkswap /dev/vda2 -L swap",
-              "swapon -L swap",
-              "mkfs.ext3 -L nixos /dev/vda3",
-              "mount LABEL=nixos /mnt",
-              "mkfs.vfat -n BOOT /dev/vda1",
-              "mkdir -p /mnt/boot",
-              "mount LABEL=BOOT /mnt/boot",
-          );
-        '';
-    };
+  separateBootFat = makeInstallerTest "separateBootFat" {
+    createPartitions = ''
+      machine.succeed(
+          "flock /dev/vda parted --script /dev/vda -- mklabel msdos"
+          + " mkpart primary ext2 1M 50MB"  # /boot
+          + " mkpart primary linux-swap 50MB 1024M"
+          + " mkpart primary ext2 1024M -1s",  # /
+          "udevadm settle",
+          "mkswap /dev/vda2 -L swap",
+          "swapon -L swap",
+          "mkfs.ext3 -L nixos /dev/vda3",
+          "mount LABEL=nixos /mnt",
+          "mkfs.vfat -n BOOT /dev/vda1",
+          "mkdir -p /mnt/boot",
+          "mount LABEL=BOOT /mnt/boot",
+      )
+    '';
+  };
 
   # zfs on / with swap
-  zfsroot = makeInstallerTest "zfs-root"
-    {
-      extraInstallerConfig = {
-        boot.supportedFilesystems = [ "zfs" ];
-      };
-
-      extraConfig = ''
-        boot.supportedFilesystems = [ "zfs" ];
-
-        # Using by-uuid overrides the default of by-id, and is unique
-        # to the qemu disks, as they don't produce by-id paths for
-        # some reason.
-        boot.zfs.devNodes = "/dev/disk/by-uuid/";
-        networking.hostId = "00000000";
-      '';
-
-      createPartitions =
-        ''
-          $machine->succeed(
-              "flock /dev/vda parted --script /dev/vda -- mklabel msdos"
-              . " mkpart primary linux-swap 1M 1024M"
-              . " mkpart primary 1024M -1s",
-              "udevadm settle",
+  zfsroot = makeInstallerTest "zfs-root" {
+    extraInstallerConfig = {
+      boot.supportedFilesystems = [ "zfs" ];
+    };
 
-              "mkswap /dev/vda1 -L swap",
-              "swapon -L swap",
+    extraConfig = ''
+      boot.supportedFilesystems = [ "zfs" ];
 
-              "zpool create rpool /dev/vda2",
-              "zfs create -o mountpoint=legacy rpool/root",
-              "mount -t zfs rpool/root /mnt",
+      # Using by-uuid overrides the default of by-id, and is unique
+      # to the qemu disks, as they don't produce by-id paths for
+      # some reason.
+      boot.zfs.devNodes = "/dev/disk/by-uuid/";
+      networking.hostId = "00000000";
+    '';
 
-              "udevadm settle"
-          );
-        '';
-    };
+    createPartitions = ''
+      machine.succeed(
+          "flock /dev/vda parted --script /dev/vda -- mklabel msdos"
+          + " mkpart primary linux-swap 1M 1024M"
+          + " mkpart primary 1024M -1s",
+          "udevadm settle",
+          "mkswap /dev/vda1 -L swap",
+          "swapon -L swap",
+          "zpool create rpool /dev/vda2",
+          "zfs create -o mountpoint=legacy rpool/root",
+          "mount -t zfs rpool/root /mnt",
+          "udevadm settle",
+      )
+    '';
+  };
 
   # Create two physical LVM partitions combined into one volume group
   # that contains the logical swap and root partitions.
-  lvm = makeInstallerTest "lvm"
-    { createPartitions =
-        ''
-          $machine->succeed(
-              "flock /dev/vda parted --script /dev/vda -- mklabel msdos"
-              . " mkpart primary 1M 2048M" # PV1
-              . " set 1 lvm on"
-              . " mkpart primary 2048M -1s" # PV2
-              . " set 2 lvm on",
-              "udevadm settle",
-              "pvcreate /dev/vda1 /dev/vda2",
-              "vgcreate MyVolGroup /dev/vda1 /dev/vda2",
-              "lvcreate --size 1G --name swap MyVolGroup",
-              "lvcreate --size 2G --name nixos MyVolGroup",
-              "mkswap -f /dev/MyVolGroup/swap -L swap",
-              "swapon -L swap",
-              "mkfs.xfs -L nixos /dev/MyVolGroup/nixos",
-              "mount LABEL=nixos /mnt",
-          );
-        '';
-    };
+  lvm = makeInstallerTest "lvm" {
+    createPartitions = ''
+      machine.succeed(
+          "flock /dev/vda parted --script /dev/vda -- mklabel msdos"
+          + " mkpart primary 1M 2048M"  # PV1
+          + " set 1 lvm on"
+          + " mkpart primary 2048M -1s"  # PV2
+          + " set 2 lvm on",
+          "udevadm settle",
+          "pvcreate /dev/vda1 /dev/vda2",
+          "vgcreate MyVolGroup /dev/vda1 /dev/vda2",
+          "lvcreate --size 1G --name swap MyVolGroup",
+          "lvcreate --size 2G --name nixos MyVolGroup",
+          "mkswap -f /dev/MyVolGroup/swap -L swap",
+          "swapon -L swap",
+          "mkfs.xfs -L nixos /dev/MyVolGroup/nixos",
+          "mount LABEL=nixos /mnt",
+      )
+    '';
+  };
 
   # Boot off an encrypted root partition with the default LUKS header format
   luksroot = makeLuksRootTest "luksroot-format1" "";
@@ -557,14 +577,14 @@ in {
   # Test whether opening encrypted filesystem with keyfile
   # Checks for regression of missing cryptsetup, when no luks device without
   # keyfile is configured
-  encryptedFSWithKeyfile = makeInstallerTest "encryptedFSWithKeyfile"
-    { createPartitions = ''
-       $machine->succeed(
+  encryptedFSWithKeyfile = makeInstallerTest "encryptedFSWithKeyfile" {
+    createPartitions = ''
+      machine.succeed(
           "flock /dev/vda parted --script /dev/vda -- mklabel msdos"
-          . " mkpart primary ext2 1M 50MB" # /boot
-          . " mkpart primary linux-swap 50M 1024M"
-          . " mkpart primary 1024M 1280M" # LUKS with keyfile
-          . " mkpart primary 1280M -1s",
+          + " mkpart primary ext2 1M 50MB"  # /boot
+          + " mkpart primary linux-swap 50M 1024M"
+          + " mkpart primary 1024M 1280M"  # LUKS with keyfile
+          + " mkpart primary 1280M -1s",
           "udevadm settle",
           "mkswap /dev/vda2 -L swap",
           "swapon -L swap",
@@ -579,89 +599,88 @@ in {
           "cryptsetup luksOpen --key-file /mnt/keyfile /dev/vda3 crypt",
           "mkfs.ext3 -L test /dev/mapper/crypt",
           "cryptsetup luksClose crypt",
-          "mkdir -p /mnt/test"
-        );
-      '';
-      extraConfig = ''
-        fileSystems."/test" =
-        { device = "/dev/disk/by-label/test";
-          fsType = "ext3";
-          encrypted.enable = true;
-          encrypted.blkDev = "/dev/vda3";
-          encrypted.label = "crypt";
-          encrypted.keyFile = "/mnt-root/keyfile";
-        };
-      '';
-    };
-
+          "mkdir -p /mnt/test",
+      )
+    '';
+    extraConfig = ''
+      fileSystems."/test" = {
+        device = "/dev/disk/by-label/test";
+        fsType = "ext3";
+        encrypted.enable = true;
+        encrypted.blkDev = "/dev/vda3";
+        encrypted.label = "crypt";
+        encrypted.keyFile = "/mnt-root/keyfile";
+      };
+    '';
+  };
 
-  swraid = makeInstallerTest "swraid"
-    { createPartitions =
-        ''
-          $machine->succeed(
-              "flock /dev/vda parted --script /dev/vda --"
-              . " mklabel msdos"
-              . " mkpart primary ext2 1M 100MB" # /boot
-              . " mkpart extended 100M -1s"
-              . " mkpart logical 102M 2102M" # md0 (root), first device
-              . " mkpart logical 2103M 4103M" # md0 (root), second device
-              . " mkpart logical 4104M 4360M" # md1 (swap), first device
-              . " mkpart logical 4361M 4617M", # md1 (swap), second device
-              "udevadm settle",
-              "ls -l /dev/vda* >&2",
-              "cat /proc/partitions >&2",
-              "udevadm control --stop-exec-queue",
-              "mdadm --create --force /dev/md0 --metadata 1.2 --level=raid1 --raid-devices=2 /dev/vda5 /dev/vda6",
-              "mdadm --create --force /dev/md1 --metadata 1.2 --level=raid1 --raid-devices=2 /dev/vda7 /dev/vda8",
-              "udevadm control --start-exec-queue",
-              "udevadm settle",
-              "mkswap -f /dev/md1 -L swap",
-              "swapon -L swap",
-              "mkfs.ext3 -L nixos /dev/md0",
-              "mount LABEL=nixos /mnt",
-              "mkfs.ext3 -L boot /dev/vda1",
-              "mkdir /mnt/boot",
-              "mount LABEL=boot /mnt/boot",
-              "udevadm settle",
-          );
-        '';
-      preBootCommands = ''
-        $machine->start;
-        $machine->fail("dmesg | grep 'immediate safe mode'");
-      '';
-    };
+  swraid = makeInstallerTest "swraid" {
+    createPartitions = ''
+      machine.succeed(
+          "flock /dev/vda parted --script /dev/vda --"
+          + " mklabel msdos"
+          + " mkpart primary ext2 1M 100MB"  # /boot
+          + " mkpart extended 100M -1s"
+          + " mkpart logical 102M 2102M"  # md0 (root), first device
+          + " mkpart logical 2103M 4103M"  # md0 (root), second device
+          + " mkpart logical 4104M 4360M"  # md1 (swap), first device
+          + " mkpart logical 4361M 4617M",  # md1 (swap), second device
+          "udevadm settle",
+          "ls -l /dev/vda* >&2",
+          "cat /proc/partitions >&2",
+          "udevadm control --stop-exec-queue",
+          "mdadm --create --force /dev/md0 --metadata 1.2 --level=raid1 "
+          + "--raid-devices=2 /dev/vda5 /dev/vda6",
+          "mdadm --create --force /dev/md1 --metadata 1.2 --level=raid1 "
+          + "--raid-devices=2 /dev/vda7 /dev/vda8",
+          "udevadm control --start-exec-queue",
+          "udevadm settle",
+          "mkswap -f /dev/md1 -L swap",
+          "swapon -L swap",
+          "mkfs.ext3 -L nixos /dev/md0",
+          "mount LABEL=nixos /mnt",
+          "mkfs.ext3 -L boot /dev/vda1",
+          "mkdir /mnt/boot",
+          "mount LABEL=boot /mnt/boot",
+          "udevadm settle",
+      )
+    '';
+    preBootCommands = ''
+      machine.start()
+      machine.fail("dmesg | grep 'immediate safe mode'")
+    '';
+  };
 
   # Test a basic install using GRUB 1.
-  grub1 = makeInstallerTest "grub1"
-    { createPartitions =
-        ''
-          $machine->succeed(
-              "flock /dev/sda parted --script /dev/sda -- mklabel msdos"
-              . " mkpart primary linux-swap 1M 1024M"
-              . " mkpart primary ext2 1024M -1s",
-              "udevadm settle",
-              "mkswap /dev/sda1 -L swap",
-              "swapon -L swap",
-              "mkfs.ext3 -L nixos /dev/sda2",
-              "mount LABEL=nixos /mnt",
-              "mkdir -p /mnt/tmp",
-          );
-        '';
-      grubVersion = 1;
-      grubDevice = "/dev/sda";
-    };
+  grub1 = makeInstallerTest "grub1" {
+    createPartitions = ''
+      machine.succeed(
+          "flock /dev/sda parted --script /dev/sda -- mklabel msdos"
+          + " mkpart primary linux-swap 1M 1024M"
+          + " mkpart primary ext2 1024M -1s",
+          "udevadm settle",
+          "mkswap /dev/sda1 -L swap",
+          "swapon -L swap",
+          "mkfs.ext3 -L nixos /dev/sda2",
+          "mount LABEL=nixos /mnt",
+          "mkdir -p /mnt/tmp",
+      )
+    '';
+    grubVersion = 1;
+    grubDevice = "/dev/sda";
+  };
 
   # Test using labels to identify volumes in grub
   simpleLabels = makeInstallerTest "simpleLabels" {
     createPartitions = ''
-      $machine->succeed(
-        "sgdisk -Z /dev/vda",
-        "sgdisk -n 1:0:+1M -n 2:0:+1G -N 3 -t 1:ef02 -t 2:8200 -t 3:8300 -c 3:root /dev/vda",
-        "mkswap /dev/vda2 -L swap",
-        "swapon -L swap",
-        "mkfs.ext4 -L root /dev/vda3",
-        "mount LABEL=root /mnt",
-      );
+      machine.succeed(
+          "sgdisk -Z /dev/vda",
+          "sgdisk -n 1:0:+1M -n 2:0:+1G -N 3 -t 1:ef02 -t 2:8200 -t 3:8300 -c 3:root /dev/vda",
+          "mkswap /dev/vda2 -L swap",
+          "swapon -L swap",
+          "mkfs.ext4 -L root /dev/vda3",
+          "mount LABEL=root /mnt",
+      )
     '';
     grubIdentifier = "label";
   };
@@ -670,22 +689,23 @@ in {
   # TODO: Fix udev so the symlinks are unneeded in /dev/disks
   simpleProvided = makeInstallerTest "simpleProvided" {
     createPartitions = ''
-      my $UUID = "\$(blkid -s UUID -o value /dev/vda2)";
-      $machine->succeed(
-        "sgdisk -Z /dev/vda",
-        "sgdisk -n 1:0:+1M -n 2:0:+100M -n 3:0:+1G -N 4 -t 1:ef02 -t 2:8300 -t 3:8200 -t 4:8300 -c 2:boot -c 4:root /dev/vda",
-        "mkswap /dev/vda3 -L swap",
-        "swapon -L swap",
-        "mkfs.ext4 -L boot /dev/vda2",
-        "mkfs.ext4 -L root /dev/vda4",
-      );
-      $machine->execute("ln -s ../../vda2 /dev/disk/by-uuid/$UUID");
-      $machine->execute("ln -s ../../vda4 /dev/disk/by-label/root");
-      $machine->succeed(
-        "mount /dev/disk/by-label/root /mnt",
-        "mkdir /mnt/boot",
-        "mount /dev/disk/by-uuid/$UUID /mnt/boot"
-      );
+      uuid = "$(blkid -s UUID -o value /dev/vda2)"
+      machine.succeed(
+          "sgdisk -Z /dev/vda",
+          "sgdisk -n 1:0:+1M -n 2:0:+100M -n 3:0:+1G -N 4 -t 1:ef02 -t 2:8300 "
+          + "-t 3:8200 -t 4:8300 -c 2:boot -c 4:root /dev/vda",
+          "mkswap /dev/vda3 -L swap",
+          "swapon -L swap",
+          "mkfs.ext4 -L boot /dev/vda2",
+          "mkfs.ext4 -L root /dev/vda4",
+      )
+      machine.execute(f"ln -s ../../vda2 /dev/disk/by-uuid/{uuid}")
+      machine.execute("ln -s ../../vda4 /dev/disk/by-label/root")
+      machine.succeed(
+          "mount /dev/disk/by-label/root /mnt",
+          "mkdir /mnt/boot",
+          f"mount /dev/disk/by-uuid/{uuid} /mnt/boot",
+      )
     '';
     grubIdentifier = "provided";
   };
@@ -693,61 +713,62 @@ in {
   # Simple btrfs grub testing
   btrfsSimple = makeInstallerTest "btrfsSimple" {
     createPartitions = ''
-      $machine->succeed(
-        "sgdisk -Z /dev/vda",
-        "sgdisk -n 1:0:+1M -n 2:0:+1G -N 3 -t 1:ef02 -t 2:8200 -t 3:8300 -c 3:root /dev/vda",
-        "mkswap /dev/vda2 -L swap",
-        "swapon -L swap",
-        "mkfs.btrfs -L root /dev/vda3",
-        "mount LABEL=root /mnt",
-      );
+      machine.succeed(
+          "sgdisk -Z /dev/vda",
+          "sgdisk -n 1:0:+1M -n 2:0:+1G -N 3 -t 1:ef02 -t 2:8200 -t 3:8300 -c 3:root /dev/vda",
+          "mkswap /dev/vda2 -L swap",
+          "swapon -L swap",
+          "mkfs.btrfs -L root /dev/vda3",
+          "mount LABEL=root /mnt",
+      )
     '';
   };
 
   # Test to see if we can detect /boot and /nix on subvolumes
   btrfsSubvols = makeInstallerTest "btrfsSubvols" {
     createPartitions = ''
-      $machine->succeed(
-        "sgdisk -Z /dev/vda",
-        "sgdisk -n 1:0:+1M -n 2:0:+1G -N 3 -t 1:ef02 -t 2:8200 -t 3:8300 -c 3:root /dev/vda",
-        "mkswap /dev/vda2 -L swap",
-        "swapon -L swap",
-        "mkfs.btrfs -L root /dev/vda3",
-        "btrfs device scan",
-        "mount LABEL=root /mnt",
-        "btrfs subvol create /mnt/boot",
-        "btrfs subvol create /mnt/nixos",
-        "btrfs subvol create /mnt/nixos/default",
-        "umount /mnt",
-        "mount -o defaults,subvol=nixos/default LABEL=root /mnt",
-        "mkdir /mnt/boot",
-        "mount -o defaults,subvol=boot LABEL=root /mnt/boot",
-      );
+      machine.succeed(
+          "sgdisk -Z /dev/vda",
+          "sgdisk -n 1:0:+1M -n 2:0:+1G -N 3 -t 1:ef02 -t 2:8200 -t 3:8300 -c 3:root /dev/vda",
+          "mkswap /dev/vda2 -L swap",
+          "swapon -L swap",
+          "mkfs.btrfs -L root /dev/vda3",
+          "btrfs device scan",
+          "mount LABEL=root /mnt",
+          "btrfs subvol create /mnt/boot",
+          "btrfs subvol create /mnt/nixos",
+          "btrfs subvol create /mnt/nixos/default",
+          "umount /mnt",
+          "mount -o defaults,subvol=nixos/default LABEL=root /mnt",
+          "mkdir /mnt/boot",
+          "mount -o defaults,subvol=boot LABEL=root /mnt/boot",
+      )
     '';
   };
 
   # Test to see if we can detect default and aux subvolumes correctly
   btrfsSubvolDefault = makeInstallerTest "btrfsSubvolDefault" {
     createPartitions = ''
-      $machine->succeed(
-        "sgdisk -Z /dev/vda",
-        "sgdisk -n 1:0:+1M -n 2:0:+1G -N 3 -t 1:ef02 -t 2:8200 -t 3:8300 -c 3:root /dev/vda",
-        "mkswap /dev/vda2 -L swap",
-        "swapon -L swap",
-        "mkfs.btrfs -L root /dev/vda3",
-        "btrfs device scan",
-        "mount LABEL=root /mnt",
-        "btrfs subvol create /mnt/badpath",
-        "btrfs subvol create /mnt/badpath/boot",
-        "btrfs subvol create /mnt/nixos",
-        "btrfs subvol set-default \$(btrfs subvol list /mnt | grep 'nixos' | awk '{print \$2}') /mnt",
-        "umount /mnt",
-        "mount -o defaults LABEL=root /mnt",
-        "mkdir -p /mnt/badpath/boot", # Help ensure the detection mechanism is actually looking up subvolumes
-        "mkdir /mnt/boot",
-        "mount -o defaults,subvol=badpath/boot LABEL=root /mnt/boot",
-      );
+      machine.succeed(
+          "sgdisk -Z /dev/vda",
+          "sgdisk -n 1:0:+1M -n 2:0:+1G -N 3 -t 1:ef02 -t 2:8200 -t 3:8300 -c 3:root /dev/vda",
+          "mkswap /dev/vda2 -L swap",
+          "swapon -L swap",
+          "mkfs.btrfs -L root /dev/vda3",
+          "btrfs device scan",
+          "mount LABEL=root /mnt",
+          "btrfs subvol create /mnt/badpath",
+          "btrfs subvol create /mnt/badpath/boot",
+          "btrfs subvol create /mnt/nixos",
+          "btrfs subvol set-default "
+          + "$(btrfs subvol list /mnt | grep 'nixos' | awk '{print \$2}') /mnt",
+          "umount /mnt",
+          "mount -o defaults LABEL=root /mnt",
+          "mkdir -p /mnt/badpath/boot",  # Help ensure the detection mechanism
+          # is actually looking up subvolumes
+          "mkdir /mnt/boot",
+          "mount -o defaults,subvol=badpath/boot LABEL=root /mnt/boot",
+      )
     '';
   };
-
 }
diff --git a/nixos/tests/keymap.nix b/nixos/tests/keymap.nix
index 2b4c1ab7b05..09d5d2a6c9e 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 ad66ada106b..7228fcb8331 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/lorri/default.nix b/nixos/tests/lorri/default.nix
index 53074385a65..198171082d8 100644
--- a/nixos/tests/lorri/default.nix
+++ b/nixos/tests/lorri/default.nix
@@ -15,12 +15,12 @@ import ../make-test-python.nix {
 
     # Start the daemon and wait until it is ready
     machine.execute("lorri daemon > lorri.stdout 2> lorri.stderr &")
-    machine.wait_until_succeeds("grep --fixed-strings 'lorri: ready' lorri.stdout")
+    machine.wait_until_succeeds("grep --fixed-strings 'ready' lorri.stdout")
 
     # Ping the daemon
-    machine.execute("lorri ping_ $(readlink -f shell.nix)")
+    machine.succeed("lorri internal__ping shell.nix")
 
     # Wait for the daemon to finish the build
-    machine.wait_until_succeeds("grep --fixed-strings 'OutputPaths' lorri.stdout")
+    machine.wait_until_succeeds("grep --fixed-strings 'Completed' lorri.stdout")
   '';
 }
diff --git a/nixos/tests/misc.nix b/nixos/tests/misc.nix
index ca28bc31cf1..17260ce6406 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 ab908c96e5e..bae9c66ed61 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 9448a104073..933a4451af9 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 d0225016ab7..97c9137fe1d 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/printing.nix b/nixos/tests/printing.nix
index 4d0df289cf7..355c94a0386 100644
--- a/nixos/tests/printing.nix
+++ b/nixos/tests/printing.nix
@@ -1,19 +1,18 @@
 # Test printing via CUPS.
 
-import ./make-test.nix ({pkgs, ... }:
+import ./make-test-python.nix ({pkgs, ... }:
 let
   printingServer = startWhenNeeded: {
     services.printing.enable = true;
     services.printing.startWhenNeeded = startWhenNeeded;
     services.printing.listenAddresses = [ "*:631" ];
     services.printing.defaultShared = true;
-    services.printing.extraConf =
-      ''
-        <Location />
-          Order allow,deny
-          Allow from all
-        </Location>
-      '';
+    services.printing.extraConf = ''
+      <Location />
+        Order allow,deny
+        Allow from all
+      </Location>
+    '';
     networking.firewall.allowedTCPPorts = [ 631 ];
     # Add a HP Deskjet printer connected via USB to the server.
     hardware.printers.ensurePrinters = [{
@@ -34,9 +33,7 @@ let
     hardware.printers.ensureDefaultPrinter = "DeskjetRemote";
   };
 
-in
-
-{
+in {
   name = "printing";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ domenkozar eelco matthewbauer ];
@@ -50,65 +47,91 @@ in
     serviceClient = { ... }: (printingClient false);
   };
 
-  testScript =
-    ''
-      startAll;
-
-      # Make sure that cups is up on both sides.
-      $serviceServer->waitForUnit("cups.service");
-      $serviceClient->waitForUnit("cups.service");
-      # wait until cups is fully initialized and ensure-printers has executed with 10s delay
-      $serviceClient->sleep(20);
-      $socketActivatedClient->waitUntilSucceeds("systemctl status ensure-printers | grep -q -E 'code=exited, status=0/SUCCESS'");
-      sub testPrinting {
-          my ($client, $server) = (@_);
-          my $clientHostname = $client->name();
-          my $serverHostname = $server->name();
-          $client->succeed("lpstat -r") =~ /scheduler is running/ or die;
-          # Test that UNIX socket is used for connections.
-          $client->succeed("lpstat -H") =~ "/var/run/cups/cups.sock" or die;
-          # Test that HTTP server is available too.
-          $client->succeed("curl --fail http://localhost:631/");
-          $client->succeed("curl --fail http://$serverHostname:631/");
-          $server->fail("curl --fail --connect-timeout 2  http://$clientHostname:631/");
-          # Do some status checks.
-          $client->succeed("lpstat -a") =~ /DeskjetRemote accepting requests/ or die;
-          $client->succeed("lpstat -h $serverHostname:631 -a") =~ /DeskjetLocal accepting requests/ or die;
-          $client->succeed("cupsdisable DeskjetRemote");
-          $client->succeed("lpq") =~ /DeskjetRemote is not ready.*no entries/s or die;
-          $client->succeed("cupsenable DeskjetRemote");
-          $client->succeed("lpq") =~ /DeskjetRemote is ready.*no entries/s or die;
-          # Test printing various file types.
-          foreach my $file ("${pkgs.groff.doc}/share/doc/*/examples/mom/penguin.pdf",
-                            "${pkgs.groff.doc}/share/doc/*/meref.ps",
-                            "${pkgs.cups.out}/share/doc/cups/images/cups.png",
-                            "${pkgs.pcre.doc}/share/doc/pcre/pcre.txt")
-          {
-              $file =~ /([^\/]*)$/; my $fn = $1;
-              subtest "print $fn", sub {
-                  # Print the file on the client.
-                  $client->succeed("lp $file");
-                  $client->waitUntilSucceeds("lpq | grep -q -E 'active.*root.*$fn'");
-                  # Ensure that a raw PCL file appeared in the server's queue
-                  # (showing that the right filters have been applied).  Of
-                  # course, since there is no actual USB printer attached, the
-                  # file will stay in the queue forever.
-                  $server->waitForFile("/var/spool/cups/d*-001");
-                  $server->waitUntilSucceeds("lpq -a | grep -q -E '$fn'");
-                  # Delete the job on the client.  It should disappear on the
-                  # server as well.
-                  $client->succeed("lprm");
-                  $client->waitUntilSucceeds("lpq -a | grep -q -E 'no entries'");
-                  Machine::retry sub {
-                    return 1 if $server->succeed("lpq -a") =~ /no entries/;
-                  };
-                  # The queue is empty already, so this should be safe.
-                  # Otherwise, pairs of "c*"-"d*-001" files might persist.
-                  $server->execute("rm /var/spool/cups/*");
-              };
-          }
-      }
-      testPrinting($serviceClient, $serviceServer);
-      testPrinting($socketActivatedClient, $socketActivatedServer);
+  testScript = ''
+    import os
+    import re
+    import sys
+
+    start_all()
+
+    with subtest("Make sure that cups is up on both sides"):
+        serviceServer.wait_for_unit("cups.service")
+        serviceClient.wait_for_unit("cups.service")
+
+    with subtest(
+        "Wait until cups is fully initialized and ensure-printers has "
+        "executed with 10s delay"
+    ):
+        serviceClient.sleep(20)
+        socketActivatedClient.wait_until_succeeds(
+            "systemctl status ensure-printers | grep -q -E 'code=exited, status=0/SUCCESS'"
+        )
+
+
+    def test_printing(client, server):
+        assert "scheduler is running" in client.succeed("lpstat -r")
+
+        with subtest("UNIX socket is used for connections"):
+            assert "/var/run/cups/cups.sock" in client.succeed("lpstat -H")
+        with subtest("HTTP server is available too"):
+            client.succeed("curl --fail http://localhost:631/")
+            client.succeed(f"curl --fail http://{server.name}:631/")
+            server.fail(f"curl --fail --connect-timeout 2 http://{client.name}:631/")
+
+        with subtest("LP status checks"):
+            assert "DeskjetRemote accepting requests" in client.succeed("lpstat -a")
+            assert "DeskjetLocal accepting requests" in client.succeed(
+                f"lpstat -h {server.name}:631 -a"
+            )
+            client.succeed("cupsdisable DeskjetRemote")
+            out = client.succeed("lpq")
+            print(out)
+            assert re.search(
+                "DeskjetRemote is not ready.*no entries",
+                client.succeed("lpq"),
+                flags=re.DOTALL,
+            )
+            client.succeed("cupsenable DeskjetRemote")
+            assert re.match(
+                "DeskjetRemote is ready.*no entries", client.succeed("lpq"), flags=re.DOTALL
+            )
+
+        # Test printing various file types.
+        for file in [
+            "${pkgs.groff.doc}/share/doc/*/examples/mom/penguin.pdf",
+            "${pkgs.groff.doc}/share/doc/*/meref.ps",
+            "${pkgs.cups.out}/share/doc/cups/images/cups.png",
+            "${pkgs.pcre.doc}/share/doc/pcre/pcre.txt",
+        ]:
+            file_name = os.path.basename(file)
+            with subtest(f"print {file_name}"):
+                # Print the file on the client.
+                print(client.succeed("lpq"))
+                client.succeed(f"lp {file}")
+                client.wait_until_succeeds(
+                    f"lpq; lpq | grep -q -E 'active.*root.*{file_name}'"
+                )
+
+                # Ensure that a raw PCL file appeared in the server's queue
+                # (showing that the right filters have been applied).  Of
+                # course, since there is no actual USB printer attached, the
+                # file will stay in the queue forever.
+                server.wait_for_file("/var/spool/cups/d*-001")
+                server.wait_until_succeeds(f"lpq -a | grep -q -E '{file_name}'")
+
+                # Delete the job on the client.  It should disappear on the
+                # server as well.
+                client.succeed("lprm")
+                client.wait_until_succeeds("lpq -a | grep -q -E 'no entries'")
+
+                retry(lambda _: "no entries" in server.succeed("lpq -a"))
+
+                # The queue is empty already, so this should be safe.
+                # Otherwise, pairs of "c*"-"d*-001" files might persist.
+                server.execute("rm /var/spool/cups/*")
+
+
+    test_printing(serviceClient, serviceServer)
+    test_printing(socketActivatedClient, socketActivatedServer)
   '';
 })
diff --git a/nixos/tests/proxy.nix b/nixos/tests/proxy.nix
index 3859d429c21..6a14a9af59a 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/restic.nix b/nixos/tests/restic.nix
new file mode 100644
index 00000000000..67bb7f1933d
--- /dev/null
+++ b/nixos/tests/restic.nix
@@ -0,0 +1,63 @@
+import ./make-test-python.nix (
+  { pkgs, ... }:
+
+    let
+      password = "some_password";
+      repository = "/tmp/restic-backup";
+      passwordFile = pkgs.writeText "password" "correcthorsebatterystaple";
+    in
+      {
+        name = "restic";
+
+        meta = with pkgs.stdenv.lib.maintainers; {
+          maintainers = [ bbigras ];
+        };
+
+        nodes = {
+          server =
+            { ... }:
+              {
+                services.restic.backups = {
+                  remotebackup = {
+                    inherit repository;
+                    passwordFile = "${passwordFile}";
+                    initialize = true;
+                    paths = [ "/opt" ];
+                    pruneOpts = [
+                      "--keep-daily 2"
+                      "--keep-weekly 1"
+                      "--keep-monthly 1"
+                      "--keep-yearly 99"
+                    ];
+                  };
+                };
+              };
+        };
+
+        testScript = ''
+          server.start()
+          server.wait_for_unit("dbus.socket")
+          server.fail(
+              "${pkgs.restic}/bin/restic -r ${repository} -p ${passwordFile} snapshots"
+          )
+          server.succeed(
+              "mkdir -p /opt",
+              "touch /opt/some_file",
+              "timedatectl set-time '2016-12-13 13:45'",
+              "systemctl start restic-backups-remotebackup.service",
+              '${pkgs.restic}/bin/restic -r ${repository} -p ${passwordFile} snapshots -c | grep -e "^1 snapshot"',
+              "timedatectl set-time '2017-12-13 13:45'",
+              "systemctl start restic-backups-remotebackup.service",
+              "timedatectl set-time '2018-12-13 13:45'",
+              "systemctl start restic-backups-remotebackup.service",
+              "timedatectl set-time '2018-12-14 13:45'",
+              "systemctl start restic-backups-remotebackup.service",
+              "timedatectl set-time '2018-12-15 13:45'",
+              "systemctl start restic-backups-remotebackup.service",
+              "timedatectl set-time '2018-12-16 13:45'",
+              "systemctl start restic-backups-remotebackup.service",
+              '${pkgs.restic}/bin/restic -r ${repository} -p ${passwordFile} snapshots -c | grep -e "^4 snapshot"',
+          )
+        '';
+      }
+)
diff --git a/nixos/tests/riak.nix b/nixos/tests/riak.nix
index 68a9b7315b3..6915779e7e9 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 c746d46dc55..ae141fe116d 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 2108e851bc5..23e1a960fb3 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 00000000000..5bc824531e8
--- /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 4b71b4d6759..8028145939b 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 00000000000..73ef8b72861
--- /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 32637d2c1ef..f03dc1cc413 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/xandikos.nix b/nixos/tests/xandikos.nix
new file mode 100644
index 00000000000..0fded20ff1a
--- /dev/null
+++ b/nixos/tests/xandikos.nix
@@ -0,0 +1,70 @@
+import ./make-test-python.nix (
+  { pkgs, lib, ... }:
+
+    {
+      name = "xandikos";
+
+      meta.maintainers = [ lib.maintainers."0x4A6F" ];
+
+      nodes = {
+        xandikos_client = {};
+        xandikos_default = {
+          networking.firewall.allowedTCPPorts = [ 8080 ];
+          services.xandikos.enable = true;
+        };
+        xandikos_proxy = {
+          networking.firewall.allowedTCPPorts = [ 80 8080 ];
+          services.xandikos.enable = true;
+          services.xandikos.address = "localhost";
+          services.xandikos.port = 8080;
+          services.xandikos.routePrefix = "/xandikos/";
+          services.xandikos.extraOptions = [
+            "--defaults"
+          ];
+          services.nginx = {
+            enable = true;
+            recommendedProxySettings = true;
+            virtualHosts."xandikos" = {
+              serverName = "xandikos.local";
+              basicAuth.xandikos = "snakeOilPassword";
+              locations."/xandikos/" = {
+                proxyPass = "http://localhost:8080/";
+              };
+            };
+          };
+        };
+      };
+
+      testScript = ''
+        start_all()
+
+        with subtest("Xandikos default"):
+            xandikos_default.wait_for_unit("multi-user.target")
+            xandikos_default.wait_for_unit("xandikos.service")
+            xandikos_default.wait_for_open_port(8080)
+            xandikos_default.succeed("curl --fail http://localhost:8080/")
+            xandikos_default.succeed(
+                "curl -s --fail --location http://localhost:8080/ | grep -qi Xandikos"
+            )
+            xandikos_client.wait_for_unit("network.target")
+            xandikos_client.fail("curl --fail http://xandikos_default:8080/")
+
+        with subtest("Xandikos proxy"):
+            xandikos_proxy.wait_for_unit("multi-user.target")
+            xandikos_proxy.wait_for_unit("xandikos.service")
+            xandikos_proxy.wait_for_open_port(8080)
+            xandikos_proxy.succeed("curl --fail http://localhost:8080/")
+            xandikos_proxy.succeed(
+                "curl -s --fail --location http://localhost:8080/ | grep -qi Xandikos"
+            )
+            xandikos_client.wait_for_unit("network.target")
+            xandikos_client.fail("curl --fail http://xandikos_proxy:8080/")
+            xandikos_client.succeed(
+                "curl -s --fail -u xandikos:snakeOilPassword -H 'Host: xandikos.local' http://xandikos_proxy/xandikos/ | grep -qi Xandikos"
+            )
+            xandikos_client.succeed(
+                "curl -s --fail -u xandikos:snakeOilPassword -H 'Host: xandikos.local' http://xandikos_proxy/xandikos/user/ | grep -qi Xandikos"
+            )
+      '';
+    }
+)
diff --git a/nixos/tests/xautolock.nix b/nixos/tests/xautolock.nix
index 10e92b40e95..4a8d3f4cebf 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 3ea96b38363..99065669661 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 ef711f8dcf6..56baae8b9d3 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 1aceeffb955..6d7f2b9249f 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 3a7dea07d53..b77bbbbb3c4 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 9108004d4df..b374ef29680 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 = {
diff --git a/nixos/tests/zfs.nix b/nixos/tests/zfs.nix
index 8f844aca416..7ba60ee9806 100644
--- a/nixos/tests/zfs.nix
+++ b/nixos/tests/zfs.nix
@@ -3,12 +3,10 @@
   pkgs ? import ../.. { inherit system config; }
 }:
 
-with import ../lib/testing.nix { inherit system pkgs; };
+with import ../lib/testing-python.nix { inherit system pkgs; };
 
 let
 
-  makeTest = import ./make-test-python.nix;
-
   makeZfsTest = name:
     { kernelPackage ? pkgs.linuxPackages_latest
     , enableUnstable ? false
@@ -20,41 +18,33 @@ let
         maintainers = [ adisbladis ];
       };
 
-      machine = { pkgs, ... }:
-        {
-          virtualisation.emptyDiskImages = [ 4096 ];
-          networking.hostId = "deadbeef";
-          boot.kernelPackages = kernelPackage;
-          boot.supportedFilesystems = [ "zfs" ];
-          boot.zfs.enableUnstable = enableUnstable;
+      machine = { pkgs, ... }: {
+        virtualisation.emptyDiskImages = [ 4096 ];
+        networking.hostId = "deadbeef";
+        boot.kernelPackages = kernelPackage;
+        boot.supportedFilesystems = [ "zfs" ];
+        boot.zfs.enableUnstable = enableUnstable;
 
-          environment.systemPackages = with pkgs; [
-            parted
-          ];
-        };
+        environment.systemPackages = [ pkgs.parted ];
+      };
 
       testScript = ''
-        machine.succeed("modprobe zfs")
-        machine.succeed("zpool status")
-
-        machine.succeed("ls /dev")
-
         machine.succeed(
-          "mkdir /tmp/mnt",
-
-          "udevadm settle",
-          "parted --script /dev/vdb mklabel msdos",
-          "parted --script /dev/vdb -- mkpart primary 1024M -1s",
-          "udevadm settle",
-
-          "zpool create rpool /dev/vdb1",
-          "zfs create -o mountpoint=legacy rpool/root",
-          "mount -t zfs rpool/root /tmp/mnt",
-          "udevadm settle",
-
-          "umount /tmp/mnt",
-          "zpool destroy rpool",
-          "udevadm settle"
+            "modprobe zfs",
+            "zpool status",
+            "ls /dev",
+            "mkdir /tmp/mnt",
+            "udevadm settle",
+            "parted --script /dev/vdb mklabel msdos",
+            "parted --script /dev/vdb -- mkpart primary 1024M -1s",
+            "udevadm settle",
+            "zpool create rpool /dev/vdb1",
+            "zfs create -o mountpoint=legacy rpool/root",
+            "mount -t zfs rpool/root /tmp/mnt",
+            "udevadm settle",
+            "umount /tmp/mnt",
+            "zpool destroy rpool",
+            "udevadm settle",
         )
       '' + extraTest;
 
@@ -69,18 +59,17 @@ in {
     enableUnstable = true;
     extraTest = ''
       machine.succeed(
-        "echo password | zpool create -o altroot=\"/tmp/mnt\" -O encryption=aes-256-gcm -O keyformat=passphrase rpool /dev/vdb1",
-        "zfs create -o mountpoint=legacy rpool/root",
-        "mount -t zfs rpool/root /tmp/mnt",
-        "udevadm settle",
-
-        "umount /tmp/mnt",
-        "zpool destroy rpool",
-        "udevadm settle"
+          'echo password | zpool create -o altroot="/tmp/mnt" '
+          + "-O encryption=aes-256-gcm -O keyformat=passphrase rpool /dev/vdb1",
+          "zfs create -o mountpoint=legacy rpool/root",
+          "mount -t zfs rpool/root /tmp/mnt",
+          "udevadm settle",
+          "umount /tmp/mnt",
+          "zpool destroy rpool",
+          "udevadm settle",
       )
     '';
   };
 
   installer = (import ./installer.nix { }).zfsroot;
-
 }