summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/configuration/declarative-packages.xml6
-rw-r--r--nixos/doc/manual/development/option-types.xml36
-rw-r--r--nixos/doc/manual/development/replace-modules.xml4
-rw-r--r--nixos/doc/manual/man-nixos-install.xml2
-rw-r--r--nixos/doc/manual/man-pages.xml2
-rw-r--r--nixos/doc/manual/release-notes/rl-2003.xml88
-rw-r--r--nixos/lib/test-driver/test-driver.py25
-rw-r--r--nixos/lib/testing-python.nix2
-rwxr-xr-xnixos/maintainers/scripts/azure/create-azure.sh4
-rw-r--r--nixos/modules/hardware/opengl.nix6
-rw-r--r--nixos/modules/hardware/usb-wwan.nix13
-rw-r--r--nixos/modules/installer/tools/nixos-rebuild.sh16
-rw-r--r--nixos/modules/installer/tools/tools.nix10
-rw-r--r--nixos/modules/misc/documentation.nix7
-rw-r--r--nixos/modules/misc/version.nix16
-rw-r--r--nixos/modules/module-list.nix8
-rw-r--r--nixos/modules/programs/gnupg.nix2
-rw-r--r--nixos/modules/programs/liboping.nix22
-rw-r--r--nixos/modules/programs/screen.nix1
-rw-r--r--nixos/modules/programs/sway.nix19
-rw-r--r--nixos/modules/programs/traceroute.nix26
-rw-r--r--nixos/modules/programs/way-cooler.nix78
-rw-r--r--nixos/modules/rename.nix7
-rw-r--r--nixos/modules/security/pam.nix3
-rw-r--r--nixos/modules/services/amqp/rabbitmq.nix5
-rw-r--r--nixos/modules/services/cluster/kubernetes/pki.nix10
-rw-r--r--nixos/modules/services/continuous-integration/buildbot/master.nix2
-rw-r--r--nixos/modules/services/continuous-integration/buildbot/worker.nix2
-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/continuous-integration/jenkins/slave.nix2
-rw-r--r--nixos/modules/services/databases/mysql.nix2
-rw-r--r--nixos/modules/services/databases/openldap.nix2
-rw-r--r--nixos/modules/services/desktops/gnome3/at-spi2-core.nix3
-rw-r--r--nixos/modules/services/hardware/actkbd.nix2
-rw-r--r--nixos/modules/services/hardware/usbmuxd.nix2
-rw-r--r--nixos/modules/services/mail/postfix.nix5
-rw-r--r--nixos/modules/services/mail/roundcube.nix79
-rw-r--r--nixos/modules/services/mail/spamassassin.nix6
-rw-r--r--nixos/modules/services/misc/gitea.nix2
-rw-r--r--nixos/modules/services/misc/home-assistant.nix9
-rw-r--r--nixos/modules/services/misc/matrix-synapse.nix3
-rw-r--r--nixos/modules/services/misc/paperless.nix14
-rw-r--r--nixos/modules/services/monitoring/prometheus/alertmanager.nix21
-rw-r--r--nixos/modules/services/networking/corerad.nix46
-rw-r--r--nixos/modules/services/networking/gnunet.nix8
-rw-r--r--nixos/modules/services/networking/knot.nix2
-rw-r--r--nixos/modules/services/networking/kresd.nix34
-rw-r--r--nixos/modules/services/networking/matterbridge.nix2
-rw-r--r--nixos/modules/services/networking/mxisd.nix1
-rw-r--r--nixos/modules/services/networking/nat.nix2
-rw-r--r--nixos/modules/services/networking/ndppd.nix20
-rw-r--r--nixos/modules/services/networking/syncthing.nix18
-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/certmgr.nix4
-rw-r--r--nixos/modules/services/security/torify.nix3
-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/ihatemoney/default.nix141
-rw-r--r--nixos/modules/services/web-apps/nextcloud.nix10
-rw-r--r--nixos/modules/services/web-servers/apache-httpd/default.nix3
-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/web-servers/uwsgi.nix19
-rw-r--r--nixos/modules/services/x11/desktop-managers/gnome3.nix12
-rw-r--r--nixos/modules/services/x11/display-managers/gdm.nix6
-rw-r--r--nixos/modules/services/x11/hardware/multitouch.nix94
-rw-r--r--nixos/modules/services/x11/unclutter.nix7
-rw-r--r--nixos/modules/system/boot/networkd.nix40
-rw-r--r--nixos/modules/system/boot/systemd-lib.nix8
-rw-r--r--nixos/modules/tasks/powertop.nix1
-rw-r--r--nixos/modules/virtualisation/amazon-init.nix11
-rw-r--r--nixos/release.nix3
-rw-r--r--nixos/tests/all-tests.nix5
-rw-r--r--nixos/tests/bittorrent.nix155
-rw-r--r--nixos/tests/buildkite-agent.nix36
-rw-r--r--nixos/tests/ceph-multi-node.nix56
-rw-r--r--nixos/tests/ceph-single-node.nix23
-rw-r--r--nixos/tests/certmgr.nix12
-rw-r--r--nixos/tests/common/user-account.nix1
-rw-r--r--nixos/tests/corerad.nix70
-rw-r--r--nixos/tests/dokuwiki.nix29
-rw-r--r--nixos/tests/elk.nix111
-rw-r--r--nixos/tests/gnome3-xorg.nix78
-rw-r--r--nixos/tests/home-assistant.nix71
-rw-r--r--nixos/tests/ihatemoney.nix52
-rw-r--r--nixos/tests/initdb.nix26
-rw-r--r--nixos/tests/kafka.nix44
-rw-r--r--nixos/tests/postgresql.nix45
-rw-r--r--nixos/tests/solr.nix101
-rw-r--r--nixos/tests/xandikos.nix70
95 files changed, 1972 insertions, 711 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/development/option-types.xml b/nixos/doc/manual/development/option-types.xml
index 1ec7e3efad7..957349ad181 100644
--- a/nixos/doc/manual/development/option-types.xml
+++ b/nixos/doc/manual/development/option-types.xml
@@ -257,9 +257,9 @@
     <listitem>
      <para>
       A set of sub options <replaceable>o</replaceable>.
-      <replaceable>o</replaceable> can be an attribute set or a function
-      returning an attribute set. Submodules are used in composed types to
-      create modular options. This is equivalent to
+      <replaceable>o</replaceable> can be an attribute set, a function
+      returning an attribute set, or a path to a file containing such a value. Submodules are used in
+      composed types to create modular options. This is equivalent to
       <literal>types.submoduleWith { modules = toList o; shorthandOnlyDefinesConfig = true; }</literal>.
       Submodules are detailed in
       <xref
@@ -352,6 +352,36 @@
       An attribute set of where all the values are of
       <replaceable>t</replaceable> type. Multiple definitions result in the
       joined attribute set.
+      <note><para>
+       This type is <emphasis>strict</emphasis> in its values, which in turn
+       means attributes cannot depend on other attributes. See <varname>
+       types.lazyAttrsOf</varname> for a lazy version.
+      </para></note>
+     </para>
+    </listitem>
+   </varlistentry>
+   <varlistentry>
+    <term>
+     <varname>types.lazyAttrsOf</varname> <replaceable>t</replaceable>
+    </term>
+    <listitem>
+     <para>
+      An attribute set of where all the values are of
+      <replaceable>t</replaceable> type. Multiple definitions result in the
+      joined attribute set. This is the lazy version of <varname>types.attrsOf
+      </varname>, allowing attributes to depend on each other.
+      <warning><para>
+       This version does not fully support conditional definitions! With an
+       option <varname>foo</varname> of this type and a definition
+       <literal>foo.attr = lib.mkIf false 10</literal>, evaluating
+       <literal>foo ? attr</literal> will return <literal>true</literal>
+       even though it should be false. Accessing the value will then throw
+       an error. For types <replaceable>t</replaceable> that have an
+       <literal>emptyValue</literal> defined, that value will be returned
+       instead of throwing an error. So if the type of <literal>foo.attr</literal>
+       was <literal>lazyAttrsOf (nullOr int)</literal>, <literal>null</literal>
+       would be returned instead for the same <literal>mkIf false</literal> definition.
+      </para></warning>
      </para>
     </listitem>
    </varlistentry>
diff --git a/nixos/doc/manual/development/replace-modules.xml b/nixos/doc/manual/development/replace-modules.xml
index 7b103c36d90..b4a466e2294 100644
--- a/nixos/doc/manual/development/replace-modules.xml
+++ b/nixos/doc/manual/development/replace-modules.xml
@@ -6,8 +6,8 @@
  <title>Replace Modules</title>
 
  <para>
-  Modules that are imported can also be disabled. The option declarations and
-  config implementation of a disabled module will be ignored, allowing another
+  Modules that are imported can also be disabled. The option declarations,
+  config implementation and the imports of a disabled module will be ignored, allowing another
   to take it's place. This can be used to import a set of modules from another
   channel while keeping the rest of the system on a stable release.
  </para>
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-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 13f4c62c018..1eef4f08c4f 100644
--- a/nixos/doc/manual/release-notes/rl-2003.xml
+++ b/nixos/doc/manual/release-notes/rl-2003.xml
@@ -170,6 +170,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>
@@ -357,6 +363,88 @@ services.xserver.displayManager.defaultSession = "xfce+icewm";
       <link linkend="opt-services.httpd.virtualHosts">services.httpd.virtualHosts.&lt;name&gt;.useACMEHost</link>.
     </para>
    </listitem>
+   <listitem>
+    <para>
+     For NixOS configuration options, the <literal>loaOf</literal> type has
+     been deprecated and will be removed in a future release. In nixpkgs,
+     options of this type will be changed to <literal>attrsOf</literal>
+     instead. If you were using one of these in your configuration, you will
+     see a warning suggesting what changes will be required.
+    </para>
+    <para>
+     For example, <link linkend="opt-users.users">users.users</link> is a
+     <literal>loaOf</literal> option that is commonly used as follows:
+     <programlisting>
+users.users =
+  [ { name = "me";
+      description = "My personal user.";
+      isNormalUser = true;
+    }
+  ];
+     </programlisting>
+     This should be rewritten by removing the list and using the
+     value of <literal>name</literal> as the name of the attribute set:
+     <programlisting>
+users.users.me =
+  { description = "My personal user.";
+    isNormalUser = true;
+  };
+     </programlisting>
+    </para>
+    <para>
+     For more information on this change have look at these links:
+     <link xlink:href="https://github.com/NixOS/nixpkgs/issues/1800">issue #1800</link>,
+     <link xlink:href="https://github.com/NixOS/nixpkgs/pull/63103">PR #63103</link>.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     For NixOS modules, the types <literal>types.submodule</literal> and <literal>types.submoduleWith</literal> now support
+     paths as allowed values, similar to how <literal>imports</literal> supports paths.
+     Because of this, if you have a module that defines an option of type
+     <literal>either (submodule ...) path</literal>, it will break since a path
+     is now treated as the first type instead of the second. To fix this, change
+     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>
   </itemizedlist>
  </section>
 
diff --git a/nixos/lib/test-driver/test-driver.py b/nixos/lib/test-driver/test-driver.py
index 7e575189209..cf204a2619f 100644
--- a/nixos/lib/test-driver/test-driver.py
+++ b/nixos/lib/test-driver/test-driver.py
@@ -84,7 +84,7 @@ CHAR_TO_KEY = {
 
 # Forward references
 nr_tests: int
-nr_succeeded: int
+failed_tests: list
 log: "Logger"
 machines: "List[Machine]"
 
@@ -221,7 +221,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
@@ -576,7 +576,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 +704,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 +842,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 +888,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/maintainers/scripts/azure/create-azure.sh b/nixos/maintainers/scripts/azure/create-azure.sh
index 2b22cb53661..0558f8dfffc 100755
--- a/nixos/maintainers/scripts/azure/create-azure.sh
+++ b/nixos/maintainers/scripts/azure/create-azure.sh
@@ -1,6 +1,6 @@
-#! /bin/sh -e
+#! /bin/sh -eu
 
-export NIX_PATH=nixpkgs=../../../..
+export NIX_PATH=nixpkgs=$(dirname $(readlink -f $0))/../../../..
 export NIXOS_CONFIG=$(dirname $(readlink -f $0))/../../../modules/virtualisation/azure-image.nix
 export TIMESTAMP=$(date +%Y%m%d%H%M)
 
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/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/tools/nixos-rebuild.sh b/nixos/modules/installer/tools/nixos-rebuild.sh
index c53dc1000c4..61b4af11027 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
@@ -92,7 +92,7 @@ while [ "$#" -gt 0 ]; do
         ;;
       --use-remote-sudo)
         # note the trailing space
-        maybeSudo="sudo "
+        maybeSudo=(sudo --)
         shift 1
         ;;
       *)
@@ -102,6 +102,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 +120,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/installer/tools/tools.nix b/nixos/modules/installer/tools/tools.nix
index e4db39b5c81..5df9c23e6b6 100644
--- a/nixos/modules/installer/tools/tools.nix
+++ b/nixos/modules/installer/tools/tools.nix
@@ -159,10 +159,12 @@ in
         #   extraGroups = [ "wheel" ]; # Enable ‘sudo’ for the user.
         # };
 
-        # This value determines the NixOS release with which your system is to be
-        # compatible, in order to avoid breaking some software such as database
-        # servers. You should change this only after NixOS release notes say you
-        # should.
+        # This value determines the NixOS release from which the default
+        # settings for stateful data, like file locations and database versions
+        # on your system were taken. It‘s perfectly fine and recommended to leave
+        # this value at the release version of the first install of this system.
+        # Before changing this value read the documentation for this option
+        # (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
         system.stateVersion = "${config.system.nixos.release}"; # Did you read the comment?
 
       }
diff --git a/nixos/modules/misc/documentation.nix b/nixos/modules/misc/documentation.nix
index 820553270e3..d09afadd609 100644
--- a/nixos/modules/misc/documentation.nix
+++ b/nixos/modules/misc/documentation.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, baseModules, extraModules, modules, ... }:
+{ config, lib, pkgs, baseModules, extraModules, modules, modulesPath, ... }:
 
 with lib;
 
@@ -22,7 +22,10 @@ let
         scrubbedEval = evalModules {
           modules = [ { nixpkgs.localSystem = config.nixpkgs.localSystem; } ] ++ manualModules;
           args = (config._module.args) // { modules = [ ]; };
-          specialArgs = { pkgs = scrubDerivations "pkgs" pkgs; };
+          specialArgs = {
+            pkgs = scrubDerivations "pkgs" pkgs;
+            inherit modulesPath;
+          };
         };
         scrubDerivations = namePrefix: pkgSet: mapAttrs
           (name: value:
diff --git a/nixos/modules/misc/version.nix b/nixos/modules/misc/version.nix
index 0540b493003..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
 
@@ -61,11 +62,18 @@ in
         configuration defaults in a way incompatible with stateful
         data. For instance, if the default version of PostgreSQL
         changes, the new version will probably be unable to read your
-        existing databases. To prevent such breakage, you can set the
+        existing databases. To prevent such breakage, you should set the
         value of this option to the NixOS release with which you want
-        to be compatible. The effect is that NixOS will option
+        to be compatible. The effect is that NixOS will use
         defaults corresponding to the specified release (such as using
         an older version of PostgreSQL).
+        It‘s perfectly fine and recommended to leave this value at the
+        release version of the first install of this system.
+        Changing this option will not upgrade your system. In fact it
+        is meant to stay constant exactly when you upgrade your system.
+        You should only bump this option, if you are sure that you can
+        or have migrated all state on your system which is affected
+        by this option.
       '';
     };
 
@@ -84,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 1abf87dfcc6..176b62f6cb5 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -127,6 +127,7 @@
   ./programs/java.nix
   ./programs/kbdlight.nix
   ./programs/less.nix
+  ./programs/liboping.nix
   ./programs/light.nix
   ./programs/mosh.nix
   ./programs/mininet.nix
@@ -152,13 +153,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
@@ -577,6 +578,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
@@ -735,6 +737,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,10 +805,12 @@
   ./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
   ./services/web-apps/icingaweb2/module-monitoring.nix
+  ./services/web-apps/ihatemoney
   ./services/web-apps/limesurvey.nix
   ./services/web-apps/mattermost.nix
   ./services/web-apps/mediawiki.nix
@@ -868,7 +873,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/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/screen.nix b/nixos/modules/programs/screen.nix
index 4fd800dbae7..728a0eb8cea 100644
--- a/nixos/modules/programs/screen.nix
+++ b/nixos/modules/programs/screen.nix
@@ -27,6 +27,7 @@ in
     environment.etc.screenrc.text = cfg.screenrc;
 
     environment.systemPackages = [ pkgs.screen ];
+    security.pam.services.screen = {};
   };
 
 }
diff --git a/nixos/modules/programs/sway.nix b/nixos/modules/programs/sway.nix
index e2a4018e902..7e646f8737d 100644
--- a/nixos/modules/programs/sway.nix
+++ b/nixos/modules/programs/sway.nix
@@ -28,6 +28,7 @@ let
 
   swayPackage = pkgs.sway.override {
     extraSessionCommands = cfg.extraSessionCommands;
+    extraOptions = cfg.extraOptions;
     withBaseWrapper = cfg.wrapperFeatures.base;
     withGtkWrapper = cfg.wrapperFeatures.gtk;
   };
@@ -67,11 +68,27 @@ in {
       '';
     };
 
+    extraOptions = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [
+        "--verbose"
+        "--debug"
+        "--unsupported-gpu"
+        "--my-next-gpu-wont-be-nvidia"
+      ];
+      description = ''
+        Command line arguments passed to launch Sway. Please DO NOT report
+        issues if you use an unsupported GPU (proprietary drivers).
+      '';
+    };
+
     extraPackages = mkOption {
       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/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/rename.nix b/nixos/modules/rename.nix
index 7109ab5a109..26de8a18d92 100644
--- a/nixos/modules/rename.nix
+++ b/nixos/modules/rename.nix
@@ -27,6 +27,13 @@ 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.
+    '')
 
     # Do NOT add any option renames here, see top of the file
   ];
diff --git a/nixos/modules/security/pam.nix b/nixos/modules/security/pam.nix
index ee37c18d980..bfc2a881387 100644
--- a/nixos/modules/security/pam.nix
+++ b/nixos/modules/security/pam.nix
@@ -776,11 +776,8 @@ in
           '';
 
         # Most of these should be moved to specific modules.
-        cups = {};
-        ftp = {};
         i3lock = {};
         i3lock-color = {};
-        screen = {};
         vlock = {};
         xlock = {};
         xscreensaver = {};
diff --git a/nixos/modules/services/amqp/rabbitmq.nix b/nixos/modules/services/amqp/rabbitmq.nix
index 697732426cc..35fb49f709a 100644
--- a/nixos/modules/services/amqp/rabbitmq.nix
+++ b/nixos/modules/services/amqp/rabbitmq.nix
@@ -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/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/buildbot/master.nix b/nixos/modules/services/continuous-integration/buildbot/master.nix
index 326d2cbd82c..e3da3092d45 100644
--- a/nixos/modules/services/continuous-integration/buildbot/master.nix
+++ b/nixos/modules/services/continuous-integration/buildbot/master.nix
@@ -222,7 +222,7 @@ in {
   };
 
   config = mkIf cfg.enable {
-    users.groups = optional (cfg.group == "buildbot") {
+    users.groups = optionalAttrs (cfg.group == "buildbot") {
       buildbot = { };
     };
 
diff --git a/nixos/modules/services/continuous-integration/buildbot/worker.nix b/nixos/modules/services/continuous-integration/buildbot/worker.nix
index 7613692f0a3..52f24b8cee3 100644
--- a/nixos/modules/services/continuous-integration/buildbot/worker.nix
+++ b/nixos/modules/services/continuous-integration/buildbot/worker.nix
@@ -136,7 +136,7 @@ in {
   config = mkIf cfg.enable {
     services.buildbot-worker.workerPassFile = mkDefault (pkgs.writeText "buildbot-worker-password" cfg.workerPass);
 
-    users.groups = optional (cfg.group == "bbworker") {
+    users.groups = optionalAttrs (cfg.group == "bbworker") {
       bbworker = { };
     };
 
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/continuous-integration/jenkins/slave.nix b/nixos/modules/services/continuous-integration/jenkins/slave.nix
index 26368cb94e4..3c0e6f78e74 100644
--- a/nixos/modules/services/continuous-integration/jenkins/slave.nix
+++ b/nixos/modules/services/continuous-integration/jenkins/slave.nix
@@ -50,7 +50,7 @@ in {
   };
 
   config = mkIf (cfg.enable && !masterCfg.enable) {
-    users.groups = optional (cfg.group == "jenkins") {
+    users.groups = optionalAttrs (cfg.group == "jenkins") {
       jenkins.gid = config.ids.gids.jenkins;
     };
 
diff --git a/nixos/modules/services/databases/mysql.nix b/nixos/modules/services/databases/mysql.nix
index 6af32700fc7..8d520b82fb5 100644
--- a/nixos/modules/services/databases/mysql.nix
+++ b/nixos/modules/services/databases/mysql.nix
@@ -320,6 +320,8 @@ in
           Type = if hasNotify then "notify" else "simple";
           RuntimeDirectory = "mysqld";
           RuntimeDirectoryMode = "0755";
+          Restart = "on-abort";
+          RestartSec = "5s";
           # The last two environment variables are used for starting Galera clusters
           ExecStart = "${mysql}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} $_WSREP_NEW_CLUSTER $_WSREP_START_POSITION";
           ExecStartPost =
diff --git a/nixos/modules/services/databases/openldap.nix b/nixos/modules/services/databases/openldap.nix
index 5bf57a1bf9c..809f61cfa81 100644
--- a/nixos/modules/services/databases/openldap.nix
+++ b/nixos/modules/services/databases/openldap.nix
@@ -259,6 +259,8 @@ in
           ${openldap.out}/bin/slapadd ${configOpts} -l ${dataFile}
         ''}
         chown -R "${cfg.user}:${cfg.group}" "${cfg.dataDir}"
+
+        ${openldap}/bin/slaptest ${configOpts}
       '';
       serviceConfig.ExecStart =
         "${openldap.out}/libexec/slapd -d '${cfg.logLevel}' " +
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/hardware/actkbd.nix b/nixos/modules/services/hardware/actkbd.nix
index 4168140b287..daa407ca1f0 100644
--- a/nixos/modules/services/hardware/actkbd.nix
+++ b/nixos/modules/services/hardware/actkbd.nix
@@ -83,7 +83,7 @@ in
 
           See <command>actkbd</command> <filename>README</filename> for documentation.
 
-          The example shows a piece of what <option>sound.enableMediaKeys</option> does when enabled.
+          The example shows a piece of what <option>sound.mediaKeys.enable</option> does when enabled.
         '';
       };
 
diff --git a/nixos/modules/services/hardware/usbmuxd.nix b/nixos/modules/services/hardware/usbmuxd.nix
index 50b931dcb48..11a4b0a858f 100644
--- a/nixos/modules/services/hardware/usbmuxd.nix
+++ b/nixos/modules/services/hardware/usbmuxd.nix
@@ -51,7 +51,7 @@ in
       };
     };
 
-    users.groups = optional (cfg.group == defaultUserGroup) {
+    users.groups = optionalAttrs (cfg.group == defaultUserGroup) {
       ${cfg.group} = { };
     };
 
diff --git a/nixos/modules/services/mail/postfix.nix b/nixos/modules/services/mail/postfix.nix
index d7378821440..19e11b31d9c 100644
--- a/nixos/modules/services/mail/postfix.nix
+++ b/nixos/modules/services/mail/postfix.nix
@@ -612,10 +612,7 @@ in
     {
 
       environment = {
-        etc = singleton
-          { source = "/var/lib/postfix/conf";
-            target = "postfix";
-          };
+        etc.postfix.source = "/var/lib/postfix/conf";
 
         # This makes it comfortable to run 'postqueue/postdrop' for example.
         systemPackages = [ pkgs.postfix ];
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 107280f7c14..75442c7cdb5 100644
--- a/nixos/modules/services/mail/spamassassin.nix
+++ b/nixos/modules/services/mail/spamassassin.nix
@@ -5,7 +5,6 @@ with lib;
 let
   cfg = config.services.spamassassin;
   spamassassin-local-cf = pkgs.writeText "local.cf" cfg.config;
-  spamassassin-init-pre = pkgs.writeText "init.pre" cfg.initPreConf;
 
   spamdEnv = pkgs.buildEnv {
     name = "spamd-env";
@@ -65,8 +64,9 @@ in
       };
 
       initPreConf = mkOption {
-        type = types.str;
+        type = with types; either str path;
         description = "The SpamAssassin init.pre config.";
+        apply = val: if builtins.isPath val then val else pkgs.writeText "init.pre" val;
         default =
         ''
           #
@@ -124,7 +124,7 @@ in
     # Allow users to run 'spamc'.
 
     environment = {
-      etc = singleton { source = spamdEnv; target = "spamassassin"; };
+      etc.spamassassin.source = spamdEnv;
       systemPackages = [ pkgs.spamassassin ];
     };
 
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 74702c97f55..cc113ca2d0c 100644
--- a/nixos/modules/services/misc/home-assistant.nix
+++ b/nixos/modules/services/misc/home-assistant.nix
@@ -11,6 +11,9 @@ let
     (recursiveUpdate defaultConfig cfg.config) else cfg.config));
   configFile = pkgs.runCommand "configuration.yaml" { preferLocalBuild = true; } ''
     ${pkgs.remarshal}/bin/json2yaml -i ${configJSON} -o $out
+    # Hack to support secrets, that are encoded as custom yaml objects,
+    # https://www.home-assistant.io/docs/configuration/secrets/
+    sed -i -e "s/'\!secret \(.*\)'/\!secret \1/" $out
   '';
 
   lovelaceConfigJSON = pkgs.writeText "ui-lovelace.json"
@@ -98,6 +101,10 @@ in {
         {
           homeassistant = {
             name = "Home";
+            latitude = "!secret latitude";
+            longitude = "!secret longitude";
+            elevation = "!secret elevation";
+            unit_system = "metric";
             time_zone = "UTC";
           };
           frontend = { };
@@ -108,6 +115,8 @@ in {
       description = ''
         Your <filename>configuration.yaml</filename> as a Nix attribute set.
         Beware that setting this option will delete your previous <filename>configuration.yaml</filename>.
+        <link xlink:href="https://www.home-assistant.io/docs/configuration/secrets/">Secrets</link>
+        are encoded as strings as shown in the example.
       '';
     };
 
diff --git a/nixos/modules/services/misc/matrix-synapse.nix b/nixos/modules/services/misc/matrix-synapse.nix
index c0d44e6feb7..750f4a292fb 100644
--- a/nixos/modules/services/misc/matrix-synapse.nix
+++ b/nixos/modules/services/misc/matrix-synapse.nix
@@ -657,8 +657,7 @@ in {
   };
 
   config = mkIf cfg.enable {
-    users.users.matrix-synapse =
-      { name = "";
+    users.users.matrix-synapse = { 
         group = "matrix-synapse";
         home = cfg.dataDir;
         createHome = true;
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/prometheus/alertmanager.nix b/nixos/modules/services/monitoring/prometheus/alertmanager.nix
index 9af6b1d94f3..2e8433fbc88 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/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/gnunet.nix b/nixos/modules/services/networking/gnunet.nix
index 178a832c166..69d4ed04775 100644
--- a/nixos/modules/services/networking/gnunet.nix
+++ b/nixos/modules/services/networking/gnunet.nix
@@ -42,6 +42,7 @@ in
     services.gnunet = {
 
       enable = mkOption {
+        type = types.bool;
         default = false;
         description = ''
           Whether to run the GNUnet daemon.  GNUnet is GNU's anonymous
@@ -51,6 +52,7 @@ in
 
       fileSharing = {
         quota = mkOption {
+          type = types.int;
           default = 1024;
           description = ''
             Maximum file system usage (in MiB) for file sharing.
@@ -60,6 +62,7 @@ in
 
       udp = {
         port = mkOption {
+          type = types.port;
           default = 2086;  # assigned by IANA
           description = ''
             The UDP port for use by GNUnet.
@@ -69,6 +72,7 @@ in
 
       tcp = {
         port = mkOption {
+          type = types.port;
           default = 2086;  # assigned by IANA
           description = ''
             The TCP port for use by GNUnet.
@@ -78,6 +82,7 @@ in
 
       load = {
         maxNetDownBandwidth = mkOption {
+          type = types.int;
           default = 50000;
           description = ''
             Maximum bandwidth usage (in bits per second) for GNUnet
@@ -86,6 +91,7 @@ in
         };
 
         maxNetUpBandwidth = mkOption {
+          type = types.int;
           default = 50000;
           description = ''
             Maximum bandwidth usage (in bits per second) for GNUnet
@@ -94,6 +100,7 @@ in
         };
 
         hardNetUpBandwidth = mkOption {
+          type = types.int;
           default = 0;
           description = ''
             Hard bandwidth limit (in bits per second) when uploading
@@ -111,6 +118,7 @@ in
       };
 
       extraOptions = mkOption {
+        type = types.lines;
         default = "";
         description = ''
           Additional options that will be copied verbatim in `gnunet.conf'.
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..bb941e93e15 100644
--- a/nixos/modules/services/networking/kresd.nix
+++ b/nixos/modules/services/networking/kresd.nix
@@ -5,12 +5,15 @@ with lib;
 let
 
   cfg = config.services.kresd;
-  package = pkgs.knot-resolver;
+  configFile = pkgs.writeText "kresd.conf" ''
+    ${optionalString (cfg.listenDoH != []) "modules.load('http')"}
+    ${cfg.extraConfig};
+  '';
 
-  configFile = pkgs.writeText "kresd.conf" cfg.extraConfig;
-in
-
-{
+  package = pkgs.knot-resolver.override {
+    extraFeatures = cfg.listenDoH != [];
+  };
+in {
   meta.maintainers = [ maintainers.vcunat /* upstream developer */ ];
 
   imports = [
@@ -67,6 +70,15 @@ in
         For detailed syntax see ListenStream in man systemd.socket.
       '';
     };
+    listenDoH = mkOption {
+      type = with types; listOf str;
+      default = [];
+      example = [ "198.51.100.1:443" "[2001:db8::1]:443" "443" ];
+      description = ''
+        Addresses and ports on which kresd should provide DNS over HTTPS (see RFC 7858).
+        For detailed syntax see ListenStream in man systemd.socket.
+      '';
+    };
     # TODO: perhaps options for more common stuff like cache size or forwarding
   };
 
@@ -104,6 +116,18 @@ in
       };
     };
 
+    systemd.sockets.kresd-doh = mkIf (cfg.listenDoH != []) rec {
+      wantedBy = [ "sockets.target" ];
+      before = wantedBy;
+      partOf = [ "kresd.socket" ];
+      listenStreams = cfg.listenDoH;
+      socketConfig = {
+        FileDescriptorName = "doh";
+        FreeBind = true;
+        Service = "kresd.service";
+      };
+    };
+
     systemd.sockets.kresd-control = rec {
       wantedBy = [ "sockets.target" ];
       before = wantedBy;
diff --git a/nixos/modules/services/networking/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/mxisd.nix b/nixos/modules/services/networking/mxisd.nix
index b59371d241e..482d6ff456b 100644
--- a/nixos/modules/services/networking/mxisd.nix
+++ b/nixos/modules/services/networking/mxisd.nix
@@ -104,7 +104,6 @@ in {
 
     users.groups.mxisd =
       {
-        name = "";
         gid = config.ids.gids.mxisd;
       };
 
diff --git a/nixos/modules/services/networking/nat.nix b/nixos/modules/services/networking/nat.nix
index f1238bc6b16..9c658af30f7 100644
--- a/nixos/modules/services/networking/nat.nix
+++ b/nixos/modules/services/networking/nat.nix
@@ -68,7 +68,7 @@ let
           destinationPorts = if (m == null) then throw "bad ip:ports `${fwd.destination}'" else elemAt m 1;
         in ''
           # Allow connections to ${loopbackip}:${toString fwd.sourcePort} from the host itself
-          iptables -w -t nat -A OUTPUT \
+          iptables -w -t nat -A nixos-nat-out \
             -d ${loopbackip} -p ${fwd.proto} \
             --dport ${builtins.toString fwd.sourcePort} \
             -j DNAT --to-destination ${fwd.destination}
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/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/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/certmgr.nix b/nixos/modules/services/security/certmgr.nix
index e89078883eb..94c0ba14117 100644
--- a/nixos/modules/services/security/certmgr.nix
+++ b/nixos/modules/services/security/certmgr.nix
@@ -113,7 +113,7 @@ in
         otherCert = "/var/certmgr/specs/other-cert.json";
       }
       '';
-      type = with types; attrsOf (either (submodule {
+      type = with types; attrsOf (either path (submodule {
         options = {
           service = mkOption {
             type = nullOr str;
@@ -148,7 +148,7 @@ in
             description = "certmgr spec request object.";
           };
         };
-    }) path);
+    }));
       description = ''
         Certificate specs as described by:
         <link xlink:href="https://github.com/cloudflare/certmgr#certificate-specs" />
diff --git a/nixos/modules/services/security/torify.nix b/nixos/modules/services/security/torify.nix
index 08da726437e..39551190dd3 100644
--- a/nixos/modules/services/security/torify.nix
+++ b/nixos/modules/services/security/torify.nix
@@ -25,6 +25,7 @@ in
     services.tor.tsocks = {
 
       enable = mkOption {
+        type = types.bool;
         default = false;
         description = ''
           Whether to build tsocks wrapper script to relay application traffic via Tor.
@@ -40,6 +41,7 @@ in
       };
 
       server = mkOption {
+        type = types.str;
         default = "localhost:9050";
         example = "192.168.0.20";
         description = ''
@@ -48,6 +50,7 @@ in
       };
 
       config = mkOption {
+        type = types.lines;
         default = "";
         description = ''
           Extra configuration. Contents will be added verbatim to TSocks
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/ihatemoney/default.nix b/nixos/modules/services/web-apps/ihatemoney/default.nix
new file mode 100644
index 00000000000..68769ac8c03
--- /dev/null
+++ b/nixos/modules/services/web-apps/ihatemoney/default.nix
@@ -0,0 +1,141 @@
+{ config, pkgs, lib, ... }:
+with lib;
+let
+  cfg = config.services.ihatemoney;
+  user = "ihatemoney";
+  group = "ihatemoney";
+  db = "ihatemoney";
+  python3 = config.services.uwsgi.package.python3;
+  pkg = python3.pkgs.ihatemoney;
+  toBool = x: if x then "True" else "False";
+  configFile = pkgs.writeText "ihatemoney.cfg" ''
+        from secrets import token_hex
+        # load a persistent secret key
+        SECRET_KEY_FILE = "/var/lib/ihatemoney/secret_key"
+        SECRET_KEY = ""
+        try:
+          with open(SECRET_KEY_FILE) as f:
+            SECRET_KEY = f.read()
+        except FileNotFoundError:
+          pass
+        if not SECRET_KEY:
+          print("ihatemoney: generating a new secret key")
+          SECRET_KEY = token_hex(50)
+          with open(SECRET_KEY_FILE, "w") as f:
+            f.write(SECRET_KEY)
+        del token_hex
+        del SECRET_KEY_FILE
+
+        # "normal" configuration
+        DEBUG = False
+        SQLALCHEMY_DATABASE_URI = '${
+          if cfg.backend == "sqlite"
+          then "sqlite:////var/lib/ihatemoney/ihatemoney.sqlite"
+          else "postgresql:///${db}"}'
+        SQLALCHEMY_TRACK_MODIFICATIONS = False
+        MAIL_DEFAULT_SENDER = ("${cfg.defaultSender.name}", "${cfg.defaultSender.email}")
+        ACTIVATE_DEMO_PROJECT = ${toBool cfg.enableDemoProject}
+        ADMIN_PASSWORD = "${toString cfg.adminHashedPassword /*toString null == ""*/}"
+        ALLOW_PUBLIC_PROJECT_CREATION = ${toBool cfg.enablePublicProjectCreation}
+        ACTIVATE_ADMIN_DASHBOARD = ${toBool cfg.enableAdminDashboard}
+
+        ${cfg.extraConfig}
+  '';
+in
+  {
+    options.services.ihatemoney = {
+      enable = mkEnableOption "ihatemoney webapp. Note that this will set uwsgi to emperor mode running as root";
+      backend = mkOption {
+        type = types.enum [ "sqlite" "postgresql" ];
+        default = "sqlite";
+        description = ''
+          The database engine to use for ihatemoney.
+          If <literal>postgresql</literal> is selected, then a database called
+          <literal>${db}</literal> will be created. If you disable this option,
+          it will however not be removed.
+        '';
+      };
+      adminHashedPassword = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "The hashed password of the administrator. To obtain it, run <literal>ihatemoney generate_password_hash</literal>";
+      };
+      uwsgiConfig = mkOption {
+        type = types.attrs;
+        example = {
+          http = ":8000";
+        };
+        description = "Additionnal configuration of the UWSGI vassal running ihatemoney. It should notably specify on which interfaces and ports the vassal should listen.";
+      };
+      defaultSender = {
+        name = mkOption {
+          type = types.str;
+          default = "Budget manager";
+          description = "The display name of the sender of ihatemoney emails";
+        };
+        email = mkOption {
+          type = types.str;
+          default = "ihatemoney@${config.networking.hostName}";
+          description = "The email of the sender of ihatemoney emails";
+        };
+      };
+      enableDemoProject = mkEnableOption "access to the demo project in ihatemoney";
+      enablePublicProjectCreation = mkEnableOption "permission to create projects in ihatemoney by anyone";
+      enableAdminDashboard = mkEnableOption "ihatemoney admin dashboard";
+      extraConfig = mkOption {
+        type = types.str;
+        default = "";
+        description = "Extra configuration appended to ihatemoney's configuration file. It is a python file, so pay attention to indentation.";
+      };
+    };
+    config = mkIf cfg.enable {
+      services.postgresql = mkIf (cfg.backend == "postgresql") {
+        enable = true;
+        ensureDatabases = [ db ];
+        ensureUsers = [ {
+          name = user;
+          ensurePermissions = {
+            "DATABASE ${db}" = "ALL PRIVILEGES";
+          };
+        } ];
+      };
+      systemd.services.postgresql = mkIf (cfg.backend == "postgresql") {
+        wantedBy = [ "uwsgi.service" ];
+        before = [ "uwsgi.service" ];
+      };
+      systemd.tmpfiles.rules = [
+        "d /var/lib/ihatemoney 770 ${user} ${group}"
+      ];
+      users = {
+        users.${user} = {
+          isSystemUser = true;
+          inherit group;
+        };
+        groups.${group} = {};
+      };
+      services.uwsgi = {
+        enable = true;
+        plugins = [ "python3" ];
+        # the vassal needs to be able to setuid
+        user = "root";
+        group = "root";
+        instance = {
+          type = "emperor";
+          vassals.ihatemoney = {
+            type = "normal";
+            strict = true;
+            uid = user;
+            gid = group;
+            # apparently flask uses threads: https://github.com/spiral-project/ihatemoney/commit/c7815e48781b6d3a457eaff1808d179402558f8c
+            enable-threads = true;
+            module = "wsgi:application";
+            chdir = "${pkg}/${pkg.pythonModule.sitePackages}/ihatemoney";
+            env = [ "IHATEMONEY_SETTINGS_FILE_PATH=${configFile}" ];
+            pythonPackages = self: [ self.ihatemoney ];
+          } // cfg.uwsgiConfig;
+        };
+      };
+    };
+  }
+
+
diff --git a/nixos/modules/services/web-apps/nextcloud.nix b/nixos/modules/services/web-apps/nextcloud.nix
index f1dabadc119..d79f2bb735f 100644
--- a/nixos/modules/services/web-apps/nextcloud.nix
+++ b/nixos/modules/services/web-apps/nextcloud.nix
@@ -229,6 +229,15 @@ in {
         '';
       };
 
+      trustedProxies = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          Trusted proxies, to provide if the nextcloud installation is being
+          proxied to secure against e.g. spoofing.
+        '';
+      };
+
       overwriteProtocol = mkOption {
         type = types.nullOr (types.enum [ "http" "https" ]);
         default = null;
@@ -352,6 +361,7 @@ in {
               ${optionalString (c.dbpassFile != null) "'dbpassword' => nix_read_pwd(),"}
               'dbtype' => '${c.dbtype}',
               'trusted_domains' => ${writePhpArrary ([ cfg.hostName ] ++ c.extraTrustedDomains)},
+              'trusted_proxies' => ${writePhpArrary (c.trustedProxies)},
             ];
           '';
           occInstallCmd = let
diff --git a/nixos/modules/services/web-servers/apache-httpd/default.nix b/nixos/modules/services/web-servers/apache-httpd/default.nix
index 4460f89ec5c..fd17e4b54f0 100644
--- a/nixos/modules/services/web-servers/apache-httpd/default.nix
+++ b/nixos/modules/services/web-servers/apache-httpd/default.nix
@@ -629,6 +629,9 @@ in
 
     environment.systemPackages = [httpd];
 
+    # required for "apachectl configtest"
+    environment.etc."httpd/httpd.conf".source = httpdConf;
+
     services.httpd.phpOptions =
       ''
         ; Needed for PHP's mail() function.
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/web-servers/uwsgi.nix b/nixos/modules/services/web-servers/uwsgi.nix
index 0c727cf44ae..3481b5e6040 100644
--- a/nixos/modules/services/web-servers/uwsgi.nix
+++ b/nixos/modules/services/web-servers/uwsgi.nix
@@ -5,10 +5,6 @@ with lib;
 let
   cfg = config.services.uwsgi;
 
-  uwsgi = pkgs.uwsgi.override {
-    plugins = cfg.plugins;
-  };
-
   buildCfg = name: c:
     let
       plugins =
@@ -23,8 +19,8 @@ let
       python =
         if hasPython2 && hasPython3 then
           throw "`plugins` attribute in UWSGI configuration shouldn't contain both python2 and python3"
-        else if hasPython2 then uwsgi.python2
-        else if hasPython3 then uwsgi.python3
+        else if hasPython2 then cfg.package.python2
+        else if hasPython3 then cfg.package.python3
         else null;
 
       pythonEnv = python.withPackages (c.pythonPackages or (self: []));
@@ -77,6 +73,11 @@ in {
         description = "Where uWSGI communication sockets can live";
       };
 
+      package = mkOption {
+        type = types.package;
+        internal = true;
+      };
+
       instance = mkOption {
         type = types.attrs;
         default = {
@@ -138,7 +139,7 @@ in {
       '';
       serviceConfig = {
         Type = "notify";
-        ExecStart = "${uwsgi}/bin/uwsgi --uid ${cfg.user} --gid ${cfg.group} --json ${buildCfg "server" cfg.instance}/server.json";
+        ExecStart = "${cfg.package}/bin/uwsgi --uid ${cfg.user} --gid ${cfg.group} --json ${buildCfg "server" cfg.instance}/server.json";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
         ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID";
         NotifyAccess = "main";
@@ -156,5 +157,9 @@ in {
     users.groups = optionalAttrs (cfg.group == "uwsgi") {
       uwsgi.gid = config.ids.gids.uwsgi;
     };
+
+    services.uwsgi.package = pkgs.uwsgi.override {
+      inherit (cfg) plugins;
+    };
   };
 }
diff --git a/nixos/modules/services/x11/desktop-managers/gnome3.nix b/nixos/modules/services/x11/desktop-managers/gnome3.nix
index 6d9bd284bc7..ba9906072b3 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
diff --git a/nixos/modules/services/x11/display-managers/gdm.nix b/nixos/modules/services/x11/display-managers/gdm.nix
index 4de3dbd8770..325023f4121 100644
--- a/nixos/modules/services/x11/display-managers/gdm.nix
+++ b/nixos/modules/services/x11/display-managers/gdm.nix
@@ -171,9 +171,13 @@ in
       "L+ /run/gdm/.config/pulse - - - - ${pulseConfig}"
     ] ++ optionals config.services.gnome3.gnome-initial-setup.enable [
       # Create stamp file for gnome-initial-setup to prevent it starting in GDM.
-      "f /run/gdm/.config/gnome-initial-setup-done 0711 gdm gdm yes"
+      "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/system/boot/networkd.nix b/nixos/modules/system/boot/networkd.nix
index 3e289a63139..56a9d6b1138 100644
--- a/nixos/modules/system/boot/networkd.nix
+++ b/nixos/modules/system/boot/networkd.nix
@@ -49,7 +49,7 @@ 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")
@@ -172,6 +172,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 +190,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"])
@@ -477,6 +485,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 +732,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 +840,11 @@ let
             ${attrsToSection def.bondConfig}
 
           ''}
+          ${optionalString (def.xfrmConfig != { }) ''
+            [Xfrm]
+            ${attrsToSection def.xfrmConfig}
+
+          ''}
           ${optionalString (def.wireguardConfig != { }) ''
             [WireGuard]
             ${attrsToSection def.wireguardConfig}
@@ -847,6 +882,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]
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/tasks/powertop.nix b/nixos/modules/tasks/powertop.nix
index 609831506e1..e8064f9fa80 100644
--- a/nixos/modules/tasks/powertop.nix
+++ b/nixos/modules/tasks/powertop.nix
@@ -15,6 +15,7 @@ in {
     systemd.services = {
       powertop = {
         wantedBy = [ "multi-user.target" ];
+        after = [ "multi-user.target" ];
         description = "Powertop tunings";
         path = [ pkgs.kmod ];
         serviceConfig = {
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/release.nix b/nixos/release.nix
index f40b5fa9bd7..9109f5751eb 100644
--- a/nixos/release.nix
+++ b/nixos/release.nix
@@ -209,7 +209,8 @@ in rec {
     hydraJob ((import lib/eval-config.nix {
       inherit system;
       modules =
-        [ versionModule
+        [ configuration
+          versionModule
           ./maintainers/scripts/ec2/amazon-image.nix
         ];
     }).config.system.build.amazonImage)
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 3f6921e0f4d..8c11464f9d6 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -33,6 +33,7 @@ in
   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,6 +62,7 @@ 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 {};
@@ -73,6 +75,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 {};
@@ -122,6 +125,7 @@ in
   i3wm = handleTest ./i3wm.nix {};
   icingaweb2 = handleTest ./icingaweb2.nix {};
   iftop = handleTest ./iftop.nix {};
+  ihatemoney = handleTest ./ihatemoney.nix {};
   incron = handleTest ./incron.nix {};
   influxdb = handleTest ./influxdb.nix {};
   initrd-network-ssh = handleTest ./initrd-network-ssh {};
@@ -294,6 +298,7 @@ in
   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 {};
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/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/ceph-multi-node.nix b/nixos/tests/ceph-multi-node.nix
index 52a0b5caf23..90dd747525d 100644
--- a/nixos/tests/ceph-multi-node.nix
+++ b/nixos/tests/ceph-multi-node.nix
@@ -19,6 +19,12 @@ let
       key = "AQBEEJNac00kExAAXEgy943BGyOpVH1LLlHafQ==";
       uuid = "5e97a838-85b6-43b0-8950-cb56d554d1e5";
     };
+    osd2 = {
+      name = "2";
+      ip = "192.168.1.4";
+      key = "AQAdyhZeIaUlARAAGRoidDAmS6Vkp546UFEf5w==";
+      uuid = "ea999274-13d0-4dd5-9af9-ad25a324f72f";
+    };
   };
   generateCephConfig = { daemonConfig }: {
     enable = true;
@@ -72,35 +78,20 @@ let
     };
   }; };
 
-  networkOsd0 = {
+  networkOsd = osd: {
     dhcpcd.enable = false;
     interfaces.eth1.ipv4.addresses = pkgs.lib.mkOverride 0 [
-      { address = cfg.osd0.ip; prefixLength = 24; }
+      { address = osd.ip; prefixLength = 24; }
     ];
     firewall = {
       allowedTCPPortRanges = [ { from = 6800; to = 7300; } ];
     };
   };
-  cephConfigOsd0 = generateCephConfig { daemonConfig = {
-    osd = {
-      enable = true;
-      daemons = [ cfg.osd0.name ];
-    };
-  }; };
 
-  networkOsd1 = {
-    dhcpcd.enable = false;
-    interfaces.eth1.ipv4.addresses = pkgs.lib.mkOverride 0 [
-      { address = cfg.osd1.ip; prefixLength = 24; }
-    ];
-    firewall = {
-      allowedTCPPortRanges = [ { from = 6800; to = 7300; } ];
-    };
-  };
-  cephConfigOsd1 = generateCephConfig { daemonConfig = {
+  cephConfigOsd = osd: generateCephConfig { daemonConfig = {
     osd = {
       enable = true;
-      daemons = [ cfg.osd1.name ];
+      daemons = [ osd.name ];
     };
   }; };
 
@@ -114,6 +105,7 @@ let
     monA.wait_for_unit("network.target")
     osd0.wait_for_unit("network.target")
     osd1.wait_for_unit("network.target")
+    osd2.wait_for_unit("network.target")
 
     # Bootstrap ceph-mon daemon
     monA.succeed(
@@ -145,8 +137,9 @@ let
     monA.succeed("cp /etc/ceph/ceph.client.admin.keyring /tmp/shared")
     osd0.succeed("cp /tmp/shared/ceph.client.admin.keyring /etc/ceph")
     osd1.succeed("cp /tmp/shared/ceph.client.admin.keyring /etc/ceph")
+    osd2.succeed("cp /tmp/shared/ceph.client.admin.keyring /etc/ceph")
 
-    # Bootstrap both OSDs
+    # Bootstrap OSDs
     osd0.succeed(
         "mkfs.xfs /dev/vdb",
         "mkdir -p /var/lib/ceph/osd/ceph-${cfg.osd0.name}",
@@ -161,6 +154,13 @@ let
         "ceph-authtool --create-keyring /var/lib/ceph/osd/ceph-${cfg.osd1.name}/keyring --name osd.${cfg.osd1.name} --add-key ${cfg.osd1.key}",
         'echo \'{"cephx_secret": "${cfg.osd1.key}"}\' | ceph osd new ${cfg.osd1.uuid} -i -',
     )
+    osd2.succeed(
+        "mkfs.xfs /dev/vdb",
+        "mkdir -p /var/lib/ceph/osd/ceph-${cfg.osd2.name}",
+        "mount /dev/vdb /var/lib/ceph/osd/ceph-${cfg.osd2.name}",
+        "ceph-authtool --create-keyring /var/lib/ceph/osd/ceph-${cfg.osd2.name}/keyring --name osd.${cfg.osd2.name} --add-key ${cfg.osd2.key}",
+        'echo \'{"cephx_secret": "${cfg.osd2.key}"}\' | ceph osd new ${cfg.osd2.uuid} -i -',
+    )
 
     # Initialize the OSDs with regular filestore
     osd0.succeed(
@@ -173,7 +173,12 @@ let
         "chown -R ceph:ceph /var/lib/ceph/osd",
         "systemctl start ceph-osd-${cfg.osd1.name}",
     )
-    monA.wait_until_succeeds("ceph osd stat | grep -e '2 osds: 2 up[^,]*, 2 in'")
+    osd2.succeed(
+        "ceph-osd -i ${cfg.osd2.name} --mkfs --osd-uuid ${cfg.osd2.uuid}",
+        "chown -R ceph:ceph /var/lib/ceph/osd",
+        "systemctl start ceph-osd-${cfg.osd2.name}",
+    )
+    monA.wait_until_succeeds("ceph osd stat | grep -e '3 osds: 3 up[^,]*, 3 in'")
     monA.wait_until_succeeds("ceph -s | grep 'mgr: ${cfg.monA.name}(active,'")
     monA.wait_until_succeeds("ceph -s | grep 'HEALTH_OK'")
 
@@ -196,16 +201,18 @@ let
     monA.crash()
     osd0.crash()
     osd1.crash()
+    osd2.crash()
 
     # Start it up
     osd0.start()
     osd1.start()
+    osd2.start()
     monA.start()
 
     # Ensure the cluster comes back up again
     monA.succeed("ceph -s | grep 'mon: 1 daemons'")
     monA.wait_until_succeeds("ceph -s | grep 'quorum ${cfg.monA.name}'")
-    monA.wait_until_succeeds("ceph osd stat | grep -e '2 osds: 2 up[^,]*, 2 in'")
+    monA.wait_until_succeeds("ceph osd stat | grep -e '3 osds: 3 up[^,]*, 3 in'")
     monA.wait_until_succeeds("ceph -s | grep 'mgr: ${cfg.monA.name}(active,'")
     monA.wait_until_succeeds("ceph -s | grep 'HEALTH_OK'")
   '';
@@ -217,8 +224,9 @@ in {
 
   nodes = {
     monA = generateHost { pkgs = pkgs; cephConfig = cephConfigMonA; networkConfig = networkMonA; };
-    osd0 = generateHost { pkgs = pkgs; cephConfig = cephConfigOsd0; networkConfig = networkOsd0; };
-    osd1 = generateHost { pkgs = pkgs; cephConfig = cephConfigOsd1; networkConfig = networkOsd1; };
+    osd0 = generateHost { pkgs = pkgs; cephConfig = cephConfigOsd cfg.osd0; networkConfig = networkOsd cfg.osd0; };
+    osd1 = generateHost { pkgs = pkgs; cephConfig = cephConfigOsd cfg.osd1; networkConfig = networkOsd cfg.osd1; };
+    osd2 = generateHost { pkgs = pkgs; cephConfig = cephConfigOsd cfg.osd2; networkConfig = networkOsd cfg.osd2; };
   };
 
   testScript = testscript;
diff --git a/nixos/tests/ceph-single-node.nix b/nixos/tests/ceph-single-node.nix
index da92a73e14d..1a027e17836 100644
--- a/nixos/tests/ceph-single-node.nix
+++ b/nixos/tests/ceph-single-node.nix
@@ -17,6 +17,11 @@ let
       key = "AQBEEJNac00kExAAXEgy943BGyOpVH1LLlHafQ==";
       uuid = "5e97a838-85b6-43b0-8950-cb56d554d1e5";
     };
+    osd2 = {
+      name = "2";
+      key = "AQAdyhZeIaUlARAAGRoidDAmS6Vkp546UFEf5w==";
+      uuid = "ea999274-13d0-4dd5-9af9-ad25a324f72f";
+    };
   };
   generateCephConfig = { daemonConfig }: {
     enable = true;
@@ -30,7 +35,7 @@ let
   generateHost = { pkgs, cephConfig, networkConfig, ... }: {
     virtualisation = {
       memorySize = 512;
-      emptyDiskImages = [ 20480 20480 ];
+      emptyDiskImages = [ 20480 20480 20480 ];
       vlans = [ 1 ];
     };
 
@@ -65,7 +70,7 @@ let
     };
     osd = {
       enable = true;
-      daemons = [ cfg.osd0.name cfg.osd1.name ];
+      daemons = [ cfg.osd0.name cfg.osd1.name cfg.osd2.name ];
     };
   }; };
 
@@ -104,29 +109,36 @@ let
     monA.wait_until_succeeds("ceph -s | grep 'quorum ${cfg.monA.name}'")
     monA.wait_until_succeeds("ceph -s | grep 'mgr: ${cfg.monA.name}(active,'")
 
-    # Bootstrap both OSDs
+    # Bootstrap OSDs
     monA.succeed(
         "mkfs.xfs /dev/vdb",
         "mkfs.xfs /dev/vdc",
+        "mkfs.xfs /dev/vdd",
         "mkdir -p /var/lib/ceph/osd/ceph-${cfg.osd0.name}",
         "mount /dev/vdb /var/lib/ceph/osd/ceph-${cfg.osd0.name}",
         "mkdir -p /var/lib/ceph/osd/ceph-${cfg.osd1.name}",
         "mount /dev/vdc /var/lib/ceph/osd/ceph-${cfg.osd1.name}",
+        "mkdir -p /var/lib/ceph/osd/ceph-${cfg.osd2.name}",
+        "mount /dev/vdd /var/lib/ceph/osd/ceph-${cfg.osd2.name}",
         "ceph-authtool --create-keyring /var/lib/ceph/osd/ceph-${cfg.osd0.name}/keyring --name osd.${cfg.osd0.name} --add-key ${cfg.osd0.key}",
         "ceph-authtool --create-keyring /var/lib/ceph/osd/ceph-${cfg.osd1.name}/keyring --name osd.${cfg.osd1.name} --add-key ${cfg.osd1.key}",
+        "ceph-authtool --create-keyring /var/lib/ceph/osd/ceph-${cfg.osd2.name}/keyring --name osd.${cfg.osd2.name} --add-key ${cfg.osd2.key}",
         'echo \'{"cephx_secret": "${cfg.osd0.key}"}\' | ceph osd new ${cfg.osd0.uuid} -i -',
         'echo \'{"cephx_secret": "${cfg.osd1.key}"}\' | ceph osd new ${cfg.osd1.uuid} -i -',
+        'echo \'{"cephx_secret": "${cfg.osd2.key}"}\' | ceph osd new ${cfg.osd2.uuid} -i -',
     )
 
     # Initialize the OSDs with regular filestore
     monA.succeed(
         "ceph-osd -i ${cfg.osd0.name} --mkfs --osd-uuid ${cfg.osd0.uuid}",
         "ceph-osd -i ${cfg.osd1.name} --mkfs --osd-uuid ${cfg.osd1.uuid}",
+        "ceph-osd -i ${cfg.osd2.name} --mkfs --osd-uuid ${cfg.osd2.uuid}",
         "chown -R ceph:ceph /var/lib/ceph/osd",
         "systemctl start ceph-osd-${cfg.osd0.name}",
         "systemctl start ceph-osd-${cfg.osd1.name}",
+        "systemctl start ceph-osd-${cfg.osd2.name}",
     )
-    monA.wait_until_succeeds("ceph osd stat | grep -e '2 osds: 2 up[^,]*, 2 in'")
+    monA.wait_until_succeeds("ceph osd stat | grep -e '3 osds: 3 up[^,]*, 3 in'")
     monA.wait_until_succeeds("ceph -s | grep 'mgr: ${cfg.monA.name}(active,'")
     monA.wait_until_succeeds("ceph -s | grep 'HEALTH_OK'")
 
@@ -161,11 +173,12 @@ let
     monA.wait_for_unit("ceph-mgr-${cfg.monA.name}")
     monA.wait_for_unit("ceph-osd-${cfg.osd0.name}")
     monA.wait_for_unit("ceph-osd-${cfg.osd1.name}")
+    monA.wait_for_unit("ceph-osd-${cfg.osd2.name}")
 
     # Ensure the cluster comes back up again
     monA.succeed("ceph -s | grep 'mon: 1 daemons'")
     monA.wait_until_succeeds("ceph -s | grep 'quorum ${cfg.monA.name}'")
-    monA.wait_until_succeeds("ceph osd stat | grep -e '2 osds: 2 up[^,]*, 2 in'")
+    monA.wait_until_succeeds("ceph osd stat | grep -e '3 osds: 3 up[^,]*, 3 in'")
     monA.wait_until_succeeds("ceph -s | grep 'mgr: ${cfg.monA.name}(active,'")
     monA.wait_until_succeeds("ceph -s | grep 'HEALTH_OK'")
   '';
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/common/user-account.nix b/nixos/tests/common/user-account.nix
index 9cd531a1f96..a57ee2d59ae 100644
--- a/nixos/tests/common/user-account.nix
+++ b/nixos/tests/common/user-account.nix
@@ -4,6 +4,7 @@
     { isNormalUser = true;
       description = "Alice Foobar";
       password = "foobar";
+      uid = 1000;
     };
 
   users.users.bob =
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/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/elk.nix b/nixos/tests/elk.nix
index b33d98b85d6..d3dc6dde135 100644
--- a/nixos/tests/elk.nix
+++ b/nixos/tests/elk.nix
@@ -6,20 +6,11 @@
   # NIXPKGS_ALLOW_UNFREE=1 nix-build nixos/tests/elk.nix -A ELK-6 --arg enableUnfree true
 }:
 
-with import ../lib/testing.nix { inherit system pkgs; };
-with pkgs.lib;
-
 let
   esUrl = "http://localhost:9200";
 
-  totalHits = message :
-    "curl --silent --show-error '${esUrl}/_search' -H 'Content-Type: application/json' " +
-    ''-d '{\"query\" : { \"match\" : { \"message\" : \"${message}\"}}}' '' +
-    "| jq .hits.total";
-
   mkElkTest = name : elk :
-   let elasticsearchGe7 = builtins.compareVersions elk.elasticsearch.version "7" >= 0;
-   in makeTest {
+    import ./make-test-python.nix ({
     inherit name;
     meta = with pkgs.stdenv.lib.maintainers; {
       maintainers = [ eelco offline basvandijk ];
@@ -50,15 +41,15 @@ let
                                         elk.journalbeat.version "6" < 0; in {
                 enable = true;
                 package = elk.journalbeat;
-                extraConfig = mkOptionDefault (''
+                extraConfig = pkgs.lib.mkOptionDefault (''
                   logging:
                     to_syslog: true
                     level: warning
                     metrics.enabled: false
                   output.elasticsearch:
                     hosts: [ "127.0.0.1:9200" ]
-                    ${optionalString lt6 "template.enabled: false"}
-                '' + optionalString (!lt6) ''
+                    ${pkgs.lib.optionalString lt6 "template.enabled: false"}
+                '' + pkgs.lib.optionalString (!lt6) ''
                   journalbeat.inputs:
                   - paths: []
                     seek: cursor
@@ -99,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:
@@ -130,11 +120,23 @@ let
       };
 
     testScript = ''
-      startAll;
+      import json
+
+
+      def total_hits(message):
+          dictionary = {"query": {"match": {"message": message}}}
+          return (
+              "curl --silent --show-error '${esUrl}/_search' "
+              + "-H 'Content-Type: application/json' "
+              + "-d '{}' ".format(json.dumps(dictionary))
+              + "| jq .hits.total"
+          )
+
+
+      start_all()
 
-      # Wait until elasticsearch is listening for connections.
-      $one->waitForUnit("elasticsearch.service");
-      $one->waitForOpenPort(9200);
+      one.wait_for_unit("elasticsearch.service")
+      one.wait_for_open_port(9200)
 
       # Continue as long as the status is not "red". The status is probably
       # "yellow" instead of "green" because we are using a single elasticsearch
@@ -142,42 +144,43 @@ let
       #
       # TODO: extend this test with multiple elasticsearch nodes
       #       and see if the status turns "green".
-      $one->waitUntilSucceeds(
-        "curl --silent --show-error '${esUrl}/_cluster/health' " .
-        "| jq .status | grep -v red");
-
-      # Perform some simple logstash tests.
-      $one->waitForUnit("logstash.service");
-      $one->waitUntilSucceeds("cat /tmp/logstash.out | grep flowers");
-      $one->waitUntilSucceeds("cat /tmp/logstash.out | grep -v dragons");
-
-      # See if kibana is healthy.
-      $one->waitForUnit("kibana.service");
-      $one->waitUntilSucceeds(
-        "curl --silent --show-error 'http://localhost:5601/api/status' " .
-        "| jq .status.overall.state | grep green");
-
-      # See if logstash messages arive in elasticsearch.
-      $one->waitUntilSucceeds("${totalHits "flowers"} | grep -v 0");
-      $one->waitUntilSucceeds("${totalHits "dragons"} | grep 0");
-
-      # Test if a message logged to the journal
-      # is ingested by elasticsearch via journalbeat.
-      $one->waitForUnit("journalbeat.service");
-      $one->execute("echo 'Supercalifragilisticexpialidocious' | systemd-cat");
-      $one->waitUntilSucceeds(
-        "${totalHits "Supercalifragilisticexpialidocious"} | grep -v 0");
-
-    '' + optionalString (!elasticsearchGe7) ''
-      # Test elasticsearch-curator.
-      $one->systemctl("stop logstash");
-      $one->systemctl("start elasticsearch-curator");
-      $one->waitUntilSucceeds(
-        "! curl --silent --show-error '${esUrl}/_cat/indices' " .
-        "| grep logstash | grep -q ^$1");
+      one.wait_until_succeeds(
+          "curl --silent --show-error '${esUrl}/_cluster/health' | jq .status | grep -v red"
+      )
+
+      with subtest("Perform some simple logstash tests"):
+          one.wait_for_unit("logstash.service")
+          one.wait_until_succeeds("cat /tmp/logstash.out | grep flowers")
+          one.wait_until_succeeds("cat /tmp/logstash.out | grep -v dragons")
+
+      with subtest("Kibana is healthy"):
+          one.wait_for_unit("kibana.service")
+          one.wait_until_succeeds(
+              "curl --silent --show-error 'http://localhost:5601/api/status' | jq .status.overall.state | grep green"
+          )
+
+      with subtest("Logstash messages arive in elasticsearch"):
+          one.wait_until_succeeds(total_hits("flowers") + " | grep -v 0")
+          one.wait_until_succeeds(total_hits("dragons") + " | grep 0")
+
+      with subtest(
+          "A message logged to the journal is ingested by elasticsearch via journalbeat"
+      ):
+          one.wait_for_unit("journalbeat.service")
+          one.execute("echo 'Supercalifragilisticexpialidocious' | systemd-cat")
+          one.wait_until_succeeds(
+              total_hits("Supercalifragilisticexpialidocious") + " | grep -v 0"
+          )
+
+      with subtest("Elasticsearch-curator works"):
+          one.systemctl("stop logstash")
+          one.systemctl("start elasticsearch-curator")
+          one.wait_until_succeeds(
+              '! curl --silent --show-error "${esUrl}/_cat/indices" | grep logstash | grep -q ^'
+          )
     '';
-  };
-in mapAttrs mkElkTest {
+  }) {};
+in pkgs.lib.mapAttrs mkElkTest {
   ELK-6 =
     if enableUnfree
     then {
diff --git a/nixos/tests/gnome3-xorg.nix b/nixos/tests/gnome3-xorg.nix
index aa03501f6a5..f793bb922ad 100644
--- a/nixos/tests/gnome3-xorg.nix
+++ b/nixos/tests/gnome3-xorg.nix
@@ -1,41 +1,79 @@
-import ./make-test.nix ({ pkgs, ...} : {
+import ./make-test-python.nix ({ pkgs, ...} : {
   name = "gnome3-xorg";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = pkgs.gnome3.maintainers;
   };
 
-  machine =
-    { ... }:
+  machine = { nodes, ... }: let
+    user = nodes.machine.config.users.users.alice;
+  in
 
     { imports = [ ./common/user-account.nix ];
 
       services.xserver.enable = true;
 
-      services.xserver.displayManager.gdm.enable = false;
-      services.xserver.displayManager.lightdm.enable = true;
-      services.xserver.displayManager.lightdm.autoLogin.enable = true;
-      services.xserver.displayManager.lightdm.autoLogin.user = "alice";
+      services.xserver.displayManager.gdm = {
+        enable = true;
+        autoLogin = {
+          enable = true;
+          user = user.name;
+        };
+      };
+
       services.xserver.desktopManager.gnome3.enable = true;
       services.xserver.displayManager.defaultSession = "gnome-xorg";
 
       virtualisation.memorySize = 1024;
     };
 
-  testScript =
-    ''
-      $machine->waitForX;
+  testScript = { nodes, ... }: let
+    user = nodes.machine.config.users.users.alice;
+    uid = toString user.uid;
+    bus = "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/${uid}/bus";
+    xauthority = "/run/user/${uid}/gdm/Xauthority";
+    display = "DISPLAY=:0.0";
+    env = "${bus} XAUTHORITY=${xauthority} ${display}";
+    gdbus = "${env} gdbus";
+    su = command: "su - ${user.name} -c '${env} ${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 = su "${gdbus} ${eval} Main.layoutManager._startingUp";
+
+    # Start gnome-terminal
+    gnomeTerminalCommand = su "gnome-terminal";
 
-      # wait for alice to be logged in
-      $machine->waitForUnit("default.target","alice");
+    # Hopefully gnome-terminal's wm class
+    wmClass = su "${gdbus} ${eval} global.display.focus_window.wm_class";
+  in ''
+      with subtest("Login to GNOME Xorg with GDM"):
+          machine.wait_for_x()
+          # Wait for alice to be logged in"
+          machine.wait_for_unit("default.target", "${user.name}")
+          machine.wait_for_file("${xauthority}")
+          machine.succeed("xauth merge ${xauthority}")
+          # Check that logging in has given the user ownership of devices
+          assert "alice" in machine.succeed("getfacl -p /dev/snd/timer")
 
-      # Check that logging in has given the user ownership of devices.
-      $machine->succeed("getfacl -p /dev/snd/timer | grep -q alice");
+      with subtest("Wait for GNOME Shell"):
+          # correct output should be (true, 'false')
+          machine.wait_until_succeeds(
+              "${startingUp} | grep -q 'true,..false'"
+          )
 
-      $machine->succeed("su - alice -c 'DISPLAY=:0.0 gnome-terminal &'");
-      $machine->succeed("xauth merge ~alice/.Xauthority");
-      $machine->waitForWindow(qr/alice.*machine/);
-      $machine->succeed("timeout 900 bash -c 'while read msg; do if [[ \$msg =~ \"GNOME Shell started\" ]]; then break; fi; done < <(journalctl -f)'");
-      $machine->sleep(10);
-      $machine->screenshot("screen");
+      with subtest("Open Gnome Terminal"):
+          machine.succeed(
+              "${gnomeTerminalCommand}"
+          )
+          # correct output should be (true, '"Gnome-terminal"')
+          machine.wait_until_succeeds(
+              "${wmClass} | grep -q  'true,...Gnome-terminal'"
+          )
+          machine.sleep(20)
+          machine.screenshot("screen")
     '';
 })
diff --git a/nixos/tests/home-assistant.nix b/nixos/tests/home-assistant.nix
index 6b53914fd85..80dca43f1f3 100644
--- a/nixos/tests/home-assistant.nix
+++ b/nixos/tests/home-assistant.nix
@@ -1,11 +1,10 @@
-import ./make-test.nix ({ pkgs, ... }:
+import ./make-test-python.nix ({ pkgs, ... }:
 
 let
   configDir = "/var/lib/foobar";
   apiPassword = "some_secret";
   mqttPassword = "another_secret";
   hassCli = "hass-cli --server http://hass:8123 --password '${apiPassword}'";
-
 in {
   name = "home-assistant";
   meta = with pkgs.stdenv.lib; {
@@ -69,36 +68,44 @@ in {
   };
 
   testScript = ''
-    startAll;
-    $hass->waitForUnit("home-assistant.service");
-
-    # The config is specified using a Nix attribute set,
-    # converted from JSON to YAML, and linked to the config dir
-    $hass->succeed("test -L ${configDir}/configuration.yaml");
-    # The lovelace config is copied because lovelaceConfigWritable = true
-    $hass->succeed("test -f ${configDir}/ui-lovelace.yaml");
-
-    # Check that Home Assistant's web interface and API can be reached
-    $hass->waitForOpenPort(8123);
-    $hass->succeed("curl --fail http://localhost:8123/states");
-    $hass->succeed("curl --fail -H 'x-ha-access: ${apiPassword}' http://localhost:8123/api/ | grep -qF 'API running'");
-
-    # Toggle a binary sensor using MQTT
-    $hass->succeed("curl http://localhost:8123/api/states/binary_sensor.mqtt_binary_sensor -H 'x-ha-access: ${apiPassword}' | grep -qF '\"state\": \"off\"'");
-    $hass->waitUntilSucceeds("mosquitto_pub -V mqttv311 -t home-assistant/test -u homeassistant -P '${mqttPassword}' -m let_there_be_light");
-    $hass->succeed("curl http://localhost:8123/api/states/binary_sensor.mqtt_binary_sensor -H 'x-ha-access: ${apiPassword}' | grep -qF '\"state\": \"on\"'");
-
-    # Toggle a binary sensor using hass-cli
-    $hass->succeed("${hassCli} --output json state get binary_sensor.mqtt_binary_sensor | grep -qF '\"state\": \"on\"'");
-    $hass->succeed("${hassCli} state edit binary_sensor.mqtt_binary_sensor --json='{\"state\": \"off\"}'");
-    $hass->succeed("curl http://localhost:8123/api/states/binary_sensor.mqtt_binary_sensor -H 'x-ha-access: ${apiPassword}' | grep -qF '\"state\": \"off\"'");
-
-    # Print log to ease debugging
-    my $log = $hass->succeed("cat ${configDir}/home-assistant.log");
-    print "\n### home-assistant.log ###\n";
-    print "$log\n";
+    start_all()
+    hass.wait_for_unit("home-assistant.service")
+    with subtest("Check that YAML configuration file is in place"):
+        hass.succeed("test -L ${configDir}/configuration.yaml")
+    with subtest("lovelace config is copied because lovelaceConfigWritable = true"):
+        hass.succeed("test -f ${configDir}/ui-lovelace.yaml")
+    with subtest("Check that Home Assistant's web interface and API can be reached"):
+        hass.wait_for_open_port(8123)
+        hass.succeed("curl --fail http://localhost:8123/states")
+        assert "API running" in hass.succeed(
+            "curl --fail -H 'x-ha-access: ${apiPassword}' http://localhost:8123/api/"
+        )
+    with subtest("Toggle a binary sensor using MQTT"):
+        assert '"state": "off"' in hass.succeed(
+            "curl http://localhost:8123/api/states/binary_sensor.mqtt_binary_sensor -H 'x-ha-access: ${apiPassword}'"
+        )
+        hass.wait_until_succeeds(
+            "mosquitto_pub -V mqttv311 -t home-assistant/test -u homeassistant -P '${mqttPassword}' -m let_there_be_light"
+        )
+        assert '"state": "on"' in hass.succeed(
+            "curl http://localhost:8123/api/states/binary_sensor.mqtt_binary_sensor -H 'x-ha-access: ${apiPassword}'"
+        )
+    with subtest("Toggle a binary sensor using hass-cli"):
+        assert '"state": "on"' in hass.succeed(
+            "${hassCli} --output json state get binary_sensor.mqtt_binary_sensor"
+        )
+        hass.succeed(
+            "${hassCli} state edit binary_sensor.mqtt_binary_sensor --json='{\"state\": \"off\"}'"
+        )
+        assert '"state": "off"' in hass.succeed(
+            "curl http://localhost:8123/api/states/binary_sensor.mqtt_binary_sensor -H 'x-ha-access: ${apiPassword}'"
+        )
+    with subtest("Print log to ease debugging"):
+        output_log = hass.succeed("cat ${configDir}/home-assistant.log")
+        print("\n### home-assistant.log ###\n")
+        print(output_log + "\n")
 
-    # Check that no errors were logged
-    $hass->fail("cat ${configDir}/home-assistant.log | grep -qF ERROR");
+    with subtest("Check that no errors were logged"):
+        assert "ERROR" not in output_log
   '';
 })
diff --git a/nixos/tests/ihatemoney.nix b/nixos/tests/ihatemoney.nix
new file mode 100644
index 00000000000..14db17fe5e6
--- /dev/null
+++ b/nixos/tests/ihatemoney.nix
@@ -0,0 +1,52 @@
+{ system ? builtins.currentSystem
+, config ? {}
+, pkgs ? import ../.. { inherit system config; }
+}:
+
+let
+  inherit (import ../lib/testing.nix { inherit system pkgs; }) makeTest;
+in
+map (
+  backend: makeTest {
+    name = "ihatemoney-${backend}";
+    machine = { lib, ... }: {
+      services.ihatemoney = {
+        enable = true;
+        enablePublicProjectCreation = true;
+        inherit backend;
+        uwsgiConfig = {
+          http = ":8000";
+        };
+      };
+      boot.cleanTmpDir = true;
+      # ihatemoney needs a local smtp server otherwise project creation just crashes
+      services.opensmtpd = {
+        enable = true;
+        serverConfiguration = ''
+          listen on lo
+          action foo relay
+          match from any for any action foo
+        '';
+      };
+    };
+    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");
+    '';
+  }
+) [ "sqlite" "postgresql" ]
diff --git a/nixos/tests/initdb.nix b/nixos/tests/initdb.nix
deleted file mode 100644
index 749d7857a13..00000000000
--- a/nixos/tests/initdb.nix
+++ /dev/null
@@ -1,26 +0,0 @@
-let
-  pkgs = import <nixpkgs> { };
-in
-with import <nixpkgs/nixos/lib/testing.nix> { inherit pkgs; system = builtins.currentSystem; };
-with pkgs.lib;
-
-makeTest {
-    name = "pg-initdb";
-
-    machine = {...}:
-      {
-        documentation.enable = false;
-        services.postgresql.enable = true;
-        services.postgresql.package = pkgs.postgresql_9_6;
-        environment.pathsToLink = [
-          "/share/postgresql"
-        ];
-      };
-
-    testScript = ''
-      $machine->start;
-      $machine->succeed("sudo -u postgres initdb -D /tmp/testpostgres2");
-      $machine->shutdown;
-    '';
-
-  }
\ No newline at end of file
diff --git a/nixos/tests/kafka.nix b/nixos/tests/kafka.nix
index 48ca98da8fa..f3de24e873b 100644
--- a/nixos/tests/kafka.nix
+++ b/nixos/tests/kafka.nix
@@ -3,11 +3,10 @@
   pkgs ? import ../.. { inherit system config; }
 }:
 
-with import ../lib/testing.nix { inherit system pkgs; };
 with pkgs.lib;
 
 let
-  makeKafkaTest = name: kafkaPackage: (makeTest {
+  makeKafkaTest = name: kafkaPackage: (import ./make-test-python.nix ({
     inherit name;
     meta = with pkgs.stdenv.lib.maintainers; {
       maintainers = [ nequissimus ];
@@ -45,24 +44,40 @@ let
     };
 
     testScript = ''
-      startAll;
+      start_all()
 
-      $zookeeper1->waitForUnit("default.target");
-      $zookeeper1->waitForUnit("zookeeper.service");
-      $zookeeper1->waitForOpenPort(2181);
+      zookeeper1.wait_for_unit("default.target")
+      zookeeper1.wait_for_unit("zookeeper.service")
+      zookeeper1.wait_for_open_port(2181)
 
-      $kafka->waitForUnit("default.target");
-      $kafka->waitForUnit("apache-kafka.service");
-      $kafka->waitForOpenPort(9092);
+      kafka.wait_for_unit("default.target")
+      kafka.wait_for_unit("apache-kafka.service")
+      kafka.wait_for_open_port(9092)
 
-      $kafka->waitUntilSucceeds("${kafkaPackage}/bin/kafka-topics.sh --create --zookeeper zookeeper1:2181 --partitions 1 --replication-factor 1 --topic testtopic");
-      $kafka->mustSucceed("echo 'test 1' | ${kafkaPackage}/bin/kafka-console-producer.sh --broker-list localhost:9092 --topic testtopic");
+      kafka.wait_until_succeeds(
+          "${kafkaPackage}/bin/kafka-topics.sh --create "
+          + "--zookeeper zookeeper1:2181 --partitions 1 "
+          + "--replication-factor 1 --topic testtopic"
+      )
+      kafka.succeed(
+          "echo 'test 1' | "
+          + "${kafkaPackage}/bin/kafka-console-producer.sh "
+          + "--broker-list localhost:9092 --topic testtopic"
+      )
     '' + (if name == "kafka_0_9" then ''
-      $kafka->mustSucceed("${kafkaPackage}/bin/kafka-console-consumer.sh --zookeeper zookeeper1:2181 --topic testtopic --from-beginning --max-messages 1 | grep 'test 1'");
+      assert "test 1" in kafka.succeed(
+          "${kafkaPackage}/bin/kafka-console-consumer.sh "
+          + "--zookeeper zookeeper1:2181 --topic testtopic "
+          + "--from-beginning --max-messages 1"
+      )
     '' else ''
-      $kafka->mustSucceed("${kafkaPackage}/bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic testtopic --from-beginning --max-messages 1 | grep 'test 1'");
+      assert "test 1" in kafka.succeed(
+          "${kafkaPackage}/bin/kafka-console-consumer.sh "
+          + "--bootstrap-server localhost:9092 --topic testtopic "
+          + "--from-beginning --max-messages 1"
+      )
     '');
-  });
+  }) {});
 
 in with pkgs; {
   kafka_0_9  = makeKafkaTest "kafka_0_9"  apacheKafka_0_9;
@@ -74,4 +89,5 @@ in with pkgs; {
   kafka_2_1  = makeKafkaTest "kafka_2_1"  apacheKafka_2_1;
   kafka_2_2  = makeKafkaTest "kafka_2_2"  apacheKafka_2_2;
   kafka_2_3  = makeKafkaTest "kafka_2_3"  apacheKafka_2_3;
+  kafka_2_4  = makeKafkaTest "kafka_2_4"  apacheKafka_2_4;
 }
diff --git a/nixos/tests/postgresql.nix b/nixos/tests/postgresql.nix
index e71c3888288..3201e22555e 100644
--- a/nixos/tests/postgresql.nix
+++ b/nixos/tests/postgresql.nix
@@ -29,11 +29,15 @@ let
 
     machine = {...}:
       {
-        services.postgresql.enable = true;
-        services.postgresql.package = postgresql-package;
+        services.postgresql = {
+          enable = true;
+          package = postgresql-package;
+        };
 
-        services.postgresqlBackup.enable = true;
-        services.postgresqlBackup.databases = optional (!backup-all) "postgres";
+        services.postgresqlBackup = {
+          enable = true;
+          databases = optional (!backup-all) "postgres";
+        };
       };
 
     testScript = let
@@ -49,23 +53,32 @@ let
       machine.start()
       machine.wait_for_unit("postgresql")
 
-      # postgresql should be available just after unit start
-      machine.succeed(
-          "cat ${test-sql} | sudo -u postgres psql"
-      )
-      machine.shutdown()  # make sure that postgresql survive restart (bug #1735)
-      time.sleep(2)
-      machine.start()
-      machine.wait_for_unit("postgresql")
+      with subtest("Postgresql is available just after unit start"):
+          machine.succeed(
+              "cat ${test-sql} | sudo -u postgres psql"
+          )
+
+      with subtest("Postgresql survives restart (bug #1735)"):
+          machine.shutdown()
+          time.sleep(2)
+          machine.start()
+          machine.wait_for_unit("postgresql")
+
       machine.fail(check_count("SELECT * FROM sth;", 3))
       machine.succeed(check_count("SELECT * FROM sth;", 5))
       machine.fail(check_count("SELECT * FROM sth;", 4))
       machine.succeed(check_count("SELECT xpath('/test/text()', doc) FROM xmltest;", 1))
 
-      # Check backup service
-      machine.succeed("systemctl start ${backupService}.service")
-      machine.succeed("zcat /var/backup/postgresql/${backupName}.sql.gz | grep '<test>ok</test>'")
-      machine.succeed("stat -c '%a' /var/backup/postgresql/${backupName}.sql.gz | grep 600")
+      with subtest("Backup service works"):
+          machine.succeed(
+              "systemctl start ${backupService}.service",
+              "zcat /var/backup/postgresql/${backupName}.sql.gz | grep '<test>ok</test>'",
+              "stat -c '%a' /var/backup/postgresql/${backupName}.sql.gz | grep 600",
+          )
+
+      with subtest("Initdb works"):
+          machine.succeed("sudo -u postgres initdb -D /tmp/testpostgres2")
+
       machine.shutdown()
     '';
 
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/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"
+            )
+      '';
+    }
+)