about summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/administration/declarative-containers.section.md2
-rw-r--r--nixos/doc/manual/default.nix2
-rw-r--r--nixos/doc/manual/from_md/administration/declarative-containers.section.xml2
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-2111.section.xml7
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-2205.section.xml198
-rw-r--r--nixos/doc/manual/man-nixos-rebuild.xml8
-rw-r--r--nixos/doc/manual/release-notes/rl-2111.section.md2
-rw-r--r--nixos/doc/manual/release-notes/rl-2205.section.md72
-rw-r--r--nixos/lib/eval-config.nix50
-rw-r--r--nixos/lib/make-options-doc/default.nix130
-rw-r--r--nixos/lib/make-options-doc/generateAsciiDoc.py37
-rw-r--r--nixos/lib/make-options-doc/generateCommonMark.py27
-rw-r--r--nixos/lib/make-options-doc/options-to-docbook.xsl4
-rw-r--r--nixos/lib/make-options-doc/optionsJSONtoXML.nix6
-rw-r--r--nixos/lib/make-options-doc/sortXML.py1
-rw-r--r--nixos/lib/make-squashfs.nix9
-rw-r--r--nixos/lib/systemd-lib.nix (renamed from nixos/modules/system/boot/systemd-lib.nix)0
-rw-r--r--nixos/lib/systemd-unit-options.nix (renamed from nixos/modules/system/boot/systemd-unit-options.nix)4
-rw-r--r--nixos/lib/test-driver/default.nix32
-rw-r--r--nixos/lib/test-driver/setup.py13
-rwxr-xr-xnixos/lib/test-driver/test_driver/__init__.py100
-rw-r--r--nixos/lib/test-driver/test_driver/driver.py161
-rw-r--r--nixos/lib/test-driver/test_driver/logger.py101
-rw-r--r--[-rwxr-xr-x]nixos/lib/test-driver/test_driver/machine.py (renamed from nixos/lib/test-driver/test-driver.py)424
-rw-r--r--nixos/lib/test-driver/test_driver/vlan.py58
-rw-r--r--nixos/lib/testing-python.nix73
-rw-r--r--nixos/lib/utils.nix7
-rw-r--r--nixos/modules/config/networking.nix8
-rw-r--r--nixos/modules/config/system-path.nix22
-rw-r--r--nixos/modules/hardware/cpu/intel-sgx.nix47
-rw-r--r--nixos/modules/hardware/gpgsmartcards.nix37
-rw-r--r--nixos/modules/hardware/keyboard/zsa.nix3
-rw-r--r--nixos/modules/hardware/pcmcia.nix1
-rw-r--r--nixos/modules/hardware/system-76.nix8
-rw-r--r--nixos/modules/misc/documentation.nix20
-rw-r--r--nixos/modules/misc/extra-arguments.nix4
-rw-r--r--nixos/modules/misc/version.nix4
-rw-r--r--nixos/modules/module-list.nix11
-rw-r--r--nixos/modules/programs/captive-browser.nix26
-rw-r--r--nixos/modules/programs/dconf.nix2
-rw-r--r--nixos/modules/programs/gnupg.nix1
-rw-r--r--nixos/modules/programs/qt5ct.nix2
-rw-r--r--nixos/modules/programs/ssh.nix11
-rw-r--r--nixos/modules/programs/zsh/zsh.nix4
-rw-r--r--nixos/modules/security/acme.nix307
-rw-r--r--nixos/modules/security/acme.xml163
-rw-r--r--nixos/modules/security/dhparams.nix6
-rw-r--r--nixos/modules/security/pam.nix54
-rw-r--r--nixos/modules/security/systemd-confinement.nix6
-rw-r--r--nixos/modules/services/audio/mpdscribble.nix13
-rw-r--r--nixos/modules/services/audio/snapserver.nix4
-rw-r--r--nixos/modules/services/backup/duplicati.nix35
-rw-r--r--nixos/modules/services/backup/restic.nix4
-rw-r--r--nixos/modules/services/backup/tarsnap.nix35
-rw-r--r--nixos/modules/services/cluster/hadoop/default.nix14
-rw-r--r--nixos/modules/services/cluster/kubernetes/addon-manager.nix2
-rw-r--r--nixos/modules/services/cluster/kubernetes/addons/dashboard.nix332
-rw-r--r--nixos/modules/services/cluster/kubernetes/addons/dns.nix6
-rw-r--r--nixos/modules/services/cluster/kubernetes/apiserver.nix7
-rw-r--r--nixos/modules/services/cluster/kubernetes/controller-manager.nix6
-rw-r--r--nixos/modules/services/cluster/kubernetes/default.nix5
-rw-r--r--nixos/modules/services/cluster/kubernetes/kubelet.nix7
-rw-r--r--nixos/modules/services/cluster/kubernetes/proxy.nix4
-rw-r--r--nixos/modules/services/cluster/kubernetes/scheduler.nix4
-rw-r--r--nixos/modules/services/computing/slurm/slurm.nix9
-rw-r--r--nixos/modules/services/continuous-integration/buildbot/master.nix4
-rw-r--r--nixos/modules/services/continuous-integration/buildbot/worker.nix4
-rw-r--r--nixos/modules/services/continuous-integration/github-runner.nix60
-rw-r--r--nixos/modules/services/continuous-integration/gocd-agent/default.nix12
-rw-r--r--nixos/modules/services/continuous-integration/gocd-server/default.nix17
-rw-r--r--nixos/modules/services/databases/couchdb.nix16
-rw-r--r--nixos/modules/services/databases/hbase.nix21
-rw-r--r--nixos/modules/services/databases/influxdb2.nix19
-rw-r--r--nixos/modules/services/databases/mysql.nix380
-rw-r--r--nixos/modules/services/databases/neo4j.nix8
-rw-r--r--nixos/modules/services/databases/postgresql.nix8
-rw-r--r--nixos/modules/services/databases/postgresql.xml86
-rw-r--r--nixos/modules/services/databases/redis.nix488
-rw-r--r--nixos/modules/services/desktops/gnome/glib-networking.nix2
-rw-r--r--nixos/modules/services/desktops/gvfs.nix4
-rw-r--r--nixos/modules/services/development/hoogle.nix1
-rw-r--r--nixos/modules/services/games/quake3-server.nix1
-rw-r--r--nixos/modules/services/games/terraria.nix5
-rw-r--r--nixos/modules/services/hardware/rasdaemon.nix1
-rw-r--r--nixos/modules/services/hardware/spacenavd.nix1
-rw-r--r--nixos/modules/services/hardware/tcsd.nix6
-rw-r--r--nixos/modules/services/hardware/thinkfan.nix4
-rw-r--r--nixos/modules/services/logging/filebeat.nix253
-rw-r--r--nixos/modules/services/logging/journalbeat.nix19
-rw-r--r--nixos/modules/services/mail/maddy.nix247
-rw-r--r--nixos/modules/services/mail/rspamd.nix5
-rw-r--r--nixos/modules/services/misc/airsonic.nix5
-rw-r--r--nixos/modules/services/misc/dysnomia.nix6
-rw-r--r--nixos/modules/services/misc/etcd.nix9
-rw-r--r--nixos/modules/services/misc/exhibitor.nix5
-rw-r--r--nixos/modules/services/misc/gitea.nix13
-rw-r--r--nixos/modules/services/misc/gitlab.nix5
-rw-r--r--nixos/modules/services/misc/gitweb.nix1
-rw-r--r--nixos/modules/services/misc/gogs.nix5
-rw-r--r--nixos/modules/services/misc/headphones.nix4
-rw-r--r--nixos/modules/services/misc/matrix-appservice-discord.nix4
-rw-r--r--nixos/modules/services/misc/matrix-synapse.nix36
-rw-r--r--nixos/modules/services/misc/mautrix-telegram.nix2
-rw-r--r--nixos/modules/services/misc/mediatomb.nix10
-rw-r--r--nixos/modules/services/misc/moonraker.nix4
-rw-r--r--nixos/modules/services/misc/mwlib.nix12
-rw-r--r--nixos/modules/services/misc/nitter.nix2
-rw-r--r--nixos/modules/services/misc/nix-daemon.nix52
-rw-r--r--nixos/modules/services/misc/rippled.nix6
-rw-r--r--nixos/modules/services/misc/sickbeard.nix4
-rw-r--r--nixos/modules/services/misc/sourcehut/builds.nix6
-rw-r--r--nixos/modules/services/misc/sourcehut/default.nix1455
-rw-r--r--nixos/modules/services/misc/sourcehut/dispatch.nix4
-rw-r--r--nixos/modules/services/misc/sourcehut/git.nix4
-rw-r--r--nixos/modules/services/misc/sourcehut/hg.nix4
-rw-r--r--nixos/modules/services/misc/sourcehut/hub.nix4
-rw-r--r--nixos/modules/services/misc/sourcehut/lists.nix4
-rw-r--r--nixos/modules/services/misc/sourcehut/man.nix4
-rw-r--r--nixos/modules/services/misc/sourcehut/meta.nix4
-rw-r--r--nixos/modules/services/misc/sourcehut/paste.nix4
-rw-r--r--nixos/modules/services/misc/sourcehut/service.nix431
-rw-r--r--nixos/modules/services/misc/sourcehut/sourcehut.xml24
-rw-r--r--nixos/modules/services/misc/sourcehut/todo.nix4
-rw-r--r--nixos/modules/services/misc/subsonic.nix9
-rw-r--r--nixos/modules/services/misc/zigbee2mqtt.nix11
-rw-r--r--nixos/modules/services/misc/zoneminder.nix2
-rw-r--r--nixos/modules/services/monitoring/collectd.nix21
-rw-r--r--nixos/modules/services/monitoring/grafana.nix1
-rw-r--r--nixos/modules/services/monitoring/graphite.nix12
-rw-r--r--nixos/modules/services/monitoring/parsedmarc.nix6
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/fastly.nix6
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/nginx.nix10
-rw-r--r--nixos/modules/services/monitoring/smartd.nix4
-rw-r--r--nixos/modules/services/monitoring/thanos.nix3
-rw-r--r--nixos/modules/services/monitoring/uptime.nix10
-rw-r--r--nixos/modules/services/monitoring/zabbix-proxy.nix8
-rw-r--r--nixos/modules/services/monitoring/zabbix-server.nix8
-rw-r--r--nixos/modules/services/network-filesystems/glusterfs.nix2
-rw-r--r--nixos/modules/services/network-filesystems/openafs/server.nix2
-rw-r--r--nixos/modules/services/networking/adguardhome.nix2
-rw-r--r--nixos/modules/services/networking/amuled.nix6
-rw-r--r--nixos/modules/services/networking/ddclient.nix2
-rw-r--r--nixos/modules/services/networking/dhcpcd.nix9
-rw-r--r--nixos/modules/services/networking/dhcpd.nix95
-rw-r--r--nixos/modules/services/networking/ergo.nix6
-rw-r--r--nixos/modules/services/networking/eternal-terminal.nix2
-rw-r--r--nixos/modules/services/networking/firewall.nix1
-rw-r--r--nixos/modules/services/networking/freeradius.nix2
-rw-r--r--nixos/modules/services/networking/jibri/default.nix2
-rw-r--r--nixos/modules/services/networking/jitsi-videobridge.nix2
-rw-r--r--nixos/modules/services/networking/kea.nix6
-rw-r--r--nixos/modules/services/networking/ntopng.nix7
-rw-r--r--nixos/modules/services/networking/pleroma.nix11
-rw-r--r--nixos/modules/services/networking/prosody.xml2
-rw-r--r--nixos/modules/services/networking/quassel.nix6
-rw-r--r--nixos/modules/services/networking/quorum.nix4
-rw-r--r--nixos/modules/services/networking/stubby.nix220
-rw-r--r--nixos/modules/services/networking/syncthing.nix24
-rw-r--r--nixos/modules/services/networking/unifi.nix11
-rw-r--r--nixos/modules/services/networking/wasabibackend.nix6
-rw-r--r--nixos/modules/services/networking/wireguard.nix4
-rw-r--r--nixos/modules/services/networking/wpa_supplicant.nix4
-rw-r--r--nixos/modules/services/networking/xrdp.nix9
-rw-r--r--nixos/modules/services/search/kibana.nix6
-rw-r--r--nixos/modules/services/security/aesmd.nix227
-rw-r--r--nixos/modules/services/security/privacyidea.nix7
-rw-r--r--nixos/modules/services/security/tor.nix8
-rw-r--r--nixos/modules/services/security/vault.nix8
-rw-r--r--nixos/modules/services/torrent/peerflix.nix4
-rw-r--r--nixos/modules/services/torrent/rtorrent.nix4
-rw-r--r--nixos/modules/services/torrent/transmission.nix46
-rw-r--r--nixos/modules/services/video/epgstation/default.nix8
-rw-r--r--nixos/modules/services/video/unifi-video.nix4
-rw-r--r--nixos/modules/services/wayland/cage.nix2
-rw-r--r--nixos/modules/services/web-apps/discourse.nix19
-rw-r--r--nixos/modules/services/web-apps/discourse.xml4
-rw-r--r--nixos/modules/services/web-apps/dokuwiki.nix4
-rw-r--r--nixos/modules/services/web-apps/galene.nix6
-rw-r--r--nixos/modules/services/web-apps/hedgedoc.nix2
-rw-r--r--nixos/modules/services/web-apps/invidious.nix3
-rw-r--r--nixos/modules/services/web-apps/jitsi-meet.xml4
-rw-r--r--nixos/modules/services/web-apps/keycloak.nix5
-rw-r--r--nixos/modules/services/web-apps/matomo.nix14
-rw-r--r--nixos/modules/services/web-apps/nextcloud.nix5
-rw-r--r--nixos/modules/services/web-apps/peertube.nix19
-rw-r--r--nixos/modules/services/web-apps/pgpkeyserver-lite.nix5
-rw-r--r--nixos/modules/services/web-apps/powerdns-admin.nix149
-rw-r--r--nixos/modules/services/web-apps/tt-rss.nix8
-rw-r--r--nixos/modules/services/web-apps/youtrack.nix1
-rw-r--r--nixos/modules/services/web-apps/zabbix.nix8
-rw-r--r--nixos/modules/services/web-servers/apache-httpd/default.nix13
-rw-r--r--nixos/modules/services/web-servers/apache-httpd/vhost-options.nix7
-rw-r--r--nixos/modules/services/web-servers/caddy/default.nix323
-rw-r--r--nixos/modules/services/web-servers/caddy/vhost-options.nix71
-rw-r--r--nixos/modules/services/web-servers/lighttpd/collectd.nix6
-rw-r--r--nixos/modules/services/web-servers/nginx/default.nix28
-rw-r--r--nixos/modules/services/web-servers/nginx/location-options.nix2
-rw-r--r--nixos/modules/services/web-servers/nginx/vhost-options.nix20
-rw-r--r--nixos/modules/services/x11/desktop-managers/pantheon.nix6
-rw-r--r--nixos/modules/services/x11/desktop-managers/pantheon.xml8
-rw-r--r--nixos/modules/services/x11/desktop-managers/xfce.nix2
-rw-r--r--nixos/modules/services/x11/display-managers/default.nix14
-rw-r--r--nixos/modules/services/x11/hardware/synaptics.nix7
-rw-r--r--nixos/modules/services/x11/picom.nix14
-rw-r--r--nixos/modules/services/x11/window-managers/xmonad.nix2
-rw-r--r--nixos/modules/system/activation/activation-script.nix1
-rw-r--r--nixos/modules/system/activation/switch-to-configuration.pl154
-rw-r--r--nixos/modules/system/activation/top-level.nix7
-rw-r--r--nixos/modules/system/boot/networkd.nix52
-rw-r--r--nixos/modules/system/boot/plymouth.nix8
-rw-r--r--nixos/modules/system/boot/stage-1-init.sh28
-rw-r--r--nixos/modules/system/boot/stage-1.nix4
-rwxr-xr-x[-rw-r--r--]nixos/modules/system/boot/stage-2-init.sh1
-rw-r--r--nixos/modules/system/boot/systemd-nspawn.nix6
-rw-r--r--nixos/modules/system/boot/systemd.nix4
-rw-r--r--nixos/modules/tasks/filesystems/zfs.nix5
-rw-r--r--nixos/modules/tasks/network-interfaces.nix4
-rw-r--r--nixos/modules/tasks/snapraid.nix2
-rw-r--r--nixos/modules/virtualisation/cri-o.nix4
-rw-r--r--nixos/modules/virtualisation/docker-rootless.nix98
-rw-r--r--nixos/modules/virtualisation/docker.nix37
-rw-r--r--nixos/modules/virtualisation/kubevirt.nix30
-rw-r--r--nixos/modules/virtualisation/nixos-containers.nix4
-rw-r--r--nixos/modules/virtualisation/podman/default.nix (renamed from nixos/modules/virtualisation/podman.nix)4
-rw-r--r--nixos/modules/virtualisation/podman/dnsname.nix (renamed from nixos/modules/virtualisation/podman-dnsname.nix)0
-rw-r--r--nixos/modules/virtualisation/podman/network-socket-ghostunnel.nix (renamed from nixos/modules/virtualisation/podman-network-socket-ghostunnel.nix)0
-rw-r--r--nixos/modules/virtualisation/podman/network-socket.nix (renamed from nixos/modules/virtualisation/podman-network-socket.nix)4
-rw-r--r--nixos/modules/virtualisation/qemu-vm.nix1
-rw-r--r--nixos/tests/acme.nix576
-rw-r--r--nixos/tests/aesmd.nix62
-rw-r--r--nixos/tests/all-tests.nix16
-rw-r--r--nixos/tests/brscan5.nix1
-rw-r--r--nixos/tests/collectd.nix33
-rw-r--r--nixos/tests/common/acme/client/default.nix6
-rw-r--r--nixos/tests/common/acme/server/default.nix5
-rw-r--r--nixos/tests/couchdb.nix3
-rw-r--r--nixos/tests/docker-rootless.nix41
-rw-r--r--nixos/tests/docker-tools.nix6
-rw-r--r--nixos/tests/elk.nix90
-rw-r--r--nixos/tests/hydra/default.nix2
-rw-r--r--nixos/tests/initrd-secrets.nix10
-rw-r--r--nixos/tests/kubernetes/base.nix1
-rw-r--r--nixos/tests/kubernetes/default.nix2
-rw-r--r--nixos/tests/kubernetes/dns.nix6
-rw-r--r--nixos/tests/kubernetes/e2e.nix2
-rw-r--r--nixos/tests/kubernetes/rbac.nix14
-rw-r--r--nixos/tests/maddy.nix58
-rw-r--r--nixos/tests/os-prober.nix6
-rw-r--r--nixos/tests/parsedmarc/default.nix27
-rw-r--r--nixos/tests/podman/default.nix (renamed from nixos/tests/podman.nix)18
-rw-r--r--nixos/tests/podman/dnsname.nix (renamed from nixos/tests/podman-dnsname.nix)4
-rw-r--r--nixos/tests/podman/tls-ghostunnel.nix (renamed from nixos/tests/podman-tls-ghostunnel.nix)4
-rw-r--r--nixos/tests/powerdns-admin.nix117
-rw-r--r--nixos/tests/prometheus-exporters.nix15
-rw-r--r--nixos/tests/redis.nix36
-rw-r--r--nixos/tests/sabnzbd.nix22
-rw-r--r--nixos/tests/samba-wsdd.nix2
-rw-r--r--nixos/tests/snapcast.nix8
-rw-r--r--nixos/tests/sourcehut.nix188
-rw-r--r--nixos/tests/switch-test.nix301
-rw-r--r--nixos/tests/systemd-networkd-dhcpserver-static-leases.nix81
-rw-r--r--nixos/tests/systemd.nix12
-rw-r--r--nixos/tests/txredisapi.nix10
-rw-r--r--nixos/tests/unifi.nix35
264 files changed, 7666 insertions, 2846 deletions
diff --git a/nixos/doc/manual/administration/declarative-containers.section.md b/nixos/doc/manual/administration/declarative-containers.section.md
index 273672fc10ca9..0d9d4017ed81b 100644
--- a/nixos/doc/manual/administration/declarative-containers.section.md
+++ b/nixos/doc/manual/administration/declarative-containers.section.md
@@ -9,7 +9,7 @@ containers.database =
   { config =
       { config, pkgs, ... }:
       { services.postgresql.enable = true;
-      services.postgresql.package = pkgs.postgresql_9_6;
+      services.postgresql.package = pkgs.postgresql_10;
       };
   };
 ```
diff --git a/nixos/doc/manual/default.nix b/nixos/doc/manual/default.nix
index 151743d9fb580..31b6da01c6bd0 100644
--- a/nixos/doc/manual/default.nix
+++ b/nixos/doc/manual/default.nix
@@ -161,7 +161,7 @@ let
 in rec {
   inherit generatedSources;
 
-  inherit (optionsDoc) optionsJSON optionsXML optionsDocBook;
+  inherit (optionsDoc) optionsJSON optionsDocBook;
 
   # Generate the NixOS manual.
   manualHTML = runCommand "nixos-manual-html"
diff --git a/nixos/doc/manual/from_md/administration/declarative-containers.section.xml b/nixos/doc/manual/from_md/administration/declarative-containers.section.xml
index a918314a2723e..7b35520d567bf 100644
--- a/nixos/doc/manual/from_md/administration/declarative-containers.section.xml
+++ b/nixos/doc/manual/from_md/administration/declarative-containers.section.xml
@@ -11,7 +11,7 @@ containers.database =
   { config =
       { config, pkgs, ... }:
       { services.postgresql.enable = true;
-      services.postgresql.package = pkgs.postgresql_9_6;
+      services.postgresql.package = pkgs.postgresql_10;
       };
   };
 </programlisting>
diff --git a/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml
index 6b706e4aeaa16..e2bda7604e48f 100644
--- a/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml
+++ b/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml
@@ -275,6 +275,13 @@
       </listitem>
       <listitem>
         <para>
+          <link xlink:href="https://maddy.email">maddy</link>, a
+          composable all-in-one mail server. Available as
+          <link xlink:href="options.html#opt-services.maddy.enable">services.maddy</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <link xlink:href="https://sr.ht">sourcehut</link>, a
           collection of tools useful for software development. Available
           as
diff --git a/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
index c84a3e3b01938..517a2e458aaea 100644
--- a/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
+++ b/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
@@ -14,13 +14,59 @@
   </itemizedlist>
   <section xml:id="sec-release-22.05-highlights">
     <title>Highlights</title>
-    <para>
-    </para>
+    <itemizedlist>
+      <listitem>
+        <para>
+          <literal>security.acme.defaults</literal> has been added to
+          simplify configuring settings for many certificates at once.
+          This also opens up the the option to use DNS-01 validation
+          when using <literal>enableACME</literal> on web server virtual
+          hosts (e.g.
+          <literal>services.nginx.virtualHosts.*.enableACME</literal>).
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          PHP 8.1 is now available
+        </para>
+      </listitem>
+    </itemizedlist>
   </section>
   <section xml:id="sec-release-22.05-new-services">
     <title>New Services</title>
-    <para>
-    </para>
+    <itemizedlist>
+      <listitem>
+        <para>
+          <link xlink:href="https://github.com/intel/linux-sgx#install-the-intelr-sgx-psw">aesmd</link>,
+          the Intel SGX Architectural Enclave Service Manager. Available
+          as
+          <link linkend="opt-services.aesmd.enable">services.aesmd</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://docs.docker.com/engine/security/rootless/">rootless
+          Docker</link>, a <literal>systemd --user</literal> Docker
+          service which runs without root permissions. Available as
+          <link xlink:href="options.html#opt-virtualisation.docker.rootless.enable">virtualisation.docker.rootless.enable</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-overview.html">filebeat</link>,
+          a lightweight shipper for forwarding and centralizing log
+          data. Available as
+          <link linkend="opt-services.filebeat.enable">services.filebeat</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://github.com/ngoduykhanh/PowerDNS-Admin">PowerDNS-Admin</link>,
+          a web interface for the PowerDNS server. Available at
+          <link xlink:href="options.html#opt-services.powerdns-admin.enable">services.powerdns-admin</link>.
+        </para>
+      </listitem>
+    </itemizedlist>
   </section>
   <section xml:id="sec-release-22.05-incompatibilities">
     <title>Backward Incompatibilities</title>
@@ -59,6 +105,12 @@
       </listitem>
       <listitem>
         <para>
+          <literal>services.kubernetes.addons.dashboard</literal> was
+          removed due to it being an outdated version.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           The <literal>wafHook</literal> hook now honors
           <literal>NIX_BUILD_CORES</literal> when
           <literal>enableParallelBuilding</literal> is not set
@@ -75,11 +127,145 @@
           release based on GTK+3 and Python 3.
         </para>
       </listitem>
+      <listitem>
+        <para>
+          The <literal>writers.writePython2</literal> and corresponding
+          <literal>writers.writePython2Bin</literal> convenience
+          functions to create executable Python 2 scripts in the store
+          were removed in preparation of removal of the Python 2
+          interpreter. Scripts have to be converted to Python 3 for use
+          with <literal>writers.writePython3</literal> or
+          <literal>writers.writePyPy2</literal> needs to be used.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          If you previously used
+          <literal>/etc/docker/daemon.json</literal>, you need to
+          incorporate the changes into the new option
+          <literal>virtualisation.docker.daemon.settings</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The <literal>autorestic</literal> package has been upgraded
+          from 1.3.0 to 1.5.0 which introduces breaking changes in
+          config file, check
+          <link xlink:href="https://autorestic.vercel.app/migration/1.4_1.5">their
+          migration guide</link> for more details.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          For <literal>pkgs.python3.pkgs.ipython</literal>, its direct
+          dependency
+          <literal>pkgs.python3.pkgs.matplotlib-inline</literal> (which
+          is really an adapter to integrate matplotlib in ipython if it
+          is installed) does not depend on
+          <literal>pkgs.python3.pkgs.matplotlib</literal> anymore. This
+          is closer to a non-Nix install of ipython. This has the added
+          benefit to reduce the closure size of
+          <literal>ipython</literal> from ~400MB to ~160MB (including
+          ~100MB for python itself).
+        </para>
+      </listitem>
     </itemizedlist>
   </section>
   <section xml:id="sec-release-22.05-notable-changes">
     <title>Other Notable Changes</title>
-    <para>
-    </para>
+    <itemizedlist>
+      <listitem>
+        <para>
+          The option
+          <link linkend="opt-services.redis.servers">services.redis.servers</link>
+          was added to support per-application
+          <literal>redis-server</literal> which is more secure since
+          Redis databases are only mere key prefixes without any
+          configuration or ACL of their own. Backward-compatibility is
+          preserved by mapping old
+          <literal>services.redis.settings</literal> to
+          <literal>services.redis.servers.&quot;&quot;.settings</literal>,
+          but you are strongly encouraged to name each
+          <literal>redis-server</literal> instance after the application
+          using it, instead of keeping that nameless one. Except for the
+          nameless
+          <literal>services.redis.servers.&quot;&quot;</literal> still
+          accessible at <literal>127.0.0.1:6379</literal>, and to the
+          members of the Unix group <literal>redis</literal> through the
+          Unix socket <literal>/run/redis/redis.sock</literal>, all
+          other <literal>services.redis.servers.${serverName}</literal>
+          are only accessible by default to the members of the Unix
+          group <literal>redis-${serverName}</literal> through the Unix
+          socket <literal>/run/redis-${serverName}/redis.sock</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The
+          <literal>writers.writePyPy2</literal>/<literal>writers.writePyPy3</literal>
+          and corresponding
+          <literal>writers.writePyPy2Bin</literal>/<literal>writers.writePyPy3Bin</literal>
+          convenience functions to create executable Python 2/3 scripts
+          using the PyPy interpreter were added.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The <literal>influxdb2</literal> package was split into
+          <literal>influxdb2-server</literal> and
+          <literal>influxdb2-cli</literal>, matching the split that took
+          place upstream. A combined <literal>influxdb2</literal>
+          package is still provided in this release for backwards
+          compatibilty, but will be removed at a later date.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The <literal>services.unifi.openPorts</literal> option default
+          value of <literal>true</literal> is now deprecated and will be
+          changed to <literal>false</literal> in 22.11. Configurations
+          using this default will print a warning when rebuilt.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>security.acme</literal> certificates will now
+          correctly check for CA revokation before reaching their
+          minimum age.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          Removing domains from
+          <literal>security.acme.certs._name_.extraDomainNames</literal>
+          will now correctly remove those domains during rebuild/renew.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The option
+          <link linkend="opt-services.ssh.enableAskPassword">services.ssh.enableAskPassword</link>
+          was added, decoupling the setting of
+          <literal>SSH_ASKPASS</literal> from
+          <literal>services.xserver.enable</literal>. This allows easy
+          usage in non-X11 environments, e.g. Wayland.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The <literal>services.stubby</literal> module was converted to
+          a
+          <link xlink:href="https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md">settings-style</link>
+          configuration.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The option <literal>services.duplicati.dataDir</literal> has
+          been added to allow changing the location of duplicati’s
+          files.
+        </para>
+      </listitem>
+    </itemizedlist>
   </section>
 </section>
diff --git a/nixos/doc/manual/man-nixos-rebuild.xml b/nixos/doc/manual/man-nixos-rebuild.xml
index 0e0ea5d74b0b5..6c7fc57f8d83c 100644
--- a/nixos/doc/manual/man-nixos-rebuild.xml
+++ b/nixos/doc/manual/man-nixos-rebuild.xml
@@ -535,12 +535,8 @@
      </para>
 
      <para>
-      If <option>--build-host</option> is not explicitly specified,
-      <option>--build-host</option> will implicitly be set to the same value as
-      <option>--target-host</option>. So, if you only specify
-      <option>--target-host</option> both building and activation will take
-      place remotely (and no build artifacts will be copied to the local
-      machine).
+      If <option>--build-host</option> is not explicitly specified, building
+      will take place locally.
      </para>
 
      <para>
diff --git a/nixos/doc/manual/release-notes/rl-2111.section.md b/nixos/doc/manual/release-notes/rl-2111.section.md
index 48adc4ad33cba..2520d176096aa 100644
--- a/nixos/doc/manual/release-notes/rl-2111.section.md
+++ b/nixos/doc/manual/release-notes/rl-2111.section.md
@@ -74,6 +74,8 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - [PeerTube](https://joinpeertube.org/), developed by Framasoft, is the free and decentralized alternative to video platforms. Available at [services.peertube](options.html#opt-services.peertube.enable).
 
+- [maddy](https://maddy.email), a composable all-in-one mail server. Available as [services.maddy](options.html#opt-services.maddy.enable).
+
 - [sourcehut](https://sr.ht), a collection of tools useful for software development. Available as [services.sourcehut](options.html#opt-services.sourcehut.enable).
 
 - [ucarp](https://download.pureftpd.org/pub/ucarp/README), an userspace implementation of the Common Address Redundancy Protocol (CARP). Available as [networking.ucarp](options.html#opt-networking.ucarp.enable).
diff --git a/nixos/doc/manual/release-notes/rl-2205.section.md b/nixos/doc/manual/release-notes/rl-2205.section.md
index 45ed69cf1b031..cfe1130068c73 100644
--- a/nixos/doc/manual/release-notes/rl-2205.section.md
+++ b/nixos/doc/manual/release-notes/rl-2205.section.md
@@ -6,8 +6,22 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 ## Highlights {#sec-release-22.05-highlights}
 
+- `security.acme.defaults` has been added to simplify configuring
+  settings for many certificates at once. This also opens up the
+  the option to use DNS-01 validation when using `enableACME` on
+  web server virtual hosts (e.g. `services.nginx.virtualHosts.*.enableACME`).
+
+- PHP 8.1 is now available
+
 ## New Services {#sec-release-22.05-new-services}
 
+- [aesmd](https://github.com/intel/linux-sgx#install-the-intelr-sgx-psw), the Intel SGX Architectural Enclave Service Manager. Available as [services.aesmd](#opt-services.aesmd.enable).
+- [rootless Docker](https://docs.docker.com/engine/security/rootless/), a `systemd --user` Docker service which runs without root permissions. Available as [virtualisation.docker.rootless.enable](options.html#opt-virtualisation.docker.rootless.enable).
+
+- [filebeat](https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-overview.html), a lightweight shipper for forwarding and centralizing log data. Available as [services.filebeat](#opt-services.filebeat.enable).
+
+- [PowerDNS-Admin](https://github.com/ngoduykhanh/PowerDNS-Admin), a web interface for the PowerDNS server. Available at [services.powerdns-admin](options.html#opt-services.powerdns-admin.enable).
+
 ## Backward Incompatibilities {#sec-release-22.05-incompatibilities}
 
 - `pkgs.ghc` now refers to `pkgs.targetPackages.haskellPackages.ghc`.
@@ -27,9 +41,67 @@ In addition to numerous new and upgraded packages, this release has the followin
   org-contrib, refer to the ones in `pkgs.emacsPackages.elpaPackages` and
   `pkgs.emacsPackages.nongnuPackages` where the new versions will release.
 
+- `services.kubernetes.addons.dashboard` was removed due to it being an outdated version.
+
 - The `wafHook` hook now honors `NIX_BUILD_CORES` when `enableParallelBuilding` is not set explicitly. Packages can restore the old behaviour by setting `enableParallelBuilding=false`.
 
 - `pkgs.claws-mail-gtk2`, representing Claws Mail's older release version three, was removed in order to get rid of Python 2.
   Please switch to `claws-mail`, which is Claws Mail's latest release based on GTK+3 and Python 3.
 
+- The `writers.writePython2` and corresponding `writers.writePython2Bin` convenience functions to create executable Python 2 scripts in the store were removed in preparation of removal of the Python 2 interpreter.
+  Scripts have to be converted to Python 3 for use with `writers.writePython3` or `writers.writePyPy2` needs to be used.
+
+- If you previously used `/etc/docker/daemon.json`, you need to incorporate the changes into the new option `virtualisation.docker.daemon.settings`.
+
+- The `autorestic` package has been upgraded from 1.3.0 to 1.5.0 which introduces breaking changes in config file, check [their migration guide](https://autorestic.vercel.app/migration/1.4_1.5) for more details.
+
+- For `pkgs.python3.pkgs.ipython`, its direct dependency `pkgs.python3.pkgs.matplotlib-inline`
+  (which is really an adapter to integrate matplotlib in ipython if it is installed) does
+  not depend on `pkgs.python3.pkgs.matplotlib` anymore.
+  This is closer to a non-Nix install of ipython.
+  This has the added benefit to reduce the closure size of `ipython` from ~400MB to ~160MB
+  (including ~100MB for python itself).
+
 ## Other Notable Changes {#sec-release-22.05-notable-changes}
+
+- The option [services.redis.servers](#opt-services.redis.servers) was added
+  to support per-application `redis-server` which is more secure since Redis databases
+  are only mere key prefixes without any configuration or ACL of their own.
+  Backward-compatibility is preserved by mapping old `services.redis.settings`
+  to `services.redis.servers."".settings`, but you are strongly encouraged
+  to name each `redis-server` instance after the application using it,
+  instead of keeping that nameless one.
+  Except for the nameless `services.redis.servers.""`
+  still accessible at `127.0.0.1:6379`,
+  and to the members of the Unix group `redis`
+  through the Unix socket `/run/redis/redis.sock`,
+  all other `services.redis.servers.${serverName}`
+  are only accessible by default
+  to the members of the Unix group `redis-${serverName}`
+  through the Unix socket `/run/redis-${serverName}/redis.sock`.
+
+- The `writers.writePyPy2`/`writers.writePyPy3` and corresponding `writers.writePyPy2Bin`/`writers.writePyPy3Bin` convenience functions to create executable Python 2/3 scripts using the PyPy interpreter were added.
+
+- The `influxdb2` package was split into `influxdb2-server` and
+  `influxdb2-cli`, matching the split that took place upstream. A
+  combined `influxdb2` package is still provided in this release for
+  backwards compatibilty, but will be removed at a later date.
+
+- The `services.unifi.openPorts` option default value of `true` is now deprecated and will be changed to `false` in 22.11.
+  Configurations using this default will print a warning when rebuilt.
+
+- `security.acme` certificates will now correctly check for CA
+  revokation before reaching their minimum age.
+
+- Removing domains from `security.acme.certs._name_.extraDomainNames`
+  will now correctly remove those domains during rebuild/renew.
+
+- The option
+  [services.ssh.enableAskPassword](#opt-services.ssh.enableAskPassword) was
+  added, decoupling the setting of `SSH_ASKPASS` from
+  `services.xserver.enable`. This allows easy usage in non-X11 environments,
+  e.g. Wayland.
+
+- The `services.stubby` module was converted to a [settings-style](https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md) configuration.
+
+- The option `services.duplicati.dataDir` has been added to allow changing the location of duplicati's files.
diff --git a/nixos/lib/eval-config.nix b/nixos/lib/eval-config.nix
index 74b52daa3c8eb..62d09b8173bda 100644
--- a/nixos/lib/eval-config.nix
+++ b/nixos/lib/eval-config.nix
@@ -8,6 +8,7 @@
 # as subcomponents (e.g. the container feature, or nixops if network
 # expressions are ever made modular at the top level) can just use
 # types.submodule instead of using eval-config.nix
+evalConfigArgs@
 { # !!! system can be set modularly, would be nice to remove
   system ? builtins.currentSystem
 , # !!! is this argument needed any more? The pkgs argument can
@@ -28,7 +29,7 @@
                  in if e == "" then [] else [(import e)]
 }:
 
-let extraArgs_ = extraArgs; pkgs_ = pkgs;
+let pkgs_ = pkgs;
 in
 
 let
@@ -51,28 +52,49 @@ let
     };
   };
 
-  noUserModules = lib.evalModules {
-    inherit prefix check;
-    modules = baseModules ++ extraModules ++ [ pkgsModule ];
-    args = extraArgs;
+  withWarnings = x:
+    lib.warnIf (evalConfigArgs?extraArgs) "The extraArgs argument to eval-config.nix is deprecated. Please set config._module.args instead."
+    lib.warnIf (evalConfigArgs?check) "The check argument to eval-config.nix is deprecated. Please set config._module.check instead."
+    x;
+
+  legacyModules =
+    lib.optional (evalConfigArgs?extraArgs) {
+      config = {
+        _module.args = extraArgs;
+      };
+    }
+    ++ lib.optional (evalConfigArgs?check) {
+      config = {
+        _module.check = lib.mkDefault check;
+      };
+    };
+  allUserModules = modules ++ legacyModules;
+
+  noUserModules = lib.evalModules ({
+    inherit prefix;
+    modules = baseModules ++ extraModules ++ [ pkgsModule modulesModule ];
     specialArgs =
       { modulesPath = builtins.toString ../modules; } // specialArgs;
-  };
+  });
 
-  # These are the extra arguments passed to every module.  In
-  # particular, Nixpkgs is passed through the "pkgs" argument.
-  extraArgs = extraArgs_ // {
-    inherit noUserModules baseModules extraModules modules;
+  # Extra arguments that are useful for constructing a similar configuration.
+  modulesModule = {
+    config = {
+      _module.args = {
+        inherit noUserModules baseModules extraModules modules;
+      };
+    };
   };
 
-in rec {
+  nixosWithUserModules = noUserModules.extendModules { modules = allUserModules; };
+
+in withWarnings {
 
   # Merge the option definitions in all modules, forming the full
   # system configuration.
-  inherit (noUserModules.extendModules { inherit modules; })
-    config options _module type;
+  inherit (nixosWithUserModules) config options _module type;
 
   inherit extraArgs;
 
-  inherit (_module.args) pkgs;
+  inherit (nixosWithUserModules._module.args) pkgs;
 }
diff --git a/nixos/lib/make-options-doc/default.nix b/nixos/lib/make-options-doc/default.nix
index e058e70f3888e..44bc25be92384 100644
--- a/nixos/lib/make-options-doc/default.nix
+++ b/nixos/lib/make-options-doc/default.nix
@@ -24,18 +24,25 @@
 }:
 
 let
-  # Replace functions by the string <function>
-  substFunction = x:
-    if builtins.isAttrs x then lib.mapAttrs (name: substFunction) x
-    else if builtins.isList x then map substFunction x
+  # Make a value safe for JSON. Functions are replaced by the string "<function>",
+  # derivations are replaced with an attrset
+  # { _type = "derivation"; name = <name of that derivation>; }.
+  # We need to handle derivations specially because consumers want to know about them,
+  # but we can't easily use the type,name subset of keys (since type is often used as
+  # a module option and might cause confusion). Use _type,name instead to the same
+  # effect, since _type is already used by the module system.
+  substSpecial = x:
+    if lib.isDerivation x then { _type = "derivation"; name = x.name; }
+    else if builtins.isAttrs x then lib.mapAttrs (name: substSpecial) x
+    else if builtins.isList x then map substSpecial x
     else if lib.isFunction x then "<function>"
     else x;
 
-  optionsListDesc = lib.flip map optionsListVisible
+  optionsList = lib.flip map optionsListVisible
    (opt: transformOptions opt
-    // lib.optionalAttrs (opt ? example) { example = substFunction opt.example; }
-    // lib.optionalAttrs (opt ? default) { default = substFunction opt.default; }
-    // lib.optionalAttrs (opt ? type) { type = substFunction opt.type; }
+    // lib.optionalAttrs (opt ? example) { example = substSpecial opt.example; }
+    // lib.optionalAttrs (opt ? default) { default = substSpecial opt.default; }
+    // lib.optionalAttrs (opt ? type) { type = substSpecial opt.type; }
     // lib.optionalAttrs (opt ? relatedPackages && opt.relatedPackages != []) { relatedPackages = genRelatedPackages opt.relatedPackages opt.name; }
    );
 
@@ -69,96 +76,25 @@ let
         + "</listitem>";
     in "<itemizedlist>${lib.concatStringsSep "\n" (map (p: describe (unpack p)) packages)}</itemizedlist>";
 
-  # Custom "less" that pushes up all the things ending in ".enable*"
-  # and ".package*"
-  optionLess = a: b:
-    let
-      ise = lib.hasPrefix "enable";
-      isp = lib.hasPrefix "package";
-      cmp = lib.splitByAndCompare ise lib.compare
-                                 (lib.splitByAndCompare isp lib.compare lib.compare);
-    in lib.compareLists cmp a.loc b.loc < 0;
-
   # Remove invisible and internal options.
   optionsListVisible = lib.filter (opt: opt.visible && !opt.internal) (lib.optionAttrSetToDocList options);
 
-  # Customly sort option list for the man page.
-  # Always ensure that the sort order matches sortXML.py!
-  optionsList = lib.sort optionLess optionsListDesc;
-
-  # Convert the list of options into an XML file.
-  # This file is *not* sorted sorted to save on eval time, since the docbook XML
-  # and the manpage depend on it and thus we evaluate this on every system rebuild.
-  optionsXML = builtins.toFile "options.xml" (builtins.toXML optionsListDesc);
-
   optionsNix = builtins.listToAttrs (map (o: { name = o.name; value = removeAttrs o ["name" "visible" "internal"]; }) optionsList);
 
-  # TODO: declarations: link to github
-  singleAsciiDoc = name: value: ''
-    == ${name}
-
-    ${value.description}
-
-    [discrete]
-    === details
-
-    Type:: ${value.type}
-    ${ if lib.hasAttr "default" value
-       then ''
-        Default::
-        +
-        ----
-        ${builtins.toJSON value.default}
-        ----
-      ''
-      else "No Default:: {blank}"
-    }
-    ${ if value.readOnly
-       then "Read Only:: {blank}"
-      else ""
-    }
-    ${ if lib.hasAttr "example" value
-       then ''
-        Example::
-        +
-        ----
-        ${builtins.toJSON value.example}
-        ----
-      ''
-      else "No Example:: {blank}"
-    }
-  '';
-
-  singleMDDoc = name: value: ''
-    ## ${lib.escape [ "<" ">" ] name}
-    ${value.description}
-
-    ${lib.optionalString (value ? type) ''
-      *_Type_*:
-      ${value.type}
-    ''}
-
-    ${lib.optionalString (value ? default) ''
-      *_Default_*
-      ```
-      ${builtins.toJSON value.default}
-      ```
-    ''}
-
-    ${lib.optionalString (value ? example) ''
-      *_Example_*
-      ```
-      ${builtins.toJSON value.example}
-      ```
-    ''}
-  '';
-
-in {
+in rec {
   inherit optionsNix;
 
-  optionsAsciiDoc = lib.concatStringsSep "\n" (lib.mapAttrsToList singleAsciiDoc optionsNix);
+  optionsAsciiDoc = pkgs.runCommand "options.adoc" {} ''
+    ${pkgs.python3Minimal}/bin/python ${./generateAsciiDoc.py} \
+      < ${optionsJSON}/share/doc/nixos/options.json \
+      > $out
+  '';
 
-  optionsMDDoc = lib.concatStringsSep "\n" (lib.mapAttrsToList singleMDDoc optionsNix);
+  optionsCommonMark = pkgs.runCommand "options.md" {} ''
+    ${pkgs.python3Minimal}/bin/python ${./generateCommonMark.py} \
+      < ${optionsJSON}/share/doc/nixos/options.json \
+      > $out
+  '';
 
   optionsJSON = pkgs.runCommand "options.json"
     { meta.description = "List of NixOS options in JSON format";
@@ -176,7 +112,19 @@ in {
       mkdir -p $out/nix-support
       echo "file json $dst/options.json" >> $out/nix-support/hydra-build-products
       echo "file json-br $dst/options.json.br" >> $out/nix-support/hydra-build-products
-    ''; # */
+    '';
+
+  # Convert options.json into an XML file.
+  # The actual generation of the xml file is done in nix purely for the convenience
+  # of not having to generate the xml some other way
+  optionsXML = pkgs.runCommand "options.xml" {} ''
+    export NIX_STORE_DIR=$TMPDIR/store
+    export NIX_STATE_DIR=$TMPDIR/state
+    ${pkgs.nix}/bin/nix-instantiate \
+      --eval --xml --strict ${./optionsJSONtoXML.nix} \
+      --argstr file ${optionsJSON}/share/doc/nixos/options.json \
+      > "$out"
+  '';
 
   optionsDocBook = pkgs.runCommand "options-docbook.xml" {} ''
     optionsXML=${optionsXML}
diff --git a/nixos/lib/make-options-doc/generateAsciiDoc.py b/nixos/lib/make-options-doc/generateAsciiDoc.py
new file mode 100644
index 0000000000000..48eadd248c5a0
--- /dev/null
+++ b/nixos/lib/make-options-doc/generateAsciiDoc.py
@@ -0,0 +1,37 @@
+import json
+import sys
+
+options = json.load(sys.stdin)
+# TODO: declarations: link to github
+for (name, value) in options.items():
+    print(f'== {name}')
+    print()
+    print(value['description'])
+    print()
+    print('[discrete]')
+    print('=== details')
+    print()
+    print(f'Type:: {value["type"]}')
+    if 'default' in value:
+        print('Default::')
+        print('+')
+        print('----')
+        print(json.dumps(value['default'], ensure_ascii=False, separators=(',', ':')))
+        print('----')
+        print()
+    else:
+        print('No Default:: {blank}')
+    if value['readOnly']:
+        print('Read Only:: {blank}')
+    else:
+        print()
+    if 'example' in value:
+        print('Example::')
+        print('+')
+        print('----')
+        print(json.dumps(value['example'], ensure_ascii=False, separators=(',', ':')))
+        print('----')
+        print()
+    else:
+        print('No Example:: {blank}')
+    print()
diff --git a/nixos/lib/make-options-doc/generateCommonMark.py b/nixos/lib/make-options-doc/generateCommonMark.py
new file mode 100644
index 0000000000000..404e53b0df9c2
--- /dev/null
+++ b/nixos/lib/make-options-doc/generateCommonMark.py
@@ -0,0 +1,27 @@
+import json
+import sys
+
+options = json.load(sys.stdin)
+for (name, value) in options.items():
+    print('##', name.replace('<', '\\<').replace('>', '\\>'))
+    print(value['description'])
+    print()
+    if 'type' in value:
+        print('*_Type_*:')
+        print(value['type'])
+        print()
+    print()
+    if 'default' in value:
+        print('*_Default_*')
+        print('```')
+        print(json.dumps(value['default'], ensure_ascii=False, separators=(',', ':')))
+        print('```')
+    print()
+    print()
+    if 'example' in value:
+        print('*_Example_*')
+        print('```')
+        print(json.dumps(value['example'], ensure_ascii=False, separators=(',', ':')))
+        print('```')
+    print()
+    print()
diff --git a/nixos/lib/make-options-doc/options-to-docbook.xsl b/nixos/lib/make-options-doc/options-to-docbook.xsl
index da4cd164bf206..b286f7b5e2c0a 100644
--- a/nixos/lib/make-options-doc/options-to-docbook.xsl
+++ b/nixos/lib/make-options-doc/options-to-docbook.xsl
@@ -20,7 +20,7 @@
       <title>Configuration Options</title>
       <variablelist xml:id="configuration-variable-list">
         <xsl:for-each select="attrs">
-          <xsl:variable name="id" select="concat('opt-', str:replace(str:replace(str:replace(attr[@name = 'name']/string/@value, '*', '_'), '&lt;', '_'), '>', '_'))" />
+          <xsl:variable name="id" select="concat('opt-', str:replace(str:replace(str:replace(str:replace(attr[@name = 'name']/string/@value, '*', '_'), '&lt;', '_'), '>', '_'), ':', '_'))" />
           <varlistentry>
             <term xlink:href="#{$id}">
               <xsl:attribute name="xml:id"><xsl:value-of select="$id"/></xsl:attribute>
@@ -189,7 +189,7 @@
   </xsl:template>
 
 
-  <xsl:template match="derivation">
+  <xsl:template match="attrs[attr[@name = '_type' and string[@value = 'derivation']]]">
     <replaceable>(build of <xsl:value-of select="attr[@name = 'name']/string/@value" />)</replaceable>
   </xsl:template>
 
diff --git a/nixos/lib/make-options-doc/optionsJSONtoXML.nix b/nixos/lib/make-options-doc/optionsJSONtoXML.nix
new file mode 100644
index 0000000000000..ba50c5f898b5a
--- /dev/null
+++ b/nixos/lib/make-options-doc/optionsJSONtoXML.nix
@@ -0,0 +1,6 @@
+{ file }:
+
+builtins.attrValues
+  (builtins.mapAttrs
+    (name: def: def // { inherit name; })
+    (builtins.fromJSON (builtins.readFile file)))
diff --git a/nixos/lib/make-options-doc/sortXML.py b/nixos/lib/make-options-doc/sortXML.py
index 717820788c944..e63ff3538b3fe 100644
--- a/nixos/lib/make-options-doc/sortXML.py
+++ b/nixos/lib/make-options-doc/sortXML.py
@@ -19,7 +19,6 @@ def sortKey(opt):
         for p in opt.findall('attr[@name="loc"]/list/string')
     ]
 
-# always ensure that the sort order matches the order used in the nix expression!
 options.sort(key=sortKey)
 
 doc = ET.Element("expr")
diff --git a/nixos/lib/make-squashfs.nix b/nixos/lib/make-squashfs.nix
index 8690c42e7ac93..170d315fb7517 100644
--- a/nixos/lib/make-squashfs.nix
+++ b/nixos/lib/make-squashfs.nix
@@ -21,8 +21,15 @@ stdenv.mkDerivation {
       # for nix-store --load-db.
       cp $closureInfo/registration nix-path-registration
 
+      # 64 cores on i686 does not work
+      # fails with FATAL ERROR: mangle2:: xz compress failed with error code 5
+      if ((NIX_BUILD_CORES > 48)); then
+        NIX_BUILD_CORES=48
+      fi
+
       # Generate the squashfs image.
       mksquashfs nix-path-registration $(cat $closureInfo/store-paths) $out \
-        -no-hardlinks -keep-as-directory -all-root -b 1048576 -comp ${comp}
+        -no-hardlinks -keep-as-directory -all-root -b 1048576 -comp ${comp} \
+        -processors $NIX_BUILD_CORES
     '';
 }
diff --git a/nixos/modules/system/boot/systemd-lib.nix b/nixos/lib/systemd-lib.nix
index 6c4d27018eed8..6c4d27018eed8 100644
--- a/nixos/modules/system/boot/systemd-lib.nix
+++ b/nixos/lib/systemd-lib.nix
diff --git a/nixos/modules/system/boot/systemd-unit-options.nix b/nixos/lib/systemd-unit-options.nix
index 4154389b2ce5f..01f954a4d3e01 100644
--- a/nixos/modules/system/boot/systemd-unit-options.nix
+++ b/nixos/lib/systemd-unit-options.nix
@@ -1,7 +1,7 @@
-{ config, lib }:
+{ lib, systemdUtils }:
 
+with systemdUtils.lib;
 with lib;
-with import ./systemd-lib.nix { inherit config lib pkgs; };
 
 let
   checkService = checkUnitConfig "Service" [
diff --git a/nixos/lib/test-driver/default.nix b/nixos/lib/test-driver/default.nix
new file mode 100644
index 0000000000000..3f63bc705b902
--- /dev/null
+++ b/nixos/lib/test-driver/default.nix
@@ -0,0 +1,32 @@
+{ lib
+, python3Packages
+, enableOCR ? false
+, qemu_pkg ? qemu_test
+, coreutils
+, imagemagick_light
+, libtiff
+, netpbm
+, qemu_test
+, socat
+, tesseract4
+, vde2
+}:
+
+python3Packages.buildPythonApplication rec {
+  pname = "nixos-test-driver";
+  version = "1.0";
+  src = ./.;
+
+  propagatedBuildInputs = [ coreutils netpbm python3Packages.colorama python3Packages.ptpython qemu_pkg socat vde2 ]
+    ++ (lib.optionals enableOCR [ imagemagick_light tesseract4 ]);
+
+  doCheck = true;
+  checkInputs = with python3Packages; [ mypy pylint black ];
+  checkPhase = ''
+    mypy --disallow-untyped-defs \
+          --no-implicit-optional \
+          --ignore-missing-imports ${src}/test_driver
+    pylint --errors-only ${src}/test_driver
+    black --check --diff ${src}/test_driver
+  '';
+}
diff --git a/nixos/lib/test-driver/setup.py b/nixos/lib/test-driver/setup.py
new file mode 100644
index 0000000000000..156995472169e
--- /dev/null
+++ b/nixos/lib/test-driver/setup.py
@@ -0,0 +1,13 @@
+from setuptools import setup, find_packages
+
+setup(
+  name="nixos-test-driver",
+  version='1.0',
+  packages=find_packages(),
+  entry_points={
+    "console_scripts": [
+      "nixos-test-driver=test_driver:main",
+      "generate-driver-symbols=test_driver:generate_driver_symbols"
+    ]
+  },
+)
diff --git a/nixos/lib/test-driver/test_driver/__init__.py b/nixos/lib/test-driver/test_driver/__init__.py
new file mode 100755
index 0000000000000..5477ab5cd038e
--- /dev/null
+++ b/nixos/lib/test-driver/test_driver/__init__.py
@@ -0,0 +1,100 @@
+from pathlib import Path
+import argparse
+import ptpython.repl
+import os
+import time
+
+from test_driver.logger import rootlog
+from test_driver.driver import Driver
+
+
+class EnvDefault(argparse.Action):
+    """An argpars Action that takes values from the specified
+    environment variable as the flags default value.
+    """
+
+    def __init__(self, envvar, required=False, default=None, nargs=None, **kwargs):  # type: ignore
+        if not default and envvar:
+            if envvar in os.environ:
+                if nargs is not None and (nargs.isdigit() or nargs in ["*", "+"]):
+                    default = os.environ[envvar].split()
+                else:
+                    default = os.environ[envvar]
+                kwargs["help"] = (
+                    kwargs["help"] + f" (default from environment: {default})"
+                )
+        if required and default:
+            required = False
+        super(EnvDefault, self).__init__(
+            default=default, required=required, nargs=nargs, **kwargs
+        )
+
+    def __call__(self, parser, namespace, values, option_string=None):  # type: ignore
+        setattr(namespace, self.dest, values)
+
+
+def main() -> None:
+    arg_parser = argparse.ArgumentParser(prog="nixos-test-driver")
+    arg_parser.add_argument(
+        "-K",
+        "--keep-vm-state",
+        help="re-use a VM state coming from a previous run",
+        action="store_true",
+    )
+    arg_parser.add_argument(
+        "-I",
+        "--interactive",
+        help="drop into a python repl and run the tests interactively",
+        action="store_true",
+    )
+    arg_parser.add_argument(
+        "--start-scripts",
+        metavar="START-SCRIPT",
+        action=EnvDefault,
+        envvar="startScripts",
+        nargs="*",
+        help="start scripts for participating virtual machines",
+    )
+    arg_parser.add_argument(
+        "--vlans",
+        metavar="VLAN",
+        action=EnvDefault,
+        envvar="vlans",
+        nargs="*",
+        help="vlans to span by the driver",
+    )
+    arg_parser.add_argument(
+        "testscript",
+        action=EnvDefault,
+        envvar="testScript",
+        help="the test script to run",
+        type=Path,
+    )
+
+    args = arg_parser.parse_args()
+
+    if not args.keep_vm_state:
+        rootlog.info("Machine state will be reset. To keep it, pass --keep-vm-state")
+
+    with Driver(
+        args.start_scripts, args.vlans, args.testscript.read_text(), args.keep_vm_state
+    ) as driver:
+        if args.interactive:
+            ptpython.repl.embed(driver.test_symbols(), {})
+        else:
+            tic = time.time()
+            driver.run_tests()
+            toc = time.time()
+            rootlog.info(f"test script finished in {(toc-tic):.2f}s")
+
+
+def generate_driver_symbols() -> None:
+    """
+    This generates a file with symbols of the test-driver code that can be used
+    in user's test scripts. That list is then used by pyflakes to lint those
+    scripts.
+    """
+    d = Driver([], [], "")
+    test_symbols = d.test_symbols()
+    with open("driver-symbols", "w") as fp:
+        fp.write(",".join(test_symbols.keys()))
diff --git a/nixos/lib/test-driver/test_driver/driver.py b/nixos/lib/test-driver/test_driver/driver.py
new file mode 100644
index 0000000000000..f3af98537ad67
--- /dev/null
+++ b/nixos/lib/test-driver/test_driver/driver.py
@@ -0,0 +1,161 @@
+from contextlib import contextmanager
+from pathlib import Path
+from typing import Any, Dict, Iterator, List
+import os
+import tempfile
+
+from test_driver.logger import rootlog
+from test_driver.machine import Machine, NixStartScript, retry
+from test_driver.vlan import VLan
+
+
+class Driver:
+    """A handle to the driver that sets up the environment
+    and runs the tests"""
+
+    tests: str
+    vlans: List[VLan]
+    machines: List[Machine]
+
+    def __init__(
+        self,
+        start_scripts: List[str],
+        vlans: List[int],
+        tests: str,
+        keep_vm_state: bool = False,
+    ):
+        self.tests = tests
+
+        tmp_dir = Path(os.environ.get("TMPDIR", tempfile.gettempdir()))
+        tmp_dir.mkdir(mode=0o700, exist_ok=True)
+
+        with rootlog.nested("start all VLans"):
+            self.vlans = [VLan(nr, tmp_dir) for nr in vlans]
+
+        def cmd(scripts: List[str]) -> Iterator[NixStartScript]:
+            for s in scripts:
+                yield NixStartScript(s)
+
+        self.machines = [
+            Machine(
+                start_command=cmd,
+                keep_vm_state=keep_vm_state,
+                name=cmd.machine_name,
+                tmp_dir=tmp_dir,
+            )
+            for cmd in cmd(start_scripts)
+        ]
+
+    def __enter__(self) -> "Driver":
+        return self
+
+    def __exit__(self, *_: Any) -> None:
+        with rootlog.nested("cleanup"):
+            for machine in self.machines:
+                machine.release()
+
+    def subtest(self, name: str) -> Iterator[None]:
+        """Group logs under a given test name"""
+        with rootlog.nested(name):
+            try:
+                yield
+                return True
+            except Exception as e:
+                rootlog.error(f'Test "{name}" failed with error: "{e}"')
+                raise e
+
+    def test_symbols(self) -> Dict[str, Any]:
+        @contextmanager
+        def subtest(name: str) -> Iterator[None]:
+            return self.subtest(name)
+
+        general_symbols = dict(
+            start_all=self.start_all,
+            test_script=self.test_script,
+            machines=self.machines,
+            vlans=self.vlans,
+            driver=self,
+            log=rootlog,
+            os=os,
+            create_machine=self.create_machine,
+            subtest=subtest,
+            run_tests=self.run_tests,
+            join_all=self.join_all,
+            retry=retry,
+            serial_stdout_off=self.serial_stdout_off,
+            serial_stdout_on=self.serial_stdout_on,
+            Machine=Machine,  # for typing
+        )
+        machine_symbols = {m.name: m for m in self.machines}
+        # If there's exactly one machine, make it available under the name
+        # "machine", even if it's not called that.
+        if len(self.machines) == 1:
+            (machine_symbols["machine"],) = self.machines
+        vlan_symbols = {
+            f"vlan{v.nr}": self.vlans[idx] for idx, v in enumerate(self.vlans)
+        }
+        print(
+            "additionally exposed symbols:\n    "
+            + ", ".join(map(lambda m: m.name, self.machines))
+            + ",\n    "
+            + ", ".join(map(lambda v: f"vlan{v.nr}", self.vlans))
+            + ",\n    "
+            + ", ".join(list(general_symbols.keys()))
+        )
+        return {**general_symbols, **machine_symbols, **vlan_symbols}
+
+    def test_script(self) -> None:
+        """Run the test script"""
+        with rootlog.nested("run the VM test script"):
+            symbols = self.test_symbols()  # call eagerly
+            exec(self.tests, symbols, None)
+
+    def run_tests(self) -> None:
+        """Run the test script (for non-interactive test runs)"""
+        self.test_script()
+        # TODO: Collect coverage data
+        for machine in self.machines:
+            if machine.is_up():
+                machine.execute("sync")
+
+    def start_all(self) -> None:
+        """Start all machines"""
+        with rootlog.nested("start all VMs"):
+            for machine in self.machines:
+                machine.start()
+
+    def join_all(self) -> None:
+        """Wait for all machines to shut down"""
+        with rootlog.nested("wait for all VMs to finish"):
+            for machine in self.machines:
+                machine.wait_for_shutdown()
+
+    def create_machine(self, args: Dict[str, Any]) -> Machine:
+        rootlog.warning(
+            "Using legacy create_machine(), please instantiate the"
+            "Machine class directly, instead"
+        )
+        tmp_dir = Path(os.environ.get("TMPDIR", tempfile.gettempdir()))
+        tmp_dir.mkdir(mode=0o700, exist_ok=True)
+
+        if args.get("startCommand"):
+            start_command: str = args.get("startCommand", "")
+            cmd = NixStartScript(start_command)
+            name = args.get("name", cmd.machine_name)
+        else:
+            cmd = Machine.create_startcommand(args)  # type: ignore
+            name = args.get("name", "machine")
+
+        return Machine(
+            tmp_dir=tmp_dir,
+            start_command=cmd,
+            name=name,
+            keep_vm_state=args.get("keep_vm_state", False),
+            allow_reboot=args.get("allow_reboot", False),
+        )
+
+    def serial_stdout_on(self) -> None:
+        rootlog._print_serial_logs = True
+
+    def serial_stdout_off(self) -> None:
+        rootlog._print_serial_logs = False
diff --git a/nixos/lib/test-driver/test_driver/logger.py b/nixos/lib/test-driver/test_driver/logger.py
new file mode 100644
index 0000000000000..5b3091a5129c3
--- /dev/null
+++ b/nixos/lib/test-driver/test_driver/logger.py
@@ -0,0 +1,101 @@
+from colorama import Style
+from contextlib import contextmanager
+from typing import Any, Dict, Iterator
+from queue import Queue, Empty
+from xml.sax.saxutils import XMLGenerator
+import codecs
+import os
+import sys
+import time
+import unicodedata
+
+
+class Logger:
+    def __init__(self) -> None:
+        self.logfile = os.environ.get("LOGFILE", "/dev/null")
+        self.logfile_handle = codecs.open(self.logfile, "wb")
+        self.xml = XMLGenerator(self.logfile_handle, encoding="utf-8")
+        self.queue: "Queue[Dict[str, str]]" = Queue()
+
+        self.xml.startDocument()
+        self.xml.startElement("logfile", attrs={})
+
+        self._print_serial_logs = True
+
+    @staticmethod
+    def _eprint(*args: object, **kwargs: Any) -> None:
+        print(*args, file=sys.stderr, **kwargs)
+
+    def close(self) -> None:
+        self.xml.endElement("logfile")
+        self.xml.endDocument()
+        self.logfile_handle.close()
+
+    def sanitise(self, message: str) -> str:
+        return "".join(ch for ch in message if unicodedata.category(ch)[0] != "C")
+
+    def maybe_prefix(self, message: str, attributes: Dict[str, str]) -> str:
+        if "machine" in attributes:
+            return "{}: {}".format(attributes["machine"], message)
+        return message
+
+    def log_line(self, message: str, attributes: Dict[str, str]) -> None:
+        self.xml.startElement("line", attributes)
+        self.xml.characters(message)
+        self.xml.endElement("line")
+
+    def info(self, *args, **kwargs) -> None:  # type: ignore
+        self.log(*args, **kwargs)
+
+    def warning(self, *args, **kwargs) -> None:  # type: ignore
+        self.log(*args, **kwargs)
+
+    def error(self, *args, **kwargs) -> None:  # type: ignore
+        self.log(*args, **kwargs)
+        sys.exit(1)
+
+    def log(self, message: str, attributes: Dict[str, str] = {}) -> None:
+        self._eprint(self.maybe_prefix(message, attributes))
+        self.drain_log_queue()
+        self.log_line(message, attributes)
+
+    def log_serial(self, message: str, machine: str) -> None:
+        self.enqueue({"msg": message, "machine": machine, "type": "serial"})
+        if self._print_serial_logs:
+            self._eprint(
+                Style.DIM + "{} # {}".format(machine, message) + Style.RESET_ALL
+            )
+
+    def enqueue(self, item: Dict[str, str]) -> None:
+        self.queue.put(item)
+
+    def drain_log_queue(self) -> None:
+        try:
+            while True:
+                item = self.queue.get_nowait()
+                msg = self.sanitise(item["msg"])
+                del item["msg"]
+                self.log_line(msg, item)
+        except Empty:
+            pass
+
+    @contextmanager
+    def nested(self, message: str, attributes: Dict[str, str] = {}) -> Iterator[None]:
+        self._eprint(self.maybe_prefix(message, attributes))
+
+        self.xml.startElement("nest", attrs={})
+        self.xml.startElement("head", attributes)
+        self.xml.characters(message)
+        self.xml.endElement("head")
+
+        tic = time.time()
+        self.drain_log_queue()
+        yield
+        self.drain_log_queue()
+        toc = time.time()
+        self.log("(finished: {}, in {:.2f} seconds)".format(message, toc - tic))
+
+        self.xml.endElement("nest")
+
+
+rootlog = Logger()
diff --git a/nixos/lib/test-driver/test-driver.py b/nixos/lib/test-driver/test_driver/machine.py
index 90c9e9be45cde..b3dbe5126fcc6 100755..100644
--- a/nixos/lib/test-driver/test-driver.py
+++ b/nixos/lib/test-driver/test_driver/machine.py
@@ -1,19 +1,11 @@
-#! /somewhere/python3
-from contextlib import contextmanager, _GeneratorContextManager
-from queue import Queue, Empty
-from typing import Tuple, Any, Callable, Dict, Iterator, Optional, List, Iterable
-from xml.sax.saxutils import XMLGenerator
-from colorama import Style
+from contextlib import _GeneratorContextManager
 from pathlib import Path
-import queue
-import io
-import threading
-import argparse
+from queue import Queue
+from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
 import base64
-import codecs
+import io
 import os
-import ptpython.repl
-import pty
+import queue
 import re
 import shlex
 import shutil
@@ -21,8 +13,10 @@ import socket
 import subprocess
 import sys
 import tempfile
+import threading
 import time
-import unicodedata
+
+from test_driver.logger import rootlog
 
 CHAR_TO_KEY = {
     "A": "shift-a",
@@ -88,115 +82,10 @@ CHAR_TO_KEY = {
 }
 
 
-class Logger:
-    def __init__(self) -> None:
-        self.logfile = os.environ.get("LOGFILE", "/dev/null")
-        self.logfile_handle = codecs.open(self.logfile, "wb")
-        self.xml = XMLGenerator(self.logfile_handle, encoding="utf-8")
-        self.queue: "Queue[Dict[str, str]]" = Queue()
-
-        self.xml.startDocument()
-        self.xml.startElement("logfile", attrs={})
-
-        self._print_serial_logs = True
-
-    @staticmethod
-    def _eprint(*args: object, **kwargs: Any) -> None:
-        print(*args, file=sys.stderr, **kwargs)
-
-    def close(self) -> None:
-        self.xml.endElement("logfile")
-        self.xml.endDocument()
-        self.logfile_handle.close()
-
-    def sanitise(self, message: str) -> str:
-        return "".join(ch for ch in message if unicodedata.category(ch)[0] != "C")
-
-    def maybe_prefix(self, message: str, attributes: Dict[str, str]) -> str:
-        if "machine" in attributes:
-            return "{}: {}".format(attributes["machine"], message)
-        return message
-
-    def log_line(self, message: str, attributes: Dict[str, str]) -> None:
-        self.xml.startElement("line", attributes)
-        self.xml.characters(message)
-        self.xml.endElement("line")
-
-    def info(self, *args, **kwargs) -> None:  # type: ignore
-        self.log(*args, **kwargs)
-
-    def warning(self, *args, **kwargs) -> None:  # type: ignore
-        self.log(*args, **kwargs)
-
-    def error(self, *args, **kwargs) -> None:  # type: ignore
-        self.log(*args, **kwargs)
-        sys.exit(1)
-
-    def log(self, message: str, attributes: Dict[str, str] = {}) -> None:
-        self._eprint(self.maybe_prefix(message, attributes))
-        self.drain_log_queue()
-        self.log_line(message, attributes)
-
-    def log_serial(self, message: str, machine: str) -> None:
-        self.enqueue({"msg": message, "machine": machine, "type": "serial"})
-        if self._print_serial_logs:
-            self._eprint(
-                Style.DIM + "{} # {}".format(machine, message) + Style.RESET_ALL
-            )
-
-    def enqueue(self, item: Dict[str, str]) -> None:
-        self.queue.put(item)
-
-    def drain_log_queue(self) -> None:
-        try:
-            while True:
-                item = self.queue.get_nowait()
-                msg = self.sanitise(item["msg"])
-                del item["msg"]
-                self.log_line(msg, item)
-        except Empty:
-            pass
-
-    @contextmanager
-    def nested(self, message: str, attributes: Dict[str, str] = {}) -> Iterator[None]:
-        self._eprint(self.maybe_prefix(message, attributes))
-
-        self.xml.startElement("nest", attrs={})
-        self.xml.startElement("head", attributes)
-        self.xml.characters(message)
-        self.xml.endElement("head")
-
-        tic = time.time()
-        self.drain_log_queue()
-        yield
-        self.drain_log_queue()
-        toc = time.time()
-        self.log("(finished: {}, in {:.2f} seconds)".format(message, toc - tic))
-
-        self.xml.endElement("nest")
-
-
-rootlog = Logger()
-
-
 def make_command(args: list) -> str:
     return " ".join(map(shlex.quote, (map(str, args))))
 
 
-def retry(fn: Callable, timeout: int = 900) -> None:
-    """Call the given function repeatedly, with 1 second intervals,
-    until it returns True or a timeout is reached.
-    """
-
-    for _ in range(timeout):
-        if fn(False):
-            return
-        time.sleep(1)
-
-    if not fn(True):
-        raise Exception(f"action timed out after {timeout} seconds")
-
-
 def _perform_ocr_on_screenshot(
     screenshot_path: str, model_ids: Iterable[int]
 ) -> List[str]:
@@ -228,6 +117,20 @@ def _perform_ocr_on_screenshot(
     return model_results
 
 
+def retry(fn: Callable, timeout: int = 900) -> None:
+    """Call the given function repeatedly, with 1 second intervals,
+    until it returns True or a timeout is reached.
+    """
+
+    for _ in range(timeout):
+        if fn(False):
+            return
+        time.sleep(1)
+
+    if not fn(True):
+        raise Exception(f"action timed out after {timeout} seconds")
+
+
 class StartCommand:
     """The Base Start Command knows how to append the necesary
     runtime qemu options as determined by a particular test driver
@@ -1066,286 +969,3 @@ class Machine:
         self.shell.close()
         self.monitor.close()
         self.serial_thread.join()
-
-
-class VLan:
-    """This class handles a VLAN that the run-vm scripts identify via its
-    number handles. The network's lifetime equals the object's lifetime.
-    """
-
-    nr: int
-    socket_dir: Path
-
-    process: subprocess.Popen
-    pid: int
-    fd: io.TextIOBase
-
-    def __repr__(self) -> str:
-        return f"<Vlan Nr. {self.nr}>"
-
-    def __init__(self, nr: int, tmp_dir: Path):
-        self.nr = nr
-        self.socket_dir = tmp_dir / f"vde{self.nr}.ctl"
-
-        # TODO: don't side-effect environment here
-        os.environ[f"QEMU_VDE_SOCKET_{self.nr}"] = str(self.socket_dir)
-
-        rootlog.info("start vlan")
-        pty_master, pty_slave = pty.openpty()
-
-        self.process = subprocess.Popen(
-            ["vde_switch", "-s", self.socket_dir, "--dirmode", "0700"],
-            stdin=pty_slave,
-            stdout=subprocess.PIPE,
-            stderr=subprocess.PIPE,
-            shell=False,
-        )
-        self.pid = self.process.pid
-        self.fd = os.fdopen(pty_master, "w")
-        self.fd.write("version\n")
-
-        # TODO: perl version checks if this can be read from
-        # an if not, dies. we could hang here forever. Fix it.
-        assert self.process.stdout is not None
-        self.process.stdout.readline()
-        if not (self.socket_dir / "ctl").exists():
-            rootlog.error("cannot start vde_switch")
-
-        rootlog.info(f"running vlan (pid {self.pid})")
-
-    def __del__(self) -> None:
-        rootlog.info(f"kill vlan (pid {self.pid})")
-        self.fd.close()
-        self.process.terminate()
-
-
-class Driver:
-    """A handle to the driver that sets up the environment
-    and runs the tests"""
-
-    tests: str
-    vlans: List[VLan]
-    machines: List[Machine]
-
-    def __init__(
-        self,
-        start_scripts: List[str],
-        vlans: List[int],
-        tests: str,
-        keep_vm_state: bool = False,
-    ):
-        self.tests = tests
-
-        tmp_dir = Path(os.environ.get("TMPDIR", tempfile.gettempdir()))
-        tmp_dir.mkdir(mode=0o700, exist_ok=True)
-
-        with rootlog.nested("start all VLans"):
-            self.vlans = [VLan(nr, tmp_dir) for nr in vlans]
-
-        def cmd(scripts: List[str]) -> Iterator[NixStartScript]:
-            for s in scripts:
-                yield NixStartScript(s)
-
-        self.machines = [
-            Machine(
-                start_command=cmd,
-                keep_vm_state=keep_vm_state,
-                name=cmd.machine_name,
-                tmp_dir=tmp_dir,
-            )
-            for cmd in cmd(start_scripts)
-        ]
-
-    def __enter__(self) -> "Driver":
-        return self
-
-    def __exit__(self, *_: Any) -> None:
-        with rootlog.nested("cleanup"):
-            for machine in self.machines:
-                machine.release()
-
-    def subtest(self, name: str) -> Iterator[None]:
-        """Group logs under a given test name"""
-        with rootlog.nested(name):
-            try:
-                yield
-                return True
-            except Exception as e:
-                rootlog.error(f'Test "{name}" failed with error: "{e}"')
-                raise e
-
-    def test_symbols(self) -> Dict[str, Any]:
-        @contextmanager
-        def subtest(name: str) -> Iterator[None]:
-            return self.subtest(name)
-
-        general_symbols = dict(
-            start_all=self.start_all,
-            test_script=self.test_script,
-            machines=self.machines,
-            vlans=self.vlans,
-            driver=self,
-            log=rootlog,
-            os=os,
-            create_machine=self.create_machine,
-            subtest=subtest,
-            run_tests=self.run_tests,
-            join_all=self.join_all,
-            retry=retry,
-            serial_stdout_off=self.serial_stdout_off,
-            serial_stdout_on=self.serial_stdout_on,
-            Machine=Machine,  # for typing
-        )
-        machine_symbols = {m.name: m for m in self.machines}
-        # If there's exactly one machine, make it available under the name
-        # "machine", even if it's not called that.
-        if len(self.machines) == 1:
-            (machine_symbols["machine"],) = self.machines
-        vlan_symbols = {
-            f"vlan{v.nr}": self.vlans[idx] for idx, v in enumerate(self.vlans)
-        }
-        print(
-            "additionally exposed symbols:\n    "
-            + ", ".join(map(lambda m: m.name, self.machines))
-            + ",\n    "
-            + ", ".join(map(lambda v: f"vlan{v.nr}", self.vlans))
-            + ",\n    "
-            + ", ".join(list(general_symbols.keys()))
-        )
-        return {**general_symbols, **machine_symbols, **vlan_symbols}
-
-    def test_script(self) -> None:
-        """Run the test script"""
-        with rootlog.nested("run the VM test script"):
-            symbols = self.test_symbols()  # call eagerly
-            exec(self.tests, symbols, None)
-
-    def run_tests(self) -> None:
-        """Run the test script (for non-interactive test runs)"""
-        self.test_script()
-        # TODO: Collect coverage data
-        for machine in self.machines:
-            if machine.is_up():
-                machine.execute("sync")
-
-    def start_all(self) -> None:
-        """Start all machines"""
-        with rootlog.nested("start all VMs"):
-            for machine in self.machines:
-                machine.start()
-
-    def join_all(self) -> None:
-        """Wait for all machines to shut down"""
-        with rootlog.nested("wait for all VMs to finish"):
-            for machine in self.machines:
-                machine.wait_for_shutdown()
-
-    def create_machine(self, args: Dict[str, Any]) -> Machine:
-        rootlog.warning(
-            "Using legacy create_machine(), please instantiate the"
-            "Machine class directly, instead"
-        )
-        tmp_dir = Path(os.environ.get("TMPDIR", tempfile.gettempdir()))
-        tmp_dir.mkdir(mode=0o700, exist_ok=True)
-
-        if args.get("startCommand"):
-            start_command: str = args.get("startCommand", "")
-            cmd = NixStartScript(start_command)
-            name = args.get("name", cmd.machine_name)
-        else:
-            cmd = Machine.create_startcommand(args)  # type: ignore
-            name = args.get("name", "machine")
-
-        return Machine(
-            tmp_dir=tmp_dir,
-            start_command=cmd,
-            name=name,
-            keep_vm_state=args.get("keep_vm_state", False),
-            allow_reboot=args.get("allow_reboot", False),
-        )
-
-    def serial_stdout_on(self) -> None:
-        rootlog._print_serial_logs = True
-
-    def serial_stdout_off(self) -> None:
-        rootlog._print_serial_logs = False
-
-
-class EnvDefault(argparse.Action):
-    """An argpars Action that takes values from the specified
-    environment variable as the flags default value.
-    """
-
-    def __init__(self, envvar, required=False, default=None, nargs=None, **kwargs):  # type: ignore
-        if not default and envvar:
-            if envvar in os.environ:
-                if nargs is not None and (nargs.isdigit() or nargs in ["*", "+"]):
-                    default = os.environ[envvar].split()
-                else:
-                    default = os.environ[envvar]
-                kwargs["help"] = (
-                    kwargs["help"] + f" (default from environment: {default})"
-                )
-        if required and default:
-            required = False
-        super(EnvDefault, self).__init__(
-            default=default, required=required, nargs=nargs, **kwargs
-        )
-
-    def __call__(self, parser, namespace, values, option_string=None):  # type: ignore
-        setattr(namespace, self.dest, values)
-
-
-if __name__ == "__main__":
-    arg_parser = argparse.ArgumentParser(prog="nixos-test-driver")
-    arg_parser.add_argument(
-        "-K",
-        "--keep-vm-state",
-        help="re-use a VM state coming from a previous run",
-        action="store_true",
-    )
-    arg_parser.add_argument(
-        "-I",
-        "--interactive",
-        help="drop into a python repl and run the tests interactively",
-        action="store_true",
-    )
-    arg_parser.add_argument(
-        "--start-scripts",
-        metavar="START-SCRIPT",
-        action=EnvDefault,
-        envvar="startScripts",
-        nargs="*",
-        help="start scripts for participating virtual machines",
-    )
-    arg_parser.add_argument(
-        "--vlans",
-        metavar="VLAN",
-        action=EnvDefault,
-        envvar="vlans",
-        nargs="*",
-        help="vlans to span by the driver",
-    )
-    arg_parser.add_argument(
-        "testscript",
-        action=EnvDefault,
-        envvar="testScript",
-        help="the test script to run",
-        type=Path,
-    )
-
-    args = arg_parser.parse_args()
-
-    if not args.keep_vm_state:
-        rootlog.info("Machine state will be reset. To keep it, pass --keep-vm-state")
-
-    with Driver(
-        args.start_scripts, args.vlans, args.testscript.read_text(), args.keep_vm_state
-    ) as driver:
-        if args.interactive:
-            ptpython.repl.embed(driver.test_symbols(), {})
-        else:
-            tic = time.time()
-            driver.run_tests()
-            toc = time.time()
-            rootlog.info(f"test script finished in {(toc-tic):.2f}s")
diff --git a/nixos/lib/test-driver/test_driver/vlan.py b/nixos/lib/test-driver/test_driver/vlan.py
new file mode 100644
index 0000000000000..e5c8f07b4edf4
--- /dev/null
+++ b/nixos/lib/test-driver/test_driver/vlan.py
@@ -0,0 +1,58 @@
+from pathlib import Path
+import io
+import os
+import pty
+import subprocess
+
+from test_driver.logger import rootlog
+
+
+class VLan:
+    """This class handles a VLAN that the run-vm scripts identify via its
+    number handles. The network's lifetime equals the object's lifetime.
+    """
+
+    nr: int
+    socket_dir: Path
+
+    process: subprocess.Popen
+    pid: int
+    fd: io.TextIOBase
+
+    def __repr__(self) -> str:
+        return f"<Vlan Nr. {self.nr}>"
+
+    def __init__(self, nr: int, tmp_dir: Path):
+        self.nr = nr
+        self.socket_dir = tmp_dir / f"vde{self.nr}.ctl"
+
+        # TODO: don't side-effect environment here
+        os.environ[f"QEMU_VDE_SOCKET_{self.nr}"] = str(self.socket_dir)
+
+        rootlog.info("start vlan")
+        pty_master, pty_slave = pty.openpty()
+
+        self.process = subprocess.Popen(
+            ["vde_switch", "-s", self.socket_dir, "--dirmode", "0700"],
+            stdin=pty_slave,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+            shell=False,
+        )
+        self.pid = self.process.pid
+        self.fd = os.fdopen(pty_master, "w")
+        self.fd.write("version\n")
+
+        # TODO: perl version checks if this can be read from
+        # an if not, dies. we could hang here forever. Fix it.
+        assert self.process.stdout is not None
+        self.process.stdout.readline()
+        if not (self.socket_dir / "ctl").exists():
+            rootlog.error("cannot start vde_switch")
+
+        rootlog.info(f"running vlan (pid {self.pid})")
+
+    def __del__(self) -> None:
+        rootlog.info(f"kill vlan (pid {self.pid})")
+        self.fd.close()
+        self.process.terminate()
diff --git a/nixos/lib/testing-python.nix b/nixos/lib/testing-python.nix
index 4306d102b2d64..365e22714573e 100644
--- a/nixos/lib/testing-python.nix
+++ b/nixos/lib/testing-python.nix
@@ -16,65 +16,6 @@ rec {
 
   inherit pkgs;
 
-  # Reifies and correctly wraps the python test driver for
-  # the respective qemu version and with or without ocr support
-  pythonTestDriver = {
-      qemu_pkg ? pkgs.qemu_test
-    , enableOCR ? false
-  }:
-    let
-      name = "nixos-test-driver";
-      testDriverScript = ./test-driver/test-driver.py;
-      ocrProg = tesseract4.override { enableLanguages = [ "eng" ]; };
-      imagemagick_tiff = imagemagick_light.override { inherit libtiff; };
-    in stdenv.mkDerivation {
-      inherit name;
-
-      nativeBuildInputs = [ makeWrapper ];
-      buildInputs = [ (python3.withPackages (p: [ p.ptpython p.colorama ])) ];
-      checkInputs = with python3Packages; [ pylint black mypy ];
-
-      dontUnpack = true;
-
-      preferLocalBuild = true;
-
-      buildPhase = ''
-        python <<EOF
-        from pydoc import importfile
-        with open('driver-symbols', 'w') as fp:
-          t = importfile('${testDriverScript}')
-          d = t.Driver([],[],"")
-          test_symbols = d.test_symbols()
-          fp.write(','.join(test_symbols.keys()))
-        EOF
-      '';
-
-      doCheck = true;
-      checkPhase = ''
-        mypy --disallow-untyped-defs \
-             --no-implicit-optional \
-             --ignore-missing-imports ${testDriverScript}
-        pylint --errors-only ${testDriverScript}
-        black --check --diff ${testDriverScript}
-      '';
-
-      installPhase =
-        ''
-          mkdir -p $out/bin
-          cp ${testDriverScript} $out/bin/nixos-test-driver
-          chmod u+x $out/bin/nixos-test-driver
-          # TODO: copy user script part into this file (append)
-
-          wrapProgram $out/bin/nixos-test-driver \
-            --argv0 ${name} \
-            --prefix PATH : "${lib.makeBinPath [ qemu_pkg vde2 netpbm coreutils socat ]}" \
-            ${lib.optionalString enableOCR
-              "--prefix PATH : '${ocrProg}/bin:${imagemagick_tiff}/bin'"} \
-
-          install -m 0644 -vD driver-symbols $out/nix-support/driver-symbols
-        '';
-    };
-
   # Run an automated test suite in the given virtual network.
   runTests = { driver, pos }:
     stdenv.mkDerivation {
@@ -112,8 +53,15 @@ rec {
     , passthru ? {}
   }:
     let
-      # FIXME: get this pkg from the module system
-      testDriver = pythonTestDriver { inherit qemu_pkg enableOCR;};
+      # Reifies and correctly wraps the python test driver for
+      # the respective qemu version and with or without ocr support
+      testDriver = pkgs.callPackage ./test-driver {
+        inherit enableOCR;
+        qemu_pkg = qemu_test;
+        imagemagick_light = imagemagick_light.override { inherit libtiff; };
+        tesseract4 = tesseract4.override { enableLanguages = [ "eng" ]; };
+      };
+
 
       testDriverName =
         let
@@ -178,10 +126,11 @@ rec {
         echo -n "$testScript" > $out/test-script
         ln -s ${testDriver}/bin/nixos-test-driver $out/bin/nixos-test-driver
 
+        ${testDriver}/bin/generate-driver-symbols
         ${lib.optionalString (!skipLint) ''
           PYFLAKES_BUILTINS="$(
             echo -n ${lib.escapeShellArg (lib.concatStringsSep "," nodeHostNames)},
-            < ${lib.escapeShellArg "${testDriver}/nix-support/driver-symbols"}
+            < ${lib.escapeShellArg "driver-symbols"}
           )" ${python3Packages.pyflakes}/bin/pyflakes $out/test-script
         ''}
 
diff --git a/nixos/lib/utils.nix b/nixos/lib/utils.nix
index f1fa9f07a9742..bbebf8ba35a01 100644
--- a/nixos/lib/utils.nix
+++ b/nixos/lib/utils.nix
@@ -1,4 +1,4 @@
-pkgs: with pkgs.lib;
+{ lib, config, pkgs }: with lib;
 
 rec {
 
@@ -165,4 +165,9 @@ rec {
       ${builtins.toJSON set}
       EOF
     '';
+
+  systemdUtils = {
+    lib = import ./systemd-lib.nix { inherit lib config pkgs; };
+    unitOptions = import ./systemd-unit-options.nix { inherit lib systemdUtils; };
+  };
 }
diff --git a/nixos/modules/config/networking.nix b/nixos/modules/config/networking.nix
index 11307e331200b..133a150df82c3 100644
--- a/nixos/modules/config/networking.nix
+++ b/nixos/modules/config/networking.nix
@@ -1,12 +1,13 @@
 # /etc files related to networking, such as /etc/services.
 
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
 
   cfg = config.networking;
+  opt = options.networking;
 
   localhostMultiple = any (elem "localhost") (attrValues (removeAttrs cfg.hosts [ "127.0.0.1" "::1" ]));
 
@@ -78,6 +79,7 @@ in
       httpProxy = lib.mkOption {
         type = types.nullOr types.str;
         default = cfg.proxy.default;
+        defaultText = literalExpression "config.${opt.proxy.default}";
         description = ''
           This option specifies the http_proxy environment variable.
         '';
@@ -87,6 +89,7 @@ in
       httpsProxy = lib.mkOption {
         type = types.nullOr types.str;
         default = cfg.proxy.default;
+        defaultText = literalExpression "config.${opt.proxy.default}";
         description = ''
           This option specifies the https_proxy environment variable.
         '';
@@ -96,6 +99,7 @@ in
       ftpProxy = lib.mkOption {
         type = types.nullOr types.str;
         default = cfg.proxy.default;
+        defaultText = literalExpression "config.${opt.proxy.default}";
         description = ''
           This option specifies the ftp_proxy environment variable.
         '';
@@ -105,6 +109,7 @@ in
       rsyncProxy = lib.mkOption {
         type = types.nullOr types.str;
         default = cfg.proxy.default;
+        defaultText = literalExpression "config.${opt.proxy.default}";
         description = ''
           This option specifies the rsync_proxy environment variable.
         '';
@@ -114,6 +119,7 @@ in
       allProxy = lib.mkOption {
         type = types.nullOr types.str;
         default = cfg.proxy.default;
+        defaultText = literalExpression "config.${opt.proxy.default}";
         description = ''
           This option specifies the all_proxy environment variable.
         '';
diff --git a/nixos/modules/config/system-path.nix b/nixos/modules/config/system-path.nix
index 6ff4ec2921cf8..875c4c9c44155 100644
--- a/nixos/modules/config/system-path.nix
+++ b/nixos/modules/config/system-path.nix
@@ -41,12 +41,17 @@ let
       pkgs.zstd
     ];
 
-    defaultPackages = map (pkg: setPrio ((pkg.meta.priority or 5) + 3) pkg)
-      [ pkgs.nano
-        pkgs.perl
-        pkgs.rsync
-        pkgs.strace
-      ];
+  defaultPackageNames =
+    [ "nano"
+      "perl"
+      "rsync"
+      "strace"
+    ];
+  defaultPackages =
+    map
+      (n: let pkg = pkgs.${n}; in setPrio ((pkg.meta.priority or 5) + 3) pkg)
+      defaultPackageNames;
+  defaultPackagesText = "[ ${concatMapStringsSep " " (n: "pkgs.${n}") defaultPackageNames } ]";
 
 in
 
@@ -73,6 +78,11 @@ in
       defaultPackages = mkOption {
         type = types.listOf types.package;
         default = defaultPackages;
+        defaultText = literalDocBook ''
+          these packages, with their <literal>meta.priority</literal> numerically increased
+          (thus lowering their installation priority):
+          <programlisting>${defaultPackagesText}</programlisting>
+        '';
         example = [];
         description = ''
           Set of default packages that aren't strictly necessary
diff --git a/nixos/modules/hardware/cpu/intel-sgx.nix b/nixos/modules/hardware/cpu/intel-sgx.nix
new file mode 100644
index 0000000000000..046479400587f
--- /dev/null
+++ b/nixos/modules/hardware/cpu/intel-sgx.nix
@@ -0,0 +1,47 @@
+{ config, lib, ... }:
+with lib;
+let
+  cfg = config.hardware.cpu.intel.sgx.provision;
+  defaultGroup = "sgx_prv";
+in
+{
+  options.hardware.cpu.intel.sgx.provision = {
+    enable = mkEnableOption "access to the Intel SGX provisioning device";
+    user = mkOption {
+      description = "Owner to assign to the SGX provisioning device.";
+      type = types.str;
+      default = "root";
+    };
+    group = mkOption {
+      description = "Group to assign to the SGX provisioning device.";
+      type = types.str;
+      default = defaultGroup;
+    };
+    mode = mkOption {
+      description = "Mode to set for the SGX provisioning device.";
+      type = types.str;
+      default = "0660";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = hasAttr cfg.user config.users.users;
+        message = "Given user does not exist";
+      }
+      {
+        assertion = (cfg.group == defaultGroup) || (hasAttr cfg.group config.users.groups);
+        message = "Given group does not exist";
+      }
+    ];
+
+    users.groups = optionalAttrs (cfg.group == defaultGroup) {
+      "${cfg.group}" = { };
+    };
+
+    services.udev.extraRules = ''
+      SUBSYSTEM=="misc", KERNEL=="sgx_provision", OWNER="${cfg.user}", GROUP="${cfg.group}", MODE="${cfg.mode}"
+    '';
+  };
+}
diff --git a/nixos/modules/hardware/gpgsmartcards.nix b/nixos/modules/hardware/gpgsmartcards.nix
new file mode 100644
index 0000000000000..6e5fcda6b8519
--- /dev/null
+++ b/nixos/modules/hardware/gpgsmartcards.nix
@@ -0,0 +1,37 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  # gnupg's manual describes how to setup ccid udev rules:
+  #   https://www.gnupg.org/howtos/card-howto/en/ch02s03.html
+  # gnupg folks advised me (https://dev.gnupg.org/T5409) to look at debian's rules:
+  # https://salsa.debian.org/debian/gnupg2/-/blob/debian/main/debian/scdaemon.udev
+
+  # the latest rev of the entire debian gnupg2 repo as of 2021-04-28
+  # the scdaemon.udev file was last commited on 2021-01-05 (7817a03):
+  scdaemonUdevRev = "01898735a015541e3ffb43c7245ac1e612f40836";
+
+  scdaemonRules = pkgs.fetchurl {
+    url = "https://salsa.debian.org/debian/gnupg2/-/raw/${scdaemonUdevRev}/debian/scdaemon.udev";
+    sha256 = "08v0vp6950bz7galvc92zdss89y9vcwbinmbfcdldy8x72w6rqr3";
+  };
+
+  # per debian's udev deb hook (https://man7.org/linux/man-pages/man1/dh_installudev.1.html)
+  destination = "60-scdaemon.rules";
+
+  scdaemonUdevRulesPkg = pkgs.runCommandNoCC "scdaemon-udev-rules" {} ''
+    loc="$out/lib/udev/rules.d/"
+    mkdir -p "''${loc}"
+    cp "${scdaemonRules}" "''${loc}/${destination}"
+  '';
+
+  cfg = config.hardware.gpgSmartcards;
+in {
+  options.hardware.gpgSmartcards = {
+    enable = mkEnableOption "udev rules for gnupg smart cards";
+  };
+
+  config = mkIf cfg.enable {
+    services.udev.packages = [ scdaemonUdevRulesPkg ];
+  };
+}
diff --git a/nixos/modules/hardware/keyboard/zsa.nix b/nixos/modules/hardware/keyboard/zsa.nix
index 5cb09e5af499f..bb69cfa0bf091 100644
--- a/nixos/modules/hardware/keyboard/zsa.nix
+++ b/nixos/modules/hardware/keyboard/zsa.nix
@@ -5,7 +5,6 @@ let
   cfg = config.hardware.keyboard.zsa;
 in
 {
-  # TODO: make group configurable like in https://github.com/NixOS/nixpkgs/blob/0b2b4b8c4e729535a61db56468809c5c2d3d175c/pkgs/tools/security/nitrokey-app/udev-rules.nix ?
   options.hardware.keyboard.zsa = {
     enable = mkOption {
       type = types.bool;
@@ -14,7 +13,6 @@ in
         Enables udev rules for keyboards from ZSA like the ErgoDox EZ, Planck EZ and Moonlander Mark I.
         You need it when you want to flash a new configuration on the keyboard
         or use their live training in the browser.
-        Access to the keyboard is granted to users in the "plugdev" group.
         You may want to install the wally-cli package.
       '';
     };
@@ -22,6 +20,5 @@ in
 
   config = mkIf cfg.enable {
     services.udev.packages = [ pkgs.zsa-udev-rules ];
-    users.groups.plugdev = {};
   };
 }
diff --git a/nixos/modules/hardware/pcmcia.nix b/nixos/modules/hardware/pcmcia.nix
index d7d002ae6c8a7..aef35a28e54da 100644
--- a/nixos/modules/hardware/pcmcia.nix
+++ b/nixos/modules/hardware/pcmcia.nix
@@ -35,6 +35,7 @@ in
 
       config = mkOption {
         default = null;
+        type = types.nullOr types.path;
         description = ''
           Path to the configuration file which maps the memory, IRQs
           and ports used by the PCMCIA hardware.
diff --git a/nixos/modules/hardware/system-76.nix b/nixos/modules/hardware/system-76.nix
index d4896541dbae4..ca40ee0ebb37a 100644
--- a/nixos/modules/hardware/system-76.nix
+++ b/nixos/modules/hardware/system-76.nix
@@ -1,8 +1,9 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 let
-  inherit (lib) mkOption mkEnableOption types mkIf mkMerge optional versionOlder;
+  inherit (lib) literalExpression mkOption mkEnableOption types mkIf mkMerge optional versionOlder;
   cfg = config.hardware.system76;
+  opt = options.hardware.system76;
 
   kpkgs = config.boot.kernelPackages;
   modules = [ "system76" "system76-io" ] ++ (optional (versionOlder kpkgs.kernel.version "5.5") "system76-acpi");
@@ -60,6 +61,7 @@ in {
 
       firmware-daemon.enable = mkOption {
         default = cfg.enableAll;
+        defaultText = literalExpression "config.${opt.enableAll}";
         example = true;
         description = "Whether to enable the system76 firmware daemon";
         type = types.bool;
@@ -67,6 +69,7 @@ in {
 
       kernel-modules.enable = mkOption {
         default = cfg.enableAll;
+        defaultText = literalExpression "config.${opt.enableAll}";
         example = true;
         description = "Whether to make the system76 out-of-tree kernel modules available";
         type = types.bool;
@@ -74,6 +77,7 @@ in {
 
       power-daemon.enable = mkOption {
         default = cfg.enableAll;
+        defaultText = literalExpression "config.${opt.enableAll}";
         example = true;
         description = "Whether to enable the system76 power daemon";
         type = types.bool;
diff --git a/nixos/modules/misc/documentation.nix b/nixos/modules/misc/documentation.nix
index 1f837f9efa22d..64b1c15086fc8 100644
--- a/nixos/modules/misc/documentation.nix
+++ b/nixos/modules/misc/documentation.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, baseModules, extraModules, modules, modulesPath, ... }:
+{ config, lib, pkgs, extendModules, noUserModules, ... }:
 
 with lib;
 
@@ -6,11 +6,8 @@ let
 
   cfg = config.documentation;
 
-  manualModules =
-    baseModules
-    # Modules for which to show options even when not imported
-    ++ [ ../virtualisation/qemu-vm.nix ]
-    ++ optionals cfg.nixos.includeAllModules (extraModules ++ modules);
+  /* Modules for which to show options even when not imported. */
+  extraDocModules = [ ../virtualisation/qemu-vm.nix ];
 
   /* For the purpose of generating docs, evaluate options with each derivation
     in `pkgs` (recursively) replaced by a fake with path "\${pkgs.attribute.path}".
@@ -24,13 +21,10 @@ let
     extraSources = cfg.nixos.extraModuleSources;
     options =
       let
-        scrubbedEval = evalModules {
-          modules = [ { nixpkgs.localSystem = config.nixpkgs.localSystem; } ] ++ manualModules;
-          args = (config._module.args) // { modules = [ ]; };
-          specialArgs = {
-            pkgs = scrubDerivations "pkgs" pkgs;
-            inherit modulesPath;
-          };
+        extendNixOS = if cfg.nixos.includeAllModules then extendModules else noUserModules.extendModules;
+        scrubbedEval = extendNixOS {
+          modules = extraDocModules;
+          specialArgs.pkgs = scrubDerivations "pkgs" pkgs;
         };
         scrubDerivations = namePrefix: pkgSet: mapAttrs
           (name: value:
diff --git a/nixos/modules/misc/extra-arguments.nix b/nixos/modules/misc/extra-arguments.nix
index 8716e3d9fef22..48891b4404986 100644
--- a/nixos/modules/misc/extra-arguments.nix
+++ b/nixos/modules/misc/extra-arguments.nix
@@ -1,7 +1,7 @@
-{ pkgs, ... }:
+{ lib, config, pkgs, ... }:
 
 {
   _module.args = {
-    utils = import ../../lib/utils.nix pkgs;
+    utils = import ../../lib/utils.nix { inherit lib config pkgs; };
   };
 }
diff --git a/nixos/modules/misc/version.nix b/nixos/modules/misc/version.nix
index 8f246a9278b70..fc0d65d5148eb 100644
--- a/nixos/modules/misc/version.nix
+++ b/nixos/modules/misc/version.nix
@@ -1,9 +1,10 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
   cfg = config.system.nixos;
+  opt = options.system.nixos;
 in
 
 {
@@ -53,6 +54,7 @@ in
     stateVersion = mkOption {
       type = types.str;
       default = cfg.release;
+      defaultText = literalExpression "config.${opt.release}";
       description = ''
         Every once in a while, a new NixOS release may change
         configuration defaults in a way incompatible with stateful
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index f36e7dd67eaee..dd6a74df30cb9 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -45,11 +45,13 @@
   ./hardware/ckb-next.nix
   ./hardware/cpu/amd-microcode.nix
   ./hardware/cpu/intel-microcode.nix
+  ./hardware/cpu/intel-sgx.nix
   ./hardware/corectrl.nix
   ./hardware/digitalbitbox.nix
   ./hardware/device-tree.nix
   ./hardware/gkraken.nix
   ./hardware/flirc.nix
+  ./hardware/gpgsmartcards.nix
   ./hardware/i2c.nix
   ./hardware/sensor/hddtemp.nix
   ./hardware/sensor/iio.nix
@@ -294,7 +296,6 @@
   ./services/cluster/hadoop/default.nix
   ./services/cluster/k3s/default.nix
   ./services/cluster/kubernetes/addons/dns.nix
-  ./services/cluster/kubernetes/addons/dashboard.nix
   ./services/cluster/kubernetes/addon-manager.nix
   ./services/cluster/kubernetes/apiserver.nix
   ./services/cluster/kubernetes/controller-manager.nix
@@ -446,6 +447,7 @@
   ./services/hardware/xow.nix
   ./services/logging/SystemdJournal2Gelf.nix
   ./services/logging/awstats.nix
+  ./services/logging/filebeat.nix
   ./services/logging/fluentd.nix
   ./services/logging/graylog.nix
   ./services/logging/heartbeat.nix
@@ -467,6 +469,7 @@
   ./services/mail/dovecot.nix
   ./services/mail/dspam.nix
   ./services/mail/exim.nix
+  ./services/mail/maddy.nix
   ./services/mail/mail.nix
   ./services/mail/mailcatcher.nix
   ./services/mail/mailhog.nix
@@ -926,6 +929,7 @@
   ./services/search/kibana.nix
   ./services/search/meilisearch.nix
   ./services/search/solr.nix
+  ./services/security/aesmd.nix
   ./services/security/certmgr.nix
   ./services/security/cfssl.nix
   ./services/security/clamav.nix
@@ -1021,6 +1025,7 @@
   ./services/web-apps/plantuml-server.nix
   ./services/web-apps/plausible.nix
   ./services/web-apps/pgpkeyserver-lite.nix
+  ./services/web-apps/powerdns-admin.nix
   ./services/web-apps/matomo.nix
   ./services/web-apps/moinmoin.nix
   ./services/web-apps/openwebrx.nix
@@ -1181,6 +1186,7 @@
   ./virtualisation/oci-containers.nix
   ./virtualisation/cri-o.nix
   ./virtualisation/docker.nix
+  ./virtualisation/docker-rootless.nix
   ./virtualisation/ecs-agent.nix
   ./virtualisation/libvirtd.nix
   ./virtualisation/lxc.nix
@@ -1191,8 +1197,7 @@
   ./virtualisation/kvmgt.nix
   ./virtualisation/openvswitch.nix
   ./virtualisation/parallels-guest.nix
-  ./virtualisation/podman.nix
-  ./virtualisation/podman-network-socket-ghostunnel.nix
+  ./virtualisation/podman/default.nix
   ./virtualisation/qemu-guest-agent.nix
   ./virtualisation/railcar.nix
   ./virtualisation/spice-usb-redirection.nix
diff --git a/nixos/modules/programs/captive-browser.nix b/nixos/modules/programs/captive-browser.nix
index 0f5d087e8d87e..dc054504ea48c 100644
--- a/nixos/modules/programs/captive-browser.nix
+++ b/nixos/modules/programs/captive-browser.nix
@@ -3,6 +3,18 @@
 with lib;
 let
   cfg = config.programs.captive-browser;
+  browserDefault = chromium: concatStringsSep " " [
+    ''env XDG_CONFIG_HOME="$PREV_CONFIG_HOME"''
+    ''${chromium}/bin/chromium''
+    ''--user-data-dir=''${XDG_DATA_HOME:-$HOME/.local/share}/chromium-captive''
+    ''--proxy-server="socks5://$PROXY"''
+    ''--host-resolver-rules="MAP * ~NOTFOUND , EXCLUDE localhost"''
+    ''--no-first-run''
+    ''--new-window''
+    ''--incognito''
+    ''-no-default-browser-check''
+    ''http://cache.nixos.org/''
+  ];
 in
 {
   ###### interface
@@ -26,18 +38,8 @@ in
       # the options below are the same as in "captive-browser.toml"
       browser = mkOption {
         type = types.str;
-        default = concatStringsSep " " [
-          ''env XDG_CONFIG_HOME="$PREV_CONFIG_HOME"''
-          ''${pkgs.chromium}/bin/chromium''
-          ''--user-data-dir=''${XDG_DATA_HOME:-$HOME/.local/share}/chromium-captive''
-          ''--proxy-server="socks5://$PROXY"''
-          ''--host-resolver-rules="MAP * ~NOTFOUND , EXCLUDE localhost"''
-          ''--no-first-run''
-          ''--new-window''
-          ''--incognito''
-          ''-no-default-browser-check''
-          ''http://cache.nixos.org/''
-        ];
+        default = browserDefault pkgs.chromium;
+        defaultText = literalExpression (browserDefault "\${pkgs.chromium}");
         description = ''
           The shell (/bin/sh) command executed once the proxy starts.
           When browser exits, the proxy exits. An extra env var PROXY is available.
diff --git a/nixos/modules/programs/dconf.nix b/nixos/modules/programs/dconf.nix
index 265c41cbbbc99..298abac8afa98 100644
--- a/nixos/modules/programs/dconf.nix
+++ b/nixos/modules/programs/dconf.nix
@@ -60,7 +60,7 @@ in
     environment.systemPackages = [ pkgs.dconf ];
 
     # Needed for unwrapped applications
-    environment.sessionVariables.GIO_EXTRA_MODULES = mkIf cfg.enable [ "${pkgs.dconf.lib}/lib/gio/modules" ];
+    environment.variables.GIO_EXTRA_MODULES = mkIf cfg.enable [ "${pkgs.dconf.lib}/lib/gio/modules" ];
   };
 
 }
diff --git a/nixos/modules/programs/gnupg.nix b/nixos/modules/programs/gnupg.nix
index 06f49182e4df1..fe5d7bd834b22 100644
--- a/nixos/modules/programs/gnupg.nix
+++ b/nixos/modules/programs/gnupg.nix
@@ -71,6 +71,7 @@ in
       type = types.nullOr (types.enum pkgs.pinentry.flavors);
       example = "gnome3";
       default = defaultPinentryFlavor;
+      defaultText = literalDocBook ''matching the configured desktop environment'';
       description = ''
         Which pinentry interface to use. If not null, the path to the
         pinentry binary will be passed to gpg-agent via commandline and
diff --git a/nixos/modules/programs/qt5ct.nix b/nixos/modules/programs/qt5ct.nix
index 3f2bcf6228369..88e861bf4031a 100644
--- a/nixos/modules/programs/qt5ct.nix
+++ b/nixos/modules/programs/qt5ct.nix
@@ -26,6 +26,6 @@ with lib;
   ###### implementation
   config = mkIf config.programs.qt5ct.enable {
     environment.variables.QT_QPA_PLATFORMTHEME = "qt5ct";
-    environment.systemPackages = with pkgs; [ qt5ct ];
+    environment.systemPackages = with pkgs; [ libsForQt5.qt5ct ];
   };
 }
diff --git a/nixos/modules/programs/ssh.nix b/nixos/modules/programs/ssh.nix
index 5da15b68cf7d7..c680063a47c34 100644
--- a/nixos/modules/programs/ssh.nix
+++ b/nixos/modules/programs/ssh.nix
@@ -33,6 +33,13 @@ in
 
     programs.ssh = {
 
+      enableAskPassword = mkOption {
+        type = types.bool;
+        default = config.services.xserver.enable;
+        defaultText = literalExpression "config.services.xserver.enable";
+        description = "Whether to configure SSH_ASKPASS in the environment.";
+      };
+
       askPassword = mkOption {
         type = types.str;
         default = "${pkgs.x11_ssh_askpass}/libexec/x11-ssh-askpass";
@@ -287,7 +294,7 @@ in
         # Allow ssh-agent to ask for confirmation. This requires the
         # unit to know about the user's $DISPLAY (via ‘systemctl
         # import-environment’).
-        environment.SSH_ASKPASS = optionalString config.services.xserver.enable askPasswordWrapper;
+        environment.SSH_ASKPASS = optionalString cfg.enableAskPassword askPasswordWrapper;
         environment.DISPLAY = "fake"; # required to make ssh-agent start $SSH_ASKPASS
       };
 
@@ -298,7 +305,7 @@ in
         fi
       '';
 
-    environment.variables.SSH_ASKPASS = optionalString config.services.xserver.enable askPassword;
+    environment.variables.SSH_ASKPASS = optionalString cfg.enableAskPassword askPassword;
 
   };
 }
diff --git a/nixos/modules/programs/zsh/zsh.nix b/nixos/modules/programs/zsh/zsh.nix
index e5c5b08f8d4da..5fe98b6801bbb 100644
--- a/nixos/modules/programs/zsh/zsh.nix
+++ b/nixos/modules/programs/zsh/zsh.nix
@@ -1,6 +1,6 @@
 # This module defines global configuration for the zshell.
 
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
@@ -9,6 +9,7 @@ let
   cfge = config.environment;
 
   cfg = config.programs.zsh;
+  opt = options.programs.zsh;
 
   zshAliases = concatStringsSep "\n" (
     mapAttrsFlatten (k: v: "alias ${k}=${escapeShellArg v}")
@@ -147,6 +148,7 @@ in
 
       enableGlobalCompInit = mkOption {
         default = cfg.enableCompletion;
+        defaultText = literalExpression "config.${opt.enableCompletion}";
         description = ''
           Enable execution of compinit call for all interactive zsh shells.
 
diff --git a/nixos/modules/security/acme.nix b/nixos/modules/security/acme.nix
index 2815e2593b23f..e244989d64086 100644
--- a/nixos/modules/security/acme.nix
+++ b/nixos/modules/security/acme.nix
@@ -2,6 +2,8 @@
 with lib;
 let
   cfg = config.security.acme;
+  opt = options.security.acme;
+  user = if cfg.useRoot then "root" else "acme";
 
   # Used to calculate timer accuracy for coalescing
   numCerts = length (builtins.attrNames cfg.certs);
@@ -22,7 +24,7 @@ let
   # security.acme.certs.<cert>.group on some of the services.
   commonServiceConfig = {
     Type = "oneshot";
-    User = "acme";
+    User = user;
     Group = mkDefault "acme";
     UMask = 0022;
     StateDirectoryMode = 750;
@@ -100,12 +102,12 @@ let
   # is configurable on a per-cert basis.
   userMigrationService = let
     script = with builtins; ''
-      chown -R acme .lego/accounts
+      chown -R ${user} .lego/accounts
     '' + (concatStringsSep "\n" (mapAttrsToList (cert: data: ''
       for fixpath in ${escapeShellArg cert} .lego/${escapeShellArg cert}; do
         if [ -d "$fixpath" ]; then
           chmod -R u=rwX,g=rX,o= "$fixpath"
-          chown -R acme:${data.group} "$fixpath"
+          chown -R ${user}:${data.group} "$fixpath"
         fi
       done
     '') certConfigs));
@@ -127,7 +129,7 @@ let
   };
 
   certToConfig = cert: data: let
-    acmeServer = if data.server != null then data.server else cfg.server;
+    acmeServer = data.server;
     useDns = data.dnsProvider != null;
     destPath = "/var/lib/acme/${cert}";
     selfsignedDeps = optionals (cfg.preliminarySelfsigned) [ "acme-selfsigned-${cert}.service" ];
@@ -155,6 +157,7 @@ let
       ${toString data.ocspMustStaple} ${data.keyType}
     '';
     certDir = mkHash hashData;
+    # TODO remove domainHash usage entirely. Waiting on go-acme/lego#1532
     domainHash = mkHash "${concatStringsSep " " extraDomains} ${data.domain}";
     accountHash = (mkAccountHash acmeServer data);
     accountDir = accountDirRoot + accountHash;
@@ -163,9 +166,8 @@ let
       [ "--dns" data.dnsProvider ]
       ++ optionals (!data.dnsPropagationCheck) [ "--dns.disable-cp" ]
       ++ optionals (data.dnsResolver != null) [ "--dns.resolvers" data.dnsResolver ]
-    ) else (
-      [ "--http" "--http.webroot" data.webroot ]
-    );
+    ) else if data.listenHTTP != null then [ "--http" "--http.port" data.listenHTTP ]
+    else [ "--http" "--http.webroot" data.webroot ];
 
     commonOpts = [
       "--accept-tos" # Checking the option is covered by the assertions
@@ -210,7 +212,7 @@ let
       description = "Renew ACME Certificate for ${cert}";
       wantedBy = [ "timers.target" ];
       timerConfig = {
-        OnCalendar = cfg.renewInterval;
+        OnCalendar = data.renewInterval;
         Unit = "acme-${cert}.service";
         Persistent = "yes";
 
@@ -267,7 +269,7 @@ let
         cat key.pem fullchain.pem > full.pem
 
         # Group might change between runs, re-apply it
-        chown 'acme:${data.group}' *
+        chown '${user}:${data.group}' *
 
         # Default permissions make the files unreadable by group + anon
         # Need to be readable by group
@@ -321,11 +323,14 @@ let
             }
           fi
         '');
+      } // optionalAttrs (data.listenHTTP != null && toInt (elemAt (splitString ":" data.listenHTTP) 1) < 1024) {
+        CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
       };
 
       # Working directory will be /tmp
       script = ''
-        set -euxo pipefail
+        ${optionalString data.enableDebugLogs "set -x"}
+        set -euo pipefail
 
         # This reimplements the expiration date check, but without querying
         # the acme server first. By doing this offline, we avoid errors
@@ -352,7 +357,7 @@ let
           expiration_s=$[expiration_date - now]
           expiration_days=$[expiration_s / (3600 * 24)]   # rounds down
 
-          [[ $expiration_days -gt ${toString cfg.validMinDays} ]]
+          [[ $expiration_days -gt ${toString data.validMinDays} ]]
         }
 
         ${optionalString (data.webroot != null) ''
@@ -369,37 +374,40 @@ let
 
         echo '${domainHash}' > domainhash.txt
 
-        # Check if we can renew
-        if [ -e 'certificates/${keyName}.key' -a -e 'certificates/${keyName}.crt' -a -n "$(ls -1 accounts)" ]; then
+        # Check if we can renew.
+        # We can only renew if the list of domains has not changed.
+        if cmp -s domainhash.txt certificates/domainhash.txt && [ -e 'certificates/${keyName}.key' -a -e 'certificates/${keyName}.crt' -a -n "$(ls -1 accounts)" ]; then
 
-          # When domains are updated, there's no need to do a full
-          # Lego run, but it's likely renew won't work if days is too low.
-          if [ -e certificates/domainhash.txt ] && cmp -s domainhash.txt certificates/domainhash.txt; then
+          # Even if a cert is not expired, it may be revoked by the CA.
+          # Try to renew, and silently fail if the cert is not expired.
+          # Avoids #85794 and resolves #129838
+          if ! lego ${renewOpts} --days ${toString data.validMinDays}; then
             if is_expiration_skippable out/full.pem; then
-              echo 1>&2 "nixos-acme: skipping renewal because expiration isn't within the coming ${toString cfg.validMinDays} days"
+              echo 1>&2 "nixos-acme: Ignoring failed renewal because expiration isn't within the coming ${toString data.validMinDays} days"
             else
-              echo 1>&2 "nixos-acme: renewing now, because certificate expires within the configured ${toString cfg.validMinDays} days"
-              lego ${renewOpts} --days ${toString cfg.validMinDays}
+              # High number to avoid Systemd reserved codes.
+              exit 11
             fi
-          else
-            echo 1>&2 "certificate domain(s) have changed; will renew now"
-            # Any number > 90 works, but this one is over 9000 ;-)
-            lego ${renewOpts} --days 9001
           fi
 
         # Otherwise do a full run
-        else
-          lego ${runOpts}
+        elif ! lego ${runOpts}; then
+          # Produce a nice error for those doing their first nixos-rebuild with these certs
+          echo Failed to fetch certificates. \
+            This may mean your DNS records are set up incorrectly. \
+            ${optionalString (cfg.preliminarySelfsigned) "Selfsigned certs are in place and dependant services will still start."}
+          # Exit 10 so that users can potentially amend SuccessExitStatus to ignore this error.
+          # High number to avoid Systemd reserved codes.
+          exit 10
         fi
 
         mv domainhash.txt certificates/
 
         # Group might change between runs, re-apply it
-        chown 'acme:${data.group}' certificates/*
+        chown '${user}:${data.group}' certificates/*
 
         # Copy all certs to the "real" certs directory
-        CERT='certificates/${keyName}.crt'
-        if [ -e "$CERT" ] && ! cmp -s "$CERT" out/fullchain.pem; then
+        if ! cmp -s 'certificates/${keyName}.crt' out/fullchain.pem; then
           touch out/renewed
           echo Installing new certificate
           cp -vp 'certificates/${keyName}.crt' out/fullchain.pem
@@ -418,29 +426,45 @@ let
 
   certConfigs = mapAttrs certToConfig cfg.certs;
 
-  certOpts = { name, ... }: {
+  # These options can be specified within
+  # security.acme.defaults or security.acme.certs.<name>
+  inheritableModule = isDefaults: { config, ... }: let
+    defaultAndText = name: default: {
+      # When ! isDefaults then this is the option declaration for the
+      # security.acme.certs.<name> path, which has the extra inheritDefaults
+      # option, which if disabled means that we can't inherit it
+      default = if isDefaults || ! config.inheritDefaults then default else cfg.defaults.${name};
+      # The docs however don't need to depend on inheritDefaults, they should
+      # stay constant. Though notably it wouldn't matter much, because to get
+      # the option information, a submodule with name `<name>` is evaluated
+      # without any definitions.
+      defaultText = if isDefaults then default else literalExpression "config.security.acme.defaults.${name}";
+    };
+  in {
     options = {
-      # user option has been removed
-      user = mkOption {
-        visible = false;
-        default = "_mkRemovedOptionModule";
+      validMinDays = mkOption {
+        type = types.int;
+        inherit (defaultAndText "validMinDays" 30) default defaultText;
+        description = "Minimum remaining validity before renewal in days.";
       };
 
-      # allowKeysForGroup option has been removed
-      allowKeysForGroup = mkOption {
-        visible = false;
-        default = "_mkRemovedOptionModule";
+      renewInterval = mkOption {
+        type = types.str;
+        inherit (defaultAndText "renewInterval" "daily") default defaultText;
+        description = ''
+          Systemd calendar expression when to check for renewal. See
+          <citerefentry><refentrytitle>systemd.time</refentrytitle>
+          <manvolnum>7</manvolnum></citerefentry>.
+        '';
       };
 
-      # extraDomains was replaced with extraDomainNames
-      extraDomains = mkOption {
-        visible = false;
-        default = "_mkMergedOptionModule";
+      enableDebugLogs = mkEnableOption "debug logging for this certificate" // {
+        inherit (defaultAndText "enableDebugLogs" true) default defaultText;
       };
 
       webroot = mkOption {
         type = types.nullOr types.str;
-        default = null;
+        inherit (defaultAndText "webroot" null) default defaultText;
         example = "/var/lib/acme/acme-challenge";
         description = ''
           Where the webroot of the HTTP vhost is located.
@@ -453,7 +477,7 @@ let
 
       server = mkOption {
         type = types.nullOr types.str;
-        default = null;
+        inherit (defaultAndText "server" null) default defaultText;
         description = ''
           ACME Directory Resource URI. Defaults to Let's Encrypt's
           production endpoint,
@@ -461,27 +485,25 @@ let
         '';
       };
 
-      domain = mkOption {
-        type = types.str;
-        default = name;
-        description = "Domain to fetch certificate for (defaults to the entry name).";
-      };
-
       email = mkOption {
-        type = types.nullOr types.str;
-        default = cfg.email;
-        description = "Contact email address for the CA to be able to reach you.";
+        type = types.str;
+        inherit (defaultAndText "email" null) default defaultText;
+        description = ''
+          Email address for account creation and correspondence from the CA.
+          It is recommended to use the same email for all certs to avoid account
+          creation limits.
+        '';
       };
 
       group = mkOption {
         type = types.str;
-        default = "acme";
+        inherit (defaultAndText "group" "acme") default defaultText;
         description = "Group running the ACME client.";
       };
 
       reloadServices = mkOption {
         type = types.listOf types.str;
-        default = [];
+        inherit (defaultAndText "reloadServices" []) default defaultText;
         description = ''
           The list of systemd services to call <code>systemctl try-reload-or-restart</code>
           on.
@@ -490,7 +512,7 @@ let
 
       postRun = mkOption {
         type = types.lines;
-        default = "";
+        inherit (defaultAndText "postRun" "") default defaultText;
         example = "cp full.pem backup.pem";
         description = ''
           Commands to run after new certificates go live. Note that
@@ -500,30 +522,9 @@ let
         '';
       };
 
-      directory = mkOption {
-        type = types.str;
-        readOnly = true;
-        default = "/var/lib/acme/${name}";
-        description = "Directory where certificate and other state is stored.";
-      };
-
-      extraDomainNames = mkOption {
-        type = types.listOf types.str;
-        default = [];
-        example = literalExpression ''
-          [
-            "example.org"
-            "mydomain.org"
-          ]
-        '';
-        description = ''
-          A list of extra domain names, which are included in the one certificate to be issued.
-        '';
-      };
-
       keyType = mkOption {
         type = types.str;
-        default = "ec256";
+        inherit (defaultAndText "keyType" "ec256") default defaultText;
         description = ''
           Key type to use for private keys.
           For an up to date list of supported values check the --key-type option
@@ -533,7 +534,7 @@ let
 
       dnsProvider = mkOption {
         type = types.nullOr types.str;
-        default = null;
+        inherit (defaultAndText "dnsProvider" null) default defaultText;
         example = "route53";
         description = ''
           DNS Challenge provider. For a list of supported providers, see the "code"
@@ -543,7 +544,7 @@ let
 
       dnsResolver = mkOption {
         type = types.nullOr types.str;
-        default = null;
+        inherit (defaultAndText "dnsResolver" null) default defaultText;
         example = "1.1.1.1:53";
         description = ''
           Set the resolver to use for performing recursive DNS queries. Supported:
@@ -554,6 +555,7 @@ let
 
       credentialsFile = mkOption {
         type = types.path;
+        inherit (defaultAndText "credentialsFile" null) default defaultText;
         description = ''
           Path to an EnvironmentFile for the cert's service containing any required and
           optional environment variables for your selected dnsProvider.
@@ -565,7 +567,7 @@ let
 
       dnsPropagationCheck = mkOption {
         type = types.bool;
-        default = true;
+        inherit (defaultAndText "dnsPropagationCheck" true) default defaultText;
         description = ''
           Toggles lego DNS propagation check, which is used alongside DNS-01
           challenge to ensure the DNS entries required are available.
@@ -574,7 +576,7 @@ let
 
       ocspMustStaple = mkOption {
         type = types.bool;
-        default = false;
+        inherit (defaultAndText "ocspMustStaple" false) default defaultText;
         description = ''
           Turns on the OCSP Must-Staple TLS extension.
           Make sure you know what you're doing! See:
@@ -587,7 +589,7 @@ let
 
       extraLegoFlags = mkOption {
         type = types.listOf types.str;
-        default = [];
+        inherit (defaultAndText "extraLegoFlags" []) default defaultText;
         description = ''
           Additional global flags to pass to all lego commands.
         '';
@@ -595,7 +597,7 @@ let
 
       extraLegoRenewFlags = mkOption {
         type = types.listOf types.str;
-        default = [];
+        inherit (defaultAndText "extraLegoRenewFlags" []) default defaultText;
         description = ''
           Additional flags to pass to lego renew.
         '';
@@ -603,7 +605,7 @@ let
 
       extraLegoRunFlags = mkOption {
         type = types.listOf types.str;
-        default = [];
+        inherit (defaultAndText "extraLegoRunFlags" []) default defaultText;
         description = ''
           Additional flags to pass to lego run.
         '';
@@ -611,43 +613,80 @@ let
     };
   };
 
-in {
+  certOpts = { name, config, ... }: {
+    options = {
+      # user option has been removed
+      user = mkOption {
+        visible = false;
+        default = "_mkRemovedOptionModule";
+      };
 
-  options = {
-    security.acme = {
+      # allowKeysForGroup option has been removed
+      allowKeysForGroup = mkOption {
+        visible = false;
+        default = "_mkRemovedOptionModule";
+      };
 
-      validMinDays = mkOption {
-        type = types.int;
-        default = 30;
-        description = "Minimum remaining validity before renewal in days.";
+      # extraDomains was replaced with extraDomainNames
+      extraDomains = mkOption {
+        visible = false;
+        default = "_mkMergedOptionModule";
       };
 
-      email = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        description = "Contact email address for the CA to be able to reach you.";
+      directory = mkOption {
+        type = types.str;
+        readOnly = true;
+        default = "/var/lib/acme/${name}";
+        description = "Directory where certificate and other state is stored.";
       };
 
-      renewInterval = mkOption {
+      domain = mkOption {
         type = types.str;
-        default = "daily";
+        default = name;
+        description = "Domain to fetch certificate for (defaults to the entry name).";
+      };
+
+      extraDomainNames = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = literalExpression ''
+          [
+            "example.org"
+            "mydomain.org"
+          ]
+        '';
         description = ''
-          Systemd calendar expression when to check for renewal. See
-          <citerefentry><refentrytitle>systemd.time</refentrytitle>
-          <manvolnum>7</manvolnum></citerefentry>.
+          A list of extra domain names, which are included in the one certificate to be issued.
         '';
       };
 
-      server = mkOption {
+      # This setting must be different for each configured certificate, otherwise
+      # two or more renewals may fail to bind to the address. Hence, it is not in
+      # the inheritableOpts.
+      listenHTTP = mkOption {
         type = types.nullOr types.str;
         default = null;
+        example = ":1360";
         description = ''
-          ACME Directory Resource URI. Defaults to Let's Encrypt's
-          production endpoint,
-          <link xlink:href="https://acme-v02.api.letsencrypt.org/directory"/>, if unset.
+          Interface and port to listen on to solve HTTP challenges
+          in the form [INTERFACE]:PORT.
+          If you use a port other than 80, you must proxy port 80 to this port.
         '';
       };
 
+      inheritDefaults = mkOption {
+        default = true;
+        example = true;
+        description = "Whether to inherit values set in `security.acme.defaults` or not.";
+        type = lib.types.bool;
+      };
+    };
+  };
+
+in {
+
+  options = {
+    security.acme = {
       preliminarySelfsigned = mkOption {
         type = types.bool;
         default = true;
@@ -670,9 +709,31 @@ in {
         '';
       };
 
+      useRoot = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to use the root user when generating certs. This is not recommended
+          for security + compatiblity reasons. If a service requires root owned certificates
+          consider following the guide on "Using ACME with services demanding root
+          owned certificates" in the NixOS manual, and only using this as a fallback
+          or for testing.
+        '';
+      };
+
+      defaults = mkOption {
+        type = types.submodule (inheritableModule true);
+        description = ''
+          Default values inheritable by all configured certs. You can
+          use this to define options shared by all your certs. These defaults
+          can also be ignored on a per-cert basis using the
+          `security.acme.certs.''${cert}.inheritDefaults' option.
+        '';
+      };
+
       certs = mkOption {
         default = { };
-        type = with types; attrsOf (submodule certOpts);
+        type = with types; attrsOf (submodule [ (inheritableModule false) certOpts ]);
         description = ''
           Attribute set of certificates to get signed and renewed. Creates
           <literal>acme-''${cert}.{service,timer}</literal> systemd units for
@@ -703,12 +764,16 @@ in {
 
       To use the let's encrypt staging server, use security.acme.server =
       "https://acme-staging-v02.api.letsencrypt.org/directory".
-    ''
-    )
+    '')
     (mkRemovedOptionModule [ "security" "acme" "directory" ] "ACME Directory is now hardcoded to /var/lib/acme and its permisisons are managed by systemd. See https://github.com/NixOS/nixpkgs/issues/53852 for more info.")
     (mkRemovedOptionModule [ "security" "acme" "preDelay" ] "This option has been removed. If you want to make sure that something executes before certificates are provisioned, add a RequiredBy=acme-\${cert}.service to the service you want to execute before the cert renewal")
     (mkRemovedOptionModule [ "security" "acme" "activationDelay" ] "This option has been removed. If you want to make sure that something executes before certificates are provisioned, add a RequiredBy=acme-\${cert}.service to the service you want to execute before the cert renewal")
-    (mkChangedOptionModule [ "security" "acme" "validMin" ] [ "security" "acme" "validMinDays" ] (config: config.security.acme.validMin / (24 * 3600)))
+    (mkChangedOptionModule [ "security" "acme" "validMin" ] [ "security" "acme" "defaults" "validMinDays" ] (config: config.security.acme.validMin / (24 * 3600)))
+    (mkChangedOptionModule [ "security" "acme" "validMinDays" ] [ "security" "acme" "defaults" "validMinDays" ] (config: config.security.acme.validMinDays))
+    (mkChangedOptionModule [ "security" "acme" "renewInterval" ] [ "security" "acme" "defaults" "renewInterval" ] (config: config.security.acme.renewInterval))
+    (mkChangedOptionModule [ "security" "acme" "email" ] [ "security" "acme" "defaults" "email" ] (config: config.security.acme.email))
+    (mkChangedOptionModule [ "security" "acme" "server" ] [ "security" "acme" "defaults" "server" ] (config: config.security.acme.server))
+    (mkChangedOptionModule [ "security" "acme" "enableDebugLogs" ] [ "security" "acme" "defaults" "enableDebugLogs" ] (config: config.security.acme.enableDebugLogs))
   ];
 
   config = mkMerge [
@@ -778,6 +843,28 @@ in {
             `security.acme.certs.${cert}.webroot` are mutually exclusive.
           '';
         }
+        {
+          assertion = data.webroot == null || data.listenHTTP == null;
+          message = ''
+            Options `security.acme.certs.${cert}.webroot` and
+            `security.acme.certs.${cert}.listenHTTP` are mutually exclusive.
+          '';
+        }
+        {
+          assertion = data.listenHTTP == null || data.dnsProvider == null;
+          message = ''
+            Options `security.acme.certs.${cert}.listenHTTP` and
+            `security.acme.certs.${cert}.dnsProvider` are mutually exclusive.
+          '';
+        }
+        {
+          assertion = data.dnsProvider != null || data.webroot != null || data.listenHTTP != null;
+          message = ''
+            One of `security.acme.certs.${cert}.dnsProvider`,
+            `security.acme.certs.${cert}.webroot`, or
+            `security.acme.certs.${cert}.listenHTTP` must be provided.
+          '';
+        }
       ]) cfg.certs));
 
       users.users.acme = {
@@ -801,8 +888,8 @@ in {
         # Create some targets which can be depended on to be "active" after cert renewals
         finishedTargets = mapAttrs' (cert: conf: nameValuePair "acme-finished-${cert}" {
           wantedBy = [ "default.target" ];
-          requires = [ "acme-${cert}.service" ] ++ conf.selfsignedDeps;
-          after = [ "acme-${cert}.service" ] ++ conf.selfsignedDeps;
+          requires = [ "acme-${cert}.service" ];
+          after = [ "acme-${cert}.service" ];
         }) certConfigs;
 
         # Create targets to limit the number of simultaneous account creations
diff --git a/nixos/modules/security/acme.xml b/nixos/modules/security/acme.xml
index bf93800a0af40..f623cc509be69 100644
--- a/nixos/modules/security/acme.xml
+++ b/nixos/modules/security/acme.xml
@@ -7,8 +7,9 @@
  <para>
   NixOS supports automatic domain validation &amp; certificate retrieval and
   renewal using the ACME protocol. Any provider can be used, but by default
-  NixOS uses Let's Encrypt. The alternative ACME client <literal>lego</literal>
-  is used under the hood.
+  NixOS uses Let's Encrypt. The alternative ACME client
+  <link xlink:href="https://go-acme.github.io/lego/">lego</link> is used under
+  the hood.
  </para>
  <para>
   Automatic cert validation and configuration for Apache and Nginx virtual
@@ -29,7 +30,7 @@
   <para>
    You must also set an email address to be used when creating accounts with
    Let's Encrypt. You can set this for all certs with
-   <literal><xref linkend="opt-security.acme.email" /></literal>
+   <literal><xref linkend="opt-security.acme.defaults.email" /></literal>
    and/or on a per-cert basis with
    <literal><xref linkend="opt-security.acme.certs._name_.email" /></literal>.
    This address is only used for registration and renewal reminders,
@@ -38,7 +39,7 @@
 
   <para>
    Alternatively, you can use a different ACME server by changing the
-   <literal><xref linkend="opt-security.acme.server" /></literal> option
+   <literal><xref linkend="opt-security.acme.defaults.server" /></literal> option
    to a provider of your choosing, or just change the server for one cert with
    <literal><xref linkend="opt-security.acme.certs._name_.server" /></literal>.
   </para>
@@ -60,12 +61,12 @@
    = true;</literal> in a virtualHost config. We first create self-signed
    placeholder certificates in place of the real ACME certs. The placeholder
    certs are overwritten when the ACME certs arrive. For
-   <literal>foo.example.com</literal> the config would look like.
+   <literal>foo.example.com</literal> the config would look like this:
   </para>
 
 <programlisting>
 <xref linkend="opt-security.acme.acceptTerms" /> = true;
-<xref linkend="opt-security.acme.email" /> = "admin+acme@example.com";
+<xref linkend="opt-security.acme.defaults.email" /> = "admin+acme@example.com";
 services.nginx = {
   <link linkend="opt-services.nginx.enable">enable</link> = true;
   <link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = {
@@ -114,7 +115,7 @@ services.nginx = {
 
 <programlisting>
 <xref linkend="opt-security.acme.acceptTerms" /> = true;
-<xref linkend="opt-security.acme.email" /> = "admin+acme@example.com";
+<xref linkend="opt-security.acme.defaults.email" /> = "admin+acme@example.com";
 
 # /var/lib/acme/.challenges must be writable by the ACME user
 # and readable by the Nginx user. The easiest way to achieve
@@ -218,7 +219,7 @@ services.bind = {
 
 # Now we can configure ACME
 <xref linkend="opt-security.acme.acceptTerms" /> = true;
-<xref linkend="opt-security.acme.email" /> = "admin+acme@example.com";
+<xref linkend="opt-security.acme.defaults.email" /> = "admin+acme@example.com";
 <xref linkend="opt-security.acme.certs" />."example.com" = {
   <link linkend="opt-security.acme.certs._name_.domain">domain</link> = "*.example.com";
   <link linkend="opt-security.acme.certs._name_.dnsProvider">dnsProvider</link> = "rfc2136";
@@ -231,25 +232,39 @@ services.bind = {
   <para>
    The <filename>dnskeys.conf</filename> and <filename>certs.secret</filename>
    must be kept secure and thus you should not keep their contents in your
-   Nix config. Instead, generate them one time with these commands:
+   Nix config. Instead, generate them one time with a systemd service:
   </para>
 
 <programlisting>
-mkdir -p /var/lib/secrets
-tsig-keygen rfc2136key.example.com &gt; /var/lib/secrets/dnskeys.conf
-chown named:root /var/lib/secrets/dnskeys.conf
-chmod 400 /var/lib/secrets/dnskeys.conf
-
-# Copy the secret value from the dnskeys.conf, and put it in
-# RFC2136_TSIG_SECRET below
-
-cat &gt; /var/lib/secrets/certs.secret &lt;&lt; EOF
-RFC2136_NAMESERVER='127.0.0.1:53'
-RFC2136_TSIG_ALGORITHM='hmac-sha256.'
-RFC2136_TSIG_KEY='rfc2136key.example.com'
-RFC2136_TSIG_SECRET='your secret key'
-EOF
-chmod 400 /var/lib/secrets/certs.secret
+systemd.services.dns-rfc2136-conf = {
+  requiredBy = ["acme-example.com.service", "bind.service"];
+  before = ["acme-example.com.service", "bind.service"];
+  unitConfig = {
+    ConditionPathExists = "!/var/lib/secrets/dnskeys.conf";
+  };
+  serviceConfig = {
+    Type = "oneshot";
+    UMask = 0077;
+  };
+  path = [ pkgs.bind ];
+  script = ''
+    mkdir -p /var/lib/secrets
+    tsig-keygen rfc2136key.example.com &gt; /var/lib/secrets/dnskeys.conf
+    chown named:root /var/lib/secrets/dnskeys.conf
+    chmod 400 /var/lib/secrets/dnskeys.conf
+
+    # Copy the secret value from the dnskeys.conf, and put it in
+    # RFC2136_TSIG_SECRET below
+
+    cat &gt; /var/lib/secrets/certs.secret &lt;&lt; EOF
+    RFC2136_NAMESERVER='127.0.0.1:53'
+    RFC2136_TSIG_ALGORITHM='hmac-sha256.'
+    RFC2136_TSIG_KEY='rfc2136key.example.com'
+    RFC2136_TSIG_SECRET='your secret key'
+    EOF
+    chmod 400 /var/lib/secrets/certs.secret
+  '';
+};
 </programlisting>
 
   <para>
@@ -258,6 +273,106 @@ chmod 400 /var/lib/secrets/certs.secret
    journalctl -fu acme-example.com.service</literal> and watching its log output.
   </para>
  </section>
+
+ <section xml:id="module-security-acme-config-dns-with-vhosts">
+  <title>Using DNS validation with web server virtual hosts</title>
+
+  <para>
+   It is possible to use DNS-01 validation with all certificates,
+   including those automatically configured via the Nginx/Apache
+   <literal><link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link></literal>
+   option. This configuration pattern is fully
+   supported and part of the module's test suite for Nginx + Apache.
+  </para>
+
+  <para>
+   You must follow the guide above on configuring DNS-01 validation
+   first, however instead of setting the options for one certificate
+   (e.g. <xref linkend="opt-security.acme.certs._name_.dnsProvider" />)
+   you will set them as defaults
+   (e.g. <xref linkend="opt-security.acme.defaults.dnsProvider" />).
+  </para>
+
+<programlisting>
+# Configure ACME appropriately
+<xref linkend="opt-security.acme.acceptTerms" /> = true;
+<xref linkend="opt-security.acme.defaults.email" /> = "admin+acme@example.com";
+<xref linkend="opt-security.acme.defaults" /> = {
+  <link linkend="opt-security.acme.defaults.dnsProvider">dnsProvider</link> = "rfc2136";
+  <link linkend="opt-security.acme.defaults.credentialsFile">credentialsFile</link> = "/var/lib/secrets/certs.secret";
+  # We don't need to wait for propagation since this is a local DNS server
+  <link linkend="opt-security.acme.defaults.dnsPropagationCheck">dnsPropagationCheck</link> = false;
+};
+
+# For each virtual host you would like to use DNS-01 validation with,
+# set acmeRoot = null
+services.nginx = {
+  <link linkend="opt-services.nginx.enable">enable</link> = true;
+  <link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = {
+    "foo.example.com" = {
+      <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true;
+      <link linkend="opt-services.nginx.virtualHosts._name_.acmeRoot">acmeRoot</link> = null;
+    };
+  };
+}
+</programlisting>
+
+  <para>
+   And that's it! Next time your configuration is rebuilt, or when
+   you add a new virtualHost, it will be DNS-01 validated.
+  </para>
+ </section>
+
+ <section xml:id="module-security-acme-root-owned">
+  <title>Using ACME with services demanding root owned certificates</title>
+
+  <para>
+   Some services refuse to start if the configured certificate files
+   are not owned by root. PostgreSQL and OpenSMTPD are examples of these.
+   There is no way to change the user the ACME module uses (it will always be
+   <literal>acme</literal>), however you can use systemd's
+   <literal>LoadCredential</literal> feature to resolve this elegantly.
+   Below is an example configuration for OpenSMTPD, but this pattern
+   can be applied to any service.
+  </para>
+
+<programlisting>
+# Configure ACME however you like (DNS or HTTP validation), adding
+# the following configuration for the relevant certificate.
+# Note: You cannot use `systemctl reload` here as that would mean
+# the LoadCredential configuration below would be skipped and
+# the service would continue to use old certificates.
+security.acme.certs."mail.example.com".postRun = ''
+  systemctl restart opensmtpd
+'';
+
+# Now you must augment OpenSMTPD's systemd service to load
+# the certificate files.
+<link linkend="opt-systemd.services._name_.requires">systemd.services.opensmtpd.requires</link> = ["acme-finished-mail.example.com.target"];
+<link linkend="opt-systemd.services._name_.serviceConfig">systemd.services.opensmtpd.serviceConfig.LoadCredential</link> = let
+  certDir = config.security.acme.certs."mail.example.com".directory;
+in [
+  "cert.pem:${certDir}/cert.pem"
+  "key.pem:${certDir}/key.pem"
+];
+
+# Finally, configure OpenSMTPD to use these certs.
+services.opensmtpd = let
+  credsDir = "/run/credentials/opensmtpd.service";
+in {
+  enable = true;
+  setSendmail = false;
+  serverConfiguration = ''
+    pki mail.example.com cert "${credsDir}/cert.pem"
+    pki mail.example.com key "${credsDir}/key.pem"
+    listen on localhost tls pki mail.example.com
+    action act1 relay host smtp://127.0.0.1:10027
+    match for local action act1
+  '';
+};
+</programlisting>
+ </section>
+
  <section xml:id="module-security-acme-regenerate">
   <title>Regenerating certificates</title>
 
diff --git a/nixos/modules/security/dhparams.nix b/nixos/modules/security/dhparams.nix
index 012be2887d898..cfa9003f12fb6 100644
--- a/nixos/modules/security/dhparams.nix
+++ b/nixos/modules/security/dhparams.nix
@@ -1,8 +1,9 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 let
-  inherit (lib) mkOption types;
+  inherit (lib) literalExpression mkOption types;
   cfg = config.security.dhparams;
+  opt = options.security.dhparams;
 
   bitType = types.addCheck types.int (b: b >= 16) // {
     name = "bits";
@@ -13,6 +14,7 @@ let
     options.bits = mkOption {
       type = bitType;
       default = cfg.defaultBitSize;
+      defaultText = literalExpression "config.${opt.defaultBitSize}";
       description = ''
         The bit size for the prime that is used during a Diffie-Hellman
         key exchange.
diff --git a/nixos/modules/security/pam.nix b/nixos/modules/security/pam.nix
index 8ed7a721a3ef8..0944b36c6d197 100644
--- a/nixos/modules/security/pam.nix
+++ b/nixos/modules/security/pam.nix
@@ -295,9 +295,14 @@ let
       };
 
       limits = mkOption {
+        default = [];
+        type = limitsType;
         description = ''
           Attribute set describing resource limits.  Defaults to the
           value of <option>security.pam.loginLimits</option>.
+          The meaning of the values is explained in <citerefentry>
+          <refentrytitle>limits.conf</refentrytitle><manvolnum>5</manvolnum>
+          </citerefentry>.
         '';
       };
 
@@ -648,6 +653,51 @@ let
          "${domain} ${type} ${item} ${toString value}\n")
          limits);
 
+  limitsType = with lib.types; listOf (submodule ({ ... }: {
+    options = {
+      domain = mkOption {
+        description = "Username, groupname, or wildcard this limit applies to";
+        example = "@wheel";
+        type = str;
+      };
+
+      type = mkOption {
+        description = "Type of this limit";
+        type = enum [ "-" "hard" "soft" ];
+        default = "-";
+      };
+
+      item = mkOption {
+        description = "Item this limit applies to";
+        type = enum [
+          "core"
+          "data"
+          "fsize"
+          "memlock"
+          "nofile"
+          "rss"
+          "stack"
+          "cpu"
+          "nproc"
+          "as"
+          "maxlogins"
+          "maxsyslogins"
+          "priority"
+          "locks"
+          "sigpending"
+          "msgqueue"
+          "nice"
+          "rtprio"
+        ];
+      };
+
+      value = mkOption {
+        description = "Value of this limit";
+        type = oneOf [ str int ];
+      };
+    };
+  }));
+
   motd = pkgs.writeText "motd" config.users.motd;
 
   makePAMService = name: service:
@@ -669,6 +719,7 @@ in
 
     security.pam.loginLimits = mkOption {
       default = [];
+      type = limitsType;
       example =
         [ { domain = "ftp";
             type   = "hard";
@@ -688,7 +739,8 @@ in
           <varname>domain</varname>, <varname>type</varname>,
           <varname>item</varname>, and <varname>value</varname>
           attribute.  The syntax and semantics of these attributes
-          must be that described in the limits.conf(5) man page.
+          must be that described in <citerefentry><refentrytitle>limits.conf</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry>.
 
           Note that these limits do not apply to systemd services,
           whose limits can be changed via <option>systemd.extraConfig</option>
diff --git a/nixos/modules/security/systemd-confinement.nix b/nixos/modules/security/systemd-confinement.nix
index d859c45c74f7a..0e3ec5af323ee 100644
--- a/nixos/modules/security/systemd-confinement.nix
+++ b/nixos/modules/security/systemd-confinement.nix
@@ -1,11 +1,9 @@
-{ config, pkgs, lib, ... }:
+{ config, pkgs, lib, utils, ... }:
 
 let
   toplevelConfig = config;
   inherit (lib) types;
-  inherit (import ../system/boot/systemd-lib.nix {
-    inherit config pkgs lib;
-  }) mkPathSafeName;
+  inherit (utils.systemdUtils.lib) mkPathSafeName;
 in {
   options.systemd.services = lib.mkOption {
     type = types.attrsOf (types.submodule ({ name, config, ... }: {
diff --git a/nixos/modules/services/audio/mpdscribble.nix b/nixos/modules/services/audio/mpdscribble.nix
index 1368543ae1a4a..333ffb709410a 100644
--- a/nixos/modules/services/audio/mpdscribble.nix
+++ b/nixos/modules/services/audio/mpdscribble.nix
@@ -1,10 +1,11 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
   cfg = config.services.mpdscribble;
   mpdCfg = config.services.mpd;
+  mpdOpt = options.services.mpd;
 
   endpointUrls = {
     "last.fm" = "http://post.audioscrobbler.com";
@@ -108,6 +109,11 @@ in {
         mpdCfg.network.listenAddress
       else
         "localhost");
+      defaultText = literalExpression ''
+        if config.${mpdOpt.network.listenAddress} != "any"
+        then config.${mpdOpt.network.listenAddress}
+        else "localhost"
+      '';
       type = types.str;
       description = ''
         Host for the mpdscribble daemon to search for a mpd daemon on.
@@ -122,6 +128,10 @@ in {
           mpdCfg.credentials).passwordFile
       else
         null;
+      defaultText = literalDocBook ''
+        The first password file with read access configured for MPD when using a local instance,
+        otherwise <literal>null</literal>.
+      '';
       type = types.nullOr types.str;
       description = ''
         File containing the password for the mpd daemon.
@@ -132,6 +142,7 @@ in {
 
     port = mkOption {
       default = mpdCfg.network.port;
+      defaultText = literalExpression "config.${mpdOpt.network.port}";
       type = types.port;
       description = ''
         Port for the mpdscribble daemon to search for a mpd daemon on.
diff --git a/nixos/modules/services/audio/snapserver.nix b/nixos/modules/services/audio/snapserver.nix
index d3e97719f3576..b82aca3976f0c 100644
--- a/nixos/modules/services/audio/snapserver.nix
+++ b/nixos/modules/services/audio/snapserver.nix
@@ -54,12 +54,12 @@ let
     # tcp json rpc
     ++ [ "--tcp.enabled ${toString cfg.tcp.enable}" ]
     ++ optionals cfg.tcp.enable [
-      "--tcp.address ${cfg.tcp.listenAddress}"
+      "--tcp.bind_to_address ${cfg.tcp.listenAddress}"
       "--tcp.port ${toString cfg.tcp.port}" ]
      # http json rpc
     ++ [ "--http.enabled ${toString cfg.http.enable}" ]
     ++ optionals cfg.http.enable [
-      "--http.address ${cfg.http.listenAddress}"
+      "--http.bind_to_address ${cfg.http.listenAddress}"
       "--http.port ${toString cfg.http.port}"
     ] ++ optional (cfg.http.docRoot != null) "--http.doc_root \"${toString cfg.http.docRoot}\"");
 
diff --git a/nixos/modules/services/backup/duplicati.nix b/nixos/modules/services/backup/duplicati.nix
index cf5aebdecd281..97864c44691b0 100644
--- a/nixos/modules/services/backup/duplicati.nix
+++ b/nixos/modules/services/backup/duplicati.nix
@@ -18,6 +18,20 @@ in
         '';
       };
 
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/duplicati";
+        description = ''
+          The directory where Duplicati stores its data files.
+
+          <note><para>
+            If left as the default value this directory will automatically be created
+            before the Duplicati server starts, otherwise you are responsible for ensuring
+            the directory exists with appropriate ownership and permissions.
+          </para></note>
+        '';
+      };
+
       interface = mkOption {
         default = "127.0.0.1";
         type = types.str;
@@ -45,20 +59,23 @@ in
       description = "Duplicati backup";
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
-      serviceConfig = {
-        User = cfg.user;
-        Group = "duplicati";
-        StateDirectory = "duplicati";
-        ExecStart = "${pkgs.duplicati}/bin/duplicati-server --webservice-interface=${cfg.interface} --webservice-port=${toString cfg.port} --server-datafolder=/var/lib/duplicati";
-        Restart = "on-failure";
-      };
+      serviceConfig = mkMerge [
+        {
+          User = cfg.user;
+          Group = "duplicati";
+          ExecStart = "${pkgs.duplicati}/bin/duplicati-server --webservice-interface=${cfg.interface} --webservice-port=${toString cfg.port} --server-datafolder=${cfg.dataDir}";
+          Restart = "on-failure";
+        }
+        (mkIf (cfg.dataDir == "/var/lib/duplicati") {
+          StateDirectory = "duplicati";
+        })
+      ];
     };
 
     users.users = lib.optionalAttrs (cfg.user == "duplicati") {
       duplicati = {
         uid = config.ids.uids.duplicati;
-        home = "/var/lib/duplicati";
-        createHome = true;
+        home = cfg.dataDir;
         group = "duplicati";
       };
     };
diff --git a/nixos/modules/services/backup/restic.nix b/nixos/modules/services/backup/restic.nix
index 67fef55614b38..8ff8e31864be2 100644
--- a/nixos/modules/services/backup/restic.nix
+++ b/nixos/modules/services/backup/restic.nix
@@ -1,10 +1,10 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, pkgs, utils, ... }:
 
 with lib;
 
 let
   # Type for a valid systemd unit option. Needed for correctly passing "timerConfig" to "systemd.timers"
-  unitOption = (import ../../system/boot/systemd-unit-options.nix { inherit config lib; }).unitOption;
+  inherit (utils.systemdUtils.unitOptions) unitOption;
 in
 {
   options.services.restic.backups = mkOption {
diff --git a/nixos/modules/services/backup/tarsnap.nix b/nixos/modules/services/backup/tarsnap.nix
index 9cce868366123..9b5fd90012e0e 100644
--- a/nixos/modules/services/backup/tarsnap.nix
+++ b/nixos/modules/services/backup/tarsnap.nix
@@ -1,9 +1,10 @@
-{ config, lib, pkgs, utils, ... }:
+{ config, lib, options, pkgs, utils, ... }:
 
 with lib;
 
 let
   gcfg = config.services.tarsnap;
+  opt = options.services.tarsnap;
 
   configFile = name: cfg: ''
     keyfile ${cfg.keyfile}
@@ -59,12 +60,13 @@ in
       };
 
       archives = mkOption {
-        type = types.attrsOf (types.submodule ({ config, ... }:
+        type = types.attrsOf (types.submodule ({ config, options, ... }:
           {
             options = {
               keyfile = mkOption {
                 type = types.str;
                 default = gcfg.keyfile;
+                defaultText = literalExpression "config.${opt.keyfile}";
                 description = ''
                   Set a specific keyfile for this archive. This defaults to
                   <literal>"/root/tarsnap.key"</literal> if left unspecified.
@@ -87,6 +89,9 @@ in
               cachedir = mkOption {
                 type = types.nullOr types.path;
                 default = "/var/cache/tarsnap/${utils.escapeSystemdPath config.keyfile}";
+                defaultText = literalExpression ''
+                  "/var/cache/tarsnap/''${utils.escapeSystemdPath config.${options.keyfile}}"
+                '';
                 description = ''
                   The cache allows tarsnap to identify previously stored data
                   blocks, reducing archival time and bandwidth usage.
@@ -320,21 +325,22 @@ in
                         ${optionalString cfg.explicitSymlinks "-H"} \
                         ${optionalString cfg.followSymlinks "-L"} \
                         ${concatStringsSep " " cfg.directories}'';
+          cachedir = escapeShellArg cfg.cachedir;
           in if (cfg.cachedir != null) then ''
-            mkdir -p ${cfg.cachedir}
-            chmod 0700 ${cfg.cachedir}
+            mkdir -p ${cachedir}
+            chmod 0700 ${cachedir}
 
             ( flock 9
-              if [ ! -e ${cfg.cachedir}/firstrun ]; then
+              if [ ! -e ${cachedir}/firstrun ]; then
                 ( flock 10
                   flock -u 9
                   ${tarsnap} --fsck
                   flock 9
-                ) 10>${cfg.cachedir}/firstrun
+                ) 10>${cachedir}/firstrun
               fi
-            ) 9>${cfg.cachedir}/lockf
+            ) 9>${cachedir}/lockf
 
-             exec flock ${cfg.cachedir}/firstrun ${run}
+             exec flock ${cachedir}/firstrun ${run}
           '' else "exec ${run}";
 
         serviceConfig = {
@@ -356,22 +362,23 @@ in
           tarsnap = ''tarsnap --configfile "/etc/tarsnap/${name}.conf"'';
           lastArchive = "$(${tarsnap} --list-archives | sort | tail -1)";
           run = ''${tarsnap} -x -f "${lastArchive}" ${optionalString cfg.verbose "-v"}'';
+          cachedir = escapeShellArg cfg.cachedir;
 
         in if (cfg.cachedir != null) then ''
-          mkdir -p ${cfg.cachedir}
-          chmod 0700 ${cfg.cachedir}
+          mkdir -p ${cachedir}
+          chmod 0700 ${cachedir}
 
           ( flock 9
-            if [ ! -e ${cfg.cachedir}/firstrun ]; then
+            if [ ! -e ${cachedir}/firstrun ]; then
               ( flock 10
                 flock -u 9
                 ${tarsnap} --fsck
                 flock 9
-              ) 10>${cfg.cachedir}/firstrun
+              ) 10>${cachedir}/firstrun
             fi
-          ) 9>${cfg.cachedir}/lockf
+          ) 9>${cachedir}/lockf
 
-           exec flock ${cfg.cachedir}/firstrun ${run}
+           exec flock ${cachedir}/firstrun ${run}
         '' else "exec ${run}";
 
         serviceConfig = {
diff --git a/nixos/modules/services/cluster/hadoop/default.nix b/nixos/modules/services/cluster/hadoop/default.nix
index 90f22c48e0552..a1a95fe31cac5 100644
--- a/nixos/modules/services/cluster/hadoop/default.nix
+++ b/nixos/modules/services/cluster/hadoop/default.nix
@@ -1,6 +1,7 @@
-{ config, lib, pkgs, ...}:
+{ config, lib, options, pkgs, ...}:
 let
   cfg = config.services.hadoop;
+  opt = options.services.hadoop;
 in
 with lib;
 {
@@ -44,6 +45,14 @@ with lib;
         "mapreduce.map.env" = "HADOOP_MAPRED_HOME=${cfg.package}/lib/${cfg.package.untarDir}";
         "mapreduce.reduce.env" = "HADOOP_MAPRED_HOME=${cfg.package}/lib/${cfg.package.untarDir}";
       };
+      defaultText = literalExpression ''
+        {
+          "mapreduce.framework.name" = "yarn";
+          "yarn.app.mapreduce.am.env" = "HADOOP_MAPRED_HOME=''${config.${opt.package}}/lib/''${config.${opt.package}.untarDir}";
+          "mapreduce.map.env" = "HADOOP_MAPRED_HOME=''${config.${opt.package}}/lib/''${config.${opt.package}.untarDir}";
+          "mapreduce.reduce.env" = "HADOOP_MAPRED_HOME=''${config.${opt.package}}/lib/''${config.${opt.package}.untarDir}";
+        }
+      '';
       type = types.attrsOf types.anything;
       example = literalExpression ''
         options.services.hadoop.mapredSite.default // {
@@ -98,6 +107,9 @@ with lib;
 
     log4jProperties = mkOption {
       default = "${cfg.package}/lib/${cfg.package.untarDir}/etc/hadoop/log4j.properties";
+      defaultText = literalExpression ''
+        "''${config.${opt.package}}/lib/''${config.${opt.package}.untarDir}/etc/hadoop/log4j.properties"
+      '';
       type = types.path;
       example = literalExpression ''
         "''${pkgs.hadoop}/lib/''${pkgs.hadoop.untarDir}/etc/hadoop/log4j.properties";
diff --git a/nixos/modules/services/cluster/kubernetes/addon-manager.nix b/nixos/modules/services/cluster/kubernetes/addon-manager.nix
index 3d988dc2479ac..9159d5915eb77 100644
--- a/nixos/modules/services/cluster/kubernetes/addon-manager.nix
+++ b/nixos/modules/services/cluster/kubernetes/addon-manager.nix
@@ -58,7 +58,7 @@ in
             "spec" = { ... };
           };
         }
-        // import <nixpkgs/nixos/modules/services/cluster/kubernetes/dashboard.nix> { cfg = config.services.kubernetes; };
+        // import <nixpkgs/nixos/modules/services/cluster/kubernetes/dns.nix> { cfg = config.services.kubernetes; };
       '';
     };
 
diff --git a/nixos/modules/services/cluster/kubernetes/addons/dashboard.nix b/nixos/modules/services/cluster/kubernetes/addons/dashboard.nix
deleted file mode 100644
index 2ed7742eda09b..0000000000000
--- a/nixos/modules/services/cluster/kubernetes/addons/dashboard.nix
+++ /dev/null
@@ -1,332 +0,0 @@
-{ config, pkgs, lib, ... }:
-
-with lib;
-
-let
-  cfg = config.services.kubernetes.addons.dashboard;
-in {
-  imports = [
-    (mkRenamedOptionModule [ "services" "kubernetes" "addons" "dashboard" "enableRBAC" ] [ "services" "kubernetes" "addons" "dashboard" "rbac" "enable" ])
-  ];
-
-  options.services.kubernetes.addons.dashboard = {
-    enable = mkEnableOption "kubernetes dashboard addon";
-
-    extraArgs = mkOption {
-      description = "Extra arguments to append to the dashboard cmdline";
-      type = types.listOf types.str;
-      default = [];
-      example = ["--enable-skip-login"];
-    };
-
-    rbac = mkOption {
-      description = "Role-based access control (RBAC) options";
-      default = {};
-      type = types.submodule {
-        options = {
-          enable = mkOption {
-            description = "Whether to enable role based access control is enabled for kubernetes dashboard";
-            type = types.bool;
-            default = elem "RBAC" config.services.kubernetes.apiserver.authorizationMode;
-          };
-
-          clusterAdmin = mkOption {
-            description = "Whether to assign cluster admin rights to the kubernetes dashboard";
-            type = types.bool;
-            default = false;
-          };
-        };
-      };
-    };
-
-    version = mkOption {
-      description = "Which version of the kubernetes dashboard to deploy";
-      type = types.str;
-      default = "v1.10.1";
-    };
-
-    image = mkOption {
-      description = "Docker image to seed for the kubernetes dashboard container.";
-      type = types.attrs;
-      default = {
-        imageName = "k8s.gcr.io/kubernetes-dashboard-amd64";
-        imageDigest = "sha256:0ae6b69432e78069c5ce2bcde0fe409c5c4d6f0f4d9cd50a17974fea38898747";
-        finalImageTag = cfg.version;
-        sha256 = "01xrr4pwgr2hcjrjsi3d14ifpzdfbxzqpzxbk2fkbjb9zkv38zxy";
-      };
-    };
-  };
-
-  config = mkIf cfg.enable {
-    services.kubernetes.kubelet.seedDockerImages = [(pkgs.dockerTools.pullImage cfg.image)];
-
-    services.kubernetes.addonManager.addons = {
-      kubernetes-dashboard-deployment = {
-        kind = "Deployment";
-        apiVersion = "apps/v1";
-        metadata = {
-          labels = {
-            k8s-addon = "kubernetes-dashboard.addons.k8s.io";
-            k8s-app = "kubernetes-dashboard";
-            version = cfg.version;
-            "kubernetes.io/cluster-service" = "true";
-            "addonmanager.kubernetes.io/mode" = "Reconcile";
-          };
-          name = "kubernetes-dashboard";
-          namespace = "kube-system";
-        };
-        spec = {
-          replicas = 1;
-          revisionHistoryLimit = 10;
-          selector.matchLabels.k8s-app = "kubernetes-dashboard";
-          template = {
-            metadata = {
-              labels = {
-                k8s-addon = "kubernetes-dashboard.addons.k8s.io";
-                k8s-app = "kubernetes-dashboard";
-                version = cfg.version;
-                "kubernetes.io/cluster-service" = "true";
-              };
-              annotations = {
-                "scheduler.alpha.kubernetes.io/critical-pod" = "";
-              };
-            };
-            spec = {
-              priorityClassName = "system-cluster-critical";
-              containers = [{
-                name = "kubernetes-dashboard";
-                image = with cfg.image; "${imageName}:${finalImageTag}";
-                ports = [{
-                  containerPort = 8443;
-                  protocol = "TCP";
-                }];
-                resources = {
-                  limits = {
-                    cpu = "100m";
-                    memory = "300Mi";
-                  };
-                  requests = {
-                    cpu = "100m";
-                    memory = "100Mi";
-                  };
-                };
-                args = ["--auto-generate-certificates"] ++ cfg.extraArgs;
-                volumeMounts = [{
-                  name = "tmp-volume";
-                  mountPath = "/tmp";
-                } {
-                  name = "kubernetes-dashboard-certs";
-                  mountPath = "/certs";
-                }];
-                livenessProbe = {
-                  httpGet = {
-                    scheme = "HTTPS";
-                    path = "/";
-                    port = 8443;
-                  };
-                  initialDelaySeconds = 30;
-                  timeoutSeconds = 30;
-                };
-              }];
-              volumes = [{
-                name = "kubernetes-dashboard-certs";
-                secret = {
-                  secretName = "kubernetes-dashboard-certs";
-                };
-              } {
-                name = "tmp-volume";
-                emptyDir = {};
-              }];
-              serviceAccountName = "kubernetes-dashboard";
-              tolerations = [{
-                key = "node-role.kubernetes.io/master";
-                effect = "NoSchedule";
-              } {
-                key = "CriticalAddonsOnly";
-                operator = "Exists";
-              }];
-            };
-          };
-        };
-      };
-
-      kubernetes-dashboard-svc = {
-        apiVersion = "v1";
-        kind = "Service";
-        metadata = {
-          labels = {
-            k8s-addon = "kubernetes-dashboard.addons.k8s.io";
-            k8s-app = "kubernetes-dashboard";
-            "kubernetes.io/cluster-service" = "true";
-            "kubernetes.io/name" = "KubeDashboard";
-            "addonmanager.kubernetes.io/mode" = "Reconcile";
-          };
-          name = "kubernetes-dashboard";
-          namespace  = "kube-system";
-        };
-        spec = {
-          ports = [{
-            port = 443;
-            targetPort = 8443;
-          }];
-          selector.k8s-app = "kubernetes-dashboard";
-        };
-      };
-
-      kubernetes-dashboard-sa = {
-        apiVersion = "v1";
-        kind = "ServiceAccount";
-        metadata = {
-          labels = {
-            k8s-app = "kubernetes-dashboard";
-            k8s-addon = "kubernetes-dashboard.addons.k8s.io";
-            "addonmanager.kubernetes.io/mode" = "Reconcile";
-          };
-          name = "kubernetes-dashboard";
-          namespace = "kube-system";
-        };
-      };
-      kubernetes-dashboard-sec-certs = {
-        apiVersion = "v1";
-        kind = "Secret";
-        metadata = {
-          labels = {
-            k8s-app = "kubernetes-dashboard";
-            # Allows editing resource and makes sure it is created first.
-            "addonmanager.kubernetes.io/mode" = "EnsureExists";
-          };
-          name = "kubernetes-dashboard-certs";
-          namespace = "kube-system";
-        };
-        type = "Opaque";
-      };
-      kubernetes-dashboard-sec-kholder = {
-        apiVersion = "v1";
-        kind = "Secret";
-        metadata = {
-          labels = {
-            k8s-app = "kubernetes-dashboard";
-            # Allows editing resource and makes sure it is created first.
-            "addonmanager.kubernetes.io/mode" = "EnsureExists";
-          };
-          name = "kubernetes-dashboard-key-holder";
-          namespace = "kube-system";
-        };
-        type = "Opaque";
-      };
-      kubernetes-dashboard-cm = {
-        apiVersion = "v1";
-        kind = "ConfigMap";
-        metadata = {
-          labels = {
-            k8s-app = "kubernetes-dashboard";
-            # Allows editing resource and makes sure it is created first.
-            "addonmanager.kubernetes.io/mode" = "EnsureExists";
-          };
-          name = "kubernetes-dashboard-settings";
-          namespace = "kube-system";
-        };
-      };
-    } // (optionalAttrs cfg.rbac.enable
-      (let
-        subjects = [{
-          kind = "ServiceAccount";
-          name = "kubernetes-dashboard";
-          namespace = "kube-system";
-        }];
-        labels = {
-          k8s-app = "kubernetes-dashboard";
-          k8s-addon = "kubernetes-dashboard.addons.k8s.io";
-          "addonmanager.kubernetes.io/mode" = "Reconcile";
-        };
-      in
-        (if cfg.rbac.clusterAdmin then {
-          kubernetes-dashboard-crb = {
-            apiVersion = "rbac.authorization.k8s.io/v1";
-            kind = "ClusterRoleBinding";
-            metadata = {
-              name = "kubernetes-dashboard";
-              inherit labels;
-            };
-            roleRef = {
-              apiGroup = "rbac.authorization.k8s.io";
-              kind = "ClusterRole";
-              name = "cluster-admin";
-            };
-            inherit subjects;
-          };
-        }
-        else
-        {
-          # Upstream role- and rolebinding as per:
-          # https://raw.githubusercontent.com/kubernetes/dashboard/master/src/deploy/alternative/kubernetes-dashboard.yaml
-          kubernetes-dashboard-role = {
-            apiVersion = "rbac.authorization.k8s.io/v1";
-            kind = "Role";
-            metadata = {
-              name = "kubernetes-dashboard-minimal";
-              namespace = "kube-system";
-              inherit labels;
-            };
-            rules = [
-              # Allow Dashboard to create 'kubernetes-dashboard-key-holder' secret.
-              {
-                apiGroups = [""];
-                resources = ["secrets"];
-                verbs = ["create"];
-              }
-              # Allow Dashboard to create 'kubernetes-dashboard-settings' config map.
-              {
-                apiGroups = [""];
-                resources = ["configmaps"];
-                verbs = ["create"];
-              }
-              # Allow Dashboard to get, update and delete Dashboard exclusive secrets.
-              {
-                apiGroups = [""];
-                resources = ["secrets"];
-                resourceNames = ["kubernetes-dashboard-key-holder"];
-                verbs = ["get" "update" "delete"];
-              }
-              # Allow Dashboard to get and update 'kubernetes-dashboard-settings' config map.
-              {
-                apiGroups = [""];
-                resources = ["configmaps"];
-                resourceNames = ["kubernetes-dashboard-settings"];
-                verbs = ["get" "update"];
-              }
-              # Allow Dashboard to get metrics from heapster.
-              {
-                apiGroups = [""];
-                resources = ["services"];
-                resourceNames = ["heapster"];
-                verbs = ["proxy"];
-              }
-              {
-                apiGroups = [""];
-                resources = ["services/proxy"];
-                resourceNames = ["heapster" "http:heapster:" "https:heapster:"];
-                verbs = ["get"];
-              }
-            ];
-          };
-
-          kubernetes-dashboard-rb = {
-            apiVersion = "rbac.authorization.k8s.io/v1";
-            kind = "RoleBinding";
-            metadata = {
-              name = "kubernetes-dashboard-minimal";
-              namespace = "kube-system";
-              inherit labels;
-            };
-            roleRef = {
-              apiGroup = "rbac.authorization.k8s.io";
-              kind = "Role";
-              name = "kubernetes-dashboard-minimal";
-            };
-            inherit subjects;
-          };
-        })
-    ));
-  };
-}
diff --git a/nixos/modules/services/cluster/kubernetes/addons/dns.nix b/nixos/modules/services/cluster/kubernetes/addons/dns.nix
index 34943fddd3d10..10f45db7883f4 100644
--- a/nixos/modules/services/cluster/kubernetes/addons/dns.nix
+++ b/nixos/modules/services/cluster/kubernetes/addons/dns.nix
@@ -1,4 +1,4 @@
-{ config, pkgs, lib, ... }:
+{ config, options, pkgs, lib, ... }:
 
 with lib;
 
@@ -23,6 +23,10 @@ in {
           take 3 (splitString "." config.services.kubernetes.apiserver.serviceClusterIpRange
         ))
       ) + ".254";
+      defaultText = literalDocBook ''
+        The <literal>x.y.z.254</literal> IP of
+        <literal>config.${options.services.kubernetes.apiserver.serviceClusterIpRange}</literal>.
+      '';
       type = types.str;
     };
 
diff --git a/nixos/modules/services/cluster/kubernetes/apiserver.nix b/nixos/modules/services/cluster/kubernetes/apiserver.nix
index 2c89310beb5ab..5b97c571d7639 100644
--- a/nixos/modules/services/cluster/kubernetes/apiserver.nix
+++ b/nixos/modules/services/cluster/kubernetes/apiserver.nix
@@ -1,9 +1,10 @@
-  { config, lib, pkgs, ... }:
+  { config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
   top = config.services.kubernetes;
+  otop = options.services.kubernetes;
   cfg = top.apiserver;
 
   isRBACEnabled = elem "RBAC" cfg.authorizationMode;
@@ -84,6 +85,7 @@ in
     clientCaFile = mkOption {
       description = "Kubernetes apiserver CA file for client auth.";
       default = top.caFile;
+      defaultText = literalExpression "config.${otop.caFile}";
       type = nullOr path;
     };
 
@@ -138,6 +140,7 @@ in
       caFile = mkOption {
         description = "Etcd ca file.";
         default = top.caFile;
+        defaultText = literalExpression "config.${otop.caFile}";
         type = types.nullOr types.path;
       };
     };
@@ -157,6 +160,7 @@ in
     featureGates = mkOption {
       description = "List set of feature gates";
       default = top.featureGates;
+      defaultText = literalExpression "config.${otop.featureGates}";
       type = listOf str;
     };
 
@@ -175,6 +179,7 @@ in
     kubeletClientCaFile = mkOption {
       description = "Path to a cert file for connecting to kubelet.";
       default = top.caFile;
+      defaultText = literalExpression "config.${otop.caFile}";
       type = nullOr path;
     };
 
diff --git a/nixos/modules/services/cluster/kubernetes/controller-manager.nix b/nixos/modules/services/cluster/kubernetes/controller-manager.nix
index 7128b5f70b1a3..ed25715fab7d7 100644
--- a/nixos/modules/services/cluster/kubernetes/controller-manager.nix
+++ b/nixos/modules/services/cluster/kubernetes/controller-manager.nix
@@ -1,9 +1,10 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
   top = config.services.kubernetes;
+  otop = options.services.kubernetes;
   cfg = top.controllerManager;
 in
 {
@@ -30,6 +31,7 @@ in
     clusterCidr = mkOption {
       description = "Kubernetes CIDR Range for Pods in cluster.";
       default = top.clusterCidr;
+      defaultText = literalExpression "config.${otop.clusterCidr}";
       type = str;
     };
 
@@ -44,6 +46,7 @@ in
     featureGates = mkOption {
       description = "List set of feature gates";
       default = top.featureGates;
+      defaultText = literalExpression "config.${otop.featureGates}";
       type = listOf str;
     };
 
@@ -67,6 +70,7 @@ in
         service account's token secret.
       '';
       default = top.caFile;
+      defaultText = literalExpression "config.${otop.caFile}";
       type = nullOr path;
     };
 
diff --git a/nixos/modules/services/cluster/kubernetes/default.nix b/nixos/modules/services/cluster/kubernetes/default.nix
index 433adf4d488c8..227c69fec36d4 100644
--- a/nixos/modules/services/cluster/kubernetes/default.nix
+++ b/nixos/modules/services/cluster/kubernetes/default.nix
@@ -1,9 +1,10 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
   cfg = config.services.kubernetes;
+  opt = options.services.kubernetes;
 
   defaultContainerdSettings = {
     version = 2;
@@ -87,6 +88,7 @@ let
       description = "${prefix} certificate authority file used to connect to kube-apiserver.";
       type = types.nullOr types.path;
       default = cfg.caFile;
+      defaultText = literalExpression "config.${opt.caFile}";
     };
 
     certFile = mkOption {
@@ -104,6 +106,7 @@ let
 in {
 
   imports = [
+    (mkRemovedOptionModule [ "services" "kubernetes" "addons" "dashboard" ] "Removed due to it being an outdated version")
     (mkRemovedOptionModule [ "services" "kubernetes" "verbose" ] "")
   ];
 
diff --git a/nixos/modules/services/cluster/kubernetes/kubelet.nix b/nixos/modules/services/cluster/kubernetes/kubelet.nix
index 2806f73375bca..3e8eac96f6bac 100644
--- a/nixos/modules/services/cluster/kubernetes/kubelet.nix
+++ b/nixos/modules/services/cluster/kubernetes/kubelet.nix
@@ -1,9 +1,10 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
   top = config.services.kubernetes;
+  otop = options.services.kubernetes;
   cfg = top.kubelet;
 
   cniConfig =
@@ -35,6 +36,7 @@ let
       key = mkOption {
         description = "Key of taint.";
         default = name;
+        defaultText = literalDocBook "Name of this submodule.";
         type = str;
       };
       value = mkOption {
@@ -76,12 +78,14 @@ in
     clusterDomain = mkOption {
       description = "Use alternative domain.";
       default = config.services.kubernetes.addons.dns.clusterDomain;
+      defaultText = literalExpression "config.${options.services.kubernetes.addons.dns.clusterDomain}";
       type = str;
     };
 
     clientCaFile = mkOption {
       description = "Kubernetes apiserver CA file for client authentication.";
       default = top.caFile;
+      defaultText = literalExpression "config.${otop.caFile}";
       type = nullOr path;
     };
 
@@ -148,6 +152,7 @@ in
     featureGates = mkOption {
       description = "List set of feature gates";
       default = top.featureGates;
+      defaultText = literalExpression "config.${otop.featureGates}";
       type = listOf str;
     };
 
diff --git a/nixos/modules/services/cluster/kubernetes/proxy.nix b/nixos/modules/services/cluster/kubernetes/proxy.nix
index a09efcef94eaf..5f3da034120b7 100644
--- a/nixos/modules/services/cluster/kubernetes/proxy.nix
+++ b/nixos/modules/services/cluster/kubernetes/proxy.nix
@@ -1,9 +1,10 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
   top = config.services.kubernetes;
+  otop = options.services.kubernetes;
   cfg = top.proxy;
 in
 {
@@ -31,6 +32,7 @@ in
     featureGates = mkOption {
       description = "List set of feature gates";
       default = top.featureGates;
+      defaultText = literalExpression "config.${otop.featureGates}";
       type = listOf str;
     };
 
diff --git a/nixos/modules/services/cluster/kubernetes/scheduler.nix b/nixos/modules/services/cluster/kubernetes/scheduler.nix
index 1b0c22a11426a..87263ee72fa43 100644
--- a/nixos/modules/services/cluster/kubernetes/scheduler.nix
+++ b/nixos/modules/services/cluster/kubernetes/scheduler.nix
@@ -1,9 +1,10 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
   top = config.services.kubernetes;
+  otop = options.services.kubernetes;
   cfg = top.scheduler;
 in
 {
@@ -27,6 +28,7 @@ in
     featureGates = mkOption {
       description = "List set of feature gates";
       default = top.featureGates;
+      defaultText = literalExpression "config.${otop.featureGates}";
       type = listOf str;
     };
 
diff --git a/nixos/modules/services/computing/slurm/slurm.nix b/nixos/modules/services/computing/slurm/slurm.nix
index d2f3feffc970c..7686ff99bfc03 100644
--- a/nixos/modules/services/computing/slurm/slurm.nix
+++ b/nixos/modules/services/computing/slurm/slurm.nix
@@ -1,10 +1,11 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
 
   cfg = config.services.slurm;
+  opt = options.services.slurm;
   # configuration file can be generated by http://slurm.schedmd.com/configurator.html
 
   defaultUser = "slurm";
@@ -90,6 +91,7 @@ in
         storageUser = mkOption {
           type = types.str;
           default = cfg.user;
+          defaultText = literalExpression "config.${opt.user}";
           description = ''
             Database user name.
           '';
@@ -154,6 +156,7 @@ in
       controlAddr = mkOption {
         type = types.nullOr types.str;
         default = cfg.controlMachine;
+        defaultText = literalExpression "config.${opt.controlMachine}";
         example = null;
         description = ''
           Name that ControlMachine should be referred to in establishing a
@@ -279,6 +282,10 @@ in
         type = types.path;
         internal = true;
         default = etcSlurm;
+        defaultText = literalDocBook ''
+          Directory created from generated config files and
+          <literal>config.${opt.extraConfigPaths}</literal>.
+        '';
         description = ''
           Path to directory with slurm config files. This option is set by default from the
           Slurm module and is meant to make the Slurm config file available to other modules.
diff --git a/nixos/modules/services/continuous-integration/buildbot/master.nix b/nixos/modules/services/continuous-integration/buildbot/master.nix
index 2dc61c21ac71b..aaa159d3cb18c 100644
--- a/nixos/modules/services/continuous-integration/buildbot/master.nix
+++ b/nixos/modules/services/continuous-integration/buildbot/master.nix
@@ -1,11 +1,12 @@
 # NixOS module for Buildbot continous integration server.
 
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
   cfg = config.services.buildbot-master;
+  opt = options.services.buildbot-master;
 
   python = cfg.package.pythonModule;
 
@@ -152,6 +153,7 @@ in {
 
       buildbotDir = mkOption {
         default = "${cfg.home}/master";
+        defaultText = literalExpression ''"''${config.${opt.home}}/master"'';
         type = types.path;
         description = "Specifies the Buildbot directory.";
       };
diff --git a/nixos/modules/services/continuous-integration/buildbot/worker.nix b/nixos/modules/services/continuous-integration/buildbot/worker.nix
index dd4f4a4a74a9c..1d7f53bb6559a 100644
--- a/nixos/modules/services/continuous-integration/buildbot/worker.nix
+++ b/nixos/modules/services/continuous-integration/buildbot/worker.nix
@@ -1,11 +1,12 @@
 # NixOS module for Buildbot Worker.
 
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
   cfg = config.services.buildbot-worker;
+  opt = options.services.buildbot-worker;
 
   python = cfg.package.pythonModule;
 
@@ -77,6 +78,7 @@ in {
 
       buildbotDir = mkOption {
         default = "${cfg.home}/worker";
+        defaultText = literalExpression ''"''${config.${opt.home}}/worker"'';
         type = types.path;
         description = "Specifies the Buildbot directory.";
       };
diff --git a/nixos/modules/services/continuous-integration/github-runner.nix b/nixos/modules/services/continuous-integration/github-runner.nix
index 59370f43fe750..afd85c972b56e 100644
--- a/nixos/modules/services/continuous-integration/github-runner.nix
+++ b/nixos/modules/services/continuous-integration/github-runner.nix
@@ -10,6 +10,8 @@ let
   stateDir = "%S/${systemdDir}";
   # %L: Log directory root (usually /var/log); see systemd.unit(5)
   logsDir = "%L/${systemdDir}";
+  # Name of file stored in service state directory
+  currentConfigTokenFilename = ".current-token";
 in
 {
   options.services.github-runner = {
@@ -144,13 +146,11 @@ in
         ExecStart = "${cfg.package}/bin/runsvc.sh";
 
         # Does the following, sequentially:
-        # - Copy the current and the previous `tokenFile` to the $RUNTIME_DIRECTORY
-        #   and make it accessible to the service user to allow for a content
-        #   comparison.
-        # - If the module configuration or the token has changed, clear the state directory.
-        # - Configure the runner.
-        # - Copy the configured `tokenFile` to the $STATE_DIRECTORY and make it
-        #   inaccessible to the service user.
+        # - If the module configuration or the token has changed, purge the state directory,
+        #   and create the current and the new token file with the contents of the configured
+        #   token. While both files have the same content, only the later is accessible by
+        #   the service user.
+        # - Configure the runner using the new token file. When finished, delete it.
         # - Set up the directory structure by creating the necessary symlinks.
         ExecStartPre =
           let
@@ -173,37 +173,20 @@ in
             currentConfigPath = "$STATE_DIRECTORY/.nixos-current-config.json";
             runnerRegistrationConfig = getAttrs [ "name" "tokenFile" "url" "runnerGroup" "extraLabels" ] cfg;
             newConfigPath = builtins.toFile "${svcName}-config.json" (builtins.toJSON runnerRegistrationConfig);
-            currentConfigTokenFilename = ".current-token";
             newConfigTokenFilename = ".new-token";
             runnerCredFiles = [
               ".credentials"
               ".credentials_rsaparams"
               ".runner"
             ];
-            ownConfigTokens = writeScript "own-config-tokens" ''
-              # Copy current and new token file to runtime dir and make it accessible to the service user
-              cp ${escapeShellArg cfg.tokenFile} "$RUNTIME_DIRECTORY/${newConfigTokenFilename}"
-              chmod 600 "$RUNTIME_DIRECTORY/${newConfigTokenFilename}"
-              chown "$USER" "$RUNTIME_DIRECTORY/${newConfigTokenFilename}"
-
-              if [[ -e "$STATE_DIRECTORY/${currentConfigTokenFilename}" ]]; then
-                cp "$STATE_DIRECTORY/${currentConfigTokenFilename}" "$RUNTIME_DIRECTORY/${currentConfigTokenFilename}"
-                chmod 600 "$RUNTIME_DIRECTORY/${currentConfigTokenFilename}"
-                chown "$USER" "$RUNTIME_DIRECTORY/${currentConfigTokenFilename}"
-              fi
-            '';
-            disownConfigTokens = writeScript "disown-config-tokens" ''
-              # Make the token inaccessible to the runner service user
-              chmod 600 "$STATE_DIRECTORY/${currentConfigTokenFilename}"
-              chown root:root "$STATE_DIRECTORY/${currentConfigTokenFilename}"
-            '';
             unconfigureRunner = writeScript "unconfigure" ''
               differs=
               # Set `differs = 1` if current and new runner config differ or if `currentConfigPath` does not exist
               ${pkgs.diffutils}/bin/diff -q '${newConfigPath}' "${currentConfigPath}" >/dev/null 2>&1 || differs=1
               # Also trigger a registration if the token content changed
               ${pkgs.diffutils}/bin/diff -q \
-                "$RUNTIME_DIRECTORY"/{${currentConfigTokenFilename},${newConfigTokenFilename}} \
+                "$STATE_DIRECTORY"/${currentConfigTokenFilename} \
+                ${escapeShellArg cfg.tokenFile} \
                 >/dev/null 2>&1 || differs=1
 
               if [[ -n "$differs" ]]; then
@@ -211,13 +194,18 @@ in
                 echo "The old runner will still appear in the GitHub Actions UI." \
                   "You have to remove it manually."
                 find "$STATE_DIRECTORY/" -mindepth 1 -delete
+
+                # Copy the configured token file to the state dir and allow the service user to read the file
+                install --mode=666 ${escapeShellArg cfg.tokenFile} "$STATE_DIRECTORY/${newConfigTokenFilename}"
+                # Also copy current file to allow for a diff on the next start
+                install --mode=600 ${escapeShellArg cfg.tokenFile} "$STATE_DIRECTORY/${currentConfigTokenFilename}"
               fi
             '';
             configureRunner = writeScript "configure" ''
-              empty=$(ls -A "$STATE_DIRECTORY")
-              if [[ -z "$empty" ]]; then
+              if [[ -e "$STATE_DIRECTORY/${newConfigTokenFilename}" ]]; then
                 echo "Configuring GitHub Actions Runner"
-                token=$(< "$RUNTIME_DIRECTORY"/${newConfigTokenFilename})
+
+                token=$(< "$STATE_DIRECTORY"/${newConfigTokenFilename})
                 RUNNER_ROOT="$STATE_DIRECTORY" ${cfg.package}/bin/config.sh \
                   --unattended \
                   --work "$RUNTIME_DIRECTORY" \
@@ -234,8 +222,7 @@ in
                 rm    -rf "$STATE_DIRECTORY/_diag/"
 
                 # Cleanup token from config
-                rm -f "$RUNTIME_DIRECTORY"/${currentConfigTokenFilename}
-                mv    "$RUNTIME_DIRECTORY"/${newConfigTokenFilename} "$STATE_DIRECTORY/${currentConfigTokenFilename}"
+                rm "$STATE_DIRECTORY/${newConfigTokenFilename}"
 
                 # Symlink to new config
                 ln -s '${newConfigPath}' "${currentConfigPath}"
@@ -250,10 +237,8 @@ in
             '';
           in
           map (x: "${x} ${escapeShellArgs [ stateDir runtimeDir logsDir ]}") [
-            "+${ownConfigTokens}" # runs as root
-            unconfigureRunner
+            "+${unconfigureRunner}" # runs as root
             configureRunner
-            "+${disownConfigTokens}" # runs as root
             setupRuntimeDir
           ];
 
@@ -266,6 +251,13 @@ in
         StateDirectoryMode = "0700";
         WorkingDirectory = runtimeDir;
 
+        InaccessiblePaths = [
+          # Token file path given in the configuration
+          cfg.tokenFile
+          # Token file in the state directory
+          "${stateDir}/${currentConfigTokenFilename}"
+        ];
+
         # By default, use a dynamically allocated user
         DynamicUser = true;
 
diff --git a/nixos/modules/services/continuous-integration/gocd-agent/default.nix b/nixos/modules/services/continuous-integration/gocd-agent/default.nix
index acc3fb12484a5..c63998c6736a6 100644
--- a/nixos/modules/services/continuous-integration/gocd-agent/default.nix
+++ b/nixos/modules/services/continuous-integration/gocd-agent/default.nix
@@ -1,9 +1,10 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
   cfg = config.services.gocd-agent;
+  opt = options.services.gocd-agent;
 in {
   options = {
     services.gocd-agent = {
@@ -98,6 +99,15 @@ in {
           "-Dcruise.console.publish.interval=10"
           "-Djava.security.egd=file:/dev/./urandom"
         ];
+        defaultText = literalExpression ''
+          [
+            "-Xms''${config.${opt.initialJavaHeapSize}}"
+            "-Xmx''${config.${opt.maxJavaHeapMemory}}"
+            "-Djava.io.tmpdir=/tmp"
+            "-Dcruise.console.publish.interval=10"
+            "-Djava.security.egd=file:/dev/./urandom"
+          ]
+        '';
         description = ''
           Specifies startup command line arguments to pass to Go.CD agent
           java process.
diff --git a/nixos/modules/services/continuous-integration/gocd-server/default.nix b/nixos/modules/services/continuous-integration/gocd-server/default.nix
index 646bf13ac67ad..3540656f93448 100644
--- a/nixos/modules/services/continuous-integration/gocd-server/default.nix
+++ b/nixos/modules/services/continuous-integration/gocd-server/default.nix
@@ -1,9 +1,10 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
   cfg = config.services.gocd-server;
+  opt = options.services.gocd-server;
 in {
   options = {
     services.gocd-server = {
@@ -106,6 +107,20 @@ in {
           "-Dcruise.server.port=${toString cfg.port}"
           "-Dcruise.server.ssl.port=${toString cfg.sslPort}"
         ];
+        defaultText = literalExpression ''
+          [
+            "-Xms''${config.${opt.initialJavaHeapSize}}"
+            "-Xmx''${config.${opt.maxJavaHeapMemory}}"
+            "-Dcruise.listen.host=''${config.${opt.listenAddress}}"
+            "-Duser.language=en"
+            "-Djruby.rack.request.size.threshold.bytes=30000000"
+            "-Duser.country=US"
+            "-Dcruise.config.dir=''${config.${opt.workDir}}/conf"
+            "-Dcruise.config.file=''${config.${opt.workDir}}/conf/cruise-config.xml"
+            "-Dcruise.server.port=''${toString config.${opt.port}}"
+            "-Dcruise.server.ssl.port=''${toString config.${opt.sslPort}}"
+          ]
+        '';
 
         description = ''
           Specifies startup command line arguments to pass to Go.CD server
diff --git a/nixos/modules/services/databases/couchdb.nix b/nixos/modules/services/databases/couchdb.nix
index 16dd64f2373e6..266bc82b69678 100644
--- a/nixos/modules/services/databases/couchdb.nix
+++ b/nixos/modules/services/databases/couchdb.nix
@@ -43,8 +43,8 @@ in {
 
       package = mkOption {
         type = types.package;
-        default = pkgs.couchdb;
-        defaultText = literalExpression "pkgs.couchdb";
+        default = pkgs.couchdb3;
+        defaultText = literalExpression "pkgs.couchdb3";
         description = ''
           CouchDB package to use.
         '';
@@ -150,6 +150,14 @@ in {
         '';
       };
 
+      argsFile = mkOption {
+        type = types.path;
+        default = "${cfg.package}/etc/vm.args";
+        description = ''
+          vm.args configuration. Overrides Couchdb's Erlang VM parameters file.
+        '';
+      };
+
       configFile = mkOption {
         type = types.path;
         description = ''
@@ -186,12 +194,14 @@ in {
       '';
 
       environment = {
-        # we are actually specifying 4 configuration files:
+        # we are actually specifying 5 configuration files:
         # 1. the preinstalled default.ini
         # 2. the module configuration
         # 3. the extraConfig from the module options
         # 4. the locally writable config file, which couchdb itself writes to
         ERL_FLAGS= ''-couch_ini ${cfg.package}/etc/default.ini ${configFile} ${pkgs.writeText "couchdb-extra.ini" cfg.extraConfig} ${cfg.configFile}'';
+        # 5. the vm.args file
+        COUCHDB_ARGS_FILE=''${cfg.argsFile}'';
       };
 
       serviceConfig = {
diff --git a/nixos/modules/services/databases/hbase.nix b/nixos/modules/services/databases/hbase.nix
index 181be2d6b0b87..fe4f05eec643c 100644
--- a/nixos/modules/services/databases/hbase.nix
+++ b/nixos/modules/services/databases/hbase.nix
@@ -1,14 +1,10 @@
-{ config, lib, pkgs, ... }:
+{ config, options, lib, pkgs, ... }:
 
 with lib;
 
 let
   cfg = config.services.hbase;
-
-  defaultConfig = {
-    "hbase.rootdir" = "file://${cfg.dataDir}/hbase";
-    "hbase.zookeeper.property.dataDir" = "${cfg.dataDir}/zookeeper";
-  };
+  opt = options.services.hbase;
 
   buildProperty = configAttr:
     (builtins.concatStringsSep "\n"
@@ -23,7 +19,7 @@ let
 
   configFile = pkgs.writeText "hbase-site.xml"
     ''<configuration>
-        ${buildProperty (defaultConfig // cfg.settings)}
+        ${buildProperty (opt.settings.default // cfg.settings)}
       </configuration>
     '';
 
@@ -96,7 +92,16 @@ in {
 
       settings = mkOption {
         type = with lib.types; attrsOf (oneOf [ str int bool ]);
-        default = defaultConfig;
+        default = {
+          "hbase.rootdir" = "file://${cfg.dataDir}/hbase";
+          "hbase.zookeeper.property.dataDir" = "${cfg.dataDir}/zookeeper";
+        };
+        defaultText = literalExpression ''
+          {
+            "hbase.rootdir" = "file://''${config.${opt.dataDir}}/hbase";
+            "hbase.zookeeper.property.dataDir" = "''${config.${opt.dataDir}}/zookeeper";
+          }
+        '';
         description = ''
           configurations in hbase-site.xml, see <link xlink:href="https://github.com/apache/hbase/blob/master/hbase-server/src/test/resources/hbase-site.xml"/> for details.
         '';
diff --git a/nixos/modules/services/databases/influxdb2.nix b/nixos/modules/services/databases/influxdb2.nix
index 15f008cbc6d6c..340c515bbb434 100644
--- a/nixos/modules/services/databases/influxdb2.nix
+++ b/nixos/modules/services/databases/influxdb2.nix
@@ -1,5 +1,7 @@
 { config, lib, pkgs, ... }:
+
 with lib;
+
 let
   format = pkgs.formats.json { };
   cfg = config.services.influxdb2;
@@ -9,12 +11,14 @@ in
   options = {
     services.influxdb2 = {
       enable = mkEnableOption "the influxdb2 server";
+
       package = mkOption {
-        default = pkgs.influxdb2;
+        default = pkgs.influxdb2-server;
         defaultText = literalExpression "pkgs.influxdb2";
         description = "influxdb2 derivation to use.";
         type = types.package;
       };
+
       settings = mkOption {
         default = { };
         description = ''configuration options for influxdb2, see <link xlink:href="https://docs.influxdata.com/influxdb/v2.0/reference/config-options"/> for details.'';
@@ -28,18 +32,20 @@ in
       assertion = !(builtins.hasAttr "bolt-path" cfg.settings) && !(builtins.hasAttr "engine-path" cfg.settings);
       message = "services.influxdb2.config: bolt-path and engine-path should not be set as they are managed by systemd";
     }];
+
     systemd.services.influxdb2 = {
       description = "InfluxDB is an open-source, distributed, time series database";
       documentation = [ "https://docs.influxdata.com/influxdb/" ];
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" ];
       environment = {
-        INFLUXD_CONFIG_PATH = "${configFile}";
+        INFLUXD_CONFIG_PATH = configFile;
       };
       serviceConfig = {
         ExecStart = "${cfg.package}/bin/influxd --bolt-path \${STATE_DIRECTORY}/influxd.bolt --engine-path \${STATE_DIRECTORY}/engine";
         StateDirectory = "influxdb2";
-        DynamicUser = true;
+        User = "influxdb2";
+        Group = "influxdb2";
         CapabilityBoundingSet = "";
         SystemCallFilter = "@system-service";
         LimitNOFILE = 65536;
@@ -47,6 +53,13 @@ in
         Restart = "on-failure";
       };
     };
+
+    users.extraUsers.influxdb2 = {
+      isSystemUser = true;
+      group = "influxdb2";
+    };
+
+    users.extraGroups.influxdb2 = {};
   };
 
   meta.maintainers = with lib.maintainers; [ nickcao ];
diff --git a/nixos/modules/services/databases/mysql.nix b/nixos/modules/services/databases/mysql.nix
index a9d9a6d80588e..625b31d081c9a 100644
--- a/nixos/modules/services/databases/mysql.nix
+++ b/nixos/modules/services/databases/mysql.nix
@@ -11,10 +11,8 @@ let
   mysqldOptions =
     "--user=${cfg.user} --datadir=${cfg.dataDir} --basedir=${cfg.package}";
 
-  settingsFile = pkgs.writeText "my.cnf" (
-    generators.toINI { listsAsDuplicateKeys = true; } cfg.settings +
-    optionalString (cfg.extraOptions != null) "[mysqld]\n${cfg.extraOptions}"
-  );
+  format = pkgs.formats.ini { listsAsDuplicateKeys = true; };
+  configFile = format.generate "my.cnf" cfg.settings;
 
 in
 
@@ -22,6 +20,9 @@ in
   imports = [
     (mkRemovedOptionModule [ "services" "mysql" "pidDir" ] "Don't wait for pidfiles, describe dependencies through systemd.")
     (mkRemovedOptionModule [ "services" "mysql" "rootPassword" ] "Use socket authentication or set the password outside of the nix store.")
+    (mkRemovedOptionModule [ "services" "mysql" "extraOptions" ] "Use services.mysql.settings.mysqld instead.")
+    (mkRemovedOptionModule [ "services" "mysql" "bind" ] "Use services.mysql.settings.mysqld.bind-address instead.")
+    (mkRemovedOptionModule [ "services" "mysql" "port" ] "Use services.mysql.settings.mysqld.port instead.")
   ];
 
   ###### interface
@@ -40,41 +41,53 @@ in
         ";
       };
 
-      bind = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        example = "0.0.0.0";
-        description = "Address to bind to. The default is to bind to all addresses.";
-      };
-
-      port = mkOption {
-        type = types.port;
-        default = 3306;
-        description = "Port of MySQL.";
-      };
-
       user = mkOption {
         type = types.str;
         default = "mysql";
-        description = "User account under which MySQL runs.";
+        description = ''
+          User account under which MySQL runs.
+
+          <note><para>
+          If left as the default value this user will automatically be created
+          on system activation, otherwise you are responsible for
+          ensuring the user exists before the MySQL service starts.
+          </para></note>
+        '';
       };
 
       group = mkOption {
         type = types.str;
         default = "mysql";
-        description = "Group under which MySQL runs.";
+        description = ''
+          Group account under which MySQL runs.
+
+          <note><para>
+          If left as the default value this group will automatically be created
+          on system activation, otherwise you are responsible for
+          ensuring the user exists before the MySQL service starts.
+          </para></note>
+        '';
       };
 
       dataDir = mkOption {
         type = types.path;
         example = "/var/lib/mysql";
-        description = "Location where MySQL stores its table files.";
+        description = ''
+          The data directory for MySQL.
+
+          <note><para>
+          If left as the default value of <literal>/var/lib/mysql</literal> this directory will automatically be created before the MySQL
+          server starts, otherwise you are responsible for ensuring the directory exists with appropriate ownership and permissions.
+          </para></note>
+        '';
       };
 
       configFile = mkOption {
         type = types.path;
-        default = settingsFile;
-        defaultText = literalExpression "settingsFile";
+        default = configFile;
+        defaultText = ''
+          A configuration file automatically generated by NixOS.
+        '';
         description = ''
           Override the configuration file used by MySQL. By default,
           NixOS generates one automatically from <option>services.mysql.settings</option>.
@@ -92,7 +105,7 @@ in
       };
 
       settings = mkOption {
-        type = with types; attrsOf (attrsOf (oneOf [ bool int str (listOf str) ]));
+        type = format.type;
         default = {};
         description = ''
           MySQL configuration. Refer to
@@ -125,23 +138,6 @@ in
         '';
       };
 
-      extraOptions = mkOption {
-        type = with types; nullOr lines;
-        default = null;
-        example = ''
-          key_buffer_size = 6G
-          table_cache = 1600
-          log-error = /var/log/mysql_err.log
-        '';
-        description = ''
-          Provide extra options to the MySQL configuration file.
-
-          Please note, that these options are added to the
-          <literal>[mysqld]</literal> section so you don't need to explicitly
-          state it again.
-        '';
-      };
-
       initialDatabases = mkOption {
         type = types.listOf (types.submodule {
           options = {
@@ -287,7 +283,7 @@ in
         };
 
         masterPort = mkOption {
-          type = types.int;
+          type = types.port;
           default = 3306;
           description = "Port number on which the MySQL master server runs.";
         };
@@ -299,9 +295,7 @@ in
 
   ###### implementation
 
-  config = mkIf config.services.mysql.enable {
-
-    warnings = optional (cfg.extraOptions != null) "services.mysql.`extraOptions` is deprecated, please use services.mysql.`settings`.";
+  config = mkIf cfg.enable {
 
     services.mysql.dataDir =
       mkDefault (if versionAtLeast config.system.stateVersion "17.09" then "/var/lib/mysql"
@@ -310,8 +304,7 @@ in
     services.mysql.settings.mysqld = mkMerge [
       {
         datadir = cfg.dataDir;
-        bind-address = mkIf (cfg.bind != null) cfg.bind;
-        port = cfg.port;
+        port = mkDefault 3306;
       }
       (mkIf (cfg.replication.role == "master" || cfg.replication.role == "slave") {
         log-bin = "mysql-bin-${toString cfg.replication.serverId}";
@@ -341,156 +334,150 @@ in
 
     environment.etc."my.cnf".source = cfg.configFile;
 
-    systemd.tmpfiles.rules = [
-      "d '${cfg.dataDir}' 0700 '${cfg.user}' '${cfg.group}' - -"
-      "z '${cfg.dataDir}' 0700 '${cfg.user}' '${cfg.group}' - -"
-    ];
-
-    systemd.services.mysql = let
-      hasNotify = isMariaDB;
-    in {
-        description = "MySQL Server";
-
-        after = [ "network.target" ];
-        wantedBy = [ "multi-user.target" ];
-        restartTriggers = [ cfg.configFile ];
-
-        unitConfig.RequiresMountsFor = "${cfg.dataDir}";
-
-        path = [
-          # Needed for the mysql_install_db command in the preStart script
-          # which calls the hostname command.
-          pkgs.nettools
-        ];
-
-        preStart = if isMariaDB then ''
-          if ! test -e ${cfg.dataDir}/mysql; then
-            ${cfg.package}/bin/mysql_install_db --defaults-file=/etc/my.cnf ${mysqldOptions}
-            touch ${cfg.dataDir}/mysql_init
-          fi
-        '' else ''
-          if ! test -e ${cfg.dataDir}/mysql; then
-            ${cfg.package}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} --initialize-insecure
-            touch ${cfg.dataDir}/mysql_init
-          fi
-        '';
-
-        script = ''
-          # https://mariadb.com/kb/en/getting-started-with-mariadb-galera-cluster/#systemd-and-galera-recovery
-          if test -n "''${_WSREP_START_POSITION}"; then
-            if test -e "${cfg.package}/bin/galera_recovery"; then
-              VAR=$(cd ${cfg.package}/bin/..; ${cfg.package}/bin/galera_recovery); [[ $? -eq 0 ]] && export _WSREP_START_POSITION=$VAR || exit 1
-            fi
-          fi
-
-          # The last two environment variables are used for starting Galera clusters
-          exec ${cfg.package}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} $_WSREP_NEW_CLUSTER $_WSREP_START_POSITION
-        '';
-
-        postStart = let
-          # The super user account to use on *first* run of MySQL server
-          superUser = if isMariaDB then cfg.user else "root";
-        in ''
-          ${optionalString (!hasNotify) ''
-            # Wait until the MySQL server is available for use
-            count=0
-            while [ ! -e /run/mysqld/mysqld.sock ]
-            do
-                if [ $count -eq 30 ]
-                then
-                    echo "Tried 30 times, giving up..."
-                    exit 1
-                fi
-
-                echo "MySQL daemon not yet started. Waiting for 1 second..."
-                count=$((count++))
-                sleep 1
-            done
-          ''}
-
-          if [ -f ${cfg.dataDir}/mysql_init ]
-          then
-              # While MariaDB comes with a 'mysql' super user account since 10.4.x, MySQL does not
-              # Since we don't want to run this service as 'root' we need to ensure the account exists on first run
-              ( echo "CREATE USER IF NOT EXISTS '${cfg.user}'@'localhost' IDENTIFIED WITH ${if isMariaDB then "unix_socket" else "auth_socket"};"
-                echo "GRANT ALL PRIVILEGES ON *.* TO '${cfg.user}'@'localhost' WITH GRANT OPTION;"
-              ) | ${cfg.package}/bin/mysql -u ${superUser} -N
-
-              ${concatMapStrings (database: ''
-                # Create initial databases
-                if ! test -e "${cfg.dataDir}/${database.name}"; then
-                    echo "Creating initial database: ${database.name}"
-                    ( echo 'create database `${database.name}`;'
-
-                      ${optionalString (database.schema != null) ''
-                      echo 'use `${database.name}`;'
-
-                      # TODO: this silently falls through if database.schema does not exist,
-                      # we should catch this somehow and exit, but can't do it here because we're in a subshell.
-                      if [ -f "${database.schema}" ]
-                      then
-                          cat ${database.schema}
-                      elif [ -d "${database.schema}" ]
-                      then
-                          cat ${database.schema}/mysql-databases/*.sql
-                      fi
-                      ''}
-                    ) | ${cfg.package}/bin/mysql -u ${superUser} -N
-                fi
-              '') cfg.initialDatabases}
-
-              ${optionalString (cfg.replication.role == "master")
-                ''
-                  # Set up the replication master
-
-                  ( echo "use mysql;"
-                    echo "CREATE USER '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}' IDENTIFIED WITH mysql_native_password;"
-                    echo "SET PASSWORD FOR '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}' = PASSWORD('${cfg.replication.masterPassword}');"
-                    echo "GRANT REPLICATION SLAVE ON *.* TO '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}';"
-                  ) | ${cfg.package}/bin/mysql -u ${superUser} -N
-                ''}
-
-              ${optionalString (cfg.replication.role == "slave")
-                ''
-                  # Set up the replication slave
-
-                  ( echo "stop slave;"
-                    echo "change master to master_host='${cfg.replication.masterHost}', master_user='${cfg.replication.masterUser}', master_password='${cfg.replication.masterPassword}';"
-                    echo "start slave;"
-                  ) | ${cfg.package}/bin/mysql -u ${superUser} -N
-                ''}
-
-              ${optionalString (cfg.initialScript != null)
-                ''
-                  # Execute initial script
-                  # using toString to avoid copying the file to nix store if given as path instead of string,
-                  # as it might contain credentials
-                  cat ${toString cfg.initialScript} | ${cfg.package}/bin/mysql -u ${superUser} -N
-                ''}
-
-              rm ${cfg.dataDir}/mysql_init
+    systemd.services.mysql = {
+      description = "MySQL Server";
+
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      restartTriggers = [ cfg.configFile ];
+
+      unitConfig.RequiresMountsFor = cfg.dataDir;
+
+      path = [
+        # Needed for the mysql_install_db command in the preStart script
+        # which calls the hostname command.
+        pkgs.nettools
+      ];
+
+      preStart = if isMariaDB then ''
+        if ! test -e ${cfg.dataDir}/mysql; then
+          ${cfg.package}/bin/mysql_install_db --defaults-file=/etc/my.cnf ${mysqldOptions}
+          touch ${cfg.dataDir}/mysql_init
+        fi
+      '' else ''
+        if ! test -e ${cfg.dataDir}/mysql; then
+          ${cfg.package}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} --initialize-insecure
+          touch ${cfg.dataDir}/mysql_init
+        fi
+      '';
+
+      script = ''
+        # https://mariadb.com/kb/en/getting-started-with-mariadb-galera-cluster/#systemd-and-galera-recovery
+        if test -n "''${_WSREP_START_POSITION}"; then
+          if test -e "${cfg.package}/bin/galera_recovery"; then
+            VAR=$(cd ${cfg.package}/bin/..; ${cfg.package}/bin/galera_recovery); [[ $? -eq 0 ]] && export _WSREP_START_POSITION=$VAR || exit 1
           fi
+        fi
+
+        # The last two environment variables are used for starting Galera clusters
+        exec ${cfg.package}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} $_WSREP_NEW_CLUSTER $_WSREP_START_POSITION
+      '';
+
+      postStart = let
+        # The super user account to use on *first* run of MySQL server
+        superUser = if isMariaDB then cfg.user else "root";
+      in ''
+        ${optionalString (!isMariaDB) ''
+          # Wait until the MySQL server is available for use
+          count=0
+          while [ ! -e /run/mysqld/mysqld.sock ]
+          do
+              if [ $count -eq 30 ]
+              then
+                  echo "Tried 30 times, giving up..."
+                  exit 1
+              fi
+
+              echo "MySQL daemon not yet started. Waiting for 1 second..."
+              count=$((count++))
+              sleep 1
+          done
+        ''}
+
+        if [ -f ${cfg.dataDir}/mysql_init ]
+        then
+            # While MariaDB comes with a 'mysql' super user account since 10.4.x, MySQL does not
+            # Since we don't want to run this service as 'root' we need to ensure the account exists on first run
+            ( echo "CREATE USER IF NOT EXISTS '${cfg.user}'@'localhost' IDENTIFIED WITH ${if isMariaDB then "unix_socket" else "auth_socket"};"
+              echo "GRANT ALL PRIVILEGES ON *.* TO '${cfg.user}'@'localhost' WITH GRANT OPTION;"
+            ) | ${cfg.package}/bin/mysql -u ${superUser} -N
 
-          ${optionalString (cfg.ensureDatabases != []) ''
-            (
             ${concatMapStrings (database: ''
-              echo "CREATE DATABASE IF NOT EXISTS \`${database}\`;"
-            '') cfg.ensureDatabases}
+              # Create initial databases
+              if ! test -e "${cfg.dataDir}/${database.name}"; then
+                  echo "Creating initial database: ${database.name}"
+                  ( echo 'create database `${database.name}`;'
+
+                    ${optionalString (database.schema != null) ''
+                    echo 'use `${database.name}`;'
+
+                    # TODO: this silently falls through if database.schema does not exist,
+                    # we should catch this somehow and exit, but can't do it here because we're in a subshell.
+                    if [ -f "${database.schema}" ]
+                    then
+                        cat ${database.schema}
+                    elif [ -d "${database.schema}" ]
+                    then
+                        cat ${database.schema}/mysql-databases/*.sql
+                    fi
+                    ''}
+                  ) | ${cfg.package}/bin/mysql -u ${superUser} -N
+              fi
+            '') cfg.initialDatabases}
+
+            ${optionalString (cfg.replication.role == "master")
+              ''
+                # Set up the replication master
+
+                ( echo "use mysql;"
+                  echo "CREATE USER '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}' IDENTIFIED WITH mysql_native_password;"
+                  echo "SET PASSWORD FOR '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}' = PASSWORD('${cfg.replication.masterPassword}');"
+                  echo "GRANT REPLICATION SLAVE ON *.* TO '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}';"
+                ) | ${cfg.package}/bin/mysql -u ${superUser} -N
+              ''}
+
+            ${optionalString (cfg.replication.role == "slave")
+              ''
+                # Set up the replication slave
+
+                ( echo "stop slave;"
+                  echo "change master to master_host='${cfg.replication.masterHost}', master_user='${cfg.replication.masterUser}', master_password='${cfg.replication.masterPassword}';"
+                  echo "start slave;"
+                ) | ${cfg.package}/bin/mysql -u ${superUser} -N
+              ''}
+
+            ${optionalString (cfg.initialScript != null)
+              ''
+                # Execute initial script
+                # using toString to avoid copying the file to nix store if given as path instead of string,
+                # as it might contain credentials
+                cat ${toString cfg.initialScript} | ${cfg.package}/bin/mysql -u ${superUser} -N
+              ''}
+
+            rm ${cfg.dataDir}/mysql_init
+        fi
+
+        ${optionalString (cfg.ensureDatabases != []) ''
+          (
+          ${concatMapStrings (database: ''
+            echo "CREATE DATABASE IF NOT EXISTS \`${database}\`;"
+          '') cfg.ensureDatabases}
+          ) | ${cfg.package}/bin/mysql -N
+        ''}
+
+        ${concatMapStrings (user:
+          ''
+            ( echo "CREATE USER IF NOT EXISTS '${user.name}'@'localhost' IDENTIFIED WITH ${if isMariaDB then "unix_socket" else "auth_socket"};"
+              ${concatStringsSep "\n" (mapAttrsToList (database: permission: ''
+                echo "GRANT ${permission} ON ${database} TO '${user.name}'@'localhost';"
+              '') user.ensurePermissions)}
             ) | ${cfg.package}/bin/mysql -N
-          ''}
-
-          ${concatMapStrings (user:
-            ''
-              ( echo "CREATE USER IF NOT EXISTS '${user.name}'@'localhost' IDENTIFIED WITH ${if isMariaDB then "unix_socket" else "auth_socket"};"
-                ${concatStringsSep "\n" (mapAttrsToList (database: permission: ''
-                  echo "GRANT ${permission} ON ${database} TO '${user.name}'@'localhost';"
-                '') user.ensurePermissions)}
-              ) | ${cfg.package}/bin/mysql -N
-            '') cfg.ensureUsers}
-        '';
+          '') cfg.ensureUsers}
+      '';
 
-        serviceConfig = {
-          Type = if hasNotify then "notify" else "simple";
+      serviceConfig = mkMerge [
+        {
+          Type = if isMariaDB then "notify" else "simple";
           Restart = "on-abort";
           RestartSec = "5s";
 
@@ -523,9 +510,12 @@ in
           PrivateMounts = true;
           # System Call Filtering
           SystemCallArchitectures = "native";
-        };
-      };
-
+        }
+        (mkIf (cfg.dataDir == "/var/lib/mysql") {
+          StateDirectory = "mysql";
+          StateDirectoryMode = "0700";
+        })
+      ];
+    };
   };
-
 }
diff --git a/nixos/modules/services/databases/neo4j.nix b/nixos/modules/services/databases/neo4j.nix
index f37e5ad16939b..8816f3b2e4b64 100644
--- a/nixos/modules/services/databases/neo4j.nix
+++ b/nixos/modules/services/databases/neo4j.nix
@@ -4,6 +4,7 @@ with lib;
 
 let
   cfg = config.services.neo4j;
+  opt = options.services.neo4j;
   certDirOpt = options.services.neo4j.directories.certificates;
   isDefaultPathOption = opt: isOption opt && opt.type == types.path && opt.highestPrio >= 1500;
 
@@ -256,6 +257,7 @@ in {
       certificates = mkOption {
         type = types.path;
         default = "${cfg.directories.home}/certificates";
+        defaultText = literalExpression ''"''${config.${opt.directories.home}}/certificates"'';
         description = ''
           Directory for storing certificates to be used by Neo4j for
           TLS connections.
@@ -280,6 +282,7 @@ in {
       data = mkOption {
         type = types.path;
         default = "${cfg.directories.home}/data";
+        defaultText = literalExpression ''"''${config.${opt.directories.home}}/data"'';
         description = ''
           Path of the data directory. You must not configure more than one
           Neo4j installation to use the same data directory.
@@ -305,6 +308,7 @@ in {
       imports = mkOption {
         type = types.path;
         default = "${cfg.directories.home}/import";
+        defaultText = literalExpression ''"''${config.${opt.directories.home}}/import"'';
         description = ''
           The root directory for file URLs used with the Cypher
           <literal>LOAD CSV</literal> clause. Only meaningful when
@@ -321,6 +325,7 @@ in {
       plugins = mkOption {
         type = types.path;
         default = "${cfg.directories.home}/plugins";
+        defaultText = literalExpression ''"''${config.${opt.directories.home}}/plugins"'';
         description = ''
           Path of the database plugin directory. Compiled Java JAR files that
           contain database procedures will be loaded if they are placed in
@@ -432,6 +437,7 @@ in {
           baseDirectory = mkOption {
             type = types.path;
             default = "${cfg.directories.certificates}/${name}";
+            defaultText = literalExpression ''"''${config.${opt.directories.certificates}}/''${name}"'';
             description = ''
               The mandatory base directory for cryptographic objects of this
               policy. This path is only automatically generated when this
@@ -493,6 +499,7 @@ in {
           revokedDir = mkOption {
             type = types.path;
             default = "${config.baseDirectory}/revoked";
+            defaultText = literalExpression ''"''${config.${options.baseDirectory}}/revoked"'';
             description = ''
               Path to directory of CRLs (Certificate Revocation Lists) in
               PEM format. Must be an absolute path. The existence of this
@@ -528,6 +535,7 @@ in {
           trustedDir = mkOption {
             type = types.path;
             default = "${config.baseDirectory}/trusted";
+            defaultText = literalExpression ''"''${config.${options.baseDirectory}}/trusted"'';
             description = ''
               Path to directory of X.509 certificates in PEM format for
               trusted parties. Must be an absolute path. The existence of this
diff --git a/nixos/modules/services/databases/postgresql.nix b/nixos/modules/services/databases/postgresql.nix
index d49cb4c51a729..2919022496a36 100644
--- a/nixos/modules/services/databases/postgresql.nix
+++ b/nixos/modules/services/databases/postgresql.nix
@@ -289,14 +289,16 @@ in
         port = cfg.port;
       };
 
-    services.postgresql.package =
+    services.postgresql.package = let
+        mkThrow = ver: throw "postgresql_${ver} was removed, please upgrade your postgresql version.";
+    in
       # Note: when changing the default, make it conditional on
       # ‘system.stateVersion’ to maintain compatibility with existing
       # systems!
       mkDefault (if versionAtLeast config.system.stateVersion "21.11" then pkgs.postgresql_13
             else if versionAtLeast config.system.stateVersion "20.03" then pkgs.postgresql_11
-            else if versionAtLeast config.system.stateVersion "17.09" then pkgs.postgresql_9_6
-            else throw "postgresql_9_5 was removed, please upgrade your postgresql version.");
+            else if versionAtLeast config.system.stateVersion "17.09" then mkThrow "9_6"
+            else mkThrow "9_5");
 
     services.postgresql.dataDir = mkDefault "/var/lib/postgresql/${cfg.package.psqlSchema}";
 
diff --git a/nixos/modules/services/databases/postgresql.xml b/nixos/modules/services/databases/postgresql.xml
index 07af4c937f037..0ca9f3faed212 100644
--- a/nixos/modules/services/databases/postgresql.xml
+++ b/nixos/modules/services/databases/postgresql.xml
@@ -52,37 +52,51 @@ Type "help" for help.
  <section xml:id="module-services-postgres-upgrading">
   <title>Upgrading</title>
 
+  <note>
+   <para>
+    The steps below demonstrate how to upgrade from an older version to <package>pkgs.postgresql_13</package>.
+    These instructions are also applicable to other versions.
+   </para>
+  </note>
   <para>
-   Major PostgreSQL upgrade requires PostgreSQL downtime and a few imperative steps to be called. To simplify this process, use the following NixOS module:
+   Major PostgreSQL upgrades require a downtime and a few imperative steps to be called. This is the case because
+   each major version has some internal changes in the databases' state during major releases. Because of that,
+   NixOS places the state into <filename>/var/lib/postgresql/&lt;version&gt;</filename> where each <literal>version</literal>
+   can be obtained like this:
 <programlisting>
-  containers.temp-pg.config.services.postgresql = {
-    enable = true;
-    package = pkgs.postgresql_12;
-    ## set a custom new dataDir
-    # dataDir = "/some/data/dir";
-  };
-  environment.systemPackages =
-    let newpg = config.containers.temp-pg.config.services.postgresql;
-    in [
-      (pkgs.writeScriptBin "upgrade-pg-cluster" ''
-        set -x
-        export OLDDATA="${config.services.postgresql.dataDir}"
-        export NEWDATA="${newpg.dataDir}"
-        export OLDBIN="${config.services.postgresql.package}/bin"
-        export NEWBIN="${newpg.package}/bin"
-
-        install -d -m 0700 -o postgres -g postgres "$NEWDATA"
-        cd "$NEWDATA"
-        sudo -u postgres $NEWBIN/initdb -D "$NEWDATA"
-
-        systemctl stop postgresql    # old one
-
-        sudo -u postgres $NEWBIN/pg_upgrade \
-          --old-datadir "$OLDDATA" --new-datadir "$NEWDATA" \
-          --old-bindir $OLDBIN --new-bindir $NEWBIN \
-          "$@"
-      '')
-    ];
+<prompt>$ </prompt>nix-instantiate --eval -A postgresql_13.psqlSchema
+"13"
+</programlisting>
+   For an upgrade, a script like this can be used to simplify the process:
+<programlisting>
+{ config, pkgs, ... }:
+{
+  <xref linkend="opt-environment.systemPackages" /> = [
+    (pkgs.writeScriptBin "upgrade-pg-cluster" ''
+      set -eux
+      # XXX it's perhaps advisable to stop all services that depend on postgresql
+      systemctl stop postgresql
+
+      # XXX replace `&lt;new version&gt;` with the psqlSchema here
+      export NEWDATA="/var/lib/postgresql/&lt;new version&gt;"
+
+      # XXX specify the postgresql package you'd like to upgrade to
+      export NEWBIN="${pkgs.postgresql_13}/bin"
+
+      export OLDDATA="${config.<xref linkend="opt-services.postgresql.dataDir"/>}"
+      export OLDBIN="${config.<xref linkend="opt-services.postgresql.package"/>}/bin"
+
+      install -d -m 0700 -o postgres -g postgres "$NEWDATA"
+      cd "$NEWDATA"
+      sudo -u postgres $NEWBIN/initdb -D "$NEWDATA"
+
+      sudo -u postgres $NEWBIN/pg_upgrade \
+        --old-datadir "$OLDDATA" --new-datadir "$NEWDATA" \
+        --old-bindir $OLDBIN --new-bindir $NEWBIN \
+        "$@"
+    '')
+  ];
+}
 </programlisting>
   </para>
 
@@ -103,17 +117,25 @@ Type "help" for help.
    </listitem>
    <listitem>
     <para>
-     Run <literal>upgrade-pg-cluster</literal>. It will stop old postgresql, initialize new one and migrate old one to new one. You may supply arguments like <literal>--jobs 4</literal> and <literal>--link</literal> to speedup migration process. See <link xlink:href="https://www.postgresql.org/docs/current/pgupgrade.html" /> for details.
+     Run <literal>upgrade-pg-cluster</literal>. It will stop old postgresql, initialize a new one and migrate the old one to the new one. You may supply arguments like <literal>--jobs 4</literal> and <literal>--link</literal> to speedup migration process. See <link xlink:href="https://www.postgresql.org/docs/current/pgupgrade.html" /> for details.
     </para>
    </listitem>
    <listitem>
     <para>
-     Change postgresql package in NixOS configuration to the one you were upgrading to, and change <literal>dataDir</literal> to the one you have migrated to. Rebuild NixOS. This should start new postgres using upgraded data directory.
+     Change postgresql package in NixOS configuration to the one you were upgrading to via <xref linkend="opt-services.postgresql.package" />. Rebuild NixOS. This should start new postgres using upgraded data directory and all services you stopped during the upgrade.
     </para>
    </listitem>
    <listitem>
     <para>
-     After upgrade you may want to <literal>ANALYZE</literal> new db.
+     After the upgrade it's advisable to analyze the new cluster (as <literal>su -l postgres</literal> in the
+     <xref linkend="opt-services.postgresql.dataDir" />, in this example <filename>/var/lib/postgresql/13</filename>):
+<programlisting>
+<prompt>$ </prompt>./analyze_new_cluster.sh
+</programlisting>
+     <warning><para>The next step removes the old state-directory!</para></warning>
+<programlisting>
+<prompt>$ </prompt>./delete_old_cluster.sh
+</programlisting>
     </para>
    </listitem>
   </orderedlist>
diff --git a/nixos/modules/services/databases/redis.nix b/nixos/modules/services/databases/redis.nix
index 578d9d9ec8d78..c5513635392cd 100644
--- a/nixos/modules/services/databases/redis.nix
+++ b/nixos/modules/services/databases/redis.nix
@@ -5,17 +5,18 @@ with lib;
 let
   cfg = config.services.redis;
 
-  ulimitNofile = cfg.maxclients + 32;
-
   mkValueString = value:
     if value == true then "yes"
     else if value == false then "no"
     else generators.mkValueStringDefault { } value;
 
-  redisConfig = pkgs.writeText "redis.conf" (generators.toKeyValue {
+  redisConfig = settings: pkgs.writeText "redis.conf" (generators.toKeyValue {
     listsAsDuplicateKeys = true;
     mkKeyValue = generators.mkKeyValueDefault { inherit mkValueString; } " ";
-  } cfg.settings);
+  } settings);
+
+  redisName = name: "redis" + optionalString (name != "") ("-"+name);
+  enabledServers = filterAttrs (name: conf: conf.enable) config.services.redis.servers;
 
 in {
   imports = [
@@ -24,7 +25,28 @@ in {
     (mkRemovedOptionModule [ "services" "redis" "dbFilename" ] "The redis module now uses /var/lib/redis/dump.rdb as database dump location.")
     (mkRemovedOptionModule [ "services" "redis" "appendOnlyFilename" ] "This option was never used.")
     (mkRemovedOptionModule [ "services" "redis" "pidFile" ] "This option was removed.")
-    (mkRemovedOptionModule [ "services" "redis" "extraConfig" ] "Use services.redis.settings instead.")
+    (mkRemovedOptionModule [ "services" "redis" "extraConfig" ] "Use services.redis.servers.*.settings instead.")
+    (mkRenamedOptionModule [ "services" "redis" "enable"] [ "services" "redis" "servers" "" "enable" ])
+    (mkRenamedOptionModule [ "services" "redis" "port"] [ "services" "redis" "servers" "" "port" ])
+    (mkRenamedOptionModule [ "services" "redis" "openFirewall"] [ "services" "redis" "servers" "" "openFirewall" ])
+    (mkRenamedOptionModule [ "services" "redis" "bind"] [ "services" "redis" "servers" "" "bind" ])
+    (mkRenamedOptionModule [ "services" "redis" "unixSocket"] [ "services" "redis" "servers" "" "unixSocket" ])
+    (mkRenamedOptionModule [ "services" "redis" "unixSocketPerm"] [ "services" "redis" "servers" "" "unixSocketPerm" ])
+    (mkRenamedOptionModule [ "services" "redis" "logLevel"] [ "services" "redis" "servers" "" "logLevel" ])
+    (mkRenamedOptionModule [ "services" "redis" "logfile"] [ "services" "redis" "servers" "" "logfile" ])
+    (mkRenamedOptionModule [ "services" "redis" "syslog"] [ "services" "redis" "servers" "" "syslog" ])
+    (mkRenamedOptionModule [ "services" "redis" "databases"] [ "services" "redis" "servers" "" "databases" ])
+    (mkRenamedOptionModule [ "services" "redis" "maxclients"] [ "services" "redis" "servers" "" "maxclients" ])
+    (mkRenamedOptionModule [ "services" "redis" "save"] [ "services" "redis" "servers" "" "save" ])
+    (mkRenamedOptionModule [ "services" "redis" "slaveOf"] [ "services" "redis" "servers" "" "slaveOf" ])
+    (mkRenamedOptionModule [ "services" "redis" "masterAuth"] [ "services" "redis" "servers" "" "masterAuth" ])
+    (mkRenamedOptionModule [ "services" "redis" "requirePass"] [ "services" "redis" "servers" "" "requirePass" ])
+    (mkRenamedOptionModule [ "services" "redis" "requirePassFile"] [ "services" "redis" "servers" "" "requirePassFile" ])
+    (mkRenamedOptionModule [ "services" "redis" "appendOnly"] [ "services" "redis" "servers" "" "appendOnly" ])
+    (mkRenamedOptionModule [ "services" "redis" "appendFsync"] [ "services" "redis" "servers" "" "appendFsync" ])
+    (mkRenamedOptionModule [ "services" "redis" "slowLogLogSlowerThan"] [ "services" "redis" "servers" "" "slowLogLogSlowerThan" ])
+    (mkRenamedOptionModule [ "services" "redis" "slowLogMaxLen"] [ "services" "redis" "servers" "" "slowLogMaxLen" ])
+    (mkRenamedOptionModule [ "services" "redis" "settings"] [ "services" "redis" "servers" "" "settings" ])
   ];
 
   ###### interface
@@ -32,18 +54,6 @@ in {
   options = {
 
     services.redis = {
-
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Whether to enable the Redis server. Note that the NixOS module for
-          Redis disables kernel support for Transparent Huge Pages (THP),
-          because this features causes major performance problems for Redis,
-          e.g. (https://redis.io/topics/latency).
-        '';
-      };
-
       package = mkOption {
         type = types.package;
         default = pkgs.redis;
@@ -51,176 +61,226 @@ in {
         description = "Which Redis derivation to use.";
       };
 
-      port = mkOption {
-        type = types.port;
-        default = 6379;
-        description = "The port for Redis to listen to.";
-      };
-
-      vmOverCommit = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Set vm.overcommit_memory to 1 (Suggested for Background Saving: http://redis.io/topics/faq)
-        '';
-      };
+      vmOverCommit = mkEnableOption ''
+        setting of vm.overcommit_memory to 1
+        (Suggested for Background Saving: http://redis.io/topics/faq)
+      '';
 
-      openFirewall = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Whether to open ports in the firewall for the server.
-        '';
-      };
+      servers = mkOption {
+        type = with types; attrsOf (submodule ({config, name, ...}@args: {
+          options = {
+            enable = mkEnableOption ''
+              Redis server.
+
+              Note that the NixOS module for Redis disables kernel support
+              for Transparent Huge Pages (THP),
+              because this features causes major performance problems for Redis,
+              e.g. (https://redis.io/topics/latency).
+            '';
+
+            user = mkOption {
+              type = types.str;
+              default = redisName name;
+              defaultText = "\"redis\" or \"redis-\${name}\" if name != \"\"";
+              description = "The username and groupname for redis-server.";
+            };
 
-      bind = mkOption {
-        type = with types; nullOr str;
-        default = "127.0.0.1";
-        description = ''
-          The IP interface to bind to.
-          <literal>null</literal> means "all interfaces".
-        '';
-        example = "192.0.2.1";
-      };
+            port = mkOption {
+              type = types.port;
+              default = 6379;
+              description = "The port for Redis to listen to.";
+            };
 
-      unixSocket = mkOption {
-        type = with types; nullOr path;
-        default = null;
-        description = "The path to the socket to bind to.";
-        example = "/run/redis/redis.sock";
-      };
+            openFirewall = mkOption {
+              type = types.bool;
+              default = false;
+              description = ''
+                Whether to open ports in the firewall for the server.
+              '';
+            };
 
-      unixSocketPerm = mkOption {
-        type = types.int;
-        default = 750;
-        description = "Change permissions for the socket";
-        example = 700;
-      };
+            bind = mkOption {
+              type = with types; nullOr str;
+              default = if name == "" then "127.0.0.1" else null;
+              defaultText = "127.0.0.1 or null if name != \"\"";
+              description = ''
+                The IP interface to bind to.
+                <literal>null</literal> means "all interfaces".
+              '';
+              example = "192.0.2.1";
+            };
 
-      logLevel = mkOption {
-        type = types.str;
-        default = "notice"; # debug, verbose, notice, warning
-        example = "debug";
-        description = "Specify the server verbosity level, options: debug, verbose, notice, warning.";
-      };
+            unixSocket = mkOption {
+              type = with types; nullOr path;
+              default = "/run/${redisName name}/redis.sock";
+              defaultText = "\"/run/redis/redis.sock\" or \"/run/redis-\${name}/redis.sock\" if name != \"\"";
+              description = "The path to the socket to bind to.";
+            };
 
-      logfile = mkOption {
-        type = types.str;
-        default = "/dev/null";
-        description = "Specify the log file name. Also 'stdout' can be used to force Redis to log on the standard output.";
-        example = "/var/log/redis.log";
-      };
+            unixSocketPerm = mkOption {
+              type = types.int;
+              default = 660;
+              description = "Change permissions for the socket";
+              example = 600;
+            };
 
-      syslog = mkOption {
-        type = types.bool;
-        default = true;
-        description = "Enable logging to the system logger.";
-      };
+            logLevel = mkOption {
+              type = types.str;
+              default = "notice"; # debug, verbose, notice, warning
+              example = "debug";
+              description = "Specify the server verbosity level, options: debug, verbose, notice, warning.";
+            };
 
-      databases = mkOption {
-        type = types.int;
-        default = 16;
-        description = "Set the number of databases.";
-      };
+            logfile = mkOption {
+              type = types.str;
+              default = "/dev/null";
+              description = "Specify the log file name. Also 'stdout' can be used to force Redis to log on the standard output.";
+              example = "/var/log/redis.log";
+            };
 
-      maxclients = mkOption {
-        type = types.int;
-        default = 10000;
-        description = "Set the max number of connected clients at the same time.";
-      };
+            syslog = mkOption {
+              type = types.bool;
+              default = true;
+              description = "Enable logging to the system logger.";
+            };
 
-      save = mkOption {
-        type = with types; listOf (listOf int);
-        default = [ [900 1] [300 10] [60 10000] ];
-        description = "The schedule in which data is persisted to disk, represented as a list of lists where the first element represent the amount of seconds and the second the number of changes.";
-      };
+            databases = mkOption {
+              type = types.int;
+              default = 16;
+              description = "Set the number of databases.";
+            };
 
-      slaveOf = mkOption {
-        type = with types; nullOr (submodule ({ ... }: {
-          options = {
-            ip = mkOption {
-              type = str;
-              description = "IP of the Redis master";
-              example = "192.168.1.100";
+            maxclients = mkOption {
+              type = types.int;
+              default = 10000;
+              description = "Set the max number of connected clients at the same time.";
             };
 
-            port = mkOption {
-              type = port;
-              description = "port of the Redis master";
-              default = 6379;
+            save = mkOption {
+              type = with types; listOf (listOf int);
+              default = [ [900 1] [300 10] [60 10000] ];
+              description = "The schedule in which data is persisted to disk, represented as a list of lists where the first element represent the amount of seconds and the second the number of changes.";
             };
-          };
-        }));
 
-        default = null;
-        description = "IP and port to which this redis instance acts as a slave.";
-        example = { ip = "192.168.1.100"; port = 6379; };
-      };
+            slaveOf = mkOption {
+              type = with types; nullOr (submodule ({ ... }: {
+                options = {
+                  ip = mkOption {
+                    type = str;
+                    description = "IP of the Redis master";
+                    example = "192.168.1.100";
+                  };
+
+                  port = mkOption {
+                    type = port;
+                    description = "port of the Redis master";
+                    default = 6379;
+                  };
+                };
+              }));
+
+              default = null;
+              description = "IP and port to which this redis instance acts as a slave.";
+              example = { ip = "192.168.1.100"; port = 6379; };
+            };
 
-      masterAuth = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = ''If the master is password protected (using the requirePass configuration)
-        it is possible to tell the slave to authenticate before starting the replication synchronization
-        process, otherwise the master will refuse the slave request.
-        (STORED PLAIN TEXT, WORLD-READABLE IN NIX STORE)'';
-      };
+            masterAuth = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = ''If the master is password protected (using the requirePass configuration)
+              it is possible to tell the slave to authenticate before starting the replication synchronization
+              process, otherwise the master will refuse the slave request.
+              (STORED PLAIN TEXT, WORLD-READABLE IN NIX STORE)'';
+            };
 
-      requirePass = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = ''
-          Password for database (STORED PLAIN TEXT, WORLD-READABLE IN NIX STORE).
-          Use requirePassFile to store it outside of the nix store in a dedicated file.
-        '';
-        example = "letmein!";
-      };
+            requirePass = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = ''
+                Password for database (STORED PLAIN TEXT, WORLD-READABLE IN NIX STORE).
+                Use requirePassFile to store it outside of the nix store in a dedicated file.
+              '';
+              example = "letmein!";
+            };
 
-      requirePassFile = mkOption {
-        type = with types; nullOr path;
-        default = null;
-        description = "File with password for the database.";
-        example = "/run/keys/redis-password";
-      };
+            requirePassFile = mkOption {
+              type = with types; nullOr path;
+              default = null;
+              description = "File with password for the database.";
+              example = "/run/keys/redis-password";
+            };
 
-      appendOnly = mkOption {
-        type = types.bool;
-        default = false;
-        description = "By default data is only periodically persisted to disk, enable this option to use an append-only file for improved persistence.";
-      };
+            appendOnly = mkOption {
+              type = types.bool;
+              default = false;
+              description = "By default data is only periodically persisted to disk, enable this option to use an append-only file for improved persistence.";
+            };
 
-      appendFsync = mkOption {
-        type = types.str;
-        default = "everysec"; # no, always, everysec
-        description = "How often to fsync the append-only log, options: no, always, everysec.";
-      };
+            appendFsync = mkOption {
+              type = types.str;
+              default = "everysec"; # no, always, everysec
+              description = "How often to fsync the append-only log, options: no, always, everysec.";
+            };
 
-      slowLogLogSlowerThan = mkOption {
-        type = types.int;
-        default = 10000;
-        description = "Log queries whose execution take longer than X in milliseconds.";
-        example = 1000;
-      };
+            slowLogLogSlowerThan = mkOption {
+              type = types.int;
+              default = 10000;
+              description = "Log queries whose execution take longer than X in milliseconds.";
+              example = 1000;
+            };
 
-      slowLogMaxLen = mkOption {
-        type = types.int;
-        default = 128;
-        description = "Maximum number of items to keep in slow log.";
-      };
+            slowLogMaxLen = mkOption {
+              type = types.int;
+              default = 128;
+              description = "Maximum number of items to keep in slow log.";
+            };
 
-      settings = mkOption {
-        type = with types; attrsOf (oneOf [ bool int str (listOf str) ]);
+            settings = mkOption {
+              # TODO: this should be converted to freeformType
+              type = with types; attrsOf (oneOf [ bool int str (listOf str) ]);
+              default = {};
+              description = ''
+                Redis configuration. Refer to
+                <link xlink:href="https://redis.io/topics/config"/>
+                for details on supported values.
+              '';
+              example = literalExpression ''
+                {
+                  loadmodule = [ "/path/to/my_module.so" "/path/to/other_module.so" ];
+                }
+              '';
+            };
+          };
+          config.settings = mkMerge [
+            {
+              port = if config.bind == null then 0 else config.port;
+              daemonize = false;
+              supervised = "systemd";
+              loglevel = config.logLevel;
+              logfile = config.logfile;
+              syslog-enabled = config.syslog;
+              databases = config.databases;
+              maxclients = config.maxclients;
+              save = map (d: "${toString (builtins.elemAt d 0)} ${toString (builtins.elemAt d 1)}") config.save;
+              dbfilename = "dump.rdb";
+              dir = "/var/lib/${redisName name}";
+              appendOnly = config.appendOnly;
+              appendfsync = config.appendFsync;
+              slowlog-log-slower-than = config.slowLogLogSlowerThan;
+              slowlog-max-len = config.slowLogMaxLen;
+            }
+            (mkIf (config.bind != null) { bind = config.bind; })
+            (mkIf (config.unixSocket != null) {
+              unixsocket = config.unixSocket;
+              unixsocketperm = toString config.unixSocketPerm;
+            })
+            (mkIf (config.slaveOf != null) { slaveof = "${config.slaveOf.ip} ${toString config.slaveOf.port}"; })
+            (mkIf (config.masterAuth != null) { masterauth = config.masterAuth; })
+            (mkIf (config.requirePass != null) { requirepass = config.requirePass; })
+          ];
+        }));
+        description = "Configuration of multiple <literal>redis-server</literal> instances.";
         default = {};
-        description = ''
-          Redis configuration. Refer to
-          <link xlink:href="https://redis.io/topics/config"/>
-          for details on supported values.
-        '';
-        example = literalExpression ''
-          {
-            loadmodule = [ "/path/to/my_module.so" "/path/to/other_module.so" ];
-          }
-        '';
       };
     };
 
@@ -229,78 +289,61 @@ in {
 
   ###### implementation
 
-  config = mkIf config.services.redis.enable {
-    assertions = [{
-      assertion = cfg.requirePass != null -> cfg.requirePassFile == null;
-      message = "You can only set one services.redis.requirePass or services.redis.requirePassFile";
-    }];
-    boot.kernel.sysctl = (mkMerge [
+  config = mkIf (enabledServers != {}) {
+
+    assertions = attrValues (mapAttrs (name: conf: {
+      assertion = conf.requirePass != null -> conf.requirePassFile == null;
+      message = ''
+        You can only set one services.redis.servers.${name}.requirePass
+        or services.redis.servers.${name}.requirePassFile
+      '';
+    }) enabledServers);
+
+    boot.kernel.sysctl = mkMerge [
       { "vm.nr_hugepages" = "0"; }
       ( mkIf cfg.vmOverCommit { "vm.overcommit_memory" = "1"; } )
-    ]);
+    ];
 
-    networking.firewall = mkIf cfg.openFirewall {
-      allowedTCPPorts = [ cfg.port ];
-    };
-
-    users.users.redis = {
-      description = "Redis database user";
-      group = "redis";
-      isSystemUser = true;
-    };
-    users.groups.redis = {};
+    networking.firewall.allowedTCPPorts = concatMap (conf:
+      optional conf.openFirewall conf.port
+    ) (attrValues enabledServers);
 
     environment.systemPackages = [ cfg.package ];
 
-    services.redis.settings = mkMerge [
-      {
-        port = cfg.port;
-        daemonize = false;
-        supervised = "systemd";
-        loglevel = cfg.logLevel;
-        logfile = cfg.logfile;
-        syslog-enabled = cfg.syslog;
-        databases = cfg.databases;
-        maxclients = cfg.maxclients;
-        save = map (d: "${toString (builtins.elemAt d 0)} ${toString (builtins.elemAt d 1)}") cfg.save;
-        dbfilename = "dump.rdb";
-        dir = "/var/lib/redis";
-        appendOnly = cfg.appendOnly;
-        appendfsync = cfg.appendFsync;
-        slowlog-log-slower-than = cfg.slowLogLogSlowerThan;
-        slowlog-max-len = cfg.slowLogMaxLen;
-      }
-      (mkIf (cfg.bind != null) { bind = cfg.bind; })
-      (mkIf (cfg.unixSocket != null) { unixsocket = cfg.unixSocket; unixsocketperm = "${toString cfg.unixSocketPerm}"; })
-      (mkIf (cfg.slaveOf != null) { slaveof = "${cfg.slaveOf.ip} ${toString cfg.slaveOf.port}"; })
-      (mkIf (cfg.masterAuth != null) { masterauth = cfg.masterAuth; })
-      (mkIf (cfg.requirePass != null) { requirepass = cfg.requirePass; })
-    ];
+    users.users = mapAttrs' (name: conf: nameValuePair (redisName name) {
+      description = "System user for the redis-server instance ${name}";
+      isSystemUser = true;
+      group = redisName name;
+    }) enabledServers;
+    users.groups = mapAttrs' (name: conf: nameValuePair (redisName name) {
+    }) enabledServers;
 
-    systemd.services.redis = {
-      description = "Redis Server";
+    systemd.services = mapAttrs' (name: conf: nameValuePair (redisName name) {
+      description = "Redis Server - ${redisName name}";
 
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" ];
 
-      preStart = ''
-        install -m 600 ${redisConfig} /run/redis/redis.conf
-      '' + optionalString (cfg.requirePassFile != null) ''
-        password=$(cat ${escapeShellArg cfg.requirePassFile})
-        echo "requirePass $password" >> /run/redis/redis.conf
-      '';
-
       serviceConfig = {
-        ExecStart = "${cfg.package}/bin/redis-server /run/redis/redis.conf";
+        ExecStart = "${cfg.package}/bin/redis-server /run/${redisName name}/redis.conf";
+        ExecStartPre = [("+"+pkgs.writeShellScript "${redisName name}-credentials" (''
+            install -o '${conf.user}' -m 600 ${redisConfig conf.settings} /run/${redisName name}/redis.conf
+          '' + optionalString (conf.requirePassFile != null) ''
+            {
+              printf requirePass' '
+              cat ${escapeShellArg conf.requirePassFile}
+            } >>/run/${redisName name}/redis.conf
+          '')
+        )];
         Type = "notify";
         # User and group
-        User = "redis";
-        Group = "redis";
+        User = conf.user;
+        Group = conf.user;
         # Runtime directory and mode
-        RuntimeDirectory = "redis";
+        RuntimeDirectory = redisName name;
         RuntimeDirectoryMode = "0750";
         # State directory and mode
-        StateDirectory = "redis";
+        StateDirectory = redisName name;
         StateDirectoryMode = "0700";
         # Access write directories
         UMask = "0077";
@@ -309,7 +352,7 @@ in {
         # Security
         NoNewPrivileges = true;
         # Process Properties
-        LimitNOFILE = "${toString ulimitNofile}";
+        LimitNOFILE = mkDefault "${toString (conf.maxclients + 32)}";
         # Sandboxing
         ProtectSystem = "strict";
         ProtectHome = true;
@@ -322,7 +365,9 @@ in {
         ProtectKernelModules = true;
         ProtectKernelTunables = true;
         ProtectControlGroups = true;
-        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+        RestrictAddressFamilies =
+          optionals (conf.bind != null) ["AF_INET" "AF_INET6"] ++
+          optional (conf.unixSocket != null) "AF_UNIX";
         RestrictNamespaces = true;
         LockPersonality = true;
         MemoryDenyWriteExecute = true;
@@ -333,6 +378,7 @@ in {
         SystemCallArchitectures = "native";
         SystemCallFilter = "~@cpu-emulation @debug @keyring @memlock @mount @obsolete @privileged @resources @setuid";
       };
-    };
+    }) enabledServers;
+
   };
 }
diff --git a/nixos/modules/services/desktops/gnome/glib-networking.nix b/nixos/modules/services/desktops/gnome/glib-networking.nix
index 1039605391ab6..4288b6b5de616 100644
--- a/nixos/modules/services/desktops/gnome/glib-networking.nix
+++ b/nixos/modules/services/desktops/gnome/glib-networking.nix
@@ -38,7 +38,7 @@ with lib;
 
     systemd.packages = [ pkgs.glib-networking ];
 
-    environment.sessionVariables.GIO_EXTRA_MODULES = [ "${pkgs.glib-networking.out}/lib/gio/modules" ];
+    environment.variables.GIO_EXTRA_MODULES = [ "${pkgs.glib-networking.out}/lib/gio/modules" ];
 
   };
 
diff --git a/nixos/modules/services/desktops/gvfs.nix b/nixos/modules/services/desktops/gvfs.nix
index f2b2855dc3e04..cc9a460327059 100644
--- a/nixos/modules/services/desktops/gvfs.nix
+++ b/nixos/modules/services/desktops/gvfs.nix
@@ -54,10 +54,10 @@ in
 
     systemd.packages = [ cfg.package ];
 
-    services.udev.packages = [ pkgs.libmtp.bin ];
+    services.udev.packages = [ pkgs.libmtp ];
 
     # Needed for unwrapped applications
-    environment.sessionVariables.GIO_EXTRA_MODULES = [ "${cfg.package}/lib/gio/modules" ];
+    environment.variables.GIO_EXTRA_MODULES = [ "${cfg.package}/lib/gio/modules" ];
 
   };
 
diff --git a/nixos/modules/services/development/hoogle.nix b/nixos/modules/services/development/hoogle.nix
index 7c635f7a5b8d7..7c2a1c8e16247 100644
--- a/nixos/modules/services/development/hoogle.nix
+++ b/nixos/modules/services/development/hoogle.nix
@@ -40,6 +40,7 @@ in {
 
     haskellPackages = mkOption {
       description = "Which haskell package set to use.";
+      type = types.attrs;
       default = pkgs.haskellPackages;
       defaultText = literalExpression "pkgs.haskellPackages";
     };
diff --git a/nixos/modules/services/games/quake3-server.nix b/nixos/modules/services/games/quake3-server.nix
index 1dc01260e8fad..175af4a838289 100644
--- a/nixos/modules/services/games/quake3-server.nix
+++ b/nixos/modules/services/games/quake3-server.nix
@@ -71,6 +71,7 @@ in {
       baseq3 = mkOption {
         type = types.either types.package types.path;
         default = defaultBaseq3;
+        defaultText = literalDocBook "Manually downloaded Quake 3 installation directory.";
         example = "/var/lib/q3ds";
         description = ''
           Path to the baseq3 files (pak*.pk3). If this is on the nix store (type = package) all .pk3 files should be saved
diff --git a/nixos/modules/services/games/terraria.nix b/nixos/modules/services/games/terraria.nix
index 7312c7e6b6352..29f976b3c2aec 100644
--- a/nixos/modules/services/games/terraria.nix
+++ b/nixos/modules/services/games/terraria.nix
@@ -1,9 +1,10 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
   cfg   = config.services.terraria;
+  opt   = options.services.terraria;
   worldSizeMap = { small = 1; medium = 2; large = 3; };
   valFlag = name: val: optionalString (val != null) "-${name} \"${escape ["\\" "\""] (toString val)}\"";
   boolFlag = name: val: optionalString val "-${name}";
@@ -36,7 +37,7 @@ in
         type        = types.bool;
         default     = false;
         description = ''
-          If enabled, starts a Terraria server. The server can be connected to via <literal>tmux -S ${cfg.dataDir}/terraria.sock attach</literal>
+          If enabled, starts a Terraria server. The server can be connected to via <literal>tmux -S ''${config.${opt.dataDir}}/terraria.sock attach</literal>
           for administration by users who are a part of the <literal>terraria</literal> group (use <literal>C-b d</literal> shortcut to detach again).
         '';
       };
diff --git a/nixos/modules/services/hardware/rasdaemon.nix b/nixos/modules/services/hardware/rasdaemon.nix
index b1efe0f18c88b..2d4c6d2ce959e 100644
--- a/nixos/modules/services/hardware/rasdaemon.nix
+++ b/nixos/modules/services/hardware/rasdaemon.nix
@@ -137,7 +137,6 @@ in
         description = "the RAS logging daemon";
         documentation = [ "man:rasdaemon(1)" ];
         wantedBy = [ "multi-user.target" ];
-        after = [ "syslog.target" ];
 
         serviceConfig = {
           StateDirectory = optionalString (cfg.record) "rasdaemon";
diff --git a/nixos/modules/services/hardware/spacenavd.nix b/nixos/modules/services/hardware/spacenavd.nix
index 74725dd23d25c..69ca6f102efe7 100644
--- a/nixos/modules/services/hardware/spacenavd.nix
+++ b/nixos/modules/services/hardware/spacenavd.nix
@@ -15,7 +15,6 @@ in {
   config = mkIf cfg.enable {
     systemd.user.services.spacenavd = {
       description = "Daemon for the Spacenavigator 6DOF mice by 3Dconnexion";
-      after = [ "syslog.target" ];
       wantedBy = [ "graphical.target" ];
       serviceConfig = {
         ExecStart = "${pkgs.spacenavd}/bin/spacenavd -d -l syslog";
diff --git a/nixos/modules/services/hardware/tcsd.nix b/nixos/modules/services/hardware/tcsd.nix
index c549a67750136..e414b9647c9bb 100644
--- a/nixos/modules/services/hardware/tcsd.nix
+++ b/nixos/modules/services/hardware/tcsd.nix
@@ -1,11 +1,12 @@
 # tcsd daemon.
 
-{ config, pkgs, lib, ... }:
+{ config, options, pkgs, lib, ... }:
 
 with lib;
 let
 
   cfg = config.services.tcsd;
+  opt = options.services.tcsd;
 
   tcsdConf = pkgs.writeText "tcsd.conf" ''
     port = 30003
@@ -83,6 +84,7 @@ in
 
       platformCred = mkOption {
         default = "${cfg.stateDir}/platform.cert";
+        defaultText = literalExpression ''"''${config.${opt.stateDir}}/platform.cert"'';
         type = types.path;
         description = ''
           Path to the platform credential for your TPM. Your TPM
@@ -96,6 +98,7 @@ in
 
       conformanceCred = mkOption {
         default = "${cfg.stateDir}/conformance.cert";
+        defaultText = literalExpression ''"''${config.${opt.stateDir}}/conformance.cert"'';
         type = types.path;
         description = ''
           Path to the conformance credential for your TPM.
@@ -104,6 +107,7 @@ in
 
       endorsementCred = mkOption {
         default = "${cfg.stateDir}/endorsement.cert";
+        defaultText = literalExpression ''"''${config.${opt.stateDir}}/endorsement.cert"'';
         type = types.path;
         description = ''
           Path to the endorsement credential for your TPM.
diff --git a/nixos/modules/services/hardware/thinkfan.nix b/nixos/modules/services/hardware/thinkfan.nix
index 7a5a7e1c41ce2..4ea829e496e88 100644
--- a/nixos/modules/services/hardware/thinkfan.nix
+++ b/nixos/modules/services/hardware/thinkfan.nix
@@ -19,7 +19,7 @@ let
         description = "tuple of" + concatMapStrings (t: " (${t.description})") ts;
       };
       level = ints.unsigned;
-      special = enum [ "level auto" "level full-speed" "level disengage" ];
+      special = enum [ "level auto" "level full-speed" "level disengaged" ];
     in
       tuple [ (either level special) level level ];
 
@@ -164,7 +164,7 @@ in {
 
           LEVEL is the fan level to use: it can be an integer (0-7 with thinkpad_acpi),
           "level auto" (to keep the default firmware behavior), "level full-speed" or
-          "level disengage" (to run the fan as fast as possible).
+          "level disengaged" (to run the fan as fast as possible).
           LOW is the temperature at which to step down to the previous level.
           HIGH is the temperature at which to step up to the next level.
           All numbers are integers.
diff --git a/nixos/modules/services/logging/filebeat.nix b/nixos/modules/services/logging/filebeat.nix
new file mode 100644
index 0000000000000..223a993c505b4
--- /dev/null
+++ b/nixos/modules/services/logging/filebeat.nix
@@ -0,0 +1,253 @@
+{ config, lib, utils, pkgs, ... }:
+
+let
+  inherit (lib)
+    attrValues
+    literalExpression
+    mkEnableOption
+    mkIf
+    mkOption
+    types;
+
+  cfg = config.services.filebeat;
+
+  json = pkgs.formats.json {};
+in
+{
+  options = {
+
+    services.filebeat = {
+
+      enable = mkEnableOption "filebeat";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.filebeat;
+        defaultText = literalExpression "pkgs.filebeat";
+        example = literalExpression "pkgs.filebeat7";
+        description = ''
+          The filebeat package to use.
+        '';
+      };
+
+      inputs = mkOption {
+        description = ''
+          Inputs specify how Filebeat locates and processes input data.
+
+          This is like <literal>services.filebeat.settings.filebeat.inputs</literal>,
+          but structured as an attribute set. This has the benefit
+          that multiple NixOS modules can contribute settings to a
+          single filebeat input.
+
+          An input type can be specified multiple times by choosing a
+          different <literal>&lt;name></literal> for each, but setting
+          <xref linkend="opt-services.filebeat.inputs._name_.type"/>
+          to the same value.
+
+          See <link xlink:href="https://www.elastic.co/guide/en/beats/filebeat/current/configuration-filebeat-options.html"/>.
+        '';
+        default = {};
+        type = types.attrsOf (types.submodule ({ name, ... }: {
+          freeformType = json.type;
+          options = {
+            type = mkOption {
+              type = types.str;
+              default = name;
+              description = ''
+                The input type.
+
+                Look for the value after <literal>type:</literal> on
+                the individual input pages linked from
+                <link xlink:href="https://www.elastic.co/guide/en/beats/filebeat/current/configuration-filebeat-options.html"/>.
+              '';
+            };
+          };
+        }));
+        example = literalExpression ''
+          {
+            journald.id = "everything";  # Only for filebeat7
+            log = {
+              enabled = true;
+              paths = [
+                "/var/log/*.log"
+              ];
+            };
+          };
+        '';
+      };
+
+      modules = mkOption {
+        description = ''
+          Filebeat modules provide a quick way to get started
+          processing common log formats. They contain default
+          configurations, Elasticsearch ingest pipeline definitions,
+          and Kibana dashboards to help you implement and deploy a log
+          monitoring solution.
+
+          This is like <literal>services.filebeat.settings.filebeat.modules</literal>,
+          but structured as an attribute set. This has the benefit
+          that multiple NixOS modules can contribute settings to a
+          single filebeat module.
+
+          A module can be specified multiple times by choosing a
+          different <literal>&lt;name></literal> for each, but setting
+          <xref linkend="opt-services.filebeat.modules._name_.module"/>
+          to the same value.
+
+          See <link xlink:href="https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-modules.html"/>.
+        '';
+        default = {};
+        type = types.attrsOf (types.submodule ({ name, ... }: {
+          freeformType = json.type;
+          options = {
+            module = mkOption {
+              type = types.str;
+              default = name;
+              description = ''
+                The name of the module.
+
+                Look for the value after <literal>module:</literal> on
+                the individual input pages linked from
+                <link xlink:href="https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-modules.html"/>.
+              '';
+            };
+          };
+        }));
+        example = literalExpression ''
+          {
+            nginx = {
+              access = {
+                enabled = true;
+                var.paths = [ "/path/to/log/nginx/access.log*" ];
+              };
+              error = {
+                enabled = true;
+                var.paths = [ "/path/to/log/nginx/error.log*" ];
+              };
+            };
+          };
+        '';
+      };
+
+      settings = mkOption {
+        type = types.submodule {
+          freeformType = json.type;
+
+          options = {
+
+            output.elasticsearch.hosts = mkOption {
+              type = with types; listOf str;
+              default = [ "127.0.0.1:9200" ];
+              example = [ "myEShost:9200" ];
+              description = ''
+                The list of Elasticsearch nodes to connect to.
+
+                The events are distributed to these nodes in round
+                robin order. If one node becomes unreachable, the
+                event is automatically sent to another node. Each
+                Elasticsearch node can be defined as a URL or
+                IP:PORT. For example:
+                <literal>http://192.15.3.2</literal>,
+                <literal>https://es.found.io:9230</literal> or
+                <literal>192.24.3.2:9300</literal>. If no port is
+                specified, <literal>9200</literal> is used.
+              '';
+            };
+
+            filebeat = {
+              inputs = mkOption {
+                type = types.listOf json.type;
+                default = [];
+                internal = true;
+                description = ''
+                  Inputs specify how Filebeat locates and processes
+                  input data. Use <xref
+                  linkend="opt-services.filebeat.inputs"/> instead.
+
+                  See <link xlink:href="https://www.elastic.co/guide/en/beats/filebeat/current/configuration-filebeat-options.html"/>.
+                '';
+              };
+              modules = mkOption {
+                type = types.listOf json.type;
+                default = [];
+                internal = true;
+                description = ''
+                  Filebeat modules provide a quick way to get started
+                  processing common log formats. They contain default
+                  configurations, Elasticsearch ingest pipeline
+                  definitions, and Kibana dashboards to help you
+                  implement and deploy a log monitoring solution.
+
+                  Use <xref linkend="opt-services.filebeat.modules"/> instead.
+
+                  See <link xlink:href="https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-modules.html"/>.
+                '';
+              };
+            };
+          };
+        };
+        default = {};
+        example = literalExpression ''
+          {
+            settings = {
+              output.elasticsearch = {
+                hosts = [ "myEShost:9200" ];
+                username = "filebeat_internal";
+                password = { _secret = "/var/keys/elasticsearch_password"; };
+              };
+              logging.level = "info";
+            };
+          };
+        '';
+
+        description = ''
+          Configuration for filebeat. See
+          <link xlink:href="https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-reference-yml.html"/>
+          for supported values.
+
+          Options containing secret data should be set to an attribute
+          set containing the attribute <literal>_secret</literal> - a
+          string pointing to a file containing the value the option
+          should be set to. See the example to get a better picture of
+          this: in the resulting
+          <filename>filebeat.yml</filename> file, the
+          <literal>output.elasticsearch.password</literal>
+          key will be set to the contents of the
+          <filename>/var/keys/elasticsearch_password</filename> file.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    services.filebeat.settings.filebeat.inputs = attrValues cfg.inputs;
+    services.filebeat.settings.filebeat.modules = attrValues cfg.modules;
+
+    systemd.services.filebeat = {
+      description = "Filebeat log shipper";
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "elasticsearch.service" ];
+      after = [ "elasticsearch.service" ];
+      serviceConfig = {
+        ExecStartPre = pkgs.writeShellScript "filebeat-exec-pre" ''
+          set -euo pipefail
+
+          umask u=rwx,g=,o=
+
+          ${utils.genJqSecretsReplacementSnippet
+              cfg.settings
+              "/var/lib/filebeat/filebeat.yml"
+           }
+        '';
+        ExecStart = ''
+          ${cfg.package}/bin/filebeat -e \
+            -c "/var/lib/filebeat/filebeat.yml" \
+            --path.data "/var/lib/filebeat"
+        '';
+        Restart = "always";
+        StateDirectory = "filebeat";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/logging/journalbeat.nix b/nixos/modules/services/logging/journalbeat.nix
index 2d98598c1bee0..4035ab48b4b87 100644
--- a/nixos/modules/services/logging/journalbeat.nix
+++ b/nixos/modules/services/logging/journalbeat.nix
@@ -5,14 +5,10 @@ with lib;
 let
   cfg = config.services.journalbeat;
 
-  lt6 = builtins.compareVersions cfg.package.version "6" < 0;
-
   journalbeatYml = pkgs.writeText "journalbeat.yml" ''
     name: ${cfg.name}
     tags: ${builtins.toJSON cfg.tags}
 
-    ${optionalString lt6 "journalbeat.cursor_state_file: /var/lib/${cfg.stateDir}/cursor-state"}
-
     ${cfg.extraConfig}
   '';
 
@@ -28,7 +24,6 @@ in
         type = types.package;
         default = pkgs.journalbeat;
         defaultText = literalExpression "pkgs.journalbeat";
-        example = literalExpression "pkgs.journalbeat7";
         description = ''
           The journalbeat package to use
         '';
@@ -58,17 +53,7 @@ in
 
       extraConfig = mkOption {
         type = types.lines;
-        default = optionalString lt6 ''
-          journalbeat:
-            seek_position: cursor
-            cursor_seek_fallback: tail
-            write_cursor_state: true
-            cursor_flush_period: 5s
-            clean_field_names: true
-            convert_to_numbers: false
-            move_metadata_to_field: journal
-            default_type: journal
-        '';
+        default = "";
         description = "Any other configuration options you want to add";
       };
 
@@ -89,6 +74,8 @@ in
     systemd.services.journalbeat = {
       description = "Journalbeat log shipper";
       wantedBy = [ "multi-user.target" ];
+      wants = [ "elasticsearch.service" ];
+      after = [ "elasticsearch.service" ];
       preStart = ''
         mkdir -p ${cfg.stateDir}/data
         mkdir -p ${cfg.stateDir}/logs
diff --git a/nixos/modules/services/mail/maddy.nix b/nixos/modules/services/mail/maddy.nix
new file mode 100644
index 0000000000000..44cfa3c2908d7
--- /dev/null
+++ b/nixos/modules/services/mail/maddy.nix
@@ -0,0 +1,247 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  name = "maddy";
+  cfg = config.services.maddy;
+  defaultConfig = ''
+    tls off
+
+    auth.pass_table local_authdb {
+      table sql_table {
+        driver sqlite3
+        dsn credentials.db
+        table_name passwords
+      }
+    }
+
+    storage.imapsql local_mailboxes {
+      driver sqlite3
+      dsn imapsql.db
+    }
+
+    table.chain local_rewrites {
+      optional_step regexp "(.+)\+(.+)@(.+)" "$1@$3"
+      optional_step static {
+        entry postmaster postmaster@$(primary_domain)
+      }
+      optional_step file /etc/maddy/aliases
+    }
+    msgpipeline local_routing {
+      destination postmaster $(local_domains) {
+        modify {
+          replace_rcpt &local_rewrites
+        }
+        deliver_to &local_mailboxes
+      }
+      default_destination {
+        reject 550 5.1.1 "User doesn't exist"
+      }
+    }
+
+    smtp tcp://0.0.0.0:25 {
+      limits {
+        all rate 20 1s
+        all concurrency 10
+      }
+      dmarc yes
+      check {
+        require_mx_record
+        dkim
+        spf
+      }
+      source $(local_domains) {
+        reject 501 5.1.8 "Use Submission for outgoing SMTP"
+      }
+      default_source {
+        destination postmaster $(local_domains) {
+          deliver_to &local_routing
+        }
+        default_destination {
+          reject 550 5.1.1 "User doesn't exist"
+        }
+      }
+    }
+
+    submission tcp://0.0.0.0:587 {
+      limits {
+        all rate 50 1s
+      }
+      auth &local_authdb
+      source $(local_domains) {
+        check {
+            authorize_sender {
+                prepare_email &local_rewrites
+                user_to_email identity
+            }
+        }
+        destination postmaster $(local_domains) {
+            deliver_to &local_routing
+        }
+        default_destination {
+            modify {
+                dkim $(primary_domain) $(local_domains) default
+            }
+            deliver_to &remote_queue
+        }
+      }
+      default_source {
+        reject 501 5.1.8 "Non-local sender domain"
+      }
+    }
+
+    target.remote outbound_delivery {
+      limits {
+        destination rate 20 1s
+        destination concurrency 10
+      }
+      mx_auth {
+        dane
+        mtasts {
+          cache fs
+          fs_dir mtasts_cache/
+        }
+        local_policy {
+            min_tls_level encrypted
+            min_mx_level none
+        }
+      }
+    }
+
+    target.queue remote_queue {
+      target &outbound_delivery
+      autogenerated_msg_domain $(primary_domain)
+      bounce {
+        destination postmaster $(local_domains) {
+          deliver_to &local_routing
+        }
+        default_destination {
+            reject 550 5.0.0 "Refusing to send DSNs to non-local addresses"
+        }
+      }
+    }
+
+    imap tcp://0.0.0.0:143 {
+      auth &local_authdb
+      storage &local_mailboxes
+    }
+  '';
+
+in {
+  options = {
+    services.maddy = {
+      enable = mkEnableOption "Maddy, a free an open source mail server";
+
+      user = mkOption {
+        default = "maddy";
+        type = with types; uniq string;
+        description = ''
+          Name of the user under which maddy will run. If not specified, a
+          default user will be created.
+        '';
+      };
+      group = mkOption {
+        default = "maddy";
+        type = with types; uniq string;
+        description = ''
+          Name of the group under which maddy will run. If not specified, a
+          default group will be created.
+        '';
+      };
+
+      hostname = mkOption {
+        default = "localhost";
+        type = with types; uniq string;
+        example = ''example.com'';
+        description = ''
+          Hostname to use. It should be FQDN.
+        '';
+      };
+      primaryDomain = mkOption {
+        default = "localhost";
+        type = with types; uniq string;
+        example = ''mail.example.com'';
+        description = ''
+          Primary MX domain to use. It should be FQDN.
+        '';
+      };
+      localDomains = mkOption {
+        type = with types; listOf str;
+        default = ["$(primary_domain)"];
+        example = [
+          "$(primary_domain)"
+          "example.com"
+          "other.example.com"
+        ];
+        description = ''
+          Define list of allowed domains.
+        '';
+      };
+      config = mkOption {
+        type = with types; nullOr lines;
+        default = defaultConfig;
+        description = ''
+          Server configuration.
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open the configured incoming and outgoing mail server ports.
+        '';
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd = {
+      packages = [ pkgs.maddy ];
+      services.maddy = {
+        serviceConfig = {
+          User = "${cfg.user}";
+          Group = "${cfg.group}";
+        };
+        wantedBy = [ "multi-user.target" ];
+      };
+    };
+
+    environment.etc."maddy/maddy.conf" = {
+      text = ''
+        $(hostname) = ${cfg.hostname}
+        $(primary_domain) = ${cfg.primaryDomain}
+        $(local_domains) = ${toString cfg.localDomains}
+        hostname ${cfg.hostname}
+        ${cfg.config}
+      '';
+    };
+
+    users.users = optionalAttrs (cfg.user == "maddy") {
+      maddy = {
+        description = "Maddy service user";
+        group = cfg.group;
+        home = "/var/lib/maddy";
+        createHome = true;
+        isSystemUser = true;
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "maddy") {
+      maddy = pkgs.lib.mkForce {
+        name = cfg.group;
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ 25 143 587 ];
+    };
+
+    environment.systemPackages = [
+      pkgs.maddy
+    ];
+  };
+}
diff --git a/nixos/modules/services/mail/rspamd.nix b/nixos/modules/services/mail/rspamd.nix
index 50208cbeb00a8..a570e137a55a7 100644
--- a/nixos/modules/services/mail/rspamd.nix
+++ b/nixos/modules/services/mail/rspamd.nix
@@ -5,6 +5,7 @@ with lib;
 let
 
   cfg = config.services.rspamd;
+  opt = options.services.rspamd;
   postfixCfg = config.services.postfix;
 
   bindSocketOpts = {options, config, ... }: {
@@ -285,8 +286,8 @@ in
               bindSockets = [{
                 socket = "/run/rspamd/rspamd.sock";
                 mode = "0660";
-                owner = "${cfg.user}";
-                group = "${cfg.group}";
+                owner = "''${config.${opt.user}}";
+                group = "''${config.${opt.group}}";
               }];
             };
             controller = {
diff --git a/nixos/modules/services/misc/airsonic.nix b/nixos/modules/services/misc/airsonic.nix
index 533a3d367a32a..5a5c30a412330 100644
--- a/nixos/modules/services/misc/airsonic.nix
+++ b/nixos/modules/services/misc/airsonic.nix
@@ -1,9 +1,10 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
   cfg = config.services.airsonic;
+  opt = options.services.airsonic;
 in {
   options = {
 
@@ -78,7 +79,7 @@ in {
         description = ''
           List of paths to transcoder executables that should be accessible
           from Airsonic. Symlinks will be created to each executable inside
-          ${cfg.home}/transcoders.
+          ''${config.${opt.home}}/transcoders.
         '';
       };
 
diff --git a/nixos/modules/services/misc/dysnomia.nix b/nixos/modules/services/misc/dysnomia.nix
index 333ba651cde22..7d9c39a697370 100644
--- a/nixos/modules/services/misc/dysnomia.nix
+++ b/nixos/modules/services/misc/dysnomia.nix
@@ -104,31 +104,37 @@ in
       properties = mkOption {
         description = "An attribute set in which each attribute represents a machine property. Optionally, these values can be shell substitutions.";
         default = {};
+        type = types.attrs;
       };
 
       containers = mkOption {
         description = "An attribute set in which each key represents a container and each value an attribute set providing its configuration properties";
         default = {};
+        type = types.attrsOf types.attrs;
       };
 
       components = mkOption {
         description = "An atttribute set in which each key represents a container and each value an attribute set in which each key represents a component and each value a derivation constructing its initial state";
         default = {};
+        type = types.attrsOf types.attrs;
       };
 
       extraContainerProperties = mkOption {
         description = "An attribute set providing additional container settings in addition to the default properties";
         default = {};
+        type = types.attrs;
       };
 
       extraContainerPaths = mkOption {
         description = "A list of paths containing additional container configurations that are added to the search folders";
         default = [];
+        type = types.listOf types.path;
       };
 
       extraModulePaths = mkOption {
         description = "A list of paths containing additional modules that are added to the search folders";
         default = [];
+        type = types.listOf types.path;
       };
 
       enableLegacyModules = mkOption {
diff --git a/nixos/modules/services/misc/etcd.nix b/nixos/modules/services/misc/etcd.nix
index 26ad1ad5536aa..3925b7dd16367 100644
--- a/nixos/modules/services/misc/etcd.nix
+++ b/nixos/modules/services/misc/etcd.nix
@@ -1,9 +1,10 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
   cfg = config.services.etcd;
+  opt = options.services.etcd;
 
 in {
 
@@ -24,6 +25,7 @@ in {
     advertiseClientUrls = mkOption {
       description = "Etcd list of this member's client URLs to advertise to the rest of the cluster.";
       default = cfg.listenClientUrls;
+      defaultText = literalExpression "config.${opt.listenClientUrls}";
       type = types.listOf types.str;
     };
 
@@ -42,12 +44,14 @@ in {
     initialAdvertisePeerUrls = mkOption {
       description = "Etcd list of this member's peer URLs to advertise to rest of the cluster.";
       default = cfg.listenPeerUrls;
+      defaultText = literalExpression "config.${opt.listenPeerUrls}";
       type = types.listOf types.str;
     };
 
     initialCluster = mkOption {
       description = "Etcd initial cluster configuration for bootstrapping.";
       default = ["${cfg.name}=http://127.0.0.1:2380"];
+      defaultText = literalExpression ''["''${config.${opt.name}}=http://127.0.0.1:2380"]'';
       type = types.listOf types.str;
     };
 
@@ -96,18 +100,21 @@ in {
     peerCertFile = mkOption {
       description = "Cert file to use for peer to peer communication";
       default = cfg.certFile;
+      defaultText = literalExpression "config.${opt.certFile}";
       type = types.nullOr types.path;
     };
 
     peerKeyFile = mkOption {
       description = "Key file to use for peer to peer communication";
       default = cfg.keyFile;
+      defaultText = literalExpression "config.${opt.keyFile}";
       type = types.nullOr types.path;
     };
 
     peerTrustedCaFile = mkOption {
       description = "Certificate authority file to use for peer to peer communication";
       default = cfg.trustedCaFile;
+      defaultText = literalExpression "config.${opt.trustedCaFile}";
       type = types.nullOr types.path;
     };
 
diff --git a/nixos/modules/services/misc/exhibitor.nix b/nixos/modules/services/misc/exhibitor.nix
index 28c98edf47afe..4c935efbd8440 100644
--- a/nixos/modules/services/misc/exhibitor.nix
+++ b/nixos/modules/services/misc/exhibitor.nix
@@ -1,9 +1,10 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
   cfg = config.services.exhibitor;
+  opt = options.services.exhibitor;
   exhibitorConfig = ''
     zookeeper-install-directory=${cfg.baseDir}/zookeeper
     zookeeper-data-directory=${cfg.zkDataDir}
@@ -165,6 +166,7 @@ in
       zkDataDir = mkOption {
         type = types.str;
         default = "${cfg.baseDir}/zkData";
+        defaultText = literalExpression ''"''${config.${opt.baseDir}}/zkData"'';
         description = ''
           The Zookeeper data directory
         '';
@@ -172,6 +174,7 @@ in
       zkLogDir = mkOption {
         type = types.path;
         default = "${cfg.baseDir}/zkLogs";
+        defaultText = literalExpression ''"''${config.${opt.baseDir}}/zkLogs"'';
         description = ''
           The Zookeeper logs directory
         '';
diff --git a/nixos/modules/services/misc/gitea.nix b/nixos/modules/services/misc/gitea.nix
index 022a73c2b596a..0096286701f43 100644
--- a/nixos/modules/services/misc/gitea.nix
+++ b/nixos/modules/services/misc/gitea.nix
@@ -1,9 +1,10 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
   cfg = config.services.gitea;
+  opt = options.services.gitea;
   gitea = cfg.package;
   pg = config.services.postgresql;
   useMysql = cfg.database.type == "mysql";
@@ -51,6 +52,7 @@ in
       log = {
         rootPath = mkOption {
           default = "${cfg.stateDir}/log";
+          defaultText = literalExpression ''"''${config.${opt.stateDir}}/log"'';
           type = types.str;
           description = "Root path for log files.";
         };
@@ -84,6 +86,11 @@ in
         port = mkOption {
           type = types.port;
           default = (if !usePostgresql then 3306 else pg.port);
+          defaultText = literalExpression ''
+            if config.${opt.database.type} != "postgresql"
+            then 3306
+            else config.${options.services.postgresql.port}
+          '';
           description = "Database host port.";
         };
 
@@ -130,6 +137,7 @@ in
         path = mkOption {
           type = types.str;
           default = "${cfg.stateDir}/data/gitea.db";
+          defaultText = literalExpression ''"''${config.${opt.stateDir}}/data/gitea.db"'';
           description = "Path to the sqlite3 database file.";
         };
 
@@ -166,6 +174,7 @@ in
         backupDir = mkOption {
           type = types.str;
           default = "${cfg.stateDir}/dump";
+          defaultText = literalExpression ''"''${config.${opt.stateDir}}/dump"'';
           description = "Path to the dump files.";
         };
       };
@@ -199,6 +208,7 @@ in
         contentDir = mkOption {
           type = types.str;
           default = "${cfg.stateDir}/data/lfs";
+          defaultText = literalExpression ''"''${config.${opt.stateDir}}/data/lfs"'';
           description = "Where to store LFS files.";
         };
       };
@@ -212,6 +222,7 @@ in
       repositoryRoot = mkOption {
         type = types.str;
         default = "${cfg.stateDir}/repositories";
+        defaultText = literalExpression ''"''${config.${opt.stateDir}}/repositories"'';
         description = "Path to the git repositories.";
       };
 
diff --git a/nixos/modules/services/misc/gitlab.nix b/nixos/modules/services/misc/gitlab.nix
index 01a7ea42d9db4..219155777db95 100644
--- a/nixos/modules/services/misc/gitlab.nix
+++ b/nixos/modules/services/misc/gitlab.nix
@@ -1,9 +1,10 @@
-{ config, lib, pkgs, utils, ... }:
+{ config, lib, options, pkgs, utils, ... }:
 
 with lib;
 
 let
   cfg = config.services.gitlab;
+  opt = options.services.gitlab;
 
   ruby = cfg.packages.gitlab.ruby;
 
@@ -309,6 +310,7 @@ in {
       backup.path = mkOption {
         type = types.str;
         default = cfg.statePath + "/backup";
+        defaultText = literalExpression ''config.${opt.statePath} + "/backup"'';
         description = "GitLab path for backups.";
       };
 
@@ -554,6 +556,7 @@ in {
         defaultForProjects = mkOption {
           type = types.bool;
           default = cfg.registry.enable;
+          defaultText = literalExpression "config.${opt.registry.enable}";
           description = "If GitLab container registry should be enabled by default for projects.";
         };
         issuer = mkOption {
diff --git a/nixos/modules/services/misc/gitweb.nix b/nixos/modules/services/misc/gitweb.nix
index 13396bf2eb028..a1180716e36bb 100644
--- a/nixos/modules/services/misc/gitweb.nix
+++ b/nixos/modules/services/misc/gitweb.nix
@@ -47,6 +47,7 @@ in
         $highlight_bin = "${pkgs.highlight}/bin/highlight";
         ${cfg.extraConfig}
       '';
+      defaultText = literalDocBook "generated config file";
       type = types.path;
       readOnly = true;
       internal = true;
diff --git a/nixos/modules/services/misc/gogs.nix b/nixos/modules/services/misc/gogs.nix
index d7233f10c7cb8..c7ae4f494071b 100644
--- a/nixos/modules/services/misc/gogs.nix
+++ b/nixos/modules/services/misc/gogs.nix
@@ -1,9 +1,10 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
   cfg = config.services.gogs;
+  opt = options.services.gogs;
   configFile = pkgs.writeText "app.ini" ''
     APP_NAME = ${cfg.appName}
     RUN_USER = ${cfg.user}
@@ -129,6 +130,7 @@ in
         path = mkOption {
           type = types.str;
           default = "${cfg.stateDir}/data/gogs.db";
+          defaultText = literalExpression ''"''${config.${opt.stateDir}}/data/gogs.db"'';
           description = "Path to the sqlite3 database file.";
         };
       };
@@ -142,6 +144,7 @@ in
       repositoryRoot = mkOption {
         type = types.str;
         default = "${cfg.stateDir}/repositories";
+        defaultText = literalExpression ''"''${config.${opt.stateDir}}/repositories"'';
         description = "Path to the git repositories.";
       };
 
diff --git a/nixos/modules/services/misc/headphones.nix b/nixos/modules/services/misc/headphones.nix
index 3ee0a4458bd0e..31bd61cb4c200 100644
--- a/nixos/modules/services/misc/headphones.nix
+++ b/nixos/modules/services/misc/headphones.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
@@ -7,6 +7,7 @@ let
   name = "headphones";
 
   cfg = config.services.headphones;
+  opt = options.services.headphones;
 
 in
 
@@ -29,6 +30,7 @@ in
       configFile = mkOption {
         type = types.path;
         default = "${cfg.dataDir}/config.ini";
+        defaultText = literalExpression ''"''${config.${opt.dataDir}}/config.ini"'';
         description = "Path to config file.";
       };
       host = mkOption {
diff --git a/nixos/modules/services/misc/matrix-appservice-discord.nix b/nixos/modules/services/misc/matrix-appservice-discord.nix
index 947471e56b46d..8a8c7f41e3cbb 100644
--- a/nixos/modules/services/misc/matrix-appservice-discord.nix
+++ b/nixos/modules/services/misc/matrix-appservice-discord.nix
@@ -1,4 +1,4 @@
-{ config, pkgs, lib, ... }:
+{ config, options, pkgs, lib, ... }:
 
 with lib;
 
@@ -7,6 +7,7 @@ let
   registrationFile = "${dataDir}/discord-registration.yaml";
   appDir = "${pkgs.matrix-appservice-discord}/${pkgs.matrix-appservice-discord.passthru.nodeAppDir}";
   cfg = config.services.matrix-appservice-discord;
+  opt = options.services.matrix-appservice-discord;
   # TODO: switch to configGen.json once RFC42 is implemented
   settingsFile = pkgs.writeText "matrix-appservice-discord-settings.json" (builtins.toJSON cfg.settings);
 
@@ -74,6 +75,7 @@ in {
       url = mkOption {
         type = types.str;
         default = "http://localhost:${toString cfg.port}";
+        defaultText = literalExpression ''"http://localhost:''${toString config.${opt.port}}"'';
         description = ''
           The URL where the application service is listening for HS requests.
         '';
diff --git a/nixos/modules/services/misc/matrix-synapse.nix b/nixos/modules/services/misc/matrix-synapse.nix
index 0f96f6b1ee225..404163d2de6c5 100644
--- a/nixos/modules/services/misc/matrix-synapse.nix
+++ b/nixos/modules/services/misc/matrix-synapse.nix
@@ -1,9 +1,10 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
   cfg = config.services.matrix-synapse;
+  opt = options.services.matrix-synapse;
   pg = config.services.postgresql;
   usePostgresql = cfg.database_type == "psycopg2";
   logConfigFile = pkgs.writeText "log_config.yaml" cfg.logConfig;
@@ -197,7 +198,7 @@ in {
       tls_certificate_path = mkOption {
         type = types.nullOr types.str;
         default = null;
-        example = "${cfg.dataDir}/homeserver.tls.crt";
+        example = "/var/lib/matrix-synapse/homeserver.tls.crt";
         description = ''
           PEM encoded X509 certificate for TLS.
           You can replace the self-signed certificate that synapse
@@ -209,7 +210,7 @@ in {
       tls_private_key_path = mkOption {
         type = types.nullOr types.str;
         default = null;
-        example = "${cfg.dataDir}/homeserver.tls.key";
+        example = "/var/lib/matrix-synapse/homeserver.tls.key";
         description = ''
           PEM encoded private key for TLS. Specify null if synapse is not
           speaking TLS directly.
@@ -218,7 +219,7 @@ in {
       tls_dh_params_path = mkOption {
         type = types.nullOr types.str;
         default = null;
-        example = "${cfg.dataDir}/homeserver.tls.dh";
+        example = "/var/lib/matrix-synapse/homeserver.tls.dh";
         description = ''
           PEM dh parameters for ephemeral keys
         '';
@@ -408,6 +409,29 @@ in {
             database = cfg.database_name;
           };
         }.${cfg.database_type};
+        defaultText = literalDocBook ''
+          <variablelist>
+            <varlistentry>
+              <term>using sqlite3</term>
+              <listitem>
+                <programlisting>
+                  { database = "''${config.${opt.dataDir}}/homeserver.db"; }
+                </programlisting>
+              </listitem>
+            </varlistentry>
+            <varlistentry>
+              <term>using psycopg2</term>
+              <listitem>
+                <programlisting>
+                  psycopg2 = {
+                    user = config.${opt.database_user};
+                    database = config.${opt.database_name};
+                  }
+                </programlisting>
+              </listitem>
+            </varlistentry>
+          </variablelist>
+        '';
         description = ''
           Arguments to pass to the engine.
         '';
@@ -739,7 +763,7 @@ in {
       after = [ "network.target" ] ++ optional hasLocalPostgresDB "postgresql.service";
       wantedBy = [ "multi-user.target" ];
       preStart = ''
-        ${cfg.package}/bin/homeserver \
+        ${cfg.package}/bin/synapse_homeserver \
           --config-path ${configFile} \
           --keys-directory ${cfg.dataDir} \
           --generate-keys
@@ -759,7 +783,7 @@ in {
           chmod 0600 ${cfg.dataDir}/homeserver.signing.key
         '')) ];
         ExecStart = ''
-          ${cfg.package}/bin/homeserver \
+          ${cfg.package}/bin/synapse_homeserver \
             ${ concatMapStringsSep "\n  " (x: "--config-path ${x} \\") ([ configFile ] ++ cfg.extraConfigFiles) }
             --keys-directory ${cfg.dataDir}
         '';
diff --git a/nixos/modules/services/misc/mautrix-telegram.nix b/nixos/modules/services/misc/mautrix-telegram.nix
index 3b070b873b048..794c4dd9ddcd7 100644
--- a/nixos/modules/services/misc/mautrix-telegram.nix
+++ b/nixos/modules/services/misc/mautrix-telegram.nix
@@ -145,7 +145,7 @@ in {
             --config='${settingsFile}' \
             --registration='${registrationFile}'
         fi
-
+      '' + lib.optionalString (pkgs.mautrix-telegram ? alembic) ''
         # run automatic database init and migration scripts
         ${pkgs.mautrix-telegram.alembic}/bin/alembic -x config='${settingsFile}' upgrade head
       '';
diff --git a/nixos/modules/services/misc/mediatomb.nix b/nixos/modules/services/misc/mediatomb.nix
index 383090575b22a..ea9ffbb867751 100644
--- a/nixos/modules/services/misc/mediatomb.nix
+++ b/nixos/modules/services/misc/mediatomb.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
@@ -6,6 +6,7 @@ let
 
   gid = config.ids.gids.mediatomb;
   cfg = config.services.mediatomb;
+  opt = options.services.mediatomb;
   name = cfg.package.pname;
   pkg = cfg.package;
   optionYesNo = option: if option then "yes" else "no";
@@ -261,6 +262,7 @@ in {
       dataDir = mkOption {
         type = types.path;
         default = "/var/lib/${name}";
+        defaultText = literalExpression ''"/var/lib/''${config.${opt.package}.pname}"'';
         description = ''
           The directory where Gerbera/Mediatomb stores its state, data, etc.
         '';
@@ -277,13 +279,13 @@ in {
       user = mkOption {
         type = types.str;
         default = "mediatomb";
-        description = "User account under which ${name} runs.";
+        description = "User account under which the service runs.";
       };
 
       group = mkOption {
         type = types.str;
         default = "mediatomb";
-        description = "Group account under which ${name} runs.";
+        description = "Group account under which the service runs.";
       };
 
       port = mkOption {
@@ -340,7 +342,7 @@ in {
         type = types.bool;
         default = false;
         description = ''
-          Allow ${name} to create and use its own config file inside the <literal>dataDir</literal> as
+          Allow the service to create and use its own config file inside the <literal>dataDir</literal> as
           configured by <option>services.mediatomb.dataDir</option>.
           Deactivated by default, the service then runs with the configuration generated from this module.
           Otherwise, when enabled, no service configuration is generated. Gerbera/Mediatomb then starts using
diff --git a/nixos/modules/services/misc/moonraker.nix b/nixos/modules/services/misc/moonraker.nix
index e08d2f84212dc..ae57aaa6d4795 100644
--- a/nixos/modules/services/misc/moonraker.nix
+++ b/nixos/modules/services/misc/moonraker.nix
@@ -1,8 +1,9 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 with lib;
 let
   pkg = pkgs.moonraker;
   cfg = config.services.moonraker;
+  opt = options.services.moonraker;
   format = pkgs.formats.ini {
     # https://github.com/NixOS/nixpkgs/pull/121613#issuecomment-885241996
     listToValue = l:
@@ -31,6 +32,7 @@ in {
       configDir = mkOption {
         type = types.path;
         default = cfg.stateDir + "/config";
+        defaultText = literalExpression ''config.${opt.stateDir} + "/config"'';
         description = ''
           The directory containing client-writable configuration files.
 
diff --git a/nixos/modules/services/misc/mwlib.nix b/nixos/modules/services/misc/mwlib.nix
index 8dd17c06c0b35..fedc1e5542a4c 100644
--- a/nixos/modules/services/misc/mwlib.nix
+++ b/nixos/modules/services/misc/mwlib.nix
@@ -1,9 +1,10 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
   cfg = config.services.mwlib;
+  opt = options.services.mwlib;
   pypkgs = pkgs.python27Packages;
 
   inherit (pypkgs) python mwlib;
@@ -46,6 +47,9 @@ in
 
       qserve = mkOption {
         default = [ "${cfg.qserve.address}:${toString cfg.qserve.port}" ];
+        defaultText = literalExpression ''
+          [ "''${config.${opt.qserve.address}}:''${toString config.${opt.qserve.port}}"
+        ]'';
         type = types.listOf types.str;
         description = "Register qserve instance.";
       }; # nserve.qserve
@@ -96,6 +100,7 @@ in
     nslave = {
       enable = mkOption {
         default = cfg.qserve.enable;
+        defaultText = literalExpression "config.${opt.qserve.enable}";
         type = types.bool;
         description = ''
           Pulls new jobs from exactly one qserve instance
@@ -127,7 +132,7 @@ in
           You have to enable it, or use your own way for serving files
           and set the http.url option accordingly.
           '';
-        type = types.submodule ({
+        type = types.submodule ({ config, options, ... }: {
           options = {
             enable = mkOption {
               default = true;
@@ -148,7 +153,8 @@ in
             }; # nslave.http.address
 
             url = mkOption {
-              default = "http://localhost:${toString cfg.nslave.http.port}/cache";
+              default = "http://localhost:${toString config.port}/cache";
+              defaultText = literalExpression ''"http://localhost:''${toString config.${options.port}}/cache"'';
               type = types.str;
               description = ''
                 Specify URL for accessing generated files from cache.
diff --git a/nixos/modules/services/misc/nitter.nix b/nixos/modules/services/misc/nitter.nix
index 0c562343d85d3..6a9eeb02095cc 100644
--- a/nixos/modules/services/misc/nitter.nix
+++ b/nixos/modules/services/misc/nitter.nix
@@ -299,7 +299,7 @@ in
     systemd.services.nitter = {
         description = "Nitter (An alternative Twitter front-end)";
         wantedBy = [ "multi-user.target" ];
-        after = [ "syslog.target" "network.target" ];
+        after = [ "network.target" ];
         serviceConfig = {
           DynamicUser = true;
           StateDirectory = "nitter";
diff --git a/nixos/modules/services/misc/nix-daemon.nix b/nixos/modules/services/misc/nix-daemon.nix
index fb643e7a66e16..869feb05eb7b3 100644
--- a/nixos/modules/services/misc/nix-daemon.nix
+++ b/nixos/modules/services/misc/nix-daemon.nix
@@ -192,15 +192,28 @@ in
         example = "batch";
         description = ''
           Nix daemon process CPU scheduling policy. This policy propagates to
-          build processes. other is the default scheduling policy for regular
-          tasks. The batch policy is similar to other, but optimised for
-          non-interactive tasks. idle is for extremely low-priority tasks
-          that should only be run when no other task requires CPU time.
-
-          Please note that while using the idle policy may greatly improve
-          responsiveness of a system performing expensive builds, it may also
-          slow down and potentially starve crucial configuration updates
-          during load.
+          build processes. <literal>other</literal> is the default scheduling
+          policy for regular tasks. The <literal>batch</literal> policy is
+          similar to <literal>other</literal>, but optimised for
+          non-interactive tasks. <literal>idle</literal> is for extremely
+          low-priority tasks that should only be run when no other task
+          requires CPU time.
+
+          Please note that while using the <literal>idle</literal> policy may
+          greatly improve responsiveness of a system performing expensive
+          builds, it may also slow down and potentially starve crucial
+          configuration updates during load.
+
+          <literal>idle</literal> may therefore be a sensible policy for
+          systems that experience only intermittent phases of high CPU load,
+          such as desktop or portable computers used interactively. Other
+          systems should use the <literal>other</literal> or
+          <literal>batch</literal> policy instead.
+
+          For more fine-grained resource control, please refer to
+          <citerefentry><refentrytitle>systemd.resource-control
+          </refentrytitle><manvolnum>5</manvolnum></citerefentry> and adjust
+          <option>systemd.services.nix-daemon</option> directly.
       '';
       };
 
@@ -210,13 +223,20 @@ in
         example = "idle";
         description = ''
           Nix daemon process I/O scheduling class. This class propagates to
-          build processes. best-effort is the default class for regular tasks.
-          The idle class is for extremely low-priority tasks that should only
-          perform I/O when no other task does.
-
-          Please note that while using the idle scheduling class can improve
-          responsiveness of a system performing expensive builds, it might also
-          slow down or starve crucial configuration updates during load.
+          build processes. <literal>best-effort</literal> is the default
+          class for regular tasks. The <literal>idle</literal> class is for
+          extremely low-priority tasks that should only perform I/O when no
+          other task does.
+
+          Please note that while using the <literal>idle</literal> scheduling
+          class can improve responsiveness of a system performing expensive
+          builds, it might also slow down or starve crucial configuration
+          updates during load.
+
+          <literal>idle</literal> may therefore be a sensible class for
+          systems that experience only intermittent phases of high I/O load,
+          such as desktop or portable computers used interactively. Other
+          systems should use the <literal>best-effort</literal> class.
       '';
       };
 
diff --git a/nixos/modules/services/misc/rippled.nix b/nixos/modules/services/misc/rippled.nix
index 9c66df2fce1c3..f6ec0677774b3 100644
--- a/nixos/modules/services/misc/rippled.nix
+++ b/nixos/modules/services/misc/rippled.nix
@@ -1,9 +1,10 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
   cfg = config.services.rippled;
+  opt = options.services.rippled;
 
   b2i = val: if val then "1" else "0";
 
@@ -165,6 +166,7 @@ let
         description = "Location to store the database.";
         type = types.path;
         default = cfg.databasePath;
+        defaultText = literalExpression "config.${opt.databasePath}";
       };
 
       compression = mkOption {
@@ -177,6 +179,7 @@ let
         description = "Enable automatic purging of older ledger information.";
         type = types.nullOr (types.addCheck types.int (v: v > 256));
         default = cfg.ledgerHistory;
+        defaultText = literalExpression "config.${opt.ledgerHistory}";
       };
 
       advisoryDelete = mkOption {
@@ -398,6 +401,7 @@ in
       config = mkOption {
         internal = true;
         default = pkgs.writeText "rippled.conf" rippledCfg;
+        defaultText = literalDocBook "generated config file";
       };
     };
   };
diff --git a/nixos/modules/services/misc/sickbeard.nix b/nixos/modules/services/misc/sickbeard.nix
index 8e871309c98e8..a3db992863426 100644
--- a/nixos/modules/services/misc/sickbeard.nix
+++ b/nixos/modules/services/misc/sickbeard.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
@@ -7,6 +7,7 @@ let
   name = "sickbeard";
 
   cfg = config.services.sickbeard;
+  opt = options.services.sickbeard;
   sickbeard = cfg.package;
 
 in
@@ -39,6 +40,7 @@ in
       configFile = mkOption {
         type = types.path;
         default = "${cfg.dataDir}/config.ini";
+        defaultText = literalExpression ''"''${config.${opt.dataDir}}/config.ini"'';
         description = "Path to config file.";
       };
       port = mkOption {
diff --git a/nixos/modules/services/misc/sourcehut/builds.nix b/nixos/modules/services/misc/sourcehut/builds.nix
index f806e8c51b99e..685a132d35070 100644
--- a/nixos/modules/services/misc/sourcehut/builds.nix
+++ b/nixos/modules/services/misc/sourcehut/builds.nix
@@ -1,8 +1,9 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 let
   cfg = config.services.sourcehut;
+  opt = options.services.sourcehut;
   scfg = cfg.builds;
   rcfg = config.services.redis;
   iniKey = "builds.sr.ht";
@@ -38,6 +39,7 @@ in
     statePath = mkOption {
       type = types.path;
       default = "${cfg.statePath}/buildsrht";
+      defaultText = literalExpression ''"''${config.${opt.statePath}}/buildsrht"'';
       description = ''
         State path for builds.sr.ht.
       '';
@@ -61,7 +63,7 @@ in
               rev = "ff96a0fa5635770390b184ae74debea75c3fd534";
               ref = "nixos-unstable";
           };
-          image_from_nixpkgs = pkgs_unstable: (import ("${pkgs.sourcehut.buildsrht}/lib/images/nixos/image.nix") {
+          image_from_nixpkgs = pkgs_unstable: (import ("''${pkgs.sourcehut.buildsrht}/lib/images/nixos/image.nix") {
             pkgs = (import pkgs_unstable {});
           });
         in
diff --git a/nixos/modules/services/misc/sourcehut/default.nix b/nixos/modules/services/misc/sourcehut/default.nix
index c84a75b0ca029..1bd21c278e000 100644
--- a/nixos/modules/services/misc/sourcehut/default.nix
+++ b/nixos/modules/services/misc/sourcehut/default.nix
@@ -1,14 +1,90 @@
 { config, pkgs, lib, ... }:
-
 with lib;
 let
+  inherit (config.services) nginx postfix postgresql redis;
+  inherit (config.users) users groups;
   cfg = config.services.sourcehut;
-  cfgIni = cfg.settings;
-  settingsFormat = pkgs.formats.ini { };
+  domain = cfg.settings."sr.ht".global-domain;
+  settingsFormat = pkgs.formats.ini {
+    listToValue = concatMapStringsSep "," (generators.mkValueStringDefault {});
+    mkKeyValue = k: v:
+      if v == null then ""
+      else generators.mkKeyValueDefault {
+        mkValueString = v:
+          if v == true then "yes"
+          else if v == false then "no"
+          else generators.mkValueStringDefault {} v;
+      } "=" k v;
+  };
+  configIniOfService = srv: settingsFormat.generate "sourcehut-${srv}-config.ini"
+    # Each service needs access to only a subset of sections (and secrets).
+    (filterAttrs (k: v: v != null)
+    (mapAttrs (section: v:
+      let srvMatch = builtins.match "^([a-z]*)\\.sr\\.ht(::.*)?$" section; in
+      if srvMatch == null # Include sections shared by all services
+      || head srvMatch == srv # Include sections for the service being configured
+      then v
+      # Enable Web links and integrations between services.
+      else if tail srvMatch == [ null ] && elem (head srvMatch) cfg.services
+      then {
+        inherit (v) origin;
+        # mansrht crashes without it
+        oauth-client-id = v.oauth-client-id or null;
+      }
+      # Drop sub-sections of other services
+      else null)
+    (recursiveUpdate cfg.settings {
+      # Those paths are mounted using BindPaths= or BindReadOnlyPaths=
+      # for services needing access to them.
+      "builds.sr.ht::worker".buildlogs = "/var/log/sourcehut/buildsrht-worker";
+      "git.sr.ht".post-update-script = "/usr/bin/gitsrht-update-hook";
+      "git.sr.ht".repos = "/var/lib/sourcehut/gitsrht/repos";
+      "hg.sr.ht".changegroup-script = "/usr/bin/hgsrht-hook-changegroup";
+      "hg.sr.ht".repos = "/var/lib/sourcehut/hgsrht/repos";
+      # Making this a per service option despite being in a global section,
+      # so that it uses the redis-server used by the service.
+      "sr.ht".redis-host = cfg.${srv}.redis.host;
+    })));
+  commonServiceSettings = srv: {
+    origin = mkOption {
+      description = "URL ${srv}.sr.ht is being served at (protocol://domain)";
+      type = types.str;
+      default = "https://${srv}.${domain}";
+      defaultText = "https://${srv}.example.com";
+    };
+    debug-host = mkOption {
+      description = "Address to bind the debug server to.";
+      type = with types; nullOr str;
+      default = null;
+    };
+    debug-port = mkOption {
+      description = "Port to bind the debug server to.";
+      type = with types; nullOr str;
+      default = null;
+    };
+    connection-string = mkOption {
+      description = "SQLAlchemy connection string for the database.";
+      type = types.str;
+      default = "postgresql:///localhost?user=${srv}srht&host=/run/postgresql";
+    };
+    migrate-on-upgrade = mkEnableOption "automatic migrations on package upgrade" // { default = true; };
+    oauth-client-id = mkOption {
+      description = "${srv}.sr.ht's OAuth client id for meta.sr.ht.";
+      type = types.str;
+    };
+    oauth-client-secret = mkOption {
+      description = "${srv}.sr.ht's OAuth client secret for meta.sr.ht.";
+      type = types.path;
+      apply = s: "<" + toString s;
+    };
+  };
 
   # Specialized python containing all the modules
   python = pkgs.sourcehut.python.withPackages (ps: with ps; [
     gunicorn
+    eventlet
+    # For monitoring Celery: sudo -u listssrht celery --app listssrht.process -b redis+socket:///run/redis-sourcehut/redis.sock?virtual_host=5 flower
+    flower
     # Sourcehut services
     srht
     buildsrht
@@ -19,72 +95,37 @@ let
     listssrht
     mansrht
     metasrht
+    # Not a python package
+    #pagessrht
     pastesrht
     todosrht
   ]);
+  mkOptionNullOrStr = description: mkOption {
+    inherit description;
+    type = with types; nullOr str;
+    default = null;
+  };
 in
 {
-  imports =
-    [
-      ./git.nix
-      ./hg.nix
-      ./hub.nix
-      ./todo.nix
-      ./man.nix
-      ./meta.nix
-      ./paste.nix
-      ./builds.nix
-      ./lists.nix
-      ./dispatch.nix
-      (mkRemovedOptionModule [ "services" "sourcehut" "nginx" "enable" ] ''
-        The sourcehut module supports `nginx` as a local reverse-proxy by default and doesn't
-        support other reverse-proxies officially.
-
-        However it's possible to use an alternative reverse-proxy by
-
-          * disabling nginx
-          * adjusting the relevant settings for server addresses and ports directly
-
-        Further details about this can be found in the `Sourcehut`-section of the NixOS-manual.
-      '')
-    ];
-
   options.services.sourcehut = {
-    enable = mkOption {
-      type = types.bool;
-      default = false;
-      description = ''
-        Enable sourcehut - git hosting, continuous integration, mailing list, ticket tracking,
-        task dispatching, wiki and account management services
-      '';
-    };
+    enable = mkEnableOption ''
+      sourcehut - git hosting, continuous integration, mailing list, ticket tracking,
+      task dispatching, wiki and account management services
+    '';
 
     services = mkOption {
-      type = types.nonEmptyListOf (types.enum [ "builds" "dispatch" "git" "hub" "hg" "lists" "man" "meta" "paste" "todo" ]);
-      default = [ "man" "meta" "paste" ];
-      example = [ "builds" "dispatch" "git" "hub" "hg" "lists" "man" "meta" "paste" "todo" ];
+      type = with types; listOf (enum
+        [ "builds" "dispatch" "git" "hg" "hub" "lists" "man" "meta" "pages" "paste" "todo" ]);
+      defaultText = "locally enabled services";
       description = ''
-        Services to enable on the sourcehut network.
+        Services that may be displayed as links in the title bar of the Web interface.
       '';
     };
 
-    originBase = mkOption {
+    listenAddress = mkOption {
       type = types.str;
-      default = with config.networking; hostName + lib.optionalString (domain != null) ".${domain}";
-      defaultText = literalExpression ''
-        with config.networking; hostName + optionalString (domain != null) ".''${domain}"
-      '';
-      description = ''
-        Host name used by reverse-proxy and for default settings. Will host services at git."''${originBase}". For example: git.sr.ht
-      '';
-    };
-
-    address = mkOption {
-      type = types.str;
-      default = "127.0.0.1";
-      description = ''
-        Address to bind to.
-      '';
+      default = "localhost";
+      description = "Address to bind to.";
     };
 
     python = mkOption {
@@ -97,105 +138,1247 @@ in
       '';
     };
 
-    statePath = mkOption {
-      type = types.path;
-      default = "/var/lib/sourcehut";
-      description = ''
-        Root state path for the sourcehut network. If left as the default value
-        this directory will automatically be created before the sourcehut server
-        starts, otherwise the sysadmin is responsible for ensuring the
-        directory exists with appropriate ownership and permissions.
-      '';
+    minio = {
+      enable = mkEnableOption ''local minio integration'';
+    };
+
+    nginx = {
+      enable = mkEnableOption ''local nginx integration'';
+      virtualHost = mkOption {
+        type = types.attrs;
+        default = {};
+        description = "Virtual-host configuration merged with all Sourcehut's virtual-hosts.";
+      };
+    };
+
+    postfix = {
+      enable = mkEnableOption ''local postfix integration'';
+    };
+
+    postgresql = {
+      enable = mkEnableOption ''local postgresql integration'';
+    };
+
+    redis = {
+      enable = mkEnableOption ''local redis integration in a dedicated redis-server'';
     };
 
     settings = mkOption {
       type = lib.types.submodule {
         freeformType = settingsFormat.type;
+        options."sr.ht" = {
+          global-domain = mkOption {
+            description = "Global domain name.";
+            type = types.str;
+            example = "example.com";
+          };
+          environment = mkOption {
+            description = "Values other than \"production\" adds a banner to each page.";
+            type = types.enum [ "development" "production" ];
+            default = "development";
+          };
+          network-key = mkOption {
+            description = ''
+              An absolute file path (which should be outside the Nix-store)
+              to a secret key to encrypt internal messages with. Use <code>srht-keygen network</code> to
+              generate this key. It must be consistent between all services and nodes.
+            '';
+            type = types.path;
+            apply = s: "<" + toString s;
+          };
+          owner-email = mkOption {
+            description = "Owner's email.";
+            type = types.str;
+            default = "contact@example.com";
+          };
+          owner-name = mkOption {
+            description = "Owner's name.";
+            type = types.str;
+            default = "John Doe";
+          };
+          site-blurb = mkOption {
+            description = "Blurb for your site.";
+            type = types.str;
+            default = "the hacker's forge";
+          };
+          site-info = mkOption {
+            description = "The top-level info page for your site.";
+            type = types.str;
+            default = "https://sourcehut.org";
+          };
+          service-key = mkOption {
+            description = ''
+              An absolute file path (which should be outside the Nix-store)
+              to a key used for encrypting session cookies. Use <code>srht-keygen service</code> to
+              generate the service key. This must be shared between each node of the same
+              service (e.g. git1.sr.ht and git2.sr.ht), but different services may use
+              different keys. If you configure all of your services with the same
+              config.ini, you may use the same service-key for all of them.
+            '';
+            type = types.path;
+            apply = s: "<" + toString s;
+          };
+          site-name = mkOption {
+            description = "The name of your network of sr.ht-based sites.";
+            type = types.str;
+            default = "sourcehut";
+          };
+          source-url = mkOption {
+            description = "The source code for your fork of sr.ht.";
+            type = types.str;
+            default = "https://git.sr.ht/~sircmpwn/srht";
+          };
+        };
+        options.mail = {
+          smtp-host = mkOptionNullOrStr "Outgoing SMTP host.";
+          smtp-port = mkOption {
+            description = "Outgoing SMTP port.";
+            type = with types; nullOr port;
+            default = null;
+          };
+          smtp-user = mkOptionNullOrStr "Outgoing SMTP user.";
+          smtp-password = mkOptionNullOrStr "Outgoing SMTP password.";
+          smtp-from = mkOptionNullOrStr "Outgoing SMTP FROM.";
+          error-to = mkOptionNullOrStr "Address receiving application exceptions";
+          error-from = mkOptionNullOrStr "Address sending application exceptions";
+          pgp-privkey = mkOptionNullOrStr ''
+            An absolute file path (which should be outside the Nix-store)
+            to an OpenPGP private key.
+
+            Your PGP key information (DO NOT mix up pub and priv here)
+            You must remove the password from your secret key, if present.
+            You can do this with <code>gpg --edit-key [key-id]</code>,
+            then use the <code>passwd</code> command and do not enter a new password.
+          '';
+          pgp-pubkey = mkOptionNullOrStr "OpenPGP public key.";
+          pgp-key-id = mkOptionNullOrStr "OpenPGP key identifier.";
+        };
+        options.objects = {
+          s3-upstream = mkOption {
+            description = "Configure the S3-compatible object storage service.";
+            type = with types; nullOr str;
+            default = null;
+          };
+          s3-access-key = mkOption {
+            description = "Access key to the S3-compatible object storage service";
+            type = with types; nullOr str;
+            default = null;
+          };
+          s3-secret-key = mkOption {
+            description = ''
+              An absolute file path (which should be outside the Nix-store)
+              to the secret key of the S3-compatible object storage service.
+            '';
+            type = with types; nullOr path;
+            default = null;
+            apply = mapNullable (s: "<" + toString s);
+          };
+        };
+        options.webhooks = {
+          private-key = mkOption {
+            description = ''
+              An absolute file path (which should be outside the Nix-store)
+              to a base64-encoded Ed25519 key for signing webhook payloads.
+              This should be consistent for all *.sr.ht sites,
+              as this key will be used to verify signatures
+              from other sites in your network.
+              Use the <code>srht-keygen webhook</code> command to generate a key.
+            '';
+            type = types.path;
+            apply = s: "<" + toString s;
+          };
+        };
+
+        options."dispatch.sr.ht" = commonServiceSettings "dispatch" // {
+        };
+        options."dispatch.sr.ht::github" = {
+          oauth-client-id = mkOptionNullOrStr "OAuth client id.";
+          oauth-client-secret = mkOptionNullOrStr "OAuth client secret.";
+        };
+        options."dispatch.sr.ht::gitlab" = {
+          enabled = mkEnableOption "GitLab integration";
+          canonical-upstream = mkOption {
+            type = types.str;
+            description = "Canonical upstream.";
+            default = "gitlab.com";
+          };
+          repo-cache = mkOption {
+            type = types.str;
+            description = "Repository cache directory.";
+            default = "./repo-cache";
+          };
+          "gitlab.com" = mkOption {
+            type = with types; nullOr str;
+            description = "GitLab id and secret.";
+            default = null;
+            example = "GitLab:application id:secret";
+          };
+        };
+
+        options."builds.sr.ht" = commonServiceSettings "builds" // {
+          allow-free = mkEnableOption "nonpaying users to submit builds";
+          redis = mkOption {
+            description = "The Redis connection used for the Celery worker.";
+            type = types.str;
+            default = "redis+socket:///run/redis-sourcehut-buildsrht/redis.sock?virtual_host=2";
+          };
+          shell = mkOption {
+            description = ''
+              Scripts used to launch on SSH connection.
+              <literal>/usr/bin/master-shell</literal> on master,
+              <literal>/usr/bin/runner-shell</literal> on runner.
+              If master and worker are on the same system
+              set to <literal>/usr/bin/runner-shell</literal>.
+            '';
+            type = types.enum ["/usr/bin/master-shell" "/usr/bin/runner-shell"];
+            default = "/usr/bin/master-shell";
+          };
+        };
+        options."builds.sr.ht::worker" = {
+          bind-address = mkOption {
+            description = ''
+              HTTP bind address for serving local build information/monitoring.
+            '';
+            type = types.str;
+            default = "localhost:8080";
+          };
+          buildlogs = mkOption {
+            description = "Path to write build logs.";
+            type = types.str;
+            default = "/var/log/sourcehut/buildsrht-worker";
+          };
+          name = mkOption {
+            description = ''
+              Listening address and listening port
+              of the build runner (with HTTP port if not 80).
+            '';
+            type = types.str;
+            default = "localhost:5020";
+          };
+          timeout = mkOption {
+            description = ''
+              Max build duration.
+              See <link xlink:href="https://golang.org/pkg/time/#ParseDuration"/>.
+            '';
+            type = types.str;
+            default = "3m";
+          };
+        };
+
+        options."git.sr.ht" = commonServiceSettings "git" // {
+          outgoing-domain = mkOption {
+            description = "Outgoing domain.";
+            type = types.str;
+            default = "https://git.localhost.localdomain";
+          };
+          post-update-script = mkOption {
+            description = ''
+              A post-update script which is installed in every git repo.
+              This setting is propagated to newer and existing repositories.
+            '';
+            type = types.path;
+            default = "${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook";
+            defaultText = "\${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook";
+          };
+          repos = mkOption {
+            description = ''
+              Path to git repositories on disk.
+              If changing the default, you must ensure that
+              the gitsrht's user as read and write access to it.
+            '';
+            type = types.str;
+            default = "/var/lib/sourcehut/gitsrht/repos";
+          };
+          webhooks = mkOption {
+            description = "The Redis connection used for the webhooks worker.";
+            type = types.str;
+            default = "redis+socket:///run/redis-sourcehut-gitsrht/redis.sock?virtual_host=1";
+          };
+        };
+        options."git.sr.ht::api" = {
+          internal-ipnet = mkOption {
+            description = ''
+              Set of IP subnets which are permitted to utilize internal API
+              authentication. This should be limited to the subnets
+              from which your *.sr.ht services are running.
+              See <xref linkend="opt-services.sourcehut.listenAddress"/>.
+            '';
+            type = with types; listOf str;
+            default = [ "127.0.0.0/8" "::1/128" ];
+          };
+        };
+
+        options."hg.sr.ht" = commonServiceSettings "hg" // {
+          changegroup-script = mkOption {
+            description = ''
+              A changegroup script which is installed in every mercurial repo.
+              This setting is propagated to newer and existing repositories.
+            '';
+            type = types.str;
+            default = "${cfg.python}/bin/hgsrht-hook-changegroup";
+            defaultText = "\${cfg.python}/bin/hgsrht-hook-changegroup";
+          };
+          repos = mkOption {
+            description = ''
+              Path to mercurial repositories on disk.
+              If changing the default, you must ensure that
+              the hgsrht's user as read and write access to it.
+            '';
+            type = types.str;
+            default = "/var/lib/sourcehut/hgsrht/repos";
+          };
+          srhtext = mkOptionNullOrStr ''
+            Path to the srht mercurial extension
+            (defaults to where the hgsrht code is)
+          '';
+          clone_bundle_threshold = mkOption {
+            description = ".hg/store size (in MB) past which the nightly job generates clone bundles.";
+            type = types.ints.unsigned;
+            default = 50;
+          };
+          hg_ssh = mkOption {
+            description = "Path to hg-ssh (if not in $PATH).";
+            type = types.str;
+            default = "${pkgs.mercurial}/bin/hg-ssh";
+            defaultText = "\${pkgs.mercurial}/bin/hg-ssh";
+          };
+          webhooks = mkOption {
+            description = "The Redis connection used for the webhooks worker.";
+            type = types.str;
+            default = "redis+socket:///run/redis-sourcehut-hgsrht/redis.sock?virtual_host=1";
+          };
+        };
+
+        options."hub.sr.ht" = commonServiceSettings "hub" // {
+        };
+
+        options."lists.sr.ht" = commonServiceSettings "lists" // {
+          allow-new-lists = mkEnableOption "Allow creation of new lists.";
+          notify-from = mkOption {
+            description = "Outgoing email for notifications generated by users.";
+            type = types.str;
+            default = "lists-notify@localhost.localdomain";
+          };
+          posting-domain = mkOption {
+            description = "Posting domain.";
+            type = types.str;
+            default = "lists.localhost.localdomain";
+          };
+          redis = mkOption {
+            description = "The Redis connection used for the Celery worker.";
+            type = types.str;
+            default = "redis+socket:///run/redis-sourcehut-listssrht/redis.sock?virtual_host=2";
+          };
+          webhooks = mkOption {
+            description = "The Redis connection used for the webhooks worker.";
+            type = types.str;
+            default = "redis+socket:///run/redis-sourcehut-listssrht/redis.sock?virtual_host=1";
+          };
+        };
+        options."lists.sr.ht::worker" = {
+          reject-mimetypes = mkOption {
+            description = ''
+              Comma-delimited list of Content-Types to reject. Messages with Content-Types
+              included in this list are rejected. Multipart messages are always supported,
+              and each part is checked against this list.
+
+              Uses fnmatch for wildcard expansion.
+            '';
+            type = with types; listOf str;
+            default = ["text/html"];
+          };
+          reject-url = mkOption {
+            description = "Reject URL.";
+            type = types.str;
+            default = "https://man.sr.ht/lists.sr.ht/etiquette.md";
+          };
+          sock = mkOption {
+            description = ''
+              Path for the lmtp daemon's unix socket. Direct incoming mail to this socket.
+              Alternatively, specify IP:PORT and an SMTP server will be run instead.
+            '';
+            type = types.str;
+            default = "/tmp/lists.sr.ht-lmtp.sock";
+          };
+          sock-group = mkOption {
+            description = ''
+              The lmtp daemon will make the unix socket group-read/write
+              for users in this group.
+            '';
+            type = types.str;
+            default = "postfix";
+          };
+        };
+
+        options."man.sr.ht" = commonServiceSettings "man" // {
+        };
+
+        options."meta.sr.ht" =
+          removeAttrs (commonServiceSettings "meta")
+            ["oauth-client-id" "oauth-client-secret"] // {
+          api-origin = mkOption {
+            description = "Origin URL for API, 100 more than web.";
+            type = types.str;
+            default = "http://${cfg.listenAddress}:${toString (cfg.meta.port + 100)}";
+            defaultText = ''http://<xref linkend="opt-services.sourcehut.listenAddress"/>:''${toString (<xref linkend="opt-services.sourcehut.meta.port"/> + 100)}'';
+          };
+          webhooks = mkOption {
+            description = "The Redis connection used for the webhooks worker.";
+            type = types.str;
+            default = "redis+socket:///run/redis-sourcehut-metasrht/redis.sock?virtual_host=1";
+          };
+          welcome-emails = mkEnableOption "sending stock sourcehut welcome emails after signup";
+        };
+        options."meta.sr.ht::api" = {
+          internal-ipnet = mkOption {
+            description = ''
+              Set of IP subnets which are permitted to utilize internal API
+              authentication. This should be limited to the subnets
+              from which your *.sr.ht services are running.
+              See <xref linkend="opt-services.sourcehut.listenAddress"/>.
+            '';
+            type = with types; listOf str;
+            default = [ "127.0.0.0/8" "::1/128" ];
+          };
+        };
+        options."meta.sr.ht::aliases" = mkOption {
+          description = "Aliases for the client IDs of commonly used OAuth clients.";
+          type = with types; attrsOf int;
+          default = {};
+          example = { "git.sr.ht" = 12345; };
+        };
+        options."meta.sr.ht::billing" = {
+          enabled = mkEnableOption "the billing system";
+          stripe-public-key = mkOptionNullOrStr "Public key for Stripe. Get your keys at https://dashboard.stripe.com/account/apikeys";
+          stripe-secret-key = mkOptionNullOrStr ''
+            An absolute file path (which should be outside the Nix-store)
+            to a secret key for Stripe. Get your keys at https://dashboard.stripe.com/account/apikeys
+          '' // {
+            apply = mapNullable (s: "<" + toString s);
+          };
+        };
+        options."meta.sr.ht::settings" = {
+          registration = mkEnableOption "public registration";
+          onboarding-redirect = mkOption {
+            description = "Where to redirect new users upon registration.";
+            type = types.str;
+            default = "https://meta.localhost.localdomain";
+          };
+          user-invites = mkOption {
+            description = ''
+              How many invites each user is issued upon registration
+              (only applicable if open registration is disabled).
+            '';
+            type = types.ints.unsigned;
+            default = 5;
+          };
+        };
+
+        options."pages.sr.ht" = commonServiceSettings "pages" // {
+          gemini-certs = mkOption {
+            description = ''
+              An absolute file path (which should be outside the Nix-store)
+              to Gemini certificates.
+            '';
+            type = with types; nullOr path;
+            default = null;
+          };
+          max-site-size = mkOption {
+            description = "Maximum size of any given site (post-gunzip), in MiB.";
+            type = types.int;
+            default = 1024;
+          };
+          user-domain = mkOption {
+            description = ''
+              Configures the user domain, if enabled.
+              All users are given &lt;username&gt;.this.domain.
+            '';
+            type = with types; nullOr str;
+            default = null;
+          };
+        };
+        options."pages.sr.ht::api" = {
+          internal-ipnet = mkOption {
+            description = ''
+              Set of IP subnets which are permitted to utilize internal API
+              authentication. This should be limited to the subnets
+              from which your *.sr.ht services are running.
+              See <xref linkend="opt-services.sourcehut.listenAddress"/>.
+            '';
+            type = with types; listOf str;
+            default = [ "127.0.0.0/8" "::1/128" ];
+          };
+        };
+
+        options."paste.sr.ht" = commonServiceSettings "paste" // {
+        };
+
+        options."todo.sr.ht" = commonServiceSettings "todo" // {
+          notify-from = mkOption {
+            description = "Outgoing email for notifications generated by users.";
+            type = types.str;
+            default = "todo-notify@localhost.localdomain";
+          };
+          webhooks = mkOption {
+            description = "The Redis connection used for the webhooks worker.";
+            type = types.str;
+            default = "redis+socket:///run/redis-sourcehut-todosrht/redis.sock?virtual_host=1";
+          };
+        };
+        options."todo.sr.ht::mail" = {
+          posting-domain = mkOption {
+            description = "Posting domain.";
+            type = types.str;
+            default = "todo.localhost.localdomain";
+          };
+          sock = mkOption {
+            description = ''
+              Path for the lmtp daemon's unix socket. Direct incoming mail to this socket.
+              Alternatively, specify IP:PORT and an SMTP server will be run instead.
+            '';
+            type = types.str;
+            default = "/tmp/todo.sr.ht-lmtp.sock";
+          };
+          sock-group = mkOption {
+            description = ''
+              The lmtp daemon will make the unix socket group-read/write
+              for users in this group.
+            '';
+            type = types.str;
+            default = "postfix";
+          };
+        };
       };
       default = { };
       description = ''
         The configuration for the sourcehut network.
       '';
     };
+
+    builds = {
+      enableWorker = mkEnableOption ''
+        worker for builds.sr.ht
+
+        <warning><para>
+        For smaller deployments, job runners can be installed alongside the master server
+        but even if you only build your own software, integration with other services
+        may cause you to run untrusted builds
+        (e.g. automatic testing of patches via listssrht).
+        See <link xlink:href="https://man.sr.ht/builds.sr.ht/configuration.md#security-model"/>.
+        </para></warning>
+      '';
+
+      images = mkOption {
+        type = with types; attrsOf (attrsOf (attrsOf package));
+        default = { };
+        example = lib.literalExpression ''(let
+            # Pinning unstable to allow usage with flakes and limit rebuilds.
+            pkgs_unstable = builtins.fetchGit {
+                url = "https://github.com/NixOS/nixpkgs";
+                rev = "ff96a0fa5635770390b184ae74debea75c3fd534";
+                ref = "nixos-unstable";
+            };
+            image_from_nixpkgs = (import ("${pkgs.sourcehut.buildsrht}/lib/images/nixos/image.nix") {
+              pkgs = (import pkgs_unstable {});
+            });
+          in
+          {
+            nixos.unstable.x86_64 = image_from_nixpkgs;
+          }
+        )'';
+        description = ''
+          Images for builds.sr.ht. Each package should be distro.release.arch and point to a /nix/store/package/root.img.qcow2.
+        '';
+      };
+    };
+
+    git = {
+      package = mkOption {
+        type = types.package;
+        default = pkgs.git;
+        example = literalExpression "pkgs.gitFull";
+        description = ''
+          Git package for git.sr.ht. This can help silence collisions.
+        '';
+      };
+      fcgiwrap.preforkProcess = mkOption {
+        description = "Number of fcgiwrap processes to prefork.";
+        type = types.int;
+        default = 4;
+      };
+    };
+
+    hg = {
+      package = mkOption {
+        type = types.package;
+        default = pkgs.mercurial;
+        description = ''
+          Mercurial package for hg.sr.ht. This can help silence collisions.
+        '';
+      };
+      cloneBundles = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Generate clonebundles (which require more disk space but dramatically speed up cloning large repositories).
+        '';
+      };
+    };
+
+    lists = {
+      process = {
+        extraArgs = mkOption {
+          type = with types; listOf str;
+          default = [ "--loglevel DEBUG" "--pool eventlet" "--without-heartbeat" ];
+          description = "Extra arguments passed to the Celery responsible for processing mails.";
+        };
+        celeryConfig = mkOption {
+          type = types.lines;
+          default = "";
+          description = "Content of the <literal>celeryconfig.py</literal> used by the Celery of <literal>listssrht-process</literal>.";
+        };
+      };
+    };
   };
 
-  config = mkIf cfg.enable {
-    assertions =
-      [
+  config = mkIf cfg.enable (mkMerge [
+    {
+      environment.systemPackages = [ pkgs.sourcehut.coresrht ];
+
+      services.sourcehut.settings = {
+        "git.sr.ht".outgoing-domain = mkDefault "https://git.${domain}";
+        "lists.sr.ht".notify-from = mkDefault "lists-notify@${domain}";
+        "lists.sr.ht".posting-domain = mkDefault "lists.${domain}";
+        "meta.sr.ht::settings".onboarding-redirect = mkDefault "https://meta.${domain}";
+        "todo.sr.ht".notify-from = mkDefault "todo-notify@${domain}";
+        "todo.sr.ht::mail".posting-domain = mkDefault "todo.${domain}";
+      };
+    }
+    (mkIf cfg.postgresql.enable {
+      assertions = [
+        { assertion = postgresql.enable;
+          message = "postgresql must be enabled and configured";
+        }
+      ];
+    })
+    (mkIf cfg.postfix.enable {
+      assertions = [
+        { assertion = postfix.enable;
+          message = "postfix must be enabled and configured";
+        }
+      ];
+      # Needed for sharing the LMTP sockets with JoinsNamespaceOf=
+      systemd.services.postfix.serviceConfig.PrivateTmp = true;
+    })
+    (mkIf cfg.redis.enable {
+      services.redis.vmOverCommit = mkDefault true;
+    })
+    (mkIf cfg.nginx.enable {
+      assertions = [
+        { assertion = nginx.enable;
+          message = "nginx must be enabled and configured";
+        }
+      ];
+      # For proxyPass= in virtual-hosts for Sourcehut services.
+      services.nginx.recommendedProxySettings = mkDefault true;
+    })
+    (mkIf (cfg.builds.enable || cfg.git.enable || cfg.hg.enable) {
+      services.openssh = {
+        # Note that sshd will continue to honor AuthorizedKeysFile.
+        # Note that you may want automatically rotate
+        # or link to /dev/null the following log files:
+        # - /var/log/gitsrht-dispatch
+        # - /var/log/{build,git,hg}srht-keys
+        # - /var/log/{git,hg}srht-shell
+        # - /var/log/gitsrht-update-hook
+        authorizedKeysCommand = ''/etc/ssh/sourcehut/subdir/srht-dispatch "%u" "%h" "%t" "%k"'';
+        # srht-dispatch will setuid/setgid according to [git.sr.ht::dispatch]
+        authorizedKeysCommandUser = "root";
+        extraConfig = ''
+          PermitUserEnvironment SRHT_*
+        '';
+      };
+      environment.etc."ssh/sourcehut/config.ini".source =
+        settingsFormat.generate "sourcehut-dispatch-config.ini"
+          (filterAttrs (k: v: k == "git.sr.ht::dispatch")
+          cfg.settings);
+      environment.etc."ssh/sourcehut/subdir/srht-dispatch" = {
+        # sshd_config(5): The program must be owned by root, not writable by group or others
+        mode = "0755";
+        source = pkgs.writeShellScript "srht-dispatch" ''
+          set -e
+          cd /etc/ssh/sourcehut/subdir
+          ${cfg.python}/bin/gitsrht-dispatch "$@"
+        '';
+      };
+      systemd.services.sshd = {
+        #path = optional cfg.git.enable [ cfg.git.package ];
+        serviceConfig = {
+          BindReadOnlyPaths =
+            # Note that those /usr/bin/* paths are hardcoded in multiple places in *.sr.ht,
+            # for instance to get the user from the [git.sr.ht::dispatch] settings.
+            # *srht-keys needs to:
+            # - access a redis-server in [sr.ht] redis-host,
+            # - access the PostgreSQL server in [*.sr.ht] connection-string,
+            # - query metasrht-api (through the HTTP API).
+            # Using this has the side effect of creating empty files in /usr/bin/
+            optionals cfg.builds.enable [
+              "${pkgs.writeShellScript "buildsrht-keys-wrapper" ''
+                set -e
+                cd /run/sourcehut/buildsrht/subdir
+                set -x
+                exec -a "$0" ${pkgs.sourcehut.buildsrht}/bin/buildsrht-keys "$@"
+              ''}:/usr/bin/buildsrht-keys"
+              "${pkgs.sourcehut.buildsrht}/bin/master-shell:/usr/bin/master-shell"
+              "${pkgs.sourcehut.buildsrht}/bin/runner-shell:/usr/bin/runner-shell"
+            ] ++
+            optionals cfg.git.enable [
+              # /path/to/gitsrht-keys calls /path/to/gitsrht-shell,
+              # or [git.sr.ht] shell= if set.
+              "${pkgs.writeShellScript "gitsrht-keys-wrapper" ''
+                set -e
+                cd /run/sourcehut/gitsrht/subdir
+                set -x
+                exec -a "$0" ${pkgs.sourcehut.gitsrht}/bin/gitsrht-keys "$@"
+              ''}:/usr/bin/gitsrht-keys"
+              "${pkgs.writeShellScript "gitsrht-shell-wrapper" ''
+                set -e
+                cd /run/sourcehut/gitsrht/subdir
+                set -x
+                exec -a "$0" ${pkgs.sourcehut.gitsrht}/bin/gitsrht-shell "$@"
+              ''}:/usr/bin/gitsrht-shell"
+              "${pkgs.writeShellScript "gitsrht-update-hook" ''
+                set -e
+                test -e "''${PWD%/*}"/config.ini ||
+                # Git hooks are run relative to their repository's directory,
+                # but gitsrht-update-hook looks up ../config.ini
+                ln -s /run/sourcehut/gitsrht/config.ini "''${PWD%/*}"/config.ini
+                # hooks/post-update calls /usr/bin/gitsrht-update-hook as hooks/stage-3
+                # but this wrapper being a bash script, it overrides $0 with /usr/bin/gitsrht-update-hook
+                # hence this hack to put hooks/stage-3 back into gitsrht-update-hook's $0
+                if test "''${STAGE3:+set}"
+                then
+                  set -x
+                  exec -a hooks/stage-3 ${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook "$@"
+                else
+                  export STAGE3=set
+                  set -x
+                  exec -a "$0" ${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook "$@"
+                fi
+              ''}:/usr/bin/gitsrht-update-hook"
+            ] ++
+            optionals cfg.hg.enable [
+              # /path/to/hgsrht-keys calls /path/to/hgsrht-shell,
+              # or [hg.sr.ht] shell= if set.
+              "${pkgs.writeShellScript "hgsrht-keys-wrapper" ''
+                set -e
+                cd /run/sourcehut/hgsrht/subdir
+                set -x
+                exec -a "$0" ${pkgs.sourcehut.hgsrht}/bin/hgsrht-keys "$@"
+              ''}:/usr/bin/hgsrht-keys"
+              "${pkgs.writeShellScript "hgsrht-shell-wrapper" ''
+                set -e
+                cd /run/sourcehut/hgsrht/subdir
+                set -x
+                exec -a "$0" ${pkgs.sourcehut.hgsrht}/bin/hgsrht-shell "$@"
+              ''}:/usr/bin/hgsrht-shell"
+              # Mercurial's changegroup hooks are run relative to their repository's directory,
+              # but hgsrht-hook-changegroup looks up ./config.ini
+              "${pkgs.writeShellScript "hgsrht-hook-changegroup" ''
+                set -e
+                test -e "''$PWD"/config.ini ||
+                ln -s /run/sourcehut/hgsrht/config.ini "''$PWD"/config.ini
+                set -x
+                exec -a "$0" ${cfg.python}/bin/hgsrht-hook-changegroup "$@"
+              ''}:/usr/bin/hgsrht-hook-changegroup"
+            ];
+        };
+      };
+    })
+  ]);
+
+  imports = [
+
+    (import ./service.nix "builds" {
+      inherit configIniOfService;
+      srvsrht = "buildsrht";
+      port = 5002;
+      # TODO: a celery worker on the master and worker are apparently needed
+      extraServices.buildsrht-worker = let
+        qemuPackage = pkgs.qemu_kvm;
+        serviceName = "buildsrht-worker";
+        statePath = "/var/lib/sourcehut/${serviceName}";
+        in mkIf cfg.builds.enableWorker {
+        path = [ pkgs.openssh pkgs.docker ];
+        preStart = ''
+          set -x
+          if test -z "$(docker images -q qemu:latest 2>/dev/null)" \
+          || test "$(cat ${statePath}/docker-image-qemu)" != "${qemuPackage.version}"
+          then
+            # Create and import qemu:latest image for docker
+            ${pkgs.dockerTools.streamLayeredImage {
+              name = "qemu";
+              tag = "latest";
+              contents = [ qemuPackage ];
+            }} | docker load
+            # Mark down current package version
+            echo '${qemuPackage.version}' >${statePath}/docker-image-qemu
+          fi
+        '';
+        serviceConfig = {
+          ExecStart = "${pkgs.sourcehut.buildsrht}/bin/builds.sr.ht-worker";
+          BindPaths = [ cfg.settings."builds.sr.ht::worker".buildlogs ];
+          LogsDirectory = [ "sourcehut/${serviceName}" ];
+          RuntimeDirectory = [ "sourcehut/${serviceName}/subdir" ];
+          StateDirectory = [ "sourcehut/${serviceName}" ];
+          TimeoutStartSec = "1800s";
+          # builds.sr.ht-worker looks up ../config.ini
+          WorkingDirectory = "-"+"/run/sourcehut/${serviceName}/subdir";
+        };
+      };
+      extraConfig = let
+        image_dirs = flatten (
+          mapAttrsToList (distro: revs:
+            mapAttrsToList (rev: archs:
+              mapAttrsToList (arch: image:
+                pkgs.runCommand "buildsrht-images" { } ''
+                  mkdir -p $out/${distro}/${rev}/${arch}
+                  ln -s ${image}/*.qcow2 $out/${distro}/${rev}/${arch}/root.img.qcow2
+                ''
+              ) archs
+            ) revs
+          ) cfg.builds.images
+        );
+        image_dir_pre = pkgs.symlinkJoin {
+          name = "builds.sr.ht-worker-images-pre";
+          paths = image_dirs;
+            # FIXME: not working, apparently because ubuntu/latest is a broken link
+            # ++ [ "${pkgs.sourcehut.buildsrht}/lib/images" ];
+        };
+        image_dir = pkgs.runCommand "builds.sr.ht-worker-images" { } ''
+          mkdir -p $out/images
+          cp -Lr ${image_dir_pre}/* $out/images
+        '';
+        in mkMerge [
         {
-          assertion = with cfgIni.webhooks; private-key != null && stringLength private-key == 44;
-          message = "The webhook's private key must be defined and of a 44 byte length.";
+          users.users.${cfg.builds.user}.shell = pkgs.bash;
+
+          virtualisation.docker.enable = true;
+
+          services.sourcehut.settings = mkMerge [
+            { # Note that git.sr.ht::dispatch is not a typo,
+              # gitsrht-dispatch always use this section
+              "git.sr.ht::dispatch"."/usr/bin/buildsrht-keys" =
+                mkDefault "${cfg.builds.user}:${cfg.builds.group}";
+            }
+            (mkIf cfg.builds.enableWorker {
+              "builds.sr.ht::worker".shell = "/usr/bin/runner-shell";
+              "builds.sr.ht::worker".images = mkDefault "${image_dir}/images";
+              "builds.sr.ht::worker".controlcmd = mkDefault "${image_dir}/images/control";
+            })
+          ];
         }
+        (mkIf cfg.builds.enableWorker {
+          users.groups = {
+            docker.members = [ cfg.builds.user ];
+          };
+        })
+        (mkIf (cfg.builds.enableWorker && cfg.nginx.enable) {
+          # Allow nginx access to buildlogs
+          users.users.${nginx.user}.extraGroups = [ cfg.builds.group ];
+          systemd.services.nginx = {
+            serviceConfig.BindReadOnlyPaths = [ cfg.settings."builds.sr.ht::worker".buildlogs ];
+          };
+          services.nginx.virtualHosts."logs.${domain}" = mkMerge [ {
+            /* FIXME: is a listen needed?
+            listen = with builtins;
+              # FIXME: not compatible with IPv6
+              let address = split ":" cfg.settings."builds.sr.ht::worker".name; in
+              [{ addr = elemAt address 0; port = lib.toInt (elemAt address 2); }];
+            */
+            locations."/logs/".alias = cfg.settings."builds.sr.ht::worker".buildlogs + "/";
+          } cfg.nginx.virtualHost ];
+        })
+      ];
+    })
 
+    (import ./service.nix "dispatch" {
+      inherit configIniOfService;
+      port = 5005;
+    })
+
+    (import ./service.nix "git" (let
+      baseService = {
+        path = [ cfg.git.package ];
+        serviceConfig.BindPaths = [ "${cfg.settings."git.sr.ht".repos}:/var/lib/sourcehut/gitsrht/repos" ];
+      };
+      in {
+      inherit configIniOfService;
+      mainService = mkMerge [ baseService {
+        serviceConfig.StateDirectory = [ "sourcehut/gitsrht" "sourcehut/gitsrht/repos" ];
+        preStart = mkIf (!versionAtLeast config.system.stateVersion "22.05") (mkBefore ''
+          # Fix Git hooks of repositories pre-dating https://github.com/NixOS/nixpkgs/pull/133984
+          (
+          set +f
+          shopt -s nullglob
+          for h in /var/lib/sourcehut/gitsrht/repos/~*/*/hooks/{pre-receive,update,post-update}
+          do ln -fnsv /usr/bin/gitsrht-update-hook "$h"; done
+          )
+        '');
+      } ];
+      port = 5001;
+      webhooks = true;
+      extraTimers.gitsrht-periodic = {
+        service = baseService;
+        timerConfig.OnCalendar = ["*:0/20"];
+      };
+      extraConfig = mkMerge [
         {
-          assertion = hasAttrByPath [ "meta.sr.ht" "origin" ] cfgIni && cfgIni."meta.sr.ht".origin != null;
-          message = "meta.sr.ht's origin must be defined.";
+          # https://stackoverflow.com/questions/22314298/git-push-results-in-fatal-protocol-error-bad-line-length-character-this
+          # Probably could use gitsrht-shell if output is restricted to just parameters...
+          users.users.${cfg.git.user}.shell = pkgs.bash;
+          services.sourcehut.settings = {
+            "git.sr.ht::dispatch"."/usr/bin/gitsrht-keys" =
+              mkDefault "${cfg.git.user}:${cfg.git.group}";
+          };
+          systemd.services.sshd = baseService;
         }
+        (mkIf cfg.nginx.enable {
+          services.nginx.virtualHosts."git.${domain}" = {
+            locations."/authorize" = {
+              proxyPass = "http://${cfg.listenAddress}:${toString cfg.git.port}";
+              extraConfig = ''
+                proxy_pass_request_body off;
+                proxy_set_header Content-Length "";
+                proxy_set_header X-Original-URI $request_uri;
+              '';
+            };
+            locations."~ ^/([^/]+)/([^/]+)/(HEAD|info/refs|objects/info/.*|git-upload-pack).*$" = {
+              root = "/var/lib/sourcehut/gitsrht/repos";
+              fastcgiParams = {
+                GIT_HTTP_EXPORT_ALL = "";
+                GIT_PROJECT_ROOT = "$document_root";
+                PATH_INFO = "$uri";
+                SCRIPT_FILENAME = "${cfg.git.package}/bin/git-http-backend";
+              };
+              extraConfig = ''
+                auth_request /authorize;
+                fastcgi_read_timeout 500s;
+                fastcgi_pass unix:/run/gitsrht-fcgiwrap.sock;
+                gzip off;
+              '';
+            };
+          };
+          systemd.sockets.gitsrht-fcgiwrap = {
+            before = [ "nginx.service" ];
+            wantedBy = [ "sockets.target" "gitsrht.service" ];
+            # This path remains accessible to nginx.service, which has no RootDirectory=
+            socketConfig.ListenStream = "/run/gitsrht-fcgiwrap.sock";
+            socketConfig.SocketUser = nginx.user;
+            socketConfig.SocketMode = "600";
+          };
+        })
       ];
+      extraServices.gitsrht-fcgiwrap = mkIf cfg.nginx.enable {
+        serviceConfig = {
+          # Socket is passed by gitsrht-fcgiwrap.socket
+          ExecStart = "${pkgs.fcgiwrap}/sbin/fcgiwrap -c ${toString cfg.git.fcgiwrap.preforkProcess}";
+          # No need for config.ini
+          ExecStartPre = mkForce [];
+          User = null;
+          DynamicUser = true;
+          BindReadOnlyPaths = [ "${cfg.settings."git.sr.ht".repos}:/var/lib/sourcehut/gitsrht/repos" ];
+          IPAddressDeny = "any";
+          InaccessiblePaths = [ "-+/run/postgresql" "-+/run/redis-sourcehut" ];
+          PrivateNetwork = true;
+          RestrictAddressFamilies = mkForce [ "none" ];
+          SystemCallFilter = mkForce [
+            "@system-service"
+            "~@aio" "~@keyring" "~@memlock" "~@privileged" "~@resources" "~@setuid"
+            # @timer is needed for alarm()
+          ];
+        };
+      };
+    }))
+
+    (import ./service.nix "hg" (let
+      baseService = {
+        path = [ cfg.hg.package ];
+        serviceConfig.BindPaths = [ "${cfg.settings."hg.sr.ht".repos}:/var/lib/sourcehut/hgsrht/repos" ];
+      };
+      in {
+      inherit configIniOfService;
+      mainService = mkMerge [ baseService {
+        serviceConfig.StateDirectory = [ "sourcehut/hgsrht" "sourcehut/hgsrht/repos" ];
+      } ];
+      port = 5010;
+      webhooks = true;
+      extraTimers.hgsrht-periodic = {
+        service = baseService;
+        timerConfig.OnCalendar = ["*:0/20"];
+      };
+      extraTimers.hgsrht-clonebundles = mkIf cfg.hg.cloneBundles {
+        service = baseService;
+        timerConfig.OnCalendar = ["daily"];
+        timerConfig.AccuracySec = "1h";
+      };
+      extraConfig = mkMerge [
+        {
+          users.users.${cfg.hg.user}.shell = pkgs.bash;
+          services.sourcehut.settings = {
+            # Note that git.sr.ht::dispatch is not a typo,
+            # gitsrht-dispatch always uses this section.
+            "git.sr.ht::dispatch"."/usr/bin/hgsrht-keys" =
+              mkDefault "${cfg.hg.user}:${cfg.hg.group}";
+          };
+          systemd.services.sshd = baseService;
+        }
+        (mkIf cfg.nginx.enable {
+          # Allow nginx access to repositories
+          users.users.${nginx.user}.extraGroups = [ cfg.hg.group ];
+          services.nginx.virtualHosts."hg.${domain}" = {
+            locations."/authorize" = {
+              proxyPass = "http://${cfg.listenAddress}:${toString cfg.hg.port}";
+              extraConfig = ''
+                proxy_pass_request_body off;
+                proxy_set_header Content-Length "";
+                proxy_set_header X-Original-URI $request_uri;
+              '';
+            };
+            # Let clients reach pull bundles. We don't really need to lock this down even for
+            # private repos because the bundles are named after the revision hashes...
+            # so someone would need to know or guess a SHA value to download anything.
+            # TODO: proxyPass to an hg serve service?
+            locations."~ ^/[~^][a-z0-9_]+/[a-zA-Z0-9_.-]+/\\.hg/bundles/.*$" = {
+              root = "/var/lib/nginx/hgsrht/repos";
+              extraConfig = ''
+                auth_request /authorize;
+                gzip off;
+              '';
+            };
+          };
+          systemd.services.nginx = {
+            serviceConfig.BindReadOnlyPaths = [ "${cfg.settings."hg.sr.ht".repos}:/var/lib/nginx/hgsrht/repos" ];
+          };
+        })
+      ];
+    }))
+
+    (import ./service.nix "hub" {
+      inherit configIniOfService;
+      port = 5014;
+      extraConfig = {
+        services.nginx = mkIf cfg.nginx.enable {
+          virtualHosts."hub.${domain}" = mkMerge [ {
+            serverAliases = [ domain ];
+          } cfg.nginx.virtualHost ];
+        };
+      };
+    })
+
+    (import ./service.nix "lists" (let
+      srvsrht = "listssrht";
+      in {
+      inherit configIniOfService;
+      port = 5006;
+      webhooks = true;
+      # Receive the mail from Postfix and enqueue them into Redis and PostgreSQL
+      extraServices.listssrht-lmtp = {
+        wants = [ "postfix.service" ];
+        unitConfig.JoinsNamespaceOf = optional cfg.postfix.enable "postfix.service";
+        serviceConfig.ExecStart = "${cfg.python}/bin/listssrht-lmtp";
+        # Avoid crashing: os.chown(sock, os.getuid(), sock_gid)
+        serviceConfig.PrivateUsers = mkForce false;
+      };
+      # Dequeue the mails from Redis and dispatch them
+      extraServices.listssrht-process = {
+        serviceConfig = {
+          preStart = ''
+            cp ${pkgs.writeText "${srvsrht}-webhooks-celeryconfig.py" cfg.lists.process.celeryConfig} \
+               /run/sourcehut/${srvsrht}-webhooks/celeryconfig.py
+          '';
+          ExecStart = "${cfg.python}/bin/celery --app listssrht.process worker --hostname listssrht-process@%%h " + concatStringsSep " " cfg.lists.process.extraArgs;
+          # Avoid crashing: os.getloadavg()
+          ProcSubset = mkForce "all";
+        };
+      };
+      extraConfig = mkIf cfg.postfix.enable {
+        users.groups.${postfix.group}.members = [ cfg.lists.user ];
+        services.sourcehut.settings."lists.sr.ht::mail".sock-group = postfix.group;
+        services.postfix = {
+          destination = [ "lists.${domain}" ];
+          # FIXME: an accurate recipient list should be queried
+          # from the lists.sr.ht PostgreSQL database to avoid backscattering.
+          # But usernames are unfortunately not in that database but in meta.sr.ht.
+          # Note that two syntaxes are allowed:
+          # - ~username/list-name@lists.${domain}
+          # - u.username.list-name@lists.${domain}
+          localRecipients = [ "@lists.${domain}" ];
+          transport = ''
+            lists.${domain} lmtp:unix:${cfg.settings."lists.sr.ht::worker".sock}
+          '';
+        };
+      };
+    }))
+
+    (import ./service.nix "man" {
+      inherit configIniOfService;
+      port = 5004;
+    })
+
+    (import ./service.nix "meta" {
+      inherit configIniOfService;
+      port = 5000;
+      webhooks = true;
+      extraServices.metasrht-api = {
+        serviceConfig.Restart = "always";
+        serviceConfig.RestartSec = "2s";
+        preStart = "set -x\n" + concatStringsSep "\n\n" (attrValues (mapAttrs (k: s:
+          let srvMatch = builtins.match "^([a-z]*)\\.sr\\.ht$" k;
+              srv = head srvMatch;
+          in
+          # Configure client(s) as "preauthorized"
+          optionalString (srvMatch != null && cfg.${srv}.enable && ((s.oauth-client-id or null) != null)) ''
+            # Configure ${srv}'s OAuth client as "preauthorized"
+            ${postgresql.package}/bin/psql '${cfg.settings."meta.sr.ht".connection-string}' \
+              -c "UPDATE oauthclient SET preauthorized = true WHERE client_id = '${s.oauth-client-id}'"
+          ''
+          ) cfg.settings));
+        serviceConfig.ExecStart = "${pkgs.sourcehut.metasrht}/bin/metasrht-api -b ${cfg.listenAddress}:${toString (cfg.meta.port + 100)}";
+      };
+      extraTimers.metasrht-daily.timerConfig = {
+        OnCalendar = ["daily"];
+        AccuracySec = "1h";
+      };
+      extraConfig = mkMerge [
+        {
+          assertions = [
+            { assertion = let s = cfg.settings."meta.sr.ht::billing"; in
+                          s.enabled == "yes" -> (s.stripe-public-key != null && s.stripe-secret-key != null);
+              message = "If meta.sr.ht::billing is enabled, the keys must be defined.";
+            }
+          ];
+          environment.systemPackages = optional cfg.meta.enable
+            (pkgs.writeShellScriptBin "metasrht-manageuser" ''
+              set -eux
+              if test "$(${pkgs.coreutils}/bin/id -n -u)" != '${cfg.meta.user}'
+              then exec sudo -u '${cfg.meta.user}' "$0" "$@"
+              else
+                # In order to load config.ini
+                if cd /run/sourcehut/metasrht
+                then exec ${cfg.python}/bin/metasrht-manageuser "$@"
+                else cat <<EOF
+                  Please run: sudo systemctl start metasrht
+              EOF
+                  exit 1
+                fi
+              fi
+            '');
+        }
+        (mkIf cfg.nginx.enable {
+          services.nginx.virtualHosts."meta.${domain}" = {
+            locations."/query" = {
+              proxyPass = cfg.settings."meta.sr.ht".api-origin;
+              extraConfig = ''
+                if ($request_method = 'OPTIONS') {
+                  add_header 'Access-Control-Allow-Origin' '*';
+                  add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
+                  add_header 'Access-Control-Allow-Headers' 'User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
+                  add_header 'Access-Control-Max-Age' 1728000;
+                  add_header 'Content-Type' 'text/plain; charset=utf-8';
+                  add_header 'Content-Length' 0;
+                  return 204;
+                }
+
+                add_header 'Access-Control-Allow-Origin' '*';
+                add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
+                add_header 'Access-Control-Allow-Headers' 'User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
+                add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
+              '';
+            };
+          };
+        })
+      ];
+    })
+
+    (import ./service.nix "pages" {
+      inherit configIniOfService;
+      port = 5112;
+      mainService = let
+        srvsrht = "pagessrht";
+        version = pkgs.sourcehut.${srvsrht}.version;
+        stateDir = "/var/lib/sourcehut/${srvsrht}";
+        iniKey = "pages.sr.ht";
+        in {
+        preStart = mkBefore ''
+          set -x
+          # Use the /run/sourcehut/${srvsrht}/config.ini
+          # installed by a previous ExecStartPre= in baseService
+          cd /run/sourcehut/${srvsrht}
+
+          if test ! -e ${stateDir}/db; then
+            ${postgresql.package}/bin/psql '${cfg.settings.${iniKey}.connection-string}' -f ${pkgs.sourcehut.pagessrht}/share/sql/schema.sql
+            echo ${version} >${stateDir}/db
+          fi
+
+          ${optionalString cfg.settings.${iniKey}.migrate-on-upgrade ''
+            # Just try all the migrations because they're not linked to the version
+            for sql in ${pkgs.sourcehut.pagessrht}/share/sql/migrations/*.sql; do
+              ${postgresql.package}/bin/psql '${cfg.settings.${iniKey}.connection-string}' -f "$sql" || true
+            done
+          ''}
+
+          # Disable webhook
+          touch ${stateDir}/webhook
+        '';
+        serviceConfig = {
+          ExecStart = mkForce "${pkgs.sourcehut.pagessrht}/bin/pages.sr.ht -b ${cfg.listenAddress}:${toString cfg.pages.port}";
+        };
+      };
+    })
+
+    (import ./service.nix "paste" {
+      inherit configIniOfService;
+      port = 5011;
+    })
+
+    (import ./service.nix "todo" {
+      inherit configIniOfService;
+      port = 5003;
+      webhooks = true;
+      extraServices.todosrht-lmtp = {
+        wants = [ "postfix.service" ];
+        unitConfig.JoinsNamespaceOf = optional cfg.postfix.enable "postfix.service";
+        serviceConfig.ExecStart = "${cfg.python}/bin/todosrht-lmtp";
+        # Avoid crashing: os.chown(sock, os.getuid(), sock_gid)
+        serviceConfig.PrivateUsers = mkForce false;
+      };
+      extraConfig = mkIf cfg.postfix.enable {
+        users.groups.${postfix.group}.members = [ cfg.todo.user ];
+        services.sourcehut.settings."todo.sr.ht::mail".sock-group = postfix.group;
+        services.postfix = {
+          destination = [ "todo.${domain}" ];
+          # FIXME: an accurate recipient list should be queried
+          # from the todo.sr.ht PostgreSQL database to avoid backscattering.
+          # But usernames are unfortunately not in that database but in meta.sr.ht.
+          # Note that two syntaxes are allowed:
+          # - ~username/tracker-name@todo.${domain}
+          # - u.username.tracker-name@todo.${domain}
+          localRecipients = [ "@todo.${domain}" ];
+          transport = ''
+            todo.${domain} lmtp:unix:${cfg.settings."todo.sr.ht::mail".sock}
+          '';
+        };
+      };
+    })
+
+    (mkRenamedOptionModule [ "services" "sourcehut" "originBase" ]
+                           [ "services" "sourcehut" "settings" "sr.ht" "global-domain" ])
+    (mkRenamedOptionModule [ "services" "sourcehut" "address" ]
+                           [ "services" "sourcehut" "listenAddress" ])
+
+  ];
 
-    virtualisation.docker.enable = true;
-    environment.etc."sr.ht/config.ini".source =
-      settingsFormat.generate "sourcehut-config.ini" (mapAttrsRecursive
-        (
-          path: v: if v == null then "" else v
-        )
-        cfg.settings);
-
-    environment.systemPackages = [ pkgs.sourcehut.coresrht ];
-
-    # PostgreSQL server
-    services.postgresql.enable = mkOverride 999 true;
-    # Mail server
-    services.postfix.enable = mkOverride 999 true;
-    # Cron daemon
-    services.cron.enable = mkOverride 999 true;
-    # Redis server
-    services.redis.enable = mkOverride 999 true;
-    services.redis.bind = mkOverride 999 "127.0.0.1";
-
-    services.sourcehut.settings = {
-      # The name of your network of sr.ht-based sites
-      "sr.ht".site-name = mkDefault "sourcehut";
-      # The top-level info page for your site
-      "sr.ht".site-info = mkDefault "https://sourcehut.org";
-      # {{ site-name }}, {{ site-blurb }}
-      "sr.ht".site-blurb = mkDefault "the hacker's forge";
-      # If this != production, we add a banner to each page
-      "sr.ht".environment = mkDefault "development";
-      # Contact information for the site owners
-      "sr.ht".owner-name = mkDefault "Drew DeVault";
-      "sr.ht".owner-email = mkDefault "sir@cmpwn.com";
-      # The source code for your fork of sr.ht
-      "sr.ht".source-url = mkDefault "https://git.sr.ht/~sircmpwn/srht";
-      # A secret key to encrypt session cookies with
-      "sr.ht".secret-key = mkDefault null;
-      "sr.ht".global-domain = mkDefault null;
-
-      # Outgoing SMTP settings
-      mail.smtp-host = mkDefault null;
-      mail.smtp-port = mkDefault null;
-      mail.smtp-user = mkDefault null;
-      mail.smtp-password = mkDefault null;
-      mail.smtp-from = mkDefault null;
-      # Application exceptions are emailed to this address
-      mail.error-to = mkDefault null;
-      mail.error-from = mkDefault null;
-      # Your PGP key information (DO NOT mix up pub and priv here)
-      # You must remove the password from your secret key, if present.
-      # You can do this with gpg --edit-key [key-id], then use the passwd
-      # command and do not enter a new password.
-      mail.pgp-privkey = mkDefault null;
-      mail.pgp-pubkey = mkDefault null;
-      mail.pgp-key-id = mkDefault null;
-
-      # base64-encoded Ed25519 key for signing webhook payloads. This should be
-      # consistent for all *.sr.ht sites, as we'll use this key to verify signatures
-      # from other sites in your network.
-      #
-      # Use the srht-webhook-keygen command to generate a key.
-      webhooks.private-key = mkDefault null;
-    };
-  };
   meta.doc = ./sourcehut.xml;
-  meta.maintainers = with maintainers; [ tomberek ];
+  meta.maintainers = with maintainers; [ julm tomberek ];
 }
diff --git a/nixos/modules/services/misc/sourcehut/dispatch.nix b/nixos/modules/services/misc/sourcehut/dispatch.nix
index a9db17bebe8e5..292a51d3e1c5d 100644
--- a/nixos/modules/services/misc/sourcehut/dispatch.nix
+++ b/nixos/modules/services/misc/sourcehut/dispatch.nix
@@ -1,8 +1,9 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 let
   cfg = config.services.sourcehut;
+  opt = options.services.sourcehut;
   cfgIni = cfg.settings;
   scfg = cfg.dispatch;
   iniKey = "dispatch.sr.ht";
@@ -38,6 +39,7 @@ in
     statePath = mkOption {
       type = types.path;
       default = "${cfg.statePath}/dispatchsrht";
+      defaultText = literalExpression ''"''${config.${opt.statePath}}/dispatchsrht"'';
       description = ''
         State path for dispatch.sr.ht.
       '';
diff --git a/nixos/modules/services/misc/sourcehut/git.nix b/nixos/modules/services/misc/sourcehut/git.nix
index 2653d77876dca..5ce16df8cd87c 100644
--- a/nixos/modules/services/misc/sourcehut/git.nix
+++ b/nixos/modules/services/misc/sourcehut/git.nix
@@ -1,8 +1,9 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 let
   cfg = config.services.sourcehut;
+  opt = options.services.sourcehut;
   scfg = cfg.git;
   iniKey = "git.sr.ht";
 
@@ -41,6 +42,7 @@ in
     statePath = mkOption {
       type = types.path;
       default = "${cfg.statePath}/gitsrht";
+      defaultText = literalExpression ''"''${config.${opt.statePath}}/gitsrht"'';
       description = ''
         State path for git.sr.ht.
       '';
diff --git a/nixos/modules/services/misc/sourcehut/hg.nix b/nixos/modules/services/misc/sourcehut/hg.nix
index 5cd36bb04550b..6ba1df8b6ddb7 100644
--- a/nixos/modules/services/misc/sourcehut/hg.nix
+++ b/nixos/modules/services/misc/sourcehut/hg.nix
@@ -1,8 +1,9 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 let
   cfg = config.services.sourcehut;
+  opt = options.services.sourcehut;
   scfg = cfg.hg;
   iniKey = "hg.sr.ht";
 
@@ -40,6 +41,7 @@ in
     statePath = mkOption {
       type = types.path;
       default = "${cfg.statePath}/hgsrht";
+      defaultText = literalExpression ''"''${config.${opt.statePath}}/hgsrht"'';
       description = ''
         State path for hg.sr.ht.
       '';
diff --git a/nixos/modules/services/misc/sourcehut/hub.nix b/nixos/modules/services/misc/sourcehut/hub.nix
index be3ea21011c7d..7d137a765056c 100644
--- a/nixos/modules/services/misc/sourcehut/hub.nix
+++ b/nixos/modules/services/misc/sourcehut/hub.nix
@@ -1,8 +1,9 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 let
   cfg = config.services.sourcehut;
+  opt = options.services.sourcehut;
   cfgIni = cfg.settings;
   scfg = cfg.hub;
   iniKey = "hub.sr.ht";
@@ -38,6 +39,7 @@ in
     statePath = mkOption {
       type = types.path;
       default = "${cfg.statePath}/hubsrht";
+      defaultText = literalExpression ''"''${config.${opt.statePath}}/hubsrht"'';
       description = ''
         State path for hub.sr.ht.
       '';
diff --git a/nixos/modules/services/misc/sourcehut/lists.nix b/nixos/modules/services/misc/sourcehut/lists.nix
index 7b1fe9fd4630e..76f155caa05bc 100644
--- a/nixos/modules/services/misc/sourcehut/lists.nix
+++ b/nixos/modules/services/misc/sourcehut/lists.nix
@@ -1,11 +1,12 @@
 # Email setup is fairly involved, useful references:
 # https://drewdevault.com/2018/08/05/Local-mail-server.html
 
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 let
   cfg = config.services.sourcehut;
+  opt = options.services.sourcehut;
   cfgIni = cfg.settings;
   scfg = cfg.lists;
   iniKey = "lists.sr.ht";
@@ -42,6 +43,7 @@ in
     statePath = mkOption {
       type = types.path;
       default = "${cfg.statePath}/listssrht";
+      defaultText = literalExpression ''"''${config.${opt.statePath}}/listssrht"'';
       description = ''
         State path for lists.sr.ht.
       '';
diff --git a/nixos/modules/services/misc/sourcehut/man.nix b/nixos/modules/services/misc/sourcehut/man.nix
index 7693396d187c3..8ca271c32ee3a 100644
--- a/nixos/modules/services/misc/sourcehut/man.nix
+++ b/nixos/modules/services/misc/sourcehut/man.nix
@@ -1,8 +1,9 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 let
   cfg = config.services.sourcehut;
+  opt = options.services.sourcehut;
   cfgIni = cfg.settings;
   scfg = cfg.man;
   iniKey = "man.sr.ht";
@@ -38,6 +39,7 @@ in
     statePath = mkOption {
       type = types.path;
       default = "${cfg.statePath}/mansrht";
+      defaultText = literalExpression ''"''${config.${opt.statePath}}/mansrht"'';
       description = ''
         State path for man.sr.ht.
       '';
diff --git a/nixos/modules/services/misc/sourcehut/meta.nix b/nixos/modules/services/misc/sourcehut/meta.nix
index 56127a824eb44..33e4f2332b53c 100644
--- a/nixos/modules/services/misc/sourcehut/meta.nix
+++ b/nixos/modules/services/misc/sourcehut/meta.nix
@@ -1,8 +1,9 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 let
   cfg = config.services.sourcehut;
+  opt = options.services.sourcehut;
   cfgIni = cfg.settings;
   scfg = cfg.meta;
   iniKey = "meta.sr.ht";
@@ -39,6 +40,7 @@ in
     statePath = mkOption {
       type = types.path;
       default = "${cfg.statePath}/metasrht";
+      defaultText = literalExpression ''"''${config.${opt.statePath}}/metasrht"'';
       description = ''
         State path for meta.sr.ht.
       '';
diff --git a/nixos/modules/services/misc/sourcehut/paste.nix b/nixos/modules/services/misc/sourcehut/paste.nix
index b2d5151969eab..b481ebaf89178 100644
--- a/nixos/modules/services/misc/sourcehut/paste.nix
+++ b/nixos/modules/services/misc/sourcehut/paste.nix
@@ -1,8 +1,9 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 let
   cfg = config.services.sourcehut;
+  opt = options.services.sourcehut;
   cfgIni = cfg.settings;
   scfg = cfg.paste;
   iniKey = "paste.sr.ht";
@@ -39,6 +40,7 @@ in
     statePath = mkOption {
       type = types.path;
       default = "${cfg.statePath}/pastesrht";
+      defaultText = literalExpression ''"''${config.${opt.statePath}}/pastesrht"'';
       description = ''
         State path for pastesrht.sr.ht.
       '';
diff --git a/nixos/modules/services/misc/sourcehut/service.nix b/nixos/modules/services/misc/sourcehut/service.nix
index 65b4ad020f9a6..f1706ad0a6a8a 100644
--- a/nixos/modules/services/misc/sourcehut/service.nix
+++ b/nixos/modules/services/misc/sourcehut/service.nix
@@ -1,66 +1,375 @@
-{ config, pkgs, lib }:
-serviceCfg: serviceDrv: iniKey: attrs:
+srv:
+{ configIniOfService
+, srvsrht ? "${srv}srht" # Because "buildsrht" does not follow that pattern (missing an "s").
+, iniKey ? "${srv}.sr.ht"
+, webhooks ? false
+, extraTimers ? {}
+, mainService ? {}
+, extraServices ? {}
+, extraConfig ? {}
+, port
+}:
+{ config, lib, pkgs, ... }:
+
+with lib;
 let
+  inherit (config.services) postgresql;
+  redis = config.services.redis.servers."sourcehut-${srvsrht}";
+  inherit (config.users) users;
   cfg = config.services.sourcehut;
-  cfgIni = cfg.settings."${iniKey}";
-  pgSuperUser = config.services.postgresql.superUser;
-
-  setupDB = pkgs.writeScript "${serviceDrv.pname}-gen-db" ''
-    #! ${cfg.python}/bin/python
-    from ${serviceDrv.pname}.app import db
-    db.create()
-  '';
+  configIni = configIniOfService srv;
+  srvCfg = cfg.${srv};
+  baseService = serviceName: { allowStripe ? false }: extraService: let
+    runDir = "/run/sourcehut/${serviceName}";
+    rootDir = "/run/sourcehut/chroots/${serviceName}";
+    in
+    mkMerge [ extraService {
+    after = [ "network.target" ] ++
+      optional cfg.postgresql.enable "postgresql.service" ++
+      optional cfg.redis.enable "redis-sourcehut-${srvsrht}.service";
+    requires =
+      optional cfg.postgresql.enable "postgresql.service" ++
+      optional cfg.redis.enable "redis-sourcehut-${srvsrht}.service";
+    path = [ pkgs.gawk ];
+    environment.HOME = runDir;
+    serviceConfig = {
+      User = mkDefault srvCfg.user;
+      Group = mkDefault srvCfg.group;
+      RuntimeDirectory = [
+        "sourcehut/${serviceName}"
+        # Used by *srht-keys which reads ../config.ini
+        "sourcehut/${serviceName}/subdir"
+        "sourcehut/chroots/${serviceName}"
+      ];
+      RuntimeDirectoryMode = "2750";
+      # No need for the chroot path once inside the chroot
+      InaccessiblePaths = [ "-+${rootDir}" ];
+      # g+rx is for group members (eg. fcgiwrap or nginx)
+      # to read Git/Mercurial repositories, buildlogs, etc.
+      # o+x is for intermediate directories created by BindPaths= and like,
+      # as they're owned by root:root.
+      UMask = "0026";
+      RootDirectory = rootDir;
+      RootDirectoryStartOnly = true;
+      PrivateTmp = true;
+      MountAPIVFS = true;
+      # config.ini is looked up in there, before /etc/srht/config.ini
+      # Note that it fails to be set in ExecStartPre=
+      WorkingDirectory = mkDefault ("-"+runDir);
+      BindReadOnlyPaths = [
+        builtins.storeDir
+        "/etc"
+        "/run/booted-system"
+        "/run/current-system"
+        "/run/systemd"
+        ] ++
+        optional cfg.postgresql.enable "/run/postgresql" ++
+        optional cfg.redis.enable "/run/redis-sourcehut-${srvsrht}";
+      # LoadCredential= are unfortunately not available in ExecStartPre=
+      # Hence this one is run as root (the +) with RootDirectoryStartOnly=
+      # to reach credentials wherever they are.
+      # Note that each systemd service gets its own ${runDir}/config.ini file.
+      ExecStartPre = mkBefore [("+"+pkgs.writeShellScript "${serviceName}-credentials" ''
+        set -x
+        # Replace values begining with a '<' by the content of the file whose name is after.
+        gawk '{ if (match($0,/^([^=]+=)<(.+)/,m)) { getline f < m[2]; print m[1] f } else print $0 }' ${configIni} |
+        ${optionalString (!allowStripe) "gawk '!/^stripe-secret-key=/' |"}
+        install -o ${srvCfg.user} -g root -m 400 /dev/stdin ${runDir}/config.ini
+      '')];
+      # The following options are only for optimizing:
+      # systemd-analyze security
+      AmbientCapabilities = "";
+      CapabilityBoundingSet = "";
+      # ProtectClock= adds DeviceAllow=char-rtc r
+      DeviceAllow = "";
+      LockPersonality = true;
+      MemoryDenyWriteExecute = true;
+      NoNewPrivileges = true;
+      PrivateDevices = true;
+      PrivateMounts = true;
+      PrivateNetwork = mkDefault false;
+      PrivateUsers = true;
+      ProcSubset = "pid";
+      ProtectClock = true;
+      ProtectControlGroups = true;
+      ProtectHome = true;
+      ProtectHostname = true;
+      ProtectKernelLogs = true;
+      ProtectKernelModules = true;
+      ProtectKernelTunables = true;
+      ProtectProc = "invisible";
+      ProtectSystem = "strict";
+      RemoveIPC = true;
+      RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+      RestrictNamespaces = true;
+      RestrictRealtime = true;
+      RestrictSUIDSGID = true;
+      #SocketBindAllow = [ "tcp:${toString srvCfg.port}" "tcp:${toString srvCfg.prometheusPort}" ];
+      #SocketBindDeny = "any";
+      SystemCallFilter = [
+        "@system-service"
+        "~@aio" "~@keyring" "~@memlock" "~@privileged" "~@resources" "~@timer"
+        "@chown" "@setuid"
+      ];
+      SystemCallArchitectures = "native";
+    };
+  } ];
 in
-with serviceCfg; with lib; recursiveUpdate
 {
-  environment.HOME = statePath;
-  path = [ config.services.postgresql.package ] ++ (attrs.path or [ ]);
-  restartTriggers = [ config.environment.etc."sr.ht/config.ini".source ];
-  serviceConfig = {
-    Type = "simple";
-    User = user;
-    Group = user;
-    Restart = "always";
-    WorkingDirectory = statePath;
-  } // (if (cfg.statePath == "/var/lib/sourcehut/${serviceDrv.pname}") then {
-          StateDirectory = [ "sourcehut/${serviceDrv.pname}" ];
-        } else {})
-  ;
-
-  preStart = ''
-    if ! test -e ${statePath}/db; then
-      # Setup the initial database
-      ${setupDB}
-
-      # Set the initial state of the database for future database upgrades
-      if test -e ${cfg.python}/bin/${serviceDrv.pname}-migrate; then
-        # Run alembic stamp head once to tell alembic the schema is up-to-date
-        ${cfg.python}/bin/${serviceDrv.pname}-migrate stamp head
-      fi
-
-      printf "%s" "${serviceDrv.version}" > ${statePath}/db
-    fi
-
-    # Update copy of each users' profile to the latest
-    # See https://lists.sr.ht/~sircmpwn/sr.ht-admins/<20190302181207.GA13778%40cirno.my.domain>
-    if ! test -e ${statePath}/webhook; then
-      # Update ${iniKey}'s users' profile copy to the latest
-      ${cfg.python}/bin/srht-update-profiles ${iniKey}
-
-      touch ${statePath}/webhook
-    fi
-
-    ${optionalString (builtins.hasAttr "migrate-on-upgrade" cfgIni && cfgIni.migrate-on-upgrade == "yes") ''
-      if [ "$(cat ${statePath}/db)" != "${serviceDrv.version}" ]; then
-        # Manage schema migrations using alembic
-        ${cfg.python}/bin/${serviceDrv.pname}-migrate -a upgrade head
-
-        # Mark down current package version
-        printf "%s" "${serviceDrv.version}" > ${statePath}/db
-      fi
-    ''}
-
-    ${attrs.preStart or ""}
-  '';
+  options.services.sourcehut.${srv} = {
+    enable = mkEnableOption "${srv} service";
+
+    user = mkOption {
+      type = types.str;
+      default = srvsrht;
+      description = ''
+        User for ${srv}.sr.ht.
+      '';
+    };
+
+    group = mkOption {
+      type = types.str;
+      default = srvsrht;
+      description = ''
+        Group for ${srv}.sr.ht.
+        Membership grants access to the Git/Mercurial repositories by default,
+        but not to the config.ini file (where secrets are).
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = port;
+      description = ''
+        Port on which the "${srv}" backend should listen.
+      '';
+    };
+
+    redis = {
+      host = mkOption {
+        type = types.str;
+        default = "unix:/run/redis-sourcehut-${srvsrht}/redis.sock?db=0";
+        example = "redis://shared.wireguard:6379/0";
+        description = ''
+          The redis host URL. This is used for caching and temporary storage, and must
+          be shared between nodes (e.g. git1.sr.ht and git2.sr.ht), but need not be
+          shared between services. It may be shared between services, however, with no
+          ill effect, if this better suits your infrastructure.
+        '';
+      };
+    };
+
+    postgresql = {
+      database = mkOption {
+        type = types.str;
+        default = "${srv}.sr.ht";
+        description = ''
+          PostgreSQL database name for the ${srv}.sr.ht service,
+          used if <xref linkend="opt-services.sourcehut.postgresql.enable"/> is <literal>true</literal>.
+        '';
+      };
+    };
+
+    gunicorn = {
+      extraArgs = mkOption {
+        type = with types; listOf str;
+        default = ["--timeout 120" "--workers 1" "--log-level=info"];
+        description = "Extra arguments passed to Gunicorn.";
+      };
+    };
+  } // optionalAttrs webhooks {
+    webhooks = {
+      extraArgs = mkOption {
+        type = with types; listOf str;
+        default = ["--loglevel DEBUG" "--pool eventlet" "--without-heartbeat"];
+        description = "Extra arguments passed to the Celery responsible for webhooks.";
+      };
+      celeryConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Content of the <literal>celeryconfig.py</literal> used by the Celery responsible for webhooks.";
+      };
+    };
+  };
+
+  config = lib.mkIf (cfg.enable && srvCfg.enable) (mkMerge [ extraConfig {
+    users = {
+      users = {
+        "${srvCfg.user}" = {
+          isSystemUser = true;
+          group = mkDefault srvCfg.group;
+          description = mkDefault "sourcehut user for ${srv}.sr.ht";
+        };
+      };
+      groups = {
+        "${srvCfg.group}" = { };
+      } // optionalAttrs (cfg.postgresql.enable
+        && hasSuffix "0" (postgresql.settings.unix_socket_permissions or "")) {
+        "postgres".members = [ srvCfg.user ];
+      } // optionalAttrs (cfg.redis.enable
+        && hasSuffix "0" (redis.settings.unixsocketperm or "")) {
+        "redis-sourcehut-${srvsrht}".members = [ srvCfg.user ];
+      };
+    };
+
+    services.nginx = mkIf cfg.nginx.enable {
+      virtualHosts."${srv}.${cfg.settings."sr.ht".global-domain}" = mkMerge [ {
+        forceSSL = mkDefault true;
+        locations."/".proxyPass = "http://${cfg.listenAddress}:${toString srvCfg.port}";
+        locations."/static" = {
+          root = "${pkgs.sourcehut.${srvsrht}}/${pkgs.sourcehut.python.sitePackages}/${srvsrht}";
+          extraConfig = mkDefault ''
+            expires 30d;
+          '';
+        };
+      } cfg.nginx.virtualHost ];
+    };
+
+    services.postgresql = mkIf cfg.postgresql.enable {
+      authentication = ''
+        local ${srvCfg.postgresql.database} ${srvCfg.user} trust
+      '';
+      ensureDatabases = [ srvCfg.postgresql.database ];
+      ensureUsers = map (name: {
+          inherit name;
+          ensurePermissions = { "DATABASE \"${srvCfg.postgresql.database}\"" = "ALL PRIVILEGES"; };
+        }) [srvCfg.user];
+    };
+
+    services.sourcehut.services = mkDefault (filter (s: cfg.${s}.enable)
+      [ "builds" "dispatch" "git" "hg" "hub" "lists" "man" "meta" "pages" "paste" "todo" ]);
+
+    services.sourcehut.settings = mkMerge [
+      {
+        "${srv}.sr.ht".origin = mkDefault "https://${srv}.${cfg.settings."sr.ht".global-domain}";
+      }
+
+      (mkIf cfg.postgresql.enable {
+        "${srv}.sr.ht".connection-string = mkDefault "postgresql:///${srvCfg.postgresql.database}?user=${srvCfg.user}&host=/run/postgresql";
+      })
+    ];
+
+    services.redis.servers."sourcehut-${srvsrht}" = mkIf cfg.redis.enable {
+      enable = true;
+      databases = 3;
+      syslog = true;
+      # TODO: set a more informed value
+      save = mkDefault [ [1800 10] [300 100] ];
+      settings = {
+        # TODO: set a more informed value
+        maxmemory = "128MB";
+        maxmemory-policy = "volatile-ttl";
+      };
+    };
+
+    systemd.services = mkMerge [
+      {
+        "${srvsrht}" = baseService srvsrht { allowStripe = srv == "meta"; } (mkMerge [
+        {
+          description = "sourcehut ${srv}.sr.ht website service";
+          before = optional cfg.nginx.enable "nginx.service";
+          wants = optional cfg.nginx.enable "nginx.service";
+          wantedBy = [ "multi-user.target" ];
+          path = optional cfg.postgresql.enable postgresql.package;
+          # Beware: change in credentials' content will not trigger restart.
+          restartTriggers = [ configIni ];
+          serviceConfig = {
+            Type = "simple";
+            Restart = mkDefault "always";
+            #RestartSec = mkDefault "2min";
+            StateDirectory = [ "sourcehut/${srvsrht}" ];
+            StateDirectoryMode = "2750";
+            ExecStart = "${cfg.python}/bin/gunicorn ${srvsrht}.app:app --name ${srvsrht} --bind ${cfg.listenAddress}:${toString srvCfg.port} " + concatStringsSep " " srvCfg.gunicorn.extraArgs;
+          };
+          preStart = let
+            version = pkgs.sourcehut.${srvsrht}.version;
+            stateDir = "/var/lib/sourcehut/${srvsrht}";
+            in mkBefore ''
+            set -x
+            # Use the /run/sourcehut/${srvsrht}/config.ini
+            # installed by a previous ExecStartPre= in baseService
+            cd /run/sourcehut/${srvsrht}
+
+            if test ! -e ${stateDir}/db; then
+              # Setup the initial database.
+              # Note that it stamps the alembic head afterward
+              ${cfg.python}/bin/${srvsrht}-initdb
+              echo ${version} >${stateDir}/db
+            fi
+
+            ${optionalString cfg.settings.${iniKey}.migrate-on-upgrade ''
+              if [ "$(cat ${stateDir}/db)" != "${version}" ]; then
+                # Manage schema migrations using alembic
+                ${cfg.python}/bin/${srvsrht}-migrate -a upgrade head
+                echo ${version} >${stateDir}/db
+              fi
+            ''}
+
+            # Update copy of each users' profile to the latest
+            # See https://lists.sr.ht/~sircmpwn/sr.ht-admins/<20190302181207.GA13778%40cirno.my.domain>
+            if test ! -e ${stateDir}/webhook; then
+              # Update ${iniKey}'s users' profile copy to the latest
+              ${cfg.python}/bin/srht-update-profiles ${iniKey}
+              touch ${stateDir}/webhook
+            fi
+          '';
+        } mainService ]);
+      }
+
+      (mkIf webhooks {
+        "${srvsrht}-webhooks" = baseService "${srvsrht}-webhooks" {}
+          {
+            description = "sourcehut ${srv}.sr.ht webhooks service";
+            after = [ "${srvsrht}.service" ];
+            wantedBy = [ "${srvsrht}.service" ];
+            partOf = [ "${srvsrht}.service" ];
+            preStart = ''
+              cp ${pkgs.writeText "${srvsrht}-webhooks-celeryconfig.py" srvCfg.webhooks.celeryConfig} \
+                 /run/sourcehut/${srvsrht}-webhooks/celeryconfig.py
+            '';
+            serviceConfig = {
+              Type = "simple";
+              Restart = "always";
+              ExecStart = "${cfg.python}/bin/celery --app ${srvsrht}.webhooks worker --hostname ${srvsrht}-webhooks@%%h " + concatStringsSep " " srvCfg.webhooks.extraArgs;
+              # Avoid crashing: os.getloadavg()
+              ProcSubset = mkForce "all";
+            };
+          };
+      })
+
+      (mapAttrs (timerName: timer: (baseService timerName {} (mkMerge [
+        {
+          description = "sourcehut ${timerName} service";
+          after = [ "network.target" "${srvsrht}.service" ];
+          serviceConfig = {
+            Type = "oneshot";
+            ExecStart = "${cfg.python}/bin/${timerName}";
+          };
+        }
+        (timer.service or {})
+      ]))) extraTimers)
+
+      (mapAttrs (serviceName: extraService: baseService serviceName {} (mkMerge [
+        {
+          description = "sourcehut ${serviceName} service";
+          # So that extraServices have the PostgreSQL database initialized.
+          after = [ "${srvsrht}.service" ];
+          wantedBy = [ "${srvsrht}.service" ];
+          partOf = [ "${srvsrht}.service" ];
+          serviceConfig = {
+            Type = "simple";
+            Restart = mkDefault "always";
+          };
+        }
+        extraService
+      ])) extraServices)
+    ];
+
+    systemd.timers = mapAttrs (timerName: timer:
+      {
+        description = "sourcehut timer for ${timerName}";
+        wantedBy = [ "timers.target" ];
+        inherit (timer) timerConfig;
+      }) extraTimers;
+  } ]);
 }
-  (builtins.removeAttrs attrs [ "path" "preStart" ])
diff --git a/nixos/modules/services/misc/sourcehut/sourcehut.xml b/nixos/modules/services/misc/sourcehut/sourcehut.xml
index ab9a8c6cb4be0..41094f65a94d9 100644
--- a/nixos/modules/services/misc/sourcehut/sourcehut.xml
+++ b/nixos/modules/services/misc/sourcehut/sourcehut.xml
@@ -14,13 +14,12 @@
   <title>Basic usage</title>
   <para>
    Sourcehut is a Python and Go based set of applications.
-   <literal><link linkend="opt-services.sourcehut.enable">services.sourcehut</link></literal>
-   by default will use
+   This NixOS module also provides basic configuration integrating Sourcehut into locally running
    <literal><link linkend="opt-services.nginx.enable">services.nginx</link></literal>,
-   <literal><link linkend="opt-services.nginx.enable">services.redis</link></literal>,
-   <literal><link linkend="opt-services.nginx.enable">services.cron</link></literal>,
+   <literal><link linkend="opt-services.redis.servers">services.redis.servers.sourcehut</link></literal>,
+   <literal><link linkend="opt-services.postfix.enable">services.postfix</link></literal>
    and
-   <literal><link linkend="opt-services.postgresql.enable">services.postgresql</link></literal>.
+   <literal><link linkend="opt-services.postgresql.enable">services.postgresql</link></literal> services.
   </para>
 
   <para>
@@ -42,18 +41,23 @@ in {
 
   services.sourcehut = {
     <link linkend="opt-services.sourcehut.enable">enable</link> = true;
-    <link linkend="opt-services.sourcehut.originBase">originBase</link> = fqdn;
-    <link linkend="opt-services.sourcehut.services">services</link> = [ "meta" "man" "git" ];
+    <link linkend="opt-services.sourcehut.git.enable">git.enable</link> = true;
+    <link linkend="opt-services.sourcehut.man.enable">man.enable</link> = true;
+    <link linkend="opt-services.sourcehut.meta.enable">meta.enable</link> = true;
+    <link linkend="opt-services.sourcehut.nginx.enable">nginx.enable</link> = true;
+    <link linkend="opt-services.sourcehut.postfix.enable">postfix.enable</link> = true;
+    <link linkend="opt-services.sourcehut.postgresql.enable">postgresql.enable</link> = true;
+    <link linkend="opt-services.sourcehut.redis.enable">redis.enable</link> = true;
     <link linkend="opt-services.sourcehut.settings">settings</link> = {
         "sr.ht" = {
           environment = "production";
           global-domain = fqdn;
           origin = "https://${fqdn}";
           # Produce keys with srht-keygen from <package>sourcehut.coresrht</package>.
-          network-key = "SECRET";
-          service-key = "SECRET";
+          network-key = "/run/keys/path/to/network-key";
+          service-key = "/run/keys/path/to/service-key";
         };
-        webhooks.private-key= "SECRET";
+        webhooks.private-key= "/run/keys/path/to/webhook-key";
     };
   };
 
diff --git a/nixos/modules/services/misc/sourcehut/todo.nix b/nixos/modules/services/misc/sourcehut/todo.nix
index aec773b066923..262fa48f59d4d 100644
--- a/nixos/modules/services/misc/sourcehut/todo.nix
+++ b/nixos/modules/services/misc/sourcehut/todo.nix
@@ -1,8 +1,9 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 let
   cfg = config.services.sourcehut;
+  opt = options.services.sourcehut;
   cfgIni = cfg.settings;
   scfg = cfg.todo;
   iniKey = "todo.sr.ht";
@@ -39,6 +40,7 @@ in
     statePath = mkOption {
       type = types.path;
       default = "${cfg.statePath}/todosrht";
+      defaultText = literalExpression ''"''${config.${opt.statePath}}/todosrht"'';
       description = ''
         State path for todo.sr.ht.
       '';
diff --git a/nixos/modules/services/misc/subsonic.nix b/nixos/modules/services/misc/subsonic.nix
index 98b85918ad180..2dda8970dd306 100644
--- a/nixos/modules/services/misc/subsonic.nix
+++ b/nixos/modules/services/misc/subsonic.nix
@@ -1,8 +1,11 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
-let cfg = config.services.subsonic; in {
+let
+  cfg = config.services.subsonic;
+  opt = options.services.subsonic;
+in {
   options = {
     services.subsonic = {
       enable = mkEnableOption "Subsonic daemon";
@@ -97,7 +100,7 @@ let cfg = config.services.subsonic; in {
         description = ''
           List of paths to transcoder executables that should be accessible
           from Subsonic. Symlinks will be created to each executable inside
-          ${cfg.home}/transcoders.
+          ''${config.${opt.home}}/transcoders.
         '';
       };
     };
diff --git a/nixos/modules/services/misc/zigbee2mqtt.nix b/nixos/modules/services/misc/zigbee2mqtt.nix
index b378d9f362fe3..ff6d595e5a6e3 100644
--- a/nixos/modules/services/misc/zigbee2mqtt.nix
+++ b/nixos/modules/services/misc/zigbee2mqtt.nix
@@ -22,13 +22,9 @@ in
 
     package = mkOption {
       description = "Zigbee2mqtt package to use";
-      default = pkgs.zigbee2mqtt.override {
-        dataDir = cfg.dataDir;
-      };
+      default = pkgs.zigbee2mqtt;
       defaultText = literalExpression ''
-        pkgs.zigbee2mqtt {
-          dataDir = services.zigbee2mqtt.dataDir
-        }
+        pkgs.zigbee2mqtt
       '';
       type = types.package;
     };
@@ -41,7 +37,7 @@ in
 
     settings = mkOption {
       type = format.type;
-      default = {};
+      default = { };
       example = literalExpression ''
         {
           homeassistant = config.services.home-assistant.enable;
@@ -83,6 +79,7 @@ in
       serviceConfig = {
         ExecStart = "${cfg.package}/bin/zigbee2mqtt";
         User = "zigbee2mqtt";
+        Group = "zigbee2mqtt";
         WorkingDirectory = cfg.dataDir;
         Restart = "on-failure";
 
diff --git a/nixos/modules/services/misc/zoneminder.nix b/nixos/modules/services/misc/zoneminder.nix
index 378da7b87442a..407742f72ad5a 100644
--- a/nixos/modules/services/misc/zoneminder.nix
+++ b/nixos/modules/services/misc/zoneminder.nix
@@ -171,7 +171,7 @@ in {
         example = "/storage/tank";
         description = ''
           ZoneMinder can generate quite a lot of data, so in case you don't want
-          to use the default ${home}, you can override the path here.
+          to use the default ${defaultDir}, you can override the path here.
         '';
       };
 
diff --git a/nixos/modules/services/monitoring/collectd.nix b/nixos/modules/services/monitoring/collectd.nix
index 660d108587dee..8d81737a3ef0d 100644
--- a/nixos/modules/services/monitoring/collectd.nix
+++ b/nixos/modules/services/monitoring/collectd.nix
@@ -5,7 +5,7 @@ with lib;
 let
   cfg = config.services.collectd;
 
-  conf = pkgs.writeText "collectd.conf" ''
+  unvalidated_conf = pkgs.writeText "collectd-unvalidated.conf" ''
     BaseDir "${cfg.dataDir}"
     AutoLoadPlugin ${boolToString cfg.autoLoadPlugin}
     Hostname "${config.networking.hostName}"
@@ -30,6 +30,15 @@ let
     ${cfg.extraConfig}
   '';
 
+  conf = if cfg.validateConfig then
+    pkgs.runCommand "collectd.conf" {} ''
+      echo testing ${unvalidated_conf}
+      # collectd -t fails if BaseDir does not exist.
+      sed '1s/^BaseDir.*$/BaseDir "."/' ${unvalidated_conf} > collectd.conf
+      ${package}/bin/collectd -t -C collectd.conf
+      cp ${unvalidated_conf} $out
+    '' else unvalidated_conf;
+
   package =
     if cfg.buildMinimalPackage
     then minimalPackage
@@ -43,6 +52,16 @@ in {
   options.services.collectd = with types; {
     enable = mkEnableOption "collectd agent";
 
+    validateConfig = mkOption {
+      default = true;
+      description = ''
+        Validate the syntax of collectd configuration file at build time.
+        Disable this if you use the Include directive on files unavailable in
+        the build sandbox, or when cross-compiling.
+      '';
+      type = types.bool;
+    };
+
     package = mkOption {
       default = pkgs.collectd;
       defaultText = literalExpression "pkgs.collectd";
diff --git a/nixos/modules/services/monitoring/grafana.nix b/nixos/modules/services/monitoring/grafana.nix
index 5067047e9690e..81fca33f5fec4 100644
--- a/nixos/modules/services/monitoring/grafana.nix
+++ b/nixos/modules/services/monitoring/grafana.nix
@@ -404,6 +404,7 @@ in {
       path = mkOption {
         description = "Database path.";
         default = "${cfg.dataDir}/data/grafana.db";
+        defaultText = literalExpression ''"''${config.${opt.dataDir}}/data/grafana.db"'';
         type = types.path;
       };
 
diff --git a/nixos/modules/services/monitoring/graphite.nix b/nixos/modules/services/monitoring/graphite.nix
index 0dbb33530c928..baa943302a00f 100644
--- a/nixos/modules/services/monitoring/graphite.nix
+++ b/nixos/modules/services/monitoring/graphite.nix
@@ -1,9 +1,10 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
   cfg = config.services.graphite;
+  opt = options.services.graphite;
   writeTextOrNull = f: t: mapNullable (pkgs.writeTextDir f) t;
 
   dataDir = cfg.dataDir;
@@ -171,6 +172,13 @@ in {
             directories:
                 - ${dataDir}/whisper
         '';
+        defaultText = literalExpression ''
+          '''
+            whisper:
+              directories:
+                - ''${config.${opt.dataDir}}/whisper
+          '''
+        '';
         example = ''
           allowed_origins:
             - dashboard.example.com
@@ -312,12 +320,14 @@ in {
 
       seyrenUrl = mkOption {
         default = "http://localhost:${toString cfg.seyren.port}/";
+        defaultText = literalExpression ''"http://localhost:''${toString config.${opt.seyren.port}}/"'';
         description = "Host where seyren is accessible.";
         type = types.str;
       };
 
       graphiteUrl = mkOption {
         default = "http://${cfg.web.listenAddress}:${toString cfg.web.port}";
+        defaultText = literalExpression ''"http://''${config.${opt.web.listenAddress}}:''${toString config.${opt.web.port}}"'';
         description = "Host where graphite service runs.";
         type = types.str;
       };
diff --git a/nixos/modules/services/monitoring/parsedmarc.nix b/nixos/modules/services/monitoring/parsedmarc.nix
index 8571e1f01ed69..ec71365ba3c1c 100644
--- a/nixos/modules/services/monitoring/parsedmarc.nix
+++ b/nixos/modules/services/monitoring/parsedmarc.nix
@@ -1,7 +1,8 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 let
   cfg = config.services.parsedmarc;
+  opt = options.services.parsedmarc;
   ini = pkgs.formats.ini {};
 in
 {
@@ -80,6 +81,9 @@ in
         datasource = lib.mkOption {
           type = lib.types.bool;
           default = cfg.provision.elasticsearch && config.services.grafana.enable;
+          defaultText = lib.literalExpression ''
+            config.${opt.provision.elasticsearch} && config.${options.services.grafana.enable}
+          '';
           apply = x: x && cfg.provision.elasticsearch;
           description = ''
             Whether the automatically provisioned Elasticsearch
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/fastly.nix b/nixos/modules/services/monitoring/prometheus/exporters/fastly.nix
index 5b35bb29a301a..55a61c4949ee5 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/fastly.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/fastly.nix
@@ -32,10 +32,10 @@ in
     script = ''
       ${optionalString (cfg.tokenPath != null)
       "export FASTLY_API_TOKEN=$(cat ${toString cfg.tokenPath})"}
-      ${pkgs.fastly-exporter}/bin/fastly-exporter \
-        -endpoint http://${cfg.listenAddress}:${cfg.port}/metrics
+      ${pkgs.prometheus-fastly-exporter}/bin/fastly-exporter \
+        -listen http://${cfg.listenAddress}:${toString cfg.port}
         ${optionalString cfg.debug "-debug true"} \
-        ${optionalString cfg.configFile "-config-file ${cfg.configFile}"}
+        ${optionalString (cfg.configFile != null) "-config-file ${cfg.configFile}"}
     '';
   };
 }
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/nginx.nix b/nixos/modules/services/monitoring/prometheus/exporters/nginx.nix
index 3cdd7866bd4db..6f69f5919d1e0 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/nginx.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/nginx.nix
@@ -46,11 +46,11 @@ in
     serviceConfig = {
       ExecStart = ''
         ${pkgs.prometheus-nginx-exporter}/bin/nginx-prometheus-exporter \
-          --nginx.scrape-uri '${cfg.scrapeUri}' \
-          --nginx.ssl-verify ${boolToString cfg.sslVerify} \
-          --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
-          --web.telemetry-path ${cfg.telemetryPath} \
-          --prometheus.const-labels ${concatStringsSep "," cfg.constLabels} \
+          --nginx.scrape-uri='${cfg.scrapeUri}' \
+          --nginx.ssl-verify=${boolToString cfg.sslVerify} \
+          --web.listen-address=${cfg.listenAddress}:${toString cfg.port} \
+          --web.telemetry-path=${cfg.telemetryPath} \
+          --prometheus.const-labels=${concatStringsSep "," cfg.constLabels} \
           ${concatStringsSep " \\\n  " cfg.extraFlags}
       '';
     };
diff --git a/nixos/modules/services/monitoring/smartd.nix b/nixos/modules/services/monitoring/smartd.nix
index 73021b1b4d38f..6d39cc3e4e6bb 100644
--- a/nixos/modules/services/monitoring/smartd.nix
+++ b/nixos/modules/services/monitoring/smartd.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
@@ -8,6 +8,7 @@ let
        + optionalString (config.networking.domain != null) ".${config.networking.domain}";
 
   cfg = config.services.smartd;
+  opt = options.services.smartd;
 
   nm = cfg.notifications.mail;
   nw = cfg.notifications.wall;
@@ -211,6 +212,7 @@ in
 
         autodetected = mkOption {
           default = cfg.defaults.monitored;
+          defaultText = literalExpression "config.${opt.defaults.monitored}";
           type = types.separatedString " ";
           description = ''
             Like <option>services.smartd.defaults.monitored</option>, but for the
diff --git a/nixos/modules/services/monitoring/thanos.nix b/nixos/modules/services/monitoring/thanos.nix
index da626788d827c..9e93d8dbb0efd 100644
--- a/nixos/modules/services/monitoring/thanos.nix
+++ b/nixos/modules/services/monitoring/thanos.nix
@@ -83,6 +83,9 @@ let
   mkArgumentsOption = cmd: mkOption {
     type = types.listOf types.str;
     default = argumentsOf cmd;
+    defaultText = literalDocBook ''
+      calculated from <literal>config.services.thanos.${cmd}</literal>
+    '';
     description = ''
       Arguments to the <literal>thanos ${cmd}</literal> command.
 
diff --git a/nixos/modules/services/monitoring/uptime.nix b/nixos/modules/services/monitoring/uptime.nix
index 245badc3e44f9..79b86be6cc715 100644
--- a/nixos/modules/services/monitoring/uptime.nix
+++ b/nixos/modules/services/monitoring/uptime.nix
@@ -1,8 +1,9 @@
-{ config, pkgs, lib, ... }:
+{ config, options, pkgs, lib, ... }:
 let
-  inherit (lib) mkOption mkEnableOption mkIf mkMerge types optional;
+  inherit (lib) literalExpression mkOption mkEnableOption mkIf mkMerge types optional;
 
   cfg = config.services.uptime;
+  opt = options.services.uptime;
 
   configDir = pkgs.runCommand "config" { preferLocalBuild = true; }
   (if cfg.configFile != null then ''
@@ -52,7 +53,10 @@ in {
 
     enableWebService = mkEnableOption "the uptime monitoring program web service";
 
-    enableSeparateMonitoringService = mkEnableOption "the uptime monitoring service" // { default = cfg.enableWebService; };
+    enableSeparateMonitoringService = mkEnableOption "the uptime monitoring service" // {
+      default = cfg.enableWebService;
+      defaultText = literalExpression "config.${opt.enableWebService}";
+    };
 
     nodeEnv = mkOption {
       description = "The node environment to run in (development, production, etc.)";
diff --git a/nixos/modules/services/monitoring/zabbix-proxy.nix b/nixos/modules/services/monitoring/zabbix-proxy.nix
index b5009f47f175c..0ebd7bcff8343 100644
--- a/nixos/modules/services/monitoring/zabbix-proxy.nix
+++ b/nixos/modules/services/monitoring/zabbix-proxy.nix
@@ -1,7 +1,8 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 let
   cfg = config.services.zabbixProxy;
+  opt = options.services.zabbixProxy;
   pgsql = config.services.postgresql;
   mysql = config.services.mysql;
 
@@ -103,6 +104,11 @@ in
         port = mkOption {
           type = types.int;
           default = if cfg.database.type == "mysql" then mysql.port else pgsql.port;
+          defaultText = literalExpression ''
+            if config.${opt.database.type} == "mysql"
+            then config.${options.services.mysql.port}
+            else config.${options.services.postgresql.port}
+          '';
           description = "Database host port.";
         };
 
diff --git a/nixos/modules/services/monitoring/zabbix-server.nix b/nixos/modules/services/monitoring/zabbix-server.nix
index 0141c073da25d..9f960517a81b0 100644
--- a/nixos/modules/services/monitoring/zabbix-server.nix
+++ b/nixos/modules/services/monitoring/zabbix-server.nix
@@ -1,7 +1,8 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 let
   cfg = config.services.zabbixServer;
+  opt = options.services.zabbixServer;
   pgsql = config.services.postgresql;
   mysql = config.services.mysql;
 
@@ -95,6 +96,11 @@ in
         port = mkOption {
           type = types.int;
           default = if cfg.database.type == "mysql" then mysql.port else pgsql.port;
+          defaultText = literalExpression ''
+            if config.${opt.database.type} == "mysql"
+            then config.${options.services.mysql.port}
+            else config.${options.services.postgresql.port}
+          '';
           description = "Database host port.";
         };
 
diff --git a/nixos/modules/services/network-filesystems/glusterfs.nix b/nixos/modules/services/network-filesystems/glusterfs.nix
index bc8be05ca8cb1..38be098de5d90 100644
--- a/nixos/modules/services/network-filesystems/glusterfs.nix
+++ b/nixos/modules/services/network-filesystems/glusterfs.nix
@@ -187,7 +187,7 @@ in
 
       wantedBy = [ "multi-user.target" ];
 
-      after = [ "syslog.target" "network.target" ];
+      after = [ "network.target" ];
 
       preStart = ''
         install -m 0755 -d /var/log/glusterfs
diff --git a/nixos/modules/services/network-filesystems/openafs/server.nix b/nixos/modules/services/network-filesystems/openafs/server.nix
index c1bf83be77b91..9c974335defae 100644
--- a/nixos/modules/services/network-filesystems/openafs/server.nix
+++ b/nixos/modules/services/network-filesystems/openafs/server.nix
@@ -248,7 +248,7 @@ in {
     systemd.services = {
       openafs-server = {
         description = "OpenAFS server";
-        after = [ "syslog.target" "network.target" ];
+        after = [ "network.target" ];
         wantedBy = [ "multi-user.target" ];
         restartIfChanged = false;
         unitConfig.ConditionPathExists = [
diff --git a/nixos/modules/services/networking/adguardhome.nix b/nixos/modules/services/networking/adguardhome.nix
index 4388ef2b7e576..03f9b9f9bad45 100644
--- a/nixos/modules/services/networking/adguardhome.nix
+++ b/nixos/modules/services/networking/adguardhome.nix
@@ -56,7 +56,7 @@ in
   config = mkIf cfg.enable {
     systemd.services.adguardhome = {
       description = "AdGuard Home: Network-level blocker";
-      after = [ "syslog.target" "network.target" ];
+      after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
       unitConfig = {
         StartLimitIntervalSec = 5;
diff --git a/nixos/modules/services/networking/amuled.nix b/nixos/modules/services/networking/amuled.nix
index 39320643dd5e1..e55ac7a6b18b7 100644
--- a/nixos/modules/services/networking/amuled.nix
+++ b/nixos/modules/services/networking/amuled.nix
@@ -1,9 +1,10 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
   cfg = config.services.amule;
+  opt = options.services.amule;
   user = if cfg.user != null then cfg.user else "amule";
 in
 
@@ -26,6 +27,9 @@ in
       dataDir = mkOption {
         type = types.str;
         default = "/home/${user}/";
+        defaultText = literalExpression ''
+          "/home/''${config.${opt.user}}/"
+        '';
         description = ''
           The directory holding configuration, incoming and temporary files.
         '';
diff --git a/nixos/modules/services/networking/ddclient.nix b/nixos/modules/services/networking/ddclient.nix
index 021b28d5c34f2..8a2c0fc7080cf 100644
--- a/nixos/modules/services/networking/ddclient.nix
+++ b/nixos/modules/services/networking/ddclient.nix
@@ -29,7 +29,7 @@ let
   configFile = if (cfg.configFile != null) then cfg.configFile else configFile';
 
   preStart = ''
-    install --mode=0400 ${configFile} /run/${RuntimeDirectory}/ddclient.conf
+    install ${configFile} /run/${RuntimeDirectory}/ddclient.conf
     ${lib.optionalString (cfg.configFile == null) (if (cfg.passwordFile != null) then ''
       password=$(printf "%q" "$(head -n 1 "${cfg.passwordFile}")")
       sed -i "s|^password=$|password=$password|" /run/${RuntimeDirectory}/ddclient.conf
diff --git a/nixos/modules/services/networking/dhcpcd.nix b/nixos/modules/services/networking/dhcpcd.nix
index 31e4b6ad2988c..2c339350acd34 100644
--- a/nixos/modules/services/networking/dhcpcd.nix
+++ b/nixos/modules/services/networking/dhcpcd.nix
@@ -207,13 +207,20 @@ in
 
         serviceConfig =
           { Type = "forking";
-            PIDFile = "/run/dhcpcd.pid";
+            PIDFile = "/run/dhcpcd/pid";
+            RuntimeDirectory = "dhcpcd";
             ExecStart = "@${dhcpcd}/sbin/dhcpcd dhcpcd --quiet ${optionalString cfg.persistent "--persistent"} --config ${dhcpcdConf}";
             ExecReload = "${dhcpcd}/sbin/dhcpcd --rebind";
             Restart = "always";
           };
       };
 
+    users.users.dhcpcd = {
+      isSystemUser = true;
+      group = "dhcpcd";
+    };
+    users.groups.dhcpcd = {};
+
     environment.systemPackages = [ dhcpcd ];
 
     environment.etc."dhcpcd.exit-hook".source = exitHook;
diff --git a/nixos/modules/services/networking/dhcpd.nix b/nixos/modules/services/networking/dhcpd.nix
index 54e4f90028598..3c4c0069dfd00 100644
--- a/nixos/modules/services/networking/dhcpd.nix
+++ b/nixos/modules/services/networking/dhcpd.nix
@@ -28,38 +28,45 @@ let
       }
     '';
 
-  dhcpdService = postfix: cfg: optionalAttrs cfg.enable {
-    "dhcpd${postfix}" = {
-      description = "DHCPv${postfix} server";
-      wantedBy = [ "multi-user.target" ];
-      after = [ "network.target" ];
-
-      preStart = ''
-        mkdir -m 755 -p ${cfg.stateDir}
-        chown dhcpd:nogroup ${cfg.stateDir}
-        touch ${cfg.stateDir}/dhcpd.leases
-      '';
-
-      serviceConfig =
-        let
-          configFile = if cfg.configFile != null then cfg.configFile else writeConfig cfg;
-          args = [ "@${pkgs.dhcp}/sbin/dhcpd" "dhcpd${postfix}" "-${postfix}"
-                   "-pf" "/run/dhcpd${postfix}/dhcpd.pid"
-                   "-cf" "${configFile}"
-                   "-lf" "${cfg.stateDir}/dhcpd.leases"
-                   "-user" "dhcpd" "-group" "nogroup"
-                 ] ++ cfg.extraFlags
-                   ++ cfg.interfaces;
-
-        in {
-          ExecStart = concatMapStringsSep " " escapeShellArg args;
-          Type = "forking";
-          Restart = "always";
-          RuntimeDirectory = [ "dhcpd${postfix}" ];
-          PIDFile = "/run/dhcpd${postfix}/dhcpd.pid";
+  dhcpdService = postfix: cfg:
+    let
+      configFile =
+        if cfg.configFile != null
+          then cfg.configFile
+          else writeConfig cfg;
+      leaseFile = "/var/lib/dhcpd${postfix}/dhcpd.leases";
+      args = [
+        "@${pkgs.dhcp}/sbin/dhcpd" "dhcpd${postfix}" "-${postfix}"
+        "-pf" "/run/dhcpd${postfix}/dhcpd.pid"
+        "-cf" configFile
+        "-lf" leaseFile
+      ] ++ cfg.extraFlags
+        ++ cfg.interfaces;
+    in
+      optionalAttrs cfg.enable {
+        "dhcpd${postfix}" = {
+          description = "DHCPv${postfix} server";
+          wantedBy = [ "multi-user.target" ];
+          after = [ "network.target" ];
+
+          preStart = "touch ${leaseFile}";
+          serviceConfig = {
+            ExecStart = concatMapStringsSep " " escapeShellArg args;
+            Type = "forking";
+            Restart = "always";
+            DynamicUser = true;
+            User = "dhcpd";
+            Group = "dhcpd";
+            AmbientCapabilities = [
+              "CAP_NET_RAW"          # to send ICMP messages
+              "CAP_NET_BIND_SERVICE" # to bind on DHCP port (67)
+            ];
+            StateDirectory   = "dhcpd${postfix}";
+            RuntimeDirectory = "dhcpd${postfix}";
+            PIDFile = "/run/dhcpd${postfix}/dhcpd.pid";
+          };
         };
-    };
-  };
+      };
 
   machineOpts = { ... }: {
 
@@ -102,15 +109,6 @@ let
       '';
     };
 
-    stateDir = mkOption {
-      type = types.path;
-      # We use /var/lib/dhcp for DHCPv4 to save backwards compatibility.
-      default = "/var/lib/dhcp${if postfix == "4" then "" else postfix}";
-      description = ''
-        State directory for the DHCP server.
-      '';
-    };
-
     extraConfig = mkOption {
       type = types.lines;
       default = "";
@@ -194,7 +192,13 @@ in
 
   imports = [
     (mkRenamedOptionModule [ "services" "dhcpd" ] [ "services" "dhcpd4" ])
-  ];
+  ] ++ flip map [ "4" "6" ] (postfix:
+    mkRemovedOptionModule [ "services" "dhcpd${postfix}" "stateDir" ] ''
+      The DHCP server state directory is now managed with the systemd's DynamicUser mechanism.
+      This means the directory is named after the service (dhcpd${postfix}), created under
+      /var/lib/private/ and symlinked to /var/lib/.
+    ''
+  );
 
   ###### interface
 
@@ -210,15 +214,6 @@ in
 
   config = mkIf (cfg4.enable || cfg6.enable) {
 
-    users = {
-      users.dhcpd = {
-        isSystemUser = true;
-        group = "dhcpd";
-        description = "DHCP daemon user";
-      };
-      groups.dhcpd = {};
-    };
-
     systemd.services = dhcpdService "4" cfg4 // dhcpdService "6" cfg6;
 
   };
diff --git a/nixos/modules/services/networking/ergo.nix b/nixos/modules/services/networking/ergo.nix
index c52de30dc361e..6e55a7cfff6c8 100644
--- a/nixos/modules/services/networking/ergo.nix
+++ b/nixos/modules/services/networking/ergo.nix
@@ -1,9 +1,10 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 let
   cfg = config.services.ergo;
+  opt = options.services.ergo;
 
-  inherit (lib) mkEnableOption mkIf mkOption optionalString types;
+  inherit (lib) literalExpression mkEnableOption mkIf mkOption optionalString types;
 
   configFile = pkgs.writeText "ergo.conf" (''
 ergo {
@@ -92,6 +93,7 @@ in {
       group = mkOption {
         type = types.str;
         default = cfg.user;
+        defaultText = literalExpression "config.${opt.user}";
         description = "The group as which to run the Ergo node.";
       };
 
diff --git a/nixos/modules/services/networking/eternal-terminal.nix b/nixos/modules/services/networking/eternal-terminal.nix
index a2e5b30dc0f02..88b4cd90540f4 100644
--- a/nixos/modules/services/networking/eternal-terminal.nix
+++ b/nixos/modules/services/networking/eternal-terminal.nix
@@ -67,7 +67,7 @@ in
       eternal-terminal = {
         description = "Eternal Terminal server.";
         wantedBy = [ "multi-user.target" ];
-        after = [ "syslog.target" "network.target" ];
+        after = [ "network.target" ];
         serviceConfig = {
           Type = "forking";
           ExecStart = "${pkgs.eternal-terminal}/bin/etserver --daemon --cfgfile=${pkgs.writeText "et.cfg" ''
diff --git a/nixos/modules/services/networking/firewall.nix b/nixos/modules/services/networking/firewall.nix
index b5b46fe6042cd..ff023a888f268 100644
--- a/nixos/modules/services/networking/firewall.nix
+++ b/nixos/modules/services/networking/firewall.nix
@@ -421,6 +421,7 @@ in
       checkReversePath = mkOption {
         type = types.either types.bool (types.enum ["strict" "loose"]);
         default = kernelHasRPFilter;
+        defaultText = literalDocBook "<literal>true</literal> if supported by the chosen kernel";
         example = "loose";
         description =
           ''
diff --git a/nixos/modules/services/networking/freeradius.nix b/nixos/modules/services/networking/freeradius.nix
index f3fdd576b65c0..7fa3a8fa17fa7 100644
--- a/nixos/modules/services/networking/freeradius.nix
+++ b/nixos/modules/services/networking/freeradius.nix
@@ -28,6 +28,7 @@ let
         ProtectHome = "on";
         Restart = "on-failure";
         RestartSec = 2;
+        LogsDirectory = "radius";
     };
   };
 
@@ -73,6 +74,7 @@ in
       users.radius = {
         /*uid = config.ids.uids.radius;*/
         description = "Radius daemon user";
+        isSystemUser = true;
       };
     };
 
diff --git a/nixos/modules/services/networking/jibri/default.nix b/nixos/modules/services/networking/jibri/default.nix
index 96832b0eb552b..113a7aa4384ab 100644
--- a/nixos/modules/services/networking/jibri/default.nix
+++ b/nixos/modules/services/networking/jibri/default.nix
@@ -132,7 +132,7 @@ in
         pkgs.writeScript "finalize_recording.sh" ''''''
         #!/bin/sh
         RECORDINGS_DIR=$1
-        ${pkgs.rclone}/bin/rclone copy $RECORDINGS_DIR RCLONE_REMOTE:jibri-recordings/ -v --log-file=/var/log/jitsi/jibri/recording-upload.txt
+        ''${pkgs.rclone}/bin/rclone copy $RECORDINGS_DIR RCLONE_REMOTE:jibri-recordings/ -v --log-file=/var/log/jitsi/jibri/recording-upload.txt
         exit 0
         '''''';
       '';
diff --git a/nixos/modules/services/networking/jitsi-videobridge.nix b/nixos/modules/services/networking/jitsi-videobridge.nix
index dd06ad98a9730..abb0bd0a25e18 100644
--- a/nixos/modules/services/networking/jitsi-videobridge.nix
+++ b/nixos/modules/services/networking/jitsi-videobridge.nix
@@ -217,6 +217,8 @@ in
         "-Dnet.java.sip.communicator.SC_HOME_DIR_NAME" = "videobridge";
         "-Djava.util.logging.config.file" = "/etc/jitsi/videobridge/logging.properties";
         "-Dconfig.file" = pkgs.writeText "jvb.conf" (toHOCON jvbConfig);
+        # Mitigate CVE-2021-44228
+        "-Dlog4j2.formatMsgNoLookups" = true;
       } // (mapAttrs' (k: v: nameValuePair "-D${k}" v) cfg.extraProperties);
     in
     {
diff --git a/nixos/modules/services/networking/kea.nix b/nixos/modules/services/networking/kea.nix
index b11402204aec9..4da47f575f79f 100644
--- a/nixos/modules/services/networking/kea.nix
+++ b/nixos/modules/services/networking/kea.nix
@@ -236,6 +236,7 @@ in
 
       environment = {
         KEA_PIDFILE_DIR = "/run/kea";
+        KEA_LOCKFILE_DIR = "/run/kea";
       };
 
       restartTriggers = [
@@ -271,6 +272,7 @@ in
 
       environment = {
         KEA_PIDFILE_DIR = "/run/kea";
+        KEA_LOCKFILE_DIR = "/run/kea";
       };
 
       restartTriggers = [
@@ -313,6 +315,7 @@ in
 
       environment = {
         KEA_PIDFILE_DIR = "/run/kea";
+        KEA_LOCKFILE_DIR = "/run/kea";
       };
 
       restartTriggers = [
@@ -353,6 +356,7 @@ in
 
       environment = {
         KEA_PIDFILE_DIR = "/run/kea";
+        KEA_LOCKFILE_DIR = "/run/kea";
       };
 
       restartTriggers = [
@@ -361,7 +365,7 @@ in
 
       serviceConfig = {
         ExecStart = "${package}/bin/kea-dhcp-ddns -c /etc/kea/dhcp-ddns.conf ${lib.escapeShellArgs cfg.dhcp-ddns.extraArgs}";
-        AmbientCapabilites = [
+        AmbientCapabilities = [
           "CAP_NET_BIND_SERVICE"
         ];
         CapabilityBoundingSet = [
diff --git a/nixos/modules/services/networking/ntopng.nix b/nixos/modules/services/networking/ntopng.nix
index c152571171370..77a004e8ab3a5 100644
--- a/nixos/modules/services/networking/ntopng.nix
+++ b/nixos/modules/services/networking/ntopng.nix
@@ -1,10 +1,11 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
 
   cfg = config.services.ntopng;
+  opt = options.services.ntopng;
   redisCfg = config.services.redis;
 
   configFile = if cfg.configText != "" then
@@ -35,8 +36,8 @@ in
           collection tool.
 
           With the default configuration, ntopng monitors all network
-          interfaces and displays its findings at http://localhost:${toString
-          cfg.http-port}. Default username and password is admin/admin.
+          interfaces and displays its findings at http://localhost:''${toString
+          config.${opt.http-port}}. Default username and password is admin/admin.
 
           See the ntopng(8) manual page and http://www.ntop.org/products/ntop/
           for more info.
diff --git a/nixos/modules/services/networking/pleroma.nix b/nixos/modules/services/networking/pleroma.nix
index 2f32faf387ca6..9b8382392c0a7 100644
--- a/nixos/modules/services/networking/pleroma.nix
+++ b/nixos/modules/services/networking/pleroma.nix
@@ -100,6 +100,7 @@ in {
       after = [ "network-online.target" "postgresql.service" ];
       wantedBy = [ "multi-user.target" ];
       restartTriggers = [ config.environment.etc."/pleroma/config.exs".source ];
+      environment.RELEASE_COOKIE = "/var/lib/pleroma/.cookie";
       serviceConfig = {
         User = cfg.user;
         Group = cfg.group;
@@ -116,8 +117,14 @@ in {
         # has not been updated. But the no-op process is pretty fast.
         # Better be safe than sorry migration-wise.
         ExecStartPre =
-          let preScript = pkgs.writers.writeBashBin "pleromaStartPre"
-            "${cfg.package}/bin/pleroma_ctl migrate";
+          let preScript = pkgs.writers.writeBashBin "pleromaStartPre" ''
+            if [ ! -f /var/lib/pleroma/.cookie ]
+            then
+              echo "Creating cookie file"
+              dd if=/dev/urandom bs=1 count=16 | hexdump -e '16/1 "%02x"' > /var/lib/pleroma/.cookie
+            fi
+            ${cfg.package}/bin/pleroma_ctl migrate
+          '';
           in "${preScript}/bin/pleromaStartPre";
 
         ExecStart = "${cfg.package}/bin/pleroma start";
diff --git a/nixos/modules/services/networking/prosody.xml b/nixos/modules/services/networking/prosody.xml
index 471240cd14752..6358d744ff780 100644
--- a/nixos/modules/services/networking/prosody.xml
+++ b/nixos/modules/services/networking/prosody.xml
@@ -72,7 +72,7 @@ services.prosody = {
    a TLS certificate for the three endponits:
     <programlisting>
 security.acme = {
-  <link linkend="opt-security.acme.email">email</link> = "root@example.org";
+  <link linkend="opt-security.acme.defaults.email">email</link> = "root@example.org";
   <link linkend="opt-security.acme.acceptTerms">acceptTerms</link> = true;
   <link linkend="opt-security.acme.certs">certs</link> = {
     "example.org" = {
diff --git a/nixos/modules/services/networking/quassel.nix b/nixos/modules/services/networking/quassel.nix
index 22940ef7a13a8..844c9a6b8b352 100644
--- a/nixos/modules/services/networking/quassel.nix
+++ b/nixos/modules/services/networking/quassel.nix
@@ -1,9 +1,10 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
   cfg = config.services.quassel;
+  opt = options.services.quassel;
   quassel = cfg.package;
   user = if cfg.user != null then cfg.user else "quassel";
 in
@@ -63,6 +64,9 @@ in
 
       dataDir = mkOption {
         default = "/home/${user}/.config/quassel-irc.org";
+        defaultText = literalExpression ''
+          "/home/''${config.${opt.user}}/.config/quassel-irc.org"
+        '';
         type = types.str;
         description = ''
           The directory holding configuration files, the SQlite database and the SSL Cert.
diff --git a/nixos/modules/services/networking/quorum.nix b/nixos/modules/services/networking/quorum.nix
index 50148dc314da0..bddcd18c7fbe1 100644
--- a/nixos/modules/services/networking/quorum.nix
+++ b/nixos/modules/services/networking/quorum.nix
@@ -1,9 +1,10 @@
-{ config, pkgs, lib, ... }:
+{ config, options, pkgs, lib, ... }:
 let
 
   inherit (lib) mkEnableOption mkIf mkOption literalExpression types optionalString;
 
   cfg = config.services.quorum;
+  opt = options.services.quorum;
   dataDir = "/var/lib/quorum";
   genesisFile = pkgs.writeText "genesis.json" (builtins.toJSON cfg.genesis);
   staticNodesFile = pkgs.writeText "static-nodes.json" (builtins.toJSON cfg.staticNodes);
@@ -23,6 +24,7 @@ in {
       group = mkOption {
         type = types.str;
         default = cfg.user;
+        defaultText = literalExpression "config.${opt.user}";
         description = "The group as which to run quorum.";
       };
 
diff --git a/nixos/modules/services/networking/stubby.nix b/nixos/modules/services/networking/stubby.nix
index c5e0f929a1267..78c13798dde2a 100644
--- a/nixos/modules/services/networking/stubby.nix
+++ b/nixos/modules/services/networking/stubby.nix
@@ -1,180 +1,51 @@
-{ config, lib, pkgs, ...}:
+{ config, lib, pkgs, ... }:
 
 with lib;
 
 let
   cfg = config.services.stubby;
+  settingsFormat = pkgs.formats.yaml { };
+  confFile = settingsFormat.generate "stubby.yml" cfg.settings;
+in {
+  imports = map (x:
+    (mkRemovedOptionModule [ "services" "stubby" x ]
+      "Stubby configuration moved to services.stubby.settings.")) [
+        "authenticationMode"
+        "fallbackProtocols"
+        "idleTimeout"
+        "listenAddresses"
+        "queryPaddingBlocksize"
+        "roundRobinUpstreams"
+        "subnetPrivate"
+        "upstreamServers"
+      ];
 
-  fallbacks = concatMapStringsSep "\n  " (x: "- ${x}") cfg.fallbackProtocols;
-  listeners = concatMapStringsSep "\n  " (x: "- ${x}") cfg.listenAddresses;
-
-  # By default, the recursive resolvers maintained by the getdns
-  # project itself are enabled. More information about both getdns's servers,
-  # as well as third party options for upstream resolvers, can be found here:
-  # https://dnsprivacy.org/wiki/display/DP/DNS+Privacy+Test+Servers
-  #
-  # You can override these values by supplying a yaml-formatted array of your
-  # preferred upstream resolvers in the following format:
-  #
-  # 106 # - address_data: IPv4 or IPv6 address of the upstream
-  #   port: Port for UDP/TCP (default is 53)
-  #   tls_auth_name: Authentication domain name checked against the server
-  #                  certificate
-  #   tls_pubkey_pinset: An SPKI pinset verified against the keys in the server
-  #                      certificate
-  #     - digest: Only "sha256" is currently supported
-  #       value: Base64 encoded value of the sha256 fingerprint of the public
-  #              key
-  #   tls_port: Port for TLS (default is 853)
-
-  defaultUpstream = ''
-    - address_data: 145.100.185.15
-      tls_auth_name: "dnsovertls.sinodun.com"
-      tls_pubkey_pinset:
-        - digest: "sha256"
-          value: 62lKu9HsDVbyiPenApnc4sfmSYTHOVfFgL3pyB+cBL4=
-    - address_data: 145.100.185.16
-      tls_auth_name: "dnsovertls1.sinodun.com"
-      tls_pubkey_pinset:
-        - digest: "sha256"
-          value: cE2ecALeE5B+urJhDrJlVFmf38cJLAvqekONvjvpqUA=
-    - address_data: 185.49.141.37
-      tls_auth_name: "getdnsapi.net"
-      tls_pubkey_pinset:
-        - digest: "sha256"
-          value: foxZRnIh9gZpWnl+zEiKa0EJ2rdCGroMWm02gaxSc9Q=
-    - address_data: 2001:610:1:40ba:145:100:185:15
-      tls_auth_name: "dnsovertls.sinodun.com"
-      tls_pubkey_pinset:
-        - digest: "sha256"
-          value: 62lKu9HsDVbyiPenApnc4sfmSYTHOVfFgL3pyB+cBL4=
-    - address_data: 2001:610:1:40ba:145:100:185:16
-      tls_auth_name: "dnsovertls1.sinodun.com"
-      tls_pubkey_pinset:
-        - digest: "sha256"
-          value: cE2ecALeE5B+urJhDrJlVFmf38cJLAvqekONvjvpqUA=
-    - address_data: 2a04:b900:0:100::38
-      tls_auth_name: "getdnsapi.net"
-      tls_pubkey_pinset:
-        - digest: "sha256"
-          value: foxZRnIh9gZpWnl+zEiKa0EJ2rdCGroMWm02gaxSc9Q=
-  '';
-
-  # Resolution type is not changeable here because it is required per the
-  # stubby documentation:
-  #
-  # "resolution_type: Work in stub mode only (not recursive mode) - required for Stubby
-  # operation."
-  #
-  # https://dnsprivacy.org/wiki/display/DP/Configuring+Stubby
-
-  confFile = pkgs.writeText "stubby.yml" ''
-    resolution_type: GETDNS_RESOLUTION_STUB
-    dns_transport_list:
-      ${fallbacks}
-    appdata_dir: "/var/cache/stubby"
-    tls_authentication: ${cfg.authenticationMode}
-    tls_query_padding_blocksize: ${toString cfg.queryPaddingBlocksize}
-    edns_client_subnet_private: ${if cfg.subnetPrivate then "1" else "0"}
-    idle_timeout: ${toString cfg.idleTimeout}
-    listen_addresses:
-      ${listeners}
-    round_robin_upstreams: ${if cfg.roundRobinUpstreams then "1" else "0"}
-    ${cfg.extraConfig}
-    upstream_recursive_servers:
-    ${cfg.upstreamServers}
-  '';
-in
-
-{
   options = {
     services.stubby = {
 
       enable = mkEnableOption "Stubby DNS resolver";
 
-      fallbackProtocols = mkOption {
-        default = [ "GETDNS_TRANSPORT_TLS" ];
-        type = with types; listOf (enum [
-          "GETDNS_TRANSPORT_TLS"
-          "GETDNS_TRANSPORT_TCP"
-          "GETDNS_TRANSPORT_UDP"
-        ]);
-        description = ''
-          Ordered list composed of one or more transport protocols.
-          Strict mode should only use <literal>GETDNS_TRANSPORT_TLS</literal>.
-          Other options are <literal>GETDNS_TRANSPORT_UDP</literal> and
-          <literal>GETDNS_TRANSPORT_TCP</literal>.
+      settings = mkOption {
+        type = types.attrsOf settingsFormat.type;
+        example = lib.literalExpression ''
+          pkgs.stubby.passthru.settingsExample // {
+            upstream_recursive_servers = [{
+              address_data = "158.64.1.29";
+              tls_auth_name = "kaitain.restena.lu";
+              tls_pubkey_pinset = [{
+                digest = "sha256";
+                value = "7ftvIkA+UeN/ktVkovd/7rPZ6mbkhVI7/8HnFJIiLa4=";
+              }];
+            }];
+          };
         '';
-      };
-
-      authenticationMode = mkOption {
-        default = "GETDNS_AUTHENTICATION_REQUIRED";
-        type = types.enum [
-          "GETDNS_AUTHENTICATION_REQUIRED"
-          "GETDNS_AUTHENTICATION_NONE"
-        ];
         description = ''
-          Selects the Strict or Opportunistic usage profile.
-          For strict, set to <literal>GETDNS_AUTHENTICATION_REQUIRED</literal>.
-          for opportunistic, use <literal>GETDNS_AUTHENTICATION_NONE</literal>.
-        '';
-      };
-
-      queryPaddingBlocksize = mkOption {
-        default = 128;
-        type = types.int;
-        description = ''
-          EDNS0 option to pad the size of the DNS query to the given blocksize.
-        '';
-      };
-
-      subnetPrivate = mkOption {
-        default = true;
-        type = types.bool;
-        description = ''
-          EDNS0 option for ECS client privacy. Default is
-          <literal>true</literal>. If set, this option prevents the client
-          subnet from being sent to authoritative nameservers.
-        '';
-      };
-
-      idleTimeout = mkOption {
-        default = 10000;
-        type = types.int;
-        description = "EDNS0 option for keepalive idle timeout expressed in
-        milliseconds.";
-      };
-
-      listenAddresses = mkOption {
-        default = [ "127.0.0.1" "0::1" ];
-        type = with types; listOf str;
-        description = ''
-          Sets the listen address for the stubby daemon.
-          Uses port 53 by default.
-          Ise IP@port to specify a different port.
-        '';
-      };
-
-      roundRobinUpstreams = mkOption {
-        default = true;
-        type = types.bool;
-        description = ''
-          Instructs stubby to distribute queries across all available name
-          servers. Default is <literal>true</literal>. Set to
-          <literal>false</literal> in order to use the first available.
-        '';
-      };
-
-      upstreamServers = mkOption {
-        default = defaultUpstream;
-        type = types.lines;
-        description = ''
-          Replace default upstreams. See <citerefentry><refentrytitle>stubby
-          </refentrytitle><manvolnum>1</manvolnum></citerefentry> for an
-          example of the entry formatting. In Strict mode, at least one of the
-          following settings must be supplied for each nameserver:
-          <literal>tls_auth_name</literal> or
-          <literal>tls_pubkey_pinset</literal>.
+          Content of the Stubby configuration file. All Stubby settings may be set or queried
+          here. The default settings are available at
+          <literal>pkgs.stubby.passthru.settingsExample</literal>. See
+          <link xlink:href="https://dnsprivacy.org/wiki/display/DP/Configuring+Stubby"/>.
+          A list of the public recursive servers can be found here:
+          <link xlink:href="https://dnsprivacy.org/wiki/display/DP/DNS+Privacy+Test+Servers"/>.
         '';
       };
 
@@ -184,20 +55,21 @@ in
         description = "Enable or disable debug level logging.";
       };
 
-      extraConfig = mkOption {
-        default = "";
-        type = types.lines;
-        description = ''
-          Add additional configuration options. see <citerefentry>
-          <refentrytitle>stubby</refentrytitle><manvolnum>1</manvolnum>
-          </citerefentry>for more options.
-        '';
-      };
     };
   };
 
   config = mkIf cfg.enable {
-    environment.systemPackages = [ pkgs.stubby ];
+    assertions = [{
+      assertion =
+        (cfg.settings.resolution_type or "") == "GETDNS_RESOLUTION_STUB";
+      message = ''
+        services.stubby.settings.resolution_type must be set to "GETDNS_RESOLUTION_STUB".
+        Is services.stubby.settings unset?
+      '';
+    }];
+
+    services.stubby.settings.appdata_dir = "/var/cache/stubby";
+
     systemd.services.stubby = {
       description = "Stubby local DNS resolver";
       after = [ "network.target" ];
diff --git a/nixos/modules/services/networking/syncthing.nix b/nixos/modules/services/networking/syncthing.nix
index 8c44687a38224..e37e324019e81 100644
--- a/nixos/modules/services/networking/syncthing.nix
+++ b/nixos/modules/services/networking/syncthing.nix
@@ -1,9 +1,10 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
   cfg = config.services.syncthing;
+  opt = options.services.syncthing;
   defaultUser = "syncthing";
   defaultGroup = defaultUser;
 
@@ -431,7 +432,26 @@ in {
           The path where the settings and keys will exist.
         '';
         default = cfg.dataDir + optionalString cond "/.config/syncthing";
-        defaultText = literalExpression "dataDir${optionalString cond " + \"/.config/syncthing\""}";
+        defaultText = literalDocBook ''
+          <variablelist>
+            <varlistentry>
+              <term><literal>stateVersion >= 19.03</literal></term>
+              <listitem>
+                <programlisting>
+                  config.${opt.dataDir} + "/.config/syncthing"
+                </programlisting>
+              </listitem>
+            </varlistentry>
+            <varlistentry>
+              <term>otherwise</term>
+              <listitem>
+                <programlisting>
+                  config.${opt.dataDir}
+                </programlisting>
+              </listitem>
+            </varlistentry>
+          </variablelist>
+        '';
       };
 
       extraFlags = mkOption {
diff --git a/nixos/modules/services/networking/unifi.nix b/nixos/modules/services/networking/unifi.nix
index 53ad4df477fcc..a683c537f05b2 100644
--- a/nixos/modules/services/networking/unifi.nix
+++ b/nixos/modules/services/networking/unifi.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, utils, ... }:
+{ config, options, lib, pkgs, utils, ... }:
 with lib;
 let
   cfg = config.services.unifi;
@@ -49,7 +49,7 @@ in
       '';
     };
 
-    services.unifi.openPorts = mkOption {
+    services.unifi.openFirewall = mkOption {
       type = types.bool;
       default = true;
       description = ''
@@ -85,6 +85,10 @@ in
 
   config = mkIf cfg.enable {
 
+    warnings = optional
+      (options.services.unifi.openFirewall.highestPrio >= (mkOptionDefault null).priority)
+      "The current services.unifi.openFirewall = true default is deprecated and will change to false in 22.11. Set it explicitly to silence this warning.";
+
     users.users.unifi = {
       isSystemUser = true;
       group = "unifi";
@@ -93,7 +97,7 @@ in
     };
     users.groups.unifi = {};
 
-    networking.firewall = mkIf cfg.openPorts {
+    networking.firewall = mkIf cfg.openFirewall {
       # https://help.ubnt.com/hc/en-us/articles/218506997
       allowedTCPPorts = [
         8080  # Port for UAP to inform controller.
@@ -191,6 +195,7 @@ in
   };
   imports = [
     (mkRemovedOptionModule [ "services" "unifi" "dataDir" ] "You should move contents of dataDir to /var/lib/unifi/data" )
+    (mkRenamedOptionModule [ "services" "unifi" "openPorts" ] [ "services" "unifi" "openFirewall" ])
   ];
 
   meta.maintainers = with lib.maintainers; [ erictapen pennae ];
diff --git a/nixos/modules/services/networking/wasabibackend.nix b/nixos/modules/services/networking/wasabibackend.nix
index 8482823e197f7..b6dcd940915a9 100644
--- a/nixos/modules/services/networking/wasabibackend.nix
+++ b/nixos/modules/services/networking/wasabibackend.nix
@@ -1,9 +1,10 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 let
   cfg = config.services.wasabibackend;
+  opt = options.services.wasabibackend;
 
-  inherit (lib) mkEnableOption mkIf mkOption optionalAttrs optionalString types;
+  inherit (lib) literalExpression mkEnableOption mkIf mkOption optionalAttrs optionalString types;
 
   confOptions = {
       BitcoinRpcConnectionString = "${cfg.rpc.user}:${cfg.rpc.password}";
@@ -103,6 +104,7 @@ in {
       group = mkOption {
         type = types.str;
         default = cfg.user;
+        defaultText = literalExpression "config.${opt.user}";
         description = "The group as which to run the wasabibackend node.";
       };
     };
diff --git a/nixos/modules/services/networking/wireguard.nix b/nixos/modules/services/networking/wireguard.nix
index 55b84935b6cb5..7cd44b2f8a0a8 100644
--- a/nixos/modules/services/networking/wireguard.nix
+++ b/nixos/modules/services/networking/wireguard.nix
@@ -1,10 +1,11 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
 
   cfg = config.networking.wireguard;
+  opt = options.networking.wireguard;
 
   kernel = config.boot.kernelPackages;
 
@@ -438,6 +439,7 @@ in
         type = types.bool;
         # 2019-05-25: Backwards compatibility.
         default = cfg.interfaces != {};
+        defaultText = literalExpression "config.${opt.interfaces} != { }";
         example = true;
       };
 
diff --git a/nixos/modules/services/networking/wpa_supplicant.nix b/nixos/modules/services/networking/wpa_supplicant.nix
index 4aa350d21a2ba..07dec8ea71815 100644
--- a/nixos/modules/services/networking/wpa_supplicant.nix
+++ b/nixos/modules/services/networking/wpa_supplicant.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, utils, ... }:
+{ config, lib, options, pkgs, utils, ... }:
 
 with lib;
 
@@ -8,6 +8,7 @@ let
     else pkgs.wpa_supplicant;
 
   cfg = config.networking.wireless;
+  opt = options.networking.wireless;
 
   # Content of wpa_supplicant.conf
   generatedConfig = concatStringsSep "\n" (
@@ -421,6 +422,7 @@ in {
       dbusControlled = mkOption {
         type = types.bool;
         default = lib.length cfg.interfaces < 2;
+        defaultText = literalExpression "length config.${opt.interfaces} < 2";
         description = ''
           Whether to enable the DBus control interface.
           This is only needed when using NetworkManager or connman.
diff --git a/nixos/modules/services/networking/xrdp.nix b/nixos/modules/services/networking/xrdp.nix
index c4f828f3c5a6b..e9f123a181aec 100644
--- a/nixos/modules/services/networking/xrdp.nix
+++ b/nixos/modules/services/networking/xrdp.nix
@@ -97,6 +97,11 @@ in
         '';
       };
 
+      confDir = mkOption {
+        type = types.path;
+        default = confDir;
+        description = "The location of the config files for xrdp.";
+      };
     };
   };
 
@@ -149,7 +154,7 @@ in
           User = "xrdp";
           Group = "xrdp";
           PermissionsStartOnly = true;
-          ExecStart = "${cfg.package}/bin/xrdp --nodaemon --port ${toString cfg.port} --config ${confDir}/xrdp.ini";
+          ExecStart = "${cfg.package}/bin/xrdp --nodaemon --port ${toString cfg.port} --config ${cfg.confDir}/xrdp.ini";
         };
       };
 
@@ -159,7 +164,7 @@ in
         description = "xrdp session manager";
         restartIfChanged = false; # do not restart on "nixos-rebuild switch". like "display-manager", it can have many interactive programs as children
         serviceConfig = {
-          ExecStart = "${cfg.package}/bin/xrdp-sesman --nodaemon --config ${confDir}/sesman.ini";
+          ExecStart = "${cfg.package}/bin/xrdp-sesman --nodaemon --config ${cfg.confDir}/sesman.ini";
           ExecStop  = "${pkgs.coreutils}/bin/kill -INT $MAINPID";
         };
       };
diff --git a/nixos/modules/services/search/kibana.nix b/nixos/modules/services/search/kibana.nix
index 381f5156ceb6d..e4ab85be9ef15 100644
--- a/nixos/modules/services/search/kibana.nix
+++ b/nixos/modules/services/search/kibana.nix
@@ -1,9 +1,10 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
   cfg = config.services.kibana;
+  opt = options.services.kibana;
 
   ge7 = builtins.compareVersions cfg.package.version "7" >= 0;
   lt6_6 = builtins.compareVersions cfg.package.version "6.6" < 0;
@@ -130,6 +131,9 @@ in {
           This defaults to the singleton list [ca] when the <option>ca</option> option is defined.
         '';
         default = if cfg.elasticsearch.ca == null then [] else [ca];
+        defaultText = literalExpression ''
+          if config.${opt.elasticsearch.ca} == null then [ ] else [ ca ]
+        '';
         type = types.listOf types.path;
       };
 
diff --git a/nixos/modules/services/security/aesmd.nix b/nixos/modules/services/security/aesmd.nix
new file mode 100644
index 0000000000000..bb53bc49e259e
--- /dev/null
+++ b/nixos/modules/services/security/aesmd.nix
@@ -0,0 +1,227 @@
+{ config, pkgs, lib, ... }:
+with lib;
+let
+  cfg = config.services.aesmd;
+
+  sgx-psw = pkgs.sgx-psw.override { inherit (cfg) debug; };
+
+  configFile = with cfg.settings; pkgs.writeText "aesmd.conf" (
+    concatStringsSep "\n" (
+      optional (whitelistUrl != null) "whitelist url = ${whitelistUrl}" ++
+      optional (proxy != null) "aesm proxy = ${proxy}" ++
+      optional (proxyType != null) "proxy type = ${proxyType}" ++
+      optional (defaultQuotingType != null) "default quoting type = ${defaultQuotingType}" ++
+      # Newline at end of file
+      [ "" ]
+    )
+  );
+in
+{
+  options.services.aesmd = {
+    enable = mkEnableOption "Intel's Architectural Enclave Service Manager (AESM) for Intel SGX";
+    debug = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Whether to build the PSW package in debug mode.";
+    };
+    settings = mkOption {
+      description = "AESM configuration";
+      default = { };
+      type = types.submodule {
+        options.whitelistUrl = mkOption {
+          type = with types; nullOr str;
+          default = null;
+          example = "http://whitelist.trustedservices.intel.com/SGX/LCWL/Linux/sgx_white_list_cert.bin";
+          description = "URL to retrieve authorized Intel SGX enclave signers.";
+        };
+        options.proxy = mkOption {
+          type = with types; nullOr str;
+          default = null;
+          example = "http://proxy_url:1234";
+          description = "HTTP network proxy.";
+        };
+        options.proxyType = mkOption {
+          type = with types; nullOr (enum [ "default" "direct" "manual" ]);
+          default = if (cfg.settings.proxy != null) then "manual" else null;
+          example = "default";
+          description = ''
+            Type of proxy to use. The <literal>default</literal> uses the system's default proxy.
+            If <literal>direct</literal> is given, uses no proxy.
+            A value of <literal>manual</literal> uses the proxy from
+            <option>services.aesmd.settings.proxy</option>.
+          '';
+        };
+        options.defaultQuotingType = mkOption {
+          type = with types; nullOr (enum [ "ecdsa_256" "epid_linkable" "epid_unlinkable" ]);
+          default = null;
+          example = "ecdsa_256";
+          description = "Attestation quote type.";
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [{
+      assertion = !(config.boot.specialFileSystems."/dev".options ? "noexec");
+      message = "SGX requires exec permission for /dev";
+    }];
+
+    hardware.cpu.intel.sgx.provision.enable = true;
+
+    systemd.services.aesmd =
+      let
+        storeAesmFolder = "${sgx-psw}/aesm";
+        # Hardcoded path AESM_DATA_FOLDER in psw/ae/aesm_service/source/oal/linux/aesm_util.cpp
+        aesmDataFolder = "/var/opt/aesmd/data";
+        aesmStateDirSystemd = "%S/aesmd";
+      in
+      {
+        description = "Intel Architectural Enclave Service Manager";
+        wantedBy = [ "multi-user.target" ];
+
+        after = [
+          "auditd.service"
+          "network.target"
+          "syslog.target"
+        ];
+
+        environment = {
+          NAME = "aesm_service";
+          AESM_PATH = storeAesmFolder;
+          LD_LIBRARY_PATH = storeAesmFolder;
+        };
+
+        # Make sure any of the SGX application enclave devices is available
+        unitConfig.AssertPathExists = [
+          # legacy out-of-tree driver
+          "|/dev/isgx"
+          # DCAP driver
+          "|/dev/sgx/enclave"
+          # in-tree driver
+          "|/dev/sgx_enclave"
+        ];
+
+        serviceConfig = rec {
+          ExecStartPre = pkgs.writeShellScript "copy-aesmd-data-files.sh" ''
+            set -euo pipefail
+            whiteListFile="${aesmDataFolder}/white_list_cert_to_be_verify.bin"
+            if [[ ! -f "$whiteListFile" ]]; then
+              ${pkgs.coreutils}/bin/install -m 644 -D \
+                "${storeAesmFolder}/data/white_list_cert_to_be_verify.bin" \
+                "$whiteListFile"
+            fi
+          '';
+          ExecStart = "${sgx-psw}/bin/aesm_service --no-daemon";
+          ExecReload = ''${pkgs.coreutils}/bin/kill -SIGHUP "$MAINPID"'';
+
+          Restart = "on-failure";
+          RestartSec = "15s";
+
+          DynamicUser = true;
+          Group = "sgx";
+          SupplementaryGroups = [
+            config.hardware.cpu.intel.sgx.provision.group
+          ];
+
+          Type = "simple";
+
+          WorkingDirectory = storeAesmFolder;
+          StateDirectory = "aesmd";
+          StateDirectoryMode = "0700";
+          RuntimeDirectory = "aesmd";
+          RuntimeDirectoryMode = "0750";
+
+          # Hardening
+
+          # chroot into the runtime directory
+          RootDirectory = "%t/aesmd";
+          BindReadOnlyPaths = [
+            builtins.storeDir
+            # Hardcoded path AESM_CONFIG_FILE in psw/ae/aesm_service/source/utils/aesm_config.cpp
+            "${configFile}:/etc/aesmd.conf"
+          ];
+          BindPaths = [
+            # Hardcoded path CONFIG_SOCKET_PATH in psw/ae/aesm_service/source/core/ipc/SocketConfig.h
+            "%t/aesmd:/var/run/aesmd"
+            "%S/aesmd:/var/opt/aesmd"
+          ];
+
+          # PrivateDevices=true will mount /dev noexec which breaks AESM
+          PrivateDevices = false;
+          DevicePolicy = "closed";
+          DeviceAllow = [
+            # legacy out-of-tree driver
+            "/dev/isgx rw"
+            # DCAP driver
+            "/dev/sgx rw"
+            # in-tree driver
+            "/dev/sgx_enclave rw"
+            "/dev/sgx_provision rw"
+          ];
+
+          # Requires Internet access for attestation
+          PrivateNetwork = false;
+
+          RestrictAddressFamilies = [
+            # Allocates the socket /var/run/aesmd/aesm.socket
+            "AF_UNIX"
+            # Uses the HTTP protocol to initialize some services
+            "AF_INET"
+            "AF_INET6"
+          ];
+
+          # True breaks stuff
+          MemoryDenyWriteExecute = false;
+
+          # needs the ipc syscall in order to run
+          SystemCallFilter = [
+            "@system-service"
+            "~@aio"
+            "~@chown"
+            "~@clock"
+            "~@cpu-emulation"
+            "~@debug"
+            "~@keyring"
+            "~@memlock"
+            "~@module"
+            "~@mount"
+            "~@privileged"
+            "~@raw-io"
+            "~@reboot"
+            "~@resources"
+            "~@setuid"
+            "~@swap"
+            "~@sync"
+            "~@timer"
+          ];
+          SystemCallArchitectures = "native";
+          SystemCallErrorNumber = "EPERM";
+
+          CapabilityBoundingSet = "";
+          KeyringMode = "private";
+          LockPersonality = true;
+          NoNewPrivileges = true;
+          NotifyAccess = "none";
+          PrivateMounts = true;
+          PrivateTmp = true;
+          PrivateUsers = true;
+          ProcSubset = "pid";
+          ProtectClock = true;
+          ProtectControlGroups = true;
+          ProtectHome = true;
+          ProtectHostname = true;
+          ProtectKernelLogs = true;
+          ProtectKernelModules = true;
+          ProtectKernelTunables = true;
+          ProtectProc = "invisible";
+          ProtectSystem = "strict";
+          RemoveIPC = true;
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          RestrictSUIDSGID = true;
+          UMask = "0066";
+        };
+      };
+  };
+}
diff --git a/nixos/modules/services/security/privacyidea.nix b/nixos/modules/services/security/privacyidea.nix
index 05f4995cc4163..b8e2d9a8b0dfc 100644
--- a/nixos/modules/services/security/privacyidea.nix
+++ b/nixos/modules/services/security/privacyidea.nix
@@ -1,9 +1,10 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
   cfg = config.services.privacyidea;
+  opt = options.services.privacyidea;
 
   uwsgi = pkgs.uwsgi.override { plugins = [ "python3" ]; };
   python = uwsgi.python3;
@@ -112,6 +113,7 @@ in
       encFile = mkOption {
         type = types.str;
         default = "${cfg.stateDir}/enckey";
+        defaultText = literalExpression ''"''${config.${opt.stateDir}}/enckey"'';
         description = ''
           This is used to encrypt the token data and token passwords
         '';
@@ -120,6 +122,7 @@ in
       auditKeyPrivate = mkOption {
         type = types.str;
         default = "${cfg.stateDir}/private.pem";
+        defaultText = literalExpression ''"''${config.${opt.stateDir}}/private.pem"'';
         description = ''
           Private Key for signing the audit log.
         '';
@@ -128,6 +131,7 @@ in
       auditKeyPublic = mkOption {
         type = types.str;
         default = "${cfg.stateDir}/public.pem";
+        defaultText = literalExpression ''"''${config.${opt.stateDir}}/public.pem"'';
         description = ''
           Public key for checking signatures of the audit log.
         '';
@@ -200,6 +204,7 @@ in
       systemd.services.privacyidea = let
         piuwsgi = pkgs.writeText "uwsgi.json" (builtins.toJSON {
           uwsgi = {
+            buffer-size = 8192;
             plugins = [ "python3" ];
             pythonpath = "${penv}/${uwsgi.python3.sitePackages}";
             socket = "/run/privacyidea/socket";
diff --git a/nixos/modules/services/security/tor.nix b/nixos/modules/services/security/tor.nix
index c3e3248ee8ab1..f3ed1d160eed9 100644
--- a/nixos/modules/services/security/tor.nix
+++ b/nixos/modules/services/security/tor.nix
@@ -1,10 +1,11 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with builtins;
 with lib;
 
 let
   cfg = config.services.tor;
+  opt = options.services.tor;
   stateDir = "/var/lib/tor";
   runDir = "/run/tor";
   descriptionGeneric = option: ''
@@ -799,6 +800,11 @@ in
           options.SOCKSPort = mkOption {
             description = descriptionGeneric "SOCKSPort";
             default = if cfg.settings.HiddenServiceNonAnonymousMode == true then [{port = 0;}] else [];
+            defaultText = literalExpression ''
+              if config.${opt.settings}.HiddenServiceNonAnonymousMode == true
+              then [ { port = 0; } ]
+              else [ ]
+            '';
             example = [{port = 9090;}];
             type = types.listOf (optionSOCKSPort true);
           };
diff --git a/nixos/modules/services/security/vault.nix b/nixos/modules/services/security/vault.nix
index b0ade62d97c9b..d48bc472cb826 100644
--- a/nixos/modules/services/security/vault.nix
+++ b/nixos/modules/services/security/vault.nix
@@ -1,9 +1,10 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
   cfg = config.services.vault;
+  opt = options.services.vault;
 
   configFile = pkgs.writeText "vault.hcl" ''
     listener "tcp" {
@@ -83,6 +84,11 @@ in
       storagePath = mkOption {
         type = types.nullOr types.path;
         default = if cfg.storageBackend == "file" then "/var/lib/vault" else null;
+        defaultText = literalExpression ''
+          if config.${opt.storageBackend} == "file"
+          then "/var/lib/vault"
+          else null
+        '';
         description = "Data directory for file backend";
       };
 
diff --git a/nixos/modules/services/torrent/peerflix.nix b/nixos/modules/services/torrent/peerflix.nix
index 3e5f80960dc7a..821c829f6b4af 100644
--- a/nixos/modules/services/torrent/peerflix.nix
+++ b/nixos/modules/services/torrent/peerflix.nix
@@ -1,9 +1,10 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
   cfg = config.services.peerflix;
+  opt = options.services.peerflix;
 
   configFile = pkgs.writeText "peerflix-config.json" ''
     {
@@ -32,6 +33,7 @@ in {
     downloadDir = mkOption {
       description = "Peerflix temporary download directory.";
       default = "${cfg.stateDir}/torrents";
+      defaultText = literalExpression ''"''${config.${opt.stateDir}}/torrents"'';
       type = types.path;
     };
   };
diff --git a/nixos/modules/services/torrent/rtorrent.nix b/nixos/modules/services/torrent/rtorrent.nix
index dd7df623c7391..759dcfe2e6c50 100644
--- a/nixos/modules/services/torrent/rtorrent.nix
+++ b/nixos/modules/services/torrent/rtorrent.nix
@@ -1,10 +1,11 @@
-{ config, pkgs, lib, ... }:
+{ config, options, pkgs, lib, ... }:
 
 with lib;
 
 let
 
   cfg = config.services.rtorrent;
+  opt = options.services.rtorrent;
 
 in {
   options.services.rtorrent = {
@@ -21,6 +22,7 @@ in {
     downloadDir = mkOption {
       type = types.str;
       default = "${cfg.dataDir}/download";
+      defaultText = literalExpression ''"''${config.${opt.dataDir}}/download"'';
       description = ''
         Where to put downloaded files.
       '';
diff --git a/nixos/modules/services/torrent/transmission.nix b/nixos/modules/services/torrent/transmission.nix
index b8b38f6ba93c7..d12d8aa239802 100644
--- a/nixos/modules/services/torrent/transmission.nix
+++ b/nixos/modules/services/torrent/transmission.nix
@@ -4,6 +4,7 @@ with lib;
 
 let
   cfg = config.services.transmission;
+  opt = options.services.transmission;
   inherit (config.environment) etc;
   apparmor = config.security.apparmor;
   rootDir = "/run/transmission";
@@ -47,11 +48,13 @@ in
           options.download-dir = mkOption {
             type = types.path;
             default = "${cfg.home}/${downloadsDir}";
+            defaultText = literalExpression ''"''${config.${opt.home}}/${downloadsDir}"'';
             description = "Directory where to download torrents.";
           };
           options.incomplete-dir = mkOption {
             type = types.path;
             default = "${cfg.home}/${incompleteDir}";
+            defaultText = literalExpression ''"''${config.${opt.home}}/${incompleteDir}"'';
             description = ''
               When enabled with
               services.transmission.home
@@ -147,6 +150,7 @@ in
           options.watch-dir = mkOption {
             type = types.path;
             default = "${cfg.home}/${watchDir}";
+            defaultText = literalExpression ''"''${config.${opt.home}}/${watchDir}"'';
             description = "Watch a directory for torrent files and add them to transmission.";
           };
           options.watch-dir-enabled = mkOption {
@@ -167,13 +171,15 @@ in
       };
 
       downloadDirPermissions = mkOption {
-        type = types.str;
-        default = "770";
-        example = "775";
+        type = with types; nullOr str;
+        default = null;
+        example = "770";
         description = ''
-          The permissions set by <literal>systemd.activationScripts.transmission-daemon</literal>
-          on the directories <xref linkend="opt-services.transmission.settings.download-dir"/>
-          and <xref linkend="opt-services.transmission.settings.incomplete-dir"/>.
+          If not <code>null</code>, is used as the permissions
+          set by <literal>systemd.activationScripts.transmission-daemon</literal>
+          on the directories <xref linkend="opt-services.transmission.settings.download-dir"/>,
+          <xref linkend="opt-services.transmission.settings.incomplete-dir"/>.
+          and <xref linkend="opt-services.transmission.settings.watch-dir"/>.
           Note that you may also want to change
           <xref linkend="opt-services.transmission.settings.umask"/>.
         '';
@@ -246,15 +252,17 @@ in
     # when /home/foo is not owned by cfg.user.
     # Note also that using an ExecStartPre= wouldn't work either
     # because BindPaths= needs these directories before.
-    system.activationScripts.transmission-daemon = ''
-      install -d -m 700 '${cfg.home}/${settingsDir}'
-      chown -R '${cfg.user}:${cfg.group}' ${cfg.home}/${settingsDir}
-      install -d -m '${cfg.downloadDirPermissions}' -o '${cfg.user}' -g '${cfg.group}' '${cfg.settings.download-dir}'
-      '' + optionalString cfg.settings.incomplete-dir-enabled ''
-      install -d -m '${cfg.downloadDirPermissions}' -o '${cfg.user}' -g '${cfg.group}' '${cfg.settings.incomplete-dir}'
-      '' + optionalString cfg.settings.watch-dir-enabled ''
-      install -d -m '${cfg.downloadDirPermissions}' -o '${cfg.user}' -g '${cfg.group}' '${cfg.settings.watch-dir}'
-      '';
+    system.activationScripts = mkIf (cfg.downloadDirPermissions != null)
+      { transmission-daemon = ''
+        install -d -m 700 '${cfg.home}/${settingsDir}'
+        chown -R '${cfg.user}:${cfg.group}' ${cfg.home}/${settingsDir}
+        install -d -m '${cfg.downloadDirPermissions}' -o '${cfg.user}' -g '${cfg.group}' '${cfg.settings.download-dir}'
+        '' + optionalString cfg.settings.incomplete-dir-enabled ''
+        install -d -m '${cfg.downloadDirPermissions}' -o '${cfg.user}' -g '${cfg.group}' '${cfg.settings.incomplete-dir}'
+        '' + optionalString cfg.settings.watch-dir-enabled ''
+        install -d -m '${cfg.downloadDirPermissions}' -o '${cfg.user}' -g '${cfg.group}' '${cfg.settings.watch-dir}'
+        '';
+      };
 
     systemd.services.transmission = {
       description = "Transmission BitTorrent Service";
@@ -313,6 +321,14 @@ in
             cfg.settings.script-torrent-done-filename ++
           optional (cfg.settings.watch-dir-enabled && !cfg.settings.trash-original-torrent-files)
             cfg.settings.watch-dir;
+        StateDirectory = [
+          "transmission"
+          "transmission/.config/transmission-daemon"
+          "transmission/.incomplete"
+          "transmission/Downloads"
+          "transmission/watch-dir"
+        ];
+        StateDirectoryMode = mkDefault 750;
         # The following options are only for optimizing:
         # systemd-analyze security transmission
         AmbientCapabilities = "";
diff --git a/nixos/modules/services/video/epgstation/default.nix b/nixos/modules/services/video/epgstation/default.nix
index 56bd9d9eeecab..41613dcbb3ba4 100644
--- a/nixos/modules/services/video/epgstation/default.nix
+++ b/nixos/modules/services/video/epgstation/default.nix
@@ -1,9 +1,10 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
   cfg = config.services.epgstation;
+  opt = options.services.epgstation;
 
   username = config.users.users.epgstation.name;
   groupname = config.users.users.epgstation.group;
@@ -72,6 +73,7 @@ in
     socketioPort = mkOption {
       type = types.port;
       default = cfg.port + 1;
+      defaultText = literalExpression "config.${opt.port} + 1";
       description = ''
         Socket.io port for EPGStation to listen on.
       '';
@@ -80,6 +82,7 @@ in
     clientSocketioPort = mkOption {
       type = types.port;
       default = cfg.socketioPort;
+      defaultText = literalExpression "config.${opt.socketioPort}";
       description = ''
         Socket.io port that the web client is going to connect to. This may be
         different from <option>socketioPort</option> if EPGStation is hidden
@@ -183,6 +186,9 @@ in
         in {
           type = types.str;
           default = "http+unix://${replaceStrings ["/"] ["%2F"] sockPath}";
+          defaultText = literalExpression ''
+            "http+unix://''${replaceStrings ["/"] ["%2F"] config.${options.services.mirakurun.unixSocket}}"
+          '';
           example = "http://localhost:40772";
           description = "URL to connect to Mirakurun.";
         });
diff --git a/nixos/modules/services/video/unifi-video.nix b/nixos/modules/services/video/unifi-video.nix
index 17971b23db823..43208a9fe4cfb 100644
--- a/nixos/modules/services/video/unifi-video.nix
+++ b/nixos/modules/services/video/unifi-video.nix
@@ -1,7 +1,8 @@
-{ config, lib, pkgs, utils, ... }:
+{ config, lib, options, pkgs, utils, ... }:
 with lib;
 let
   cfg = config.services.unifi-video;
+  opt = options.services.unifi-video;
   mainClass = "com.ubnt.airvision.Main";
   cmd = ''
     ${pkgs.jsvc}/bin/jsvc \
@@ -164,6 +165,7 @@ in
       pidFile = mkOption {
         type = types.path;
         default = "${cfg.dataDir}/unifi-video.pid";
+        defaultText = literalExpression ''"''${config.${opt.dataDir}}/unifi-video.pid"'';
         description = "Location of unifi-video pid file.";
       };
 
diff --git a/nixos/modules/services/wayland/cage.nix b/nixos/modules/services/wayland/cage.nix
index 273693a3b2fe6..d2bbc4fc057b3 100644
--- a/nixos/modules/services/wayland/cage.nix
+++ b/nixos/modules/services/wayland/cage.nix
@@ -74,6 +74,8 @@ in {
         TTYVTDisallocate = "yes";
         # Fail to start if not controlling the virtual terminal.
         StandardInput = "tty-fail";
+        StandardOutput = "journal";
+        StandardError = "journal";
         # Set up a full (custom) user session for the user, required by Cage.
         PAMName = "cage";
       };
diff --git a/nixos/modules/services/web-apps/discourse.nix b/nixos/modules/services/web-apps/discourse.nix
index c4fb7e2b316f8..2c2911aada3f1 100644
--- a/nixos/modules/services/web-apps/discourse.nix
+++ b/nixos/modules/services/web-apps/discourse.nix
@@ -4,6 +4,7 @@ let
   json = pkgs.formats.json {};
 
   cfg = config.services.discourse;
+  opt = options.services.discourse;
 
   # Keep in sync with https://github.com/discourse/discourse_docker/blob/master/image/base/Dockerfile#L5
   upstreamPostgresqlVersion = lib.getVersion pkgs.postgresql_13;
@@ -327,6 +328,7 @@ in
         useSSL = lib.mkOption {
           type = lib.types.bool;
           default = cfg.redis.host != "localhost";
+          defaultText = lib.literalExpression ''config.${opt.redis.host} != "localhost"'';
           description = ''
             Connect to Redis with SSL.
           '';
@@ -399,6 +401,7 @@ in
           domain = lib.mkOption {
             type = lib.types.str;
             default = cfg.hostname;
+            defaultText = lib.literalExpression "config.${opt.hostname}";
             description = ''
               HELO domain to use for outgoing mail.
             '';
@@ -621,12 +624,13 @@ in
 
       max_user_api_reqs_per_minute = 20;
       max_user_api_reqs_per_day = 2880;
-      max_admin_api_reqs_per_key_per_minute = 60;
+      max_admin_api_reqs_per_minute = 60;
       max_reqs_per_ip_per_minute = 200;
       max_reqs_per_ip_per_10_seconds = 50;
       max_asset_reqs_per_ip_per_10_seconds = 200;
       max_reqs_per_ip_mode = "block";
       max_reqs_rate_limit_on_private = false;
+      skip_per_ip_rate_limit_trust_level = 1;
       force_anonymous_min_queue_seconds = 1;
       force_anonymous_min_per_10_seconds = 3;
       background_requests_max_queue_length = 0.5;
@@ -646,6 +650,9 @@ in
       enable_email_sync_demon = false;
       max_digests_enqueued_per_30_mins_per_site = 10000;
       cluster_name = null;
+      multisite_config_path = "config/multisite.yml";
+      enable_long_polling = null;
+      long_polling_interval = null;
     };
 
     services.redis.enable = lib.mkDefault (cfg.redis.host == "localhost");
@@ -825,7 +832,7 @@ in
 
       appendHttpConfig = ''
         # inactive means we keep stuff around for 1440m minutes regardless of last access (1 week)
-        # levels means it is a 2 deep heirarchy cause we can have lots of files
+        # levels means it is a 2 deep hierarchy cause we can have lots of files
         # max_size limits the size of the cache
         proxy_cache_path /var/cache/nginx inactive=1440m levels=1:2 keys_zone=discourse:10m max_size=600m;
 
@@ -837,7 +844,7 @@ in
         inherit (cfg) sslCertificate sslCertificateKey enableACME;
         forceSSL = lib.mkDefault tlsEnabled;
 
-        root = "/run/discourse/public";
+        root = "${cfg.package}/share/discourse/public";
 
         locations =
           let
@@ -889,7 +896,7 @@ in
               "~ ^/uploads/" = proxy {
                 extraConfig = cache_1y + ''
                   proxy_set_header X-Sendfile-Type X-Accel-Redirect;
-                  proxy_set_header X-Accel-Mapping /run/discourse/public/=/downloads/;
+                  proxy_set_header X-Accel-Mapping ${cfg.package}/share/discourse/public/=/downloads/;
 
                   # custom CSS
                   location ~ /stylesheet-cache/ {
@@ -911,7 +918,7 @@ in
               "~ ^/admin/backups/" = proxy {
                 extraConfig = ''
                   proxy_set_header X-Sendfile-Type X-Accel-Redirect;
-                  proxy_set_header X-Accel-Mapping /run/discourse/public/=/downloads/;
+                  proxy_set_header X-Accel-Mapping ${cfg.package}/share/discourse/public/=/downloads/;
                 '';
               };
               "~ ^/(svg-sprite/|letter_avatar/|letter_avatar_proxy/|user_avatar|highlight-js|stylesheets|theme-javascripts|favicon/proxied|service-worker)" = proxy {
@@ -938,7 +945,7 @@ in
               };
               "/downloads/".extraConfig = ''
                 internal;
-                alias /run/discourse/public/;
+                alias ${cfg.package}/share/discourse/public/;
               '';
             };
       };
diff --git a/nixos/modules/services/web-apps/discourse.xml b/nixos/modules/services/web-apps/discourse.xml
index 184c9c6363e50..ad9b65abf51e0 100644
--- a/nixos/modules/services/web-apps/discourse.xml
+++ b/nixos/modules/services/web-apps/discourse.xml
@@ -25,7 +25,7 @@ services.discourse = {
   };
   <link linkend="opt-services.discourse.secretKeyBaseFile">secretKeyBaseFile</link> = "/path/to/secret_key_base_file";
 };
-<link linkend="opt-security.acme.email">security.acme.email</link> = "me@example.com";
+<link linkend="opt-security.acme.defaults.email">security.acme.email</link> = "me@example.com";
 <link linkend="opt-security.acme.acceptTerms">security.acme.acceptTerms</link> = true;
 </programlisting>
    </para>
@@ -297,7 +297,7 @@ services.discourse = {
       the script:
       <programlisting language="bash">
 ./update.py update-plugins
-</programlisting>.
+</programlisting>
     </para>
 
     <para>
diff --git a/nixos/modules/services/web-apps/dokuwiki.nix b/nixos/modules/services/web-apps/dokuwiki.nix
index fc0e23729b3c4..9b9ae931f9a74 100644
--- a/nixos/modules/services/web-apps/dokuwiki.nix
+++ b/nixos/modules/services/web-apps/dokuwiki.nix
@@ -308,6 +308,9 @@ in
         inherit user;
         group = webserver.group;
 
+        # Not yet compatible with php 8 https://www.dokuwiki.org/requirements
+        # https://github.com/splitbrain/dokuwiki/issues/3545
+        phpPackage = pkgs.php74;
         phpEnv = {
           DOKUWIKI_LOCAL_CONFIG = "${dokuwikiLocalConfig hostName cfg}";
           DOKUWIKI_PLUGINS_LOCAL_CONFIG = "${dokuwikiPluginsLocalConfig hostName cfg}";
@@ -446,5 +449,6 @@ in
   meta.maintainers = with maintainers; [
     _1000101
     onny
+    dandellion
   ];
 }
diff --git a/nixos/modules/services/web-apps/galene.nix b/nixos/modules/services/web-apps/galene.nix
index db9dfeb474995..1d0a620585b0b 100644
--- a/nixos/modules/services/web-apps/galene.nix
+++ b/nixos/modules/services/web-apps/galene.nix
@@ -1,8 +1,9 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 let
   cfg = config.services.galene;
+  opt = options.services.galene;
   defaultstateDir = "/var/lib/galene";
   defaultrecordingsDir = "${cfg.stateDir}/recordings";
   defaultgroupsDir = "${cfg.stateDir}/groups";
@@ -88,6 +89,7 @@ in
       recordingsDir = mkOption {
         type = types.str;
         default = defaultrecordingsDir;
+        defaultText = literalExpression ''"''${config.${opt.stateDir}}/recordings"'';
         example = "/var/lib/galene/recordings";
         description = "Recordings directory.";
       };
@@ -95,6 +97,7 @@ in
       dataDir = mkOption {
         type = types.str;
         default = defaultdataDir;
+        defaultText = literalExpression ''"''${config.${opt.stateDir}}/data"'';
         example = "/var/lib/galene/data";
         description = "Data directory.";
       };
@@ -102,6 +105,7 @@ in
       groupsDir = mkOption {
         type = types.str;
         default = defaultgroupsDir;
+        defaultText = literalExpression ''"''${config.${opt.stateDir}}/groups"'';
         example = "/var/lib/galene/groups";
         description = "Web server directory.";
       };
diff --git a/nixos/modules/services/web-apps/hedgedoc.nix b/nixos/modules/services/web-apps/hedgedoc.nix
index e0c00fe67ea32..9eeabb9d5662b 100644
--- a/nixos/modules/services/web-apps/hedgedoc.nix
+++ b/nixos/modules/services/web-apps/hedgedoc.nix
@@ -33,7 +33,7 @@ in
       type = types.listOf types.str;
       default = [];
       description = ''
-        Groups to which the user ${name} should be added.
+        Groups to which the service user should be added.
       '';
     };
 
diff --git a/nixos/modules/services/web-apps/invidious.nix b/nixos/modules/services/web-apps/invidious.nix
index 7fb826af5835e..10b30bf1fd1d7 100644
--- a/nixos/modules/services/web-apps/invidious.nix
+++ b/nixos/modules/services/web-apps/invidious.nix
@@ -11,7 +11,7 @@ let
     systemd.services.invidious = {
       description = "Invidious (An alternative YouTube front-end)";
       wants = [ "network-online.target" ];
-      after = [ "syslog.target" "network-online.target" ];
+      after = [ "network-online.target" ];
       wantedBy = [ "multi-user.target" ];
 
       script =
@@ -225,6 +225,7 @@ in
       port = lib.mkOption {
         type = types.port;
         default = options.services.postgresql.port.default;
+        defaultText = lib.literalExpression "options.services.postgresql.port.default";
         description = ''
           The port of the database Invidious should use.
 
diff --git a/nixos/modules/services/web-apps/jitsi-meet.xml b/nixos/modules/services/web-apps/jitsi-meet.xml
index 97373bc6d9a87..ff44c724adf44 100644
--- a/nixos/modules/services/web-apps/jitsi-meet.xml
+++ b/nixos/modules/services/web-apps/jitsi-meet.xml
@@ -20,7 +20,7 @@
   };
   <link linkend="opt-services.jitsi-videobridge.openFirewall">services.jitsi-videobridge.openFirewall</link> = true;
   <link linkend="opt-networking.firewall.allowedTCPPorts">networking.firewall.allowedTCPPorts</link> = [ 80 443 ];
-  <link linkend="opt-security.acme.email">security.acme.email</link> = "me@example.com";
+  <link linkend="opt-security.acme.defaults.email">security.acme.email</link> = "me@example.com";
   <link linkend="opt-security.acme.acceptTerms">security.acme.acceptTerms</link> = true;
 }</programlisting>
    </para>
@@ -46,7 +46,7 @@
   };
   <link linkend="opt-services.jitsi-videobridge.openFirewall">services.jitsi-videobridge.openFirewall</link> = true;
   <link linkend="opt-networking.firewall.allowedTCPPorts">networking.firewall.allowedTCPPorts</link> = [ 80 443 ];
-  <link linkend="opt-security.acme.email">security.acme.email</link> = "me@example.com";
+  <link linkend="opt-security.acme.defaults.email">security.acme.email</link> = "me@example.com";
   <link linkend="opt-security.acme.acceptTerms">security.acme.acceptTerms</link> = true;
 }</programlisting>
    </para>
diff --git a/nixos/modules/services/web-apps/keycloak.nix b/nixos/modules/services/web-apps/keycloak.nix
index df8c7114102fd..e08f6dcabd2f5 100644
--- a/nixos/modules/services/web-apps/keycloak.nix
+++ b/nixos/modules/services/web-apps/keycloak.nix
@@ -1,7 +1,8 @@
-{ config, pkgs, lib, ... }:
+{ config, options, pkgs, lib, ... }:
 
 let
   cfg = config.services.keycloak;
+  opt = options.services.keycloak;
 in
 {
   options.services.keycloak = {
@@ -139,6 +140,7 @@ in
           lib.mkOption {
             type = lib.types.port;
             default = dbPorts.${cfg.database.type};
+            defaultText = lib.literalDocBook "default port of selected database";
             description = ''
               Port of the database to connect to.
             '';
@@ -147,6 +149,7 @@ in
       useSSL = lib.mkOption {
         type = lib.types.bool;
         default = cfg.database.host != "localhost";
+        defaultText = lib.literalExpression ''config.${opt.database.host} != "localhost"'';
         description = ''
           Whether the database connection should be secured by SSL /
           TLS.
diff --git a/nixos/modules/services/web-apps/matomo.nix b/nixos/modules/services/web-apps/matomo.nix
index eba55e7e9befa..8a0ca33b51f03 100644
--- a/nixos/modules/services/web-apps/matomo.nix
+++ b/nixos/modules/services/web-apps/matomo.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 with lib;
 let
   cfg = config.services.matomo;
@@ -12,10 +12,7 @@ let
   phpExecutionUnit = "phpfpm-${pool}";
   databaseService = "mysql.service";
 
-  fqdn =
-    let
-      join = hostName: domain: hostName + optionalString (domain != null) ".${domain}";
-     in join config.networking.hostName config.networking.domain;
+  fqdn = if config.networking.domain != null then config.networking.fqdn else config.networking.hostName;
 
 in {
   imports = [
@@ -81,9 +78,14 @@ in {
       hostname = mkOption {
         type = types.str;
         default = "${user}.${fqdn}";
+        defaultText = literalExpression ''
+          if config.${options.networking.domain} != null
+          then "${user}.''${config.${options.networking.fqdn}}"
+          else "${user}.''${config.${options.networking.hostName}}"
+        '';
         example = "matomo.yourdomain.org";
         description = ''
-          URL of the host, without https prefix. By default, this is ${user}.${fqdn}, but you may want to change it if you
+          URL of the host, without https prefix. You may want to change it if you
           run Matomo on a different URL than matomo.yourdomain.
         '';
       };
diff --git a/nixos/modules/services/web-apps/nextcloud.nix b/nixos/modules/services/web-apps/nextcloud.nix
index b1a536e519db4..6692d67081c5a 100644
--- a/nixos/modules/services/web-apps/nextcloud.nix
+++ b/nixos/modules/services/web-apps/nextcloud.nix
@@ -499,6 +499,7 @@ in {
     occ = mkOption {
       type = types.package;
       default = occ;
+      defaultText = literalDocBook "generated script";
       internal = true;
       description = ''
         The nextcloud-occ program preconfigured to target this Nextcloud instance.
@@ -526,8 +527,8 @@ in {
         # FIXME(@Ma27) remove as soon as nextcloud properly supports
         # mariadb >=10.6.
         isUnsupportedMariadb =
-          # All currently supported Nextcloud versions are affected.
-          (versionOlder cfg.package.version "23")
+          # All currently supported Nextcloud versions are affected (https://github.com/nextcloud/server/issues/25436).
+          (versionOlder cfg.package.version "24")
           # This module uses mysql
           && (cfg.config.dbtype == "mysql")
           # MySQL is managed via NixOS
diff --git a/nixos/modules/services/web-apps/peertube.nix b/nixos/modules/services/web-apps/peertube.nix
index 932ddcfef198a..a65428018260d 100644
--- a/nixos/modules/services/web-apps/peertube.nix
+++ b/nixos/modules/services/web-apps/peertube.nix
@@ -1,7 +1,8 @@
-{ lib, pkgs, config, ... }:
+{ lib, pkgs, config, options, ... }:
 
 let
   cfg = config.services.peertube;
+  opt = options.services.peertube;
 
   settingsFormat = pkgs.formats.json {};
   configFile = settingsFormat.generate "production.json" cfg.settings;
@@ -153,6 +154,11 @@ in {
       host = lib.mkOption {
         type = lib.types.str;
         default = if cfg.database.createLocally then "/run/postgresql" else null;
+        defaultText = lib.literalExpression ''
+          if config.${opt.database.createLocally}
+          then "/run/postgresql"
+          else null
+        '';
         example = "192.168.15.47";
         description = "Database host address or unix socket.";
       };
@@ -193,12 +199,22 @@ in {
       host = lib.mkOption {
         type = lib.types.nullOr lib.types.str;
         default = if cfg.redis.createLocally && !cfg.redis.enableUnixSocket then "127.0.0.1" else null;
+        defaultText = lib.literalExpression ''
+          if config.${opt.redis.createLocally} && !config.${opt.redis.enableUnixSocket}
+          then "127.0.0.1"
+          else null
+        '';
         description = "Redis host.";
       };
 
       port = lib.mkOption {
         type = lib.types.nullOr lib.types.port;
         default = if cfg.redis.createLocally && cfg.redis.enableUnixSocket then null else 6379;
+        defaultText = lib.literalExpression ''
+          if config.${opt.redis.createLocally} && config.${opt.redis.enableUnixSocket}
+          then null
+          else 6379
+        '';
         description = "Redis port.";
       };
 
@@ -212,6 +228,7 @@ in {
       enableUnixSocket = lib.mkOption {
         type = lib.types.bool;
         default = cfg.redis.createLocally;
+        defaultText = lib.literalExpression "config.${opt.redis.createLocally}";
         description = "Use Unix socket.";
       };
     };
diff --git a/nixos/modules/services/web-apps/pgpkeyserver-lite.nix b/nixos/modules/services/web-apps/pgpkeyserver-lite.nix
index 5642627d397df..faf0ce13238e4 100644
--- a/nixos/modules/services/web-apps/pgpkeyserver-lite.nix
+++ b/nixos/modules/services/web-apps/pgpkeyserver-lite.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
@@ -6,6 +6,7 @@ let
 
   cfg = config.services.pgpkeyserver-lite;
   sksCfg = config.services.sks;
+  sksOpt = options.services.sks;
 
   webPkg = cfg.package;
 
@@ -37,6 +38,7 @@ in
 
       hkpAddress = mkOption {
         default = builtins.head sksCfg.hkpAddress;
+        defaultText = literalExpression "head config.${sksOpt.hkpAddress}";
         type = types.str;
         description = "
           Wich ip address the sks-keyserver is listening on.
@@ -45,6 +47,7 @@ in
 
       hkpPort = mkOption {
         default = sksCfg.hkpPort;
+        defaultText = literalExpression "config.${sksOpt.hkpPort}";
         type = types.int;
         description = "
           Which port the sks-keyserver is listening on.
diff --git a/nixos/modules/services/web-apps/powerdns-admin.nix b/nixos/modules/services/web-apps/powerdns-admin.nix
new file mode 100644
index 0000000000000..ce99b606c318c
--- /dev/null
+++ b/nixos/modules/services/web-apps/powerdns-admin.nix
@@ -0,0 +1,149 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.powerdns-admin;
+
+  configText = ''
+    ${cfg.config}
+  ''
+  + optionalString (cfg.secretKeyFile != null) ''
+    with open('${cfg.secretKeyFile}') as file:
+      SECRET_KEY = file.read()
+  ''
+  + optionalString (cfg.saltFile != null) ''
+    with open('${cfg.saltFile}') as file:
+      SALT = file.read()
+  '';
+in
+{
+  options.services.powerdns-admin = {
+    enable = mkEnableOption "the PowerDNS web interface";
+
+    extraArgs = mkOption {
+      type = types.listOf types.str;
+      default = [ ];
+      example = literalExpression ''
+        [ "-b" "127.0.0.1:8000" ]
+      '';
+      description = ''
+        Extra arguments passed to powerdns-admin.
+      '';
+    };
+
+    config = mkOption {
+      type = types.str;
+      default = "";
+      example = ''
+        BIND_ADDRESS = '127.0.0.1'
+        PORT = 8000
+        SQLALCHEMY_DATABASE_URI = 'postgresql://powerdnsadmin@/powerdnsadmin?host=/run/postgresql'
+      '';
+      description = ''
+        Configuration python file.
+        See <link xlink:href="https://github.com/ngoduykhanh/PowerDNS-Admin/blob/v${pkgs.powerdns-admin.version}/configs/development.py">the example configuration</link>
+        for options.
+      '';
+    };
+
+    secretKeyFile = mkOption {
+      type = types.nullOr types.path;
+      example = "/etc/powerdns-admin/secret";
+      description = ''
+        The secret used to create cookies.
+        This needs to be set, otherwise the default is used and everyone can forge valid login cookies.
+        Set this to null to ignore this setting and configure it through another way.
+      '';
+    };
+
+    saltFile = mkOption {
+      type = types.nullOr types.path;
+      example = "/etc/powerdns-admin/salt";
+      description = ''
+        The salt used for serialization.
+        This should be set, otherwise the default is used.
+        Set this to null to ignore this setting and configure it through another way.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.powerdns-admin = {
+      description = "PowerDNS web interface";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "networking.target" ];
+
+      environment.FLASK_CONF = builtins.toFile "powerdns-admin-config.py" configText;
+      environment.PYTHONPATH = pkgs.powerdns-admin.pythonPath;
+      serviceConfig = {
+        ExecStart = "${pkgs.powerdns-admin}/bin/powerdns-admin --pid /run/powerdns-admin/pid ${escapeShellArgs cfg.extraArgs}";
+        ExecStartPre = "${pkgs.coreutils}/bin/env FLASK_APP=${pkgs.powerdns-admin}/share/powerdnsadmin/__init__.py ${pkgs.python3Packages.flask}/bin/flask db upgrade -d ${pkgs.powerdns-admin}/share/migrations";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        ExecStop = "${pkgs.coreutils}/bin/kill -TERM $MAINPID";
+        PIDFile = "/run/powerdns-admin/pid";
+        RuntimeDirectory = "powerdns-admin";
+        User = "powerdnsadmin";
+        Group = "powerdnsadmin";
+
+        AmbientCapabilities = "CAP_NET_BIND_SERVICE";
+        BindReadOnlyPaths = [
+          "/nix/store"
+          "-/etc/resolv.conf"
+          "-/etc/nsswitch.conf"
+          "-/etc/hosts"
+          "-/etc/localtime"
+        ]
+        ++ (optional (cfg.secretKeyFile != null) cfg.secretKeyFile)
+        ++ (optional (cfg.saltFile != null) cfg.saltFile);
+        CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
+        # ProtectClock= adds DeviceAllow=char-rtc r
+        DeviceAllow = "";
+        # Implies ProtectSystem=strict, which re-mounts all paths
+        #DynamicUser = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
+        # Needs to start a server
+        #PrivateNetwork = true;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProcSubset = "pid";
+        ProtectClock = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        # Would re-mount paths ignored by temporary root
+        #ProtectSystem = "strict";
+        ProtectControlGroups = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        # gunicorn needs setuid
+        SystemCallFilter = [
+          "@system-service"
+          "~@privileged @resources @keyring"
+          # These got removed by the line above but are needed
+          "@setuid @chown"
+        ];
+        TemporaryFileSystem = "/:ro";
+        # Does not work well with the temporary root
+        #UMask = "0066";
+      };
+    };
+
+    users.groups.powerdnsadmin = { };
+    users.users.powerdnsadmin = {
+      description = "PowerDNS web interface user";
+      isSystemUser = true;
+      group = "powerdnsadmin";
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/tt-rss.nix b/nixos/modules/services/web-apps/tt-rss.nix
index 08356cee1dfe7..9aa38ab25c9a3 100644
--- a/nixos/modules/services/web-apps/tt-rss.nix
+++ b/nixos/modules/services/web-apps/tt-rss.nix
@@ -18,11 +18,11 @@ let
   tt-rss-config = let
     password =
       if (cfg.database.password != null) then
-        "${(escape ["'" "\\"] cfg.database.password)}"
+        "'${(escape ["'" "\\"] cfg.database.password)}'"
       else if (cfg.database.passwordFile != null) then
-        "file_get_contents('${cfg.database.passwordFile}'"
+        "file_get_contents('${cfg.database.passwordFile}')"
       else
-        ""
+        null
       ;
   in pkgs.writeText "config.php" ''
     <?php
@@ -40,7 +40,7 @@ let
       putenv('TTRSS_DB_HOST=${optionalString (cfg.database.host != null) cfg.database.host}');
       putenv('TTRSS_DB_USER=${cfg.database.user}');
       putenv('TTRSS_DB_NAME=${cfg.database.name}');
-      putenv('TTRSS_DB_PASS=${password}');
+      putenv('TTRSS_DB_PASS=' ${optionalString (password != null) ". ${password}"});
       putenv('TTRSS_DB_PORT=${toString dbPort}');
 
       putenv('TTRSS_AUTH_AUTO_CREATE=${boolToString cfg.auth.autoCreate}');
diff --git a/nixos/modules/services/web-apps/youtrack.nix b/nixos/modules/services/web-apps/youtrack.nix
index 7a70ae6cd5238..b83265ffeab6c 100644
--- a/nixos/modules/services/web-apps/youtrack.nix
+++ b/nixos/modules/services/web-apps/youtrack.nix
@@ -128,6 +128,7 @@ in
         Type = "simple";
         User = "youtrack";
         Group = "youtrack";
+        Restart = "on-failure";
         ExecStart = ''${cfg.package}/bin/youtrack --J-Xmx${cfg.maxMemory} --J-XX:MaxMetaspaceSize=${cfg.maxMetaspaceSize} ${cfg.jvmOpts} ${cfg.address}:${toString cfg.port}'';
       };
     };
diff --git a/nixos/modules/services/web-apps/zabbix.nix b/nixos/modules/services/web-apps/zabbix.nix
index ff50b95254f90..538dac0d5be22 100644
--- a/nixos/modules/services/web-apps/zabbix.nix
+++ b/nixos/modules/services/web-apps/zabbix.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 let
 
@@ -6,6 +6,7 @@ let
   inherit (lib) literalExpression mapAttrs optionalString versionAtLeast;
 
   cfg = config.services.zabbixWeb;
+  opt = options.services.zabbixWeb;
   fpm = config.services.phpfpm.pools.zabbix;
 
   user = "zabbix";
@@ -82,6 +83,11 @@ in
             if cfg.database.type == "mysql" then config.services.mysql.port
             else if cfg.database.type == "pgsql" then config.services.postgresql.port
             else 1521;
+          defaultText = literalExpression ''
+            if config.${opt.database.type} == "mysql" then config.${options.services.mysql.port}
+            else if config.${opt.database.type} == "pgsql" then config.${options.services.postgresql.port}
+            else 1521
+          '';
           description = "Database host port.";
         };
 
diff --git a/nixos/modules/services/web-servers/apache-httpd/default.nix b/nixos/modules/services/web-servers/apache-httpd/default.nix
index 992a58875e435..1a49b4ca15c7c 100644
--- a/nixos/modules/services/web-servers/apache-httpd/default.nix
+++ b/nixos/modules/services/web-servers/apache-httpd/default.nix
@@ -154,7 +154,7 @@ let
       sslServerKey = if useACME then "${sslCertDir}/key.pem" else hostOpts.sslServerKey;
       sslServerChain = if useACME then "${sslCertDir}/chain.pem" else hostOpts.sslServerChain;
 
-      acmeChallenge = optionalString useACME ''
+      acmeChallenge = optionalString (useACME && hostOpts.acmeRoot != null) ''
         Alias /.well-known/acme-challenge/ "${hostOpts.acmeRoot}/.well-known/acme-challenge/"
         <Directory "${hostOpts.acmeRoot}">
             AllowOverride None
@@ -677,9 +677,16 @@ in
     };
 
     security.acme.certs = let
-      acmePairs = map (hostOpts: nameValuePair hostOpts.hostName {
+      acmePairs = map (hostOpts: let
+        hasRoot = hostOpts.acmeRoot != null;
+      in nameValuePair hostOpts.hostName {
         group = mkDefault cfg.group;
-        webroot = hostOpts.acmeRoot;
+        # if acmeRoot is null inherit config.security.acme
+        # Since config.security.acme.certs.<cert>.webroot's own default value
+        # should take precedence set priority higher than mkOptionDefault
+        webroot = mkOverride (if hasRoot then 1000 else 2000) hostOpts.acmeRoot;
+        # Also nudge dnsProvider to null in case it is inherited
+        dnsProvider = mkOverride (if hasRoot then 1000 else 2000) null;
         extraDomainNames = hostOpts.serverAliases;
         # Use the vhost-specific email address if provided, otherwise let
         # security.acme.email or security.acme.certs.<cert>.email be used.
diff --git a/nixos/modules/services/web-servers/apache-httpd/vhost-options.nix b/nixos/modules/services/web-servers/apache-httpd/vhost-options.nix
index 8bb7e91ec9cdb..c52ab2c596e01 100644
--- a/nixos/modules/services/web-servers/apache-httpd/vhost-options.nix
+++ b/nixos/modules/services/web-servers/apache-httpd/vhost-options.nix
@@ -128,9 +128,12 @@ in
     };
 
     acmeRoot = mkOption {
-      type = types.str;
+      type = types.nullOr types.str;
       default = "/var/lib/acme/acme-challenge";
-      description = "Directory for the acme challenge which is PUBLIC, don't put certs or keys in here";
+      description = ''
+        Directory for the acme challenge which is PUBLIC, don't put certs or keys in here.
+        Set to null to inherit from config.security.acme.
+      '';
     };
 
     sslServerCert = mkOption {
diff --git a/nixos/modules/services/web-servers/caddy/default.nix b/nixos/modules/services/web-servers/caddy/default.nix
index ed27dd375c86d..d51effa31c975 100644
--- a/nixos/modules/services/web-servers/caddy/default.nix
+++ b/nixos/modules/services/web-servers/caddy/default.nix
@@ -4,111 +4,159 @@ with lib;
 
 let
   cfg = config.services.caddy;
-  vhostToConfig = vhostName: vhostAttrs: ''
-    ${vhostName} ${builtins.concatStringsSep " " vhostAttrs.serverAliases} {
-      ${vhostAttrs.extraConfig}
-    }
-  '';
-  configFile = pkgs.writeText "Caddyfile" (builtins.concatStringsSep "\n"
-    ([ cfg.config ] ++ (mapAttrsToList vhostToConfig cfg.virtualHosts)));
-
-  formattedConfig = pkgs.runCommand "formattedCaddyFile" { } ''
-    ${cfg.package}/bin/caddy fmt ${configFile} > $out
-  '';
-
-  tlsConfig = {
-    apps.tls.automation.policies = [{
-      issuers = [{
-        inherit (cfg) ca email;
-        module = "acme";
-      }];
-    }];
-  };
 
-  adaptedConfig = pkgs.runCommand "caddy-config-adapted.json" { } ''
-    ${cfg.package}/bin/caddy adapt \
-      --config ${formattedConfig} --adapter ${cfg.adapter} > $out
-  '';
-  tlsJSON = pkgs.writeText "tls.json" (builtins.toJSON tlsConfig);
-
-  # merge the TLS config options we expose with the ones originating in the Caddyfile
-  configJSON =
-    if cfg.ca != null then
-      let tlsConfigMerge = ''
-        {"apps":
-          {"tls":
-            {"automation":
-              {"policies":
-                (if .[0].apps.tls.automation.policies == .[1]?.apps.tls.automation.policies
-                 then .[0].apps.tls.automation.policies
-                 else (.[0].apps.tls.automation.policies + .[1]?.apps.tls.automation.policies)
-                 end)
-              }
-            }
-          }
-        }'';
-      in
-      pkgs.runCommand "caddy-config.json" { } ''
-        ${pkgs.jq}/bin/jq -s '.[0] * ${tlsConfigMerge}' ${adaptedConfig} ${tlsJSON} > $out
+  virtualHosts = attrValues cfg.virtualHosts;
+  acmeVHosts = filter (hostOpts: hostOpts.useACMEHost != null) virtualHosts;
+
+  mkVHostConf = hostOpts:
+    let
+      sslCertDir = config.security.acme.certs.${hostOpts.useACMEHost}.directory;
+    in
       ''
-    else
-      adaptedConfig;
+        ${hostOpts.hostName} ${concatStringsSep " " hostOpts.serverAliases} {
+          bind ${concatStringsSep " " hostOpts.listenAddresses}
+          ${optionalString (hostOpts.useACMEHost != null) "tls ${sslCertDir}/cert.pem ${sslCertDir}/key.pem"}
+          log {
+            ${hostOpts.logFormat}
+          }
+
+          ${hostOpts.extraConfig}
+        }
+      '';
+
+  configFile =
+    let
+      Caddyfile = pkgs.writeText "Caddyfile" ''
+        {
+          ${optionalString (cfg.email != null) "email ${cfg.email}"}
+          ${optionalString (cfg.acmeCA != null) "acme_ca ${cfg.acmeCA}"}
+          log {
+            ${cfg.logFormat}
+          }
+        }
+        ${cfg.extraConfig}
+      '';
+
+      Caddyfile-formatted = pkgs.runCommand "Caddyfile-formatted" { nativeBuildInputs = [ cfg.package ]; } ''
+        ${cfg.package}/bin/caddy fmt ${Caddyfile} > $out
+      '';
+    in
+      if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform then Caddyfile-formatted else Caddyfile;
 in
 {
   imports = [
     (mkRemovedOptionModule [ "services" "caddy" "agree" ] "this option is no longer necessary for Caddy 2")
+    (mkRenamedOptionModule [ "services" "caddy" "ca" ] [ "services" "caddy" "acmeCA" ])
+    (mkRenamedOptionModule [ "services" "caddy" "config" ] [ "services" "caddy" "extraConfig" ])
   ];
 
+  # interface
   options.services.caddy = {
     enable = mkEnableOption "Caddy web server";
 
-    config = mkOption {
-      default = "";
-      example = ''
-        example.com {
-          encode gzip
-          log
-          root /srv/http
-        }
+    user = mkOption {
+      default = "caddy";
+      type = types.str;
+      description = ''
+        User account under which caddy runs.
+
+        <note><para>
+          If left as the default value this user will automatically be created
+          on system activation, otherwise you are responsible for
+          ensuring the user exists before the Caddy service starts.
+        </para></note>
       '';
-      type = types.lines;
+    };
+
+    group = mkOption {
+      default = "caddy";
+      type = types.str;
       description = ''
-        Verbatim Caddyfile to use.
-        Caddy v2 supports multiple config formats via adapters (see <option>services.caddy.adapter</option>).
+        Group account under which caddy runs.
+
+        <note><para>
+          If left as the default value this user will automatically be created
+          on system activation, otherwise you are responsible for
+          ensuring the user exists before the Caddy service starts.
+        </para></note>
       '';
     };
 
-    virtualHosts = mkOption {
-      type = types.attrsOf (types.submodule (import ./vhost-options.nix {
-        inherit config lib;
-      }));
-      default = { };
-      example = literalExpression ''
-        {
-          "hydra.example.com" = {
-            serverAliases = [ "www.hydra.example.com" ];
-            extraConfig = ''''''
-              encode gzip
-              log
-              root /srv/http
-            '''''';
-          };
-        };
+    package = mkOption {
+      default = pkgs.caddy;
+      defaultText = literalExpression "pkgs.caddy";
+      type = types.package;
+      description = ''
+        Caddy package to use.
       '';
-      description = "Declarative vhost config";
     };
 
+    dataDir = mkOption {
+      type = types.path;
+      default = "/var/lib/caddy";
+      description = ''
+        The data directory for caddy.
 
-    user = mkOption {
-      default = "caddy";
-      type = types.str;
-      description = "User account under which caddy runs.";
+        <note>
+          <para>
+            If left as the default value this directory will automatically be created
+            before the Caddy server starts, otherwise you are responsible for ensuring
+            the directory exists with appropriate ownership and permissions.
+          </para>
+          <para>
+            Caddy v2 replaced <literal>CADDYPATH</literal> with XDG directories.
+            See <link xlink:href="https://caddyserver.com/docs/conventions#file-locations"/>.
+          </para>
+        </note>
+      '';
     };
 
-    group = mkOption {
-      default = "caddy";
-      type = types.str;
-      description = "Group account under which caddy runs.";
+    logDir = mkOption {
+      type = types.path;
+      default = "/var/log/caddy";
+      description = ''
+        Directory for storing Caddy access logs.
+
+        <note><para>
+          If left as the default value this directory will automatically be created
+          before the Caddy server starts, otherwise the sysadmin is responsible for
+          ensuring the directory exists with appropriate ownership and permissions.
+        </para></note>
+      '';
+    };
+
+    logFormat = mkOption {
+      type = types.lines;
+      default = ''
+        level ERROR
+      '';
+      example = literalExpression ''
+        mkForce "level INFO";
+      '';
+      description = ''
+        Configuration for the default logger. See
+        <link xlink:href="https://caddyserver.com/docs/caddyfile/options#log"/>
+        for details.
+      '';
+    };
+
+    configFile = mkOption {
+      type = types.path;
+      default = configFile;
+      defaultText = "A Caddyfile automatically generated by values from services.caddy.*";
+      example = literalExpression ''
+        pkgs.writeText "Caddyfile" '''
+          example.com
+
+          root * /var/www/wordpress
+          php_fastcgi unix//run/php/php-version-fpm.sock
+          file_server
+        ''';
+      '';
+      description = ''
+        Override the configuration file used by Caddy. By default,
+        NixOS generates one automatically.
+      '';
     };
 
     adapter = mkOption {
@@ -117,7 +165,13 @@ in
       type = types.str;
       description = ''
         Name of the config adapter to use.
-        See https://caddyserver.com/docs/config-adapters for the full list.
+        See <link xlink:href="https://caddyserver.com/docs/config-adapters"/>
+        for the full list.
+
+        <note><para>
+          Any value other than <literal>caddyfile</literal> is only valid when
+          providing your own <option>configFile</option>.
+        </para></note>
       '';
     };
 
@@ -125,54 +179,87 @@ in
       default = false;
       type = types.bool;
       description = ''
-        Use saved config, if any (and prefer over configuration passed with <option>services.caddy.config</option>).
+        Use saved config, if any (and prefer over any specified configuration passed with <literal>--config</literal>).
       '';
     };
 
-    ca = mkOption {
-      default = "https://acme-v02.api.letsencrypt.org/directory";
-      example = "https://acme-staging-v02.api.letsencrypt.org/directory";
-      type = types.nullOr types.str;
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      example = ''
+        example.com {
+          encode gzip
+          log
+          root /srv/http
+        }
+      '';
       description = ''
-        Certificate authority ACME server. The default (Let's Encrypt
-        production server) should be fine for most people. Set it to null if
-        you don't want to include any authority (or if you want to write a more
-        fine-graned configuration manually)
+        Additional lines of configuration appended to the automatically
+        generated <literal>Caddyfile</literal>.
       '';
     };
 
-    email = mkOption {
-      default = "";
-      type = types.str;
-      description = "Email address (for Let's Encrypt certificate)";
+    virtualHosts = mkOption {
+      type = with types; attrsOf (submodule (import ./vhost-options.nix { inherit cfg; }));
+      default = {};
+      example = literalExpression ''
+        {
+          "hydra.example.com" = {
+            serverAliases = [ "www.hydra.example.com" ];
+            extraConfig = '''
+              encode gzip
+              root /srv/http
+            ''';
+          };
+        };
+      '';
+      description = ''
+        Declarative specification of virtual hosts served by Caddy.
+      '';
     };
 
-    dataDir = mkOption {
-      default = "/var/lib/caddy";
-      type = types.path;
+    acmeCA = mkOption {
+      default = "https://acme-v02.api.letsencrypt.org/directory";
+      example = "https://acme-staging-v02.api.letsencrypt.org/directory";
+      type = with types; nullOr str;
       description = ''
-        The data directory, for storing certificates. Before 17.09, this
-        would create a .caddy directory. With 17.09 the contents of the
-        .caddy directory are in the specified data directory instead.
+        The URL to the ACME CA's directory. It is strongly recommended to set
+        this to Let's Encrypt's staging endpoint for testing or development.
 
-        Caddy v2 replaced CADDYPATH with XDG directories.
-        See https://caddyserver.com/docs/conventions#file-locations.
+        Set it to <literal>null</literal> if you want to write a more
+        fine-grained configuration manually.
       '';
     };
 
-    package = mkOption {
-      default = pkgs.caddy;
-      defaultText = literalExpression "pkgs.caddy";
-      type = types.package;
+    email = mkOption {
+      default = null;
+      type = with types; nullOr str;
       description = ''
-        Caddy package to use.
+        Your email address. Mainly used when creating an ACME account with your
+        CA, and is highly recommended in case there are problems with your
+        certificates.
       '';
     };
+
   };
 
+  # implementation
   config = mkIf cfg.enable {
+
+    assertions = [
+      { assertion = cfg.adapter != "caddyfile" -> cfg.configFile != configFile;
+        message = "Any value other than 'caddyfile' is only valid when providing your own `services.caddy.configFile`";
+      }
+    ];
+
+    services.caddy.extraConfig = concatMapStringsSep "\n" mkVHostConf virtualHosts;
+
     systemd.packages = [ cfg.package ];
     systemd.services.caddy = {
+      wants = map (hostOpts: "acme-finished-${hostOpts.useACMEHost}.target") acmeVHosts;
+      after = map (hostOpts: "acme-selfsigned-${hostOpts.useACMEHost}.service") acmeVHosts;
+      before = map (hostOpts: "acme-${hostOpts.useACMEHost}.service") acmeVHosts;
+
       wantedBy = [ "multi-user.target" ];
       startLimitIntervalSec = 14400;
       startLimitBurst = 10;
@@ -180,13 +267,17 @@ in
       serviceConfig = {
         # https://www.freedesktop.org/software/systemd/man/systemd.service.html#ExecStart=
         # If the empty string is assigned to this option, the list of commands to start is reset, prior assignments of this option will have no effect.
-        ExecStart = [ "" "${cfg.package}/bin/caddy run ${optionalString cfg.resume "--resume"} --config ${configJSON}" ];
-        ExecReload = [ "" "${cfg.package}/bin/caddy reload --config ${configJSON}" ];
+        ExecStart = [ "" "${cfg.package}/bin/caddy run --config ${cfg.configFile} --adapter ${cfg.adapter} ${optionalString cfg.resume "--resume"}" ];
+        ExecReload = [ "" "${cfg.package}/bin/caddy reload --config ${cfg.configFile} --adapter ${cfg.adapter}" ];
 
+        ExecStartPre = "${cfg.package}/bin/caddy validate --config ${cfg.configFile} --adapter ${cfg.adapter}";
         User = cfg.user;
         Group = cfg.group;
         ReadWriteDirectories = cfg.dataDir;
+        StateDirectory = mkIf (cfg.dataDir == "/var/lib/caddy") [ "caddy" ];
+        LogsDirectory = mkIf (cfg.logDir == "/var/log/caddy") [ "caddy" ];
         Restart = "on-abnormal";
+        SupplementaryGroups = mkIf (length acmeVHosts != 0) [ "acme" ];
 
         # TODO: attempt to upstream these options
         NoNewPrivileges = true;
@@ -200,7 +291,6 @@ in
         group = cfg.group;
         uid = config.ids.uids.caddy;
         home = cfg.dataDir;
-        createHome = true;
       };
     };
 
@@ -208,5 +298,12 @@ in
       caddy.gid = config.ids.gids.caddy;
     };
 
+    security.acme.certs =
+      let
+        eachACMEHost = unique (catAttrs "useACMEHost" acmeVHosts);
+        reloads = map (useACMEHost: nameValuePair useACMEHost { reloadServices = [ "caddy.service" ]; }) eachACMEHost;
+      in
+        listToAttrs reloads;
+
   };
 }
diff --git a/nixos/modules/services/web-servers/caddy/vhost-options.nix b/nixos/modules/services/web-servers/caddy/vhost-options.nix
index 1f74295fc9a2e..f240ec605c293 100644
--- a/nixos/modules/services/web-servers/caddy/vhost-options.nix
+++ b/nixos/modules/services/web-servers/caddy/vhost-options.nix
@@ -1,15 +1,19 @@
-# This file defines the options that can be used both for the Nginx
-# main server configuration, and for the virtual hosts.  (The latter
-# has additional options that affect the web server as a whole, like
-# the user/group to run under.)
-
-{ lib, ... }:
-
-with lib;
+{ cfg }:
+{ config, lib, name, ... }:
+let
+  inherit (lib) literalExpression mkOption types;
+in
 {
   options = {
+
+    hostName = mkOption {
+      type = types.str;
+      default = name;
+      description = "Canonical hostname for the server.";
+    };
+
     serverAliases = mkOption {
-      type = types.listOf types.str;
+      type = with types; listOf str;
       default = [ ];
       example = [ "www.example.org" "example.org" ];
       description = ''
@@ -17,12 +21,59 @@ with lib;
       '';
     };
 
+    listenAddresses = mkOption {
+      type = with types; listOf str;
+      description = ''
+        A list of host interfaces to bind to for this virtual host.
+      '';
+      default = [ ];
+      example = [ "127.0.0.1" "::1" ];
+    };
+
+    useACMEHost = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        A host of an existing Let's Encrypt certificate to use.
+        This is mostly useful if you use DNS challenges but Caddy does not
+        currently support your provider.
+
+        <emphasis>Note that this option does not create any certificates, nor
+        does it add subdomains to existing ones – you will need to create them
+        manually using <xref linkend="opt-security.acme.certs"/>. Additionally,
+        you should probably add the <literal>caddy</literal> user to the
+        <literal>acme</literal> group to grant access to the certificates.</emphasis>
+      '';
+    };
+
+    logFormat = mkOption {
+      type = types.lines;
+      default = ''
+        output file ${cfg.logDir}/access-${config.hostName}.log
+      '';
+      defaultText = ''
+        output file ''${config.services.caddy.logDir}/access-''${hostName}.log
+      '';
+      example = literalExpression ''
+        mkForce '''
+          output discard
+        ''';
+      '';
+      description = ''
+        Configuration for HTTP request logging (also known as access logs). See
+        <link xlink:href="https://caddyserver.com/docs/caddyfile/directives/log#log"/>
+        for details.
+      '';
+    };
+
     extraConfig = mkOption {
       type = types.lines;
       default = "";
       description = ''
-        These lines go into the vhost verbatim
+        Additional lines of configuration appended to this virtual host in the
+        automatically generated <literal>Caddyfile</literal>.
       '';
     };
+
   };
 }
diff --git a/nixos/modules/services/web-servers/lighttpd/collectd.nix b/nixos/modules/services/web-servers/lighttpd/collectd.nix
index 3f262451c2cb7..5f091591daf91 100644
--- a/nixos/modules/services/web-servers/lighttpd/collectd.nix
+++ b/nixos/modules/services/web-servers/lighttpd/collectd.nix
@@ -1,9 +1,10 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
   cfg = config.services.lighttpd.collectd;
+  opt = options.services.lighttpd.collectd;
 
   collectionConf = pkgs.writeText "collection.conf" ''
     datadir: "${config.services.collectd.dataDir}"
@@ -29,6 +30,9 @@ in
     collectionCgi = mkOption {
       type = types.path;
       default = defaultCollectionCgi;
+      defaultText = literalDocBook ''
+        <literal>config.${options.services.collectd.package}</literal> configured for lighttpd
+      '';
       description = ''
         Path to collection.cgi script from (collectd sources)/contrib/collection.cgi
         This option allows to use a customized version
diff --git a/nixos/modules/services/web-servers/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix
index 96e45cfc4f77d..05b7870fc3a13 100644
--- a/nixos/modules/services/web-servers/nginx/default.nix
+++ b/nixos/modules/services/web-servers/nginx/default.nix
@@ -278,7 +278,7 @@ let
         acmeLocation = optionalString (vhost.enableACME || vhost.useACMEHost != null) ''
           location /.well-known/acme-challenge {
             ${optionalString (vhost.acmeFallbackHost != null) "try_files $uri @acme-fallback;"}
-            root ${vhost.acmeRoot};
+            ${optionalString (vhost.acmeRoot != null) "root ${vhost.acmeRoot};"}
             auth_basic off;
           }
           ${optionalString (vhost.acmeFallbackHost != null) ''
@@ -317,9 +317,12 @@ let
           ${optionalString (hasSSL && vhost.sslTrustedCertificate != null) ''
             ssl_trusted_certificate ${vhost.sslTrustedCertificate};
           ''}
-          ${optionalString vhost.rejectSSL ''
+          ${optionalString (hasSSL && vhost.rejectSSL) ''
             ssl_reject_handshake on;
           ''}
+          ${optionalString (hasSSL && vhost.kTLS) ''
+            ssl_conf_command Options KTLS;
+          ''}
 
           ${mkBasicAuth vhostName vhost}
 
@@ -825,6 +828,14 @@ in
       }
 
       {
+        assertion = any (host: host.kTLS) (attrValues virtualHosts) -> versionAtLeast cfg.package.version "1.21.4";
+        message = ''
+          services.nginx.virtualHosts.<name>.kTLS requires nginx version
+          1.21.4 or above; see the documentation for services.nginx.package.
+        '';
+      }
+
+      {
         assertion = all (host: !(host.enableACME && host.useACMEHost != null)) (attrValues virtualHosts);
         message = ''
           Options services.nginx.service.virtualHosts.<name>.enableACME and
@@ -900,7 +911,7 @@ in
         PrivateMounts = true;
         # System Call Filtering
         SystemCallArchitectures = "native";
-        SystemCallFilter = "~@cpu-emulation @debug @keyring @ipc @mount @obsolete @privileged @setuid @mincore";
+        SystemCallFilter = [ "~@cpu-emulation @debug @keyring @mount @obsolete @privileged @setuid @mincore" ] ++ optionals (cfg.package != pkgs.tengine) [ "~@ipc" ];
       };
     };
 
@@ -937,9 +948,16 @@ in
     };
 
     security.acme.certs = let
-      acmePairs = map (vhostConfig: nameValuePair vhostConfig.serverName {
+      acmePairs = map (vhostConfig: let
+        hasRoot = vhostConfig.acmeRoot != null;
+      in nameValuePair vhostConfig.serverName {
         group = mkDefault cfg.group;
-        webroot = vhostConfig.acmeRoot;
+        # if acmeRoot is null inherit config.security.acme
+        # Since config.security.acme.certs.<cert>.webroot's own default value
+        # should take precedence set priority higher than mkOptionDefault
+        webroot = mkOverride (if hasRoot then 1000 else 2000) vhostConfig.acmeRoot;
+        # Also nudge dnsProvider to null in case it is inherited
+        dnsProvider = mkOverride (if hasRoot then 1000 else 2000) null;
         extraDomainNames = vhostConfig.serverAliases;
       # Filter for enableACME-only vhosts. Don't want to create dud certs
       }) (filter (vhostConfig: vhostConfig.useACMEHost == null) acmeEnabledVhosts);
diff --git a/nixos/modules/services/web-servers/nginx/location-options.nix b/nixos/modules/services/web-servers/nginx/location-options.nix
index 56a5381e05c83..6fd00b3869745 100644
--- a/nixos/modules/services/web-servers/nginx/location-options.nix
+++ b/nixos/modules/services/web-servers/nginx/location-options.nix
@@ -102,7 +102,7 @@ with lib;
     };
 
     fastcgiParams = mkOption {
-      type = types.attrsOf types.str;
+      type = types.attrsOf (types.either types.str types.path);
       default = {};
       description = ''
         FastCGI parameters to override.  Unlike in the Nginx
diff --git a/nixos/modules/services/web-servers/nginx/vhost-options.nix b/nixos/modules/services/web-servers/nginx/vhost-options.nix
index 7ee041d372113..c4e8285dc48bd 100644
--- a/nixos/modules/services/web-servers/nginx/vhost-options.nix
+++ b/nixos/modules/services/web-servers/nginx/vhost-options.nix
@@ -3,7 +3,7 @@
 # has additional options that affect the web server as a whole, like
 # the user/group to run under.)
 
-{ lib, ... }:
+{ config, lib, ... }:
 
 with lib;
 {
@@ -85,9 +85,12 @@ with lib;
     };
 
     acmeRoot = mkOption {
-      type = types.str;
+      type = types.nullOr types.str;
       default = "/var/lib/acme/acme-challenge";
-      description = "Directory for the acme challenge which is PUBLIC, don't put certs or keys in here";
+      description = ''
+        Directory for the acme challenge which is PUBLIC, don't put certs or keys in here.
+        Set to null to inherit from config.security.acme.
+      '';
     };
 
     acmeFallbackHost = mkOption {
@@ -147,6 +150,17 @@ with lib;
       '';
     };
 
+    kTLS = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable kTLS support.
+        Implementing TLS in the kernel (kTLS) improves performance by significantly
+        reducing the need for copying operations between user space and the kernel.
+        Required Nginx version 1.21.4 or later.
+      '';
+    };
+
     sslCertificate = mkOption {
       type = types.path;
       example = "/var/host.cert";
diff --git a/nixos/modules/services/x11/desktop-managers/pantheon.nix b/nixos/modules/services/x11/desktop-managers/pantheon.nix
index 3296b72204856..980a6b939d5a4 100644
--- a/nixos/modules/services/x11/desktop-managers/pantheon.nix
+++ b/nixos/modules/services/x11/desktop-managers/pantheon.nix
@@ -227,9 +227,9 @@ in
       environment.sessionVariables.GTK_CSD = "1";
       environment.etc."gtk-3.0/settings.ini".source = "${pkgs.pantheon.elementary-default-settings}/etc/gtk-3.0/settings.ini";
 
-      xdg.portal.extraPortals = with pkgs; [
-        pantheon.elementary-files
-        pantheon.elementary-settings-daemon
+      xdg.portal.extraPortals = with pkgs.pantheon; [
+        elementary-files
+        elementary-settings-daemon
         xdg-desktop-portal-pantheon
       ];
 
diff --git a/nixos/modules/services/x11/desktop-managers/pantheon.xml b/nixos/modules/services/x11/desktop-managers/pantheon.xml
index 64933349e7988..fe0a1c4962231 100644
--- a/nixos/modules/services/x11/desktop-managers/pantheon.xml
+++ b/nixos/modules/services/x11/desktop-managers/pantheon.xml
@@ -105,8 +105,14 @@ switchboard-with-plugs.override {
     </term>
     <listitem>
      <para>
-      AppCenter has been available since 20.03, but it is of little use. This is because there is no functioning PackageKit backend for Nix 2.0. The Flatpak backend will not work before <link xlink:href="https://github.com/elementary/appcenter/issues/1076">flag for Flatpak-only</link> is provided. See this <link xlink:href="https://github.com/NixOS/nixpkgs/issues/70214">issue</link>.
+      AppCenter has been available since 20.03, but it is of little use. This is because there is no functioning PackageKit backend for Nix 2.0. Starting from 21.11, the Flatpak backend should work so you can install some Flatpak applications using it. See this <link xlink:href="https://github.com/NixOS/nixpkgs/issues/70214">issue</link>.
      </para>
+     <para>
+      To use AppCenter on NixOS, add <literal>pantheon.appcenter</literal> to <xref linkend="opt-environment.systemPackages" />, <link linkend="module-services-flatpak">enable Flatpak support</link> and optionally add the <literal>appcenter</literal> Flatpak remote:
+     </para>
+<screen>
+<prompt>$ </prompt>flatpak remote-add --if-not-exists appcenter https://flatpak.elementary.io/repo.flatpakrepo
+</screen>
     </listitem>
    </varlistentry>
   </variablelist>
diff --git a/nixos/modules/services/x11/desktop-managers/xfce.nix b/nixos/modules/services/x11/desktop-managers/xfce.nix
index 25276e1d649ec..3cf92f98c56fd 100644
--- a/nixos/modules/services/x11/desktop-managers/xfce.nix
+++ b/nixos/modules/services/x11/desktop-managers/xfce.nix
@@ -9,7 +9,7 @@ in
 {
 
   meta = {
-    maintainers = with maintainers; [ ];
+    maintainers = teams.xfce.members;
   };
 
   imports = [
diff --git a/nixos/modules/services/x11/display-managers/default.nix b/nixos/modules/services/x11/display-managers/default.nix
index bdc46faa7fd0c..92b3af8527f1b 100644
--- a/nixos/modules/services/x11/display-managers/default.nix
+++ b/nixos/modules/services/x11/display-managers/default.nix
@@ -7,13 +7,14 @@
 # (e.g., KDE, Gnome or a plain xterm), and optionally the *window
 # manager* (e.g. kwin or twm).
 
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
 
   cfg = config.services.xserver;
+  opt = options.services.xserver;
   xorg = pkgs.xorg;
 
   fontconfig = config.fonts.fontconfig;
@@ -147,6 +148,7 @@ in
       xauthBin = mkOption {
         internal = true;
         default = "${xorg.xauth}/bin/xauth";
+        defaultText = literalExpression ''"''${pkgs.xorg.xauth}/bin/xauth"'';
         description = "Path to the <command>xauth</command> program used by display managers.";
       };
 
@@ -278,6 +280,9 @@ in
             defaultSessionFromLegacyOptions
           else
             null;
+        defaultText = literalDocBook ''
+          Taken from display manager settings or window manager settings, if either is set.
+        '';
         example = "gnome";
         description = ''
           Graphical session to pre-select in the session chooser (only effective for GDM, LightDM and SDDM).
@@ -337,11 +342,12 @@ in
 
       # Configuration for automatic login. Common for all DM.
       autoLogin = mkOption {
-        type = types.submodule {
+        type = types.submodule ({ config, options, ... }: {
           options = {
             enable = mkOption {
               type = types.bool;
-              default = cfg.displayManager.autoLogin.user != null;
+              default = config.user != null;
+              defaultText = literalExpression "config.${options.user} != null";
               description = ''
                 Automatically log in as <option>autoLogin.user</option>.
               '';
@@ -355,7 +361,7 @@ in
               '';
             };
           };
-        };
+        });
 
         default = {};
         description = ''
diff --git a/nixos/modules/services/x11/hardware/synaptics.nix b/nixos/modules/services/x11/hardware/synaptics.nix
index 22af869f1f8aa..93dd560bca407 100644
--- a/nixos/modules/services/x11/hardware/synaptics.nix
+++ b/nixos/modules/services/x11/hardware/synaptics.nix
@@ -1,8 +1,9 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let cfg = config.services.xserver.synaptics;
+    opt = options.services.xserver.synaptics;
     tapConfig = if cfg.tapButtons then enabledTapConfig else disabledTapConfig;
     enabledTapConfig = ''
       Option "MaxTapTime" "180"
@@ -77,24 +78,28 @@ in {
       horizTwoFingerScroll = mkOption {
         type = types.bool;
         default = cfg.twoFingerScroll;
+        defaultText = literalExpression "config.${opt.twoFingerScroll}";
         description = "Whether to enable horizontal two-finger drag-scrolling.";
       };
 
       vertTwoFingerScroll = mkOption {
         type = types.bool;
         default = cfg.twoFingerScroll;
+        defaultText = literalExpression "config.${opt.twoFingerScroll}";
         description = "Whether to enable vertical two-finger drag-scrolling.";
       };
 
       horizEdgeScroll = mkOption {
         type = types.bool;
         default = ! cfg.horizTwoFingerScroll;
+        defaultText = literalExpression "! config.${opt.horizTwoFingerScroll}";
         description = "Whether to enable horizontal edge drag-scrolling.";
       };
 
       vertEdgeScroll = mkOption {
         type = types.bool;
         default = ! cfg.vertTwoFingerScroll;
+        defaultText = literalExpression "! config.${opt.vertTwoFingerScroll}";
         description = "Whether to enable vertical edge drag-scrolling.";
       };
 
diff --git a/nixos/modules/services/x11/picom.nix b/nixos/modules/services/x11/picom.nix
index dbd4b1cefef18..b40e20bcd3572 100644
--- a/nixos/modules/services/x11/picom.nix
+++ b/nixos/modules/services/x11/picom.nix
@@ -1,10 +1,11 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
 let
 
   cfg = config.services.picom;
+  opt = options.services.picom;
 
   pairOf = x: with types;
     addCheck (listOf x) (y: length y == 2)
@@ -178,7 +179,16 @@ in {
 
     wintypes = mkOption {
       type = types.attrs;
-      default = { popup_menu = { opacity = cfg.menuOpacity; }; dropdown_menu = { opacity = cfg.menuOpacity; }; };
+      default = {
+        popup_menu = { opacity = cfg.menuOpacity; };
+        dropdown_menu = { opacity = cfg.menuOpacity; };
+      };
+      defaultText = literalExpression ''
+        {
+          popup_menu = { opacity = config.${opt.menuOpacity}; };
+          dropdown_menu = { opacity = config.${opt.menuOpacity}; };
+        }
+      '';
       example = {};
       description = ''
         Rules for specific window types.
diff --git a/nixos/modules/services/x11/window-managers/xmonad.nix b/nixos/modules/services/x11/window-managers/xmonad.nix
index a8f38046137a6..ecad411ff6835 100644
--- a/nixos/modules/services/x11/window-managers/xmonad.nix
+++ b/nixos/modules/services/x11/window-managers/xmonad.nix
@@ -39,10 +39,12 @@ in {
   options = {
     services.xserver.windowManager.xmonad = {
       enable = mkEnableOption "xmonad";
+
       haskellPackages = mkOption {
         default = pkgs.haskellPackages;
         defaultText = literalExpression "pkgs.haskellPackages";
         example = literalExpression "pkgs.haskell.packages.ghc784";
+        type = types.attrs;
         description = ''
           haskellPackages used to build Xmonad and other packages.
           This can be used to change the GHC version used to build
diff --git a/nixos/modules/system/activation/activation-script.nix b/nixos/modules/system/activation/activation-script.nix
index 4a32387db8da5..d6f14d01dbaa6 100644
--- a/nixos/modules/system/activation/activation-script.nix
+++ b/nixos/modules/system/activation/activation-script.nix
@@ -142,6 +142,7 @@ in
       readOnly = true;
       internal = true;
       default = systemActivationScript (removeAttrs config.system.activationScripts [ "script" ]) true;
+      defaultText = literalDocBook "generated activation script";
     };
 
     system.userActivationScripts = mkOption {
diff --git a/nixos/modules/system/activation/switch-to-configuration.pl b/nixos/modules/system/activation/switch-to-configuration.pl
index 053496441d81c..3fbab8b94c932 100644
--- a/nixos/modules/system/activation/switch-to-configuration.pl
+++ b/nixos/modules/system/activation/switch-to-configuration.pl
@@ -11,7 +11,6 @@ use Cwd 'abs_path';
 
 my $out = "@out@";
 
-# FIXME: maybe we should use /proc/1/exe to get the current systemd.
 my $curSystemd = abs_path("/run/current-system/sw/bin");
 
 # To be robust against interruption, record what units need to be started etc.
@@ -25,7 +24,7 @@ my $reloadByActivationFile = "/run/nixos/activation-reload-list";
 my $dryRestartByActivationFile = "/run/nixos/dry-activation-restart-list";
 my $dryReloadByActivationFile = "/run/nixos/dry-activation-reload-list";
 
-make_path("/run/nixos", { mode => 0755 });
+make_path("/run/nixos", { mode => oct(755) });
 
 my $action = shift @ARGV;
 
@@ -147,6 +146,79 @@ sub fingerprintUnit {
     return abs_path($s) . (-f "${s}.d/overrides.conf" ? " " . abs_path "${s}.d/overrides.conf" : "");
 }
 
+sub handleModifiedUnit {
+    my ($unit, $baseName, $newUnitFile, $activePrev, $unitsToStop, $unitsToStart, $unitsToReload, $unitsToRestart, $unitsToSkip) = @_;
+
+    if ($unit eq "sysinit.target" || $unit eq "basic.target" || $unit eq "multi-user.target" || $unit eq "graphical.target" || $unit =~ /\.path$/ || $unit =~ /\.slice$/) {
+        # Do nothing.  These cannot be restarted directly.
+
+        # Slices and Paths don't have to be restarted since
+        # properties (resource limits and inotify watches)
+        # seem to get applied on daemon-reload.
+    } elsif ($unit =~ /\.mount$/) {
+        # Reload the changed mount unit to force a remount.
+        $unitsToReload->{$unit} = 1;
+        recordUnit($reloadListFile, $unit);
+    } elsif ($unit =~ /\.socket$/) {
+        # FIXME: do something?
+        # Attempt to fix this: https://github.com/NixOS/nixpkgs/pull/141192
+        # Revert of the attempt: https://github.com/NixOS/nixpkgs/pull/147609
+        # More details: https://github.com/NixOS/nixpkgs/issues/74899#issuecomment-981142430
+    } else {
+        my $unitInfo = parseUnit($newUnitFile);
+        if (boolIsTrue($unitInfo->{'X-ReloadIfChanged'} // "no")) {
+            $unitsToReload->{$unit} = 1;
+            recordUnit($reloadListFile, $unit);
+        }
+        elsif (!boolIsTrue($unitInfo->{'X-RestartIfChanged'} // "yes") || boolIsTrue($unitInfo->{'RefuseManualStop'} // "no") || boolIsTrue($unitInfo->{'X-OnlyManualStart'} // "no")) {
+            $unitsToSkip->{$unit} = 1;
+        } else {
+            # It doesn't make sense to stop and start non-services because
+            # they can't have ExecStop=
+            if (!boolIsTrue($unitInfo->{'X-StopIfChanged'} // "yes") || $unit !~ /\.service$/) {
+                # This unit should be restarted instead of
+                # stopped and started.
+                $unitsToRestart->{$unit} = 1;
+                recordUnit($restartListFile, $unit);
+            } else {
+                # If this unit is socket-activated, then stop the
+                # socket unit(s) as well, and restart the
+                # socket(s) instead of the service.
+                my $socketActivated = 0;
+                if ($unit =~ /\.service$/) {
+                    my @sockets = split / /, ($unitInfo->{Sockets} // "");
+                    if (scalar @sockets == 0) {
+                        @sockets = ("$baseName.socket");
+                    }
+                    foreach my $socket (@sockets) {
+                        if (defined $activePrev->{$socket}) {
+                            $unitsToStop->{$socket} = 1;
+                            # Only restart sockets that actually
+                            # exist in new configuration:
+                            if (-e "$out/etc/systemd/system/$socket") {
+                                $unitsToStart->{$socket} = 1;
+                                recordUnit($startListFile, $socket);
+                                $socketActivated = 1;
+                            }
+                        }
+                    }
+                }
+
+                # If the unit is not socket-activated, record
+                # that this unit needs to be started below.
+                # We write this to a file to ensure that the
+                # service gets restarted if we're interrupted.
+                if (!$socketActivated) {
+                    $unitsToStart->{$unit} = 1;
+                    recordUnit($startListFile, $unit);
+                }
+
+                $unitsToStop->{$unit} = 1;
+            }
+        }
+    }
+}
+
 # Figure out what units need to be stopped, started, restarted or reloaded.
 my (%unitsToStop, %unitsToSkip, %unitsToStart, %unitsToRestart, %unitsToReload);
 
@@ -219,65 +291,7 @@ while (my ($unit, $state) = each %{$activePrev}) {
         }
 
         elsif (fingerprintUnit($prevUnitFile) ne fingerprintUnit($newUnitFile)) {
-            if ($unit eq "sysinit.target" || $unit eq "basic.target" || $unit eq "multi-user.target" || $unit eq "graphical.target") {
-                # Do nothing.  These cannot be restarted directly.
-            } elsif ($unit =~ /\.mount$/) {
-                # Reload the changed mount unit to force a remount.
-                $unitsToReload{$unit} = 1;
-                recordUnit($reloadListFile, $unit);
-            } elsif ($unit =~ /\.socket$/ || $unit =~ /\.path$/ || $unit =~ /\.slice$/) {
-                # FIXME: do something?
-            } else {
-                my $unitInfo = parseUnit($newUnitFile);
-                if (boolIsTrue($unitInfo->{'X-ReloadIfChanged'} // "no")) {
-                    $unitsToReload{$unit} = 1;
-                    recordUnit($reloadListFile, $unit);
-                }
-                elsif (!boolIsTrue($unitInfo->{'X-RestartIfChanged'} // "yes") || boolIsTrue($unitInfo->{'RefuseManualStop'} // "no") || boolIsTrue($unitInfo->{'X-OnlyManualStart'} // "no")) {
-                    $unitsToSkip{$unit} = 1;
-                } else {
-                    if (!boolIsTrue($unitInfo->{'X-StopIfChanged'} // "yes")) {
-                        # This unit should be restarted instead of
-                        # stopped and started.
-                        $unitsToRestart{$unit} = 1;
-                        recordUnit($restartListFile, $unit);
-                    } else {
-                        # If this unit is socket-activated, then stop the
-                        # socket unit(s) as well, and restart the
-                        # socket(s) instead of the service.
-                        my $socketActivated = 0;
-                        if ($unit =~ /\.service$/) {
-                            my @sockets = split / /, ($unitInfo->{Sockets} // "");
-                            if (scalar @sockets == 0) {
-                                @sockets = ("$baseName.socket");
-                            }
-                            foreach my $socket (@sockets) {
-                                if (defined $activePrev->{$socket}) {
-                                    $unitsToStop{$socket} = 1;
-                                    # Only restart sockets that actually
-                                    # exist in new configuration:
-                                    if (-e "$out/etc/systemd/system/$socket") {
-                                        $unitsToStart{$socket} = 1;
-                                        recordUnit($startListFile, $socket);
-                                        $socketActivated = 1;
-                                    }
-                                }
-                            }
-                        }
-
-                        # If the unit is not socket-activated, record
-                        # that this unit needs to be started below.
-                        # We write this to a file to ensure that the
-                        # service gets restarted if we're interrupted.
-                        if (!$socketActivated) {
-                            $unitsToStart{$unit} = 1;
-                            recordUnit($startListFile, $unit);
-                        }
-
-                        $unitsToStop{$unit} = 1;
-                    }
-                }
-            }
+            handleModifiedUnit($unit, $baseName, $newUnitFile, $activePrev, \%unitsToStop, \%unitsToStart, \%unitsToReload, \%unitsToRestart, \%unitsToSkip);
         }
     }
 }
@@ -348,8 +362,14 @@ foreach my $device (keys %$prevSwaps) {
 
 # Should we have systemd re-exec itself?
 my $prevSystemd = abs_path("/proc/1/exe") // "/unknown";
+my $prevSystemdSystemConfig = abs_path("/etc/systemd/system.conf") // "/unknown";
 my $newSystemd = abs_path("@systemd@/lib/systemd/systemd") or die;
+my $newSystemdSystemConfig = abs_path("$out/etc/systemd/system.conf") // "/unknown";
+
 my $restartSystemd = $prevSystemd ne $newSystemd;
+if ($prevSystemdSystemConfig ne $newSystemdSystemConfig) {
+    $restartSystemd = 1;
+}
 
 
 sub filterUnits {
@@ -382,12 +402,12 @@ if ($action eq "dry-activate") {
         split('\n', read_file($dryReloadByActivationFile, err_mode => 'quiet') // "");
 
     print STDERR "would restart systemd\n" if $restartSystemd;
+    print STDERR "would reload the following units: ", join(", ", sort(keys %unitsToReload)), "\n"
+        if scalar(keys %unitsToReload) > 0;
     print STDERR "would restart the following units: ", join(", ", sort(keys %unitsToRestart)), "\n"
         if scalar(keys %unitsToRestart) > 0;
     print STDERR "would start the following units: ", join(", ", @unitsToStartFiltered), "\n"
         if scalar @unitsToStartFiltered;
-    print STDERR "would reload the following units: ", join(", ", sort(keys %unitsToReload)), "\n"
-        if scalar(keys %unitsToReload) > 0;
     unlink($dryRestartByActivationFile);
     unlink($dryReloadByActivationFile);
     exit 0;
@@ -400,7 +420,7 @@ if (scalar (keys %unitsToStop) > 0) {
     print STDERR "stopping the following units: ", join(", ", @unitsToStopFiltered), "\n"
         if scalar @unitsToStopFiltered;
     # Use current version of systemctl binary before daemon is reexeced.
-    system("$curSystemd/systemctl", "stop", "--", sort(keys %unitsToStop)); # FIXME: ignore errors?
+    system("$curSystemd/systemctl", "stop", "--", sort(keys %unitsToStop));
 }
 
 print STDERR "NOT restarting the following changed units: ", join(", ", sort(keys %unitsToSkip)), "\n"
@@ -485,7 +505,7 @@ unlink($startListFile);
 
 
 # Print failed and new units.
-my (@failed, @new, @restarting);
+my (@failed, @new);
 my $activeNew = getActiveUnits;
 while (my ($unit, $state) = each %{$activeNew}) {
     if ($state->{state} eq "failed") {
@@ -501,7 +521,9 @@ while (my ($unit, $state) = each %{$activeNew}) {
             push @failed, $unit;
         }
     }
-    elsif ($state->{state} ne "failed" && !defined $activePrev->{$unit}) {
+    # Ignore scopes since they are not managed by this script but rather
+    # created and managed by third-party services via the systemd dbus API.
+    elsif ($state->{state} ne "failed" && !defined $activePrev->{$unit} && $unit !~ /\.scope$/) {
         push @new, $unit;
     }
 }
diff --git a/nixos/modules/system/activation/top-level.nix b/nixos/modules/system/activation/top-level.nix
index 58377ea64438e..501998fa399e2 100644
--- a/nixos/modules/system/activation/top-level.nix
+++ b/nixos/modules/system/activation/top-level.nix
@@ -78,6 +78,13 @@ let
       export localeArchive="${config.i18n.glibcLocales}/lib/locale/locale-archive"
       substituteAll ${./switch-to-configuration.pl} $out/bin/switch-to-configuration
       chmod +x $out/bin/switch-to-configuration
+      ${optionalString (pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) ''
+        if ! output=$($perl/bin/perl -c $out/bin/switch-to-configuration 2>&1); then
+          echo "switch-to-configuration syntax is not valid:"
+          echo "$output"
+          exit 1
+        fi
+      ''}
 
       echo -n "${toString config.system.extraDependencies}" > $out/extra-dependencies
 
diff --git a/nixos/modules/system/boot/networkd.nix b/nixos/modules/system/boot/networkd.nix
index 2e17bdf6bb659..1145831ee2eaa 100644
--- a/nixos/modules/system/boot/networkd.nix
+++ b/nixos/modules/system/boot/networkd.nix
@@ -1,8 +1,8 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, pkgs, utils, ... }:
 
+with utils.systemdUtils.unitOptions;
+with utils.systemdUtils.lib;
 with lib;
-with import ./systemd-unit-options.nix { inherit config lib; };
-with import ./systemd-lib.nix { inherit config lib pkgs; };
 
 let
 
@@ -572,6 +572,7 @@ let
           "Family"
           "User"
           "SuppressPrefixLength"
+          "Type"
         ])
         (assertInt "TypeOfService")
         (assertRange "TypeOfService" 0 255)
@@ -584,6 +585,7 @@ let
         (assertValueOneOf "Family" ["ipv4" "ipv6" "both"])
         (assertInt "SuppressPrefixLength")
         (assertRange "SuppressPrefixLength" 0 128)
+        (assertValueOneOf "Type" ["blackhole" "unreachable" "prohibit"])
       ];
 
       sectionRoute = checkUnitConfig "Route" [
@@ -821,6 +823,16 @@ let
         (assertValueOneOf "OnLink" boolValues)
       ];
 
+      sectionDHCPServerStaticLease = checkUnitConfig "DHCPServerStaticLease" [
+        (assertOnlyFields [
+          "MACAddress"
+          "Address"
+        ])
+        (assertHasField "MACAddress")
+        (assertHasField "Address")
+        (assertMacAddress "MACAddress")
+      ];
+
     };
   };
 
@@ -1161,6 +1173,25 @@ let
     };
   };
 
+  dhcpServerStaticLeaseOptions = {
+    options = {
+      dhcpServerStaticLeaseConfig = mkOption {
+        default = {};
+        example = { MACAddress = "65:43:4a:5b:d8:5f"; Address = "192.168.1.42"; };
+        type = types.addCheck (types.attrsOf unitOption) check.network.sectionDHCPServerStaticLease;
+        description = ''
+          Each attribute in this set specifies an option in the
+          <literal>[DHCPServerStaticLease]</literal> section of the unit.  See
+          <citerefentry><refentrytitle>systemd.network</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry> for details.
+
+          Make sure to configure the corresponding client interface to use
+          <literal>ClientIdentifier=mac</literal>.
+        '';
+      };
+    };
+  };
+
   networkOptions = commonNetworkOptions // {
 
     linkConfig = mkOption {
@@ -1273,6 +1304,17 @@ let
       '';
     };
 
+    dhcpServerStaticLeases = mkOption {
+      default = [];
+      example = [ { MACAddress = "65:43:4a:5b:d8:5f"; Address = "192.168.1.42"; } ];
+      type = with types; listOf (submodule dhcpServerStaticLeaseOptions);
+      description = ''
+        A list of DHCPServerStaticLease sections to be added to the unit.  See
+        <citerefentry><refentrytitle>systemd.network</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
     ipv6Prefixes = mkOption {
       default = [];
       example = [ { AddressAutoconfiguration = true; OnLink = true; } ];
@@ -1644,6 +1686,10 @@ let
           [IPv6Prefix]
           ${attrsToSection x.ipv6PrefixConfig}
         '')
+        + flip concatMapStrings def.dhcpServerStaticLeases (x: ''
+          [DHCPServerStaticLease]
+          ${attrsToSection x.dhcpServerStaticLeaseConfig}
+        '')
         + def.extraConfig;
     };
 
diff --git a/nixos/modules/system/boot/plymouth.nix b/nixos/modules/system/boot/plymouth.nix
index 4b8194d2f85c1..78ae8e9d20b77 100644
--- a/nixos/modules/system/boot/plymouth.nix
+++ b/nixos/modules/system/boot/plymouth.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
@@ -7,6 +7,7 @@ let
   inherit (pkgs) plymouth nixos-icons;
 
   cfg = config.boot.plymouth;
+  opt = options.boot.plymouth;
 
   nixosBreezePlymouth = pkgs.plasma5Packages.breeze-plymouth.override {
     logoFile = cfg.logo;
@@ -71,6 +72,11 @@ in
 
       themePackages = mkOption {
         default = lib.optional (cfg.theme == "breeze") nixosBreezePlymouth;
+        defaultText = literalDocBook ''
+          A NixOS branded variant of the breeze theme when
+          <literal>config.${opt.theme} == "breeze"</literal>, otherwise
+          <literal>[ ]</literal>.
+        '';
         type = types.listOf types.package;
         description = ''
           Extra theme packages for plymouth.
diff --git a/nixos/modules/system/boot/stage-1-init.sh b/nixos/modules/system/boot/stage-1-init.sh
index 3dfcc010b64e8..98409ed6ae525 100644
--- a/nixos/modules/system/boot/stage-1-init.sh
+++ b/nixos/modules/system/boot/stage-1-init.sh
@@ -119,6 +119,18 @@ specialMount() {
 }
 source @earlyMountScript@
 
+# Copy initrd secrets from /.initrd-secrets to their actual destinations
+if [ -d "/.initrd-secrets" ]; then
+    #
+    # Secrets are named by their full destination pathname and stored
+    # under /.initrd-secrets/
+    #
+    for secret in $(cd "/.initrd-secrets"; find . -type f); do
+        mkdir -p $(dirname "/$secret")
+        cp "/.initrd-secrets/$secret" "$secret"
+    done
+fi
+
 # Log the script output to /dev/kmsg or /run/log/stage-1-init.log.
 mkdir -p /tmp
 mkfifo /tmp/stage-1-init.log.fifo
@@ -251,15 +263,6 @@ if test -n "$debug1devices"; then fail; fi
 @postDeviceCommands@
 
 
-# Return true if the machine is on AC power, or if we can't determine
-# whether it's on AC power.
-onACPower() {
-    ! test -d "/proc/acpi/battery" ||
-    ! ls /proc/acpi/battery/BAT[0-9]* > /dev/null 2>&1 ||
-    ! cat /proc/acpi/battery/BAT*/state | grep "^charging state" | grep -q "discharg"
-}
-
-
 # Check the specified file system, if appropriate.
 checkFS() {
     local device="$1"
@@ -304,13 +307,6 @@ checkFS() {
         return 0
     fi
 
-    # Don't run `fsck' if the machine is on battery power.  !!! Is
-    # this a good idea?
-    if ! onACPower; then
-        echo "on battery power, so no \`fsck' will be performed on \`$device'"
-        return 0
-    fi
-
     echo "checking $device..."
 
     fsckFlags=
diff --git a/nixos/modules/system/boot/stage-1.nix b/nixos/modules/system/boot/stage-1.nix
index adbed9d8d58e7..409424a5b0f65 100644
--- a/nixos/modules/system/boot/stage-1.nix
+++ b/nixos/modules/system/boot/stage-1.nix
@@ -411,8 +411,8 @@ let
         ${lib.concatStringsSep "\n" (mapAttrsToList (dest: source:
             let source' = if source == null then dest else toString source; in
               ''
-                mkdir -p $(dirname "$tmp/${dest}")
-                cp -a ${source'} "$tmp/${dest}"
+                mkdir -p $(dirname "$tmp/.initrd-secrets/${dest}")
+                cp -a ${source'} "$tmp/.initrd-secrets/${dest}"
               ''
           ) config.boot.initrd.secrets)
          }
diff --git a/nixos/modules/system/boot/stage-2-init.sh b/nixos/modules/system/boot/stage-2-init.sh
index afaca2e4158d7..a90f58042d2d6 100644..100755
--- a/nixos/modules/system/boot/stage-2-init.sh
+++ b/nixos/modules/system/boot/stage-2-init.sh
@@ -172,4 +172,5 @@ echo "starting systemd..."
 
 PATH=/run/current-system/systemd/lib/systemd:@fsPackagesPath@ \
     LOCALE_ARCHIVE=/run/current-system/sw/lib/locale/locale-archive @systemdUnitPathEnvVar@ \
+    TZDIR=/etc/zoneinfo \
     exec @systemdExecutable@
diff --git a/nixos/modules/system/boot/systemd-nspawn.nix b/nixos/modules/system/boot/systemd-nspawn.nix
index b450d77429b21..02d2660add897 100644
--- a/nixos/modules/system/boot/systemd-nspawn.nix
+++ b/nixos/modules/system/boot/systemd-nspawn.nix
@@ -1,8 +1,8 @@
-{ config, lib , pkgs, ...}:
+{ config, lib, pkgs, utils, ...}:
 
+with utils.systemdUtils.unitOptions;
+with utils.systemdUtils.lib;
 with lib;
-with import ./systemd-unit-options.nix { inherit config lib; };
-with import ./systemd-lib.nix { inherit config lib pkgs; };
 
 let
   cfg = config.systemd.nspawn;
diff --git a/nixos/modules/system/boot/systemd.nix b/nixos/modules/system/boot/systemd.nix
index 6e0ee437d9151..ec5dea075bbce 100644
--- a/nixos/modules/system/boot/systemd.nix
+++ b/nixos/modules/system/boot/systemd.nix
@@ -1,9 +1,9 @@
 { config, lib, pkgs, utils, ... }:
 
 with utils;
+with systemdUtils.unitOptions;
+with systemdUtils.lib;
 with lib;
-with import ./systemd-unit-options.nix { inherit config lib; };
-with import ./systemd-lib.nix { inherit config lib pkgs; };
 
 let
 
diff --git a/nixos/modules/tasks/filesystems/zfs.nix b/nixos/modules/tasks/filesystems/zfs.nix
index 65364801c32aa..3bc0dedec00e2 100644
--- a/nixos/modules/tasks/filesystems/zfs.nix
+++ b/nixos/modules/tasks/filesystems/zfs.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, utils, ... }:
+{ config, lib, options, pkgs, utils, ... }:
 #
 # TODO: zfs tunables
 
@@ -8,6 +8,7 @@ with lib;
 let
 
   cfgZfs = config.boot.zfs;
+  optZfs = options.boot.zfs;
   cfgExpandOnBoot = config.services.zfs.expandOnBoot;
   cfgSnapshots = config.services.zfs.autoSnapshot;
   cfgSnapFlags = cfgSnapshots.flags;
@@ -112,6 +113,7 @@ in
         readOnly = true;
         type = types.bool;
         default = inInitrd || inSystem;
+        defaultText = literalDocBook "<literal>true</literal> if ZFS filesystem support is enabled";
         description = "True if ZFS filesystem support is enabled";
       };
 
@@ -346,6 +348,7 @@ in
     services.zfs.zed = {
       enableMail = mkEnableOption "ZED's ability to send emails" // {
         default = cfgZfs.package.enableMail;
+        defaultText = literalExpression "config.${optZfs.package}.enableMail";
       };
 
       settings = mkOption {
diff --git a/nixos/modules/tasks/network-interfaces.nix b/nixos/modules/tasks/network-interfaces.nix
index 49901cda848df..3d1fa793eb378 100644
--- a/nixos/modules/tasks/network-interfaces.nix
+++ b/nixos/modules/tasks/network-interfaces.nix
@@ -6,6 +6,7 @@ with utils;
 let
 
   cfg = config.networking;
+  opt = options.networking;
   interfaces = attrValues cfg.interfaces;
   hasVirtuals = any (i: i.virtual) interfaces;
   hasSits = cfg.sits != { };
@@ -1169,6 +1170,9 @@ in
 
     networking.tempAddresses = mkOption {
       default = if cfg.enableIPv6 then "default" else "disabled";
+      defaultText = literalExpression ''
+        if ''${config.${opt.enableIPv6}} then "default" else "disabled"
+      '';
       type = types.enum (lib.attrNames tempaddrValues);
       description = ''
         Whether to enable IPv6 Privacy Extensions for interfaces not
diff --git a/nixos/modules/tasks/snapraid.nix b/nixos/modules/tasks/snapraid.nix
index ff956f3067096..c8dde5b48993a 100644
--- a/nixos/modules/tasks/snapraid.nix
+++ b/nixos/modules/tasks/snapraid.nix
@@ -207,7 +207,7 @@ in
             SystemCallArchitectures = "native";
             SystemCallFilter = "@system-service";
             SystemCallErrorNumber = "EPERM";
-            CapabilityBoundingSet = "CAP_DAC_OVERRIDE" ++
+            CapabilityBoundingSet = "CAP_DAC_OVERRIDE" +
               lib.optionalString cfg.touchBeforeSync " CAP_FOWNER";
 
             ProtectSystem = "strict";
diff --git a/nixos/modules/virtualisation/cri-o.nix b/nixos/modules/virtualisation/cri-o.nix
index 38766113f3916..cf51100015033 100644
--- a/nixos/modules/virtualisation/cri-o.nix
+++ b/nixos/modules/virtualisation/cri-o.nix
@@ -71,6 +71,10 @@ in
     package = mkOption {
       type = types.package;
       default = crioPackage;
+      defaultText = literalDocBook ''
+        <literal>pkgs.cri-o</literal> built with
+        <literal>config.${opt.extraPackages}</literal>.
+      '';
       internal = true;
       description = ''
         The final CRI-O package (including extra packages).
diff --git a/nixos/modules/virtualisation/docker-rootless.nix b/nixos/modules/virtualisation/docker-rootless.nix
new file mode 100644
index 0000000000000..0e7f050314208
--- /dev/null
+++ b/nixos/modules/virtualisation/docker-rootless.nix
@@ -0,0 +1,98 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.virtualisation.docker.rootless;
+  proxy_env = config.networking.proxy.envVars;
+  settingsFormat = pkgs.formats.json {};
+  daemonSettingsFile = settingsFormat.generate "daemon.json" cfg.daemon.settings;
+
+in
+
+{
+  ###### interface
+
+  options.virtualisation.docker.rootless = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        This option enables docker in a rootless mode, a daemon that manages
+        linux containers. To interact with the daemon, one needs to set
+        <command>DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock</command>.
+      '';
+    };
+
+    setSocketVariable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Point <command>DOCKER_HOST</command> to rootless Docker instance for
+        normal users by default.
+      '';
+    };
+
+    daemon.settings = mkOption {
+      type = settingsFormat.type;
+      default = { };
+      example = {
+        ipv6 = true;
+        "fixed-cidr-v6" = "fd00::/80";
+      };
+      description = ''
+        Configuration for docker daemon. The attributes are serialized to JSON used as daemon.conf.
+        See https://docs.docker.com/engine/reference/commandline/dockerd/#daemon-configuration-file
+      '';
+    };
+
+    package = mkOption {
+      default = pkgs.docker;
+      defaultText = literalExpression "pkgs.docker";
+      type = types.package;
+      example = literalExpression "pkgs.docker-edge";
+      description = ''
+        Docker package to be used in the module.
+      '';
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+
+    environment.extraInit = optionalString cfg.setSocketVariable ''
+      if [ -z "$DOCKER_HOST" -a -n "$XDG_RUNTIME_DIR" ]; then
+        export DOCKER_HOST="unix://$XDG_RUNTIME_DIR/docker.sock"
+      fi
+    '';
+
+    # Taken from https://github.com/moby/moby/blob/master/contrib/dockerd-rootless-setuptool.sh
+    systemd.user.services.docker = {
+      wantedBy = [ "default.target" ];
+      description = "Docker Application Container Engine (Rootless)";
+      # needs newuidmap from pkgs.shadow
+      path = [ "/run/wrappers" ];
+      environment = proxy_env;
+      unitConfig.StartLimitInterval = "60s";
+      serviceConfig = {
+        Type = "notify";
+        ExecStart = "${cfg.package}/bin/dockerd-rootless --config-file=${daemonSettingsFile}";
+        ExecReload = "${pkgs.procps}/bin/kill -s HUP $MAINPID";
+        TimeoutSec = 0;
+        RestartSec = 2;
+        Restart = "always";
+        StartLimitBurst = 3;
+        LimitNOFILE = "infinity";
+        LimitNPROC = "infinity";
+        LimitCORE = "infinity";
+        Delegate = true;
+        NotifyAccess = "all";
+        KillMode = "mixed";
+      };
+    };
+  };
+
+}
diff --git a/nixos/modules/virtualisation/docker.nix b/nixos/modules/virtualisation/docker.nix
index 06858e150309c..a69cbe55c7845 100644
--- a/nixos/modules/virtualisation/docker.nix
+++ b/nixos/modules/virtualisation/docker.nix
@@ -8,7 +8,8 @@ let
 
   cfg = config.virtualisation.docker;
   proxy_env = config.networking.proxy.envVars;
-
+  settingsFormat = pkgs.formats.json {};
+  daemonSettingsFile = settingsFormat.generate "daemon.json" cfg.daemon.settings;
 in
 
 {
@@ -52,6 +53,20 @@ in
           '';
       };
 
+    daemon.settings =
+      mkOption {
+        type = settingsFormat.type;
+        default = { };
+        example = {
+          ipv6 = true;
+          "fixed-cidr-v6" = "fd00::/80";
+        };
+        description = ''
+          Configuration for docker daemon. The attributes are serialized to JSON used as daemon.conf.
+          See https://docs.docker.com/engine/reference/commandline/dockerd/#daemon-configuration-file
+        '';
+      };
+
     enableNvidia =
       mkOption {
         type = types.bool;
@@ -171,12 +186,7 @@ in
             ""
             ''
               ${cfg.package}/bin/dockerd \
-                --group=docker \
-                --host=fd:// \
-                --log-driver=${cfg.logDriver} \
-                ${optionalString (cfg.storageDriver != null) "--storage-driver=${cfg.storageDriver}"} \
-                ${optionalString cfg.liveRestore "--live-restore" } \
-                ${optionalString cfg.enableNvidia "--add-runtime nvidia=${pkgs.nvidia-docker}/bin/nvidia-container-runtime" } \
+                --config-file=${daemonSettingsFile} \
                 ${cfg.extraOptions}
             ''];
           ExecReload=[
@@ -219,6 +229,19 @@ in
         { assertion = cfg.enableNvidia -> config.hardware.opengl.driSupport32Bit or false;
           message = "Option enableNvidia requires 32bit support libraries";
         }];
+
+      virtualisation.docker.daemon.settings = {
+        group = "docker";
+        hosts = [ "fd://" ];
+        log-driver = mkDefault cfg.logDriver;
+        storage-driver = mkIf (cfg.storageDriver != null) (mkDefault cfg.storageDriver);
+        live-restore = mkDefault cfg.liveRestore;
+        runtimes = mkIf cfg.enableNvidia {
+          nvidia = {
+            path = "${pkgs.nvidia-docker}/bin/nvidia-container-runtime";
+          };
+        };
+      };
     }
   ]);
 
diff --git a/nixos/modules/virtualisation/kubevirt.nix b/nixos/modules/virtualisation/kubevirt.nix
new file mode 100644
index 0000000000000..408822b6af0bd
--- /dev/null
+++ b/nixos/modules/virtualisation/kubevirt.nix
@@ -0,0 +1,30 @@
+{ config, lib, pkgs, ... }:
+
+{
+  imports = [
+    ../profiles/qemu-guest.nix
+  ];
+
+  config = {
+    fileSystems."/" = {
+      device = "/dev/disk/by-label/nixos";
+      fsType = "ext4";
+      autoResize = true;
+    };
+
+    boot.growPartition = true;
+    boot.kernelParams = [ "console=ttyS0" ];
+    boot.loader.grub.device = "/dev/vda";
+    boot.loader.timeout = 0;
+
+    services.qemuGuest.enable = true;
+    services.openssh.enable = true;
+    services.cloud-init.enable = true;
+    systemd.services."serial-getty@ttyS0".enable = true;
+
+    system.build.kubevirtImage = import ../../lib/make-disk-image.nix {
+      inherit lib config pkgs;
+      format = "qcow2";
+    };
+  };
+}
diff --git a/nixos/modules/virtualisation/nixos-containers.nix b/nixos/modules/virtualisation/nixos-containers.nix
index 279c965673539..0838a57f0f372 100644
--- a/nixos/modules/virtualisation/nixos-containers.nix
+++ b/nixos/modules/virtualisation/nixos-containers.nix
@@ -716,9 +716,9 @@ in
               { config =
                   { config, pkgs, ... }:
                   { services.postgresql.enable = true;
-                    services.postgresql.package = pkgs.postgresql_9_6;
+                    services.postgresql.package = pkgs.postgresql_10;
 
-                    system.stateVersion = "17.03";
+                    system.stateVersion = "21.05";
                   };
               };
           }
diff --git a/nixos/modules/virtualisation/podman.nix b/nixos/modules/virtualisation/podman/default.nix
index 385475c84a1aa..94fd727a4b564 100644
--- a/nixos/modules/virtualisation/podman.nix
+++ b/nixos/modules/virtualisation/podman/default.nix
@@ -39,8 +39,8 @@ let
 in
 {
   imports = [
-    ./podman-dnsname.nix
-    ./podman-network-socket.nix
+    ./dnsname.nix
+    ./network-socket.nix
     (lib.mkRenamedOptionModule [ "virtualisation" "podman" "libpod" ] [ "virtualisation" "containers" "containersConf" ])
   ];
 
diff --git a/nixos/modules/virtualisation/podman-dnsname.nix b/nixos/modules/virtualisation/podman/dnsname.nix
index beef19755079c..beef19755079c 100644
--- a/nixos/modules/virtualisation/podman-dnsname.nix
+++ b/nixos/modules/virtualisation/podman/dnsname.nix
diff --git a/nixos/modules/virtualisation/podman-network-socket-ghostunnel.nix b/nixos/modules/virtualisation/podman/network-socket-ghostunnel.nix
index a0e7e433164a4..a0e7e433164a4 100644
--- a/nixos/modules/virtualisation/podman-network-socket-ghostunnel.nix
+++ b/nixos/modules/virtualisation/podman/network-socket-ghostunnel.nix
diff --git a/nixos/modules/virtualisation/podman-network-socket.nix b/nixos/modules/virtualisation/podman/network-socket.nix
index 1429164630b3e..94d8da9d2b614 100644
--- a/nixos/modules/virtualisation/podman-network-socket.nix
+++ b/nixos/modules/virtualisation/podman/network-socket.nix
@@ -9,6 +9,10 @@ let
 
 in
 {
+  imports = [
+    ./network-socket-ghostunnel.nix
+  ];
+
   options.virtualisation.podman.networkSocket = {
     enable = mkOption {
       type = types.bool;
diff --git a/nixos/modules/virtualisation/qemu-vm.nix b/nixos/modules/virtualisation/qemu-vm.nix
index c7c3d7474645a..fa3e25afb03eb 100644
--- a/nixos/modules/virtualisation/qemu-vm.nix
+++ b/nixos/modules/virtualisation/qemu-vm.nix
@@ -835,6 +835,7 @@ in
 
     # FIXME: Consolidate this one day.
     virtualisation.qemu.options = mkMerge [
+      [ "-device virtio-keyboard" ]
       (mkIf pkgs.stdenv.hostPlatform.isx86 [
         "-usb" "-device usb-tablet,bus=usb-bus.0"
       ])
diff --git a/nixos/tests/acme.nix b/nixos/tests/acme.nix
index 72b7bb8a396a3..0dd7743c52b67 100644
--- a/nixos/tests/acme.nix
+++ b/nixos/tests/acme.nix
@@ -1,9 +1,9 @@
-let
+import ./make-test-python.nix ({ pkgs, lib, ... }: let
   commonConfig = ./common/acme/client;
 
   dnsServerIP = nodes: nodes.dnsserver.config.networking.primaryIPAddress;
 
-  dnsScript = {pkgs, nodes}: let
+  dnsScript = nodes: let
     dnsAddress = dnsServerIP nodes;
   in pkgs.writeShellScript "dns-hook.sh" ''
     set -euo pipefail
@@ -15,30 +15,137 @@ let
     fi
   '';
 
-  documentRoot = pkgs: pkgs.runCommand "docroot" {} ''
+  dnsConfig = nodes: {
+    dnsProvider = "exec";
+    dnsPropagationCheck = false;
+    credentialsFile = pkgs.writeText "wildcard.env" ''
+      EXEC_PATH=${dnsScript nodes}
+      EXEC_POLLING_INTERVAL=1
+      EXEC_PROPAGATION_TIMEOUT=1
+      EXEC_SEQUENCE_INTERVAL=1
+    '';
+  };
+
+  documentRoot = pkgs.runCommand "docroot" {} ''
     mkdir -p "$out"
     echo hello world > "$out/index.html"
   '';
 
-  vhostBase = pkgs: {
+  vhostBase = {
     forceSSL = true;
-    locations."/".root = documentRoot pkgs;
+    locations."/".root = documentRoot;
+  };
+
+  vhostBaseHttpd = {
+    forceSSL = true;
+    inherit documentRoot;
+  };
+
+  # Base specialisation config for testing general ACME features
+  webserverBasicConfig = {
+    services.nginx.enable = true;
+    services.nginx.virtualHosts."a.example.test" = vhostBase // {
+      enableACME = true;
+    };
   };
 
-in import ./make-test-python.nix ({ lib, ... }: {
+  # Generate specialisations for testing a web server
+  mkServerConfigs = { server, group, vhostBaseData, extraConfig ? {} }: let
+    baseConfig = { nodes, config, specialConfig ? {} }: lib.mkMerge [
+      {
+        security.acme = {
+          defaults = (dnsConfig nodes) // {
+            inherit group;
+          };
+          # One manual wildcard cert
+          certs."example.test" = {
+            domain = "*.example.test";
+          };
+        };
+
+        services."${server}" = {
+          enable = true;
+          virtualHosts = {
+            # Run-of-the-mill vhost using HTTP-01 validation
+            "${server}-http.example.test" = vhostBaseData // {
+              serverAliases = [ "${server}-http-alias.example.test" ];
+              enableACME = true;
+            };
+
+            # Another which inherits the DNS-01 config
+            "${server}-dns.example.test" = vhostBaseData // {
+              serverAliases = [ "${server}-dns-alias.example.test" ];
+              enableACME = true;
+              # Set acmeRoot to null instead of using the default of "/var/lib/acme/acme-challenge"
+              # webroot + dnsProvider are mutually exclusive.
+              acmeRoot = null;
+            };
+
+            # One using the wildcard certificate
+            "${server}-wildcard.example.test" = vhostBaseData // {
+              serverAliases = [ "${server}-wildcard-alias.example.test" ];
+              useACMEHost = "example.test";
+            };
+          };
+        };
+
+        # Used to determine if service reload was triggered
+        systemd.targets."test-renew-${server}" = {
+          wants = [ "acme-${server}-http.example.test.service" ];
+          after = [ "acme-${server}-http.example.test.service" "${server}-config-reload.service" ];
+        };
+      }
+      specialConfig
+      extraConfig
+    ];
+  in {
+    "${server}".configuration = { nodes, config, ... }: baseConfig {
+      inherit nodes config;
+    };
+
+    # Test that server reloads when an alias is removed (and subsequently test removal works in acme)
+    "${server}-remove-alias".configuration = { nodes, config, ... }: baseConfig {
+      inherit nodes config;
+      specialConfig = {
+        # Remove an alias, but create a standalone vhost in its place for testing.
+        # This configuration results in certificate errors as useACMEHost does not imply
+        # append extraDomains, and thus we can validate the SAN is removed.
+        services."${server}" = {
+          virtualHosts."${server}-http.example.test".serverAliases = lib.mkForce [];
+          virtualHosts."${server}-http-alias.example.test" = vhostBaseData // {
+            useACMEHost = "${server}-http.example.test";
+          };
+        };
+      };
+    };
+
+    # Test that the server reloads when only the acme configuration is changed.
+    "${server}-change-acme-conf".configuration = { nodes, config, ... }: baseConfig {
+      inherit nodes config;
+      specialConfig = {
+        security.acme.certs."${server}-http.example.test" = {
+          keyType = "ec384";
+          # Also test that postRun is exec'd as root
+          postRun = "id | grep root";
+        };
+      };
+    };
+  };
+
+in {
   name = "acme";
   meta.maintainers = lib.teams.acme.members;
 
   nodes = {
     # The fake ACME server which will respond to client requests
-    acme = { nodes, lib, ... }: {
+    acme = { nodes, ... }: {
       imports = [ ./common/acme/server ];
       networking.nameservers = lib.mkForce [ (dnsServerIP nodes) ];
     };
 
     # A fake DNS server which can be configured with records as desired
     # Used to test DNS-01 challenge
-    dnsserver = { nodes, pkgs, ... }: {
+    dnsserver = { nodes, ... }: {
       networking.firewall.allowedTCPPorts = [ 8055 53 ];
       networking.firewall.allowedUDPPorts = [ 53 ];
       systemd.services.pebble-challtestsrv = {
@@ -54,7 +161,7 @@ in import ./make-test-python.nix ({ lib, ... }: {
     };
 
     # A web server which will be the node requesting certs
-    webserver = { pkgs, nodes, lib, config, ... }: {
+    webserver = { nodes, config, ... }: {
       imports = [ commonConfig ];
       networking.nameservers = lib.mkForce [ (dnsServerIP nodes) ];
       networking.firewall.allowedTCPPorts = [ 80 443 ];
@@ -63,130 +170,142 @@ in import ./make-test-python.nix ({ lib, ... }: {
       environment.systemPackages = [ pkgs.openssl ];
 
       # Set log level to info so that we can see when the service is reloaded
-      services.nginx.enable = true;
       services.nginx.logError = "stderr info";
 
-      # First tests configure a basic cert and run a bunch of openssl checks
-      services.nginx.virtualHosts."a.example.test" = (vhostBase pkgs) // {
-        enableACME = true;
-      };
-
-      # Used to determine if service reload was triggered
-      systemd.targets.test-renew-nginx = {
-        wants = [ "acme-a.example.test.service" ];
-        after = [ "acme-a.example.test.service" "nginx-config-reload.service" ];
-      };
-
-      # Test that account creation is collated into one service
-      specialisation.account-creation.configuration = { nodes, pkgs, lib, ... }: let
-        email = "newhostmaster@example.test";
-        caDomain = nodes.acme.config.test-support.acme.caDomain;
-        # Exit 99 to make it easier to track if this is the reason a renew failed
-        testScript = ''
-          test -e accounts/${caDomain}/${email}/account.json || exit 99
-        '';
-      in {
-        security.acme.email = lib.mkForce email;
-        systemd.services."b.example.test".preStart = testScript;
-        systemd.services."c.example.test".preStart = testScript;
-
-        services.nginx.virtualHosts."b.example.test" = (vhostBase pkgs) // {
-          enableACME = true;
-        };
-        services.nginx.virtualHosts."c.example.test" = (vhostBase pkgs) // {
-          enableACME = true;
-        };
-      };
-
-      # Cert config changes will not cause the nginx configuration to change.
-      # This tests that the reload service is correctly triggered.
-      # It also tests that postRun is exec'd as root
-      specialisation.cert-change.configuration = { pkgs, ... }: {
-        security.acme.certs."a.example.test".keyType = "ec384";
-        security.acme.certs."a.example.test".postRun = ''
-          set -euo pipefail
-          touch /home/test
-          chown root:root /home/test
-          echo testing > /home/test
-        '';
-      };
-
-      # Now adding an alias to ensure that the certs are updated
-      specialisation.nginx-aliases.configuration = { pkgs, ... }: {
-        services.nginx.virtualHosts."a.example.test" = {
-          serverAliases = [ "b.example.test" ];
-        };
-      };
-
-      # Test OCSP Stapling
-      specialisation.ocsp-stapling.configuration = { pkgs, ... }: {
-        security.acme.certs."a.example.test" = {
-          ocspMustStaple = true;
-        };
-        services.nginx.virtualHosts."a.example.com" = {
-          extraConfig = ''
-            ssl_stapling on;
-            ssl_stapling_verify on;
+      specialisation = {
+        # First derivation used to test general ACME features
+        general.configuration = { ... }: let
+          caDomain = nodes.acme.config.test-support.acme.caDomain;
+          email = config.security.acme.defaults.email;
+          # Exit 99 to make it easier to track if this is the reason a renew failed
+          accountCreateTester = ''
+            test -e accounts/${caDomain}/${email}/account.json || exit 99
           '';
-        };
-      };
-
-      # Test using Apache HTTPD
-      specialisation.httpd-aliases.configuration = { pkgs, config, lib, ... }: {
-        services.nginx.enable = lib.mkForce false;
-        services.httpd.enable = true;
-        services.httpd.adminAddr = config.security.acme.email;
-        services.httpd.virtualHosts."c.example.test" = {
-          serverAliases = [ "d.example.test" ];
-          forceSSL = true;
-          enableACME = true;
-          documentRoot = documentRoot pkgs;
-        };
-
-        # Used to determine if service reload was triggered
-        systemd.targets.test-renew-httpd = {
-          wants = [ "acme-c.example.test.service" ];
-          after = [ "acme-c.example.test.service" "httpd-config-reload.service" ];
-        };
-      };
-
-      # Validation via DNS-01 challenge
-      specialisation.dns-01.configuration = { pkgs, config, nodes, ... }: {
-        security.acme.certs."example.test" = {
-          domain = "*.example.test";
-          group = config.services.nginx.group;
-          dnsProvider = "exec";
-          dnsPropagationCheck = false;
-          credentialsFile = pkgs.writeText "wildcard.env" ''
-            EXEC_PATH=${dnsScript { inherit pkgs nodes; }}
-          '';
-        };
-
-        services.nginx.virtualHosts."dns.example.test" = (vhostBase pkgs) // {
-          useACMEHost = "example.test";
-        };
-      };
-
-      # Validate service relationships by adding a slow start service to nginx' wants.
-      # Reproducer for https://github.com/NixOS/nixpkgs/issues/81842
-      specialisation.slow-startup.configuration = { pkgs, config, nodes, lib, ... }: {
-        systemd.services.my-slow-service = {
-          wantedBy = [ "multi-user.target" "nginx.service" ];
-          before = [ "nginx.service" ];
-          preStart = "sleep 5";
-          script = "${pkgs.python3}/bin/python -m http.server";
+        in lib.mkMerge [
+          webserverBasicConfig
+          {
+            # Used to test that account creation is collated into one service.
+            # These should not run until after acme-finished-a.example.test.target
+            systemd.services."b.example.test".preStart = accountCreateTester;
+            systemd.services."c.example.test".preStart = accountCreateTester;
+
+            services.nginx.virtualHosts."b.example.test" = vhostBase // {
+              enableACME = true;
+            };
+            services.nginx.virtualHosts."c.example.test" = vhostBase // {
+              enableACME = true;
+            };
+          }
+        ];
+
+        # Test OCSP Stapling
+        ocsp-stapling.configuration = { ... }: lib.mkMerge [
+          webserverBasicConfig
+          {
+            security.acme.certs."a.example.test".ocspMustStaple = true;
+            services.nginx.virtualHosts."a.example.test" = {
+              extraConfig = ''
+                ssl_stapling on;
+                ssl_stapling_verify on;
+              '';
+            };
+          }
+        ];
+
+        # Validate service relationships by adding a slow start service to nginx' wants.
+        # Reproducer for https://github.com/NixOS/nixpkgs/issues/81842
+        slow-startup.configuration = { ... }: lib.mkMerge [
+          webserverBasicConfig
+          {
+            systemd.services.my-slow-service = {
+              wantedBy = [ "multi-user.target" "nginx.service" ];
+              before = [ "nginx.service" ];
+              preStart = "sleep 5";
+              script = "${pkgs.python3}/bin/python -m http.server";
+            };
+
+            services.nginx.virtualHosts."slow.example.test" = {
+              forceSSL = true;
+              enableACME = true;
+              locations."/".proxyPass = "http://localhost:8000";
+            };
+          }
+        ];
+
+        # Test lego internal server (listenHTTP option)
+        # Also tests useRoot option
+        lego-server.configuration = { ... }: {
+          security.acme.useRoot = true;
+          security.acme.certs."lego.example.test" = {
+            listenHTTP = ":80";
+            group = "nginx";
+          };
+          services.nginx.enable = true;
+          services.nginx.virtualHosts."lego.example.test" = {
+            useACMEHost = "lego.example.test";
+            onlySSL = true;
+          };
         };
 
-        services.nginx.virtualHosts."slow.example.com" = {
-          forceSSL = true;
-          enableACME = true;
-          locations."/".proxyPass = "http://localhost:8000";
+      # Test compatiblity with Caddy
+      # It only supports useACMEHost, hence not using mkServerConfigs
+      } // (let
+        baseCaddyConfig = { nodes, config, ... }: {
+          security.acme = {
+            defaults = (dnsConfig nodes) // {
+              group = config.services.caddy.group;
+            };
+            # One manual wildcard cert
+            certs."example.test" = {
+              domain = "*.example.test";
+            };
+          };
+
+          services.caddy = {
+            enable = true;
+            virtualHosts."a.exmaple.test" = {
+              useACMEHost = "example.test";
+              extraConfig = ''
+                root * ${documentRoot}
+              '';
+            };
+          };
         };
-      };
+      in {
+        caddy.configuration = baseCaddyConfig;
+
+        # Test that the server reloads when only the acme configuration is changed.
+        "caddy-change-acme-conf".configuration = { nodes, config, ... }: lib.mkMerge [
+          (baseCaddyConfig {
+            inherit nodes config;
+          })
+          {
+            security.acme.certs."example.test" = {
+              keyType = "ec384";
+            };
+          }
+        ];
+
+      # Test compatibility with Nginx
+      }) // (mkServerConfigs {
+          server = "nginx";
+          group = "nginx";
+          vhostBaseData = vhostBase;
+        })
+
+      # Test compatibility with Apache HTTPD
+        // (mkServerConfigs {
+          server = "httpd";
+          group = "wwwrun";
+          vhostBaseData = vhostBaseHttpd;
+          extraConfig = {
+            services.httpd.adminAddr = config.security.acme.defaults.email;
+          };
+        });
     };
 
     # The client will be used to curl the webserver to validate configuration
-    client = {nodes, lib, pkgs, ...}: {
+    client = { nodes, ... }: {
       imports = [ commonConfig ];
       networking.nameservers = lib.mkForce [ (dnsServerIP nodes) ];
 
@@ -195,7 +314,7 @@ in import ./make-test-python.nix ({ lib, ... }: {
     };
   };
 
-  testScript = {nodes, ...}:
+  testScript = { nodes, ... }:
     let
       caDomain = nodes.acme.config.test-support.acme.caDomain;
       newServerSystem = nodes.webserver.config.system.build.toplevel;
@@ -204,23 +323,26 @@ in import ./make-test-python.nix ({ lib, ... }: {
     # Note, wait_for_unit does not work for oneshot services that do not have RemainAfterExit=true,
     # this is because a oneshot goes from inactive => activating => inactive, and never
     # reaches the active state. Targets do not have this issue.
-
     ''
       import time
 
 
-      has_switched = False
+      def switch_to(node, name):
+          # On first switch, this will create a symlink to the current system so that we can
+          # quickly switch between derivations
+          root_specs = "/tmp/specialisation"
+          node.execute(
+            f"test -e {root_specs}"
+            f" || ln -s $(readlink /run/current-system)/specialisation {root_specs}"
+          )
 
+          switcher_path = f"/run/current-system/specialisation/{name}/bin/switch-to-configuration"
+          rc, _ = node.execute(f"test -e '{switcher_path}'")
+          if rc > 0:
+              switcher_path = f"/tmp/specialisation/{name}/bin/switch-to-configuration"
 
-      def switch_to(node, name):
-          global has_switched
-          if has_switched:
-              node.succeed(
-                  "${switchToNewServer}"
-              )
-          has_switched = True
           node.succeed(
-              f"/run/current-system/specialisation/{name}/bin/switch-to-configuration test"
+              f"{switcher_path} test"
           )
 
 
@@ -310,8 +432,7 @@ in import ./make-test-python.nix ({ lib, ... }: {
               return download_ca_certs(node, retries - 1)
 
 
-      client.start()
-      dnsserver.start()
+      start_all()
 
       dnsserver.wait_for_unit("pebble-challtestsrv.service")
       client.wait_for_unit("default.target")
@@ -320,19 +441,30 @@ in import ./make-test-python.nix ({ lib, ... }: {
           'curl --data \'{"host": "${caDomain}", "addresses": ["${nodes.acme.config.networking.primaryIPAddress}"]}\' http://${dnsServerIP nodes}:8055/add-a'
       )
 
-      acme.start()
-      webserver.start()
-
       acme.wait_for_unit("network-online.target")
       acme.wait_for_unit("pebble.service")
 
       download_ca_certs(client)
 
-      with subtest("Can request certificate with HTTPS-01 challenge"):
+      # Perform general tests first
+      switch_to(webserver, "general")
+
+      with subtest("Can request certificate with HTTP-01 challenge"):
           webserver.wait_for_unit("acme-finished-a.example.test.target")
+          check_fullchain(webserver, "a.example.test")
+          check_issuer(webserver, "a.example.test", "pebble")
+          webserver.wait_for_unit("nginx.service")
+          check_connection(client, "a.example.test")
+
+      with subtest("Runs 1 cert for account creation before others"):
+          webserver.wait_for_unit("acme-finished-b.example.test.target")
+          webserver.wait_for_unit("acme-finished-c.example.test.target")
+          check_connection(client, "b.example.test")
+          check_connection(client, "c.example.test")
 
       with subtest("Certificates and accounts have safe + valid permissions"):
-          group = "${nodes.webserver.config.security.acme.certs."a.example.test".group}"
+          # Nginx will set the group appropriately when enableACME is used
+          group = "nginx"
           webserver.succeed(
               f"test $(stat -L -c '%a %U %G' /var/lib/acme/a.example.test/*.pem | tee /dev/stderr | grep '640 acme {group}' | wc -l) -eq 5"
           )
@@ -346,12 +478,6 @@ in import ./make-test-python.nix ({ lib, ... }: {
               f"test $(find /var/lib/acme/accounts -type f -exec stat -L -c '%a %U %G' {{}} \\; | tee /dev/stderr | grep -v '600 acme {group}' | wc -l) -eq 0"
           )
 
-      with subtest("Certs are accepted by web server"):
-          webserver.succeed("systemctl start nginx.service")
-          check_fullchain(webserver, "a.example.test")
-          check_issuer(webserver, "a.example.test", "pebble")
-          check_connection(client, "a.example.test")
-
       # Selfsigned certs tests happen late so we aren't fighting the system init triggering cert renewal
       with subtest("Can generate valid selfsigned certs"):
           webserver.succeed("systemctl clean acme-a.example.test.service --what=state")
@@ -365,77 +491,107 @@ in import ./make-test-python.nix ({ lib, ... }: {
           # Will succeed if nginx can load the certs
           webserver.succeed("systemctl start nginx-config-reload.service")
 
-      with subtest("Can reload nginx when timer triggers renewal"):
-          webserver.succeed("systemctl start test-renew-nginx.target")
-          check_issuer(webserver, "a.example.test", "pebble")
-          check_connection(client, "a.example.test")
-
-      with subtest("Runs 1 cert for account creation before others"):
-          switch_to(webserver, "account-creation")
-          webserver.wait_for_unit("acme-finished-a.example.test.target")
-          check_connection(client, "a.example.test")
-          webserver.wait_for_unit("acme-finished-b.example.test.target")
-          webserver.wait_for_unit("acme-finished-c.example.test.target")
-          check_connection(client, "b.example.test")
-          check_connection(client, "c.example.test")
-
-      with subtest("Can reload web server when cert configuration changes"):
-          switch_to(webserver, "cert-change")
-          webserver.wait_for_unit("acme-finished-a.example.test.target")
-          check_connection_key_bits(client, "a.example.test", "384")
-          webserver.succeed("grep testing /home/test")
-          # Clean to remove the testing file (and anything else messy we did)
-          webserver.succeed("systemctl clean acme-a.example.test.service --what=state")
-
       with subtest("Correctly implements OCSP stapling"):
           switch_to(webserver, "ocsp-stapling")
           webserver.wait_for_unit("acme-finished-a.example.test.target")
           check_stapling(client, "a.example.test")
 
-      with subtest("Can request certificate with HTTPS-01 when nginx startup is delayed"):
+      with subtest("Can request certificate with HTTP-01 using lego's internal web server"):
+          switch_to(webserver, "lego-server")
+          webserver.wait_for_unit("acme-finished-lego.example.test.target")
+          webserver.wait_for_unit("nginx.service")
+          webserver.succeed("echo HENLO && systemctl cat nginx.service")
+          webserver.succeed("test \"$(stat -c '%U' /var/lib/acme/* | uniq)\" = \"root\"")
+          check_connection(client, "a.example.test")
+          check_connection(client, "lego.example.test")
+
+      with subtest("Can request certificate with HTTP-01 when nginx startup is delayed"):
+          webserver.execute("systemctl stop nginx")
           switch_to(webserver, "slow-startup")
-          webserver.wait_for_unit("acme-finished-slow.example.com.target")
-          check_issuer(webserver, "slow.example.com", "pebble")
-          check_connection(client, "slow.example.com")
+          webserver.wait_for_unit("acme-finished-slow.example.test.target")
+          check_issuer(webserver, "slow.example.test", "pebble")
+          webserver.wait_for_unit("nginx.service")
+          check_connection(client, "slow.example.test")
 
-      with subtest("Can request certificate for vhost + aliases (nginx)"):
-          # Check the key hash before and after adding an alias. It should not change.
-          # The previous test reverts the ed384 change
-          webserver.wait_for_unit("acme-finished-a.example.test.target")
-          switch_to(webserver, "nginx-aliases")
-          webserver.wait_for_unit("acme-finished-a.example.test.target")
-          check_issuer(webserver, "a.example.test", "pebble")
+      with subtest("Works with caddy"):
+          switch_to(webserver, "caddy")
+          webserver.wait_for_unit("acme-finished-example.test.target")
+          webserver.wait_for_unit("caddy.service")
+          # FIXME reloading caddy is not sufficient to load new certs.
+          # Restart it manually until this is fixed.
+          webserver.succeed("systemctl restart caddy.service")
           check_connection(client, "a.example.test")
-          check_connection(client, "b.example.test")
 
-      with subtest("Can request certificates for vhost + aliases (apache-httpd)"):
-          try:
-              switch_to(webserver, "httpd-aliases")
-              webserver.wait_for_unit("acme-finished-c.example.test.target")
-          except Exception as err:
-              _, output = webserver.execute(
-                  "cat /var/log/httpd/*.log && ls -al /var/lib/acme/acme-challenge"
-              )
-              print(output)
-              raise err
-          check_issuer(webserver, "c.example.test", "pebble")
-          check_connection(client, "c.example.test")
-          check_connection(client, "d.example.test")
-
-      with subtest("Can reload httpd when timer triggers renewal"):
-          # Switch to selfsigned first
-          webserver.succeed("systemctl clean acme-c.example.test.service --what=state")
-          webserver.succeed("systemctl start acme-selfsigned-c.example.test.service")
-          check_issuer(webserver, "c.example.test", "minica")
-          webserver.succeed("systemctl start httpd-config-reload.service")
-          webserver.succeed("systemctl start test-renew-httpd.target")
-          check_issuer(webserver, "c.example.test", "pebble")
-          check_connection(client, "c.example.test")
-
-      with subtest("Can request wildcard certificates using DNS-01 challenge"):
-          switch_to(webserver, "dns-01")
+      with subtest("security.acme changes reflect on caddy"):
+          switch_to(webserver, "caddy-change-acme-conf")
           webserver.wait_for_unit("acme-finished-example.test.target")
-          check_issuer(webserver, "example.test", "pebble")
-          check_connection(client, "dns.example.test")
+          webserver.wait_for_unit("caddy.service")
+          # FIXME reloading caddy is not sufficient to load new certs.
+          # Restart it manually until this is fixed.
+          webserver.succeed("systemctl restart caddy.service")
+          check_connection_key_bits(client, "a.example.test", "384")
+
+      domains = ["http", "dns", "wildcard"]
+      for server, logsrc in [
+          ("nginx", "journalctl -n 30 -u nginx.service"),
+          ("httpd", "tail -n 30 /var/log/httpd/*.log"),
+      ]:
+          wait_for_server = lambda: webserver.wait_for_unit(f"{server}.service")
+          with subtest(f"Works with {server}"):
+              try:
+                  switch_to(webserver, server)
+                  # Skip wildcard domain for this check ([:-1])
+                  for domain in domains[:-1]:
+                      webserver.wait_for_unit(
+                          f"acme-finished-{server}-{domain}.example.test.target"
+                      )
+              except Exception as err:
+                  _, output = webserver.execute(
+                      f"{logsrc} && ls -al /var/lib/acme/acme-challenge"
+                  )
+                  print(output)
+                  raise err
+
+              wait_for_server()
+
+              for domain in domains[:-1]:
+                  check_issuer(webserver, f"{server}-{domain}.example.test", "pebble")
+              for domain in domains:
+                  check_connection(client, f"{server}-{domain}.example.test")
+                  check_connection(client, f"{server}-{domain}-alias.example.test")
+
+          test_domain = f"{server}-{domains[0]}.example.test"
+
+          with subtest(f"Can reload {server} when timer triggers renewal"):
+              # Switch to selfsigned first
+              webserver.succeed(f"systemctl clean acme-{test_domain}.service --what=state")
+              webserver.succeed(f"systemctl start acme-selfsigned-{test_domain}.service")
+              check_issuer(webserver, test_domain, "minica")
+              webserver.succeed(f"systemctl start {server}-config-reload.service")
+              webserver.succeed(f"systemctl start test-renew-{server}.target")
+              check_issuer(webserver, test_domain, "pebble")
+              check_connection(client, test_domain)
+
+          with subtest("Can remove an alias from a domain + cert is updated"):
+              test_alias = f"{server}-{domains[0]}-alias.example.test"
+              switch_to(webserver, f"{server}-remove-alias")
+              webserver.wait_for_unit(f"acme-finished-{test_domain}.target")
+              wait_for_server()
+              check_connection(client, test_domain)
+              rc, _ = client.execute(
+                  f"openssl s_client -CAfile /tmp/ca.crt -connect {test_alias}:443"
+                  " </dev/null 2>/dev/null | openssl x509 -noout -text"
+                  f" | grep DNS: | grep {test_alias}"
+              )
+              assert rc > 0, "Removed extraDomainName was not removed from the cert"
+
+          with subtest("security.acme changes reflect on web server"):
+              # Switch back to normal server config first, reset everything.
+              switch_to(webserver, server)
+              wait_for_server()
+              switch_to(webserver, f"{server}-change-acme-conf")
+              webserver.wait_for_unit(f"acme-finished-{test_domain}.target")
+              wait_for_server()
+              check_connection_key_bits(client, test_domain, "384")
     '';
 })
diff --git a/nixos/tests/aesmd.nix b/nixos/tests/aesmd.nix
new file mode 100644
index 0000000000000..59c04fe7e96a3
--- /dev/null
+++ b/nixos/tests/aesmd.nix
@@ -0,0 +1,62 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "aesmd";
+  meta = {
+    maintainers = with lib.maintainers; [ veehaitch ];
+  };
+
+  machine = { lib, ... }: {
+    services.aesmd = {
+      enable = true;
+      settings = {
+        defaultQuotingType = "ecdsa_256";
+        proxyType = "direct";
+        whitelistUrl = "http://nixos.org";
+      };
+    };
+
+    # Should have access to the AESM socket
+    users.users."sgxtest" = {
+      isNormalUser = true;
+      extraGroups = [ "sgx" ];
+    };
+
+    # Should NOT have access to the AESM socket
+    users.users."nosgxtest".isNormalUser = true;
+
+    # We don't have a real SGX machine in NixOS tests
+    systemd.services.aesmd.unitConfig.AssertPathExists = lib.mkForce [ ];
+  };
+
+  testScript = ''
+    with subtest("aesmd.service starts"):
+      machine.wait_for_unit("aesmd.service")
+      status, main_pid = machine.systemctl("show --property MainPID --value aesmd.service")
+      assert status == 0, "Could not get MainPID of aesmd.service"
+      main_pid = main_pid.strip()
+
+    with subtest("aesmd.service runtime directory permissions"):
+      runtime_dir = "/run/aesmd";
+      res = machine.succeed(f"stat -c '%a %U %G' {runtime_dir}").strip()
+      assert "750 aesmd sgx" == res, f"{runtime_dir} does not have the expected permissions: {res}"
+
+    with subtest("aesm.socket available on host"):
+      socket_path = "/var/run/aesmd/aesm.socket"
+      machine.wait_until_succeeds(f"test -S {socket_path}")
+      machine.succeed(f"test 777 -eq $(stat -c '%a' {socket_path})")
+      for op in [ "-r", "-w", "-x" ]:
+        machine.succeed(f"sudo -u sgxtest test {op} {socket_path}")
+        machine.fail(f"sudo -u nosgxtest test {op} {socket_path}")
+
+    with subtest("Copies white_list_cert_to_be_verify.bin"):
+      whitelist_path = "/var/opt/aesmd/data/white_list_cert_to_be_verify.bin"
+      whitelist_perms = machine.succeed(
+        f"nsenter -m -t {main_pid} ${pkgs.coreutils}/bin/stat -c '%a' {whitelist_path}"
+      ).strip()
+      assert "644" == whitelist_perms, f"white_list_cert_to_be_verify.bin has permissions {whitelist_perms}"
+
+    with subtest("Writes and binds aesm.conf in service namespace"):
+      aesmd_config = machine.succeed(f"nsenter -m -t {main_pid} ${pkgs.coreutils}/bin/cat /etc/aesmd.conf")
+
+      assert aesmd_config == "whitelist url = http://nixos.org\nproxy type = direct\ndefault quoting type = ecdsa_256\n", "aesmd.conf differs"
+  '';
+})
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 3745a0194b937..63a990d3d8106 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -23,6 +23,7 @@ in
 {
   _3proxy = handleTest ./3proxy.nix {};
   acme = handleTest ./acme.nix {};
+  aesmd = handleTest ./aesmd.nix {};
   agda = handleTest ./agda.nix {};
   airsonic = handleTest ./airsonic.nix {};
   amazon-init-shell = handleTest ./amazon-init-shell.nix {};
@@ -69,6 +70,7 @@ in
   cloud-init = handleTest ./cloud-init.nix {};
   cntr = handleTest ./cntr.nix {};
   cockroachdb = handleTestOn ["x86_64-linux"] ./cockroachdb.nix {};
+  collectd = handleTest ./collectd.nix {};
   consul = handleTest ./consul.nix {};
   containers-bridge = handleTest ./containers-bridge.nix {};
   containers-custom-pkgs.nix = handleTest ./containers-custom-pkgs.nix {};
@@ -103,6 +105,7 @@ in
   dnscrypt-wrapper = handleTestOn ["x86_64-linux"] ./dnscrypt-wrapper {};
   doas = handleTest ./doas.nix {};
   docker = handleTestOn ["x86_64-linux"] ./docker.nix {};
+  docker-rootless = handleTestOn ["x86_64-linux"] ./docker-rootless.nix {};
   docker-edge = handleTestOn ["x86_64-linux"] ./docker-edge.nix {};
   docker-registry = handleTest ./docker-registry.nix {};
   docker-tools = handleTestOn ["x86_64-linux"] ./docker-tools.nix {};
@@ -247,6 +250,7 @@ in
   lxd-image-server = handleTest ./lxd-image-server.nix {};
   #logstash = handleTest ./logstash.nix {};
   lorri = handleTest ./lorri/default.nix {};
+  maddy = handleTest ./maddy.nix {};
   magic-wormhole-mailbox-server = handleTest ./magic-wormhole-mailbox-server.nix {};
   magnetico = handleTest ./magnetico.nix {};
   mailcatcher = handleTest ./mailcatcher.nix {};
@@ -360,6 +364,7 @@ in
   php = handleTest ./php {};
   php74 = handleTest ./php { php = pkgs.php74; };
   php80 = handleTest ./php { php = pkgs.php80; };
+  php81 = handleTest ./php { php = pkgs.php81; };
   pinnwand = handleTest ./pinnwand.nix {};
   plasma5 = handleTest ./plasma5.nix {};
   plasma5-systemd-start = handleTest ./plasma5-systemd-start.nix {};
@@ -368,9 +373,9 @@ in
   plikd = handleTest ./plikd.nix {};
   plotinus = handleTest ./plotinus.nix {};
   podgrab = handleTest ./podgrab.nix {};
-  podman = handleTestOn ["x86_64-linux"] ./podman.nix {};
-  podman-dnsname = handleTestOn ["x86_64-linux"] ./podman-dnsname.nix {};
-  podman-tls-ghostunnel = handleTestOn ["x86_64-linux"] ./podman-tls-ghostunnel.nix {};
+  podman = handleTestOn ["x86_64-linux"] ./podman/default.nix {};
+  podman-dnsname = handleTestOn ["x86_64-linux"] ./podman/dnsname.nix {};
+  podman-tls-ghostunnel = handleTestOn ["x86_64-linux"] ./podman/tls-ghostunnel.nix {};
   pomerium = handleTestOn ["x86_64-linux"] ./pomerium.nix {};
   postfix = handleTest ./postfix.nix {};
   postfix-raise-smtpd-tls-security-level = handleTest ./postfix-raise-smtpd-tls-security-level.nix {};
@@ -379,6 +384,7 @@ in
   postgresql = handleTest ./postgresql.nix {};
   postgresql-wal-receiver = handleTest ./postgresql-wal-receiver.nix {};
   powerdns = handleTest ./powerdns.nix {};
+  powerdns-admin = handleTest ./powerdns-admin.nix {};
   power-profiles-daemon = handleTest ./power-profiles-daemon.nix {};
   pppd = handleTest ./pppd.nix {};
   predictable-interface-names = handleTest ./predictable-interface-names.nix {};
@@ -409,6 +415,7 @@ in
   rss2email = handleTest ./rss2email.nix {};
   rsyslogd = handleTest ./rsyslogd.nix {};
   rxe = handleTest ./rxe.nix {};
+  sabnzbd = handleTest ./sabnzbd.nix {};
   samba = handleTest ./samba.nix {};
   samba-wsdd = handleTest ./samba-wsdd.nix {};
   sanoid = handleTest ./sanoid.nix {};
@@ -430,6 +437,7 @@ in
   solanum = handleTest ./solanum.nix {};
   solr = handleTest ./solr.nix {};
   sonarr = handleTest ./sonarr.nix {};
+  sourcehut = handleTest ./sourcehut.nix {};
   spacecookie = handleTest ./spacecookie.nix {};
   spark = handleTestOn ["x86_64-linux"] ./spark {};
   sslh = handleTest ./sslh.nix {};
@@ -453,6 +461,7 @@ in
   systemd-journal = handleTest ./systemd-journal.nix {};
   systemd-networkd = handleTest ./systemd-networkd.nix {};
   systemd-networkd-dhcpserver = handleTest ./systemd-networkd-dhcpserver.nix {};
+  systemd-networkd-dhcpserver-static-leases = handleTest ./systemd-networkd-dhcpserver-static-leases.nix {};
   systemd-networkd-ipv6-prefix-delegation = handleTest ./systemd-networkd-ipv6-prefix-delegation.nix {};
   systemd-networkd-vrf = handleTest ./systemd-networkd-vrf.nix {};
   systemd-nspawn = handleTest ./systemd-nspawn.nix {};
@@ -481,6 +490,7 @@ in
   ucarp = handleTest ./ucarp.nix {};
   udisks2 = handleTest ./udisks2.nix {};
   unbound = handleTest ./unbound.nix {};
+  unifi = handleTest ./unifi.nix {};
   unit-php = handleTest ./web-servers/unit-php.nix {};
   upnp = handleTest ./upnp.nix {};
   usbguard = handleTest ./usbguard.nix {};
diff --git a/nixos/tests/brscan5.nix b/nixos/tests/brscan5.nix
index 715191b383cb6..9aed742f6de79 100644
--- a/nixos/tests/brscan5.nix
+++ b/nixos/tests/brscan5.nix
@@ -23,6 +23,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     };
 
   testScript = ''
+    import re
     # sane loads libsane-brother5.so.1 successfully, and scanimage doesn't die
     strace = machine.succeed('strace scanimage -L 2>&1').split("\n")
     regexp = 'openat\(.*libsane-brother5.so.1", O_RDONLY|O_CLOEXEC\) = \d\d*$'
diff --git a/nixos/tests/collectd.nix b/nixos/tests/collectd.nix
new file mode 100644
index 0000000000000..cb196224a2317
--- /dev/null
+++ b/nixos/tests/collectd.nix
@@ -0,0 +1,33 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "collectd";
+  meta = { };
+
+  machine =
+    { pkgs, ... }:
+
+    {
+      services.collectd = {
+        enable = true;
+        plugins = {
+          rrdtool = ''
+            DataDir "/var/lib/collectd/rrd"
+          '';
+          load = "";
+        };
+      };
+      environment.systemPackages = [ pkgs.rrdtool ];
+    };
+
+  testScript = ''
+    machine.wait_for_unit("collectd.service")
+    hostname = machine.succeed("hostname").strip()
+    file = f"/var/lib/collectd/rrd/{hostname}/load/load.rrd"
+    machine.wait_for_file(file);
+    machine.succeed(f"rrdinfo {file} | logger")
+    # check that this file contains a shortterm metric
+    machine.succeed(f"rrdinfo {file} | grep -F 'ds[shortterm].min = '")
+    # check that there are frequent updates
+    machine.succeed(f"cp {file} before")
+    machine.wait_until_fails(f"cmp before {file}")
+  '';
+})
diff --git a/nixos/tests/common/acme/client/default.nix b/nixos/tests/common/acme/client/default.nix
index 1e9885e375c7f..9dbe345e7a011 100644
--- a/nixos/tests/common/acme/client/default.nix
+++ b/nixos/tests/common/acme/client/default.nix
@@ -5,9 +5,11 @@ let
 
 in {
   security.acme = {
-    server = "https://${caDomain}/dir";
-    email = "hostmaster@example.test";
     acceptTerms = true;
+    defaults = {
+      server = "https://${caDomain}/dir";
+      email = "hostmaster@example.test";
+    };
   };
 
   security.pki.certificateFiles = [ caCert ];
diff --git a/nixos/tests/common/acme/server/default.nix b/nixos/tests/common/acme/server/default.nix
index 1c3bfdf76b7e7..450d49e603996 100644
--- a/nixos/tests/common/acme/server/default.nix
+++ b/nixos/tests/common/acme/server/default.nix
@@ -120,6 +120,11 @@ in {
         enable = true;
         description = "Pebble ACME server";
         wantedBy = [ "network.target" ];
+        environment = {
+          # We're not testing lego, we're just testing our configuration.
+          # No need to sleep.
+          PEBBLE_VA_NOSLEEP = "1";
+        };
 
         serviceConfig = {
           RuntimeDirectory = "pebble";
diff --git a/nixos/tests/couchdb.nix b/nixos/tests/couchdb.nix
index 049532481b15f..453f5dcd66e86 100644
--- a/nixos/tests/couchdb.nix
+++ b/nixos/tests/couchdb.nix
@@ -56,5 +56,8 @@ with lib;
     couchdb3.succeed(
         "${curlJqCheck testlogin "GET" "_all_dbs" ". | length" "0"}"
     )
+    couchdb3.succeed(
+        "${curlJqCheck testlogin "GET" "_node/couchdb@127.0.0.1" ".couchdb" "Welcome"}"
+    )
   '';
 })
diff --git a/nixos/tests/docker-rootless.nix b/nixos/tests/docker-rootless.nix
new file mode 100644
index 0000000000000..e2a926eb3cb0e
--- /dev/null
+++ b/nixos/tests/docker-rootless.nix
@@ -0,0 +1,41 @@
+# This test runs docker and checks if simple container starts
+
+import ./make-test-python.nix ({ lib, pkgs, ...} : {
+  name = "docker-rootless";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ abbradar ];
+  };
+
+  nodes = {
+    machine = { pkgs, ... }: {
+      virtualisation.docker.rootless.enable = true;
+
+      users.users.alice = {
+        uid = 1000;
+        isNormalUser = true;
+      };
+    };
+  };
+
+  testScript = { nodes, ... }:
+    let
+      user = nodes.machine.config.users.users.alice;
+      sudo = lib.concatStringsSep " " [
+        "XDG_RUNTIME_DIR=/run/user/${toString user.uid}"
+        "DOCKER_HOST=unix:///run/user/${toString user.uid}/docker.sock"
+        "sudo" "--preserve-env=XDG_RUNTIME_DIR,DOCKER_HOST" "-u" "alice"
+      ];
+    in ''
+      machine.wait_for_unit("multi-user.target")
+
+      machine.succeed("loginctl enable-linger alice")
+      machine.wait_until_succeeds("${sudo} systemctl --user is-active docker.service")
+
+      machine.succeed("tar cv --files-from /dev/null | ${sudo} docker import - scratchimg")
+      machine.succeed(
+          "${sudo} docker run -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
+      )
+      machine.succeed("${sudo} docker ps | grep sleeping")
+      machine.succeed("${sudo} docker stop sleeping")
+    '';
+})
diff --git a/nixos/tests/docker-tools.nix b/nixos/tests/docker-tools.nix
index 19ebed3ebd0bd..8a240ddb17f24 100644
--- a/nixos/tests/docker-tools.nix
+++ b/nixos/tests/docker-tools.nix
@@ -215,6 +215,12 @@ import ./make-test-python.nix ({ pkgs, ... }: {
                 f"docker run --rm  ${examples.layersOrder.imageName} cat /tmp/layer{index}"
             )
 
+    with subtest("Ensure layers unpacked in correct order before runAsRoot runs"):
+        assert "abc" in docker.succeed(
+            "docker load --input='${examples.layersUnpackOrder}'",
+            "docker run --rm ${examples.layersUnpackOrder.imageName} cat /layer-order"
+        )
+
     with subtest("Ensure environment variables are correctly inherited"):
         docker.succeed(
             "docker load --input='${examples.environmentVariables}'"
diff --git a/nixos/tests/elk.nix b/nixos/tests/elk.nix
index ae746d7e1f03d..f42be00f23b82 100644
--- a/nixos/tests/elk.nix
+++ b/nixos/tests/elk.nix
@@ -40,9 +40,8 @@ let
 
             services = {
 
-              journalbeat = let lt6 = builtins.compareVersions
-                                        elk.journalbeat.version "6" < 0; in {
-                enable = true;
+              journalbeat = {
+                enable = elk ? journalbeat;
                 package = elk.journalbeat;
                 extraConfig = pkgs.lib.mkOptionDefault (''
                   logging:
@@ -51,14 +50,29 @@ let
                     metrics.enabled: false
                   output.elasticsearch:
                     hosts: [ "127.0.0.1:9200" ]
-                    ${pkgs.lib.optionalString lt6 "template.enabled: false"}
-                '' + pkgs.lib.optionalString (!lt6) ''
                   journalbeat.inputs:
                   - paths: []
                     seek: cursor
                 '');
               };
 
+              filebeat = {
+                enable = elk ? filebeat;
+                package = elk.filebeat;
+                inputs.journald.id = "everything";
+
+                inputs.log = {
+                  enabled = true;
+                  paths = [
+                    "/var/lib/filebeat/test"
+                  ];
+                };
+
+                settings = {
+                  logging.level = "info";
+                };
+              };
+
               metricbeat = {
                 enable = true;
                 package = elk.metricbeat;
@@ -142,27 +156,43 @@ let
       };
 
     passthru.elkPackages = elk;
-    testScript = ''
+    testScript =
+      let
+        valueObject = lib.optionalString (lib.versionAtLeast elk.elasticsearch.version "7") ".value";
+      in ''
       import json
 
 
-      def total_hits(message):
+      def expect_hits(message):
+          dictionary = {"query": {"match": {"message": message}}}
+          return (
+              "curl --silent --show-error --fail-with-body '${esUrl}/_search' "
+              + "-H 'Content-Type: application/json' "
+              + "-d '{}' ".format(json.dumps(dictionary))
+              + " | tee /dev/console"
+              + " | jq -es 'if . == [] then null else .[] | .hits.total${valueObject} > 0 end'"
+          )
+
+
+      def expect_no_hits(message):
           dictionary = {"query": {"match": {"message": message}}}
           return (
-              "curl --silent --show-error '${esUrl}/_search' "
+              "curl --silent --show-error --fail-with-body '${esUrl}/_search' "
               + "-H 'Content-Type: application/json' "
               + "-d '{}' ".format(json.dumps(dictionary))
-              + "| jq .hits.total"
+              + " | tee /dev/console"
+              + " | jq -es 'if . == [] then null else .[] | .hits.total${valueObject} == 0 end'"
           )
 
 
       def has_metricbeat():
           dictionary = {"query": {"match": {"event.dataset": {"query": "system.cpu"}}}}
           return (
-              "curl --silent --show-error '${esUrl}/_search' "
+              "curl --silent --show-error --fail-with-body '${esUrl}/_search' "
               + "-H 'Content-Type: application/json' "
               + "-d '{}' ".format(json.dumps(dictionary))
-              + "| jq '.hits.total > 0'"
+              + " | tee /dev/console"
+              + " | jq -es 'if . == [] then null else .[] | .hits.total${valueObject} > 0 end'"
           )
 
 
@@ -178,7 +208,8 @@ let
       # TODO: extend this test with multiple elasticsearch nodes
       #       and see if the status turns "green".
       one.wait_until_succeeds(
-          "curl --silent --show-error '${esUrl}/_cluster/health' | jq .status | grep -v red"
+          "curl --silent --show-error --fail-with-body '${esUrl}/_cluster/health'"
+          + " | jq -es 'if . == [] then null else .[] | .status != \"red\" end'"
       )
 
       with subtest("Perform some simple logstash tests"):
@@ -189,33 +220,50 @@ let
       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"
+              "curl --silent --show-error --fail-with-body 'http://localhost:5601/api/status'"
+              + " | jq -es 'if . == [] then null else .[] | .status.overall.state == \"green\" end'"
           )
 
       with subtest("Metricbeat is running"):
           one.wait_for_unit("metricbeat.service")
 
       with subtest("Metricbeat metrics arrive in elasticsearch"):
-          one.wait_until_succeeds(has_metricbeat() + " | tee /dev/console | grep 'true'")
+          one.wait_until_succeeds(has_metricbeat())
 
       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")
+          one.wait_until_succeeds(expect_hits("flowers"))
+          one.wait_until_succeeds(expect_no_hits("dragons"))
 
+    '' + lib.optionalString (elk ? journalbeat) ''
       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"
+              expect_hits("Supercalifragilisticexpialidocious")
           )
-
+    '' + lib.optionalString (elk ? filebeat) ''
+      with subtest(
+          "A message logged to the journal is ingested by elasticsearch via filebeat"
+      ):
+          one.wait_for_unit("filebeat.service")
+          one.execute("echo 'Superdupercalifragilisticexpialidocious' | systemd-cat")
+          one.wait_until_succeeds(
+              expect_hits("Superdupercalifragilisticexpialidocious")
+          )
+          one.execute(
+              "echo 'SuperdupercalifragilisticexpialidociousIndeed' >> /var/lib/filebeat/test"
+          )
+          one.wait_until_succeeds(
+              expect_hits("SuperdupercalifragilisticexpialidociousIndeed")
+          )
+    '' + ''
       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 ^'
+              '! curl --silent --show-error --fail-with-body "${esUrl}/_cat/indices" | grep logstash | grep ^'
           )
     '';
   }) { inherit pkgs system; };
@@ -235,7 +283,7 @@ in {
   #   elasticsearch = pkgs.elasticsearch7-oss;
   #   logstash      = pkgs.logstash7-oss;
   #   kibana        = pkgs.kibana7-oss;
-  #   journalbeat   = pkgs.journalbeat7;
+  #   filebeat      = pkgs.filebeat7;
   #   metricbeat    = pkgs.metricbeat7;
   # };
   unfree = lib.dontRecurseIntoAttrs {
@@ -250,7 +298,7 @@ in {
       elasticsearch = pkgs.elasticsearch7;
       logstash      = pkgs.logstash7;
       kibana        = pkgs.kibana7;
-      journalbeat   = pkgs.journalbeat7;
+      filebeat      = pkgs.filebeat7;
       metricbeat    = pkgs.metricbeat7;
     };
   };
diff --git a/nixos/tests/hydra/default.nix b/nixos/tests/hydra/default.nix
index d92f032b82922..ef5e677953dcd 100644
--- a/nixos/tests/hydra/default.nix
+++ b/nixos/tests/hydra/default.nix
@@ -17,7 +17,7 @@ let
   makeHydraTest = with pkgs.lib; name: package: makeTest {
     name = "hydra-${name}";
     meta = with pkgs.lib.maintainers; {
-      maintainers = [ pstn lewo ma27 ];
+      maintainers = [ lewo ma27 ];
     };
 
     machine = { pkgs, lib, ... }: {
diff --git a/nixos/tests/initrd-secrets.nix b/nixos/tests/initrd-secrets.nix
index 10dd908502d5b..113a9cebf7880 100644
--- a/nixos/tests/initrd-secrets.nix
+++ b/nixos/tests/initrd-secrets.nix
@@ -13,7 +13,12 @@ let
 
     machine = { ... }: {
       virtualisation.useBootLoader = true;
-      boot.initrd.secrets."/test" = secretInStore;
+      boot.initrd.secrets = {
+        "/test" = secretInStore;
+
+        # This should *not* need to be copied in postMountCommands
+        "/run/keys/test" = secretInStore;
+      };
       boot.initrd.postMountCommands = ''
         cp /test /mnt-root/secret-from-initramfs
       '';
@@ -26,7 +31,8 @@ let
       start_all()
       machine.wait_for_unit("multi-user.target")
       machine.succeed(
-          "cmp ${secretInStore} /secret-from-initramfs"
+          "cmp ${secretInStore} /secret-from-initramfs",
+          "cmp ${secretInStore} /run/keys/test",
       )
     '';
   };
diff --git a/nixos/tests/kubernetes/base.nix b/nixos/tests/kubernetes/base.nix
index 1f23ca55fb234..e1736f6fe1726 100644
--- a/nixos/tests/kubernetes/base.nix
+++ b/nixos/tests/kubernetes/base.nix
@@ -51,7 +51,6 @@ let
               environment.systemPackages = [ kubectl ];
               services.flannel.iface = "eth1";
               services.kubernetes = {
-                addons.dashboard.enable = true;
                 proxy.hostname = "${masterName}.${domain}";
 
                 easyCerts = true;
diff --git a/nixos/tests/kubernetes/default.nix b/nixos/tests/kubernetes/default.nix
index 90b73c68a76de..60ba482758fbe 100644
--- a/nixos/tests/kubernetes/default.nix
+++ b/nixos/tests/kubernetes/default.nix
@@ -1,5 +1,5 @@
 { system ? builtins.currentSystem
-, pkgs ? import <nixpkgs> { inherit system; }
+, pkgs ? import ../../.. { inherit system; }
 }:
 let
   dns = import ./dns.nix { inherit system pkgs; };
diff --git a/nixos/tests/kubernetes/dns.nix b/nixos/tests/kubernetes/dns.nix
index b6cd811c5aefd..3fd1dd31f746b 100644
--- a/nixos/tests/kubernetes/dns.nix
+++ b/nixos/tests/kubernetes/dns.nix
@@ -1,4 +1,4 @@
-{ system ? builtins.currentSystem, pkgs ? import <nixpkgs> { inherit system; } }:
+{ system ? builtins.currentSystem, pkgs ? import ../../.. { inherit system; } }:
 with import ./base.nix { inherit system; };
 let
   domain = "my.zyx";
@@ -100,7 +100,7 @@ let
       machine1.succeed("host redis.default.svc.cluster.local")
 
       # check dns inside the container
-      machine1.succeed("kubectl exec -ti probe -- /bin/host redis.default.svc.cluster.local")
+      machine1.succeed("kubectl exec probe -- /bin/host redis.default.svc.cluster.local")
     '';
   };
 
@@ -142,7 +142,7 @@ let
       machine2.succeed("host redis.default.svc.cluster.local")
 
       # check dns inside the container
-      machine1.succeed("kubectl exec -ti probe -- /bin/host redis.default.svc.cluster.local")
+      machine1.succeed("kubectl exec probe -- /bin/host redis.default.svc.cluster.local")
     '';
   };
 in {
diff --git a/nixos/tests/kubernetes/e2e.nix b/nixos/tests/kubernetes/e2e.nix
index 175d8413045ed..fb29d9cc6953f 100644
--- a/nixos/tests/kubernetes/e2e.nix
+++ b/nixos/tests/kubernetes/e2e.nix
@@ -1,4 +1,4 @@
-{ system ? builtins.currentSystem, pkgs ? import <nixpkgs> { inherit system; } }:
+{ system ? builtins.currentSystem, pkgs ? import ../../.. { inherit system; } }:
 with import ./base.nix { inherit system; };
 let
   domain = "my.zyx";
diff --git a/nixos/tests/kubernetes/rbac.nix b/nixos/tests/kubernetes/rbac.nix
index 3fc8ed0fbe389..ca73562256e47 100644
--- a/nixos/tests/kubernetes/rbac.nix
+++ b/nixos/tests/kubernetes/rbac.nix
@@ -1,4 +1,4 @@
-{ system ? builtins.currentSystem, pkgs ? import <nixpkgs> { inherit system; } }:
+{ system ? builtins.currentSystem, pkgs ? import ../../.. { inherit system; } }:
 with import ./base.nix { inherit system; };
 let
 
@@ -115,9 +115,9 @@ let
 
       machine1.wait_until_succeeds("kubectl get pod kubectl | grep Running")
 
-      machine1.wait_until_succeeds("kubectl exec -ti kubectl -- kubectl get pods")
-      machine1.fail("kubectl exec -ti kubectl -- kubectl create -f /kubectl-pod-2.json")
-      machine1.fail("kubectl exec -ti kubectl -- kubectl delete pods -l name=kubectl")
+      machine1.wait_until_succeeds("kubectl exec kubectl -- kubectl get pods")
+      machine1.fail("kubectl exec kubectl -- kubectl create -f /kubectl-pod-2.json")
+      machine1.fail("kubectl exec kubectl -- kubectl delete pods -l name=kubectl")
     '';
   };
 
@@ -152,9 +152,9 @@ let
 
       machine1.wait_until_succeeds("kubectl get pod kubectl | grep Running")
 
-      machine1.wait_until_succeeds("kubectl exec -ti kubectl -- kubectl get pods")
-      machine1.fail("kubectl exec -ti kubectl -- kubectl create -f /kubectl-pod-2.json")
-      machine1.fail("kubectl exec -ti kubectl -- kubectl delete pods -l name=kubectl")
+      machine1.wait_until_succeeds("kubectl exec kubectl -- kubectl get pods")
+      machine1.fail("kubectl exec kubectl -- kubectl create -f /kubectl-pod-2.json")
+      machine1.fail("kubectl exec kubectl -- kubectl delete pods -l name=kubectl")
     '';
   };
 
diff --git a/nixos/tests/maddy.nix b/nixos/tests/maddy.nix
new file mode 100644
index 0000000000000..581748c1fa59b
--- /dev/null
+++ b/nixos/tests/maddy.nix
@@ -0,0 +1,58 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "maddy";
+  meta = with pkgs.lib.maintainers; { maintainers = [ onny ]; };
+
+  nodes = {
+    server = { ... }: {
+      services.maddy = {
+        enable = true;
+        hostname = "server";
+        primaryDomain = "server";
+        openFirewall = true;
+      };
+    };
+
+    client = { ... }: {
+      environment.systemPackages = [
+        (pkgs.writers.writePython3Bin "send-testmail" { } ''
+          import smtplib
+          from email.mime.text import MIMEText
+
+          msg = MIMEText("Hello World")
+          msg['Subject'] = 'Test'
+          msg['From'] = "postmaster@server"
+          msg['To'] = "postmaster@server"
+          with smtplib.SMTP('server', 587) as smtp:
+              smtp.login('postmaster@server', 'test')
+              smtp.sendmail('postmaster@server', 'postmaster@server', msg.as_string())
+        '')
+        (pkgs.writers.writePython3Bin "test-imap" { } ''
+          import imaplib
+
+          with imaplib.IMAP4('server') as imap:
+              imap.login('postmaster@server', 'test')
+              imap.select()
+              status, refs = imap.search(None, 'ALL')
+              assert status == 'OK'
+              assert len(refs) == 1
+              status, msg = imap.fetch(refs[0], 'BODY[TEXT]')
+              assert status == 'OK'
+              assert msg[0][1].strip() == b"Hello World"
+        '')
+      ];
+    };
+  };
+
+  testScript = ''
+    start_all()
+    server.wait_for_unit("maddy.service")
+    server.wait_for_open_port(143)
+    server.wait_for_open_port(587)
+
+    server.succeed("echo test | maddyctl creds create postmaster@server")
+    server.succeed("maddyctl imap-acct create postmaster@server")
+
+    client.succeed("send-testmail")
+    client.succeed("test-imap")
+  '';
+})
diff --git a/nixos/tests/os-prober.nix b/nixos/tests/os-prober.nix
index a7b955d447215..c1e29b0f68b41 100644
--- a/nixos/tests/os-prober.nix
+++ b/nixos/tests/os-prober.nix
@@ -53,12 +53,12 @@ let
   };
   # /etc/nixos/configuration.nix for the vm
   configFile = pkgs.writeText "configuration.nix"  ''
-    {config, pkgs, ...}: ({
+    {config, pkgs, lib, ...}: ({
     imports =
           [ ./hardware-configuration.nix
             <nixpkgs/nixos/modules/testing/test-instrumentation.nix>
           ];
-    } // pkgs.lib.importJSON ${
+    } // lib.importJSON ${
       pkgs.writeText "simpleConfig.json" (builtins.toJSON simpleConfig)
     })
   '';
@@ -114,7 +114,7 @@ in {
         "${configFile}",
         "/etc/nixos/configuration.nix",
     )
-    machine.succeed("nixos-rebuild boot >&2")
+    machine.succeed("nixos-rebuild boot --show-trace >&2")
 
     machine.succeed("egrep 'menuentry.*debian' /boot/grub/grub.cfg")
   '';
diff --git a/nixos/tests/parsedmarc/default.nix b/nixos/tests/parsedmarc/default.nix
index d838d3b6a39c6..50b977723e9c7 100644
--- a/nixos/tests/parsedmarc/default.nix
+++ b/nixos/tests/parsedmarc/default.nix
@@ -4,6 +4,7 @@
 { pkgs, ... }@args:
 let
   inherit (import ../../lib/testing-python.nix args) makeTest;
+  inherit (pkgs) lib;
 
   dmarcTestReport = builtins.fetchurl {
     name = "dmarc-test-report";
@@ -54,7 +55,7 @@ in
   localMail = makeTest
     {
       name = "parsedmarc-local-mail";
-      meta = with pkgs.lib.maintainers; {
+      meta = with lib.maintainers; {
         maintainers = [ talyz ];
       };
 
@@ -83,7 +84,7 @@ in
             };
           };
 
-          services.elasticsearch.package = pkgs.elasticsearch7-oss;
+          services.elasticsearch.package = pkgs.elasticsearch-oss;
 
           environment.systemPackages = [
             (sendEmail "dmarc@localhost")
@@ -94,6 +95,7 @@ in
       testScript = { nodes }:
         let
           esPort = toString nodes.parsedmarc.config.services.elasticsearch.port;
+          valueObject = lib.optionalString (lib.versionAtLeast nodes.parsedmarc.config.services.elasticsearch.package.version "7") ".value";
         in ''
           parsedmarc.start()
           parsedmarc.wait_for_unit("postfix.service")
@@ -104,11 +106,15 @@ in
           )
 
           parsedmarc.fail(
-              "curl -sS -f http://localhost:${esPort}/_search?q=report_id:2940 | jq -e 'if .hits.total.value > 0 then true else null end'"
+              "curl -sS -f http://localhost:${esPort}/_search?q=report_id:2940"
+              + " | tee /dev/console"
+              + " | jq -es 'if . == [] then null else .[] | .hits.total${valueObject} > 0 end'"
           )
           parsedmarc.succeed("send-email")
           parsedmarc.wait_until_succeeds(
-              "curl -sS -f http://localhost:${esPort}/_search?q=report_id:2940 | jq -e 'if .hits.total.value > 0 then true else null end'"
+              "curl -sS -f http://localhost:${esPort}/_search?q=report_id:2940"
+              + " | tee /dev/console"
+              + " | jq -es 'if . == [] then null else .[] | .hits.total${valueObject} > 0 end'"
           )
         '';
     };
@@ -121,7 +127,7 @@ in
     in
       makeTest {
         name = "parsedmarc-external-mail";
-        meta = with pkgs.lib.maintainers; {
+        meta = with lib.maintainers; {
           maintainers = [ talyz ];
         };
 
@@ -153,7 +159,7 @@ in
                 };
               };
 
-              services.elasticsearch.package = pkgs.elasticsearch7-oss;
+              services.elasticsearch.package = pkgs.elasticsearch-oss;
 
               environment.systemPackages = [
                 pkgs.jq
@@ -201,6 +207,7 @@ in
         testScript = { nodes }:
           let
             esPort = toString nodes.parsedmarc.config.services.elasticsearch.port;
+            valueObject = lib.optionalString (lib.versionAtLeast nodes.parsedmarc.config.services.elasticsearch.package.version "7") ".value";
           in ''
             mail.start()
             mail.wait_for_unit("postfix.service")
@@ -213,11 +220,15 @@ in
             )
 
             parsedmarc.fail(
-                "curl -sS -f http://localhost:${esPort}/_search?q=report_id:2940 | jq -e 'if .hits.total.value > 0 then true else null end'"
+                "curl -sS -f http://localhost:${esPort}/_search?q=report_id:2940"
+                + " | tee /dev/console"
+                + " | jq -es 'if . == [] then null else .[] | .hits.total${valueObject} > 0 end'"
             )
             mail.succeed("send-email")
             parsedmarc.wait_until_succeeds(
-                "curl -sS -f http://localhost:${esPort}/_search?q=report_id:2940 | jq -e 'if .hits.total.value > 0 then true else null end'"
+                "curl -sS -f http://localhost:${esPort}/_search?q=report_id:2940"
+                + " | tee /dev/console"
+                + " | jq -es 'if . == [] then null else .[] | .hits.total${valueObject} > 0 end'"
             )
           '';
       };
diff --git a/nixos/tests/podman.nix b/nixos/tests/podman/default.nix
index 6184561e6dddf..b52a7f060ad66 100644
--- a/nixos/tests/podman.nix
+++ b/nixos/tests/podman/default.nix
@@ -1,6 +1,6 @@
 # This test runs podman and checks if simple container starts
 
-import ./make-test-python.nix (
+import ../make-test-python.nix (
   { pkgs, lib, ... }: {
     name = "podman";
     meta = {
@@ -48,7 +48,7 @@ import ./make-test-python.nix (
       start_all()
 
       with subtest("Run container as root with runc"):
-          podman.succeed("tar cvf scratchimg.tar --files-from /dev/null && podman import scratchimg.tar scratchimg")
+          podman.succeed("tar cv --files-from /dev/null | podman import - scratchimg")
           podman.succeed(
               "podman run --runtime=runc -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
           )
@@ -57,7 +57,7 @@ import ./make-test-python.nix (
           podman.succeed("podman rm sleeping")
 
       with subtest("Run container as root with crun"):
-          podman.succeed("tar cvf scratchimg.tar --files-from /dev/null && podman import scratchimg.tar scratchimg")
+          podman.succeed("tar cv --files-from /dev/null | podman import - scratchimg")
           podman.succeed(
               "podman run --runtime=crun -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
           )
@@ -66,7 +66,7 @@ import ./make-test-python.nix (
           podman.succeed("podman rm sleeping")
 
       with subtest("Run container as root with the default backend"):
-          podman.succeed("tar cvf scratchimg.tar --files-from /dev/null && podman import scratchimg.tar scratchimg")
+          podman.succeed("tar cv --files-from /dev/null | podman import - scratchimg")
           podman.succeed(
               "podman run -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
           )
@@ -78,7 +78,7 @@ import ./make-test-python.nix (
       podman.succeed("loginctl enable-linger alice")
 
       with subtest("Run container rootless with runc"):
-          podman.succeed(su_cmd("tar cvf scratchimg.tar --files-from /dev/null && podman import scratchimg.tar scratchimg"))
+          podman.succeed(su_cmd("tar cv --files-from /dev/null | podman import - scratchimg"))
           podman.succeed(
               su_cmd(
                   "podman run --runtime=runc -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
@@ -89,7 +89,7 @@ import ./make-test-python.nix (
           podman.succeed(su_cmd("podman rm sleeping"))
 
       with subtest("Run container rootless with crun"):
-          podman.succeed(su_cmd("tar cvf scratchimg.tar --files-from /dev/null && podman import scratchimg.tar scratchimg"))
+          podman.succeed(su_cmd("tar cv --files-from /dev/null | podman import - scratchimg"))
           podman.succeed(
               su_cmd(
                   "podman run --runtime=crun -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
@@ -100,7 +100,7 @@ import ./make-test-python.nix (
           podman.succeed(su_cmd("podman rm sleeping"))
 
       with subtest("Run container rootless with the default backend"):
-          podman.succeed(su_cmd("tar cvf scratchimg.tar --files-from /dev/null && podman import scratchimg.tar scratchimg"))
+          podman.succeed(su_cmd("tar cv --files-from /dev/null | podman import - scratchimg"))
           podman.succeed(
               su_cmd(
                   "podman run -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
@@ -112,7 +112,7 @@ import ./make-test-python.nix (
 
       with subtest("Run container with init"):
           podman.succeed(
-              "tar cvf busybox.tar -C ${pkgs.pkgsStatic.busybox} . && podman import busybox.tar busybox"
+              "tar cv -C ${pkgs.pkgsStatic.busybox} . | podman import - busybox"
           )
           pid = podman.succeed("podman run --rm busybox readlink /proc/self").strip()
           assert pid == "1"
@@ -124,7 +124,7 @@ import ./make-test-python.nix (
 
       with subtest("Run container via docker cli"):
           podman.succeed("docker network create default")
-          podman.succeed("tar cvf scratchimg.tar --files-from /dev/null && podman import scratchimg.tar scratchimg")
+          podman.succeed("tar cv --files-from /dev/null | podman import - scratchimg")
           podman.succeed(
             "docker run -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
           )
diff --git a/nixos/tests/podman-dnsname.nix b/nixos/tests/podman/dnsname.nix
index 9e4e8fdb08a2f..3768ae79e0676 100644
--- a/nixos/tests/podman-dnsname.nix
+++ b/nixos/tests/podman/dnsname.nix
@@ -1,4 +1,4 @@
-import ./make-test-python.nix (
+import ../make-test-python.nix (
   { pkgs, lib, ... }:
   let
     inherit (pkgs) writeTextDir python3 curl;
@@ -21,7 +21,7 @@ import ./make-test-python.nix (
       podman.wait_for_unit("sockets.target")
 
       with subtest("DNS works"): # also tests inter-container tcp routing
-        podman.succeed("tar cvf scratchimg.tar --files-from /dev/null && podman import scratchimg.tar scratchimg")
+        podman.succeed("tar cv --files-from /dev/null | podman import - scratchimg")
         podman.succeed(
           "podman run -d --name=webserver -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin -w ${webroot} scratchimg ${python3}/bin/python -m http.server 8000"
         )
diff --git a/nixos/tests/podman-tls-ghostunnel.nix b/nixos/tests/podman/tls-ghostunnel.nix
index b5836c436497b..c0bc47cc40b1b 100644
--- a/nixos/tests/podman-tls-ghostunnel.nix
+++ b/nixos/tests/podman/tls-ghostunnel.nix
@@ -1,7 +1,7 @@
 /*
   This test runs podman as a backend for the Docker CLI.
  */
-import ./make-test-python.nix (
+import ../make-test-python.nix (
   { pkgs, lib, ... }:
 
   let gen-ca = pkgs.writeScript "gen-ca" ''
@@ -126,7 +126,7 @@ import ./make-test-python.nix (
           client.succeed("docker version")
 
           # via socket would be nicer
-          podman.succeed("tar cvf scratchimg.tar --files-from /dev/null && podman import scratchimg.tar scratchimg")
+          podman.succeed("tar cv --files-from /dev/null | podman import - scratchimg")
 
           client.succeed(
             "docker run -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
diff --git a/nixos/tests/powerdns-admin.nix b/nixos/tests/powerdns-admin.nix
new file mode 100644
index 0000000000000..4d763c9c6f6e8
--- /dev/null
+++ b/nixos/tests/powerdns-admin.nix
@@ -0,0 +1,117 @@
+# Test powerdns-admin
+{ system ? builtins.currentSystem
+, config ? { }
+, pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+let
+  defaultConfig = ''
+    BIND_ADDRESS = '127.0.0.1'
+    PORT = 8000
+  '';
+
+  makeAppTest = name: configs: makeTest {
+    name = "powerdns-admin-${name}";
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ Flakebi zhaofengli ];
+    };
+
+    nodes.server = { pkgs, config, ... }: mkMerge ([
+      {
+        services.powerdns-admin = {
+          enable = true;
+          secretKeyFile = "/etc/powerdns-admin/secret";
+          saltFile = "/etc/powerdns-admin/salt";
+        };
+        # It's insecure to have secrets in the world-readable nix store, but this is just a test
+        environment.etc."powerdns-admin/secret".text = "secret key";
+        environment.etc."powerdns-admin/salt".text = "salt";
+        environment.systemPackages = [
+          (pkgs.writeShellScriptBin "run-test" config.system.build.testScript)
+        ];
+      }
+    ] ++ configs);
+
+    testScript = ''
+      server.wait_for_unit("powerdns-admin.service")
+      server.wait_until_succeeds("run-test", timeout=10)
+    '';
+  };
+
+  matrix = {
+    backend = {
+      mysql = {
+        services.powerdns-admin = {
+          config = ''
+            ${defaultConfig}
+            SQLALCHEMY_DATABASE_URI = 'mysql://powerdnsadmin@/powerdnsadmin?unix_socket=/run/mysqld/mysqld.sock'
+          '';
+        };
+        systemd.services.powerdns-admin = {
+          after = [ "mysql.service" ];
+          serviceConfig.BindPaths = "/run/mysqld";
+        };
+
+        services.mysql = {
+          enable = true;
+          package = pkgs.mariadb;
+          ensureDatabases = [ "powerdnsadmin" ];
+          ensureUsers = [
+            {
+              name = "powerdnsadmin";
+              ensurePermissions = {
+                "powerdnsadmin.*" = "ALL PRIVILEGES";
+              };
+            }
+          ];
+        };
+      };
+      postgresql = {
+        services.powerdns-admin = {
+          config = ''
+            ${defaultConfig}
+            SQLALCHEMY_DATABASE_URI = 'postgresql://powerdnsadmin@/powerdnsadmin?host=/run/postgresql'
+          '';
+        };
+        systemd.services.powerdns-admin = {
+          after = [ "postgresql.service" ];
+          serviceConfig.BindPaths = "/run/postgresql";
+        };
+
+        services.postgresql = {
+          enable = true;
+          ensureDatabases = [ "powerdnsadmin" ];
+          ensureUsers = [
+            {
+              name = "powerdnsadmin";
+              ensurePermissions = {
+                "DATABASE powerdnsadmin" = "ALL PRIVILEGES";
+              };
+            }
+          ];
+        };
+      };
+    };
+    listen = {
+      tcp = {
+        services.powerdns-admin.extraArgs = [ "-b" "127.0.0.1:8000" ];
+        system.build.testScript = ''
+          curl -sSf http://127.0.0.1:8000/
+        '';
+      };
+      unix = {
+        services.powerdns-admin.extraArgs = [ "-b" "unix:/run/powerdns-admin/http.sock" ];
+        system.build.testScript = ''
+          curl -sSf --unix-socket /run/powerdns-admin/http.sock http://somehost/
+        '';
+      };
+    };
+  };
+in
+with matrix; {
+  postgresql = makeAppTest "postgresql" [ backend.postgresql listen.tcp ];
+  mysql = makeAppTest "mysql" [ backend.mysql listen.tcp ];
+  unix-listener = makeAppTest "unix-listener" [ backend.postgresql listen.unix ];
+}
diff --git a/nixos/tests/prometheus-exporters.nix b/nixos/tests/prometheus-exporters.nix
index 62deb38649514..036c037e426c4 100644
--- a/nixos/tests/prometheus-exporters.nix
+++ b/nixos/tests/prometheus-exporters.nix
@@ -259,6 +259,19 @@ let
       '';
     };
 
+    fastly = {
+      exporterConfig = {
+        enable = true;
+        tokenPath = pkgs.writeText "token" "abc123";
+      };
+
+      # noop: fastly's exporter can't start without first talking to fastly
+      # see: https://github.com/peterbourgon/fastly-exporter/issues/87
+      exporterTest = ''
+        succeed("true");
+      '';
+    };
+
     fritzbox = {
       # TODO add proper test case
       exporterConfig = {
@@ -939,7 +952,7 @@ let
       exporterConfig = {
         enable = true;
       };
-      metricProvider.services.redis.enable = true;
+      metricProvider.services.redis.servers."".enable = true;
       exporterTest = ''
         wait_for_unit("redis.service")
         wait_for_unit("prometheus-redis-exporter.service")
diff --git a/nixos/tests/redis.nix b/nixos/tests/redis.nix
index 28b6058c2c026..7b70c239ad6ed 100644
--- a/nixos/tests/redis.nix
+++ b/nixos/tests/redis.nix
@@ -1,7 +1,4 @@
 import ./make-test-python.nix ({ pkgs, ... }:
-let
-  redisSocket = "/run/redis/redis.sock";
-in
 {
   name = "redis";
   meta = with pkgs.lib.maintainers; {
@@ -10,35 +7,40 @@ in
 
   nodes = {
     machine =
-      { pkgs, ... }:
+      { pkgs, lib, ... }: with lib;
 
       {
-        services.redis.enable = true;
-        services.redis.unixSocket = redisSocket;
+        services.redis.servers."".enable = true;
+        services.redis.servers."test".enable = true;
 
-        # Allow access to the unix socket for the "redis" group.
-        services.redis.unixSocketPerm = 770;
-
-        users.users."member" = {
+        users.users = listToAttrs (map (suffix: nameValuePair "member${suffix}" {
           createHome = false;
-          description = "A member of the redis group";
+          description = "A member of the redis${suffix} group";
           isNormalUser = true;
-          extraGroups = [
-            "redis"
-          ];
-        };
+          extraGroups = [ "redis${suffix}" ];
+        }) ["" "-test"]);
       };
   };
 
-  testScript = ''
+  testScript = { nodes, ... }: let
+    inherit (nodes.machine.config.services) redis;
+    in ''
     start_all()
     machine.wait_for_unit("redis")
+    machine.wait_for_unit("redis-test")
+
+    # The unnamed Redis server still opens a port for backward-compatibility
     machine.wait_for_open_port("6379")
 
+    machine.wait_for_file("${redis.servers."".unixSocket}")
+    machine.wait_for_file("${redis.servers."test".unixSocket}")
+
     # The unix socket is accessible to the redis group
     machine.succeed('su member -c "redis-cli ping | grep PONG"')
+    machine.succeed('su member-test -c "redis-cli ping | grep PONG"')
 
     machine.succeed("redis-cli ping | grep PONG")
-    machine.succeed("redis-cli -s ${redisSocket} ping | grep PONG")
+    machine.succeed("redis-cli -s ${redis.servers."".unixSocket} ping | grep PONG")
+    machine.succeed("redis-cli -s ${redis.servers."test".unixSocket} ping | grep PONG")
   '';
 })
diff --git a/nixos/tests/sabnzbd.nix b/nixos/tests/sabnzbd.nix
new file mode 100644
index 0000000000000..fb35b212b493b
--- /dev/null
+++ b/nixos/tests/sabnzbd.nix
@@ -0,0 +1,22 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "sabnzbd";
+  meta = with pkgs.lib; {
+    maintainers = with maintainers; [ jojosch ];
+  };
+
+  machine = { pkgs, ... }: {
+    services.sabnzbd = {
+      enable = true;
+    };
+
+    # unrar is unfree
+    nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [ "unrar" ];
+  };
+
+  testScript = ''
+    machine.wait_for_unit("sabnzbd.service")
+    machine.wait_until_succeeds(
+        "curl --fail -L http://localhost:8080/"
+    )
+  '';
+})
diff --git a/nixos/tests/samba-wsdd.nix b/nixos/tests/samba-wsdd.nix
index e7dd17c089a3e..0e3185b0c6849 100644
--- a/nixos/tests/samba-wsdd.nix
+++ b/nixos/tests/samba-wsdd.nix
@@ -38,7 +38,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
     server_wsdd.wait_for_unit("samba-wsdd")
 
     client_wsdd.wait_until_succeeds(
-        "echo list | ${pkgs.libressl.nc}/bin/nc -U /run/wsdd/wsdd.sock | grep -i SERVER-WSDD"
+        "echo list | ${pkgs.libressl.nc}/bin/nc -N -U /run/wsdd/wsdd.sock | grep -i SERVER-WSDD"
     )
   '';
 })
diff --git a/nixos/tests/snapcast.nix b/nixos/tests/snapcast.nix
index 8d960b4cc069c..30b8343e2ffee 100644
--- a/nixos/tests/snapcast.nix
+++ b/nixos/tests/snapcast.nix
@@ -40,6 +40,7 @@ in {
           };
         };
       };
+      environment.systemPackages = [ pkgs.snapcast ];
     };
     client = {
       environment.systemPackages = [ pkgs.snapcast ];
@@ -71,6 +72,13 @@ in {
             "curl --fail http://localhost:${toString httpPort}/jsonrpc -d '{json.dumps(get_rpc_version)}'"
         )
 
+    with subtest("test a ipv6 connection"):
+        server.execute("systemd-run --unit=snapcast-local-client snapclient -h ::1 -p ${toString port}")
+        server.wait_until_succeeds(
+            "journalctl -o cat -u snapserver.service | grep -q 'Hello from'"
+        )
+        server.wait_until_succeeds("journalctl -o cat -u snapcast-local-client | grep -q 'buffer: ${toString bufferSize}'")
+
     with subtest("test a connection"):
         client.execute("systemd-run --unit=snapcast-client snapclient -h server -p ${toString port}")
         server.wait_until_succeeds(
diff --git a/nixos/tests/sourcehut.nix b/nixos/tests/sourcehut.nix
index b56a14ebf85ea..d1536c5932259 100644
--- a/nixos/tests/sourcehut.nix
+++ b/nixos/tests/sourcehut.nix
@@ -1,29 +1,197 @@
-import ./make-test-python.nix ({ pkgs, ... }:
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+let
+  domain = "sourcehut.localdomain";
 
+  # Note that wildcard certificates just under the TLD (eg. *.com)
+  # would be rejected by clients like curl.
+  tls-cert = pkgs.runCommand "selfSignedCerts" { buildInputs = [ pkgs.openssl ]; } ''
+    openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -nodes -days 36500 \
+      -subj '/CN=${domain}' -extensions v3_req \
+      -addext 'subjectAltName = DNS:*.${domain}'
+    install -D -t $out key.pem cert.pem
+  '';
+
+  images = {
+    nixos.unstable.x86_64 =
+      let
+        systemConfig = { pkgs, ... }: {
+          # passwordless ssh server
+          services.openssh = {
+            enable = true;
+            permitRootLogin = "yes";
+            extraConfig = "PermitEmptyPasswords yes";
+          };
+
+          users = {
+            mutableUsers = false;
+            # build user
+            extraUsers."build" = {
+              isNormalUser = true;
+              uid = 1000;
+              extraGroups = [ "wheel" ];
+              password = "";
+            };
+            users.root.password = "";
+          };
+
+          security.sudo.wheelNeedsPassword = false;
+          nix.trustedUsers = [ "root" "build" ];
+          documentation.nixos.enable = false;
+
+          # builds.sr.ht-image-specific network settings
+          networking = {
+            hostName = "build";
+            dhcpcd.enable = false;
+            defaultGateway.address = "10.0.2.2";
+            usePredictableInterfaceNames = false;
+            interfaces."eth0".ipv4.addresses = [{
+              address = "10.0.2.15";
+              prefixLength = 25;
+            }];
+            enableIPv6 = false;
+            nameservers = [
+              # OpenNIC anycast
+              "185.121.177.177"
+              "169.239.202.202"
+              # Google
+              "8.8.8.8"
+            ];
+            firewall.allowedTCPPorts = [ 22 ];
+          };
+
+          environment.systemPackages = [
+            pkgs.gitMinimal
+            #pkgs.mercurial
+            pkgs.curl
+            pkgs.gnupg
+          ];
+        };
+        qemuConfig = { pkgs, ... }: {
+          imports = [ systemConfig ];
+          fileSystems."/".device = "/dev/disk/by-label/nixos";
+          boot.initrd.availableKernelModules = [
+            "ahci"
+            "ehci_pci"
+            "sd_mod"
+            "usb_storage"
+            "usbhid"
+            "virtio_balloon"
+            "virtio_blk"
+            "virtio_pci"
+            "virtio_ring"
+            "xhci_pci"
+          ];
+          boot.loader = {
+            grub = {
+              version = 2;
+              device = "/dev/vda";
+            };
+            timeout = 0;
+          };
+        };
+        config = (import (pkgs.path + "/nixos/lib/eval-config.nix") {
+          inherit pkgs; modules = [ qemuConfig ];
+          system = "x86_64-linux";
+        }).config;
+      in
+      import (pkgs.path + "/nixos/lib/make-disk-image.nix") {
+        inherit pkgs lib config;
+        diskSize = 16000;
+        format = "qcow2-compressed";
+        contents = [
+          { source = pkgs.writeText "gitconfig" ''
+              [user]
+                name = builds.sr.ht
+                email = build@sr.ht
+            '';
+            target = "/home/build/.gitconfig";
+            user = "build";
+            group = "users";
+            mode = "644";
+          }
+        ];
+      };
+  };
+
+in
 {
   name = "sourcehut";
 
   meta.maintainers = [ pkgs.lib.maintainers.tomberek ];
 
-  machine = { config, pkgs, ... }: {
-    virtualisation.memorySize = 2048;
-    networking.firewall.allowedTCPPorts = [ 80 ];
+  machine = { config, pkgs, nodes, ... }: {
+    # buildsrht needs space
+    virtualisation.diskSize = 4 * 1024;
+    virtualisation.memorySize = 2 * 1024;
+    networking.domain = domain;
+    networking.extraHosts = ''
+      ${config.networking.primaryIPAddress} meta.${domain}
+      ${config.networking.primaryIPAddress} builds.${domain}
+    '';
 
     services.sourcehut = {
       enable = true;
-      services = [ "meta" ];
-      originBase = "sourcehut";
-      settings."sr.ht".service-key =   "8888888888888888888888888888888888888888888888888888888888888888";
-      settings."sr.ht".network-key = "0000000000000000000000000000000000000000000=";
-      settings.webhooks.private-key = "0000000000000000000000000000000000000000000=";
+      services = [ "meta" "builds" ];
+      nginx.enable = true;
+      nginx.virtualHost = {
+        forceSSL = true;
+        sslCertificate = "${tls-cert}/cert.pem";
+        sslCertificateKey = "${tls-cert}/key.pem";
+      };
+      postgresql.enable = true;
+      redis.enable = true;
+
+      meta.enable = true;
+      builds = {
+        enable = true;
+        # FIXME: see why it does not seem to activate fully.
+        #enableWorker = true;
+        inherit images;
+      };
+      settings."sr.ht" = {
+        global-domain = config.networking.domain;
+        service-key = pkgs.writeText "service-key" "8b327279b77e32a3620e2fc9aabce491cc46e7d821fd6713b2a2e650ce114d01";
+        network-key = pkgs.writeText "network-key" "cEEmc30BRBGkgQZcHFksiG7hjc6_dK1XR2Oo5Jb9_nQ=";
+      };
+      settings."builds.sr.ht" = {
+        oauth-client-secret = pkgs.writeText "buildsrht-oauth-client-secret" "2260e9c4d9b8dcedcef642860e0504bc";
+        oauth-client-id = "299db9f9c2013170";
+      };
+      settings.webhooks.private-key = pkgs.writeText "webhook-key" "Ra3IjxgFiwG9jxgp4WALQIZw/BMYt30xWiOsqD0J7EA=";
+    };
+
+    networking.firewall.allowedTCPPorts = [ 443 ];
+    security.pki.certificateFiles = [ "${tls-cert}/cert.pem" ];
+    services.nginx = {
+      enable = true;
+      recommendedGzipSettings = true;
+      recommendedOptimisation = true;
+      recommendedTlsSettings = true;
+      recommendedProxySettings = true;
+    };
+
+    services.postgresql = {
+      enable = true;
+      enableTCPIP = false;
+      settings.unix_socket_permissions = "0770";
     };
   };
 
   testScript = ''
     start_all()
     machine.wait_for_unit("multi-user.target")
+
+    # Testing metasrht
+    machine.wait_for_unit("metasrht-api.service")
     machine.wait_for_unit("metasrht.service")
     machine.wait_for_open_port(5000)
-    machine.succeed("curl -sL http://localhost:5000 | grep meta.sourcehut")
+    machine.succeed("curl -sL http://localhost:5000 | grep meta.${domain}")
+    machine.succeed("curl -sL http://meta.${domain} | grep meta.${domain}")
+
+    # Testing buildsrht
+    machine.wait_for_unit("buildsrht.service")
+    machine.wait_for_open_port(5002)
+    machine.succeed("curl -sL http://localhost:5002 | grep builds.${domain}")
+    #machine.wait_for_unit("buildsrht-worker.service")
   '';
 })
diff --git a/nixos/tests/switch-test.nix b/nixos/tests/switch-test.nix
index 78adf7ffa7da5..daad9134885f7 100644
--- a/nixos/tests/switch-test.nix
+++ b/nixos/tests/switch-test.nix
@@ -3,21 +3,138 @@
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "switch-test";
   meta = with pkgs.lib.maintainers; {
-    maintainers = [ gleber ];
+    maintainers = [ gleber das_j ];
   };
 
   nodes = {
-    machine = { ... }: {
+    machine = { pkgs, lib, ... }: {
       users.mutableUsers = false;
+
+      specialisation = rec {
+        simpleService.configuration = {
+          systemd.services.test = {
+            wantedBy = [ "multi-user.target" ];
+            serviceConfig = {
+              Type = "oneshot";
+              RemainAfterExit = true;
+              ExecStart = "${pkgs.coreutils}/bin/true";
+            };
+          };
+        };
+
+        simpleServiceModified.configuration = {
+          imports = [ simpleService.configuration ];
+          systemd.services.test.serviceConfig.X-Test = true;
+        };
+
+        simpleServiceNostop.configuration = {
+          imports = [ simpleService.configuration ];
+          systemd.services.test.stopIfChanged = false;
+        };
+
+        simpleServiceReload.configuration = {
+          imports = [ simpleService.configuration ];
+          systemd.services.test = {
+            reloadIfChanged = true;
+            serviceConfig.ExecReload = "${pkgs.coreutils}/bin/true";
+          };
+        };
+
+        simpleServiceNorestart.configuration = {
+          imports = [ simpleService.configuration ];
+          systemd.services.test.restartIfChanged = false;
+        };
+
+        mount.configuration = {
+          systemd.mounts = [
+            {
+              description = "Testmount";
+              what = "tmpfs";
+              type = "tmpfs";
+              where = "/testmount";
+              options = "size=1M";
+              wantedBy = [ "local-fs.target" ];
+            }
+          ];
+        };
+
+        mountModified.configuration = {
+          systemd.mounts = [
+            {
+              description = "Testmount";
+              what = "tmpfs";
+              type = "tmpfs";
+              where = "/testmount";
+              options = "size=10M";
+              wantedBy = [ "local-fs.target" ];
+            }
+          ];
+        };
+
+        timer.configuration = {
+          systemd.timers.test-timer = {
+            wantedBy = [ "timers.target" ];
+            timerConfig.OnCalendar = "@1395716396"; # chosen by fair dice roll
+          };
+          systemd.services.test-timer = {
+            serviceConfig = {
+              Type = "oneshot";
+              ExecStart = "${pkgs.coreutils}/bin/true";
+            };
+          };
+        };
+
+        timerModified.configuration = {
+          imports = [ timer.configuration ];
+          systemd.timers.test-timer.timerConfig.OnCalendar = lib.mkForce "Fri 2012-11-23 16:00:00";
+        };
+
+        path.configuration = {
+          systemd.paths.test-watch = {
+            wantedBy = [ "paths.target" ];
+            pathConfig.PathExists = "/testpath";
+          };
+          systemd.services.test-watch = {
+            serviceConfig = {
+              Type = "oneshot";
+              ExecStart = "${pkgs.coreutils}/bin/touch /testpath-modified";
+            };
+          };
+        };
+
+        pathModified.configuration = {
+          imports = [ path.configuration ];
+          systemd.paths.test-watch.pathConfig.PathExists = lib.mkForce "/testpath2";
+        };
+
+        slice.configuration = {
+          systemd.slices.testslice.sliceConfig.MemoryMax = "1"; # don't allow memory allocation
+          systemd.services.testservice = {
+            serviceConfig = {
+              Type = "oneshot";
+              RemainAfterExit = true;
+              ExecStart = "${pkgs.coreutils}/bin/true";
+              Slice = "testslice.slice";
+            };
+          };
+        };
+
+        sliceModified.configuration = {
+          imports = [ slice.configuration ];
+          systemd.slices.testslice.sliceConfig.MemoryMax = lib.mkForce null;
+        };
+      };
     };
-    other = { ... }: {
+
+    other = {
       users.mutableUsers = true;
     };
   };
 
-  testScript = {nodes, ...}: let
+  testScript = { nodes, ... }: let
     originalSystem = nodes.machine.config.system.build.toplevel;
     otherSystem = nodes.other.config.system.build.toplevel;
+    machine = nodes.machine.config.system.build.toplevel;
 
     # Ensures failures pass through using pipefail, otherwise failing to
     # switch-to-configuration is hidden by the success of `tee`.
@@ -27,12 +144,186 @@ import ./make-test-python.nix ({ pkgs, ...} : {
       set -o pipefail
       exec env -i "$@" | tee /dev/stderr
     '';
-  in ''
+  in /* python */ ''
+    def switch_to_specialisation(system, name, action="test"):
+        if name == "":
+            stc = f"{system}/bin/switch-to-configuration"
+        else:
+            stc = f"{system}/specialisation/{name}/bin/switch-to-configuration"
+        out = machine.succeed(f"{stc} {action} 2>&1")
+        assert_lacks(out, "switch-to-configuration line")  # Perl warnings
+        return out
+
+    def assert_contains(haystack, needle):
+        if needle not in haystack:
+            print("The haystack that will cause the following exception is:")
+            print("---")
+            print(haystack)
+            print("---")
+            raise Exception(f"Expected string '{needle}' was not found")
+
+    def assert_lacks(haystack, needle):
+        if needle in haystack:
+            print("The haystack that will cause the following exception is:")
+            print("---")
+            print(haystack, end="")
+            print("---")
+            raise Exception(f"Unexpected string '{needle}' was found")
+
+
     machine.succeed(
         "${stderrRunner} ${originalSystem}/bin/switch-to-configuration test"
     )
     machine.succeed(
         "${stderrRunner} ${otherSystem}/bin/switch-to-configuration test"
     )
+
+    with subtest("services"):
+        switch_to_specialisation("${machine}", "")
+        # Nothing happens when nothing is changed
+        out = switch_to_specialisation("${machine}", "")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        assert_lacks(out, "as well:")
+
+        # Start a simple service
+        out = switch_to_specialisation("${machine}", "simpleService")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_contains(out, "reloading the following units: dbus.service\n")  # huh
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_contains(out, "the following new units were started: test.service\n")
+        assert_lacks(out, "as well:")
+
+        # Not changing anything doesn't do anything
+        out = switch_to_specialisation("${machine}", "simpleService")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        assert_lacks(out, "as well:")
+
+        # Restart the simple service
+        out = switch_to_specialisation("${machine}", "simpleServiceModified")
+        assert_contains(out, "stopping the following units: test.service\n")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_contains(out, "\nstarting the following units: test.service\n")
+        assert_lacks(out, "the following new units were started:")
+        assert_lacks(out, "as well:")
+
+        # Restart the service with stopIfChanged=false
+        out = switch_to_specialisation("${machine}", "simpleServiceNostop")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_contains(out, "\nrestarting the following units: test.service\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        assert_lacks(out, "as well:")
+
+        # Reload the service with reloadIfChanged=true
+        out = switch_to_specialisation("${machine}", "simpleServiceReload")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_contains(out, "reloading the following units: test.service\n")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        assert_lacks(out, "as well:")
+
+        # Nothing happens when restartIfChanged=false
+        out = switch_to_specialisation("${machine}", "simpleServiceNorestart")
+        assert_lacks(out, "stopping the following units:")
+        assert_contains(out, "NOT restarting the following changed units: test.service\n")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        assert_lacks(out, "as well:")
+
+        # Dry mode shows different messages
+        out = switch_to_specialisation("${machine}", "simpleService", action="dry-activate")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        assert_lacks(out, "as well:")
+        assert_contains(out, "would start the following units: test.service\n")
+
+    with subtest("mounts"):
+        switch_to_specialisation("${machine}", "mount")
+        out = machine.succeed("mount | grep 'on /testmount'")
+        assert_contains(out, "size=1024k")
+        out = switch_to_specialisation("${machine}", "mountModified")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_contains(out, "reloading the following units: testmount.mount\n")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        assert_lacks(out, "as well:")
+        # It changed
+        out = machine.succeed("mount | grep 'on /testmount'")
+        assert_contains(out, "size=10240k")
+
+    with subtest("timers"):
+        switch_to_specialisation("${machine}", "timer")
+        out = machine.succeed("systemctl show test-timer.timer")
+        assert_contains(out, "OnCalendar=2014-03-25 02:59:56 UTC")
+        out = switch_to_specialisation("${machine}", "timerModified")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_contains(out, "restarting the following units: test-timer.timer\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        assert_lacks(out, "as well:")
+        # It changed
+        out = machine.succeed("systemctl show test-timer.timer")
+        assert_contains(out, "OnCalendar=Fri 2012-11-23 16:00:00")
+
+    with subtest("paths"):
+        out = switch_to_specialisation("${machine}", "path")
+        assert_contains(out, "stopping the following units: test-timer.timer\n")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_contains(out, "the following new units were started: test-watch.path")
+        assert_lacks(out, "as well:")
+        machine.fail("test -f /testpath-modified")
+
+        # touch the file, unit should be triggered
+        machine.succeed("touch /testpath")
+        machine.wait_until_succeeds("test -f /testpath-modified")
+        machine.succeed("rm /testpath /testpath-modified")
+        switch_to_specialisation("${machine}", "pathModified")
+        machine.succeed("touch /testpath")
+        machine.fail("test -f /testpath-modified")
+        machine.succeed("touch /testpath2")
+        machine.wait_until_succeeds("test -f /testpath-modified")
+
+    # This test ensures that changes to slice configuration get applied.
+    # We test this by having a slice that allows no memory allocation at
+    # all and starting a service within it. If the service crashes, the slice
+    # is applied and if we modify the slice to allow memory allocation, the
+    # service should successfully start.
+    with subtest("slices"):
+        machine.succeed("echo 0 > /proc/sys/vm/panic_on_oom")  # allow OOMing
+        out = switch_to_specialisation("${machine}", "slice")
+        machine.fail("systemctl start testservice.service")
+        out = switch_to_specialisation("${machine}", "sliceModified")
+        machine.succeed("systemctl start testservice.service")
+        machine.succeed("echo 1 > /proc/sys/vm/panic_on_oom")  # disallow OOMing
   '';
 })
diff --git a/nixos/tests/systemd-networkd-dhcpserver-static-leases.nix b/nixos/tests/systemd-networkd-dhcpserver-static-leases.nix
new file mode 100644
index 0000000000000..a8254a158016b
--- /dev/null
+++ b/nixos/tests/systemd-networkd-dhcpserver-static-leases.nix
@@ -0,0 +1,81 @@
+# In contrast to systemd-networkd-dhcpserver, this test configures
+# the router with a static DHCP lease for the client's MAC address.
+import ./make-test-python.nix ({ lib, ... }: {
+  name = "systemd-networkd-dhcpserver-static-leases";
+  meta = with lib.maintainers; {
+    maintainers = [ veehaitch tomfitzhenry ];
+  };
+  nodes = {
+    router = {
+      virtualisation.vlans = [ 1 ];
+      systemd.services.systemd-networkd.environment.SYSTEMD_LOG_LEVEL = "debug";
+      networking = {
+        useNetworkd = true;
+        useDHCP = false;
+        firewall.enable = false;
+      };
+      systemd.network = {
+        networks = {
+          # systemd-networkd will load the first network unit file
+          # that matches, ordered lexiographically by filename.
+          # /etc/systemd/network/{40-eth1,99-main}.network already
+          # exists. This network unit must be loaded for the test,
+          # however, hence why this network is named such.
+          "01-eth1" = {
+            name = "eth1";
+            networkConfig = {
+              DHCPServer = true;
+              Address = "10.0.0.1/24";
+            };
+            dhcpServerStaticLeases = [{
+              dhcpServerStaticLeaseConfig = {
+                MACAddress = "02:de:ad:be:ef:01";
+                Address = "10.0.0.10";
+              };
+            }];
+          };
+        };
+      };
+    };
+
+    client = {
+      virtualisation.vlans = [ 1 ];
+      systemd.services.systemd-networkd.environment.SYSTEMD_LOG_LEVEL = "debug";
+      networking = {
+        useNetworkd = true;
+        useDHCP = false;
+        firewall.enable = false;
+        interfaces.eth1 = {
+          useDHCP = true;
+          macAddress = "02:de:ad:be:ef:01";
+        };
+      };
+
+      # This setting is important to have the router assign the
+      # configured lease based on the client's MAC address. Also see:
+      # https://github.com/systemd/systemd/issues/21368#issuecomment-982193546
+      systemd.network.networks."40-eth1".dhcpV4Config.ClientIdentifier = "mac";
+    };
+  };
+  testScript = ''
+    start_all()
+
+    with subtest("check router network configuration"):
+      router.wait_for_unit("systemd-networkd-wait-online.service")
+      eth1_status = router.succeed("networkctl status eth1")
+      assert "Network File: /etc/systemd/network/01-eth1.network" in eth1_status, \
+        "The router interface eth1 is not using the expected network file"
+      assert "10.0.0.1" in eth1_status, "Did not find expected router IPv4"
+
+    with subtest("check client network configuration"):
+      client.wait_for_unit("systemd-networkd-wait-online.service")
+      eth1_status = client.succeed("networkctl status eth1")
+      assert "Network File: /etc/systemd/network/40-eth1.network" in eth1_status, \
+        "The client interface eth1 is not using the expected network file"
+      assert "10.0.0.10" in eth1_status, "Did not find expected client IPv4"
+
+    with subtest("router and client can reach each other"):
+      client.wait_until_succeeds("ping -c 5 10.0.0.1")
+      router.wait_until_succeeds("ping -c 5 10.0.0.10")
+  '';
+})
diff --git a/nixos/tests/systemd.nix b/nixos/tests/systemd.nix
index 6561f7efe1a5f..f86daa5eea974 100644
--- a/nixos/tests/systemd.nix
+++ b/nixos/tests/systemd.nix
@@ -31,6 +31,13 @@ import ./make-test-python.nix ({ pkgs, ... }: {
       umount /tmp/shared
     '';
 
+    systemd.services.oncalendar-test = {
+      description = "calendar test";
+      # Japan does not have DST which makes the test a little bit simpler
+      startAt = "Wed 10:00 Asia/Tokyo";
+      script = "true";
+    };
+
     systemd.services.testservice1 = {
       description = "Test Service 1";
       wantedBy = [ "multi-user.target" ];
@@ -69,6 +76,11 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     # wait for user services
     machine.wait_for_unit("default.target", "alice")
 
+    # Regression test for https://github.com/NixOS/nixpkgs/issues/105049
+    with subtest("systemd reads timezone database in /etc/zoneinfo"):
+        timer = machine.succeed("TZ=UTC systemctl show --property=TimersCalendar oncalendar-test.timer")
+        assert re.search("next_elapse=Wed ....-..-.. 01:00:00 UTC", timer), f"got {timer.strip()}"
+
     # Regression test for https://github.com/NixOS/nixpkgs/issues/35415
     with subtest("configuration files are recognized by systemd"):
         machine.succeed("test -e /system_conf_read")
diff --git a/nixos/tests/txredisapi.nix b/nixos/tests/txredisapi.nix
index bc3814a713750..7c6b36a5c47d5 100644
--- a/nixos/tests/txredisapi.nix
+++ b/nixos/tests/txredisapi.nix
@@ -10,17 +10,19 @@ import ./make-test-python.nix ({ pkgs, ... }:
       { pkgs, ... }:
 
       {
-        services.redis.enable = true;
-        services.redis.unixSocket = "/run/redis/redis.sock";
+        services.redis.servers."".enable = true;
 
         environment.systemPackages = with pkgs; [ (python38.withPackages (ps: [ ps.twisted ps.txredisapi ps.mock ]))];
       };
   };
 
-  testScript = ''
+  testScript = { nodes, ... }: let
+    inherit (nodes.machine.config.services) redis;
+    in ''
     start_all()
     machine.wait_for_unit("redis")
-    machine.wait_for_open_port("6379")
+    machine.wait_for_file("${redis.servers."".unixSocket}")
+    machine.succeed("ln -s ${redis.servers."".unixSocket} /tmp/redis.sock")
 
     tests = machine.succeed("PYTHONPATH=\"${pkgs.python3Packages.txredisapi.src}\" python -m twisted.trial ${pkgs.python3Packages.txredisapi.src}/tests")
   '';
diff --git a/nixos/tests/unifi.nix b/nixos/tests/unifi.nix
new file mode 100644
index 0000000000000..34284811abfb0
--- /dev/null
+++ b/nixos/tests/unifi.nix
@@ -0,0 +1,35 @@
+# Test UniFi controller
+
+{ system ? builtins.currentSystem
+, config ? { allowUnfree = true; }
+, pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+let
+  makeAppTest = unifi: makeTest {
+    name = "unifi-controller-${unifi.version}";
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ zhaofengli ];
+    };
+
+    nodes.server = {
+      services.unifi = {
+        enable = true;
+        unifiPackage = unifi;
+        openFirewall = false;
+      };
+    };
+
+    testScript = ''
+      server.wait_for_unit("unifi.service")
+      server.wait_until_succeeds("curl -Lk https://localhost:8443 >&2", timeout=300)
+    '';
+  };
+in with pkgs; {
+  unifiLTS = makeAppTest unifiLTS;
+  unifi5 = makeAppTest unifi5;
+  unifi6 = makeAppTest unifi6;
+}