summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
authorAlyssa Ross <hi@alyssa.is>2020-08-25 16:53:05 +0000
committerAlyssa Ross <hi@alyssa.is>2020-08-25 16:53:05 +0000
commitb0849305d63df6cc0acfe199cec3a997ad3c5a75 (patch)
tree6ff64291dec94a85c6e523a3f78da127a7daa1dd /nixos
parentc901f337b8fed63ba0bb53674950ce4c7bf94dcd (diff)
parent7e07d142e78656c5f16b18d81ee4eb9444c9b93d (diff)
downloadnixpkgs-b0849305d63df6cc0acfe199cec3a997ad3c5a75.tar
nixpkgs-b0849305d63df6cc0acfe199cec3a997ad3c5a75.tar.gz
nixpkgs-b0849305d63df6cc0acfe199cec3a997ad3c5a75.tar.bz2
nixpkgs-b0849305d63df6cc0acfe199cec3a997ad3c5a75.tar.lz
nixpkgs-b0849305d63df6cc0acfe199cec3a997ad3c5a75.tar.xz
nixpkgs-b0849305d63df6cc0acfe199cec3a997ad3c5a75.tar.zst
nixpkgs-b0849305d63df6cc0acfe199cec3a997ad3c5a75.zip
Merge remote-tracking branch 'nixpkgs/master' into master
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/administration/boot-problems.xml42
-rw-r--r--nixos/doc/manual/configuration/configuration.xml1
-rw-r--r--nixos/doc/manual/configuration/file-systems.xml11
-rw-r--r--nixos/doc/manual/configuration/gpu-accel.xml258
-rw-r--r--nixos/doc/manual/configuration/profiles/demo.xml3
-rw-r--r--nixos/doc/manual/configuration/x-windows.xml46
-rw-r--r--nixos/doc/manual/development/freeform-modules.xml68
-rw-r--r--nixos/doc/manual/development/running-nixos-tests-interactively.xml9
-rw-r--r--nixos/doc/manual/development/settings-options.xml216
-rw-r--r--nixos/doc/manual/development/writing-modules.xml2
-rw-r--r--nixos/doc/manual/development/writing-nixos-tests.xml22
-rw-r--r--nixos/doc/manual/installation/installing-from-other-distro.xml2
-rw-r--r--nixos/doc/manual/installation/installing-pxe.xml2
-rw-r--r--nixos/doc/manual/installation/installing-virtualbox-guest.xml2
-rw-r--r--nixos/doc/manual/installation/installing.xml6
-rw-r--r--nixos/doc/manual/man-nixos-build-vms.xml6
-rw-r--r--nixos/doc/manual/man-nixos-enter.xml12
-rw-r--r--nixos/doc/manual/man-nixos-rebuild.xml2
-rw-r--r--nixos/doc/manual/release-notes/rl-2003.xml2
-rw-r--r--nixos/doc/manual/release-notes/rl-2009.xml618
-rw-r--r--nixos/lib/eval-config.nix4
-rw-r--r--nixos/lib/make-disk-image.nix85
-rw-r--r--nixos/lib/make-ext4-fs.nix42
-rw-r--r--nixos/lib/make-iso9660-image.nix8
-rw-r--r--nixos/lib/make-iso9660-image.sh23
-rw-r--r--nixos/lib/make-options-doc/default.nix6
-rw-r--r--nixos/lib/qemu-flags.nix11
-rw-r--r--nixos/lib/test-driver/Logger.pm8
-rw-r--r--nixos/lib/test-driver/test-driver.py60
-rw-r--r--nixos/lib/testing-python.nix8
-rw-r--r--nixos/lib/testing.nix6
-rw-r--r--nixos/lib/testing/jquery-ui.nix24
-rw-r--r--nixos/lib/testing/jquery.nix36
-rw-r--r--nixos/lib/utils.nix8
-rw-r--r--nixos/maintainers/scripts/azure-new/.gitignore2
-rw-r--r--nixos/maintainers/scripts/azure-new/README.md2
-rwxr-xr-xnixos/maintainers/scripts/azure-new/upload-image.sh4
-rw-r--r--nixos/maintainers/scripts/ec2/amazon-image.nix4
-rwxr-xr-xnixos/maintainers/scripts/ec2/create-amis.sh2
-rw-r--r--nixos/modules/config/appstream.nix8
-rw-r--r--nixos/modules/config/fonts/fontconfig-penultimate.nix292
-rw-r--r--nixos/modules/config/fonts/fontconfig.nix103
-rw-r--r--nixos/modules/config/i18n.nix3
-rw-r--r--nixos/modules/config/ldap.nix4
-rw-r--r--nixos/modules/config/networking.nix33
-rw-r--r--nixos/modules/config/no-x-libs.nix2
-rw-r--r--nixos/modules/config/nsswitch.nix48
-rw-r--r--nixos/modules/config/power-management.nix2
-rw-r--r--nixos/modules/config/resolvconf.nix2
-rw-r--r--nixos/modules/config/system-path.nix10
-rw-r--r--nixos/modules/config/update-users-groups.pl55
-rw-r--r--nixos/modules/config/users-groups.nix113
-rw-r--r--nixos/modules/config/zram.nix2
-rw-r--r--nixos/modules/hardware/bladeRF.nix2
-rw-r--r--nixos/modules/hardware/ckb-next.nix1
-rw-r--r--nixos/modules/hardware/device-tree.nix13
-rw-r--r--nixos/modules/hardware/logitech.nix94
-rw-r--r--nixos/modules/hardware/onlykey.nix2
-rw-r--r--nixos/modules/hardware/printers.nix2
-rw-r--r--nixos/modules/hardware/tuxedo-keyboard.nix4
-rw-r--r--nixos/modules/hardware/video/hidpi.nix16
-rw-r--r--nixos/modules/hardware/video/uvcvideo/default.nix2
-rw-r--r--nixos/modules/hardware/xpadneo.nix29
-rw-r--r--nixos/modules/i18n/input-method/ibus.nix2
-rw-r--r--nixos/modules/i18n/input-method/uim.nix2
-rw-r--r--nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix20
-rw-r--r--nixos/modules/installer/cd-dvd/installation-cd-graphical-plasma5.nix4
-rw-r--r--nixos/modules/installer/cd-dvd/iso-image.nix11
-rw-r--r--nixos/modules/installer/cd-dvd/sd-image-aarch64.nix8
-rw-r--r--nixos/modules/installer/cd-dvd/sd-image-armv7l-multiplatform.nix8
-rw-r--r--nixos/modules/installer/cd-dvd/sd-image-raspberrypi.nix8
-rw-r--r--nixos/modules/installer/cd-dvd/sd-image-raspberrypi4.nix7
-rw-r--r--nixos/modules/installer/cd-dvd/sd-image.nix26
-rw-r--r--nixos/modules/installer/cd-dvd/system-tarball-pc-readme.txt2
-rw-r--r--nixos/modules/installer/tools/nix-fallback-paths.nix8
-rw-r--r--nixos/modules/installer/tools/nixos-build-vms/nixos-build-vms.sh2
-rw-r--r--nixos/modules/installer/tools/nixos-enter.sh2
-rw-r--r--nixos/modules/installer/tools/nixos-generate-config.pl5
-rw-r--r--nixos/modules/installer/tools/nixos-install.sh13
-rw-r--r--nixos/modules/installer/tools/nixos-rebuild.sh4
-rw-r--r--nixos/modules/installer/tools/nixos-version.sh2
-rw-r--r--nixos/modules/installer/tools/tools.nix5
-rw-r--r--nixos/modules/misc/documentation.nix38
-rw-r--r--nixos/modules/misc/ids.nix12
-rw-r--r--nixos/modules/module-list.nix44
-rw-r--r--nixos/modules/profiles/base.nix1
-rw-r--r--nixos/modules/profiles/demo.nix10
-rw-r--r--nixos/modules/programs/autojump.nix4
-rw-r--r--nixos/modules/programs/bash/bash.nix3
-rw-r--r--nixos/modules/programs/ccache.nix2
-rw-r--r--nixos/modules/programs/chromium.nix19
-rw-r--r--nixos/modules/programs/dconf.nix37
-rw-r--r--nixos/modules/programs/fish.nix3
-rw-r--r--nixos/modules/programs/freetds.nix4
-rw-r--r--nixos/modules/programs/gnupg.nix3
-rw-r--r--nixos/modules/programs/hamster.nix15
-rw-r--r--nixos/modules/programs/shadow.nix5
-rw-r--r--nixos/modules/programs/ssh.nix30
-rw-r--r--nixos/modules/programs/ssmtp.nix77
-rw-r--r--nixos/modules/programs/steam.nix25
-rw-r--r--nixos/modules/programs/zsh/zsh.nix12
-rw-r--r--nixos/modules/rename.nix16
-rw-r--r--nixos/modules/security/acme.nix60
-rw-r--r--nixos/modules/security/google_oslogin.nix1
-rw-r--r--nixos/modules/security/pam.nix51
-rw-r--r--nixos/modules/security/sudo.nix4
-rw-r--r--nixos/modules/security/tpm2.nix1
-rw-r--r--nixos/modules/security/wrappers/default.nix3
-rw-r--r--nixos/modules/services/audio/icecast.nix4
-rw-r--r--nixos/modules/services/audio/mpd.nix14
-rw-r--r--nixos/modules/services/audio/roon-server.nix6
-rw-r--r--nixos/modules/services/audio/snapserver.nix166
-rw-r--r--nixos/modules/services/audio/spotifyd.nix2
-rw-r--r--nixos/modules/services/backup/bacula.nix36
-rw-r--r--nixos/modules/services/backup/restic.nix66
-rw-r--r--nixos/modules/services/backup/zfs-replication.nix2
-rw-r--r--nixos/modules/services/computing/slurm/slurm.nix4
-rw-r--r--nixos/modules/services/computing/torque/mom.nix2
-rw-r--r--nixos/modules/services/computing/torque/server.nix2
-rw-r--r--nixos/modules/services/continuous-integration/buildbot/master.nix32
-rw-r--r--nixos/modules/services/continuous-integration/buildbot/worker.nix11
-rw-r--r--nixos/modules/services/continuous-integration/gitlab-runner.nix93
-rw-r--r--nixos/modules/services/databases/mysql.nix307
-rw-r--r--nixos/modules/services/databases/openldap.nix32
-rw-r--r--nixos/modules/services/databases/postgresql.nix101
-rw-r--r--nixos/modules/services/databases/redis.nix5
-rw-r--r--nixos/modules/services/databases/rethinkdb.nix5
-rw-r--r--nixos/modules/services/databases/riak-cs.nix2
-rw-r--r--nixos/modules/services/desktops/deepin/deepin.nix2
-rw-r--r--nixos/modules/services/desktops/espanso.nix25
-rw-r--r--nixos/modules/services/desktops/flatpak.nix1
-rw-r--r--nixos/modules/services/desktops/malcontent.nix5
-rw-r--r--nixos/modules/services/development/jupyter/default.nix28
-rw-r--r--nixos/modules/services/development/jupyterhub/default.nix190
-rw-r--r--nixos/modules/services/editors/emacs.nix47
-rw-r--r--nixos/modules/services/editors/emacs.xml12
-rw-r--r--nixos/modules/services/games/minetest-server.nix16
-rw-r--r--nixos/modules/services/games/teeworlds.nix119
-rw-r--r--nixos/modules/services/games/terraria.nix2
-rw-r--r--nixos/modules/services/hardware/fwupd.nix35
-rw-r--r--nixos/modules/services/hardware/thinkfan.nix2
-rw-r--r--nixos/modules/services/hardware/tlp.nix59
-rw-r--r--nixos/modules/services/hardware/trezord.nix4
-rw-r--r--nixos/modules/services/hardware/u2f.nix23
-rw-r--r--nixos/modules/services/hardware/udev.nix4
-rw-r--r--nixos/modules/services/hardware/undervolt.nix100
-rw-r--r--nixos/modules/services/logging/logrotate.nix151
-rw-r--r--nixos/modules/services/logging/logstash.nix6
-rw-r--r--nixos/modules/services/mail/dovecot.nix38
-rw-r--r--nixos/modules/services/mail/mailman.nix455
-rw-r--r--nixos/modules/services/mail/mailman.xml59
-rw-r--r--nixos/modules/services/mail/opensmtpd.nix23
-rw-r--r--nixos/modules/services/mail/pfix-srsd.nix2
-rw-r--r--nixos/modules/services/mail/postfix.nix90
-rw-r--r--nixos/modules/services/mail/roundcube.nix18
-rw-r--r--nixos/modules/services/misc/bazarr.nix76
-rw-r--r--nixos/modules/services/misc/docker-registry.nix2
-rw-r--r--nixos/modules/services/misc/freeswitch.nix6
-rw-r--r--nixos/modules/services/misc/gitea.nix144
-rw-r--r--nixos/modules/services/misc/gitlab.nix73
-rw-r--r--nixos/modules/services/misc/gitolite.nix33
-rw-r--r--nixos/modules/services/misc/gollum.nix9
-rw-r--r--nixos/modules/services/misc/home-assistant.nix11
-rw-r--r--nixos/modules/services/misc/jellyfin.nix15
-rw-r--r--nixos/modules/services/misc/mathics.nix54
-rw-r--r--nixos/modules/services/misc/matrix-appservice-discord.nix162
-rw-r--r--nixos/modules/services/misc/matrix-synapse.nix15
-rw-r--r--nixos/modules/services/misc/matrix-synapse.xml38
-rw-r--r--nixos/modules/services/misc/mautrix-telegram.nix163
-rw-r--r--nixos/modules/services/misc/mesos-master.nix125
-rw-r--r--nixos/modules/services/misc/mesos-slave.nix220
-rw-r--r--nixos/modules/services/misc/nix-daemon.nix170
-rw-r--r--nixos/modules/services/misc/octoprint.nix4
-rw-r--r--nixos/modules/services/misc/pinnwand.nix78
-rw-r--r--nixos/modules/services/misc/redmine.nix4
-rw-r--r--nixos/modules/services/misc/siproxd.nix8
-rw-r--r--nixos/modules/services/misc/ssm-agent.nix6
-rw-r--r--nixos/modules/services/misc/sssd.nix10
-rw-r--r--nixos/modules/services/misc/tzupdate.nix4
-rw-r--r--nixos/modules/services/misc/zigbee2mqtt.nix98
-rw-r--r--nixos/modules/services/misc/zoneminder.nix8
-rw-r--r--nixos/modules/services/monitoring/cadvisor.nix2
-rw-r--r--nixos/modules/services/monitoring/dd-agent/dd-agent.nix4
-rw-r--r--nixos/modules/services/monitoring/do-agent.nix17
-rw-r--r--nixos/modules/services/monitoring/monit.nix18
-rw-r--r--nixos/modules/services/monitoring/netdata.nix45
-rw-r--r--nixos/modules/services/monitoring/prometheus/default.nix27
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters.nix14
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/apcupsd.nix38
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/keylight.nix19
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/lnd.nix46
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/modemmanager.nix33
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/redis.nix19
-rw-r--r--nixos/modules/services/monitoring/smartd.nix18
-rw-r--r--nixos/modules/services/monitoring/teamviewer.nix2
-rw-r--r--nixos/modules/services/monitoring/tuptime.nix9
-rw-r--r--nixos/modules/services/monitoring/zabbix-agent.nix41
-rw-r--r--nixos/modules/services/monitoring/zabbix-proxy.nix75
-rw-r--r--nixos/modules/services/monitoring/zabbix-server.nix72
-rw-r--r--nixos/modules/services/network-filesystems/ipfs.nix192
-rw-r--r--nixos/modules/services/network-filesystems/samba.nix1
-rw-r--r--nixos/modules/services/networking/3proxy.nix2
-rw-r--r--nixos/modules/services/networking/avahi-daemon.nix4
-rw-r--r--nixos/modules/services/networking/bitcoind.nix206
-rw-r--r--nixos/modules/services/networking/blockbook-frontend.nix275
-rw-r--r--nixos/modules/services/networking/corerad.nix48
-rw-r--r--nixos/modules/services/networking/dhcpcd.nix4
-rw-r--r--nixos/modules/services/networking/dhcpd.nix12
-rw-r--r--nixos/modules/services/networking/dnschain.nix184
-rw-r--r--nixos/modules/services/networking/dnscrypt-proxy2.nix1
-rw-r--r--nixos/modules/services/networking/dnscrypt-wrapper.nix94
-rw-r--r--nixos/modules/services/networking/ergo.nix141
-rw-r--r--nixos/modules/services/networking/gateone.nix2
-rw-r--r--nixos/modules/services/networking/go-neb.nix53
-rw-r--r--nixos/modules/services/networking/haproxy.nix32
-rw-r--r--nixos/modules/services/networking/jicofo.nix152
-rw-r--r--nixos/modules/services/networking/jitsi-videobridge.nix276
-rw-r--r--nixos/modules/services/networking/kresd.nix13
-rw-r--r--nixos/modules/services/networking/mstpd.nix2
-rw-r--r--nixos/modules/services/networking/namecoind.nix5
-rw-r--r--nixos/modules/services/networking/ncdns.nix278
-rw-r--r--nixos/modules/services/networking/networkmanager.nix7
-rw-r--r--nixos/modules/services/networking/nextdns.nix44
-rw-r--r--nixos/modules/services/networking/nghttpx/default.nix6
-rw-r--r--nixos/modules/services/networking/nsd.nix11
-rw-r--r--nixos/modules/services/networking/onedrive.nix72
-rw-r--r--nixos/modules/services/networking/onedrive.xml34
-rw-r--r--nixos/modules/services/networking/prosody.nix2
-rw-r--r--nixos/modules/services/networking/radicale.nix11
-rw-r--r--nixos/modules/services/networking/resilio.nix20
-rw-r--r--nixos/modules/services/networking/skydns.nix2
-rw-r--r--nixos/modules/services/networking/sslh.nix18
-rw-r--r--nixos/modules/services/networking/supplicant.nix26
-rw-r--r--nixos/modules/services/networking/tinc.nix16
-rw-r--r--nixos/modules/services/networking/trickster.nix5
-rw-r--r--nixos/modules/services/networking/unifi.nix2
-rw-r--r--nixos/modules/services/networking/v2ray.nix8
-rw-r--r--nixos/modules/services/networking/vsftpd.nix3
-rw-r--r--nixos/modules/services/networking/wasabibackend.nix158
-rw-r--r--nixos/modules/services/networking/websockify.nix6
-rw-r--r--nixos/modules/services/networking/wg-quick.nix20
-rw-r--r--nixos/modules/services/networking/wireguard.nix15
-rw-r--r--nixos/modules/services/networking/wpa_supplicant.nix6
-rw-r--r--nixos/modules/services/networking/xandikos.nix4
-rw-r--r--nixos/modules/services/networking/yggdrasil.nix140
-rw-r--r--nixos/modules/services/networking/yggdrasil.xml157
-rw-r--r--nixos/modules/services/scheduling/chronos.nix54
-rw-r--r--nixos/modules/services/scheduling/marathon.nix98
-rw-r--r--nixos/modules/services/security/fprintd.nix1
-rw-r--r--nixos/modules/services/security/haveged.nix18
-rw-r--r--nixos/modules/services/security/nginx-sso.nix11
-rw-r--r--nixos/modules/services/security/oauth2_proxy.nix12
-rw-r--r--nixos/modules/services/security/physlock.nix1
-rw-r--r--nixos/modules/services/security/privacyidea.nix278
-rw-r--r--nixos/modules/services/security/tor.nix2
-rw-r--r--nixos/modules/services/security/usbguard.nix143
-rw-r--r--nixos/modules/services/security/yubikey-agent.nix60
-rw-r--r--nixos/modules/services/system/cgmanager.nix26
-rw-r--r--nixos/modules/services/system/earlyoom.nix24
-rw-r--r--nixos/modules/services/system/nscd.conf2
-rw-r--r--nixos/modules/services/torrent/rtorrent.nix209
-rw-r--r--nixos/modules/services/torrent/transmission.nix452
-rw-r--r--nixos/modules/services/video/mirakurun.nix165
-rw-r--r--nixos/modules/services/wayland/cage.nix2
-rw-r--r--nixos/modules/services/web-apps/codimd.nix4
-rw-r--r--nixos/modules/services/web-apps/convos.nix72
-rw-r--r--nixos/modules/services/web-apps/dokuwiki.nix28
-rw-r--r--nixos/modules/services/web-apps/engelsystem.nix186
-rw-r--r--nixos/modules/services/web-apps/gerrit.nix21
-rw-r--r--nixos/modules/services/web-apps/jitsi-meet.nix333
-rw-r--r--nixos/modules/services/web-apps/moodle.nix14
-rw-r--r--nixos/modules/services/web-apps/nextcloud.nix279
-rw-r--r--nixos/modules/services/web-apps/nextcloud.xml67
-rw-r--r--nixos/modules/services/web-apps/pgpkeyserver-lite.nix2
-rw-r--r--nixos/modules/services/web-apps/rss-bridge.nix127
-rw-r--r--nixos/modules/services/web-apps/sogo.nix272
-rw-r--r--nixos/modules/services/web-apps/trilium.nix2
-rw-r--r--nixos/modules/services/web-apps/tt-rss.nix7
-rw-r--r--nixos/modules/services/web-servers/apache-httpd/default.nix32
-rw-r--r--nixos/modules/services/web-servers/jboss/builder.sh12
-rw-r--r--nixos/modules/services/web-servers/meguca.nix174
-rw-r--r--nixos/modules/services/web-servers/molly-brown.nix117
-rw-r--r--nixos/modules/services/web-servers/nginx/default.nix45
-rw-r--r--nixos/modules/services/web-servers/shellinabox.nix2
-rw-r--r--nixos/modules/services/web-servers/unit/default.nix5
-rw-r--r--nixos/modules/services/web-servers/uwsgi.nix20
-rw-r--r--nixos/modules/services/x11/colord.nix2
-rw-r--r--nixos/modules/services/x11/desktop-managers/cde.nix17
-rw-r--r--nixos/modules/services/x11/desktop-managers/default.nix2
-rw-r--r--nixos/modules/services/x11/desktop-managers/enlightenment.nix39
-rw-r--r--nixos/modules/services/x11/desktop-managers/gnome3.nix6
-rw-r--r--nixos/modules/services/x11/desktop-managers/lumina.nix9
-rw-r--r--nixos/modules/services/x11/desktop-managers/maxx.nix31
-rw-r--r--nixos/modules/services/x11/desktop-managers/pantheon.nix1
-rw-r--r--nixos/modules/services/x11/desktop-managers/plasma5.nix21
-rw-r--r--nixos/modules/services/x11/display-managers/default.nix42
-rw-r--r--nixos/modules/services/x11/display-managers/gdm.nix66
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm-greeters/pantheon.nix2
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm.nix74
-rw-r--r--nixos/modules/services/x11/display-managers/sddm.nix67
-rw-r--r--nixos/modules/services/x11/imwheel.nix3
-rw-r--r--nixos/modules/services/x11/urserver.nix38
-rw-r--r--nixos/modules/services/x11/window-managers/berry.nix25
-rw-r--r--nixos/modules/services/x11/window-managers/default.nix4
-rw-r--r--nixos/modules/services/x11/window-managers/lwm.nix25
-rw-r--r--nixos/modules/services/x11/window-managers/qtile.nix2
-rw-r--r--nixos/modules/services/x11/window-managers/smallwm.nix25
-rw-r--r--nixos/modules/services/x11/window-managers/xmonad.nix11
-rw-r--r--nixos/modules/services/x11/window-managers/yeahwm.nix25
-rw-r--r--nixos/modules/services/x11/xautolock.nix2
-rw-r--r--nixos/modules/services/x11/xserver.nix2
-rw-r--r--nixos/modules/system/activation/top-level.nix19
-rw-r--r--nixos/modules/system/boot/binfmt.nix9
-rw-r--r--nixos/modules/system/boot/emergency-mode.nix2
-rw-r--r--nixos/modules/system/boot/initrd-network.nix2
-rw-r--r--nixos/modules/system/boot/initrd-openvpn.nix81
-rw-r--r--nixos/modules/system/boot/kernel_config.nix7
-rw-r--r--nixos/modules/system/boot/loader/generations-dir/generations-dir-builder.sh2
-rw-r--r--nixos/modules/system/boot/loader/generic-extlinux-compatible/default.nix29
-rw-r--r--nixos/modules/system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.sh17
-rw-r--r--nixos/modules/system/boot/loader/grub/grub.nix143
-rw-r--r--nixos/modules/system/boot/loader/grub/install-grub.pl195
-rw-r--r--nixos/modules/system/boot/loader/init-script/init-script-builder.sh2
-rw-r--r--nixos/modules/system/boot/loader/raspberrypi/raspberrypi-builder.sh5
-rw-r--r--nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py20
-rw-r--r--nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix4
-rw-r--r--nixos/modules/system/boot/luksroot.nix34
-rw-r--r--nixos/modules/system/boot/modprobe.nix2
-rw-r--r--nixos/modules/system/boot/networkd.nix1502
-rw-r--r--nixos/modules/system/boot/plymouth.nix3
-rw-r--r--nixos/modules/system/boot/resolved.nix1
-rw-r--r--nixos/modules/system/boot/stage-1-init.sh22
-rw-r--r--nixos/modules/system/boot/stage-1.nix28
-rw-r--r--nixos/modules/system/boot/stage-2-init.sh2
-rw-r--r--nixos/modules/system/boot/stage-2.nix10
-rw-r--r--nixos/modules/system/boot/systemd-nspawn.nix6
-rw-r--r--nixos/modules/system/boot/systemd-unit-options.nix11
-rw-r--r--nixos/modules/system/boot/systemd.nix173
-rw-r--r--nixos/modules/system/boot/timesyncd.nix1
-rw-r--r--nixos/modules/system/boot/tmp.nix2
-rw-r--r--nixos/modules/system/etc/make-etc.sh7
-rw-r--r--nixos/modules/tasks/auto-upgrade.nix98
-rw-r--r--nixos/modules/tasks/bcache.nix2
-rw-r--r--nixos/modules/tasks/encrypted-devices.nix19
-rw-r--r--nixos/modules/tasks/filesystems/btrfs.nix5
-rw-r--r--nixos/modules/tasks/filesystems/zfs.nix62
-rw-r--r--nixos/modules/tasks/lvm.nix67
-rw-r--r--nixos/modules/tasks/network-interfaces-scripted.nix83
-rw-r--r--nixos/modules/tasks/network-interfaces.nix26
-rw-r--r--nixos/modules/testing/test-instrumentation.nix18
-rw-r--r--nixos/modules/virtualisation/azure-agent.nix2
-rw-r--r--nixos/modules/virtualisation/azure-image.nix2
-rw-r--r--nixos/modules/virtualisation/containers.nix63
-rw-r--r--nixos/modules/virtualisation/cri-o.nix79
-rw-r--r--nixos/modules/virtualisation/docker-preloader.nix134
-rw-r--r--nixos/modules/virtualisation/docker.nix1
-rw-r--r--nixos/modules/virtualisation/hyperv-image.nix1
-rw-r--r--nixos/modules/virtualisation/kvmgt.nix52
-rw-r--r--nixos/modules/virtualisation/libvirtd.nix21
-rw-r--r--nixos/modules/virtualisation/lxd.nix17
-rw-r--r--nixos/modules/virtualisation/podman.nix28
-rw-r--r--nixos/modules/virtualisation/qemu-vm.nix172
-rw-r--r--nixos/modules/virtualisation/railcar.nix12
-rw-r--r--nixos/modules/virtualisation/rkt.nix64
-rw-r--r--nixos/modules/virtualisation/virtualbox-guest.nix2
-rw-r--r--nixos/modules/virtualisation/virtualbox-image.nix66
-rw-r--r--nixos/modules/virtualisation/vmware-image.nix90
-rw-r--r--nixos/release.nix5
-rw-r--r--nixos/tests/acme.nix62
-rw-r--r--nixos/tests/agda.nix41
-rw-r--r--nixos/tests/all-tests.nix52
-rw-r--r--nixos/tests/bazarr.nix26
-rw-r--r--nixos/tests/bcachefs.nix2
-rw-r--r--nixos/tests/bitcoind.nix46
-rw-r--r--nixos/tests/bittorrent.nix24
-rw-r--r--nixos/tests/bitwarden.nix188
-rw-r--r--nixos/tests/blockbook-frontend.nix28
-rw-r--r--nixos/tests/borgbackup.nix2
-rw-r--r--nixos/tests/ceph-multi-node.nix6
-rw-r--r--nixos/tests/ceph-single-node.nix6
-rw-r--r--nixos/tests/common/auto.nix4
-rw-r--r--nixos/tests/common/ec2.nix58
-rw-r--r--nixos/tests/consul.nix136
-rw-r--r--nixos/tests/containers-portforward.nix2
-rw-r--r--nixos/tests/containers-reloadable.nix4
-rw-r--r--nixos/tests/convos.nix30
-rw-r--r--nixos/tests/corerad.nix37
-rw-r--r--nixos/tests/cri-o.nix19
-rw-r--r--nixos/tests/dnscrypt-wrapper/default.nix71
-rw-r--r--nixos/tests/dnscrypt-wrapper/public.key1
-rw-r--r--nixos/tests/dnscrypt-wrapper/secret.key1
-rw-r--r--nixos/tests/docker-preloader.nix27
-rw-r--r--nixos/tests/docker-tools.nix89
-rw-r--r--nixos/tests/docker.nix4
-rw-r--r--nixos/tests/dokuwiki.nix24
-rw-r--r--nixos/tests/ec2.nix140
-rw-r--r--nixos/tests/engelsystem.nix41
-rw-r--r--nixos/tests/enlightenment.nix16
-rw-r--r--nixos/tests/ergo.nix18
-rw-r--r--nixos/tests/firejail.nix82
-rw-r--r--nixos/tests/gnome3-xorg.nix6
-rw-r--r--nixos/tests/gnome3.nix16
-rw-r--r--nixos/tests/go-neb.nix44
-rw-r--r--nixos/tests/graphite.nix11
-rw-r--r--nixos/tests/grub.nix60
-rw-r--r--nixos/tests/haproxy.nix8
-rw-r--r--nixos/tests/hardened.nix114
-rw-r--r--nixos/tests/hocker-fetchdocker/default.nix9
-rw-r--r--nixos/tests/home-assistant.nix134
-rw-r--r--nixos/tests/hostname.nix66
-rw-r--r--nixos/tests/hydra/common.nix1
-rw-r--r--nixos/tests/hydra/db-migration.nix6
-rw-r--r--nixos/tests/ihatemoney.nix13
-rw-r--r--nixos/tests/initrd-network-openvpn/default.nix145
-rw-r--r--nixos/tests/initrd-network-openvpn/initrd.ovpn29
-rw-r--r--nixos/tests/initrd-network-openvpn/shared.key21
-rw-r--r--nixos/tests/installed-tests/default.nix6
-rw-r--r--nixos/tests/installed-tests/flatpak.nix9
-rw-r--r--nixos/tests/installed-tests/ibus.nix14
-rw-r--r--nixos/tests/installed-tests/ostree.nix11
-rw-r--r--nixos/tests/installer.nix23
-rw-r--r--nixos/tests/ipfs.nix18
-rw-r--r--nixos/tests/jitsi-meet.nix55
-rw-r--r--nixos/tests/kubernetes/base.nix9
-rw-r--r--nixos/tests/kubernetes/dns.nix74
-rw-r--r--nixos/tests/kubernetes/rbac.nix86
-rw-r--r--nixos/tests/leaps.nix2
-rw-r--r--nixos/tests/lorri/default.nix2
-rw-r--r--nixos/tests/lxd-nftables.nix50
-rw-r--r--nixos/tests/lxd.nix135
-rw-r--r--nixos/tests/mathics.nix20
-rw-r--r--nixos/tests/matrix-synapse.nix4
-rw-r--r--nixos/tests/mesos.nix92
-rw-r--r--nixos/tests/mesos_test.py72
-rw-r--r--nixos/tests/misc.nix32
-rw-r--r--nixos/tests/molly-brown.nix71
-rw-r--r--nixos/tests/mongodb.nix6
-rw-r--r--nixos/tests/mysql/mariadb-galera-mariabackup.nix32
-rw-r--r--nixos/tests/mysql/mariadb-galera-rsync.nix32
-rw-r--r--nixos/tests/mysql/mysql-replication.nix12
-rw-r--r--nixos/tests/mysql/mysql.nix114
-rw-r--r--nixos/tests/ncdns.nix77
-rw-r--r--nixos/tests/networking.nix21
-rw-r--r--nixos/tests/nextcloud/basic.nix27
-rw-r--r--nixos/tests/nextcloud/with-mysql-and-memcached.nix1
-rw-r--r--nixos/tests/nextcloud/with-postgresql-and-redis.nix1
-rw-r--r--nixos/tests/nfs/kerberos.nix4
-rw-r--r--nixos/tests/nginx-pubhtml.nix1
-rw-r--r--nixos/tests/nginx-sandbox.nix66
-rw-r--r--nixos/tests/nginx-variants.nix33
-rw-r--r--nixos/tests/openstack-image.nix70
-rw-r--r--nixos/tests/os-prober.nix33
-rw-r--r--nixos/tests/pinnwand.nix86
-rw-r--r--nixos/tests/plasma5.nix2
-rw-r--r--nixos/tests/podman.nix33
-rw-r--r--nixos/tests/postfix-raise-smtpd-tls-security-level.nix44
-rw-r--r--nixos/tests/postfix.nix76
-rw-r--r--nixos/tests/postgresql-wal-receiver.nix4
-rw-r--r--nixos/tests/privacyidea.nix36
-rw-r--r--nixos/tests/prometheus-exporters.nix117
-rw-r--r--nixos/tests/prometheus.nix7
-rw-r--r--nixos/tests/pt2-clone.nix35
-rw-r--r--nixos/tests/qboot.nix13
-rw-r--r--nixos/tests/radicale.nix28
-rw-r--r--nixos/tests/restic.nix55
-rw-r--r--nixos/tests/sddm.nix4
-rw-r--r--nixos/tests/shattered-pixel-dungeon.nix29
-rw-r--r--nixos/tests/slurm.nix93
-rw-r--r--nixos/tests/snapcast.nix58
-rw-r--r--nixos/tests/sogo.nix58
-rw-r--r--nixos/tests/sslh.nix83
-rw-r--r--nixos/tests/sudo.nix2
-rw-r--r--nixos/tests/syncthing-init.nix3
-rw-r--r--nixos/tests/syncthing.nix65
-rw-r--r--nixos/tests/systemd-binfmt.nix24
-rw-r--r--nixos/tests/systemd-boot.nix89
-rw-r--r--nixos/tests/systemd-networkd-vrf.nix24
-rw-r--r--nixos/tests/systemd.nix52
-rw-r--r--nixos/tests/taskserver.nix310
-rw-r--r--nixos/tests/teeworlds.nix55
-rw-r--r--nixos/tests/tiddlywiki.nix2
-rw-r--r--nixos/tests/transmission.nix2
-rw-r--r--nixos/tests/trezord.nix5
-rw-r--r--nixos/tests/trickster.nix6
-rw-r--r--nixos/tests/virtualbox.nix1
-rw-r--r--nixos/tests/wasabibackend.nix38
-rw-r--r--nixos/tests/web-servers/unit-php.nix14
-rw-r--r--nixos/tests/wireguard/basic.nix74
-rw-r--r--nixos/tests/wireguard/default.nix96
-rw-r--r--nixos/tests/wireguard/generated.nix5
-rw-r--r--nixos/tests/wireguard/namespaces.nix8
-rw-r--r--nixos/tests/wireguard/wg-quick.nix4
-rw-r--r--nixos/tests/xandikos.nix6
-rw-r--r--nixos/tests/xfce.nix4
-rw-r--r--nixos/tests/yggdrasil.nix43
-rw-r--r--nixos/tests/zfs.nix23
-rw-r--r--nixos/tests/zigbee2mqtt.nix19
-rw-r--r--nixos/tests/zoneminder.nix23
498 files changed, 16928 insertions, 6014 deletions
diff --git a/nixos/doc/manual/administration/boot-problems.xml b/nixos/doc/manual/administration/boot-problems.xml
index de3d8ac21ae..badc374ebcf 100644
--- a/nixos/doc/manual/administration/boot-problems.xml
+++ b/nixos/doc/manual/administration/boot-problems.xml
@@ -19,9 +19,9 @@
     </term>
     <listitem>
      <para>
-      Start a root shell if something goes wrong in stage 1 of the boot process
-      (the initial ramdisk). This is disabled by default because there is no
-      authentication for the root shell.
+      Allows the user to start a root shell if something goes wrong in stage 1
+      of the boot process (the initial ramdisk). This is disabled by default
+      because there is no authentication for the root shell.
      </para>
     </listitem>
    </varlistentry>
@@ -40,6 +40,33 @@
    </varlistentry>
    <varlistentry>
     <term>
+     <literal>boot.debug1devices</literal>
+    </term>
+    <listitem>
+     <para>
+      Like <literal>boot.debug1</literal>, but runs stage1 until kernel modules are loaded and device nodes are created.
+      This may help with e.g. making the keyboard work.
+     </para>
+    </listitem>
+   </varlistentry>
+   <varlistentry>
+    <term>
+     <literal>boot.debug1mounts</literal>
+    </term>
+    <listitem>
+     <para>
+      Like <literal>boot.debug1</literal> or
+      <literal>boot.debug1devices</literal>, but runs stage1 until all
+      filesystems that are mounted during initrd are mounted (see
+      <option><link linkend="opt-fileSystems._name__.neededForBoot">neededForBoot</link></option>
+      ). As a motivating example, this could be useful if you've forgotten to set
+      <option><link linkend="opt-fileSystems._name__.neededForBoot">neededForBoot</link></option>
+      on a file system.
+     </para>
+    </listitem>
+   </varlistentry>
+   <varlistentry>
+    <term>
      <literal>boot.trace</literal>
     </term>
     <listitem>
@@ -80,6 +107,15 @@
  </para>
 
  <para>
+  Notice that for <literal>boot.shell_on_fail</literal>,
+  <literal>boot.debug1</literal>, <literal>boot.debug1devices</literal>, and
+  <literal>boot.debug1mounts</literal>, if you did <emphasis>not</emphasis>
+  select "start the new shell as pid 1", and you <literal>exit</literal> from
+  the new shell, boot will proceed normally from the point where it failed, as
+  if you'd chosen "ignore the error and continue".
+ </para>
+
+ <para>
   If no login prompts or X11 login screens appear (e.g. due to hanging
   dependencies), you can press Alt+ArrowUp. If you’re lucky, this will start
   rescue mode (described above). (Also note that since most units have a
diff --git a/nixos/doc/manual/configuration/configuration.xml b/nixos/doc/manual/configuration/configuration.xml
index 507d28814ea..6eb8f50baca 100644
--- a/nixos/doc/manual/configuration/configuration.xml
+++ b/nixos/doc/manual/configuration/configuration.xml
@@ -18,6 +18,7 @@
  <xi:include href="user-mgmt.xml" />
  <xi:include href="file-systems.xml" />
  <xi:include href="x-windows.xml" />
+ <xi:include href="gpu-accel.xml" />
  <xi:include href="xfce.xml" />
  <xi:include href="networking.xml" />
  <xi:include href="linux-kernel.xml" />
diff --git a/nixos/doc/manual/configuration/file-systems.xml b/nixos/doc/manual/configuration/file-systems.xml
index e4c03de71b7..3ac02a975eb 100644
--- a/nixos/doc/manual/configuration/file-systems.xml
+++ b/nixos/doc/manual/configuration/file-systems.xml
@@ -16,6 +16,17 @@
     fsType = "ext4";
   };
 </programlisting>
+  This will create an entry in <filename>/etc/fstab</filename>, which will
+  generate a corresponding
+  <link xlink:href="https://www.freedesktop.org/software/systemd/man/systemd.mount.html">systemd.mount</link>
+  unit via
+  <link xlink:href="https://www.freedesktop.org/software/systemd/man/systemd-fstab-generator.html">systemd-fstab-generator</link>.
+  The filesystem will be mounted automatically unless
+  <literal>"noauto"</literal> is present in <link
+  linkend="opt-fileSystems._name__.options">options</link>.
+  <literal>"noauto"</literal> filesystems can be mounted explicitly using
+  <command>systemctl</command> e.g. <command>systemctl start
+  data.mount</command>.
   Mount points are created automatically if they don’t already exist. For
   <option><link linkend="opt-fileSystems._name__.device">device</link></option>,
   it’s best to use the topology-independent device aliases in
diff --git a/nixos/doc/manual/configuration/gpu-accel.xml b/nixos/doc/manual/configuration/gpu-accel.xml
new file mode 100644
index 00000000000..251e5c26ba4
--- /dev/null
+++ b/nixos/doc/manual/configuration/gpu-accel.xml
@@ -0,0 +1,258 @@
+<chapter xmlns="http://docbook.org/ns/docbook"
+         xmlns:xlink="http://www.w3.org/1999/xlink"
+         xmlns:xi="http://www.w3.org/2001/XInclude"
+         version="5.0"
+         xml:id="sec-gpu-accel">
+  <title>GPU acceleration</title>
+
+  <para>
+    NixOS provides various APIs that benefit from GPU hardware
+    acceleration, such as VA-API and VDPAU for video playback; OpenGL and
+    Vulkan for 3D graphics; and OpenCL for general-purpose computing.
+    This chapter describes how to set up GPU hardware acceleration (as far
+    as this is not done automatically) and how to verify that hardware
+    acceleration is indeed used.
+  </para>
+
+  <para>
+    Most of the aforementioned APIs are agnostic with regards to which
+    display server is used. Consequently, these instructions should apply
+    both to the X Window System and Wayland compositors.
+  </para>
+
+  <section xml:id="sec-gpu-accel-opencl">
+    <title>OpenCL</title>
+
+    <para>
+      <link xlink:href="https://en.wikipedia.org/wiki/OpenCL">OpenCL</link> is a
+      general compute API. It is used by various applications such as
+      Blender and Darktable to accelerate certain operations.
+    </para>
+
+    <para>
+      OpenCL applications load drivers through the <emphasis>Installable Client
+      Driver</emphasis> (ICD) mechanism. In this mechanism, an ICD file
+      specifies the path to the OpenCL driver for a particular GPU family.
+      In NixOS, there are two ways to make ICD files visible to the ICD
+      loader. The first is through the <varname>OCL_ICD_VENDORS</varname>
+      environment variable. This variable can contain a directory which
+      is scanned by the ICL loader for ICD files. For example:
+
+      <screen><prompt>$</prompt> export \
+  OCL_ICD_VENDORS=`nix-build '&lt;nixpkgs&gt;' --no-out-link -A rocm-opencl-icd`/etc/OpenCL/vendors/</screen>
+    </para>
+
+    <para>
+      The second mechanism is to add the OpenCL driver package to
+      <xref linkend="opt-hardware.opengl.extraPackages"/>. This links the
+      ICD file under <filename>/run/opengl-driver</filename>, where it will
+      be visible to the ICD loader.
+    </para>
+
+    <para>
+      The proper installation of OpenCL drivers can be verified through
+      the <command>clinfo</command> command of the <package>clinfo</package>
+      package. This command will report the number of hardware devices
+      that is found and give detailed information for each device:
+    </para>
+
+    <screen><prompt>$</prompt> clinfo | head -n3
+Number of platforms  1
+Platform Name        AMD Accelerated Parallel Processing
+Platform Vendor      Advanced Micro Devices, Inc.</screen>
+
+    <section xml:id="sec-gpu-accel-opencl-amd">
+      <title>AMD</title>
+
+      <para>
+	Modern AMD <link
+	xlink:href="https://en.wikipedia.org/wiki/Graphics_Core_Next">Graphics
+	Core Next</link> (GCN) GPUs are supported through the
+	<package>rocm-opencl-icd</package> package. Adding this package to
+	<xref linkend="opt-hardware.opengl.extraPackages"/> enables OpenCL
+	support:
+
+	<programlisting><xref linkend="opt-hardware.opengl.extraPackages"/> = [
+  rocm-opencl-icd
+];</programlisting>
+      </para>
+    </section>
+
+    <section xml:id="sec-gpu-accel-opencl-intel">
+      <title>Intel</title>
+
+      <para>
+       <link
+         xlink:href="https://en.wikipedia.org/wiki/List_of_Intel_graphics_processing_units#Gen8">Intel
+       Gen8 and later GPUs</link> are supported by the Intel NEO OpenCL
+       runtime that is provided by the
+       <package>intel-compute-runtime</package> package. For Gen7 GPUs,
+       the deprecated Beignet runtime can be used, which is provided
+       by the <package>beignet</package> package. The proprietary Intel
+       OpenCL runtime, in the <package>intel-ocl</package> package, is
+       an alternative for Gen7 GPUs.
+      </para>
+
+      <para>
+       The <package>intel-compute-runtime</package>, <package>beignet</package>,
+       or <package>intel-ocl</package> package can be added to
+       <xref linkend="opt-hardware.opengl.extraPackages"/> to enable OpenCL
+       support. For example, for Gen8 and later GPUs, the following
+       configuration can be used:
+
+	      <programlisting><xref linkend="opt-hardware.opengl.extraPackages"/> = [
+  intel-compute-runtime
+];</programlisting>
+
+      </para>
+    </section>
+  </section>
+
+  <section xml:id="sec-gpu-accel-vulkan">
+    <title>Vulkan</title>
+
+    <para>
+      <link xlink:href="https://en.wikipedia.org/wiki/Vulkan_(API)">Vulkan</link> is a
+      graphics and compute API for GPUs. It is used directly by games or indirectly though
+      compatibility layers like <link xlink:href="https://github.com/doitsujin/dxvk/wiki">DXVK</link>.
+    </para>
+
+    <para>
+     By default, if <xref linkend="opt-hardware.opengl.driSupport"/> is enabled,
+     <package>mesa</package> is installed and provides Vulkan for supported hardware.
+    </para>
+
+    <para>
+      Similar to OpenCL, Vulkan drivers are loaded through the <emphasis>Installable Client
+      Driver</emphasis> (ICD) mechanism. ICD files for Vulkan are JSON files that specify
+      the path to the driver library and the supported Vulkan version. All successfully
+      loaded drivers are exposed to the application as different GPUs.
+      In NixOS, there are two ways to make ICD files visible to Vulkan applications: an
+      environment variable and a module option.
+    </para>
+
+    <para>
+      The first option is through the <varname>VK_ICD_FILENAMES</varname>
+      environment variable. This variable can contain multiple JSON files, separated by
+      <literal>:</literal>. For example:
+
+      <screen><prompt>$</prompt> export \
+  VK_ICD_FILENAMES=`nix-build '&lt;nixpkgs&gt;' --no-out-link -A amdvlk`/share/vulkan/icd.d/amd_icd64.json</screen>
+    </para>
+
+    <para>
+      The second mechanism is to add the Vulkan driver package to
+      <xref linkend="opt-hardware.opengl.extraPackages"/>. This links the
+      ICD file under <filename>/run/opengl-driver</filename>, where it will
+      be visible to the ICD loader.
+    </para>
+
+    <para>
+      The proper installation of Vulkan drivers can be verified through
+      the <command>vulkaninfo</command> command of the <package>vulkan-tools</package>
+      package. This command will report the hardware devices and drivers found,
+      in this example output amdvlk and radv:
+    </para>
+
+    <screen><prompt>$</prompt> vulkaninfo | grep GPU
+                GPU id  : 0 (Unknown AMD GPU)
+                GPU id  : 1 (AMD RADV NAVI10 (LLVM 9.0.1))
+     ...
+GPU0:
+        deviceType     = PHYSICAL_DEVICE_TYPE_DISCRETE_GPU
+        deviceName     = Unknown AMD GPU
+GPU1:
+        deviceType     = PHYSICAL_DEVICE_TYPE_DISCRETE_GPU</screen>
+
+    <para>
+      A simple graphical application that uses Vulkan is <command>vkcube</command>
+      from the <package>vulkan-tools</package> package.
+    </para>
+
+    <section xml:id="sec-gpu-accel-vulkan-amd">
+      <title>AMD</title>
+
+      <para>
+	Modern AMD <link
+	xlink:href="https://en.wikipedia.org/wiki/Graphics_Core_Next">Graphics
+	Core Next</link> (GCN) GPUs are supported through either radv, which is
+	part of <package>mesa</package>, or the <package>amdvlk</package> package.
+	Adding the <package>amdvlk</package> package to
+	<xref linkend="opt-hardware.opengl.extraPackages"/> makes both drivers
+	available for applications and lets them choose. A specific driver can
+	be forced as follows:
+
+	<programlisting><xref linkend="opt-hardware.opengl.extraPackages"/> = [
+  <package>amdvlk</package>
+];
+
+# For amdvlk
+<xref linkend="opt-environment.variables"/>.VK_ICD_FILENAMES =
+   "/run/opengl-driver/share/vulkan/icd.d/amd_icd64.json";
+# For radv
+<xref linkend="opt-environment.variables"/>.VK_ICD_FILENAMES =
+  "/run/opengl-driver/share/vulkan/icd.d/radeon_icd.x86_64.json";
+</programlisting>
+      </para>
+    </section>
+  </section>
+
+  <section xml:id="sec-gpu-accel-common-issues">
+   <title>Common issues</title>
+
+   <section xml:id="sec-gpu-accel-common-issues-permissions">
+    <title>User permissions</title>
+
+    <para>
+     Except where noted explicitly, it should not be necessary to
+     adjust user permissions to use these acceleration APIs. In the default
+     configuration, GPU devices have world-read/write permissions
+     (<filename>/dev/dri/renderD*</filename>) or are tagged as
+     <code>uaccess</code> (<filename>/dev/dri/card*</filename>).  The
+     access control lists of devices with the <varname>uaccess</varname>
+     tag will be updated automatically when a user logs in through
+     <command>systemd-logind</command>. For example, if the user
+     <emphasis>jane</emphasis> is logged in, the access control list
+     should look as follows:
+
+     <screen><prompt>$</prompt> getfacl /dev/dri/card0
+# file: dev/dri/card0
+# owner: root
+# group: video
+user::rw-
+user:jane:rw-
+group::rw-
+mask::rw-
+other::---</screen>
+
+     If you disabled (this functionality of) <command>systemd-logind</command>,
+     you may need to add the user to the <code>video</code> group and
+     log in again.
+    </para>
+   </section>
+
+   <section xml:id="sec-gpu-accel-common-issues-mixing-nixpkgs">
+    <title>Mixing different versions of nixpkgs</title>
+
+    <para>
+     The <emphasis>Installable Client Driver</emphasis> (ICD)
+     mechanism used by OpenCL and Vulkan loads runtimes into its address
+     space using <code>dlopen</code>. Mixing an ICD loader mechanism and
+     runtimes from different version of nixpkgs may not work. For example,
+     if the ICD loader uses an older version of <package>glibc</package>
+     than the runtime, the runtime may not be loadable due to
+     missing symbols. Unfortunately, the loader will generally be quiet
+     about such issues.
+    </para>
+
+    <para>
+     If you suspect that you are running into library version mismatches
+     between an ICL loader and a runtime, you could run an application with
+     the <code>LD_DEBUG</code> variable set to get more diagnostic
+     information. For example, OpenCL can be tested with
+     <code>LD_DEBUG=files clinfo</code>, which should report missing
+     symbols.
+    </para>
+   </section>
+  </section>
+</chapter>
diff --git a/nixos/doc/manual/configuration/profiles/demo.xml b/nixos/doc/manual/configuration/profiles/demo.xml
index 395a5ec357c..bc801bb3dc5 100644
--- a/nixos/doc/manual/configuration/profiles/demo.xml
+++ b/nixos/doc/manual/configuration/profiles/demo.xml
@@ -9,7 +9,6 @@
   This profile just enables a <systemitem class="username">demo</systemitem>
   user, with password <literal>demo</literal>, uid <literal>1000</literal>,
   <systemitem class="groupname">wheel</systemitem> group and
-  <link linkend="opt-services.xserver.displayManager.sddm.autoLogin"> autologin
-  in the SDDM display manager</link>.
+  <link linkend="opt-services.xserver.displayManager.autoLogin">autologin in the SDDM display manager</link>.
  </para>
 </section>
diff --git a/nixos/doc/manual/configuration/x-windows.xml b/nixos/doc/manual/configuration/x-windows.xml
index 110712baf5f..18f0be5e7f3 100644
--- a/nixos/doc/manual/configuration/x-windows.xml
+++ b/nixos/doc/manual/configuration/x-windows.xml
@@ -90,10 +90,50 @@
   using lightdm for a user <literal>alice</literal>:
 <programlisting>
 <xref linkend="opt-services.xserver.displayManager.lightdm.enable"/> = true;
-<xref linkend="opt-services.xserver.displayManager.lightdm.autoLogin.enable"/> = true;
-<xref linkend="opt-services.xserver.displayManager.lightdm.autoLogin.user"/> = "alice";
+<xref linkend="opt-services.xserver.displayManager.autoLogin.enable"/> = true;
+<xref linkend="opt-services.xserver.displayManager.autoLogin.user"/> = "alice";
 </programlisting>
-  The options are named identically for all other display managers.
+  </para>
+ </simplesect>
+ <simplesect xml:id="sec-x11--graphics-cards-intel">
+  <title>Intel Graphics drivers</title>
+  <para>
+   There are two choices for Intel Graphics drivers in X.org:
+   <literal>modesetting</literal> (included in the <package>xorg-server</package> itself)
+   and <literal>intel</literal> (provided by the package <package>xf86-video-intel</package>).
+  </para>
+  <para>
+   The default and recommended is <literal>modesetting</literal>.
+   It is a generic driver which uses the kernel
+   <link xlink:href="https://en.wikipedia.org/wiki/Mode_setting">mode setting</link>
+   (KMS) mechanism. It supports Glamor (2D graphics acceleration via OpenGL)
+   and is actively maintained but may perform worse in some cases (like in old chipsets).
+  </para>
+  <para>
+   The second driver, <literal>intel</literal>, is specific to Intel GPUs,
+   but not recommended by most distributions: it lacks several modern features
+   (for example, it doesn't support Glamor) and the package hasn't been officially
+   updated since 2015.
+  </para>
+  <para>
+   The results vary depending on the hardware, so you may have to try both drivers.
+   Use the option <xref linkend="opt-services.xserver.videoDrivers"/> to set one.
+   The recommended configuration for modern systems is:
+<programlisting>
+  <xref linkend="opt-services.xserver.videoDrivers"/> = [ "modesetting" ];
+  <xref linkend="opt-services.xserver.useGlamor"/> = true;
+</programlisting>
+   If you experience screen tearing no matter what, this configuration was
+   reported to resolve the issue:
+<programlisting>
+  <xref linkend="opt-services.xserver.videoDrivers"/> = [ "intel" ];
+  <xref linkend="opt-services.xserver.deviceSection"/> = ''
+    Option "DRI" "2"
+    Option "TearFree" "true"
+  '';
+</programlisting>
+   Note that this will likely downgrade the performance compared to
+  <literal>modesetting</literal> or <literal>intel</literal> with DRI 3 (default).
   </para>
  </simplesect>
  <simplesect xml:id="sec-x11-graphics-cards-nvidia">
diff --git a/nixos/doc/manual/development/freeform-modules.xml b/nixos/doc/manual/development/freeform-modules.xml
new file mode 100644
index 00000000000..257e6b11bf0
--- /dev/null
+++ b/nixos/doc/manual/development/freeform-modules.xml
@@ -0,0 +1,68 @@
+<section xmlns="http://docbook.org/ns/docbook"
+        xmlns:xlink="http://www.w3.org/1999/xlink"
+        xmlns:xi="http://www.w3.org/2001/XInclude"
+        version="5.0"
+        xml:id="sec-freeform-modules">
+ <title>Freeform modules</title>
+ <para>
+  Freeform modules allow you to define values for option paths that have not been declared explicitly. This can be used to add attribute-specific types to what would otherwise have to be <literal>attrsOf</literal> options in order to accept all attribute names.
+ </para>
+ <para>
+  This feature can be enabled by using the attribute <literal>freeformType</literal> to define a freeform type. By doing this, all assignments without an associated option will be merged using the freeform type and combined into the resulting <literal>config</literal> set. Since this feature nullifies name checking for entire option trees, it is only recommended for use in submodules.
+ </para>
+ <example xml:id="ex-freeform-module">
+  <title>Freeform submodule</title>
+  <para>
+   The following shows a submodule assigning a freeform type that allows arbitrary attributes with <literal>str</literal> values below <literal>settings</literal>, but also declares an option for the <literal>settings.port</literal> attribute to have it type-checked and assign a default value. See <xref linkend="ex-settings-typed-attrs"/> for a more complete example.
+  </para>
+ <programlisting>
+{ lib, config, ... }: {
+
+  options.settings = lib.mkOption {
+    type = lib.types.submodule {
+
+      freeformType = with lib.types; attrsOf str;
+
+      # We want this attribute to be checked for the correct type
+      options.port = lib.mkOption {
+        type = lib.types.port;
+        # Declaring the option also allows defining a default value
+        default = 8080;
+      };
+
+    };
+  };
+}
+ </programlisting>
+ <para>
+  And the following shows what such a module then allows
+ </para>
+ <programlisting>
+{
+  # Not a declared option, but the freeform type allows this
+  settings.logLevel = "debug";
+
+  # Not allowed because the the freeform type only allows strings
+  # settings.enable = true;
+
+  # Allowed because there is a port option declared
+  settings.port = 80;
+
+  # Not allowed because the port option doesn't allow strings
+  # settings.port = "443";
+}
+ </programlisting>
+ </example>
+ <note>
+  <para>
+   Freeform attributes cannot depend on other attributes of the same set without infinite recursion:
+<programlisting>
+{
+  # This throws infinite recursion encountered
+  settings.logLevel = lib.mkIf (config.settings.port == 80) "debug";
+}
+</programlisting>
+   To prevent this, declare options for all attributes that need to depend on others. For above example this means to declare <literal>logLevel</literal> to be an option.
+  </para>
+ </note>
+</section>
diff --git a/nixos/doc/manual/development/running-nixos-tests-interactively.xml b/nixos/doc/manual/development/running-nixos-tests-interactively.xml
index 31216874c70..a11a9382764 100644
--- a/nixos/doc/manual/development/running-nixos-tests-interactively.xml
+++ b/nixos/doc/manual/development/running-nixos-tests-interactively.xml
@@ -38,7 +38,12 @@ starting VDE switch for network 1
  </para>
 
  <para>
-  The machine state is kept across VM restarts in
-  <filename>/tmp/vm-state-</filename><varname>machinename</varname>.
+   You can re-use the VM states coming from a previous run
+   by setting the <command>--keep-vm-state</command> flag.
+<screen>
+<prompt>$ </prompt>./result/bin/nixos-run-vms --keep-vm-state
+</screen>
+  The machine state is stored in the
+  <filename>$TMPDIR/vm-state-</filename><varname>machinename</varname> directory.
  </para>
 </section>
diff --git a/nixos/doc/manual/development/settings-options.xml b/nixos/doc/manual/development/settings-options.xml
new file mode 100644
index 00000000000..c99c3af92f8
--- /dev/null
+++ b/nixos/doc/manual/development/settings-options.xml
@@ -0,0 +1,216 @@
+<section xmlns="http://docbook.org/ns/docbook"
+        xmlns:xlink="http://www.w3.org/1999/xlink"
+        xmlns:xi="http://www.w3.org/2001/XInclude"
+        version="5.0"
+        xml:id="sec-settings-options">
+ <title>Options for Program Settings</title>
+
+ <para>
+   Many programs have configuration files where program-specific settings can be declared. File formats can be separated into two categories:
+   <itemizedlist>
+     <listitem>
+       <para>
+         Nix-representable ones: These can trivially be mapped to a subset of Nix syntax. E.g. JSON is an example, since its values like <literal>{"foo":{"bar":10}}</literal> can be mapped directly to Nix: <literal>{ foo = { bar = 10; }; }</literal>. Other examples are INI, YAML and TOML. The following section explains the convention for these settings.
+       </para>
+     </listitem>
+     <listitem>
+       <para>
+         Non-nix-representable ones: These can't be trivially mapped to a subset of Nix syntax. Most generic programming languages are in this group, e.g. bash, since the statement <literal>if true; then echo hi; fi</literal> doesn't have a trivial representation in Nix.
+       </para>
+       <para>
+         Currently there are no fixed conventions for these, but it is common to have a <literal>configFile</literal> option for setting the configuration file path directly. The default value of <literal>configFile</literal> can be an auto-generated file, with convenient options for controlling the contents. For example an option of type <literal>attrsOf str</literal> can be used for representing environment variables which generates a section like <literal>export FOO="foo"</literal>. Often it can also be useful to also include an <literal>extraConfig</literal> option of type <literal>lines</literal> to allow arbitrary text after the autogenerated part of the file.
+       </para>
+     </listitem>
+   </itemizedlist>
+ </para>
+ <section xml:id="sec-settings-nix-representable">
+   <title>Nix-representable Formats (JSON, YAML, TOML, INI, ...)</title>
+   <para>
+     By convention, formats like this are handled with a generic <literal>settings</literal> option, representing the full program configuration as a Nix value. The type of this option should represent the format. The most common formats have a predefined type and string generator already declared under <literal>pkgs.formats</literal>:
+     <variablelist>
+       <varlistentry>
+         <term>
+           <varname>pkgs.formats.json</varname> { }
+         </term>
+         <listitem>
+           <para>
+             A function taking an empty attribute set (for future extensibility) and returning a set with JSON-specific attributes <varname>type</varname> and <varname>generate</varname> as specified <link linkend='pkgs-formats-result'>below</link>.
+           </para>
+         </listitem>
+       </varlistentry>
+       <varlistentry>
+         <term>
+           <varname>pkgs.formats.yaml</varname> { }
+         </term>
+         <listitem>
+           <para>
+             A function taking an empty attribute set (for future extensibility) and returning a set with YAML-specific attributes <varname>type</varname> and <varname>generate</varname> as specified <link linkend='pkgs-formats-result'>below</link>.
+           </para>
+         </listitem>
+       </varlistentry>
+       <varlistentry>
+         <term>
+           <varname>pkgs.formats.ini</varname> { <replaceable>listsAsDuplicateKeys</replaceable> ? false, ... }
+         </term>
+         <listitem>
+           <para>
+             A function taking an attribute set with values
+             <variablelist>
+               <varlistentry>
+                 <term>
+                   <varname>listsAsDuplicateKeys</varname>
+                 </term>
+                 <listitem>
+                   <para>
+                     A boolean for controlling whether list values can be used to represent duplicate INI keys
+                   </para>
+                 </listitem>
+               </varlistentry>
+             </variablelist>
+            It returns a set with INI-specific attributes <varname>type</varname> and <varname>generate</varname> as specified <link linkend='pkgs-formats-result'>below</link>.
+           </para>
+         </listitem>
+       </varlistentry>
+       <varlistentry>
+         <term>
+           <varname>pkgs.formats.toml</varname> { }
+         </term>
+         <listitem>
+           <para>
+             A function taking an empty attribute set (for future extensibility) and returning a set with TOML-specific attributes <varname>type</varname> and <varname>generate</varname> as specified <link linkend='pkgs-formats-result'>below</link>.
+           </para>
+         </listitem>
+       </varlistentry>
+     </variablelist>
+
+   </para>
+   <para xml:id="pkgs-formats-result">
+     These functions all return an attribute set with these values:
+    <variablelist>
+      <varlistentry>
+        <term>
+          <varname>type</varname>
+        </term>
+        <listitem>
+          <para>
+            A module system type representing a value of the format
+          </para>
+        </listitem>
+      </varlistentry>
+      <varlistentry>
+        <term>
+          <varname>generate</varname> <replaceable>filename</replaceable> <replaceable>jsonValue</replaceable>
+        </term>
+        <listitem>
+          <para>
+           A function that can render a value of the format to a file. Returns a file path.
+           <note>
+            <para>
+             This function puts the value contents in the Nix store. So this should be avoided for secrets.
+            </para>
+           </note>
+          </para>
+        </listitem>
+      </varlistentry>
+    </variablelist>
+   </para>
+   <example xml:id="ex-settings-nix-representable">
+     <title>Module with conventional <literal>settings</literal> option</title>
+     <para>
+       The following shows a module for an example program that uses a JSON configuration file. It demonstrates how above values can be used, along with some other related best practices. See the comments for explanations.
+     </para>
+<programlisting>
+{ options, config, lib, pkgs, ... }:
+let
+  cfg = config.services.foo;
+  # Define the settings format used for this program
+  settingsFormat = pkgs.formats.json {};
+in {
+
+  options.services.foo = {
+    enable = lib.mkEnableOption "foo service";
+
+    settings = lib.mkOption {
+      # Setting this type allows for correct merging behavior
+      type = settingsFormat.type;
+      default = {};
+      description = ''
+        Configuration for foo, see
+        &lt;link xlink:href="https://example.com/docs/foo"/&gt;
+        for supported settings.
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    # We can assign some default settings here to make the service work by just
+    # enabling it. We use `mkDefault` for values that can be changed without
+    # problems
+    services.foo.settings = {
+      # Fails at runtime without any value set
+      log_level = lib.mkDefault "WARN";
+
+      # We assume systemd's `StateDirectory` is used, so we require this value,
+      # therefore no mkDefault
+      data_path = "/var/lib/foo";
+
+      # Since we use this to create a user we need to know the default value at
+      # eval time
+      user = lib.mkDefault "foo";
+    };
+
+    environment.etc."foo.json".source =
+      # The formats generator function takes a filename and the Nix value
+      # representing the format value and produces a filepath with that value
+      # rendered in the format
+      settingsFormat.generate "foo-config.json" cfg.settings;
+
+    # We know that the `user` attribute exists because we set a default value
+    # for it above, allowing us to use it without worries here
+    users.users.${cfg.settings.user} = {};
+
+    # ...
+  };
+}
+</programlisting>
+   </example>
+   <section xml:id="sec-settings-attrs-options">
+    <title>Option declarations for attributes</title>
+    <para>
+     Some <literal>settings</literal> attributes may deserve some extra care. They may need a different type, default or merging behavior, or they are essential options that should show their documentation in the manual. This can be done using <xref linkend='sec-freeform-modules'/>.
+     <example xml:id="ex-settings-typed-attrs">
+      <title>Declaring a type-checked <literal>settings</literal> attribute</title>
+      <para>
+       We extend above example using freeform modules to declare an option for the port, which will enforce it to be a valid integer and make it show up in the manual.
+      </para>
+<programlisting>
+settings = lib.mkOption {
+  type = lib.types.submodule {
+
+    freeformType = settingsFormat.type;
+
+    # Declare an option for the port such that the type is checked and this option
+    # is shown in the manual.
+    options.port = lib.mkOption {
+      type = lib.types.port;
+      default = 8080;
+      description = ''
+        Which port this service should listen on.
+      '';
+    };
+
+  };
+  default = {};
+  description = ''
+    Configuration for Foo, see
+    &lt;link xlink:href="https://example.com/docs/foo"/&gt;
+    for supported values.
+  '';
+};
+</programlisting>
+     </example>
+    </para>
+   </section>
+ </section>
+
+</section>
diff --git a/nixos/doc/manual/development/writing-modules.xml b/nixos/doc/manual/development/writing-modules.xml
index bbf793bb0be..d244356dbed 100644
--- a/nixos/doc/manual/development/writing-modules.xml
+++ b/nixos/doc/manual/development/writing-modules.xml
@@ -183,4 +183,6 @@ in {
  <xi:include href="meta-attributes.xml" />
  <xi:include href="importing-modules.xml" />
  <xi:include href="replace-modules.xml" />
+ <xi:include href="freeform-modules.xml" />
+ <xi:include href="settings-options.xml" />
 </chapter>
diff --git a/nixos/doc/manual/development/writing-nixos-tests.xml b/nixos/doc/manual/development/writing-nixos-tests.xml
index e5a887c18c7..74ab23605b3 100644
--- a/nixos/doc/manual/development/writing-nixos-tests.xml
+++ b/nixos/doc/manual/development/writing-nixos-tests.xml
@@ -216,12 +216,12 @@ start_all()
    </varlistentry>
    <varlistentry>
     <term>
-     <methodname>send_keys</methodname>
+     <methodname>send_key</methodname>
     </term>
     <listitem>
      <para>
       Simulate pressing keys on the virtual keyboard, e.g.,
-      <literal>send_keys("ctrl-alt-delete")</literal>.
+      <literal>send_key("ctrl-alt-delete")</literal>.
      </para>
     </listitem>
    </varlistentry>
@@ -232,7 +232,7 @@ start_all()
     <listitem>
      <para>
       Simulate typing a sequence of characters on the virtual keyboard, e.g.,
-      <literal>send_keys("foobar\n")</literal> will type the string
+      <literal>send_chars("foobar\n")</literal> will type the string
       <literal>foobar</literal> followed by the Enter key.
      </para>
     </listitem>
@@ -362,6 +362,18 @@ start_all()
    </varlistentry>
    <varlistentry>
     <term>
+     <methodname>wait_for_console_text</methodname>
+    </term>
+    <listitem>
+     <para>
+      Wait until the supplied regular expressions match a line of the serial
+      console output. This method is useful when OCR is not possibile or
+      accurate enough.
+     </para>
+    </listitem>
+   </varlistentry>
+   <varlistentry>
+    <term>
      <methodname>wait_for_window</methodname>
     </term>
     <listitem>
@@ -373,12 +385,12 @@ start_all()
    </varlistentry>
    <varlistentry>
     <term>
-     <methodname>copy_file_from_host</methodname>
+     <methodname>copy_from_host</methodname>
     </term>
     <listitem>
      <para>
       Copies a file from host to machine, e.g.,
-      <literal>copy_file_from_host("myfile", "/etc/my/important/file")</literal>.
+      <literal>copy_from_host("myfile", "/etc/my/important/file")</literal>.
      </para>
      <para>
       The first argument is the file on the host. The file needs to be
diff --git a/nixos/doc/manual/installation/installing-from-other-distro.xml b/nixos/doc/manual/installation/installing-from-other-distro.xml
index 8ed45899fd7..45d68f8787f 100644
--- a/nixos/doc/manual/installation/installing-from-other-distro.xml
+++ b/nixos/doc/manual/installation/installing-from-other-distro.xml
@@ -89,7 +89,7 @@ nixpkgs https://nixos.org/channels/nixpkgs-unstable</screen>
     NixOS partition. They are installed by default on NixOS, but you don't have
     NixOS yet..
    </para>
-<screen><prompt>$ </prompt>nix-env -iE "_: with import &lt;nixpkgs/nixos&gt; { configuration = {}; }; with config.system.build; [ nixos-generate-config nixos-install nixos-enter manual.manpages ]"</screen>
+<screen><prompt>$ </prompt>nix-env -f '&lt;nixpkgs/nixos&gt;' --arg configuration {} -iA config.system.build.{nixos-generate-config,nixos-install,nixos-enter,manual.manpages}</screen>
   </listitem>
   <listitem>
    <note>
diff --git a/nixos/doc/manual/installation/installing-pxe.xml b/nixos/doc/manual/installation/installing-pxe.xml
index 94199e5e028..ea88fbdad7e 100644
--- a/nixos/doc/manual/installation/installing-pxe.xml
+++ b/nixos/doc/manual/installation/installing-pxe.xml
@@ -16,7 +16,7 @@
  </para>
 
 <programlisting>
-nix-build -A netboot nixos/release.nix
+nix-build -A netboot.x86_64-linux nixos/release.nix
 </programlisting>
 
  <para>
diff --git a/nixos/doc/manual/installation/installing-virtualbox-guest.xml b/nixos/doc/manual/installation/installing-virtualbox-guest.xml
index 0ba909fa953..1cffeed4807 100644
--- a/nixos/doc/manual/installation/installing-virtualbox-guest.xml
+++ b/nixos/doc/manual/installation/installing-virtualbox-guest.xml
@@ -49,7 +49,7 @@
   </listitem>
   <listitem>
    <para>
-    Click on Settings / Display / Screen and select VBoxVGA as Graphics Controller
+    Click on Settings / Display / Screen and select VMSVGA as Graphics Controller
    </para>
   </listitem>
   <listitem>
diff --git a/nixos/doc/manual/installation/installing.xml b/nixos/doc/manual/installation/installing.xml
index 673df8f2e4c..5f216df66f8 100644
--- a/nixos/doc/manual/installation/installing.xml
+++ b/nixos/doc/manual/installation/installing.xml
@@ -42,7 +42,7 @@
   </para>
 
   <para>
-   If the text is too small to be legible, try <command>setfont ter-132n</command>
+   If the text is too small to be legible, try <command>setfont ter-v32n</command>
    to increase the font size.
   </para>
 
@@ -146,7 +146,7 @@
        partition. It uses the initially reserved 512MiB at the start of the
        disk.
 <screen language="commands"><prompt># </prompt>parted /dev/sda -- mkpart ESP fat32 1MiB 512MiB
-<prompt># </prompt>parted /dev/sda -- set 3 boot on</screen>
+<prompt># </prompt>parted /dev/sda -- set 3 esp on</screen>
       </para>
      </listitem>
     </orderedlist>
@@ -513,7 +513,7 @@ Retype new UNIX password: ***</screen>
 <prompt># </prompt>parted /dev/sda -- mkpart primary 512MiB -8GiB
 <prompt># </prompt>parted /dev/sda -- mkpart primary linux-swap -8GiB 100%
 <prompt># </prompt>parted /dev/sda -- mkpart ESP fat32 1MiB 512MiB
-<prompt># </prompt>parted /dev/sda -- set 3 boot on</screen>
+<prompt># </prompt>parted /dev/sda -- set 3 esp on</screen>
   </example>
 
   <example xml:id="ex-install-sequence">
diff --git a/nixos/doc/manual/man-nixos-build-vms.xml b/nixos/doc/manual/man-nixos-build-vms.xml
index d114261f53b..fa7c8c0c6d7 100644
--- a/nixos/doc/manual/man-nixos-build-vms.xml
+++ b/nixos/doc/manual/man-nixos-build-vms.xml
@@ -13,15 +13,15 @@
  </refnamediv>
  <refsynopsisdiv>
   <cmdsynopsis>
-   <command>nixos-build-vms</command> 
+   <command>nixos-build-vms</command>
    <arg>
     <option>--show-trace</option>
    </arg>
-    
+
    <arg>
     <option>--no-out-link</option>
    </arg>
-    
+
    <arg>
     <option>--help</option>
   </arg>
diff --git a/nixos/doc/manual/man-nixos-enter.xml b/nixos/doc/manual/man-nixos-enter.xml
index fe560d3efdd..f533d66099d 100644
--- a/nixos/doc/manual/man-nixos-enter.xml
+++ b/nixos/doc/manual/man-nixos-enter.xml
@@ -13,21 +13,21 @@
  </refnamediv>
  <refsynopsisdiv>
   <cmdsynopsis>
-   <command>nixos-enter</command> 
+   <command>nixos-enter</command>
    <arg>
     <arg choice='plain'>
      <option>--root</option>
     </arg>
      <replaceable>root</replaceable>
    </arg>
-    
+
    <arg>
     <arg choice='plain'>
      <option>--system</option>
     </arg>
      <replaceable>system</replaceable>
    </arg>
-    
+
    <arg>
     <arg choice='plain'>
      <option>-c</option>
@@ -40,13 +40,13 @@
      <option>--silent</option>
     </arg>
    </arg>
-    
+
    <arg>
     <arg choice='plain'>
      <option>--help</option>
     </arg>
    </arg>
-    
+
    <arg>
     <arg choice='plain'>
      <option>--</option>
@@ -136,7 +136,7 @@
    <filename>/mnt</filename>:
   </para>
 <screen>
-# nixos-enter /mnt
+# nixos-enter --root /mnt
 </screen>
   <para>
    Run a shell command:
diff --git a/nixos/doc/manual/man-nixos-rebuild.xml b/nixos/doc/manual/man-nixos-rebuild.xml
index f4f663b84f0..f70f08a0f8a 100644
--- a/nixos/doc/manual/man-nixos-rebuild.xml
+++ b/nixos/doc/manual/man-nixos-rebuild.xml
@@ -315,7 +315,7 @@
        switch</command>), because the hardware and boot loader configuration in
        the VM are different. The boot loader is installed on an automatically
        generated virtual disk containing a <filename>/boot</filename>
-       partition, which is mounted read-only in the VM.
+       partition.
       </para>
      </listitem>
     </varlistentry>
diff --git a/nixos/doc/manual/release-notes/rl-2003.xml b/nixos/doc/manual/release-notes/rl-2003.xml
index 393a9286ca4..0e9ba027a38 100644
--- a/nixos/doc/manual/release-notes/rl-2003.xml
+++ b/nixos/doc/manual/release-notes/rl-2003.xml
@@ -792,7 +792,7 @@ users.users.me =
      The <option>services.xserver.displayManager.auto</option> module has been removed.
      It was only intended for use in internal NixOS tests, and gave the false impression
      of it being a special display manager when it's actually LightDM.
-     Please use the <xref linkend="opt-services.xserver.displayManager.lightdm.autoLogin"/> options instead,
+     Please use the <option>services.xserver.displayManager.lightdm.autoLogin</option> options instead,
      or any other display manager in NixOS as they all support auto-login. If you used this module specifically
      because it permitted root auto-login you can override the lightdm-autologin pam module like:
 <programlisting>
diff --git a/nixos/doc/manual/release-notes/rl-2009.xml b/nixos/doc/manual/release-notes/rl-2009.xml
index ede7bed609d..166aec25512 100644
--- a/nixos/doc/manual/release-notes/rl-2009.xml
+++ b/nixos/doc/manual/release-notes/rl-2009.xml
@@ -28,6 +28,12 @@
    </listitem>
    <listitem>
     <para>
+    <package>maxx</package> package removed along with <varname>services.xserver.desktopManager.maxx</varname> module.
+    Please migrate to <package>cdesktopenv</package> and <varname>services.xserver.desktopManager.cde</varname> module.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
      We now distribute a GNOME ISO.
     </para>
    </listitem>
@@ -38,6 +44,16 @@
    </listitem>
    <listitem>
     <para>
+     PHP 7.2 is no longer supported due to upstream not supporting this version for the entire lifecycle of the 20.09 release.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     Python 3 now defaults to Python 3.8 instead of 3.7.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
      Two new options, <link linkend="opt-services.openssh.authorizedKeysCommand">authorizedKeysCommand</link>
      and <link linkend="opt-services.openssh.authorizedKeysCommandUser">authorizedKeysCommandUser</link>, have
      been added to the <literal>openssh</literal> module. If you have <literal>AuthorizedKeysCommand</literal>
@@ -61,6 +77,133 @@
       This is to make it possible to use <literal>podman</literal> instead of <literal>docker</literal>.
     </para>
    </listitem>
+   <listitem>
+    <para>
+      MariaDB has been updated to 10.4, MariaDB Galera to 26.4.
+      Before you upgrade, it would be best to take a backup of your database.
+      For MariaDB Galera Cluster, see <link xlink:href="https://mariadb.com/kb/en/upgrading-from-mariadb-103-to-mariadb-104-with-galera-cluster/">Upgrading
+      from MariaDB 10.3 to MariaDB 10.4 with Galera Cluster</link> instead.
+      Before doing the upgrade read <link xlink:href="https://mariadb.com/kb/en/upgrading-from-mariadb-103-to-mariadb-104/#incompatible-changes-between-103-and-104">Incompatible
+      Changes Between 10.3 and 10.4</link>.
+      After the upgrade you will need to run <literal>mysql_upgrade</literal>.
+      MariaDB 10.4 introduces a number of changes to the authentication process, intended to make things easier and more
+      intuitive. See <link xlink:href="https://mariadb.com/kb/en/authentication-from-mariadb-104/">Authentication from MariaDB 10.4</link>.
+      unix_socket auth plugin does not use a password, and uses the connecting user's UID instead. When a new MariaDB data directory is initialized, two MariaDB users are
+      created and can be used with new unix_socket auth plugin, as well as traditional mysql_native_password plugin: root@localhost and mysql@localhost. To actually use
+      the traditional mysql_native_password plugin method, one must run the following:
+<programlisting>
+services.mysql.initialScript = pkgs.writeText "mariadb-init.sql" ''
+  ALTER USER root@localhost IDENTIFIED VIA mysql_native_password USING PASSWORD("verysecret");
+'';
+</programlisting>
+      When MariaDB data directory is just upgraded (not initialized), the users are not created or modified.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+      MySQL server is now started with additional systemd sandbox/hardening options for better security. The PrivateTmp, ProtectHome, and ProtectSystem options
+      may be problematic when MySQL is attempting to read from or write to your filesystem anywhere outside of its own state directory, for example when
+      calling <literal>LOAD DATA INFILE or SELECT * INTO OUTFILE</literal>. In this scenario a variant of the following may be required:
+        - allow MySQL to read from /home and /tmp directories when using <literal>LOAD DATA INFILE</literal>
+<programlisting>
+systemd.services.mysql.serviceConfig.ProtectHome = lib.mkForce "read-only";
+</programlisting>
+        - allow MySQL to write to custom folder <literal>/var/data</literal> when using <literal>SELECT * INTO OUTFILE</literal>, assuming the mysql user has write
+          access to <literal>/var/data</literal>
+<programlisting>
+systemd.services.mysql.serviceConfig.ReadWritePaths = [ "/var/data" ];
+</programlisting>
+    </para>
+    <para>
+      The MySQL service no longer runs its <literal>systemd</literal> service startup script as <literal>root</literal> anymore. A dedicated non <literal>root</literal>
+      super user account is required for operation. This means users with an existing MySQL or MariaDB database server are required to run the following SQL statements
+      as a super admin user before upgrading:
+<programlisting>
+CREATE USER IF NOT EXISTS 'mysql'@'localhost' identified with unix_socket;
+GRANT ALL PRIVILEGES ON *.* TO 'mysql'@'localhost' WITH GRANT OPTION;
+</programlisting>
+      If you use MySQL instead of MariaDB please replace <literal>unix_socket</literal> with <literal>auth_socket</literal>. If you have changed the value of <xref linkend="opt-services.mysql.user"/>
+      from the default of <literal>mysql</literal> to a different user please change <literal>'mysql'@'localhost'</literal> to the corresponding user instead.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+      The new option <link linkend="opt-documentation.man.generateCaches">documentation.man.generateCaches</link>
+      has been added to automatically generate the <literal>man-db</literal> caches, which are needed by utilities
+      like <command>whatis</command> and <command>apropos</command>. The caches are generated during the build of
+      the NixOS configuration: since this can be expensive when a large number of packages are installed, the
+      feature is disabled by default.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     <varname>services.postfix.sslCACert</varname> was replaced by <varname>services.postfix.tlsTrustedAuthorities</varname> which now defaults to system certificate authorities.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+      Subordinate GID and UID mappings are now set up automatically for all normal users.
+      This will make container tools like Podman work as non-root users out of the box.
+    </para>
+   </listitem>
+   <listitem>
+     <para>
+       The various documented workarounds to use steam have been converted to a module. <varname>programs.steam.enable</varname> enables steam, controller support and the workarounds.
+     </para>
+   </listitem>
+   <listitem>
+     <para>
+       Support for built-in LCDs in various pieces of Logitech hardware (keyboards and USB speakers). <varname>hardware.logitech.lcd.enable</varname> enables support for all hardware supported by the g15daemon project.
+     </para>
+   </listitem>
+   <listitem>
+    <para>
+      Zabbix now defaults to 5.0, updated from 4.4. Please carefully read through
+      <link xlink:href="https://www.zabbix.com/documentation/current/manual/installation/upgrade/sources">the upgrade guide</link>
+      and apply any changes required. Be sure to take special note of the section on
+      <link xlink:href="https://www.zabbix.com/documentation/current/manual/installation/upgrade_notes_500#enabling_extended_range_of_numeric_float_values">enabling extended range of numeric (float) values</link>
+      as you will need to apply this database migration manually.
+    </para>
+    <para>
+      If you are using Zabbix Server with a MySQL or MariaDB database you should note that using a character set of <literal>utf8</literal> and a collate of <literal>utf8_bin</literal> has become mandatory with
+      this release. See the upstream <link xlink:href="https://support.zabbix.com/browse/ZBX-17357">issue</link> for further discussion. Before upgrading you should check the character set and collation used by
+      your database and ensure they are correct:
+<programlisting>
+  SELECT
+    default_character_set_name,
+    default_collation_name
+  FROM
+    information_schema.schemata
+  WHERE
+    schema_name = 'zabbix';
+</programlisting>
+      If these values are not correct you should take a backup of your database and convert the character set and collation as required. Here is an
+      <link xlink:href="https://www.zabbix.com/forum/zabbix-help/396573-reinstall-after-upgrade?p=396891#post396891">example</link> of how to do so, taken from
+      the Zabbix forums:
+<programlisting>
+  ALTER DATABASE `zabbix` DEFAULT CHARACTER SET utf8 COLLATE utf8_bin;
+
+  -- the following will produce a list of SQL commands you should subsequently execute
+  SELECT CONCAT("ALTER TABLE ", TABLE_NAME," CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin;") AS ExecuteTheString
+  FROM information_schema.`COLUMNS`
+  WHERE table_schema = "zabbix" AND COLLATION_NAME = "utf8_general_ci";
+</programlisting>
+    </para>
+   </listitem>
+   <listitem>
+     <para>
+      The NixOS module system now supports freeform modules as a mix between <literal>types.attrsOf</literal> and <literal>types.submodule</literal>. These allow you to explicitly declare a subset of options while still permitting definitions without an associated option. See <xref linkend='sec-freeform-modules'/> for how to use them.
+     </para>
+   </listitem>
+   <listitem>
+     <para>
+       The GRUB module gained support for basic password protection, which
+       allows to restrict non-default entries in the boot menu to one or more
+       users. The users and passwords are defined via the option
+       <option>boot.loader.grub.users</option>.
+       Note: Password support is only avaiable in GRUB version 2.
+     </para>
+   </listitem>
   </itemizedlist>
  </section>
 
@@ -100,11 +243,23 @@
   <itemizedlist>
    <listitem>
     <para>
+     <literal>buildGoModule</literal> now internally creates a vendor directory
+     in the source tree for downloaded modules instead of using go's <link
+     xlink:href="https://golang.org/cmd/go/#hdr-Module_proxy_protocol">module
+     proxy protocol</link>. This storage format is simpler and therefore less
+     likely to break with future versions of go. As a result
+     <literal>buildGoModule</literal> switched from
+     <literal>modSha256</literal> to the <literal>vendorSha256</literal>
+     attribute to pin fetched version data.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
      Grafana is now built without support for phantomjs by default. Phantomjs support has been
      <link xlink:href="https://grafana.com/docs/grafana/latest/guides/whats-new-in-v6-4/">deprecated in Grafana</link>
      and the <package>phantomjs</package> project is
      <link xlink:href="https://github.com/ariya/phantomjs/issues/15344#issue-302015362">currently unmaintained</link>.
-     It can still be enabled by providing <literal>phantomJsSupport = true</literal> to the package instanciation:
+     It can still be enabled by providing <literal>phantomJsSupport = true</literal> to the package instantiation:
 <programlisting>{
   services.grafana.package = pkgs.grafana.overrideAttrs (oldAttrs: rec {
     phantomJsSupport = false;
@@ -116,7 +271,7 @@
     <para>
       The <link linkend="opt-services.supybot.enable">supybot</link> module now uses <literal>/var/lib/supybot</literal>
       as its default <link linkend="opt-services.supybot.stateDir">stateDir</link> path if <literal>stateVersion</literal>
-      is 20.09 or higher. It also enables number of
+      is 20.09 or higher. It also enables a number of
       <link xlink:href="https://www.freedesktop.org/software/systemd/man/systemd.exec.html#Sandboxing">systemd sandboxing options</link>
       which may possibly interfere with some plugins. If this is the case you can disable the options through attributes in
       <option>systemd.services.supybot.serviceConfig</option>.
@@ -136,6 +291,12 @@
    </listitem>
    <listitem>
     <para>
+      <literal>vmware</literal> has been removed from the <literal>services.x11.videoDrivers</literal> defaults.
+      For VMWare guests set <literal>virtualisation.vmware.guest.enable</literal> to <literal>true</literal> which will include the appropriate drivers.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
      The initrd SSH support now uses OpenSSH rather than Dropbear to
      allow the use of Ed25519 keys and other OpenSSH-specific
      functionality. Host keys must now be in the OpenSSH format, and at
@@ -189,13 +350,13 @@ environment.systemPackages = [
              customizable to your liking by using
              <literal>php.withExtensions</literal> or
              <literal>php.buildEnv</literal> instead of writing config files
-             or changing configure flags.             
+             or changing configure flags.
            </para>
          </listitem>
          <listitem>
            <para>
              The remaining configuration flags can now be set directly on
-             the <literal>php</literal> attribute. For example, instead of 
+             the <literal>php</literal> attribute. For example, instead of
 
              <programlisting>
 php.override {
@@ -235,7 +396,16 @@ php.override {
        Be aware that backwards state migrations are not supported by Deluge.
      </para>
    </listitem>
-
+   <listitem>
+     <para>
+       Add option <literal>services.nginx.enableSandbox</literal> to starting Nginx web server with additional sandbox/hardening options.
+       By default, write access to <literal>services.nginx.stateDir</literal> is allowed. To allow writing to other folders,
+       use <literal>systemd.services.nginx.serviceConfig.ReadWritePaths</literal>
+       <programlisting>
+systemd.services.nginx.serviceConfig.ReadWritePaths = [ "/var/www" ];
+       </programlisting>
+     </para>
+   </listitem>
    <listitem>
     <para>
       The NixOS options <literal>nesting.clone</literal> and
@@ -344,6 +514,244 @@ php.override {
       will have changed.
     </para>
    </listitem>
+   <listitem>
+    <para>
+      The rkt module has been removed, it was archived by upstream.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+      The <link xlink:href="https://bazaar.canonical.com">Bazaar</link> VCS is
+      unmaintained and, as consequence of the Python 2 EOL, the packages
+      <literal>bazaar</literal> and <literal>bazaarTools</literal> were
+      removed. Breezy, the backward compatible fork of Bazaar (see the
+      <link xlink:href="https://www.jelmer.uk/breezy-intro.html">announcement</link>),
+      was packaged as <literal>breezy</literal> and can be used instead.
+    </para>
+    <para>
+      Regarding Nixpkgs, <literal>fetchbzr</literal>,
+      <literal>nix-prefetch-bzr</literal> and Bazaar support in Hydra will
+      continue to work through Breezy.
+    </para>
+   </listitem>
+   <listitem>
+     <para>
+       In addition to the hostname, the fully qualified domain name (FQDN),
+       which consists of <literal>${cfg.hostName}</literal> and
+       <literal>${cfg.domain}</literal> is now added to
+       <literal>/etc/hosts</literal>, to allow local FQDN resolution, as used by the
+       <literal>hostname --fqdn</literal> command and other applications that
+       try to determine the FQDN. These new entries take precedence over entries
+       from the DNS which could cause regressions in some very specific setups.
+       Additionally the hostname is now resolved to <literal>127.0.0.2</literal>
+       instead of <literal>127.0.1.1</literal> to be consistent with what
+       <literal>nss-myhostname</literal> (from systemd) returns.
+       The old behaviour can e.g. be restored by using
+       <literal>networking.hosts = lib.mkForce { "127.0.1.1" = [ config.networking.hostName ]; };</literal>.
+     </para>
+   </listitem>
+   <listitem>
+     <para>
+       The hostname (<literal>networking.hostName</literal>) must now be a valid
+       DNS label (see RFC 1035) and as such must not contain the domain part.
+       This means that the hostname must start with a letter, end with a letter
+       or digit, and have as interior characters only letters, digits, and
+       hyphen. The maximum length is 63 characters. Additionally it is
+       recommended to only use lower-case characters.
+     </para>
+   </listitem>
+   <listitem>
+    <para>
+     The GRUB specific option <option>boot.loader.grub.extraInitrd</option>
+     has been replaced with the generic option
+     <option>boot.initrd.secrets</option>. This option creates a secondary
+     initrd from the specified files, rather than using a manually created
+     initrd file.
+
+     Due to an existing bug with <option>boot.loader.grub.extraInitrd</option>,
+     it is not possible to directly boot an older generation that used that
+     option. It is still possible to rollback to that generation if the required
+     initrd file has not been deleted.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The <link xlink:href="https://github.com/okTurtles/dnschain">DNSChain</link>
+     package and NixOS module have been removed from Nixpkgs as the software is
+     unmaintained and can't be built. For more information see issue
+     <link xlink:href="https://github.com/NixOS/nixpkgs/issues/89205">#89205</link>.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     In the <literal>resilio</literal> module, <xref linkend="opt-services.resilio.httpListenAddr"/> has been changed to listen to <literal>[::1]</literal> instead of <literal>0.0.0.0</literal>.
+     </para>
+   </listitem>
+   <listitem>
+    <para>
+     Users of <link xlink:href="http://openafs.org">OpenAFS 1.6</link> must
+     upgrade their services to OpenAFS 1.8! In this release, the OpenAFS package
+     version 1.6.24 is marked broken but can be used during transition to
+     OpenAFS 1.8.x. Use the options
+     <option>services.openafsClient.packages.module</option>,
+     <option>services.openafsClient.packages.programs</option> and
+     <option>services.openafsServer.package</option> to select a different
+     OpenAFS package. OpenAFS 1.6 will be removed in the next release. The
+     package <literal>openafs</literal> and the service options will then
+     silently point to the OpenAFS 1.8 release.
+    </para>
+    <para>
+     See also the OpenAFS <link
+     xlink:href="http://docs.openafs.org/AdminGuide/index.html">Administrator
+     Guide</link> for instructions. Beware of the following when updating
+     servers:
+     <itemizedlist>
+      <listitem>
+       <para>
+       The storage format of the server key has changed and the key must be converted before running the new release.
+       </para>
+      </listitem>
+      <listitem>
+       <para>
+       When updating multiple database servers, turn off the database servers
+       from the highest IP down to the lowest with resting periods in
+       between. Start up in reverse order. Do not concurrently run database
+       servers working with different OpenAFS releases!
+       </para>
+      </listitem>
+      <listitem>
+       <para>
+       Update servers first, then clients.
+       </para>
+      </listitem>
+     </itemizedlist>
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     Radicale's default package has changed from 2.x to 3.x. An upgrade
+     checklist can be found
+     <link xlink:href="https://github.com/Kozea/Radicale/blob/3.0.x/NEWS.md#upgrade-checklist">here</link>.
+     You can use the newer version in the NixOS service by setting the
+     <literal>package</literal> to <literal>radicale3</literal>, which is done
+     automatically if <literal>stateVersion</literal> is 20.09 or higher.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+      <literal>udpt</literal> experienced a complete rewrite from C++ to rust. The configuration format changed from ini to toml.
+      The new configuration documentation can be found at
+      <link xlink:href="https://naim94a.github.io/udpt/config.html">the official website</link> and example
+      configuration is packaged in <literal>${udpt}/share/udpt/udpt.toml</literal>.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     We now have a unified <xref linkend="opt-services.xserver.displayManager.autoLogin"/> option interface
+     to be used for every display-manager in NixOS.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The <literal>bitcoind</literal> module has changed to multi-instance, using submodules.
+     Therefore, it is now mandatory to name each instance.
+     To use this new multi-instance config with an existing bitcoind data directory and user,
+     you have to adjust the original config, e.g.:
+<programlisting>
+  services.bitcoind = {
+    enable = true;
+    extraConfig = "...";
+    ...
+  };
+</programlisting>
+     To something similar:
+<programlisting>
+  services.bitcoind.mainnet = {
+    enable = true;
+    dataDir = "/var/lib/bitcoind";
+    user = "bitcoin";
+    extraConfig = "...";
+    ...
+  };
+</programlisting>
+     The key settings are:
+     <itemizedlist>
+      <listitem>
+       <para>
+        <literal>dataDir</literal> - to continue using the same data directory.
+       </para>
+      </listitem>
+      <listitem>
+       <para>
+        <literal>user</literal> - to continue using the same user so that bitcoind maintains access to its files.
+       </para>
+      </listitem>
+     </itemizedlist>
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+      Graylog introduced a change in the LDAP server certificate validation behaviour for version 3.3.3 which might break existing setups.
+      When updating Graylog from a version before 3.3.3 make sure to check the Graylog <link xlink:href="https://www.graylog.org/post/announcing-graylog-v3-3-3">release info</link> for information on how to avoid the issue.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The <literal>dokuwiki</literal> module has changed to multi-instance, using submodules.
+     Therefore, it is now mandatory to name each instance. Moreover, forcing SSL by default has been dropped, so
+     <literal>nginx.forceSSL</literal> and <literal>nginx.enableACME</literal> are no longer set to <literal>true</literal>.
+     To continue using your service with the original SSL settings, you have to adjust the original config, e.g.:
+<programlisting>
+services.dokuwiki = {
+  enable = true;
+  ...
+};
+</programlisting>
+     To something similar:
+<programlisting>
+services.dokuwiki."mywiki" = {
+  enable = true;
+  nginx = {
+    forceSSL = true;
+    enableACME = true;
+  };
+  ...
+};
+</programlisting>
+     The base package has also been upgraded to the 2020-07-29 "Hogfather" release. Plugins might be incompatible or require upgrading.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+      The <xref linkend="opt-services.postgresql.dataDir"/> option is now set to <literal>"/var/lib/postgresql/${cfg.package.psqlSchema}"</literal> regardless of your
+      <xref linkend="opt-system.stateVersion"/>. Users with an existing postgresql install that have a <xref linkend="opt-system.stateVersion"/> of <literal>17.03</literal> or below
+      should double check what the value of their <xref linkend="opt-services.postgresql.dataDir"/> option is (<literal>/var/db/postgresql</literal>) and then explicitly
+      set this value to maintain compatibility:
+<programlisting>
+services.postgresql.dataDir = "/var/db/postgresql";
+</programlisting>
+    </para>
+    <para>
+     The postgresql module now expects there to be a database super user account called <literal>postgres</literal> regardless of your <xref linkend="opt-system.stateVersion"/>. Users
+     with an existing postgresql install that have a <xref linkend="opt-system.stateVersion"/> of <literal>17.03</literal> or below should run the following SQL statements as a
+     database super admin user before upgrading:
+<programlisting>
+CREATE ROLE postgres LOGIN SUPERUSER;
+</programlisting>
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The USBGuard module now removes options and instead hardcodes values for <literal>IPCAccessControlFiles</literal>, <literal>ruleFiles</literal>, and <literal>auditFilePath</literal>. Audit logs can be found in the journal.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The NixOS module system now evaluates option definitions more strictly, allowing it to detect a larger set of problems.
+     As a result, what previously evaluated may not do so anymore.
+     See <link xlink:href="https://github.com/NixOS/nixpkgs/pull/82743#issuecomment-674520472">the PR that changed this</link> for more info.
+    </para>
+   </listitem>
   </itemizedlist>
  </section>
 
@@ -356,6 +764,9 @@ php.override {
 
   <itemizedlist>
    <listitem>
+    <para>SD images are now compressed by default using <literal>zstd</literal>. The compression for ISO images has also been changed to <literal>zstd</literal>, but ISO images are still not compressed by default.</para>
+   </listitem>
+   <listitem>
     <para>
      <option>services.journald.rateLimitBurst</option> was updated from
      <literal>1000</literal> to <literal>10000</literal> to follow the new
@@ -372,10 +783,205 @@ php.override {
     </para>
    </listitem>
    <listitem>
+    <para>
+      The default output of <literal>buildGoPackage</literal> is now <literal>$out</literal> instead of <literal>$bin</literal>.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     <literal>buildGoModule</literal> <literal>doCheck</literal> now defaults to <literal>true</literal>.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     Packages built using <literal>buildRustPackage</literal> now use <literal>release</literal>
+     mode for the <literal>checkPhase</literal> by default.
+    </para>
+    <para>
+     Please note that Rust packages utilizing a custom build/install procedure
+     (e.g. by using a <filename>Makefile</filename>) or test suites that rely on the
+     structure of the <filename>target/</filename> directory may break due to those assumptions.
+     For further information, please read the Rust section in the Nixpkgs manual.
+    </para>
+   </listitem>
+   <listitem>
    <para>
-     The default output of <literal>buildGoPackage</literal> is now <literal>$out</literal> instead of <literal>$bin</literal>.
+     The cc- and binutils-wrapper's "infix salt" and <literal>_BUILD_</literal> and <literal>_TARGET_</literal> user infixes have been replaced with with a "suffix salt" and suffixes and <literal>_FOR_BUILD</literal> and <literal>_FOR_TARGET</literal>.
+      This matches the autotools convention for env vars which standard for these things, making interfacing with other tools easier.
    </para>
    </listitem>
+   <listitem>
+   <para>
+     Additional Git documentation (HTML and text files) is now available via the <literal>git-doc</literal> package.
+   </para>
+   </listitem>
+   <listitem>
+   <para>
+     Default algorithm for ZRAM swap was changed to <literal>zstd</literal>.
+   </para>
+   </listitem>
+   <listitem>
+    <para>
+     The scripted networking system now uses <literal>.link</literal> files in
+     <literal>/etc/systemd/network</literal> to configure mac address and link MTU,
+     instead of the sometimes buggy <literal>network-link-*</literal> units, which
+     have been removed.
+     Bringing the interface up has been moved to the beginning of the
+     <literal>network-addresses-*</literal> unit.
+     Note this doesn't require <command>systemd-networkd</command> - it's udev that
+     parses <literal>.link</literal> files.
+     Extra care needs to be taken in the presence of <link xlink:href="https://wiki.debian.org/NetworkInterfaceNames#THE_.22PERSISTENT_NAMES.22_SCHEME">legacy udev rules</link>
+     to rename interfaces, as MAC Address and MTU defined in these options can only match on the original link name.
+     In such cases, you most likely want to create a <literal>10-*.link</literal> file through <xref linkend="opt-systemd.network.links"/> and set both name and MAC Address / MTU there.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     Grafana received a major update to version 7.x. A plugin is now needed for
+     image rendering support, and plugins must now be signed by default. More
+     information can be found
+     <link xlink:href="https://grafana.com/docs/grafana/latest/installation/upgrading/#upgrading-to-v7-0">in the Grafana documentation</link>.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The <literal>hardware.u2f</literal> module, which was installing udev rules
+     was removed, as udev gained native support to handle FIDO security tokens.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The <literal>services.transmission</literal> module
+     was enhanced with the new options:
+     <xref linkend="opt-services.transmission.credentialsFile"/>,
+     <xref linkend="opt-services.transmission.openFirewall"/>,
+     and <xref linkend="opt-services.transmission.performanceNetParameters"/>.
+    </para>
+    <para>
+     <literal>transmission-daemon</literal> is now started with additional systemd sandbox/hardening options for better security.
+     Please <link xlink:href="https://github.com/NixOS/nixpkgs/issues">report</link>
+     any use case where this is not working well.
+     In particular, the <literal>RootDirectory</literal> option newly set
+     forbids uploading or downloading a torrent outside of the default directory
+     configured at <link linkend="opt-services.transmission.settings">settings.download-dir</link>.
+     If you really need Transmission to access other directories,
+     you must include those directories into the <literal>BindPaths</literal> of the service:
+<programlisting>
+systemd.services.transmission.serviceConfig.BindPaths = [ "/path/to/alternative/download-dir" ];
+</programlisting>
+    </para>
+    <para>
+     Also, connection to the RPC (Remote Procedure Call) of <literal>transmission-daemon</literal>
+     is now only available on the local network interface by default.
+     Use:
+<programlisting>
+services.transmission.settings.rpc-bind-address = "0.0.0.0";
+</programlisting>
+     to get the previous behavior of listening on all network interfaces.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     With this release <literal>systemd-networkd</literal> (when enabled through <xref linkend="opt-networking.useNetworkd"/>)
+     has it's netlink socket created through a <literal>systemd.socket</literal> unit. This gives us control over
+     socket buffer sizes and other parameters. For larger setups where networkd has to create a lot of (virtual)
+     devices the default buffer size (currently 128MB) is not enough.
+    </para>
+    <para>
+     On a machine with &gt;100 virtual interfaces (e.g., wireguard tunnels, VLANs, …), that all have to
+     be brought up during system startup, the receive buffer size will spike for a brief period.
+     Eventually some of the message will be dropped since there is not enough (permitted) buffer
+     space available.
+    </para>
+    <para>
+     By having <literal>systemd-networkd</literal> start with a netlink socket created by
+     <literal>systemd</literal> we can configure the <literal>ReceiveBufferSize=</literal> parameter
+     in the socket options (i.e. <literal>systemd.sockets.systemd-networkd.socketOptions.ReceiveBufferSize</literal>)
+     without recompiling <literal>systemd-networkd</literal>.
+    </para>
+    <para>
+     Since the actual memory requirements depend on hardware, timing, exact
+     configurations etc. it isn't currently possible to infer a good default
+     from within the NixOS module system. Administrators are advised to
+     monitor the logs of <literal>systemd-networkd</literal> for <literal>rtnl: kernel receive buffer
+     overrun</literal> spam and increase the memory limit as they see fit.
+    </para>
+    <para>
+     Note: Increasing the <literal>ReceiveBufferSize=</literal> doesn't allocate any memory. It just increases
+     the upper bound on the kernel side. The memory allocation depends on the amount of messages that are
+     queued on the kernel side of the netlink socket.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     Specifying <link linkend="opt-services.dovecot2.mailboxes">mailboxes</link> in the <package>dovecot2</package> module
+     as a list is deprecated and will break eval in 21.03. Instead, an attribute-set should be specified where the <literal>name</literal>
+     should be the key of the attribute.
+    </para>
+    <para>
+     This means that a configuration like this
+<programlisting>{
+  <link linkend="opt-services.dovecot2.mailboxes">services.dovecot2.mailboxes</link> = [
+    { name = "Junk";
+      auto = "create";
+    }
+  ];
+}</programlisting>
+    should now look like this:
+<programlisting>{
+  <link linkend="opt-services.dovecot2.mailboxes">services.dovecot2.mailboxes</link> = {
+    Junk.auto = "create";
+  };
+}</programlisting>
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+      <package>netbeans</package> was upgraded to 12.0 and now defaults to OpenJDK 11. This might cause problems if your projects depend on packages that were removed in Java 11.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     <package>nextcloud</package> has been updated to <link xlink:href="https://nextcloud.com/blog/nextcloud-hub-brings-productivity-to-home-office/">v19</link>.
+    </para>
+    <para>
+     If you have an existing installation, please make sure that you're on
+     <package>nextcloud18</package> before upgrading to <package>nextcloud19</package>
+     since Nextcloud doesn't support upgrades across multiple major versions.
+    </para>
+     <para>
+       The <literal>nixos-run-vms</literal> script now deletes the
+       previous run machines states on test startup. You can use the
+       <literal>--keep-vm-state</literal> flag to match the previous
+       behaviour and keep the same VM state between different test runs.
+     </para>
+   </listitem>
+   <listitem>
+    <para>
+     The <link linkend="opt-nix.buildMachines">nix.buildMachines</link> option is now type-checked.
+     There are no functional changes, however this may require updating some configurations to use correct types for all attributes.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The <literal>fontconfig</literal> module stopped generating fontconfig 2.10.x config and cache.
+     Fontconfig 2.10.x was removed from Nixpkgs - it hasn't been used in any nixpkgs package anymore.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     Nginx module <literal>nginxModules.fastcgi-cache-purge</literal> renamed to official name <literal>nginxModules.cache-purge</literal>.
+     Nginx module <literal>nginxModules.ngx_aws_auth</literal> renamed to official name <literal>nginxModules.aws-auth</literal>.
+      The packages <package>perl</package>, <package>rsync</package> and <package>strace</package> were removed from <option>systemPackages</option>. If you need them, install them again with <code><xref linkend="opt-environment.systemPackages"/> = with pkgs; [ perl rsync strace ];</code> in your <filename>configuration.nix</filename>.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+      The <literal>undervolt</literal> option no longer needs to apply its
+      settings every 30s. If they still become undone, open an issue and restore
+      the previous behaviour using <literal>undervolt.useTimer</literal>.
+    </para>
+   </listitem>
   </itemizedlist>
  </section>
 </section>
diff --git a/nixos/lib/eval-config.nix b/nixos/lib/eval-config.nix
index c8824c2690d..15429a7160c 100644
--- a/nixos/lib/eval-config.nix
+++ b/nixos/lib/eval-config.nix
@@ -24,11 +24,11 @@
   check ? true
 , prefix ? []
 , lib ? import ../../lib
+, extraModules ? let e = builtins.getEnv "NIXOS_EXTRA_MODULE_PATH";
+                 in if e == "" then [] else [(import e)]
 }:
 
 let extraArgs_ = extraArgs; pkgs_ = pkgs;
-    extraModules = let e = builtins.getEnv "NIXOS_EXTRA_MODULE_PATH";
-                   in if e == "" then [] else [(import e)];
 in
 
 let
diff --git a/nixos/lib/make-disk-image.nix b/nixos/lib/make-disk-image.nix
index 5e86ea479d5..8aa606a56af 100644
--- a/nixos/lib/make-disk-image.nix
+++ b/nixos/lib/make-disk-image.nix
@@ -5,21 +5,32 @@
   config
 
 , # The size of the disk, in megabytes.
-  diskSize
+  # if "auto" size is calculated based on the contents copied to it and
+  #   additionalSpace is taken into account.
+  diskSize ? "auto"
 
-  # The files and directories to be placed in the target file system.
+, # additional disk space to be added to the image if diskSize "auto"
+  # is used
+  additionalSpace ? "512M"
+
+, # size of the boot partition, is only used if partitionTableType is
+  # either "efi" or "hybrid"
+  bootSize ? "256M"
+
+, # The files and directories to be placed in the target file system.
   # This is a list of attribute sets {source, target} where `source'
   # is the file system object (regular file or directory) to be
   # grafted in the file system at path `target'.
-, contents ? []
+  contents ? []
 
 , # Type of partition table to use; either "legacy", "efi", or "none".
   # For "efi" images, the GPT partition table is used and a mandatory ESP
   #   partition of reasonable size is created in addition to the root partition.
-  #   If `installBootLoader` is true, GRUB will be installed in EFI mode.
   # For "legacy", the msdos partition table is used and a single large root
-  #   partition is created. If `installBootLoader` is true, GRUB will be
-  #   installed in legacy mode.
+  #   partition is created.
+  # For "hybrid", the GPT partition table is used and a mandatory ESP
+  #   partition of reasonable size is created in addition to the root partition.
+  #   Also a legacy MBR will be present.
   # For "none", no partition table is created. Enabling `installBootLoader`
   #   most likely fails as GRUB will probably refuse to install.
   partitionTableType ? "legacy"
@@ -39,11 +50,11 @@
 
 , name ? "nixos-disk-image"
 
-, # Disk image format, one of qcow2, qcow2-compressed, vpc, raw.
+, # Disk image format, one of qcow2, qcow2-compressed, vdi, vpc, raw.
   format ? "raw"
 }:
 
-assert partitionTableType == "legacy" || partitionTableType == "efi" || partitionTableType == "none";
+assert partitionTableType == "legacy" || partitionTableType == "efi" || partitionTableType == "hybrid" || partitionTableType == "none";
 # We use -E offset=X below, which is only supported by e2fsprogs
 assert partitionTableType != "none" -> fsType == "ext4";
 
@@ -57,13 +68,15 @@ let format' = format; in let
 
   filename = "nixos." + {
     qcow2 = "qcow2";
+    vdi   = "vdi";
     vpc   = "vhd";
     raw   = "img";
-  }.${format};
+  }.${format} or format;
 
   rootPartition = { # switch-case
     legacy = "1";
     efi = "2";
+    hybrid = "3";
   }.${partitionTableType};
 
   partitionDiskScript = { # switch-case
@@ -75,9 +88,18 @@ let format' = format; in let
     efi = ''
       parted --script $diskImage -- \
         mklabel gpt \
-        mkpart ESP fat32 8MiB 256MiB \
+        mkpart ESP fat32 8MiB ${bootSize} \
         set 1 boot on \
-        mkpart primary ext4 256MiB -1
+        mkpart primary ext4 ${bootSize} -1
+    '';
+    hybrid = ''
+      parted --script $diskImage -- \
+        mklabel gpt \
+        mkpart ESP fat32 8MiB ${bootSize} \
+        set 1 boot on \
+        mkpart no-fs 0 1024KiB \
+        set 2 bios_grub on \
+        mkpart primary ext4 ${bootSize} -1
     '';
     none = "";
   }.${partitionTableType};
@@ -128,19 +150,6 @@ let format' = format; in let
     }
 
     mkdir $out
-    diskImage=nixos.raw
-    truncate -s ${toString diskSize}M $diskImage
-
-    ${partitionDiskScript}
-
-    ${if partitionTableType != "none" then ''
-      # Get start & length of the root partition in sectors to $START and $SECTORS.
-      eval $(partx $diskImage -o START,SECTORS --nr ${rootPartition} --pairs)
-
-      mkfs.${fsType} -F -L ${label} $diskImage -E offset=$(sectorsToBytes $START) $(sectorsToKilobytes $SECTORS)K
-    '' else ''
-      mkfs.${fsType} -F -L ${label} $diskImage
-    ''}
 
     root="$PWD/root"
     mkdir -p $root
@@ -180,10 +189,36 @@ let format' = format; in let
     export NIX_STATE_DIR=$TMPDIR/state
     nix-store --load-db < ${closureInfo}/registration
 
+    chmod 755 "$TMPDIR"
     echo "running nixos-install..."
     nixos-install --root $root --no-bootloader --no-root-passwd \
       --system ${config.system.build.toplevel} --channel ${channelSources} --substituters ""
 
+    diskImage=nixos.raw
+
+    ${if diskSize == "auto" then ''
+      ${if partitionTableType == "efi" || partitionTableType == "hybrid" then ''
+        additionalSpace=$(( ($(numfmt --from=iec '${additionalSpace}') + $(numfmt --from=iec '${bootSize}')) / 1000 ))
+      '' else ''
+        additionalSpace=$(( $(numfmt --from=iec '${additionalSpace}') / 1000 ))
+      ''}
+      diskSize=$(( $(set -- $(du -d0 $root); echo "$1") + $additionalSpace ))
+      truncate -s "$diskSize"K $diskImage
+    '' else ''
+      truncate -s ${toString diskSize}M $diskImage
+    ''}
+
+    ${partitionDiskScript}
+
+    ${if partitionTableType != "none" then ''
+      # Get start & length of the root partition in sectors to $START and $SECTORS.
+      eval $(partx $diskImage -o START,SECTORS --nr ${rootPartition} --pairs)
+
+      mkfs.${fsType} -F -L ${label} $diskImage -E offset=$(sectorsToBytes $START) $(sectorsToKilobytes $SECTORS)K
+    '' else ''
+      mkfs.${fsType} -F -L ${label} $diskImage
+    ''}
+
     echo "copying staging root to image..."
     cptofs -p ${optionalString (partitionTableType != "none") "-P ${rootPartition}"} -t ${fsType} -i $diskImage $root/* /
   '';
@@ -217,7 +252,7 @@ in pkgs.vmTools.runInLinuxVM (
 
       # Create the ESP and mount it. Unlike e2fsprogs, mkfs.vfat doesn't support an
       # '-E offset=X' option, so we can't do this outside the VM.
-      ${optionalString (partitionTableType == "efi") ''
+      ${optionalString (partitionTableType == "efi" || partitionTableType == "hybrid") ''
         mkdir -p /mnt/boot
         mkfs.vfat -n ESP /dev/vda1
         mount /dev/vda1 /mnt/boot
diff --git a/nixos/lib/make-ext4-fs.nix b/nixos/lib/make-ext4-fs.nix
index 627ac324cf5..33dbc8f5ec4 100644
--- a/nixos/lib/make-ext4-fs.nix
+++ b/nixos/lib/make-ext4-fs.nix
@@ -17,7 +17,7 @@
 , e2fsprogs
 , libfaketime
 , perl
-, lkl
+, fakeroot
 }:
 
 let
@@ -26,7 +26,7 @@ in
 pkgs.stdenv.mkDerivation {
   name = "ext4-fs.img${lib.optionalString compressImage ".zst"}";
 
-  nativeBuildInputs = [ e2fsprogs.bin libfaketime perl lkl ]
+  nativeBuildInputs = [ e2fsprogs.bin libfaketime perl fakeroot ]
   ++ lib.optional compressImage zstd;
 
   buildCommand =
@@ -37,32 +37,34 @@ pkgs.stdenv.mkDerivation {
       ${populateImageCommands}
       )
 
-      # Add the closures of the top-level store objects.
-      storePaths=$(cat ${sdClosureInfo}/store-paths)
+      echo "Preparing store paths for image..."
+
+      # Create nix/store before copying path
+      mkdir -p ./rootImage/nix/store
+
+      xargs -I % cp -a --reflink=auto % -t ./rootImage/nix/store/ < ${sdClosureInfo}/store-paths
+      (
+        GLOBIGNORE=".:.."
+        shopt -u dotglob
+
+        for f in ./files/*; do
+            cp -a --reflink=auto -t ./rootImage/ "$f"
+        done
+      )
+
+      # Also include a manifest of the closures in a format suitable for nix-store --load-db
+      cp ${sdClosureInfo}/registration ./rootImage/nix-path-registration
 
       # Make a crude approximation of the size of the target image.
       # If the script starts failing, increase the fudge factors here.
-      numInodes=$(find $storePaths ./files | wc -l)
-      numDataBlocks=$(du -s -c -B 4096 --apparent-size $storePaths ./files | tail -1 | awk '{ print int($1 * 1.03) }')
+      numInodes=$(find ./rootImage | wc -l)
+      numDataBlocks=$(du -s -c -B 4096 --apparent-size ./rootImage | tail -1 | awk '{ print int($1 * 1.10) }')
       bytes=$((2 * 4096 * $numInodes + 4096 * $numDataBlocks))
       echo "Creating an EXT4 image of $bytes bytes (numInodes=$numInodes, numDataBlocks=$numDataBlocks)"
 
       truncate -s $bytes $img
-      faketime -f "1970-01-01 00:00:01" mkfs.ext4 -L ${volumeLabel} -U ${uuid} $img
-
-      # Also include a manifest of the closures in a format suitable for nix-store --load-db.
-      cp ${sdClosureInfo}/registration nix-path-registration
-      cptofs -t ext4 -i $img nix-path-registration /
-
-      # Create nix/store before copying paths
-      faketime -f "1970-01-01 00:00:01" mkdir -p nix/store
-      cptofs -t ext4 -i $img nix /
-
-      echo "copying store paths to image..."
-      cptofs -t ext4 -i $img $storePaths /nix/store/
 
-      echo "copying files to image..."
-      cptofs -t ext4 -i $img ./files/* /
+      faketime -f "1970-01-01 00:00:01" fakeroot mkfs.ext4 -L ${volumeLabel} -U ${uuid} -d ./rootImage $img
 
       export EXT2FS_NO_MTAB_OK=yes
       # I have ended up with corrupted images sometimes, I suspect that happens when the build machine's disk gets full during the build.
diff --git a/nixos/lib/make-iso9660-image.nix b/nixos/lib/make-iso9660-image.nix
index 0f3f2b5b523..6a0e0e7c635 100644
--- a/nixos/lib/make-iso9660-image.nix
+++ b/nixos/lib/make-iso9660-image.nix
@@ -1,4 +1,4 @@
-{ stdenv, closureInfo, xorriso, syslinux
+{ stdenv, closureInfo, xorriso, syslinux, libossp_uuid
 
 , # The file name of the resulting ISO image.
   isoName ? "cd.iso"
@@ -34,8 +34,8 @@
 , # The path (outside the ISO file system) of the isohybrid-mbr image.
   isohybridMbrImage ? ""
 
-, # Whether to compress the resulting ISO image with bzip2.
-  compressImage ? false
+, # Whether to compress the resulting ISO image with zstd.
+  compressImage ? false, zstd
 
 , # The volume ID.
   volumeID ? ""
@@ -48,7 +48,7 @@ assert usbBootable -> isohybridMbrImage != "";
 stdenv.mkDerivation {
   name = isoName;
   builder = ./make-iso9660-image.sh;
-  buildInputs = [ xorriso syslinux ];
+  buildInputs = [ xorriso syslinux zstd libossp_uuid ];
 
   inherit isoName bootable bootImage compressImage volumeID efiBootImage efiBootable isohybridMbrImage usbBootable;
 
diff --git a/nixos/lib/make-iso9660-image.sh b/nixos/lib/make-iso9660-image.sh
index d4633d2c8d1..4740b05f955 100644
--- a/nixos/lib/make-iso9660-image.sh
+++ b/nixos/lib/make-iso9660-image.sh
@@ -99,7 +99,12 @@ done
 
 mkdir -p $out/iso
 
+# daed2280-b91e-42c0-aed6-82c825ca41f3 is an arbitrary namespace, to prevent
+# independent applications from generating the same UUID for the same value.
+# (the chance of that being problematic seem pretty slim here, but that's how
+# version-5 UUID's work)
 xorriso="xorriso
+ -boot_image any gpt_disk_guid=$(uuid -v 5 daed2280-b91e-42c0-aed6-82c825ca41f3 $out | tr -d -)
  -as mkisofs
  -iso-level 3
  -volid ${volumeID}
@@ -118,20 +123,16 @@ xorriso="xorriso
 
 $xorriso -output $out/iso/$isoName
 
-if test -n "$usbBootable"; then
-    echo "Making image hybrid..."
-    if test -n "$efiBootable"; then
-        isohybrid --uefi $out/iso/$isoName
-    else
-        isohybrid $out/iso/$isoName
-    fi
-fi
-
 if test -n "$compressImage"; then
     echo "Compressing image..."
-    bzip2 $out/iso/$isoName
+    zstd -T$NIX_BUILD_CORES --rm $out/iso/$isoName
 fi
 
 mkdir -p $out/nix-support
 echo $system > $out/nix-support/system
-echo "file iso $out/iso/$isoName" >> $out/nix-support/hydra-build-products
+
+if test -n "$compressImage"; then
+    echo "file iso $out/iso/$isoName.zst" >> $out/nix-support/hydra-build-products
+else
+    echo "file iso $out/iso/$isoName" >> $out/nix-support/hydra-build-products
+fi
diff --git a/nixos/lib/make-options-doc/default.nix b/nixos/lib/make-options-doc/default.nix
index 772b7d3add9..a1161621f0d 100644
--- a/nixos/lib/make-options-doc/default.nix
+++ b/nixos/lib/make-options-doc/default.nix
@@ -36,7 +36,7 @@ let
     // 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 ? relatedPackages && opt.relatedPackages != []) { relatedPackages = genRelatedPackages opt.relatedPackages; }
+    // lib.optionalAttrs (opt ? relatedPackages && opt.relatedPackages != []) { relatedPackages = genRelatedPackages opt.relatedPackages opt.name; }
    );
 
   # Generate DocBook documentation for a list of packages. This is
@@ -48,7 +48,7 @@ let
   # - a list:    that will be interpreted as an attribute path from `pkgs`,
   # - an attrset: that can specify `name`, `path`, `package`, `comment`
   #   (either of `name`, `path` is required, the rest are optional).
-  genRelatedPackages = packages:
+  genRelatedPackages = packages: optName:
     let
       unpack = p: if lib.isString p then { name = p; }
                   else if lib.isList p then { path = p; }
@@ -58,7 +58,7 @@ let
           title = args.title or null;
           name = args.name or (lib.concatStringsSep "." args.path);
           path = args.path or [ args.name ];
-          package = args.package or (lib.attrByPath path (throw "Invalid package attribute path `${toString path}'") pkgs);
+          package = args.package or (lib.attrByPath path (throw "Invalid package attribute path `${toString path}' found while evaluating `relatedPackages' of option `${optName}'") pkgs);
         in "<listitem>"
         + "<para><literal>${lib.optionalString (title != null) "${title} aka "}pkgs.${name} (${package.meta.name})</literal>"
         + lib.optionalString (!package.meta.available) " <emphasis>[UNAVAILABLE]</emphasis>"
diff --git a/nixos/lib/qemu-flags.nix b/nixos/lib/qemu-flags.nix
index 859d9e975fe..0cf6977af4b 100644
--- a/nixos/lib/qemu-flags.nix
+++ b/nixos/lib/qemu-flags.nix
@@ -2,13 +2,18 @@
 { pkgs }:
 
 let
-  zeroPad = n: if n < 10 then "0${toString n}" else toString n;
+  zeroPad = n:
+    pkgs.lib.optionalString (n < 16) "0" +
+      (if n > 255
+       then throw "Can't have more than 255 nets or nodes!"
+       else pkgs.lib.toHexString n);
 in
 
-{
+rec {
+  qemuNicMac = net: machine: "52:54:00:12:${zeroPad net}:${zeroPad machine}";
 
   qemuNICFlags = nic: net: machine:
-    [ "-device virtio-net-pci,netdev=vlan${toString nic},mac=52:54:00:12:${zeroPad net}:${zeroPad machine}"
+    [ "-device virtio-net-pci,netdev=vlan${toString nic},mac=${qemuNicMac net machine}"
       "-netdev vde,id=vlan${toString nic},sock=$QEMU_VDE_SOCKET_${toString net}"
     ];
 
diff --git a/nixos/lib/test-driver/Logger.pm b/nixos/lib/test-driver/Logger.pm
index 080310ea34e..a3384084a0e 100644
--- a/nixos/lib/test-driver/Logger.pm
+++ b/nixos/lib/test-driver/Logger.pm
@@ -8,17 +8,17 @@ use Time::HiRes qw(clock_gettime CLOCK_MONOTONIC);
 
 sub new {
     my ($class) = @_;
-    
+
     my $logFile = defined $ENV{LOGFILE} ? "$ENV{LOGFILE}" : "/dev/null";
     my $log = new XML::Writer(OUTPUT => new IO::File(">$logFile"));
-    
+
     my $self = {
         log => $log,
         logQueue => Thread::Queue->new()
     };
-    
+
     $self->{log}->startTag("logfile");
-    
+
     bless $self, $class;
     return $self;
 }
diff --git a/nixos/lib/test-driver/test-driver.py b/nixos/lib/test-driver/test-driver.py
index bf46d0df97f..f4e2bb6100f 100644
--- a/nixos/lib/test-driver/test-driver.py
+++ b/nixos/lib/test-driver/test-driver.py
@@ -3,7 +3,10 @@ from contextlib import contextmanager, _GeneratorContextManager
 from queue import Queue, Empty
 from typing import Tuple, Any, Callable, Dict, Iterator, Optional, List
 from xml.sax.saxutils import XMLGenerator
+import queue
+import io
 import _thread
+import argparse
 import atexit
 import base64
 import codecs
@@ -19,6 +22,7 @@ import subprocess
 import sys
 import tempfile
 import time
+import traceback
 import unicodedata
 
 CHAR_TO_KEY = {
@@ -420,15 +424,18 @@ class Machine:
                 output += out
         return output
 
-    def fail(self, *commands: str) -> None:
+    def fail(self, *commands: str) -> str:
         """Execute each command and check that it fails."""
+        output = ""
         for command in commands:
             with self.nested("must fail: {}".format(command)):
-                status, output = self.execute(command)
+                (status, out) = self.execute(command)
                 if status == 0:
                     raise Exception(
                         "command `{}` unexpectedly succeeded".format(command)
                     )
+                output += out
+        return output
 
     def wait_until_succeeds(self, command: str) -> str:
         """Wait until a command returns success and return its output.
@@ -598,11 +605,8 @@ class Machine:
                 shutil.copytree(host_src, host_intermediate)
             else:
                 shutil.copy(host_src, host_intermediate)
-            self.succeed("sync")
             self.succeed(make_command(["mkdir", "-p", vm_target.parent]))
             self.succeed(make_command(["cp", "-r", vm_intermediate, vm_target]))
-        # Make sure the cleanup is synced into VM
-        self.succeed("sync")
 
     def copy_from_vm(self, source: str, target_dir: str = "") -> None:
         """Copy a file from the VM (specified by an in-VM source path) to a path
@@ -620,7 +624,6 @@ class Machine:
             # Copy the file to the shared directory inside VM
             self.succeed(make_command(["mkdir", "-p", vm_shared_temp]))
             self.succeed(make_command(["cp", "-r", vm_src, vm_intermediate]))
-            self.succeed("sync")
             abs_target = out_dir / target_dir / vm_src.name
             abs_target.parent.mkdir(exist_ok=True, parents=True)
             # Copy the file from the shared directory outside VM
@@ -628,8 +631,6 @@ class Machine:
                 shutil.copytree(intermediate, abs_target)
             else:
                 shutil.copy(intermediate, abs_target)
-        # Make sure the cleanup is synced into VM
-        self.succeed("sync")
 
     def dump_tty_contents(self, tty: str) -> None:
         """Debugging: Dump the contents of the TTY<n>
@@ -677,6 +678,22 @@ class Machine:
         with self.nested("waiting for {} to appear on screen".format(regex)):
             retry(screen_matches)
 
+    def wait_for_console_text(self, regex: str) -> None:
+        self.log("waiting for {} to appear on console".format(regex))
+        # Buffer the console output, this is needed
+        # to match multiline regexes.
+        console = io.StringIO()
+        while True:
+            try:
+                console.write(self.last_lines.get())
+            except queue.Empty:
+                self.sleep(1)
+                continue
+            console.seek(0)
+            matches = re.search(regex, console.read())
+            if matches is not None:
+                return
+
     def send_key(self, key: str) -> None:
         key = CHAR_TO_KEY.get(key, key)
         self.send_monitor_command("sendkey {}".format(key))
@@ -740,11 +757,16 @@ class Machine:
         self.monitor, _ = self.monitor_socket.accept()
         self.shell, _ = self.shell_socket.accept()
 
+        # Store last serial console lines for use
+        # of wait_for_console_text
+        self.last_lines: Queue = Queue()
+
         def process_serial_output() -> None:
             assert self.process.stdout is not None
             for _line in self.process.stdout:
                 # Ignore undecodable bytes that may occur in boot menus
                 line = _line.decode(errors="ignore").replace("\r", "").rstrip()
+                self.last_lines.put(line)
                 eprint("{} # {}".format(self.name, line))
                 self.logger.enqueue({"msg": line, "machine": self.name})
 
@@ -757,6 +779,11 @@ class Machine:
 
         self.log("QEMU running (pid {})".format(self.pid))
 
+    def cleanup_statedir(self) -> None:
+        self.log("delete the VM state directory")
+        if os.path.isfile(self.state_dir):
+            shutil.rmtree(self.state_dir)
+
     def shutdown(self) -> None:
         if not self.booted:
             return
@@ -869,7 +896,8 @@ def run_tests() -> None:
             try:
                 exec(tests, globals())
             except Exception as e:
-                eprint("error: {}".format(str(e)))
+                eprint("error: ")
+                traceback.print_exc()
                 sys.exit(1)
     else:
         ptpython.repl.embed(locals(), globals())
@@ -895,6 +923,15 @@ def subtest(name: str) -> Iterator[None]:
 
 
 if __name__ == "__main__":
+    arg_parser = argparse.ArgumentParser()
+    arg_parser.add_argument(
+        "-K",
+        "--keep-vm-state",
+        help="re-use a VM state coming from a previous run",
+        action="store_true",
+    )
+    (cli_args, vm_scripts) = arg_parser.parse_known_args()
+
     log = Logger()
 
     vlan_nrs = list(dict.fromkeys(os.environ.get("VLANS", "").split()))
@@ -902,8 +939,10 @@ if __name__ == "__main__":
     for nr, vde_socket, _, _ in vde_sockets:
         os.environ["QEMU_VDE_SOCKET_{}".format(nr)] = vde_socket
 
-    vm_scripts = sys.argv[1:]
     machines = [create_machine({"startCommand": s}) for s in vm_scripts]
+    for machine in machines:
+        if not cli_args.keep_vm_state:
+            machine.cleanup_statedir()
     machine_eval = [
         "{0} = machines[{1}]".format(m.name, idx) for idx, m in enumerate(machines)
     ]
@@ -917,7 +956,6 @@ if __name__ == "__main__":
                     continue
                 log.log("killing {} (pid {})".format(machine.name, machine.pid))
                 machine.process.kill()
-
             for _, _, process, _ in vde_sockets:
                 process.terminate()
         log.close()
diff --git a/nixos/lib/testing-python.nix b/nixos/lib/testing-python.nix
index 88801f20517..c6939c7d698 100644
--- a/nixos/lib/testing-python.nix
+++ b/nixos/lib/testing-python.nix
@@ -10,11 +10,7 @@
 with import ./build-vms.nix { inherit system pkgs minimal extraConfigurations; };
 with pkgs;
 
-let
-  jquery-ui = callPackage ./testing/jquery-ui.nix { };
-  jquery = callPackage ./testing/jquery.nix { };
-
-in rec {
+rec {
 
   inherit pkgs;
 
@@ -118,7 +114,7 @@ in rec {
 
       imagemagick_tiff = imagemagick_light.override { inherit libtiff; };
 
-      # Generate onvenience wrappers for running the test driver
+      # Generate convenience wrappers for running the test driver
       # interactively with the specified network, and for starting the
       # VMs from the command line.
       driver = let warn = if skipLint then lib.warn "Linting is disabled!" else lib.id; in warn (runCommand testDriverName
diff --git a/nixos/lib/testing.nix b/nixos/lib/testing.nix
index cbb7faf039e..5c784c2f0ab 100644
--- a/nixos/lib/testing.nix
+++ b/nixos/lib/testing.nix
@@ -10,11 +10,7 @@
 with import ./build-vms.nix { inherit system pkgs minimal extraConfigurations; };
 with pkgs;
 
-let
-  jquery-ui = callPackage ./testing/jquery-ui.nix { };
-  jquery = callPackage ./testing/jquery.nix { };
-
-in rec {
+rec {
 
   inherit pkgs;
 
diff --git a/nixos/lib/testing/jquery-ui.nix b/nixos/lib/testing/jquery-ui.nix
deleted file mode 100644
index 05bd8dc2f94..00000000000
--- a/nixos/lib/testing/jquery-ui.nix
+++ /dev/null
@@ -1,24 +0,0 @@
-{ stdenv, fetchurl, unzip }:
-
-stdenv.mkDerivation rec {
-  name = "jquery-ui-1.11.4";
-
-  src = fetchurl {
-    url = "https://jqueryui.com/resources/download/${name}.zip";
-    sha256 = "0ciyaj1acg08g8hpzqx6whayq206fvf4whksz2pjgxlv207lqgjh";
-  };
-
-  buildInputs = [ unzip ];
-
-  installPhase =
-    ''
-      mkdir -p "$out/js"
-      cp -rv . "$out/js"
-    '';
-
-  meta = {
-    homepage = "https://jqueryui.com/";
-    description = "A library of JavaScript widgets and effects";
-    platforms = stdenv.lib.platforms.all;
-  };
-}
diff --git a/nixos/lib/testing/jquery.nix b/nixos/lib/testing/jquery.nix
deleted file mode 100644
index 732fdb3ba87..00000000000
--- a/nixos/lib/testing/jquery.nix
+++ /dev/null
@@ -1,36 +0,0 @@
-{ stdenv, fetchurl, compressed ? true }:
-
-with stdenv.lib;
-
-stdenv.mkDerivation rec {
-  name = "jquery-1.11.3";
-
-  src = if compressed then
-    fetchurl {
-      url = "http://code.jquery.com/${name}.min.js";
-      sha256 = "1f4glgxxn3jnvry3dpzmazj3207baacnap5w20gr2xlk789idfgc";
-    }
-    else
-    fetchurl {
-      url = "http://code.jquery.com/${name}.js";
-      sha256 = "1v956yf5spw0156rni5z77hzqwmby7ajwdcd6mkhb6zvl36awr90";
-    };
-
-  dontUnpack = true;
-
-  installPhase =
-    ''
-      mkdir -p "$out/js"
-      cp -v "$src" "$out/js/jquery.js"
-      ${optionalString compressed ''
-        (cd "$out/js" && ln -s jquery.js jquery.min.js)
-      ''}
-    '';
-
-  meta = with stdenv.lib; {
-    description = "JavaScript library designed to simplify the client-side scripting of HTML";
-    homepage = "http://jquery.com/";
-    license = licenses.mit;
-    platforms = platforms.all;
-  };
-}
diff --git a/nixos/lib/utils.nix b/nixos/lib/utils.nix
index 21f4c7c6988..543c8a8882e 100644
--- a/nixos/lib/utils.nix
+++ b/nixos/lib/utils.nix
@@ -2,9 +2,11 @@ pkgs: with pkgs.lib;
 
 rec {
 
-  # Check whenever fileSystem is needed for boot
-  fsNeededForBoot = fs: fs.neededForBoot
-                     || elem fs.mountPoint [ "/" "/nix" "/nix/store" "/var" "/var/log" "/var/lib" "/etc" ];
+  # Check whenever fileSystem is needed for boot.  NOTE: Make sure
+  # pathsNeededForBoot is closed under the parent relationship, i.e. if /a/b/c
+  # is in the list, put /a and /a/b in as well.
+  pathsNeededForBoot = [ "/" "/nix" "/nix/store" "/var" "/var/log" "/var/lib" "/etc" ];
+  fsNeededForBoot = fs: fs.neededForBoot || elem fs.mountPoint pathsNeededForBoot;
 
   # Check whenever `b` depends on `a` as a fileSystem
   fsBefore = a: b: a.mountPoint == b.device
diff --git a/nixos/maintainers/scripts/azure-new/.gitignore b/nixos/maintainers/scripts/azure-new/.gitignore
index 26905a86234..9271abf14a0 100644
--- a/nixos/maintainers/scripts/azure-new/.gitignore
+++ b/nixos/maintainers/scripts/azure-new/.gitignore
@@ -1 +1 @@
-azure
\ No newline at end of file
+azure
diff --git a/nixos/maintainers/scripts/azure-new/README.md b/nixos/maintainers/scripts/azure-new/README.md
index 20e81c44ce5..e5b69dacec0 100644
--- a/nixos/maintainers/scripts/azure-new/README.md
+++ b/nixos/maintainers/scripts/azure-new/README.md
@@ -20,7 +20,7 @@ $ ./upload-image.sh ./examples/basic/image.nix
 + nix-build ./examples/basic/image.nix --out-link azure
 /nix/store/qdpzknpskzw30vba92mb24xzll1dqsmd-azure-image
 ...
-95.5 %, 0 Done, 0 Failed, 1 Pending, 0 Skipped, 1 Total, 2-sec Throughput (Mb/s): 932.9565 
+95.5 %, 0 Done, 0 Failed, 1 Pending, 0 Skipped, 1 Total, 2-sec Throughput (Mb/s): 932.9565
 ...
 /subscriptions/aff271ee-e9be-4441-b9bb-42f5af4cbaeb/resourceGroups/nixos-images/providers/Microsoft.Compute/images/azure-image-todo-makethisbetter
 ```
diff --git a/nixos/maintainers/scripts/azure-new/upload-image.sh b/nixos/maintainers/scripts/azure-new/upload-image.sh
index 1466dcd1f0a..143afbd7f96 100755
--- a/nixos/maintainers/scripts/azure-new/upload-image.sh
+++ b/nixos/maintainers/scripts/azure-new/upload-image.sh
@@ -37,8 +37,8 @@ if ! az disk show -g "${group}" -n "${img_name}" &>/dev/null; then
   )"
 
   azcopy copy "${img_file}" "${sasurl}" \
-    --blob-type PageBlob 
-    
+    --blob-type PageBlob
+
   az disk revoke-access \
     --resource-group "${group}" \
     --name "${img_name}"
diff --git a/nixos/maintainers/scripts/ec2/amazon-image.nix b/nixos/maintainers/scripts/ec2/amazon-image.nix
index 36f3e7af873..b09f4ca47a3 100644
--- a/nixos/maintainers/scripts/ec2/amazon-image.nix
+++ b/nixos/maintainers/scripts/ec2/amazon-image.nix
@@ -63,8 +63,8 @@ in {
     fsType = "ext4";
     configFile = pkgs.writeText "configuration.nix"
       ''
-        {
-          imports = [ <nixpkgs/nixos/modules/virtualisation/amazon-image.nix> ];
+        { modulesPath, ... }: {
+          imports = [ "''${modulesPath}/virtualisation/amazon-image.nix" ];
           ${optionalString config.ec2.hvm ''
             ec2.hvm = true;
           ''}
diff --git a/nixos/maintainers/scripts/ec2/create-amis.sh b/nixos/maintainers/scripts/ec2/create-amis.sh
index 145eb49ced7..89e24f2ccfd 100755
--- a/nixos/maintainers/scripts/ec2/create-amis.sh
+++ b/nixos/maintainers/scripts/ec2/create-amis.sh
@@ -29,7 +29,7 @@ log() {
     echo "$@" >&2
 }
 
-if [ -z "$1" ]; then
+if [ "$#" -ne 1 ]; then
     log "Usage: ./upload-amazon-image.sh IMAGE_OUTPUT"
     exit 1
 fi
diff --git a/nixos/modules/config/appstream.nix b/nixos/modules/config/appstream.nix
index 483ac9c3cd7..a72215c2f56 100644
--- a/nixos/modules/config/appstream.nix
+++ b/nixos/modules/config/appstream.nix
@@ -7,18 +7,18 @@ with lib;
       type = types.bool;
       default = true;
       description = ''
-        Whether to install files to support the 
+        Whether to install files to support the
         <link xlink:href="https://www.freedesktop.org/software/appstream/docs/index.html">AppStream metadata specification</link>.
       '';
     };
   };
 
   config = mkIf config.appstream.enable {
-    environment.pathsToLink = [ 
+    environment.pathsToLink = [
       # per component metadata
-      "/share/metainfo" 
+      "/share/metainfo"
       # legacy path for above
-      "/share/appdata" 
+      "/share/appdata"
     ];
   };
 
diff --git a/nixos/modules/config/fonts/fontconfig-penultimate.nix b/nixos/modules/config/fonts/fontconfig-penultimate.nix
deleted file mode 100644
index 7e311a21acf..00000000000
--- a/nixos/modules/config/fonts/fontconfig-penultimate.nix
+++ /dev/null
@@ -1,292 +0,0 @@
-{ config, pkgs, lib, ... }:
-
-with lib;
-
-let
-  cfg = config.fonts.fontconfig;
-
-  fcBool = x: "<bool>" + (boolToString x) + "</bool>";
-
-  # back-supported fontconfig version and package
-  # version is used for font cache generation
-  supportVersion = "210";
-  supportPkg     = pkgs."fontconfig_${supportVersion}";
-
-  # latest fontconfig version and package
-  # version is used for configuration folder name, /etc/fonts/VERSION/
-  # note: format differs from supportVersion and can not be used with makeCacheConf
-  latestVersion  = pkgs.fontconfig.configVersion;
-  latestPkg      = pkgs.fontconfig;
-
-  # supported version fonts.conf
-  supportFontsConf = pkgs.makeFontsConf { fontconfig = supportPkg; fontDirectories = config.fonts.fonts; };
-
-  # configuration file to read fontconfig cache
-  # version dependent
-  # priority 0
-  cacheConfSupport = makeCacheConf { version = supportVersion; };
-  cacheConfLatest  = makeCacheConf {};
-
-  # generate the font cache setting file for a fontconfig version
-  # use latest when no version is passed
-  makeCacheConf = { version ? null }:
-    let
-      fcPackage = if version == null
-                  then "fontconfig"
-                  else "fontconfig_${version}";
-      makeCache = fontconfig: pkgs.makeFontsCache { inherit fontconfig; fontDirectories = config.fonts.fonts; };
-      cache     = makeCache pkgs.${fcPackage};
-      cache32   = makeCache pkgs.pkgsi686Linux.${fcPackage};
-    in
-    pkgs.writeText "fc-00-nixos-cache.conf" ''
-      <?xml version='1.0'?>
-      <!DOCTYPE fontconfig SYSTEM 'fonts.dtd'>
-      <fontconfig>
-        <!-- Font directories -->
-        ${concatStringsSep "\n" (map (font: "<dir>${font}</dir>") config.fonts.fonts)}
-        <!-- Pre-generated font caches -->
-        <cachedir>${cache}</cachedir>
-        ${optionalString (pkgs.stdenv.isx86_64 && cfg.cache32Bit) ''
-          <cachedir>${cache32}</cachedir>
-        ''}
-      </fontconfig>
-    '';
-
-  # local configuration file
-  localConf = pkgs.writeText "fc-local.conf" cfg.localConf;
-
-  # rendering settings configuration files
-  # priority 10
-  hintingConf = pkgs.writeText "fc-10-hinting.conf" ''
-    <?xml version='1.0'?>
-    <!DOCTYPE fontconfig SYSTEM 'fonts.dtd'>
-    <fontconfig>
-
-      <!-- Default rendering settings -->
-      <match target="pattern">
-        <edit mode="append" name="hinting">
-          ${fcBool cfg.hinting.enable}
-        </edit>
-        <edit mode="append" name="autohint">
-          ${fcBool cfg.hinting.autohint}
-        </edit>
-        <edit mode="append" name="hintstyle">
-          <const>hintslight</const>
-        </edit>
-      </match>
-
-    </fontconfig>
-  '';
-
-  antialiasConf = pkgs.writeText "fc-10-antialias.conf" ''
-    <?xml version='1.0'?>
-    <!DOCTYPE fontconfig SYSTEM 'fonts.dtd'>
-    <fontconfig>
-
-      <!-- Default rendering settings -->
-      <match target="pattern">
-        <edit mode="append" name="antialias">
-          ${fcBool cfg.antialias}
-        </edit>
-      </match>
-
-    </fontconfig>
-  '';
-
-  subpixelConf = pkgs.writeText "fc-10-subpixel.conf" ''
-    <?xml version='1.0'?>
-    <!DOCTYPE fontconfig SYSTEM 'fonts.dtd'>
-    <fontconfig>
-
-      <!-- Default rendering settings -->
-      <match target="pattern">
-        <edit mode="append" name="rgba">
-          <const>${cfg.subpixel.rgba}</const>
-        </edit>
-        <edit mode="append" name="lcdfilter">
-          <const>lcd${cfg.subpixel.lcdfilter}</const>
-        </edit>
-      </match>
-
-    </fontconfig>
-  '';
-
-  dpiConf = pkgs.writeText "fc-11-dpi.conf" ''
-    <?xml version='1.0'?>
-    <!DOCTYPE fontconfig SYSTEM 'fonts.dtd'>
-    <fontconfig>
-
-      <match target="pattern">
-        <edit name="dpi" mode="assign">
-          <double>${toString cfg.dpi}</double>
-        </edit>
-      </match>
-
-    </fontconfig>
-  '';
-
-  # default fonts configuration file
-  # priority 52
-  defaultFontsConf =
-    let genDefault = fonts: name:
-      optionalString (fonts != []) ''
-        <alias>
-          <family>${name}</family>
-          <prefer>
-          ${concatStringsSep ""
-          (map (font: ''
-            <family>${font}</family>
-          '') fonts)}
-          </prefer>
-        </alias>
-      '';
-    in
-    pkgs.writeText "fc-52-nixos-default-fonts.conf" ''
-    <?xml version='1.0'?>
-    <!DOCTYPE fontconfig SYSTEM 'fonts.dtd'>
-    <fontconfig>
-
-      <!-- Default fonts -->
-      ${genDefault cfg.defaultFonts.sansSerif "sans-serif"}
-
-      ${genDefault cfg.defaultFonts.serif     "serif"}
-
-      ${genDefault cfg.defaultFonts.monospace "monospace"}
-
-    </fontconfig>
-  '';
-
-  # reject Type 1 fonts
-  # priority 53
-  rejectType1 = pkgs.writeText "fc-53-nixos-reject-type1.conf" ''
-    <?xml version="1.0"?>
-    <!DOCTYPE fontconfig SYSTEM "fonts.dtd">
-    <fontconfig>
-
-    <!-- Reject Type 1 fonts -->
-    <selectfont>
-      <rejectfont>
-        <pattern>
-          <patelt name="fontformat"><string>Type 1</string></patelt>
-        </pattern>
-      </rejectfont>
-    </selectfont>
-
-    </fontconfig>
-  '';
-
-  # The configuration to be included in /etc/font/
-  penultimateConf = pkgs.runCommand "fontconfig-penultimate-conf" {
-    preferLocalBuild = true;
-  } ''
-    support_folder=$out/etc/fonts/conf.d
-    latest_folder=$out/etc/fonts/${latestVersion}/conf.d
-
-    mkdir -p $support_folder
-    mkdir -p $latest_folder
-
-    # fonts.conf
-    ln -s ${supportFontsConf} $support_folder/../fonts.conf
-    ln -s ${latestPkg.out}/etc/fonts/fonts.conf \
-          $latest_folder/../fonts.conf
-
-    # fontconfig-penultimate various configuration files
-    ln -s ${pkgs.fontconfig-penultimate}/etc/fonts/conf.d/*.conf \
-          $support_folder
-    ln -s ${pkgs.fontconfig-penultimate}/etc/fonts/conf.d/*.conf \
-          $latest_folder
-
-    ln -s ${cacheConfSupport} $support_folder/00-nixos-cache.conf
-    ln -s ${cacheConfLatest}  $latest_folder/00-nixos-cache.conf
-
-    rm $support_folder/10-antialias.conf $latest_folder/10-antialias.conf
-    ln -s ${antialiasConf} $support_folder/10-antialias.conf
-    ln -s ${antialiasConf} $latest_folder/10-antialias.conf
-
-    rm $support_folder/10-hinting.conf $latest_folder/10-hinting.conf
-    ln -s ${hintingConf} $support_folder/10-hinting.conf
-    ln -s ${hintingConf} $latest_folder/10-hinting.conf
-
-    ${optionalString cfg.useEmbeddedBitmaps ''
-    rm $support_folder/10-no-embedded-bitmaps.conf
-    rm $latest_folder/10-no-embedded-bitmaps.conf
-    ''}
-
-    rm $support_folder/10-subpixel.conf $latest_folder/10-subpixel.conf
-    ln -s ${subpixelConf} $support_folder/10-subpixel.conf
-    ln -s ${subpixelConf} $latest_folder/10-subpixel.conf
-
-    ${optionalString (cfg.dpi != 0) ''
-    ln -s ${dpiConf} $support_folder/11-dpi.conf
-    ln -s ${dpiConf} $latest_folder/11-dpi.conf
-    ''}
-
-    # 50-user.conf
-    ${optionalString (!cfg.includeUserConf) ''
-    rm $support_folder/50-user.conf
-    rm $latest_folder/50-user.conf
-    ''}
-
-    # 51-local.conf
-    rm $latest_folder/51-local.conf
-    substitute \
-      ${pkgs.fontconfig-penultimate}/etc/fonts/conf.d/51-local.conf \
-      $latest_folder/51-local.conf \
-      --replace local.conf /etc/fonts/${latestVersion}/local.conf
-
-    # local.conf (indirect priority 51)
-    ${optionalString (cfg.localConf != "") ''
-    ln -s ${localConf}        $support_folder/../local.conf
-    ln -s ${localConf}        $latest_folder/../local.conf
-    ''}
-
-    # 52-nixos-default-fonts.conf
-    ln -s ${defaultFontsConf} $support_folder/52-nixos-default-fonts.conf
-    ln -s ${defaultFontsConf} $latest_folder/52-nixos-default-fonts.conf
-
-    # 53-no-bitmaps.conf
-    ${optionalString cfg.allowBitmaps ''
-    rm $support_folder/53-no-bitmaps.conf
-    rm $latest_folder/53-no-bitmaps.conf
-    ''}
-
-    ${optionalString (!cfg.allowType1) ''
-    # 53-nixos-reject-type1.conf
-    ln -s ${rejectType1} $support_folder/53-nixos-reject-type1.conf
-    ln -s ${rejectType1} $latest_folder/53-nixos-reject-type1.conf
-    ''}
-  '';
-
-in
-{
-
-  options = {
-
-    fonts = {
-
-      fontconfig = {
-
-        penultimate = {
-          enable = mkOption {
-            type = types.bool;
-            default = false;
-            description = ''
-              Enable fontconfig-penultimate settings to supplement the
-              NixOS defaults by providing per-font rendering defaults and
-              metric aliases.
-            '';
-          };
-        };
-
-      };
-    };
-
-  };
-
-  config = mkIf (config.fonts.fontconfig.enable && config.fonts.fontconfig.penultimate.enable) {
-
-    fonts.fontconfig.confPackages = [ penultimateConf ];
-
-  };
-
-}
diff --git a/nixos/modules/config/fonts/fontconfig.nix b/nixos/modules/config/fonts/fontconfig.nix
index 6ac64b0ec9c..1f1044bc5af 100644
--- a/nixos/modules/config/fonts/fontconfig.nix
+++ b/nixos/modules/config/fonts/fontconfig.nix
@@ -1,11 +1,6 @@
 /*
 
-NixOS support 2 fontconfig versions, "support" and "latest".
-
-- "latest" refers to default fontconfig package (pkgs.fontconfig).
-  configuration files are linked to /etc/fonts/VERSION/conf.d/
-- "support" refers to supportPkg (pkgs."fontconfig_${supportVersion}").
-  configuration files are linked to /etc/fonts/conf.d/
+Configuration files are linked to /etc/fonts/${pkgs.fontconfig.configVersion}/conf.d/
 
 This module generates a package containing configuration files and link it in /etc/fonts.
 
@@ -22,40 +17,21 @@ let
   cfg = config.fonts.fontconfig;
 
   fcBool = x: "<bool>" + (boolToString x) + "</bool>";
-
-  # back-supported fontconfig version and package
-  # version is used for font cache generation
-  supportVersion = "210";
-  supportPkg     = pkgs."fontconfig_${supportVersion}";
-
-  # latest fontconfig version and package
-  # version is used for configuration folder name, /etc/fonts/VERSION/
-  # note: format differs from supportVersion and can not be used with makeCacheConf
-  latestVersion  = pkgs.fontconfig.configVersion;
-  latestPkg      = pkgs.fontconfig;
-
-  # supported version fonts.conf
-  supportFontsConf = pkgs.makeFontsConf { fontconfig = supportPkg; fontDirectories = config.fonts.fonts; };
+  pkg = pkgs.fontconfig;
 
   # configuration file to read fontconfig cache
-  # version dependent
   # priority 0
-  cacheConfSupport = makeCacheConf { version = supportVersion; };
-  cacheConfLatest  = makeCacheConf {};
+  cacheConf  = makeCacheConf {};
 
-  # generate the font cache setting file for a fontconfig version
-  # use latest when no version is passed
+  # generate the font cache setting file
   # When cross-compiling, we can’t generate the cache, so we skip the
   # <cachedir> part. fontconfig still works but is a little slower in
   # looking things up.
-  makeCacheConf = { version ? null }:
+  makeCacheConf = { }:
     let
-      fcPackage = if version == null
-                  then "fontconfig"
-                  else "fontconfig_${version}";
       makeCache = fontconfig: pkgs.makeFontsCache { inherit fontconfig; fontDirectories = config.fonts.fonts; };
-      cache     = makeCache pkgs.${fcPackage};
-      cache32   = makeCache pkgs.pkgsi686Linux.${fcPackage};
+      cache     = makeCache pkgs.fontconfig;
+      cache32   = makeCache pkgs.pkgsi686Linux.fontconfig;
     in
     pkgs.writeText "fc-00-nixos-cache.conf" ''
       <?xml version='1.0'?>
@@ -200,63 +176,47 @@ let
   confPkg = pkgs.runCommand "fontconfig-conf" {
     preferLocalBuild = true;
   } ''
-    support_folder=$out/etc/fonts/conf.d
-    latest_folder=$out/etc/fonts/${latestVersion}/conf.d
-
-    mkdir -p $support_folder
-    mkdir -p $latest_folder
+    dst=$out/etc/fonts/${pkg.configVersion}/conf.d
+    mkdir -p $dst
 
     # fonts.conf
-    ln -s ${supportFontsConf} $support_folder/../fonts.conf
-    ln -s ${latestPkg.out}/etc/fonts/fonts.conf \
-          $latest_folder/../fonts.conf
+    ln -s ${pkg.out}/etc/fonts/fonts.conf \
+          $dst/../fonts.conf
+    # TODO: remove this legacy symlink once people stop using packages built before #95358 was merged
+    ln -s /etc/fonts/${pkg.configVersion}/fonts.conf \
+          $out/etc/fonts/fonts.conf
 
     # fontconfig default config files
-    ln -s ${supportPkg.out}/etc/fonts/conf.d/*.conf \
-          $support_folder/
-    ln -s ${latestPkg.out}/etc/fonts/conf.d/*.conf \
-          $latest_folder/
-
-    # update latest 51-local.conf path to look at the latest local.conf
-    rm    $latest_folder/51-local.conf
-
-    substitute ${latestPkg.out}/etc/fonts/conf.d/51-local.conf \
-               $latest_folder/51-local.conf \
-               --replace local.conf /etc/fonts/${latestVersion}/local.conf
+    ln -s ${pkg.out}/etc/fonts/conf.d/*.conf \
+          $dst/
 
     # 00-nixos-cache.conf
-    ln -s ${cacheConfSupport} \
-          $support_folder/00-nixos-cache.conf
-    ln -s ${cacheConfLatest}  $latest_folder/00-nixos-cache.conf
+    ln -s ${cacheConf}  $dst/00-nixos-cache.conf
 
     # 10-nixos-rendering.conf
-    ln -s ${renderConf}       $support_folder/10-nixos-rendering.conf
-    ln -s ${renderConf}       $latest_folder/10-nixos-rendering.conf
+    ln -s ${renderConf}       $dst/10-nixos-rendering.conf
 
     # 50-user.conf
-    ${optionalString (!cfg.includeUserConf) ''
-    rm $support_folder/50-user.conf
-    rm $latest_folder/50-user.conf
+    # Since latest fontconfig looks for default files inside the package,
+    # we had to move this one elsewhere to be able to exclude it here.
+    ${optionalString cfg.includeUserConf ''
+    ln -s ${pkg.out}/etc/fonts/conf.d.bak/50-user.conf $dst/50-user.conf
     ''}
 
     # local.conf (indirect priority 51)
     ${optionalString (cfg.localConf != "") ''
-    ln -s ${localConf}        $support_folder/../local.conf
-    ln -s ${localConf}        $latest_folder/../local.conf
+    ln -s ${localConf}        $dst/../local.conf
     ''}
 
     # 52-nixos-default-fonts.conf
-    ln -s ${defaultFontsConf} $support_folder/52-nixos-default-fonts.conf
-    ln -s ${defaultFontsConf} $latest_folder/52-nixos-default-fonts.conf
+    ln -s ${defaultFontsConf} $dst/52-nixos-default-fonts.conf
 
     # 53-no-bitmaps.conf
-    ln -s ${rejectBitmaps} $support_folder/53-no-bitmaps.conf
-    ln -s ${rejectBitmaps} $latest_folder/53-no-bitmaps.conf
+    ln -s ${rejectBitmaps} $dst/53-no-bitmaps.conf
 
     ${optionalString (!cfg.allowType1) ''
     # 53-nixos-reject-type1.conf
-    ln -s ${rejectType1} $support_folder/53-nixos-reject-type1.conf
-    ln -s ${rejectType1} $latest_folder/53-nixos-reject-type1.conf
+    ln -s ${rejectType1} $dst/53-nixos-reject-type1.conf
     ''}
   '';
 
@@ -278,7 +238,14 @@ in
     (mkRemovedOptionModule [ "fonts" "fontconfig" "hinting" "style" ] "")
     (mkRemovedOptionModule [ "fonts" "fontconfig" "forceAutohint" ] "")
     (mkRemovedOptionModule [ "fonts" "fontconfig" "renderMonoTTFAsBitmap" ] "")
-  ];
+  ] ++ lib.forEach [ "enable" "substitutions" "preset" ]
+     (opt: lib.mkRemovedOptionModule [ "fonts" "fontconfig" "ultimate" "${opt}" ] ''
+       The fonts.fontconfig.ultimate module and configuration is obsolete.
+       The repository has since been archived and activity has ceased.
+       https://github.com/bohoomil/fontconfig-ultimate/issues/171.
+       No action should be needed for font configuration, as the fonts.fontconfig
+       module is already used by default.
+     '');
 
   options = {
 
@@ -483,7 +450,7 @@ in
       environment.systemPackages    = [ pkgs.fontconfig ];
       environment.etc.fonts.source  = "${fontconfigEtc}/etc/fonts/";
     })
-    (mkIf (cfg.enable && !cfg.penultimate.enable) {
+    (mkIf cfg.enable {
       fonts.fontconfig.confPackages = [ confPkg ];
     })
   ];
diff --git a/nixos/modules/config/i18n.nix b/nixos/modules/config/i18n.nix
index cc2ddda9d32..feb76581a72 100644
--- a/nixos/modules/config/i18n.nix
+++ b/nixos/modules/config/i18n.nix
@@ -68,7 +68,8 @@ with lib;
   config = {
 
     environment.systemPackages =
-      optional (config.i18n.supportedLocales != []) config.i18n.glibcLocales;
+      # We increase the priority a little, so that plain glibc in systemPackages can't win.
+      optional (config.i18n.supportedLocales != []) (lib.setPrio (-1) config.i18n.glibcLocales);
 
     environment.sessionVariables =
       { LANG = config.i18n.defaultLocale;
diff --git a/nixos/modules/config/ldap.nix b/nixos/modules/config/ldap.nix
index 4c8b527676b..1a5dbcd4e26 100644
--- a/nixos/modules/config/ldap.nix
+++ b/nixos/modules/config/ldap.nix
@@ -244,6 +244,10 @@ in
       if cfg.daemon.enable then nss_pam_ldapd else nss_ldap
     );
 
+    system.nssDatabases.group = optional cfg.nsswitch "ldap";
+    system.nssDatabases.passwd = optional cfg.nsswitch "ldap";
+    system.nssDatabases.shadow = optional cfg.nsswitch "ldap";
+
     users = mkIf cfg.daemon.enable {
       groups.nslcd = {
         gid = config.ids.gids.nslcd;
diff --git a/nixos/modules/config/networking.nix b/nixos/modules/config/networking.nix
index 03944de8249..4cb7d81c997 100644
--- a/nixos/modules/config/networking.nix
+++ b/nixos/modules/config/networking.nix
@@ -8,9 +8,6 @@ let
 
   cfg = config.networking;
 
-  localhostMapped4 = cfg.hosts ? "127.0.0.1" && elem "localhost" cfg.hosts."127.0.0.1";
-  localhostMapped6 = cfg.hosts ? "::1"       && elem "localhost" cfg.hosts."::1";
-
   localhostMultiple = any (elem "localhost") (attrValues (removeAttrs cfg.hosts [ "127.0.0.1" "::1" ]));
 
 in
@@ -147,12 +144,6 @@ in
   config = {
 
     assertions = [{
-      assertion = localhostMapped4;
-      message = ''`networking.hosts` doesn't map "127.0.0.1" to "localhost"'';
-    } {
-      assertion = !cfg.enableIPv6 || localhostMapped6;
-      message = ''`networking.hosts` doesn't map "::1" to "localhost"'';
-    } {
       assertion = !localhostMultiple;
       message = ''
         `networking.hosts` maps "localhost" to something other than "127.0.0.1"
@@ -161,22 +152,34 @@ in
       '';
     }];
 
-    networking.hosts = {
-      "127.0.0.1" = [ "localhost" ];
-    } // optionalAttrs (cfg.hostName != "") {
-      "127.0.1.1" = [ cfg.hostName ];
+    # These entries are required for "hostname -f" and to resolve both the
+    # hostname and FQDN correctly:
+    networking.hosts = let
+      hostnames = # Note: The FQDN (canonical hostname) has to come first:
+        optional (cfg.hostName != "" && cfg.domain != null) "${cfg.hostName}.${cfg.domain}"
+        ++ optional (cfg.hostName != "") cfg.hostName; # Then the hostname (without the domain)
+    in {
+      "127.0.0.2" = hostnames;
     } // optionalAttrs cfg.enableIPv6 {
-      "::1" = [ "localhost" ];
+      "::1" = hostnames;
     };
 
     networking.hostFiles = let
+      # Note: localhostHosts has to appear first in /etc/hosts so that 127.0.0.1
+      # resolves back to "localhost" (as some applications assume) instead of
+      # the FQDN! By default "networking.hosts" also contains entries for the
+      # FQDN so that e.g. "hostname -f" works correctly.
+      localhostHosts = pkgs.writeText "localhost-hosts" ''
+        127.0.0.1 localhost
+        ${optionalString cfg.enableIPv6 "::1 localhost"}
+      '';
       stringHosts =
         let
           oneToString = set: ip: ip + " " + concatStringsSep " " set.${ip} + "\n";
           allToString = set: concatMapStrings (oneToString set) (attrNames set);
         in pkgs.writeText "string-hosts" (allToString (filterAttrs (_: v: v != []) cfg.hosts));
       extraHosts = pkgs.writeText "extra-hosts" cfg.extraHosts;
-    in mkBefore [ stringHosts extraHosts ];
+    in mkBefore [ localhostHosts stringHosts extraHosts ];
 
     environment.etc =
       { # /etc/services: TCP/UDP port assignments.
diff --git a/nixos/modules/config/no-x-libs.nix b/nixos/modules/config/no-x-libs.nix
index 873b8073fed..941ab78f863 100644
--- a/nixos/modules/config/no-x-libs.nix
+++ b/nixos/modules/config/no-x-libs.nix
@@ -27,6 +27,7 @@ with lib;
     fonts.fontconfig.enable = false;
 
     nixpkgs.overlays = singleton (const (super: {
+      cairo = super.cairo.override { x11Support = false; };
       dbus = super.dbus.override { x11Support = false; };
       networkmanager-fortisslvpn = super.networkmanager-fortisslvpn.override { withGnome = false; };
       networkmanager-l2tp = super.networkmanager-l2tp.override { withGnome = false; };
@@ -35,6 +36,7 @@ with lib;
       networkmanager-vpnc = super.networkmanager-vpnc.override { withGnome = false; };
       networkmanager-iodine = super.networkmanager-iodine.override { withGnome = false; };
       gobject-introspection = super.gobject-introspection.override { x11Support = false; };
+      qemu = super.qemu.override { gtkSupport = false; spiceSupport = false; sdlSupport = false; };
     }));
   };
 }
diff --git a/nixos/modules/config/nsswitch.nix b/nixos/modules/config/nsswitch.nix
index 22ddb3490c8..d19d35a4890 100644
--- a/nixos/modules/config/nsswitch.nix
+++ b/nixos/modules/config/nsswitch.nix
@@ -4,34 +4,7 @@
 
 with lib;
 
-let
-
-  # only with nscd up and running we can load NSS modules that are not integrated in NSS
-  canLoadExternalModules = config.services.nscd.enable;
-  # XXX Move these to their respective modules
-  nssmdns = canLoadExternalModules && config.services.avahi.nssmdns;
-  nsswins = canLoadExternalModules && config.services.samba.nsswins;
-  ldap = canLoadExternalModules && (config.users.ldap.enable && config.users.ldap.nsswitch);
-
-  hostArray = mkMerge [
-    (mkBefore [ "files" ])
-    (mkIf nssmdns [ "mdns_minimal [NOTFOUND=return]" ])
-    (mkIf nsswins [ "wins" ])
-    (mkAfter [ "dns" ])
-    (mkIf nssmdns (mkOrder 1501 [ "mdns" ])) # 1501 to ensure it's after dns
-  ];
-
-  passwdArray = mkMerge [
-    (mkBefore [ "files" ])
-    (mkIf ldap [ "ldap" ])
-  ];
-
-  shadowArray = mkMerge [
-    (mkBefore [ "files" ])
-    (mkIf ldap [ "ldap" ])
-  ];
-
-in {
+{
   options = {
 
     # NSS modules.  Hacky!
@@ -122,9 +95,11 @@ in {
   config = {
     assertions = [
       {
-        # generic catch if the NixOS module adding to nssModules does not prevent it with specific message.
-        assertion = config.system.nssModules.path != "" -> canLoadExternalModules;
-        message = "Loading NSS modules from path ${config.system.nssModules.path} requires nscd being enabled.";
+        # Prevent users from disabling nscd, with nssModules being set.
+        # If disabling nscd is really necessary, it's still possible to opt out
+        # by forcing config.system.nssModules to [].
+        assertion = config.system.nssModules.path != "" -> config.services.nscd.enable;
+        message = "Loading NSS modules from system.nssModules (${config.system.nssModules.path}), requires services.nscd.enable being set to true.";
       }
     ];
 
@@ -145,10 +120,13 @@ in {
     '';
 
     system.nssDatabases = {
-      passwd = passwdArray;
-      group = passwdArray;
-      shadow = shadowArray;
-      hosts = hostArray;
+      passwd = mkBefore [ "files" ];
+      group = mkBefore [ "files" ];
+      shadow = mkBefore [ "files" ];
+      hosts = mkMerge [
+        (mkBefore [ "files" ])
+        (mkAfter [ "dns" ])
+      ];
       services = mkBefore [ "files" ];
     };
   };
diff --git a/nixos/modules/config/power-management.nix b/nixos/modules/config/power-management.nix
index 64cdf50f141..cc0ff732ffa 100644
--- a/nixos/modules/config/power-management.nix
+++ b/nixos/modules/config/power-management.nix
@@ -94,7 +94,7 @@ in
         after = [ "suspend.target" "hibernate.target" "hybrid-sleep.target" ];
         script =
           ''
-            ${config.systemd.package}/bin/systemctl try-restart post-resume.target
+            /run/current-system/systemd/bin/systemctl try-restart post-resume.target
             ${cfg.resumeCommands}
             ${cfg.powerUpCommands}
           '';
diff --git a/nixos/modules/config/resolvconf.nix b/nixos/modules/config/resolvconf.nix
index cc202bca6c4..cd0ed491383 100644
--- a/nixos/modules/config/resolvconf.nix
+++ b/nixos/modules/config/resolvconf.nix
@@ -21,7 +21,7 @@ let
     '' + optionalString config.services.nscd.enable ''
       # Invalidate the nscd cache whenever resolv.conf is
       # regenerated.
-      libc_restart='${pkgs.systemd}/bin/systemctl try-restart --no-block nscd.service 2> /dev/null'
+      libc_restart='/run/current-system/systemd/bin/systemctl try-restart --no-block nscd.service 2> /dev/null'
     '' + optionalString (length resolvconfOptions > 0) ''
       # Options as described in resolv.conf(5)
       resolv_conf_options='${concatStringsSep " " resolvconfOptions}'
diff --git a/nixos/modules/config/system-path.nix b/nixos/modules/config/system-path.nix
index 4100ec89701..b3c5c6f93f3 100644
--- a/nixos/modules/config/system-path.nix
+++ b/nixos/modules/config/system-path.nix
@@ -8,8 +8,7 @@ with lib;
 let
 
   requiredPackages = map (pkg: setPrio ((pkg.meta.priority or 5) + 3) pkg)
-    [ config.nix.package
-      pkgs.acl
+    [ pkgs.acl
       pkgs.attr
       pkgs.bashInteractive # bash with ncurses support
       pkgs.bzip2
@@ -33,16 +32,13 @@ let
       pkgs.nano
       pkgs.ncurses
       pkgs.netcat
-      pkgs.nix-info
       config.programs.ssh.package
-      pkgs.perl
       pkgs.procps
-      pkgs.rsync
-      pkgs.strace
       pkgs.su
       pkgs.time
       pkgs.utillinux
-      pkgs.which # 88K size
+      pkgs.which
+      pkgs.zstd
     ];
 
 in
diff --git a/nixos/modules/config/update-users-groups.pl b/nixos/modules/config/update-users-groups.pl
index 15e448b787a..e1c7a46e430 100644
--- a/nixos/modules/config/update-users-groups.pl
+++ b/nixos/modules/config/update-users-groups.pl
@@ -281,3 +281,58 @@ foreach my $u (values %usersOut) {
 }
 
 updateFile("/etc/shadow", \@shadowNew, 0600);
+
+# Rewrite /etc/subuid & /etc/subgid to include default container mappings
+
+my $subUidMapFile = "/var/lib/nixos/auto-subuid-map";
+my $subUidMap = -e $subUidMapFile ? decode_json(read_file($subUidMapFile)) : {};
+
+my (%subUidsUsed, %subUidsPrevUsed);
+
+$subUidsPrevUsed{$_} = 1 foreach values %{$subUidMap};
+
+sub allocSubUid {
+    my ($name, @rest) = @_;
+
+    # TODO: No upper bounds?
+    my ($min, $max, $up) = (100000, 100000 * 100, 1);
+    my $prevId = $subUidMap->{$name};
+    if (defined $prevId && !defined $subUidsUsed{$prevId}) {
+        $subUidsUsed{$prevId} = 1;
+        return $prevId;
+    }
+
+    my $id = allocId(\%subUidsUsed, \%subUidsPrevUsed, $min, $max, $up, sub { my ($uid) = @_; getpwuid($uid) });
+    my $offset = $id - 100000;
+    my $count = $offset * 65536;
+    my $subordinate = 100000 + $count;
+    return $subordinate;
+}
+
+my @subGids;
+my @subUids;
+foreach my $u (values %usersOut) {
+    my $name = $u->{name};
+
+    foreach my $range (@{$u->{subUidRanges}}) {
+        my $value = join(":", ($name, $range->{startUid}, $range->{count}));
+        push @subUids, $value;
+    }
+
+    foreach my $range (@{$u->{subGidRanges}}) {
+        my $value = join(":", ($name, $range->{startGid}, $range->{count}));
+        push @subGids, $value;
+    }
+
+    if($u->{isNormalUser}) {
+        my $subordinate = allocSubUid($name);
+        $subUidMap->{$name} = $subordinate;
+        my $value = join(":", ($name, $subordinate, 65536));
+        push @subUids, $value;
+        push @subGids, $value;
+    }
+}
+
+updateFile("/etc/subuid", join("\n", @subUids) . "\n");
+updateFile("/etc/subgid", join("\n", @subGids) . "\n");
+updateFile($subUidMapFile, encode_json($subUidMap) . "\n");
diff --git a/nixos/modules/config/users-groups.nix b/nixos/modules/config/users-groups.nix
index 141e43fec39..56b7af98b61 100644
--- a/nixos/modules/config/users-groups.nix
+++ b/nixos/modules/config/users-groups.nix
@@ -6,6 +6,16 @@ let
   ids = config.ids;
   cfg = config.users;
 
+  # Check whether a password hash will allow login.
+  allowsLogin = hash:
+    hash == "" # login without password
+    || !(lib.elem hash
+      [ null   # password login disabled
+        "!"    # password login disabled
+        "!!"   # a variant of "!"
+        "*"    # password unset
+      ]);
+
   passwordDescription = ''
     The options <option>hashedPassword</option>,
     <option>password</option> and <option>passwordFile</option>
@@ -25,8 +35,19 @@ let
   '';
 
   hashedPasswordDescription = ''
-    To generate hashed password install <literal>mkpasswd</literal>
+    To generate a hashed password install the <literal>mkpasswd</literal>
     package and run <literal>mkpasswd -m sha-512</literal>.
+
+    If set to an empty string (<literal>""</literal>), this user will
+    be able to log in without being asked for a password (but not via remote
+    services such as SSH, or indirectly via <command>su</command> or
+    <command>sudo</command>). This should only be used for e.g. bootable
+    live systems. Note: this is different from setting an empty password,
+    which ca be achieved using <option>users.users.&lt;name?&gt;.password</option>.
+
+    If set to <literal>null</literal> (default) this user will not
+    be able to log in using a password (i.e. via <command>login</command>
+    command).
   '';
 
   userOpts = { name, config, ... }: {
@@ -354,18 +375,6 @@ let
     };
   };
 
-  mkSubuidEntry = user: concatStrings (
-    map (range: "${user.name}:${toString range.startUid}:${toString range.count}\n")
-      user.subUidRanges);
-
-  subuidFile = concatStrings (map mkSubuidEntry (attrValues cfg.users));
-
-  mkSubgidEntry = user: concatStrings (
-    map (range: "${user.name}:${toString range.startGid}:${toString range.count}\n")
-        user.subGidRanges);
-
-  subgidFile = concatStrings (map mkSubgidEntry (attrValues cfg.users));
-
   idsAreUnique = set: idAttr: !(fold (name: args@{ dup, acc }:
     let
       id = builtins.toString (builtins.getAttr idAttr (builtins.getAttr name set));
@@ -385,6 +394,7 @@ let
       { inherit (u)
           name uid group description home createHome isSystemUser
           password passwordFile hashedPassword
+          isNormalUser subUidRanges subGidRanges
           initialPassword initialHashedPassword;
         shell = utils.toShellPath u.shell;
       }) cfg.users;
@@ -406,6 +416,12 @@ in {
   imports = [
     (mkAliasOptionModule [ "users" "extraUsers" ] [ "users" "users" ])
     (mkAliasOptionModule [ "users" "extraGroups" ] [ "users" "groups" ])
+    (mkChangedOptionModule
+      [ "security" "initialRootPassword" ]
+      [ "users" "users" "root" "initialHashedPassword" ]
+      (cfg: if cfg.security.initialRootPassword == "!"
+            then null
+            else cfg.security.initialRootPassword))
   ];
 
   ###### interface
@@ -477,14 +493,6 @@ in {
       '';
     };
 
-    # FIXME: obsolete - will remove.
-    security.initialRootPassword = mkOption {
-      type = types.str;
-      default = "!";
-      example = "";
-      visible = false;
-    };
-
   };
 
 
@@ -499,7 +507,6 @@ in {
         home = "/root";
         shell = mkDefault cfg.defaultUserShell;
         group = "root";
-        initialHashedPassword = mkDefault config.security.initialRootPassword;
       };
       nobody = {
         uid = ids.uids.nobody;
@@ -549,16 +556,7 @@ in {
     # Install all the user shells
     environment.systemPackages = systemShells;
 
-    environment.etc = {
-      subuid = {
-        text = subuidFile;
-        mode = "0644";
-      };
-      subgid = {
-        text = subgidFile;
-        mode = "0644";
-      };
-    } // (mapAttrs' (name: { packages, ... }: {
+    environment.etc = (mapAttrs' (name: { packages, ... }: {
       name = "profiles/per-user/${name}";
       value.source = pkgs.buildEnv {
         name = "user-environment";
@@ -583,22 +581,65 @@ in {
         # password or an SSH authorized key. Privileged accounts are
         # root and users in the wheel group.
         assertion = !cfg.mutableUsers ->
-          any id (mapAttrsToList (name: cfg:
+          any id ((mapAttrsToList (name: cfg:
             (name == "root"
              || cfg.group == "wheel"
              || elem "wheel" cfg.extraGroups)
             &&
-            ((cfg.hashedPassword != null && cfg.hashedPassword != "!")
+            (allowsLogin cfg.hashedPassword
              || cfg.password != null
              || cfg.passwordFile != null
              || cfg.openssh.authorizedKeys.keys != []
              || cfg.openssh.authorizedKeys.keyFiles != [])
-          ) cfg.users);
+          ) cfg.users) ++ [
+            config.security.googleOsLogin.enable
+          ]);
         message = ''
           Neither the root account nor any wheel user has a password or SSH authorized key.
           You must set one to prevent being locked out of your system.'';
       }
-    ];
+    ] ++ flip mapAttrsToList cfg.users (name: user:
+      {
+        assertion = (user.hashedPassword != null)
+                    -> (builtins.match ".*:.*" user.hashedPassword == null);
+        message = ''
+          The password hash of user "${name}" contains a ":" character.
+          This is invalid and would break the login system because the fields
+          of /etc/shadow (file where hashes are stored) are colon-separated.
+          Please check the value of option `users.users."${name}".hashedPassword`.'';
+      }
+    );
+
+    warnings =
+      builtins.filter (x: x != null) (
+        flip mapAttrsToList cfg.users (name: user:
+        # This regex matches a subset of the Modular Crypto Format (MCF)[1]
+        # informal standard. Since this depends largely on the OS or the
+        # specific implementation of crypt(3) we only support the (sane)
+        # schemes implemented by glibc and BSDs. In particular the original
+        # DES hash is excluded since, having no structure, it would validate
+        # common mistakes like typing the plaintext password.
+        #
+        # [1]: https://en.wikipedia.org/wiki/Crypt_(C)
+        let
+          sep = "\\$";
+          base64 = "[a-zA-Z0-9./]+";
+          id = "[a-z0-9-]+";
+          value = "[a-zA-Z0-9/+.-]+";
+          options = "${id}(=${value})?(,${id}=${value})*";
+          scheme  = "${id}(${sep}${options})?";
+          content = "${base64}${sep}${base64}";
+          mcf = "^${sep}${scheme}${sep}${content}$";
+        in
+        if (allowsLogin user.hashedPassword
+            && user.hashedPassword != ""  # login without password
+            && builtins.match mcf user.hashedPassword == null)
+        then ''
+          The password hash of user "${name}" may be invalid. You must set a
+          valid hash or the user will be locked out of their account. Please
+          check the value of option `users.users."${name}".hashedPassword`.''
+        else null
+      ));
 
   };
 
diff --git a/nixos/modules/config/zram.nix b/nixos/modules/config/zram.nix
index 5d411c73a56..5e9870bf6b1 100644
--- a/nixos/modules/config/zram.nix
+++ b/nixos/modules/config/zram.nix
@@ -91,7 +91,7 @@ in
       };
 
       algorithm = mkOption {
-        default = "lzo";
+        default = "zstd";
         example = "lz4";
         type = with types; either (enum [ "lzo" "lz4" "zstd" ]) str;
         description = ''
diff --git a/nixos/modules/hardware/bladeRF.nix b/nixos/modules/hardware/bladeRF.nix
index 92544347714..35b74b8382e 100644
--- a/nixos/modules/hardware/bladeRF.nix
+++ b/nixos/modules/hardware/bladeRF.nix
@@ -25,4 +25,4 @@ in
     services.udev.packages = [ pkgs.libbladeRF ];
     users.groups.bladerf = {};
   };
-}
\ No newline at end of file
+}
diff --git a/nixos/modules/hardware/ckb-next.nix b/nixos/modules/hardware/ckb-next.nix
index fe0ca9f26d5..6932be1c54c 100644
--- a/nixos/modules/hardware/ckb-next.nix
+++ b/nixos/modules/hardware/ckb-next.nix
@@ -43,7 +43,6 @@ in
         serviceConfig = {
           ExecStart = "${cfg.package}/bin/ckb-next-daemon ${optionalString (cfg.gid != null) "--gid=${builtins.toString cfg.gid}"}";
           Restart = "on-failure";
-          StandardOutput = "syslog";
         };
       };
     };
diff --git a/nixos/modules/hardware/device-tree.nix b/nixos/modules/hardware/device-tree.nix
index cf553497c89..b3f1dda98c8 100644
--- a/nixos/modules/hardware/device-tree.nix
+++ b/nixos/modules/hardware/device-tree.nix
@@ -22,11 +22,22 @@ in {
           example = literalExample "pkgs.device-tree_rpi";
           type = types.path;
           description = ''
-            The package containing the base device-tree (.dtb) to boot. Contains
+            The path containing the base device-tree (.dtb) to boot. Contains
             device trees bundled with the Linux kernel by default.
           '';
         };
 
+        name = mkOption {
+          default = null;
+          example = "some-dtb.dtb";
+          type = types.nullOr types.str;
+          description = ''
+            The name of an explicit dtb to be loaded, relative to the dtb base.
+            Useful in extlinux scenarios if the bootloader doesn't pick the
+            right .dtb file from FDTDIR.
+          '';
+        };
+
         overlays = mkOption {
           default = [];
           example = literalExample
diff --git a/nixos/modules/hardware/logitech.nix b/nixos/modules/hardware/logitech.nix
index d6f43bdddcc..3ebe6aacf5d 100644
--- a/nixos/modules/hardware/logitech.nix
+++ b/nixos/modules/hardware/logitech.nix
@@ -5,24 +5,92 @@ with lib;
 let
   cfg = config.hardware.logitech;
 
-in {
+  vendor = "046d";
+
+  daemon = "g15daemon";
+
+in
+{
+  imports = [
+    (mkRenamedOptionModule [ "hardware" "logitech" "enable" ] [ "hardware" "logitech" "wireless" "enable" ])
+    (mkRenamedOptionModule [ "hardware" "logitech" "enableGraphical" ] [ "hardware" "logitech" "wireless" "enableGraphical" ])
+  ];
+
   options.hardware.logitech = {
-    enable = mkEnableOption "Logitech Devices";
 
-    enableGraphical = mkOption {
-      type = types.bool;
-      default = false;
-      description = "Enable graphical support applications.";
+    lcd = {
+      enable = mkEnableOption "Logitech LCD Devices";
+
+      startWhenNeeded = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Only run the service when an actual supported device is plugged.
+        '';
+      };
+
+      devices = mkOption {
+        type = types.listOf types.str;
+        default = [ "0a07" "c222" "c225" "c227" "c251" ];
+        description = ''
+          List of USB device ids supported by g15daemon.
+          </para>
+          <para>
+          You most likely do not need to change this.
+        '';
+      };
+    };
+
+    wireless = {
+      enable = mkEnableOption "Logitech Wireless Devices";
+
+      enableGraphical = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable graphical support applications.";
+      };
     };
   };
 
-  config = lib.mkIf cfg.enable {
-    environment.systemPackages = [
-      pkgs.ltunify
-    ] ++ lib.optional cfg.enableGraphical pkgs.solaar;
+  config = lib.mkIf (cfg.wireless.enable || cfg.lcd.enable) {
+    environment.systemPackages = []
+      ++ lib.optional cfg.wireless.enable pkgs.ltunify
+      ++ lib.optional cfg.wireless.enableGraphical pkgs.solaar;
+
+    services.udev = {
+      # ltunifi and solaar both provide udev rules but the most up-to-date have been split
+      # out into a dedicated derivation
 
-    # ltunifi and solaar both provide udev rules but the most up-to-date have been split
-    # out into a dedicated derivation
-    services.udev.packages = with pkgs; [ logitech-udev-rules ];
+      packages = []
+      ++ lib.optional cfg.wireless.enable pkgs.logitech-udev-rules
+      ++ lib.optional cfg.lcd.enable pkgs.g15daemon;
+
+      extraRules = ''
+        # nixos: hardware.logitech.lcd
+      '' + lib.concatMapStringsSep "\n" (
+        dev:
+          ''ACTION=="add", SUBSYSTEMS=="usb", ATTRS{idVendor}=="${vendor}", ATTRS{idProduct}=="${dev}", TAG+="systemd", ENV{SYSTEMD_WANTS}+="${daemon}.service"''
+      ) cfg.lcd.devices;
+    };
+
+    systemd.services."${daemon}" = lib.mkIf cfg.lcd.enable {
+      description = "Logitech LCD Support Daemon";
+      documentation = [ "man:g15daemon(1)" ];
+      wantedBy = lib.mkIf (! cfg.lcd.startWhenNeeded) "multi-user.target";
+
+      serviceConfig = {
+        Type = "forking";
+        ExecStart = "${pkgs.g15daemon}/bin/g15daemon";
+        # we patch it to write to /run/g15daemon/g15daemon.pid instead of
+        # /run/g15daemon.pid so systemd will do the cleanup for us.
+        PIDFile = "/run/${daemon}/g15daemon.pid";
+        PrivateTmp = true;
+        PrivateNetwork = true;
+        ProtectHome = "tmpfs";
+        ProtectSystem = "full"; # strict doesn't work
+        RuntimeDirectory = daemon;
+        Restart = "on-failure";
+      };
+    };
   };
 }
diff --git a/nixos/modules/hardware/onlykey.nix b/nixos/modules/hardware/onlykey.nix
index b6820fe0191..07358c8a878 100644
--- a/nixos/modules/hardware/onlykey.nix
+++ b/nixos/modules/hardware/onlykey.nix
@@ -26,7 +26,7 @@ with lib;
   ####### implementation
 
   config = mkIf config.hardware.onlykey.enable {
-    services.udev.extraRules = builtin.readFile ./onlykey.udev;
+    services.udev.extraRules = builtins.readFile ./onlykey.udev;
   };
 
 
diff --git a/nixos/modules/hardware/printers.nix b/nixos/modules/hardware/printers.nix
index 56b91933477..752de41f26d 100644
--- a/nixos/modules/hardware/printers.nix
+++ b/nixos/modules/hardware/printers.nix
@@ -84,7 +84,7 @@ in {
             model = mkOption {
               type = types.str;
               example = literalExample ''
-                gutenprint.''${lib.version.majorMinor (lib.getVersion pkgs.cups)}://brother-hl-5140/expert
+                gutenprint.''${lib.versions.majorMinor (lib.getVersion pkgs.gutenprint)}://brother-hl-5140/expert
               '';
               description = ''
                 Location of the ppd driver file for the printer.
diff --git a/nixos/modules/hardware/tuxedo-keyboard.nix b/nixos/modules/hardware/tuxedo-keyboard.nix
index 898eed24493..97af7c61f3c 100644
--- a/nixos/modules/hardware/tuxedo-keyboard.nix
+++ b/nixos/modules/hardware/tuxedo-keyboard.nix
@@ -2,7 +2,7 @@
 
 with lib;
 
-let 
+let
   cfg = config.hardware.tuxedo-keyboard;
   tuxedo-keyboard = config.boot.kernelPackages.tuxedo-keyboard;
 in
@@ -27,7 +27,7 @@ in
       '';
     };
 
-    config = mkIf cfg.enable 
+    config = mkIf cfg.enable
     {
       boot.kernelModules = ["tuxedo_keyboard"];
       boot.extraModulePackages = [ tuxedo-keyboard ];
diff --git a/nixos/modules/hardware/video/hidpi.nix b/nixos/modules/hardware/video/hidpi.nix
new file mode 100644
index 00000000000..ac72b652504
--- /dev/null
+++ b/nixos/modules/hardware/video/hidpi.nix
@@ -0,0 +1,16 @@
+{ lib, pkgs, config, ...}:
+with lib;
+
+{
+  options.hardware.video.hidpi.enable = mkEnableOption "Font/DPI configuration optimized for HiDPI displays";
+
+  config = mkIf config.hardware.video.hidpi.enable {
+    console.font = lib.mkDefault "${pkgs.terminus_font}/share/consolefonts/ter-v32n.psf.gz";
+
+    # Needed when typing in passwords for full disk encryption
+    console.earlySetup = mkDefault true;
+    boot.loader.systemd-boot.consoleMode = mkDefault "1";
+
+    # TODO Find reasonable defaults X11 & wayland
+  };
+}
diff --git a/nixos/modules/hardware/video/uvcvideo/default.nix b/nixos/modules/hardware/video/uvcvideo/default.nix
index 7e3e94fdf2b..cf6aa052abb 100644
--- a/nixos/modules/hardware/video/uvcvideo/default.nix
+++ b/nixos/modules/hardware/video/uvcvideo/default.nix
@@ -26,7 +26,7 @@ in
           Whether to enable <command>uvcvideo</command> dynamic controls.
 
           Note that enabling this brings the <command>uvcdynctrl</command> tool
-          into your environement and register all dynamic controls from
+          into your environment and register all dynamic controls from
           specified <command>packages</command> to the <command>uvcvideo</command> driver.
         '';
       };
diff --git a/nixos/modules/hardware/xpadneo.nix b/nixos/modules/hardware/xpadneo.nix
new file mode 100644
index 00000000000..d504697e61f
--- /dev/null
+++ b/nixos/modules/hardware/xpadneo.nix
@@ -0,0 +1,29 @@
+{ config, lib, ... }:
+
+with lib;
+let
+  cfg = config.hardware.xpadneo;
+in
+{
+  options.hardware.xpadneo = {
+    enable = mkEnableOption "the xpadneo driver for Xbox One wireless controllers";
+  };
+
+  config = mkIf cfg.enable {
+    boot = {
+      # Must disable Enhanced Retransmission Mode to support bluetooth pairing
+      # https://wiki.archlinux.org/index.php/Gamepad#Connect_Xbox_Wireless_Controller_with_Bluetooth
+      extraModprobeConfig =
+        mkIf
+          config.hardware.bluetooth.enable
+          "options bluetooth disable_ertm=1";
+
+      extraModulePackages = with config.boot.kernelPackages; [ xpadneo ];
+      kernelModules = [ "hid_xpadneo" ];
+    };
+  };
+
+  meta = {
+    maintainers = with maintainers; [ metadark ];
+  };
+}
diff --git a/nixos/modules/i18n/input-method/ibus.nix b/nixos/modules/i18n/input-method/ibus.nix
index b4746b21b65..cf24ecf5863 100644
--- a/nixos/modules/i18n/input-method/ibus.nix
+++ b/nixos/modules/i18n/input-method/ibus.nix
@@ -64,7 +64,7 @@ in
     # Without dconf enabled it is impossible to use IBus
     programs.dconf.enable = true;
 
-    programs.dconf.profiles.ibus = "${ibusPackage}/etc/dconf/profile/ibus";
+    programs.dconf.packages = [ ibusPackage ];
 
     services.dbus.packages = [
       ibusAutostart
diff --git a/nixos/modules/i18n/input-method/uim.nix b/nixos/modules/i18n/input-method/uim.nix
index 7ad68bf851f..459294657e0 100644
--- a/nixos/modules/i18n/input-method/uim.nix
+++ b/nixos/modules/i18n/input-method/uim.nix
@@ -2,7 +2,7 @@
 
 with lib;
 
-let 
+let
   cfg = config.i18n.inputMethod.uim;
 in
 {
diff --git a/nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix b/nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix
index 3707c4b7ec6..8c98691116d 100644
--- a/nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix
+++ b/nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix
@@ -11,15 +11,17 @@ with lib;
 
   services.xserver.desktopManager.gnome3.enable = true;
 
-  services.xserver.displayManager.gdm = {
-    enable = true;
-    # autoSuspend makes the machine automatically suspend after inactivity.
-    # It's possible someone could/try to ssh'd into the machine and obviously
-    # have issues because it's inactive.
-    # See:
-    # * https://github.com/NixOS/nixpkgs/pull/63790
-    # * https://gitlab.gnome.org/GNOME/gnome-control-center/issues/22
-    autoSuspend = false;
+  services.xserver.displayManager = {
+    gdm = {
+      enable = true;
+      # autoSuspend makes the machine automatically suspend after inactivity.
+      # It's possible someone could/try to ssh'd into the machine and obviously
+      # have issues because it's inactive.
+      # See:
+      # * https://github.com/NixOS/nixpkgs/pull/63790
+      # * https://gitlab.gnome.org/GNOME/gnome-control-center/issues/22
+      autoSuspend = false;
+    };
     autoLogin = {
       enable = true;
       user = "nixos";
diff --git a/nixos/modules/installer/cd-dvd/installation-cd-graphical-plasma5.nix b/nixos/modules/installer/cd-dvd/installation-cd-graphical-plasma5.nix
index e76e06654ac..098c2b2870b 100644
--- a/nixos/modules/installer/cd-dvd/installation-cd-graphical-plasma5.nix
+++ b/nixos/modules/installer/cd-dvd/installation-cd-graphical-plasma5.nix
@@ -16,8 +16,8 @@ with lib;
     };
 
     # Automatically login as nixos.
-    displayManager.sddm = {
-      enable = true;
+    displayManager = {
+      sddm.enable = true;
       autoLogin = {
         enable = true;
         user = "nixos";
diff --git a/nixos/modules/installer/cd-dvd/iso-image.nix b/nixos/modules/installer/cd-dvd/iso-image.nix
index cce7cc235ec..405fbfa10db 100644
--- a/nixos/modules/installer/cd-dvd/iso-image.nix
+++ b/nixos/modules/installer/cd-dvd/iso-image.nix
@@ -413,10 +413,18 @@ in
       default = false;
       description = ''
         Whether the ISO image should be compressed using
-        <command>bzip2</command>.
+        <command>zstd</command>.
       '';
     };
 
+    isoImage.squashfsCompression = mkOption {
+      default = "xz -Xdict-size 100%";
+      description = ''
+        Compression settings to use for the squashfs nix store.
+      '';
+      example = "zstd -Xcompression-level 6";
+    };
+
     isoImage.edition = mkOption {
       default = "";
       description = ''
@@ -614,6 +622,7 @@ in
     # Create the squashfs image that contains the Nix store.
     system.build.squashfsStore = pkgs.callPackage ../../../lib/make-squashfs.nix {
       storeContents = config.isoImage.storeContents;
+      comp = config.isoImage.squashfsCompression;
     };
 
     # Individual files to be included on the CD, outside of the Nix
diff --git a/nixos/modules/installer/cd-dvd/sd-image-aarch64.nix b/nixos/modules/installer/cd-dvd/sd-image-aarch64.nix
index 2d34406a032..bef6cd2fb5a 100644
--- a/nixos/modules/installer/cd-dvd/sd-image-aarch64.nix
+++ b/nixos/modules/installer/cd-dvd/sd-image-aarch64.nix
@@ -2,12 +2,6 @@
 # nix-build nixos -I nixos-config=nixos/modules/installer/cd-dvd/sd-image-aarch64.nix -A config.system.build.sdImage
 { config, lib, pkgs, ... }:
 
-let
-  extlinux-conf-builder =
-    import ../../system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.nix {
-      pkgs = pkgs.buildPackages;
-    };
-in
 {
   imports = [
     ../../profiles/base.nix
@@ -56,7 +50,7 @@ in
       '';
     populateRootCommands = ''
       mkdir -p ./files/boot
-      ${extlinux-conf-builder} -t 3 -c ${config.system.build.toplevel} -d ./files/boot
+      ${config.boot.loader.generic-extlinux-compatible.populateCmd} -c ${config.system.build.toplevel} -d ./files/boot
     '';
   };
 
diff --git a/nixos/modules/installer/cd-dvd/sd-image-armv7l-multiplatform.nix b/nixos/modules/installer/cd-dvd/sd-image-armv7l-multiplatform.nix
index 651d1a36dc1..d2ba611532e 100644
--- a/nixos/modules/installer/cd-dvd/sd-image-armv7l-multiplatform.nix
+++ b/nixos/modules/installer/cd-dvd/sd-image-armv7l-multiplatform.nix
@@ -2,12 +2,6 @@
 # nix-build nixos -I nixos-config=nixos/modules/installer/cd-dvd/sd-image-armv7l-multiplatform.nix -A config.system.build.sdImage
 { config, lib, pkgs, ... }:
 
-let
-  extlinux-conf-builder =
-    import ../../system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.nix {
-      pkgs = pkgs.buildPackages;
-    };
-in
 {
   imports = [
     ../../profiles/base.nix
@@ -53,7 +47,7 @@ in
       '';
     populateRootCommands = ''
       mkdir -p ./files/boot
-      ${extlinux-conf-builder} -t 3 -c ${config.system.build.toplevel} -d ./files/boot
+      ${config.boot.loader.generic-extlinux-compatible.populateCmd} -c ${config.system.build.toplevel} -d ./files/boot
     '';
   };
 
diff --git a/nixos/modules/installer/cd-dvd/sd-image-raspberrypi.nix b/nixos/modules/installer/cd-dvd/sd-image-raspberrypi.nix
index ba4127eaa0e..40a01f96177 100644
--- a/nixos/modules/installer/cd-dvd/sd-image-raspberrypi.nix
+++ b/nixos/modules/installer/cd-dvd/sd-image-raspberrypi.nix
@@ -2,12 +2,6 @@
 # nix-build nixos -I nixos-config=nixos/modules/installer/cd-dvd/sd-image-raspberrypi.nix -A config.system.build.sdImage
 { config, lib, pkgs, ... }:
 
-let
-  extlinux-conf-builder =
-    import ../../system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.nix {
-      pkgs = pkgs.buildPackages;
-    };
-in
 {
   imports = [
     ../../profiles/base.nix
@@ -42,7 +36,7 @@ in
       '';
     populateRootCommands = ''
       mkdir -p ./files/boot
-      ${extlinux-conf-builder} -t 3 -c ${config.system.build.toplevel} -d ./files/boot
+      ${config.boot.loader.generic-extlinux-compatible.populateCmd} -c ${config.system.build.toplevel} -d ./files/boot
     '';
   };
 
diff --git a/nixos/modules/installer/cd-dvd/sd-image-raspberrypi4.nix b/nixos/modules/installer/cd-dvd/sd-image-raspberrypi4.nix
index c545a1e7e24..79c835dc390 100644
--- a/nixos/modules/installer/cd-dvd/sd-image-raspberrypi4.nix
+++ b/nixos/modules/installer/cd-dvd/sd-image-raspberrypi4.nix
@@ -18,6 +18,7 @@
 
   sdImage = {
     firmwareSize = 128;
+    firmwarePartitionName = "NIXOS_BOOT";
     # This is a hack to avoid replicating config.txt from boot.loader.raspberryPi
     populateFirmwareCommands =
       "${config.system.build.installBootLoader} ${config.system.build.toplevel} -d ./firmware";
@@ -25,6 +26,12 @@
     populateRootCommands = "";
   };
 
+  fileSystems."/boot/firmware" = {
+    # This effectively "renames" the loaOf entry set in sd-image.nix
+    mountPoint = "/boot";
+    neededForBoot = true;
+  };
+
   # the installation media is also the installation target,
   # so we don't want to provide the installation configuration.nix.
   installer.cloneConfig = false;
diff --git a/nixos/modules/installer/cd-dvd/sd-image.nix b/nixos/modules/installer/cd-dvd/sd-image.nix
index 901c60befb6..ddad1116c94 100644
--- a/nixos/modules/installer/cd-dvd/sd-image.nix
+++ b/nixos/modules/installer/cd-dvd/sd-image.nix
@@ -63,12 +63,20 @@ in
       '';
     };
 
+    firmwarePartitionName = mkOption {
+      type = types.str;
+      default = "FIRMWARE";
+      description = ''
+        Name of the filesystem which holds the boot firmware.
+      '';
+    };
+
     rootPartitionUUID = mkOption {
       type = types.nullOr types.str;
       default = null;
       example = "14e19a7b-0ae0-484d-9d54-43bd6fdc20c7";
       description = ''
-        UUID for the main NixOS partition on the SD card.
+        UUID for the filesystem on the main NixOS partition on the SD card.
       '';
     };
 
@@ -91,7 +99,7 @@ in
     };
 
     populateRootCommands = mkOption {
-      example = literalExample "''\${extlinux-conf-builder} -t 3 -c \${config.system.build.toplevel} -d ./files/boot''";
+      example = literalExample "''\${config.boot.loader.generic-extlinux-compatible.populateCmd} -c \${config.system.build.toplevel} -d ./files/boot''";
       description = ''
         Shell commands to populate the ./files directory.
         All files in that directory are copied to the
@@ -105,7 +113,7 @@ in
       default = true;
       description = ''
         Whether the SD image should be compressed using
-        <command>bzip2</command>.
+        <command>zstd</command>.
       '';
     };
 
@@ -114,7 +122,7 @@ in
   config = {
     fileSystems = {
       "/boot/firmware" = {
-        device = "/dev/disk/by-label/FIRMWARE";
+        device = "/dev/disk/by-label/${config.sdImage.firmwarePartitionName}";
         fsType = "vfat";
         # Alternatively, this could be removed from the configuration.
         # The filesystem is not needed at runtime, it could be treated
@@ -130,10 +138,10 @@ in
     sdImage.storePaths = [ config.system.build.toplevel ];
 
     system.build.sdImage = pkgs.callPackage ({ stdenv, dosfstools, e2fsprogs,
-    mtools, libfaketime, utillinux, bzip2, zstd }: stdenv.mkDerivation {
+    mtools, libfaketime, utillinux, zstd }: stdenv.mkDerivation {
       name = config.sdImage.imageName;
 
-      nativeBuildInputs = [ dosfstools e2fsprogs mtools libfaketime utillinux bzip2 zstd ];
+      nativeBuildInputs = [ dosfstools e2fsprogs mtools libfaketime utillinux zstd ];
 
       inherit (config.sdImage) compressImage;
 
@@ -143,7 +151,7 @@ in
 
         echo "${pkgs.stdenv.buildPlatform.system}" > $out/nix-support/system
         if test -n "$compressImage"; then
-          echo "file sd-image $img.bz2" >> $out/nix-support/hydra-build-products
+          echo "file sd-image $img.zst" >> $out/nix-support/hydra-build-products
         else
           echo "file sd-image $img" >> $out/nix-support/hydra-build-products
         fi
@@ -178,7 +186,7 @@ in
         # Create a FAT32 /boot/firmware partition of suitable size into firmware_part.img
         eval $(partx $img -o START,SECTORS --nr 1 --pairs)
         truncate -s $((SECTORS * 512)) firmware_part.img
-        faketime "1970-01-01 00:00:00" mkfs.vfat -i ${config.sdImage.firmwarePartitionID} -n FIRMWARE firmware_part.img
+        faketime "1970-01-01 00:00:00" mkfs.vfat -i ${config.sdImage.firmwarePartitionID} -n ${config.sdImage.firmwarePartitionName} firmware_part.img
 
         # Populate the files intended for /boot/firmware
         mkdir firmware
@@ -190,7 +198,7 @@ in
         fsck.vfat -vn firmware_part.img
         dd conv=notrunc if=firmware_part.img of=$img seek=$START count=$SECTORS
         if test -n "$compressImage"; then
-            bzip2 $img
+            zstd -T$NIX_BUILD_CORES --rm $img
         fi
       '';
     }) {};
diff --git a/nixos/modules/installer/cd-dvd/system-tarball-pc-readme.txt b/nixos/modules/installer/cd-dvd/system-tarball-pc-readme.txt
index 84252f292c5..887bf60d0fb 100644
--- a/nixos/modules/installer/cd-dvd/system-tarball-pc-readme.txt
+++ b/nixos/modules/installer/cd-dvd/system-tarball-pc-readme.txt
@@ -63,7 +63,7 @@ Activate the system: look for a directory in nix/store similar to:
 Having found it, activate that nixos system *twice*:
   chroot . /nix/store/SOMETHING-nixos-SOMETHING/activate
   chroot . /nix/store/SOMETHING-nixos-SOMETHING/activate
-  
+
 This runs a 'hostname' command. Restore your old hostname with:
   hostname OLDHOSTNAME
 
diff --git a/nixos/modules/installer/tools/nix-fallback-paths.nix b/nixos/modules/installer/tools/nix-fallback-paths.nix
index 842976c3574..a15a2dbadb8 100644
--- a/nixos/modules/installer/tools/nix-fallback-paths.nix
+++ b/nixos/modules/installer/tools/nix-fallback-paths.nix
@@ -1,6 +1,6 @@
 {
-  x86_64-linux = "/nix/store/8928ygfyf9iassfrnj76v55s6zid58ja-nix-2.3.4";
-  i686-linux = "/nix/store/b5cx3nmba9ahx3wk5ybxa67k40pdpdxn-nix-2.3.4";
-  aarch64-linux = "/nix/store/p6j4mis6agdjlk4j0cyg7yh58wpm3kif-nix-2.3.4";
-  x86_64-darwin = "/nix/store/aizhr07dljmlbf17wfrj40x3s0b5iv3d-nix-2.3.4";
+  x86_64-linux = "/nix/store/4vz8sh9ngx34ivi0bw5hlycxdhvy5hvz-nix-2.3.7";
+  i686-linux = "/nix/store/dzxkg9lpp60bjmzvagns42vqlz3yq5kx-nix-2.3.7";
+  aarch64-linux = "/nix/store/cfvf8nl8mwyw817by5y8zd3s8pnf5m9f-nix-2.3.7";
+  x86_64-darwin = "/nix/store/5ira7xgs92inqz1x8l0n1wci4r79hnd0-nix-2.3.7";
 }
diff --git a/nixos/modules/installer/tools/nixos-build-vms/nixos-build-vms.sh b/nixos/modules/installer/tools/nixos-build-vms/nixos-build-vms.sh
index 25106733087..2a6c3ab1149 100644
--- a/nixos/modules/installer/tools/nixos-build-vms/nixos-build-vms.sh
+++ b/nixos/modules/installer/tools/nixos-build-vms/nixos-build-vms.sh
@@ -1,4 +1,4 @@
-#! @shell@ -e
+#! @runtimeShell@ -e
 
 # Shows the usage of this command to the user
 
diff --git a/nixos/modules/installer/tools/nixos-enter.sh b/nixos/modules/installer/tools/nixos-enter.sh
index 1fdd4627a90..c72ef6e9c28 100644
--- a/nixos/modules/installer/tools/nixos-enter.sh
+++ b/nixos/modules/installer/tools/nixos-enter.sh
@@ -1,4 +1,4 @@
-#! @shell@
+#! @runtimeShell@
 
 set -e
 
diff --git a/nixos/modules/installer/tools/nixos-generate-config.pl b/nixos/modules/installer/tools/nixos-generate-config.pl
index 422c405054d..c8303a6eb60 100644
--- a/nixos/modules/installer/tools/nixos-generate-config.pl
+++ b/nixos/modules/installer/tools/nixos-generate-config.pl
@@ -497,8 +497,8 @@ if (-f $fb_modes_file && -r $fb_modes_file) {
     $modes =~ m/([0-9]+)x([0-9]+)/;
     my $console_width = $1, my $console_height = $2;
     if ($console_width > 1920) {
-        push @attrs, "# High-DPI console";
-        push @attrs, 'console.font = lib.mkDefault "${pkgs.terminus_font}/share/consolefonts/ter-u28n.psf.gz";';
+        push @attrs, "# high-resolution display";
+        push @attrs, 'hardware.video.hidpi.enable = lib.mkDefault true;';
     }
 }
 
@@ -628,6 +628,7 @@ EOF
         write_file($fn, <<EOF);
 @configuration@
 EOF
+        print STDERR "For more hardware-specific settings, see https://github.com/NixOS/nixos-hardware"
     } else {
         print STDERR "warning: not overwriting existing $fn\n";
     }
diff --git a/nixos/modules/installer/tools/nixos-install.sh b/nixos/modules/installer/tools/nixos-install.sh
index 1bccbbfaf24..e0252befdfd 100644
--- a/nixos/modules/installer/tools/nixos-install.sh
+++ b/nixos/modules/installer/tools/nixos-install.sh
@@ -1,4 +1,4 @@
-#! @shell@
+#! @runtimeShell@
 
 set -e
 shopt -s nullglob
@@ -71,6 +71,17 @@ if ! test -e "$mountPoint"; then
     exit 1
 fi
 
+# Verify permissions are okay-enough
+checkPath="$(realpath "$mountPoint")"
+while [[ "$checkPath" != "/" ]]; do
+    mode="$(stat -c '%a' "$checkPath")"
+    if [[ "${mode: -1}" -lt "5" ]]; then
+        echo "path $checkPath should have permissions 755, but had permissions $mode. Consider running 'chmod o+rx $checkPath'."
+        exit 1
+    fi
+    checkPath="$(dirname "$checkPath")"
+done
+
 # Get the path of the NixOS configuration file.
 if [[ -z $NIXOS_CONFIG ]]; then
     NIXOS_CONFIG=$mountPoint/etc/nixos/configuration.nix
diff --git a/nixos/modules/installer/tools/nixos-rebuild.sh b/nixos/modules/installer/tools/nixos-rebuild.sh
index 354274478a3..437199bae1d 100644
--- a/nixos/modules/installer/tools/nixos-rebuild.sh
+++ b/nixos/modules/installer/tools/nixos-rebuild.sh
@@ -1,6 +1,6 @@
-#! @shell@
+#! @runtimeShell@
 
-if [ -x "@shell@" ]; then export SHELL="@shell@"; fi;
+if [ -x "@runtimeShell@" ]; then export SHELL="@runtimeShell@"; fi;
 
 set -e
 set -o pipefail
diff --git a/nixos/modules/installer/tools/nixos-version.sh b/nixos/modules/installer/tools/nixos-version.sh
index fb0fe26116a..f5e3f32b3c6 100644
--- a/nixos/modules/installer/tools/nixos-version.sh
+++ b/nixos/modules/installer/tools/nixos-version.sh
@@ -1,4 +1,4 @@
-#! @shell@
+#! @runtimeShell@
 
 case "$1" in
   -h|--help)
diff --git a/nixos/modules/installer/tools/tools.nix b/nixos/modules/installer/tools/tools.nix
index 11128621424..1582f049309 100644
--- a/nixos/modules/installer/tools/tools.nix
+++ b/nixos/modules/installer/tools/tools.nix
@@ -14,11 +14,13 @@ let
   nixos-build-vms = makeProg {
     name = "nixos-build-vms";
     src = ./nixos-build-vms/nixos-build-vms.sh;
+    inherit (pkgs) runtimeShell;
   };
 
   nixos-install = makeProg {
     name = "nixos-install";
     src = ./nixos-install.sh;
+    inherit (pkgs) runtimeShell;
     nix = config.nix.package.out;
     path = makeBinPath [ nixos-enter ];
   };
@@ -28,6 +30,7 @@ let
     makeProg {
       name = "nixos-rebuild";
       src = ./nixos-rebuild.sh;
+      inherit (pkgs) runtimeShell;
       nix = config.nix.package.out;
       nix_x86_64_linux = fallback.x86_64-linux;
       nix_i686_linux = fallback.i686-linux;
@@ -50,6 +53,7 @@ let
   nixos-version = makeProg {
     name = "nixos-version";
     src = ./nixos-version.sh;
+    inherit (pkgs) runtimeShell;
     inherit (config.system.nixos) version codeName revision;
     inherit (config.system) configurationRevision;
     json = builtins.toJSON ({
@@ -64,6 +68,7 @@ let
   nixos-enter = makeProg {
     name = "nixos-enter";
     src = ./nixos-enter.sh;
+    inherit (pkgs) runtimeShell;
   };
 
 in
diff --git a/nixos/modules/misc/documentation.nix b/nixos/modules/misc/documentation.nix
index 7ad4be9a02e..71a40b4f4d6 100644
--- a/nixos/modules/misc/documentation.nix
+++ b/nixos/modules/misc/documentation.nix
@@ -102,6 +102,16 @@ in
         '';
       };
 
+      man.generateCaches = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to generate the manual page index caches using
+          <literal>mandb(8)</literal>. This allows searching for a page or
+          keyword using utilities like <literal>apropos(1)</literal>.
+        '';
+      };
+
       info.enable = mkOption {
         type = types.bool;
         default = true;
@@ -187,7 +197,33 @@ in
       environment.systemPackages = [ pkgs.man-db ];
       environment.pathsToLink = [ "/share/man" ];
       environment.extraOutputsToInstall = [ "man" ] ++ optional cfg.dev.enable "devman";
-      environment.etc."man.conf".source = "${pkgs.man-db}/etc/man_db.conf";
+      environment.etc."man_db.conf".text =
+        let
+          manualPages = pkgs.buildEnv {
+            name = "man-paths";
+            paths = config.environment.systemPackages;
+            pathsToLink = [ "/share/man" ];
+            extraOutputsToInstall = ["man"];
+            ignoreCollisions = true;
+          };
+          manualCache = pkgs.runCommandLocal "man-cache" { }
+          ''
+            echo "MANDB_MAP ${manualPages}/share/man $out" > man.conf
+            ${pkgs.man-db}/bin/mandb -C man.conf -psc
+          '';
+        in
+        ''
+          # Manual pages paths for NixOS
+          MANPATH_MAP /run/current-system/sw/bin /run/current-system/sw/share/man
+          MANPATH_MAP /run/wrappers/bin          /run/current-system/sw/share/man
+
+          ${optionalString cfg.man.generateCaches ''
+          # Generated manual pages cache for NixOS (immutable)
+          MANDB_MAP /run/current-system/sw/share/man ${manualCache}
+          ''}
+          # Manual pages caches for NixOS
+          MANDB_MAP /run/current-system/sw/share/man /var/cache/man/nixos
+        '';
     })
 
     (mkIf cfg.info.enable {
diff --git a/nixos/modules/misc/ids.nix b/nixos/modules/misc/ids.nix
index 85e5534e906..394da9a3889 100644
--- a/nixos/modules/misc/ids.nix
+++ b/nixos/modules/misc/ids.nix
@@ -198,7 +198,7 @@ in
       bosun = 161;
       kubernetes = 162;
       peerflix = 163;
-      chronos = 164;
+      #chronos = 164; # removed 2020-08-15
       gitlab = 165;
       tox-bootstrapd = 166;
       cadvisor = 167;
@@ -239,7 +239,6 @@ in
       shout = 206;
       gateone = 207;
       namecoin = 208;
-      dnschain = 209;
       #lxd = 210; # unused
       kibana = 211;
       xtreemfs = 212;
@@ -248,7 +247,7 @@ in
       bepasty = 215;
       # pumpio = 216; # unused, removed 2018-02-24
       nm-openvpn = 217;
-      mathics = 218;
+      # mathics = 218; # unused, removed 2020-08-15
       ejabberd = 219;
       postsrsd = 220;
       opendkim = 221;
@@ -322,7 +321,7 @@ in
       monetdb = 290;
       restic = 291;
       openvpn = 292;
-      meguca = 293;
+      # meguca = 293; # removed 2020-08-21
       yarn = 294;
       hdfs = 295;
       mapred = 296;
@@ -346,6 +345,7 @@ in
       zoneminder = 314;
       paperless = 315;
       #mailman = 316;  # removed 2019-08-30
+      zigbee2mqtt = 317;
 
       # When adding a uid, make sure it doesn't match an existing gid. And don't use uids above 399!
 
@@ -549,7 +549,6 @@ in
       #shout = 206; #unused
       gateone = 207;
       namecoin = 208;
-      #dnschain = 209; #unused
       lxd = 210; # unused
       #kibana = 211;
       xtreemfs = 212;
@@ -623,7 +622,7 @@ in
       monetdb = 290;
       restic = 291;
       openvpn = 292;
-      meguca = 293;
+      # meguca = 293; # removed 2020-08-21
       yarn = 294;
       hdfs = 295;
       mapred = 296;
@@ -647,6 +646,7 @@ in
       zoneminder = 314;
       paperless = 315;
       #mailman = 316;  # removed 2019-08-30
+      zigbee2mqtt = 317;
 
       # When adding a gid, make sure it doesn't match an existing
       # uid. Users and groups with the same name should have equal
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 40904ef0c17..df68b8ceb04 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -1,7 +1,6 @@
 [
   ./config/debug-info.nix
   ./config/fonts/fontconfig.nix
-  ./config/fonts/fontconfig-penultimate.nix
   ./config/fonts/fontdir.nix
   ./config/fonts/fonts.nix
   ./config/fonts/ghostscript.nix
@@ -72,9 +71,11 @@
   ./hardware/video/capture/mwprocapture.nix
   ./hardware/video/bumblebee.nix
   ./hardware/video/displaylink.nix
+  ./hardware/video/hidpi.nix
   ./hardware/video/nvidia.nix
   ./hardware/video/uvcvideo/default.nix
   ./hardware/video/webcam/facetimehd.nix
+  ./hardware/xpadneo.nix
   ./i18n/input-method/default.nix
   ./i18n/input-method/fcitx.nix
   ./i18n/input-method/ibus.nix
@@ -126,6 +127,7 @@
   ./programs/gpaste.nix
   ./programs/gnupg.nix
   ./programs/gphoto2.nix
+  ./programs/hamster.nix
   ./programs/iftop.nix
   ./programs/iotop.nix
   ./programs/java.nix
@@ -153,6 +155,7 @@
   ./programs/ssmtp.nix
   ./programs/sysdig.nix
   ./programs/systemtap.nix
+  ./programs/steam.nix
   ./programs/sway.nix
   ./programs/system-config-printer.nix
   ./programs/thefuck.nix
@@ -297,6 +300,7 @@
   ./services/desktops/dleyna-renderer.nix
   ./services/desktops/dleyna-server.nix
   ./services/desktops/pantheon/files.nix
+  ./services/desktops/espanso.nix
   ./services/desktops/flatpak.nix
   ./services/desktops/geoclue2.nix
   ./services/desktops/gsignond.nix
@@ -327,6 +331,7 @@
   ./services/development/bloop.nix
   ./services/development/hoogle.nix
   ./services/development/jupyter/default.nix
+  ./services/development/jupyterhub/default.nix
   ./services/development/lorri.nix
   ./services/editors/emacs.nix
   ./services/editors/infinoted.nix
@@ -334,6 +339,7 @@
   ./services/games/minecraft-server.nix
   ./services/games/minetest-server.nix
   ./services/games/openarena.nix
+  ./services/games/teeworlds.nix
   ./services/games/terraria.nix
   ./services/hardware/acpid.nix
   ./services/hardware/actkbd.nix
@@ -361,7 +367,6 @@
   ./services/hardware/throttled.nix
   ./services/hardware/trezord.nix
   ./services/hardware/triggerhappy.nix
-  ./services/hardware/u2f.nix
   ./services/hardware/udev.nix
   ./services/hardware/udisks2.nix
   ./services/hardware/upower.nix
@@ -415,6 +420,7 @@
   ./services/misc/apache-kafka.nix
   ./services/misc/autofs.nix
   ./services/misc/autorandr.nix
+  ./services/misc/bazarr.nix
   ./services/misc/beanstalkd.nix
   ./services/misc/bees.nix
   ./services/misc/bepasty.nix
@@ -460,12 +466,11 @@
   ./services/misc/leaps.nix
   ./services/misc/lidarr.nix
   ./services/misc/mame.nix
-  ./services/misc/mathics.nix
+  ./services/misc/matrix-appservice-discord.nix
   ./services/misc/matrix-synapse.nix
+  ./services/misc/mautrix-telegram.nix
   ./services/misc/mbpfan.nix
   ./services/misc/mediatomb.nix
-  ./services/misc/mesos-master.nix
-  ./services/misc/mesos-slave.nix
   ./services/misc/metabase.nix
   ./services/misc/mwlib.nix
   ./services/misc/nix-daemon.nix
@@ -481,6 +486,7 @@
   ./services/misc/parsoid.nix
   ./services/misc/plex.nix
   ./services/misc/tautulli.nix
+  ./services/misc/pinnwand.nix
   ./services/misc/pykms.nix
   ./services/misc/radarr.nix
   ./services/misc/redmine.nix
@@ -506,6 +512,7 @@
   ./services/misc/uhub.nix
   ./services/misc/weechat.nix
   ./services/misc/xmr-stak.nix
+  ./services/misc/zigbee2mqtt.nix
   ./services/misc/zoneminder.nix
   ./services/misc/zookeeper.nix
   ./services/monitoring/alerta.nix
@@ -585,6 +592,7 @@
   ./services/networking/autossh.nix
   ./services/networking/bird.nix
   ./services/networking/bitlbee.nix
+  ./services/networking/blockbook-frontend.nix
   ./services/networking/charybdis.nix
   ./services/networking/cjdns.nix
   ./services/networking/cntlm.nix
@@ -598,13 +606,14 @@
   ./services/networking/dhcpcd.nix
   ./services/networking/dhcpd.nix
   ./services/networking/dnscache.nix
-  ./services/networking/dnschain.nix
   ./services/networking/dnscrypt-proxy2.nix
   ./services/networking/dnscrypt-wrapper.nix
   ./services/networking/dnsdist.nix
   ./services/networking/dnsmasq.nix
+  ./services/networking/ncdns.nix
   ./services/networking/ejabberd.nix
   ./services/networking/epmd.nix
+  ./services/networking/ergo.nix
   ./services/networking/eternal-terminal.nix
   ./services/networking/fakeroute.nix
   ./services/networking/ferm.nix
@@ -620,6 +629,7 @@
   ./services/networking/gdomap.nix
   ./services/networking/git-daemon.nix
   ./services/networking/gnunet.nix
+  ./services/networking/go-neb.nix
   ./services/networking/go-shadowsocks2.nix
   ./services/networking/gogoclient.nix
   ./services/networking/gvpe.nix
@@ -635,6 +645,8 @@
   ./services/networking/iperf3.nix
   ./services/networking/ircd-hybrid/default.nix
   ./services/networking/iwd.nix
+  ./services/networking/jicofo.nix
+  ./services/networking/jitsi-videobridge.nix
   ./services/networking/keepalived/default.nix
   ./services/networking/keybase.nix
   ./services/networking/kippo.nix
@@ -663,6 +675,7 @@
   ./services/networking/nat.nix
   ./services/networking/ndppd.nix
   ./services/networking/networkmanager.nix
+  ./services/networking/nextdns.nix
   ./services/networking/nftables.nix
   ./services/networking/ngircd.nix
   ./services/networking/nghttpx/default.nix
@@ -680,6 +693,7 @@
   ./services/networking/ocserv.nix
   ./services/networking/ofono.nix
   ./services/networking/oidentd.nix
+  ./services/networking/onedrive.nix
   ./services/networking/openfire.nix
   ./services/networking/openvpn.nix
   ./services/networking/ostinato.nix
@@ -752,6 +766,7 @@
   ./services/networking/v2ray.nix
   ./services/networking/vsftpd.nix
   ./services/networking/wakeonlan.nix
+  ./services/networking/wasabibackend.nix
   ./services/networking/websockify.nix
   ./services/networking/wg-quick.nix
   ./services/networking/wicd.nix
@@ -768,10 +783,8 @@
   ./services/networking/znc/default.nix
   ./services/printing/cupsd.nix
   ./services/scheduling/atd.nix
-  ./services/scheduling/chronos.nix
   ./services/scheduling/cron.nix
   ./services/scheduling/fcron.nix
-  ./services/scheduling/marathon.nix
   ./services/search/elasticsearch.nix
   ./services/search/elasticsearch-curator.nix
   ./services/search/hound.nix
@@ -792,6 +805,7 @@
   ./services/security/nginx-sso.nix
   ./services/security/oauth2_proxy.nix
   ./services/security/oauth2_proxy_nginx.nix
+  ./services/security/privacyidea.nix
   ./services/security/physlock.nix
   ./services/security/shibboleth-sp.nix
   ./services/security/sks.nix
@@ -801,7 +815,7 @@
   ./services/security/torsocks.nix
   ./services/security/usbguard.nix
   ./services/security/vault.nix
-  ./services/system/cgmanager.nix
+  ./services/security/yubikey-agent.nix
   ./services/system/cloud-init.nix
   ./services/system/dbus.nix
   ./services/system/earlyoom.nix
@@ -815,18 +829,22 @@
   ./services/torrent/magnetico.nix
   ./services/torrent/opentracker.nix
   ./services/torrent/peerflix.nix
+  ./services/torrent/rtorrent.nix
   ./services/torrent/transmission.nix
   ./services/ttys/agetty.nix
   ./services/ttys/gpm.nix
   ./services/ttys/kmscon.nix
   ./services/wayland/cage.nix
+  ./services/video/mirakurun.nix
   ./services/web-apps/atlassian/confluence.nix
   ./services/web-apps/atlassian/crowd.nix
   ./services/web-apps/atlassian/jira.nix
   ./services/web-apps/codimd.nix
+  ./services/web-apps/convos.nix
   ./services/web-apps/cryptpad.nix
   ./services/web-apps/documize.nix
   ./services/web-apps/dokuwiki.nix
+  ./services/web-apps/engelsystem.nix
   ./services/web-apps/frab.nix
   ./services/web-apps/gerrit.nix
   ./services/web-apps/gotify-server.nix
@@ -835,6 +853,7 @@
   ./services/web-apps/icingaweb2/module-monitoring.nix
   ./services/web-apps/ihatemoney
   ./services/web-apps/jirafeau.nix
+  ./services/web-apps/jitsi-meet.nix
   ./services/web-apps/limesurvey.nix
   ./services/web-apps/mattermost.nix
   ./services/web-apps/mediawiki.nix
@@ -846,6 +865,8 @@
   ./services/web-apps/matomo.nix
   ./services/web-apps/moinmoin.nix
   ./services/web-apps/restya-board.nix
+  ./services/web-apps/sogo.nix
+  ./services/web-apps/rss-bridge.nix
   ./services/web-apps/tt-rss.nix
   ./services/web-apps/trac.nix
   ./services/web-apps/trilium.nix
@@ -866,9 +887,9 @@
   ./services/web-servers/lighttpd/collectd.nix
   ./services/web-servers/lighttpd/default.nix
   ./services/web-servers/lighttpd/gitweb.nix
-  ./services/web-servers/meguca.nix
   ./services/web-servers/mighttpd2.nix
   ./services/web-servers/minio.nix
+  ./services/web-servers/molly-brown.nix
   ./services/web-servers/nginx/default.nix
   ./services/web-servers/nginx/gitweb.nix
   ./services/web-servers/phpfpm/default.nix
@@ -903,6 +924,7 @@
   ./services/x11/gdk-pixbuf.nix
   ./services/x11/imwheel.nix
   ./services/x11/redshift.nix
+  ./services/x11/urserver.nix
   ./services/x11/urxvtd.nix
   ./services/x11/window-managers/awesome.nix
   ./services/x11/window-managers/default.nix
@@ -926,6 +948,7 @@
   ./system/boot/grow-partition.nix
   ./system/boot/initrd-network.nix
   ./system/boot/initrd-ssh.nix
+  ./system/boot/initrd-openvpn.nix
   ./system/boot/kernel.nix
   ./system/boot/kexec.nix
   ./system/boot/loader/efi.nix
@@ -1001,7 +1024,6 @@
   ./virtualisation/podman.nix
   ./virtualisation/qemu-guest-agent.nix
   ./virtualisation/railcar.nix
-  ./virtualisation/rkt.nix
   ./virtualisation/virtualbox-guest.nix
   ./virtualisation/virtualbox-host.nix
   ./virtualisation/vmware-guest.nix
diff --git a/nixos/modules/profiles/base.nix b/nixos/modules/profiles/base.nix
index 2a2fe119d30..3b67d628f9f 100644
--- a/nixos/modules/profiles/base.nix
+++ b/nixos/modules/profiles/base.nix
@@ -26,6 +26,7 @@
     pkgs.fuse
     pkgs.fuse3
     pkgs.sshfs-fuse
+    pkgs.rsync
     pkgs.socat
     pkgs.screen
 
diff --git a/nixos/modules/profiles/demo.nix b/nixos/modules/profiles/demo.nix
index 18f190071ba..4e8c74deedb 100644
--- a/nixos/modules/profiles/demo.nix
+++ b/nixos/modules/profiles/demo.nix
@@ -11,9 +11,11 @@
       uid = 1000;
     };
 
-  services.xserver.displayManager.sddm.autoLogin = {
-    enable = true;
-    relogin = true;
-    user = "demo";
+  services.xserver.displayManager = {
+    autoLogin = {
+      enable = true;
+      user = "demo";
+    };
+    sddm.autoLogin.relogin = true;
   };
 }
diff --git a/nixos/modules/programs/autojump.nix b/nixos/modules/programs/autojump.nix
index 3a8feec4bb4..ecfc2f65807 100644
--- a/nixos/modules/programs/autojump.nix
+++ b/nixos/modules/programs/autojump.nix
@@ -18,7 +18,7 @@ in
         '';
       };
     };
-  }; 
+  };
 
   ###### implementation
 
@@ -26,7 +26,7 @@ in
     environment.pathsToLink = [ "/share/autojump" ];
     environment.systemPackages = [ pkgs.autojump ];
 
-    programs.bash.interactiveShellInit = "source ${pkgs.autojump}/share/autojump/autojump.bash"; 
+    programs.bash.interactiveShellInit = "source ${pkgs.autojump}/share/autojump/autojump.bash";
     programs.zsh.interactiveShellInit = mkIf prg.zsh.enable "source ${pkgs.autojump}/share/autojump/autojump.zsh";
     programs.fish.interactiveShellInit = mkIf prg.fish.enable "source ${pkgs.autojump}/share/autojump/autojump.fish";
   };
diff --git a/nixos/modules/programs/bash/bash.nix b/nixos/modules/programs/bash/bash.nix
index be964ce7f3f..1b3254b54a5 100644
--- a/nixos/modules/programs/bash/bash.nix
+++ b/nixos/modules/programs/bash/bash.nix
@@ -238,9 +238,6 @@ in
       "/share/bash-completion"
     ];
 
-    environment.systemPackages = optional cfg.enableCompletion
-      pkgs.nix-bash-completions;
-
     environment.shells =
       [ "/run/current-system/sw/bin/bash"
         "/run/current-system/sw/bin/sh"
diff --git a/nixos/modules/programs/ccache.nix b/nixos/modules/programs/ccache.nix
index 874774c72b4..3c9e64932f1 100644
--- a/nixos/modules/programs/ccache.nix
+++ b/nixos/modules/programs/ccache.nix
@@ -80,4 +80,4 @@ in {
       ];
     })
   ];
-}
\ No newline at end of file
+}
diff --git a/nixos/modules/programs/chromium.nix b/nixos/modules/programs/chromium.nix
index 16c063ebc89..3f042913619 100644
--- a/nixos/modules/programs/chromium.nix
+++ b/nixos/modules/programs/chromium.nix
@@ -69,11 +69,24 @@ in
       extraOpts = mkOption {
         type = types.attrs;
         description = ''
-          Extra chromium policy options, see
-          <link xlink:href="https://www.chromium.org/administrators/policy-list-3">https://www.chromium.org/administrators/policy-list-3</link>
-          for a list of avalible options
+          Extra chromium policy options. A list of available policies
+          can be found in the Chrome Enterprise documentation:
+          <link xlink:href="https://cloud.google.com/docs/chrome-enterprise/policies/">https://cloud.google.com/docs/chrome-enterprise/policies/</link>
+          Make sure the selected policy is supported on Linux and your browser version.
         '';
         default = {};
+        example = literalExample ''
+          {
+            "BrowserSignin" = 0;
+            "SyncDisabled" = true;
+            "PasswordManagerEnabled" = false;
+            "SpellcheckEnabled" = true;
+            "SpellcheckLanguage" = [
+                                     "de"
+                                     "en-US"
+                                   ];
+          }
+        '';
       };
     };
   };
diff --git a/nixos/modules/programs/dconf.nix b/nixos/modules/programs/dconf.nix
index 6702e8efd1c..ec85cb9d18c 100644
--- a/nixos/modules/programs/dconf.nix
+++ b/nixos/modules/programs/dconf.nix
@@ -4,13 +4,24 @@ with lib;
 
 let
   cfg = config.programs.dconf;
-
-  mkDconfProfile = name: path:
-    {
-      name = "dconf/profile/${name}";
-      value.source = path; 
-    };
-
+  cfgDir = pkgs.symlinkJoin {
+    name = "dconf-system-config";
+    paths = map (x: "${x}/etc/dconf") cfg.packages;
+    postBuild = ''
+      mkdir -p $out/profile
+      mkdir -p $out/db
+    '' + (
+      concatStringsSep "\n" (
+        mapAttrsToList (
+          name: path: ''
+            ln -s ${path} $out/profile/${name}
+          ''
+        ) cfg.profiles
+      )
+    ) + ''
+      ${pkgs.dconf}/bin/dconf update $out/db
+    '';
+  };
 in
 {
   ###### interface
@@ -22,18 +33,24 @@ in
       profiles = mkOption {
         type = types.attrsOf types.path;
         default = {};
-        description = "Set of dconf profile files.";
+        description = "Set of dconf profile files, installed at <filename>/etc/dconf/profiles/<replaceable>name</replaceable></filename>.";
         internal = true;
       };
 
+      packages = mkOption {
+        type = types.listOf types.package;
+        default = [];
+        description = "A list of packages which provide dconf profiles and databases in <filename>/etc/dconf</filename>.";
+      };
     };
   };
 
   ###### implementation
 
   config = mkIf (cfg.profiles != {} || cfg.enable) {
-    environment.etc = optionalAttrs (cfg.profiles != {})
-      (mapAttrs' mkDconfProfile cfg.profiles);
+    environment.etc.dconf = mkIf (cfg.profiles != {} || cfg.packages != []) {
+      source = cfgDir;
+    };
 
     services.dbus.packages = [ pkgs.dconf ];
 
diff --git a/nixos/modules/programs/fish.nix b/nixos/modules/programs/fish.nix
index 48b324a0fe8..39b92edf2ac 100644
--- a/nixos/modules/programs/fish.nix
+++ b/nixos/modules/programs/fish.nix
@@ -102,6 +102,9 @@ in
 
     programs.fish.shellAliases = mapAttrs (name: mkDefault) cfge.shellAliases;
 
+    # Required for man completions
+    documentation.man.generateCaches = true;
+
     environment.etc."fish/foreign-env/shellInit".text = cfge.shellInit;
     environment.etc."fish/foreign-env/loginShellInit".text = cfge.loginShellInit;
     environment.etc."fish/foreign-env/interactiveShellInit".text = cfge.interactiveShellInit;
diff --git a/nixos/modules/programs/freetds.nix b/nixos/modules/programs/freetds.nix
index e0860a242b7..b4b657e391b 100644
--- a/nixos/modules/programs/freetds.nix
+++ b/nixos/modules/programs/freetds.nix
@@ -25,7 +25,7 @@ in
           ''';
         }
       '';
-      description = 
+      description =
         ''
         Configure freetds database entries. Each attribute denotes
         a section within freetds.conf, and the value (a string) is the config
@@ -47,7 +47,7 @@ in
     environment.variables.FREETDS = "/etc/freetds.conf";
     environment.variables.SYBASE = "${pkgs.freetds}";
 
-    environment.etc."freetds.conf" = { text = 
+    environment.etc."freetds.conf" = { text =
       (concatStrings (mapAttrsToList (name: value:
         ''
         [${name}]
diff --git a/nixos/modules/programs/gnupg.nix b/nixos/modules/programs/gnupg.nix
index 7a3cb588ee7..ce8799b21d6 100644
--- a/nixos/modules/programs/gnupg.nix
+++ b/nixos/modules/programs/gnupg.nix
@@ -70,6 +70,7 @@ in
     agent.pinentryFlavor = mkOption {
       type = types.nullOr (types.enum pkgs.pinentry.flavors);
       example = "gnome3";
+      default = defaultPinentryFlavor;
       description = ''
         Which pinentry interface to use. If not null, the path to the
         pinentry binary will be passed to gpg-agent via commandline and
@@ -91,8 +92,6 @@ in
   };
 
   config = mkIf cfg.agent.enable {
-    programs.gnupg.agent.pinentryFlavor = mkDefault defaultPinentryFlavor;
-
     # This overrides the systemd user unit shipped with the gnupg package
     systemd.user.services.gpg-agent = mkIf (cfg.agent.pinentryFlavor != null) {
       serviceConfig.ExecStart = [ "" ''
diff --git a/nixos/modules/programs/hamster.nix b/nixos/modules/programs/hamster.nix
new file mode 100644
index 00000000000..b2f4a82b260
--- /dev/null
+++ b/nixos/modules/programs/hamster.nix
@@ -0,0 +1,15 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  meta.maintainers = pkgs.hamster.meta.maintainers;
+
+  options.programs.hamster.enable =
+    mkEnableOption "Whether to enable hamster time tracking.";
+
+  config = lib.mkIf config.programs.hamster.enable {
+    environment.systemPackages = [ pkgs.hamster ];
+    services.dbus.packages = [ pkgs.hamster ];
+  };
+}
diff --git a/nixos/modules/programs/shadow.nix b/nixos/modules/programs/shadow.nix
index fc352795c01..386ded9d98b 100644
--- a/nixos/modules/programs/shadow.nix
+++ b/nixos/modules/programs/shadow.nix
@@ -114,8 +114,9 @@ in
       newgrp.source    = "${pkgs.shadow.out}/bin/newgrp";
       newuidmap.source = "${pkgs.shadow.out}/bin/newuidmap";
       newgidmap.source = "${pkgs.shadow.out}/bin/newgidmap";
-    } // (if config.users.mutableUsers then {
+    } // lib.optionalAttrs config.users.mutableUsers {
+      chsh.source      = "${pkgs.shadow.out}/bin/chsh";
       passwd.source    = "${pkgs.shadow.out}/bin/passwd";
-    } else {});
+    };
   };
 }
diff --git a/nixos/modules/programs/ssh.nix b/nixos/modules/programs/ssh.nix
index 44e65ee8a9a..a983ffa4b89 100644
--- a/nixos/modules/programs/ssh.nix
+++ b/nixos/modules/programs/ssh.nix
@@ -194,6 +194,33 @@ in
         '';
       };
 
+      kexAlgorithms = mkOption {
+        type = types.nullOr (types.listOf types.str);
+        default = null;
+        example = [ "curve25519-sha256@libssh.org" "diffie-hellman-group-exchange-sha256" ];
+        description = ''
+          Specifies the available KEX (Key Exchange) algorithms.
+        '';
+      };
+
+      ciphers = mkOption {
+        type = types.nullOr (types.listOf types.str);
+        default = null;
+        example = [ "chacha20-poly1305@openssh.com" "aes256-gcm@openssh.com" ];
+        description = ''
+          Specifies the ciphers allowed and their order of preference.
+        '';
+      };
+
+      macs = mkOption {
+        type = types.nullOr (types.listOf types.str);
+        default = null;
+        example = [ "hmac-sha2-512-etm@openssh.com" "hmac-sha1" ];
+        description = ''
+          Specifies the MAC (message authentication code) algorithms in order of preference. The MAC algorithm is used
+          for data integrity protection.
+        '';
+      };
     };
 
   };
@@ -232,6 +259,9 @@ in
 
         ${optionalString (cfg.pubkeyAcceptedKeyTypes != []) "PubkeyAcceptedKeyTypes ${concatStringsSep "," cfg.pubkeyAcceptedKeyTypes}"}
         ${optionalString (cfg.hostKeyAlgorithms != []) "HostKeyAlgorithms ${concatStringsSep "," cfg.hostKeyAlgorithms}"}
+        ${optionalString (cfg.kexAlgorithms != null) "KexAlgorithms ${concatStringsSep "," cfg.kexAlgorithms}"}
+        ${optionalString (cfg.ciphers != null) "Ciphers ${concatStringsSep "," cfg.ciphers}"}
+        ${optionalString (cfg.macs != null) "MACs ${concatStringsSep "," cfg.macs}"}
       '';
 
     environment.etc."ssh/ssh_known_hosts".text = knownHostsText;
diff --git a/nixos/modules/programs/ssmtp.nix b/nixos/modules/programs/ssmtp.nix
index c7a94739349..15d2750c193 100644
--- a/nixos/modules/programs/ssmtp.nix
+++ b/nixos/modules/programs/ssmtp.nix
@@ -21,9 +21,11 @@ in
     (mkRenamedOptionModule [ "networking" "defaultMailServer" "useTLS" ] [ "services" "ssmtp" "useTLS" ])
     (mkRenamedOptionModule [ "networking" "defaultMailServer" "useSTARTTLS" ] [ "services" "ssmtp" "useSTARTTLS" ])
     (mkRenamedOptionModule [ "networking" "defaultMailServer" "authUser" ] [ "services" "ssmtp" "authUser" ])
-    (mkRenamedOptionModule [ "networking" "defaultMailServer" "authPass" ] [ "services" "ssmtp" "authPass" ])
     (mkRenamedOptionModule [ "networking" "defaultMailServer" "authPassFile" ] [ "services" "ssmtp" "authPassFile" ])
     (mkRenamedOptionModule [ "networking" "defaultMailServer" "setSendmail" ] [ "services" "ssmtp" "setSendmail" ])
+
+    (mkRemovedOptionModule [ "networking" "defaultMailServer" "authPass" ] "authPass has been removed since it leaks the clear-text password into the world-readable store. Use authPassFile instead and make sure it's not a store path")
+    (mkRemovedOptionModule [ "services" "ssmtp" "authPass" ] "authPass has been removed since it leaks the clear-text password into the world-readable store. Use authPassFile instead and make sure it's not a store path")
   ];
 
   options = {
@@ -45,6 +47,21 @@ in
         '';
       };
 
+      settings = mkOption {
+        type = with types; attrsOf (oneOf [ bool str ]);
+        default = {};
+        description = ''
+          <citerefentry><refentrytitle>ssmtp</refentrytitle><manvolnum>5</manvolnum></citerefentry> configuration. Refer
+          to <link xlink:href="https://linux.die.net/man/5/ssmtp.conf"/> for details on supported values.
+        '';
+        example = literalExample ''
+          {
+            Debug = true;
+            FromLineOverride = false;
+          }
+        '';
+      };
+
       hostName = mkOption {
         type = types.str;
         example = "mail.example.org";
@@ -101,18 +118,6 @@ in
         '';
       };
 
-      authPass = mkOption {
-        type = types.str;
-        default = "";
-        example = "correctHorseBatteryStaple";
-        description = ''
-          Password used for SMTP auth. (STORED PLAIN TEXT, WORLD-READABLE IN NIX STORE)
-
-          It's recommended to use <option>authPassFile</option>
-          which takes precedence over <option>authPass</option>.
-        '';
-      };
-
       authPassFile = mkOption {
         type = types.nullOr types.str;
         default = null;
@@ -121,11 +126,6 @@ in
           Path to a file that contains the password used for SMTP auth. The file
           should not contain a trailing newline, if the password does not contain one.
           This file should be readable by the users that need to execute ssmtp.
-
-          <option>authPassFile</option> takes precedence over <option>authPass</option>.
-
-          Warning: when <option>authPass</option> is non-empty <option>authPassFile</option>
-          defaults to a file in the WORLD-READABLE Nix store containing that password.
         '';
       };
 
@@ -142,25 +142,28 @@ in
 
   config = mkIf cfg.enable {
 
-    services.ssmtp.authPassFile = mkIf (cfg.authPass != "")
-      (mkDefault (toString (pkgs.writeTextFile {
-        name = "ssmtp-authpass";
-        text = cfg.authPass;
-      })));
-
-    environment.etc."ssmtp/ssmtp.conf".text =
-      let yesNo = yes : if yes then "YES" else "NO"; in
-      ''
-        MailHub=${cfg.hostName}
-        FromLineOverride=YES
-        ${optionalString (cfg.root   != "") "root=${cfg.root}"}
-        ${optionalString (cfg.domain != "") "rewriteDomain=${cfg.domain}"}
-        UseTLS=${yesNo cfg.useTLS}
-        UseSTARTTLS=${yesNo cfg.useSTARTTLS}
-        #Debug=YES
-        ${optionalString (cfg.authUser != "")       "AuthUser=${cfg.authUser}"}
-        ${optionalString (cfg.authPassFile != null) "AuthPassFile=${cfg.authPassFile}"}
-      '';
+    services.ssmtp.settings = mkMerge [
+      ({
+        MailHub = cfg.hostName;
+        FromLineOverride = mkDefault true;
+        UseTLS = cfg.useTLS;
+        UseSTARTTLS = cfg.useSTARTTLS;
+      })
+      (mkIf (cfg.root != "") { root = cfg.root; })
+      (mkIf (cfg.domain != "") { rewriteDomain = cfg.domain; })
+      (mkIf (cfg.authUser != "") { AuthUser = cfg.authUser; })
+      (mkIf (cfg.authPassFile != null) { AuthPassFile = cfg.authPassFile; })
+    ];
+
+    environment.etc."ssmtp/ssmtp.conf".source =
+      let
+        toStr = value:
+          if value == true then "YES"
+          else if value == false then "NO"
+          else builtins.toString value
+        ;
+      in
+        pkgs.writeText "ssmtp.conf" (concatStringsSep "\n" (mapAttrsToList (key: value: "${key}=${toStr value}") cfg.settings));
 
     environment.systemPackages = [pkgs.ssmtp];
 
diff --git a/nixos/modules/programs/steam.nix b/nixos/modules/programs/steam.nix
new file mode 100644
index 00000000000..3c919c47a0c
--- /dev/null
+++ b/nixos/modules/programs/steam.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.steam;
+in {
+  options.programs.steam.enable = mkEnableOption "steam";
+
+  config = mkIf cfg.enable {
+    hardware.opengl = { # this fixes the "glXChooseVisual failed" bug, context: https://github.com/NixOS/nixpkgs/issues/47932
+      enable = true;
+      driSupport32Bit = true;
+    };
+
+    # optionally enable 32bit pulseaudio support if pulseaudio is enabled
+    hardware.pulseaudio.support32Bit = config.hardware.pulseaudio.enable;
+
+    hardware.steam-hardware.enable = true;
+
+    environment.systemPackages = [ pkgs.steam ];
+  };
+
+  meta.maintainers = with maintainers; [ mkg20001 ];
+}
diff --git a/nixos/modules/programs/zsh/zsh.nix b/nixos/modules/programs/zsh/zsh.nix
index 930cc1987a3..049a315c762 100644
--- a/nixos/modules/programs/zsh/zsh.nix
+++ b/nixos/modules/programs/zsh/zsh.nix
@@ -135,6 +135,13 @@ in
         type = types.bool;
       };
 
+      enableBashCompletion = mkOption {
+        default = false;
+        description = ''
+          Enable compatibility with bash's programmable completion system.
+        '';
+        type = types.bool;
+      };
 
       enableGlobalCompInit = mkOption {
         default = cfg.enableCompletion;
@@ -239,6 +246,11 @@ in
           autoload -U compinit && compinit
         ''}
 
+        ${optionalString cfg.enableBashCompletion ''
+          # Enable compatibility with bash's completion system.
+          autoload -U bashcompinit && bashcompinit
+        ''}
+
         # Setup custom interactive shell init stuff.
         ${cfge.interactiveShellInit}
 
diff --git a/nixos/modules/rename.nix b/nixos/modules/rename.nix
index a946268494e..1fe00e9142b 100644
--- a/nixos/modules/rename.nix
+++ b/nixos/modules/rename.nix
@@ -17,16 +17,22 @@ with lib;
     (mkAliasOptionModule [ "environment" "checkConfigurationOptions" ] [ "_module" "check" ])
 
     # Completely removed modules
+    (mkRemovedOptionModule [ "fonts" "fontconfig" "penultimate" ] "The corresponding package has removed from nixpkgs.")
+    (mkRemovedOptionModule [ "services" "chronos" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "firefox" "syncserver" "user" ] "")
     (mkRemovedOptionModule [ "services" "firefox" "syncserver" "group" ] "")
+    (mkRemovedOptionModule [ "services" "marathon" ] "The corresponding package was removed from nixpkgs.")
+    (mkRemovedOptionModule [ "services" "mesos" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "winstone" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "networking" "vpnc" ] "Use environment.etc.\"vpnc/service.conf\" instead.")
     (mkRemovedOptionModule [ "environment" "blcr" "enable" ] "The BLCR module has been removed")
     (mkRemovedOptionModule [ "services" "beegfsEnable" ] "The BeeGFS module has been removed")
     (mkRemovedOptionModule [ "services" "beegfs" ] "The BeeGFS module has been removed")
+    (mkRemovedOptionModule ["services" "cgmanager" "enable"] "cgmanager was deprecated by lxc and therefore removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "osquery" ] "The osquery module has been removed")
     (mkRemovedOptionModule [ "services" "fourStore" ] "The fourStore module has been removed")
     (mkRemovedOptionModule [ "services" "fourStoreEndpoint" ] "The fourStoreEndpoint module has been removed")
+    (mkRemovedOptionModule [ "services" "mathics" ] "The Mathics module has been removed")
     (mkRemovedOptionModule [ "programs" "way-cooler" ] ("way-cooler is abandoned by its author: " +
       "https://way-cooler.org/blog/2020/01/09/way-cooler-post-mortem.html"))
     (mkRemovedOptionModule [ "services" "xserver" "multitouch" ] ''
@@ -38,21 +44,29 @@ with lib;
       The services.xserver.displayManager.auto module has been removed
       because it was only intended for use in internal NixOS tests, and gave the
       false impression of it being a special display manager when it's actually
-      LightDM. Please use the services.xserver.displayManager.lightdm.autoLogin options
+      LightDM. Please use the services.xserver.displayManager.autoLogin options
       instead, or any other display manager in NixOS as they all support auto-login.
     '')
     (mkRemovedOptionModule [ "services" "dnscrypt-proxy" ] "Use services.dnscrypt-proxy2 instead")
+    (mkRemovedOptionModule [ "services" "meguca" ] "Use meguca has been removed from nixpkgs")
     (mkRemovedOptionModule ["hardware" "brightnessctl" ] ''
       The brightnessctl module was removed because newer versions of
       brightnessctl don't require the udev rules anymore (they can use the
       systemd-logind API). Instead of using the module you can now
       simply add the brightnessctl package to environment.systemPackages.
     '')
+    (mkRemovedOptionModule [ "virtualisation" "rkt" ] "The rkt module has been removed, it was archived by upstream")
 
     (mkRemovedOptionModule ["services" "prey" ] ''
       prey-bash-client is deprecated upstream
     '')
 
+    (mkRemovedOptionModule ["hardware" "u2f" ] ''
+      The U2F modules module was removed, as all it did was adding the
+      udev rules from libu2f-host to the system. Udev gained native support
+      to handle FIDO security tokens, so this isn't necessary anymore.
+    '')
+
     # Do NOT add any option renames here, see top of the file
   ];
 }
diff --git a/nixos/modules/security/acme.nix b/nixos/modules/security/acme.nix
index 776ef07d716..29635dbe864 100644
--- a/nixos/modules/security/acme.nix
+++ b/nixos/modules/security/acme.nix
@@ -23,16 +23,16 @@ let
         type = types.nullOr types.str;
         default = null;
         description = ''
-          ACME Directory Resource URI. Defaults to let's encrypt
+          ACME Directory Resource URI. Defaults to Let's Encrypt's
           production endpoint,
-          https://acme-v02.api.letsencrypt.org/directory, if unset.
+          <link xlink:href="https://acme-v02.api.letsencrypt.org/directory"/>, if unset.
         '';
       };
 
       domain = mkOption {
         type = types.str;
         default = name;
-        description = "Domain to fetch certificate for (defaults to the entry name)";
+        description = "Domain to fetch certificate for (defaults to the entry name).";
       };
 
       email = mkOption {
@@ -103,7 +103,7 @@ let
         description = ''
           Key type to use for private keys.
           For an up to date list of supported values check the --key-type option
-          at https://go-acme.github.io/lego/usage/cli/#usage.
+          at <link xlink:href="https://go-acme.github.io/lego/usage/cli/#usage"/>.
         '';
       };
 
@@ -113,7 +113,7 @@ let
         example = "route53";
         description = ''
           DNS Challenge provider. For a list of supported providers, see the "code"
-          field of the DNS providers listed at https://go-acme.github.io/lego/dns/.
+          field of the DNS providers listed at <link xlink:href="https://go-acme.github.io/lego/dns/"/>.
         '';
       };
 
@@ -123,7 +123,7 @@ let
           Path to an EnvironmentFile for the cert's service containing any required and
           optional environment variables for your selected dnsProvider.
           To find out what values you need to set, consult the documentation at
-          https://go-acme.github.io/lego/dns/ for the corresponding dnsProvider.
+          <link xlink:href="https://go-acme.github.io/lego/dns/"/> for the corresponding dnsProvider.
         '';
         example = "/var/src/secrets/example.org-route53-api-token";
       };
@@ -150,6 +150,14 @@ let
         '';
       };
 
+      extraLegoFlags = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          Additional global flags to pass to all lego commands.
+        '';
+      };
+
       extraLegoRenewFlags = mkOption {
         type = types.listOf types.str;
         default = [];
@@ -157,6 +165,14 @@ let
           Additional flags to pass to lego renew.
         '';
       };
+
+      extraLegoRunFlags = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          Additional flags to pass to lego run.
+        '';
+      };
     };
   };
 
@@ -169,7 +185,7 @@ in
     (mkRemovedOptionModule [ "security" "acme" "production" ] ''
       Use security.acme.server to define your staging ACME server URL instead.
 
-      To use the let's encrypt staging server, use security.acme.server =
+      To use Let's Encrypt's staging server, use security.acme.server =
       "https://acme-staging-v02.api.letsencrypt.org/directory".
     ''
     )
@@ -207,9 +223,9 @@ in
         type = types.nullOr types.str;
         default = null;
         description = ''
-          ACME Directory Resource URI. Defaults to let's encrypt
+          ACME Directory Resource URI. Defaults to Let's Encrypt's
           production endpoint,
-          <literal>https://acme-v02.api.letsencrypt.org/directory</literal>, if unset.
+          <link xlink:href="https://acme-v02.api.letsencrypt.org/directory"/>, if unset.
         '';
       };
 
@@ -230,8 +246,8 @@ in
         type = types.bool;
         default = false;
         description = ''
-          Accept the CA's terms of service. The default provier is Let's Encrypt,
-          you can find their ToS at https://letsencrypt.org/repository/
+          Accept the CA's terms of service. The default provider is Let's Encrypt,
+          you can find their ToS at <link xlink:href="https://letsencrypt.org/repository/"/>.
         '';
       };
 
@@ -302,20 +318,27 @@ in
                 lpath = "acme/${cert}";
                 apath = "/var/lib/${lpath}";
                 spath = "/var/lib/acme/.lego/${cert}";
+                keyName = builtins.replaceStrings ["*"] ["_"] data.domain;
+                requestedDomains = pipe ([ data.domain ] ++ (attrNames data.extraDomains)) [
+                  (domains: sort builtins.lessThan domains)
+                  (domains: concatStringsSep "," domains)
+                ];
                 fileMode = if data.allowKeysForGroup then "640" else "600";
                 globalOpts = [ "-d" data.domain "--email" data.email "--path" "." "--key-type" data.keyType ]
                           ++ optionals (cfg.acceptTerms) [ "--accept-tos" ]
                           ++ optionals (data.dnsProvider != null && !data.dnsPropagationCheck) [ "--dns.disable-cp" ]
                           ++ concatLists (mapAttrsToList (name: root: [ "-d" name ]) data.extraDomains)
                           ++ (if data.dnsProvider != null then [ "--dns" data.dnsProvider ] else [ "--http" "--http.webroot" data.webroot ])
-                          ++ optionals (cfg.server != null || data.server != null) ["--server" (if data.server == null then cfg.server else data.server)];
+                          ++ optionals (cfg.server != null || data.server != null) ["--server" (if data.server == null then cfg.server else data.server)]
+                          ++ data.extraLegoFlags;
                 certOpts = optionals data.ocspMustStaple [ "--must-staple" ];
-                runOpts = escapeShellArgs (globalOpts ++ [ "run" ] ++ certOpts);
+                runOpts = escapeShellArgs (globalOpts ++ [ "run" ] ++ certOpts ++ data.extraLegoRunFlags);
                 renewOpts = escapeShellArgs (globalOpts ++
                   [ "renew" "--days" (toString cfg.validMinDays) ] ++
                   certOpts ++ data.extraLegoRenewFlags);
                 acmeService = {
                   description = "Renew ACME Certificate for ${cert}";
+                  path = with pkgs; [ openssl ];
                   after = [ "network.target" "network-online.target" ];
                   wants = [ "network-online.target" ];
                   wantedBy = mkIf (!config.boot.isContainer) [ "multi-user.target" ];
@@ -332,11 +355,18 @@ in
                     ExecStart = pkgs.writeScript "acme-start" ''
                       #!${pkgs.runtimeShell} -e
                       test -L ${spath}/accounts -o -d ${spath}/accounts || ln -s ../accounts ${spath}/accounts
-                      ${pkgs.lego}/bin/lego ${renewOpts} || ${pkgs.lego}/bin/lego ${runOpts}
+                      LEGO_ARGS=(${runOpts})
+                      if [ -e ${spath}/certificates/${keyName}.crt ]; then
+                        REQUESTED_DOMAINS="${requestedDomains}"
+                        EXISTING_DOMAINS="$(openssl x509 -in ${spath}/certificates/${keyName}.crt -noout -ext subjectAltName | tail -n1 | sed -e 's/ *DNS://g')"
+                        if [ "''${REQUESTED_DOMAINS}" == "''${EXISTING_DOMAINS}" ]; then
+                          LEGO_ARGS=(${renewOpts})
+                        fi
+                      fi
+                      ${pkgs.lego}/bin/lego ''${LEGO_ARGS[@]}
                     '';
                     ExecStartPost =
                       let
-                        keyName = builtins.replaceStrings ["*"] ["_"] data.domain;
                         script = pkgs.writeScript "acme-post-start" ''
                           #!${pkgs.runtimeShell} -e
                           cd ${apath}
diff --git a/nixos/modules/security/google_oslogin.nix b/nixos/modules/security/google_oslogin.nix
index 78c2089baeb..c2889a0f0d1 100644
--- a/nixos/modules/security/google_oslogin.nix
+++ b/nixos/modules/security/google_oslogin.nix
@@ -50,6 +50,7 @@ in
     # enable the nss module, so user lookups etc. work
     system.nssModules = [ package ];
     system.nssDatabases.passwd = [ "cache_oslogin" "oslogin" ];
+    system.nssDatabases.group = [ "cache_oslogin" "oslogin" ];
 
     # Ugly: sshd refuses to start if a store path is given because /nix/store is group-writable.
     # So indirect by a symlink.
diff --git a/nixos/modules/security/pam.nix b/nixos/modules/security/pam.nix
index e1a94b0121a..565c15dec24 100644
--- a/nixos/modules/security/pam.nix
+++ b/nixos/modules/security/pam.nix
@@ -36,6 +36,17 @@ let
         '';
       };
 
+      p11Auth = mkOption {
+        default = config.security.pam.p11.enable;
+        type = types.bool;
+        description = ''
+          If set, keys listed in
+          <filename>~/.ssh/authorized_keys</filename> and
+          <filename>~/.eid/authorized_certificates</filename>
+          can be used to log in with the associated PKCS#11 tokens.
+        '';
+      };
+
       u2fAuth = mkOption {
         default = config.security.pam.u2f.enable;
         type = types.bool;
@@ -352,6 +363,8 @@ let
               "auth sufficient ${pkgs.pam_ssh_agent_auth}/libexec/pam_ssh_agent_auth.so file=~/.ssh/authorized_keys:~/.ssh/authorized_keys2:/etc/ssh/authorized_keys.d/%u"}
           ${optionalString cfg.fprintAuth
               "auth sufficient ${pkgs.fprintd}/lib/security/pam_fprintd.so"}
+          ${let p11 = config.security.pam.p11; in optionalString cfg.p11Auth
+              "auth ${p11.control} ${pkgs.pam_p11}/lib/security/pam_p11.so ${pkgs.opensc}/lib/opensc-pkcs11.so"}
           ${let u2f = config.security.pam.u2f; in optionalString cfg.u2fAuth
               "auth ${u2f.control} ${pkgs.pam_u2f}/lib/security/pam_u2f.so ${optionalString u2f.debug "debug"} ${optionalString (u2f.authFile != null) "authfile=${u2f.authFile}"} ${optionalString u2f.interactive "interactive"} ${optionalString u2f.cue "cue"}"}
           ${optionalString cfg.usbAuth
@@ -436,6 +449,8 @@ let
               "session required ${pkgs.pam}/lib/security/pam_lastlog.so silent"}
           ${optionalString config.security.pam.enableEcryptfs
               "session optional ${pkgs.ecryptfs}/lib/security/pam_ecryptfs.so"}
+          ${optionalString cfg.pamMount
+              "session optional ${pkgs.pam_mount}/lib/security/pam_mount.so"}
           ${optionalString use_ldap
               "session optional ${pam_ldap}/lib/security/pam_ldap.so"}
           ${optionalString config.services.sssd.enable
@@ -452,8 +467,6 @@ let
               "session required ${pkgs.pam}/lib/security/pam_limits.so conf=${makeLimitsConf cfg.limits}"}
           ${optionalString (cfg.showMotd && config.users.motd != null)
               "session optional ${pkgs.pam}/lib/security/pam_motd.so motd=${motd}"}
-          ${optionalString cfg.pamMount
-              "session optional ${pkgs.pam_mount}/lib/security/pam_mount.so"}
           ${optionalString (cfg.enableAppArmor && config.security.apparmor.enable)
               "session optional ${pkgs.apparmor-pam}/lib/security/pam_apparmor.so order=user,group,default debug"}
           ${optionalString (cfg.enableKwallet)
@@ -566,6 +579,39 @@ in
 
     security.pam.enableOTPW = mkEnableOption "the OTPW (one-time password) PAM module";
 
+    security.pam.p11 = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Enables P11 PAM (<literal>pam_p11</literal>) module.
+
+          If set, users can log in with SSH keys and PKCS#11 tokens.
+
+          More information can be found <link
+          xlink:href="https://github.com/OpenSC/pam_p11">here</link>.
+        '';
+      };
+
+      control = mkOption {
+        default = "sufficient";
+        type = types.enum [ "required" "requisite" "sufficient" "optional" ];
+        description = ''
+          This option sets pam "control".
+          If you want to have multi factor authentication, use "required".
+          If you want to use the PKCS#11 device instead of the regular password,
+          use "sufficient".
+
+          Read
+          <citerefentry>
+            <refentrytitle>pam.conf</refentrytitle>
+            <manvolnum>5</manvolnum>
+          </citerefentry>
+          for better understanding of this option.
+        '';
+      };
+    };
+
     security.pam.u2f = {
       enable = mkOption {
         default = false;
@@ -747,6 +793,7 @@ in
       ++ optionals config.krb5.enable [pam_krb5 pam_ccreds]
       ++ optionals config.security.pam.enableOTPW [ pkgs.otpw ]
       ++ optionals config.security.pam.oath.enable [ pkgs.oathToolkit ]
+      ++ optionals config.security.pam.p11.enable [ pkgs.pam_p11 ]
       ++ optionals config.security.pam.u2f.enable [ pkgs.pam_u2f ];
 
     boot.supportedFilesystems = optionals config.security.pam.enableEcryptfs [ "ecryptfs" ];
diff --git a/nixos/modules/security/sudo.nix b/nixos/modules/security/sudo.nix
index e3e43177def..1ed5269c5ae 100644
--- a/nixos/modules/security/sudo.nix
+++ b/nixos/modules/security/sudo.nix
@@ -173,7 +173,9 @@ in
 
   config = mkIf cfg.enable {
 
-    security.sudo.extraRules = [
+    # We `mkOrder 600` so that the default rule shows up first, but there is
+    # still enough room for a user to `mkBefore` it.
+    security.sudo.extraRules = mkOrder 600 [
       { groups = [ "wheel" ];
         commands = [ { command = "ALL"; options = (if cfg.wheelNeedsPassword then [ "SETENV" ] else [ "NOPASSWD" "SETENV" ]); } ];
       }
diff --git a/nixos/modules/security/tpm2.nix b/nixos/modules/security/tpm2.nix
index 13804fb82cb..27f9b58c975 100644
--- a/nixos/modules/security/tpm2.nix
+++ b/nixos/modules/security/tpm2.nix
@@ -170,7 +170,6 @@ in {
           Restart = "always";
           RestartSec = 30;
           BusName = "com.intel.tss2.Tabrmd";
-          StandardOutput = "syslog";
           ExecStart = "${cfg.abrmd.package}/bin/tpm2-abrmd";
           User = "tss";
           Group = "nogroup";
diff --git a/nixos/modules/security/wrappers/default.nix b/nixos/modules/security/wrappers/default.nix
index a0fadb018ec..2def74f8535 100644
--- a/nixos/modules/security/wrappers/default.nix
+++ b/nixos/modules/security/wrappers/default.nix
@@ -160,8 +160,11 @@ in
   config = {
 
     security.wrappers = {
+      # These are mount related wrappers that require the +s permission.
       fusermount.source = "${pkgs.fuse}/bin/fusermount";
       fusermount3.source = "${pkgs.fuse3}/bin/fusermount3";
+      mount.source = "${lib.getBin pkgs.utillinux}/bin/mount";
+      umount.source = "${lib.getBin pkgs.utillinux}/bin/umount";
     };
 
     boot.specialFileSystems.${parentWrapperDir} = {
diff --git a/nixos/modules/services/audio/icecast.nix b/nixos/modules/services/audio/icecast.nix
index 6a8a0f9975b..f40ea6be29d 100644
--- a/nixos/modules/services/audio/icecast.nix
+++ b/nixos/modules/services/audio/icecast.nix
@@ -23,7 +23,7 @@ let
       <listen-socket>
         <port>${toString cfg.listen.port}</port>
         <bind-address>${cfg.listen.address}</bind-address>
-      </listen-socket>   
+      </listen-socket>
 
       <security>
         <chroot>0</chroot>
@@ -70,7 +70,7 @@ in {
         description = "Base directory used for logging.";
         default = "/var/log/icecast";
       };
-      
+
       listen = {
         port = mkOption {
           type = types.int;
diff --git a/nixos/modules/services/audio/mpd.nix b/nixos/modules/services/audio/mpd.nix
index f4eb4a265a4..1d2a982ac53 100644
--- a/nixos/modules/services/audio/mpd.nix
+++ b/nixos/modules/services/audio/mpd.nix
@@ -21,6 +21,12 @@ let
 
     ${optionalString (cfg.network.listenAddress != "any") ''bind_to_address "${cfg.network.listenAddress}"''}
     ${optionalString (cfg.network.port != 6600)  ''port "${toString cfg.network.port}"''}
+    ${optionalString (cfg.fluidsynth) ''
+      decoder {
+              plugin "fluidsynth"
+              soundfont "${pkgs.soundfont-fluid}/share/soundfonts/FluidR3_GM2-2.sf2"
+      }
+    ''}
 
     ${cfg.extraConfig}
   '';
@@ -133,6 +139,14 @@ in {
           parameter is omitted from the configuration.
         '';
       };
+
+      fluidsynth = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          If set, add fluidsynth soundfont and configure the plugin.
+        '';
+      };
     };
 
   };
diff --git a/nixos/modules/services/audio/roon-server.nix b/nixos/modules/services/audio/roon-server.nix
index 6aed485638c..eceb65044c5 100644
--- a/nixos/modules/services/audio/roon-server.nix
+++ b/nixos/modules/services/audio/roon-server.nix
@@ -45,14 +45,14 @@ in {
       environment.ROON_DATAROOT = "/var/lib/${name}";
 
       serviceConfig = {
-        ExecStart = "${pkgs.roon-server}/opt/start.sh";
+        ExecStart = "${pkgs.roon-server}/start.sh";
         LimitNOFILE = 8192;
         User = cfg.user;
         Group = cfg.group;
         StateDirectory = name;
       };
     };
-    
+
     networking.firewall = mkIf cfg.openFirewall {
       allowedTCPPortRanges = [
         { from = 9100; to = 9200; }
@@ -60,7 +60,7 @@ in {
       allowedUDPPorts = [ 9003 ];
     };
 
-    
+
     users.groups.${cfg.group} = {};
     users.users.${cfg.user} =
       if cfg.user == "roon-server" then {
diff --git a/nixos/modules/services/audio/snapserver.nix b/nixos/modules/services/audio/snapserver.nix
index b0b9264e816..f614f0ba3e1 100644
--- a/nixos/modules/services/audio/snapserver.nix
+++ b/nixos/modules/services/audio/snapserver.nix
@@ -31,27 +31,42 @@ let
     let
       os = val:
         optionalString (val != null) "${val}";
-      os' = prefixx: val:
-        optionalString (val != null) (prefixx + "${val}");
+      os' = prefix: val:
+        optionalString (val != null) (prefix + "${val}");
       flatten = key: value:
         "&${key}=${value}";
     in
-      "-s ${opt.type}://" + os opt.location + "?" + os' "name=" name
-        + concatStrings (mapAttrsToList flatten opt.query);
+      "--stream.stream=\"${opt.type}://" + os opt.location + "?" + os' "name=" name
+        + concatStrings (mapAttrsToList flatten opt.query) + "\"";
 
   optionalNull = val: ret:
     optional (val != null) ret;
 
   optionString = concatStringsSep " " (mapAttrsToList streamToOption cfg.streams
-             ++ ["-p ${toString cfg.port}"]
-             ++ ["--controlPort ${toString cfg.controlPort}"]
-             ++ optionalNull cfg.sampleFormat "--sampleFormat ${cfg.sampleFormat}"
-             ++ optionalNull cfg.codec "-c ${cfg.codec}"
-             ++ optionalNull cfg.streamBuffer "--streamBuffer ${cfg.streamBuffer}"
-             ++ optionalNull cfg.buffer "-b ${cfg.buffer}"
-             ++ optional cfg.sendToMuted "--sendToMuted");
+    # global options
+    ++ [ "--stream.bind_to_address ${cfg.listenAddress}" ]
+    ++ [ "--stream.port ${toString cfg.port}" ]
+    ++ optionalNull cfg.sampleFormat "--stream.sampleformat ${cfg.sampleFormat}"
+    ++ optionalNull cfg.codec "--stream.codec ${cfg.codec}"
+    ++ optionalNull cfg.streamBuffer "--stream.stream_buffer ${cfg.streamBuffer}"
+    ++ optionalNull cfg.buffer "--stream.buffer ${cfg.buffer}"
+    ++ optional cfg.sendToMuted "--stream.send_to_muted"
+    # tcp json rpc
+    ++ [ "--tcp.enabled ${toString cfg.tcp.enable}" ]
+    ++ optionals cfg.tcp.enable [
+      "--tcp.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.port ${toString cfg.http.port}"
+    ] ++ optional (cfg.http.docRoot != null) "--http.doc_root \"${toString cfg.http.docRoot}\"");
 
 in {
+  imports = [
+    (mkRenamedOptionModule [ "services" "snapserver" "controlPort"] [ "services" "snapserver" "tcp" "port" ])
+  ];
 
   ###### interface
 
@@ -67,6 +82,15 @@ in {
         '';
       };
 
+      listenAddress = mkOption {
+        type = types.str;
+        default = "::";
+        example = "0.0.0.0";
+        description = ''
+          The address where snapclients can connect.
+        '';
+      };
+
       port = mkOption {
         type = types.port;
         default = 1704;
@@ -75,24 +99,100 @@ in {
         '';
       };
 
-      controlPort = mkOption {
+      openFirewall = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to automatically open the specified ports in the firewall.
+        '';
+      };
+
+      inherit sampleFormat;
+      inherit codec;
+
+      streamBuffer = mkOption {
+        type = with types; nullOr int;
+        default = null;
+        description = ''
+          Stream read (input) buffer in ms.
+        '';
+        example = 20;
+      };
+
+      buffer = mkOption {
+        type = with types; nullOr int;
+        default = null;
+        description = ''
+          Network buffer in ms.
+        '';
+        example = 1000;
+      };
+
+      sendToMuted = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Send audio to muted clients.
+        '';
+      };
+
+      tcp.enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable the JSON-RPC via TCP.
+        '';
+      };
+
+      tcp.listenAddress = mkOption {
+        type = types.str;
+        default = "::";
+        example = "0.0.0.0";
+        description = ''
+          The address where the TCP JSON-RPC listens on.
+        '';
+      };
+
+      tcp.port = mkOption {
         type = types.port;
         default = 1705;
         description = ''
-          The port for control connections (JSON-RPC).
+          The port where the TCP JSON-RPC listens on.
         '';
       };
 
-      openFirewall = mkOption {
+      http.enable = mkOption {
         type = types.bool;
         default = true;
         description = ''
-          Whether to automatically open the specified ports in the firewall.
+          Whether to enable the JSON-RPC via HTTP.
         '';
       };
 
-      inherit sampleFormat;
-      inherit codec;
+      http.listenAddress = mkOption {
+        type = types.str;
+        default = "::";
+        example = "0.0.0.0";
+        description = ''
+          The address where the HTTP JSON-RPC listens on.
+        '';
+      };
+
+      http.port = mkOption {
+        type = types.port;
+        default = 1780;
+        description = ''
+          The port where the HTTP JSON-RPC listens on.
+        '';
+      };
+
+      http.docRoot = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        description = ''
+          Path to serve from the HTTP servers root.
+        '';
+      };
 
       streams = mkOption {
         type = with types; attrsOf (submodule {
@@ -147,34 +247,7 @@ in {
           };
         '';
       };
-
-      streamBuffer = mkOption {
-        type = with types; nullOr int;
-        default = null;
-        description = ''
-          Stream read (input) buffer in ms.
-        '';
-        example = 20;
-      };
-
-      buffer = mkOption {
-        type = with types; nullOr int;
-        default = null;
-        description = ''
-          Network buffer in ms.
-        '';
-        example = 1000;
-      };
-
-      sendToMuted = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Send audio to muted clients.
-        '';
-      };
     };
-
   };
 
 
@@ -206,7 +279,10 @@ in {
       };
     };
 
-    networking.firewall.allowedTCPPorts = optionals cfg.openFirewall [ cfg.port cfg.controlPort ];
+    networking.firewall.allowedTCPPorts =
+      optionals cfg.openFirewall [ cfg.port ]
+      ++ optional cfg.tcp.enable cfg.tcp.port
+      ++ optional cfg.http.enable cfg.http.port;
   };
 
   meta = {
diff --git a/nixos/modules/services/audio/spotifyd.nix b/nixos/modules/services/audio/spotifyd.nix
index 4b74e753279..a589153248f 100644
--- a/nixos/modules/services/audio/spotifyd.nix
+++ b/nixos/modules/services/audio/spotifyd.nix
@@ -16,7 +16,7 @@ in
         type = types.lines;
         description = ''
           Configuration for Spotifyd. For syntax and directives, see
-          https://github.com/Spotifyd/spotifyd#Configuration.
+          <link xlink:href="https://github.com/Spotifyd/spotifyd#Configuration"/>.
         '';
       };
     };
diff --git a/nixos/modules/services/backup/bacula.nix b/nixos/modules/services/backup/bacula.nix
index cef304734ae..3d69a69038a 100644
--- a/nixos/modules/services/backup/bacula.nix
+++ b/nixos/modules/services/backup/bacula.nix
@@ -18,7 +18,7 @@ let
         Pid Directory = "/run";
         ${fd_cfg.extraClientConfig}
       }
-     
+
       ${concatStringsSep "\n" (mapAttrsToList (name: value: ''
       Director {
         Name = "${name}";
@@ -26,7 +26,7 @@ let
         Monitor = "${value.monitor}";
       }
       '') fd_cfg.director)}
-     
+
       Messages {
         Name = Standard;
         syslog = all, !skipped, !restored
@@ -35,7 +35,7 @@ let
     '';
 
   sd_cfg = config.services.bacula-sd;
-  sd_conf = pkgs.writeText "bacula-sd.conf" 
+  sd_conf = pkgs.writeText "bacula-sd.conf"
     ''
       Storage {
         Name = "${sd_cfg.name}";
@@ -80,7 +80,7 @@ let
     '';
 
   dir_cfg = config.services.bacula-dir;
-  dir_conf = pkgs.writeText "bacula-dir.conf" 
+  dir_conf = pkgs.writeText "bacula-dir.conf"
     ''
     Director {
       Name = "${dir_cfg.name}";
@@ -125,10 +125,10 @@ let
 
           The password is plain text. It is not generated through any special
           process but as noted above, it is better to use random text for
-          security reasons. 
+          security reasons.
         '';
       };
-      
+
       monitor = mkOption {
         default = "no";
         example = "yes";
@@ -140,7 +140,7 @@ let
 
           Please note that if this director is being used by a Monitor, we
           highly recommend to set this directive to yes to avoid serious
-          security problems. 
+          security problems.
         '';
       };
     };
@@ -163,7 +163,7 @@ let
           type of autochanger, what you specify here can vary. This directive
           is optional. See the Using AutochangersAutochangersChapter chapter of
           this manual for more details of using this and the following
-          autochanger directives.         
+          autochanger directives.
           '';
       };
 
@@ -200,7 +200,7 @@ let
           Extra configuration to be passed in Autochanger directive.
         '';
         example = ''
-   
+
         '';
       };
     };
@@ -222,7 +222,7 @@ let
           if you are archiving to disk storage. In this case, you must supply
           the full absolute path to the directory. When specifying a tape
           device, it is preferable that the "non-rewind" variant of the device
-          file name be given. 
+          file name be given.
         '';
       };
 
@@ -290,7 +290,7 @@ in {
           Whether to enable the Bacula File Daemon.
         '';
       };
- 
+
       name = mkOption {
         default = "${config.networking.hostName}-fd";
         description = ''
@@ -300,7 +300,7 @@ in {
           Clients. This directive is required.
         '';
       };
- 
+
       port = mkOption {
         default = 9102;
         type = types.int;
@@ -310,7 +310,7 @@ in {
           the Client resource of the Director's configuration file.
         '';
       };
- 
+
       director = mkOption {
         default = {};
         description = ''
@@ -349,14 +349,14 @@ in {
           Whether to enable Bacula Storage Daemon.
         '';
       };
- 
+
       name = mkOption {
         default = "${config.networking.hostName}-sd";
         description = ''
           Specifies the Name of the Storage daemon.
         '';
       };
- 
+
       port = mkOption {
         default = 9103;
         type = types.int;
@@ -410,7 +410,7 @@ in {
           console = all
         '';
       };
- 
+
     };
 
     services.bacula-dir = {
@@ -429,7 +429,7 @@ in {
           required.
         '';
       };
- 
+
       port = mkOption {
         default = 9101;
         type = types.int;
@@ -442,7 +442,7 @@ in {
           specify DirAddresses (N.B plural) directive.
         '';
       };
- 
+
       password = mkOption {
         # TODO: required?
         description = ''
diff --git a/nixos/modules/services/backup/restic.nix b/nixos/modules/services/backup/restic.nix
index 2388f1d6ca1..d869835bf07 100644
--- a/nixos/modules/services/backup/restic.nix
+++ b/nixos/modules/services/backup/restic.nix
@@ -31,6 +31,59 @@ in
           '';
         };
 
+        rcloneOptions = mkOption {
+          type = with types; nullOr (attrsOf (oneOf [ str bool ]));
+          default = null;
+          description = ''
+            Options to pass to rclone to control its behavior.
+            See <link xlink:href="https://rclone.org/docs/#options"/> for
+            available options. When specifying option names, strip the
+            leading <literal>--</literal>. To set a flag such as
+            <literal>--drive-use-trash</literal>, which does not take a value,
+            set the value to the Boolean <literal>true</literal>.
+          '';
+          example = {
+            bwlimit = "10M";
+            drive-use-trash = "true";
+          };
+        };
+
+        rcloneConfig = mkOption {
+          type = with types; nullOr (attrsOf (oneOf [ str bool ]));
+          default = null;
+          description = ''
+            Configuration for the rclone remote being used for backup.
+            See the remote's specific options under rclone's docs at
+            <link xlink:href="https://rclone.org/docs/"/>. When specifying
+            option names, use the "config" name specified in the docs.
+            For example, to set <literal>--b2-hard-delete</literal> for a B2
+            remote, use <literal>hard_delete = true</literal> in the
+            attribute set.
+            Warning: Secrets set in here will be world-readable in the Nix
+            store! Consider using the <literal>rcloneConfigFile</literal>
+            option instead to specify secret values separately. Note that
+            options set here will override those set in the config file.
+          '';
+          example = {
+            type = "b2";
+            account = "xxx";
+            key = "xxx";
+            hard_delete = true;
+          };
+        };
+
+        rcloneConfigFile = mkOption {
+          type = with types; nullOr path;
+          default = null;
+          description = ''
+            Path to the file containing rclone configuration. This file
+            must contain configuration for the remote specified in this backup
+            set and also must be readable by root. Options set in
+            <literal>rcloneConfig</literal> will override those set in this
+            file.
+          '';
+        };
+
         repository = mkOption {
           type = types.str;
           description = ''
@@ -170,11 +223,22 @@ in
             ( resticCmd + " forget --prune " + (concatStringsSep " " backup.pruneOpts) )
             ( resticCmd + " check" )
           ];
+          # Helper functions for rclone remotes
+          rcloneRemoteName = builtins.elemAt (splitString ":" backup.repository) 1;
+          rcloneAttrToOpt = v: "RCLONE_" + toUpper (builtins.replaceStrings [ "-" ] [ "_" ] v);
+          rcloneAttrToConf = v: "RCLONE_CONFIG_" + toUpper (rcloneRemoteName + "_" + v);
+          toRcloneVal = v: if lib.isBool v then lib.boolToString v else v;
         in nameValuePair "restic-backups-${name}" ({
           environment = {
             RESTIC_PASSWORD_FILE = backup.passwordFile;
             RESTIC_REPOSITORY = backup.repository;
-          };
+          } // optionalAttrs (backup.rcloneOptions != null) (mapAttrs' (name: value:
+            nameValuePair (rcloneAttrToOpt name) (toRcloneVal value)
+          ) backup.rcloneOptions) // optionalAttrs (backup.rcloneConfigFile != null) {
+            RCLONE_CONFIG = backup.rcloneConfigFile;
+          } // optionalAttrs (backup.rcloneConfig != null) (mapAttrs' (name: value:
+            nameValuePair (rcloneAttrToConf name) (toRcloneVal value)
+          ) backup.rcloneConfig);
           path = [ pkgs.openssh ];
           restartIfChanged = false;
           serviceConfig = {
diff --git a/nixos/modules/services/backup/zfs-replication.nix b/nixos/modules/services/backup/zfs-replication.nix
index 5a64304275d..6d75774c78f 100644
--- a/nixos/modules/services/backup/zfs-replication.nix
+++ b/nixos/modules/services/backup/zfs-replication.nix
@@ -18,7 +18,7 @@ in {
       };
 
       host = mkOption {
-        description = "Remote host where snapshots should be sent.";
+        description = "Remote host where snapshots should be sent. <literal>lz4</literal> is expected to be installed on this host.";
         example = "example.com";
         type = types.str;
       };
diff --git a/nixos/modules/services/computing/slurm/slurm.nix b/nixos/modules/services/computing/slurm/slurm.nix
index 050872e933f..705390a21d4 100644
--- a/nixos/modules/services/computing/slurm/slurm.nix
+++ b/nixos/modules/services/computing/slurm/slurm.nix
@@ -67,7 +67,7 @@ in
           type = types.bool;
           default = false;
           description = ''
-            Wether to enable the slurm control daemon.
+            Whether to enable the slurm control daemon.
             Note that the standard authentication method is "munge".
             The "munge" service needs to be provided with a password file in order for
             slurm to work properly (see <literal>services.munge.password</literal>).
@@ -135,7 +135,7 @@ in
         type = types.bool;
         default = false;
         description = ''
-          Wether to provide a slurm.conf file.
+          Whether to provide a slurm.conf file.
           Enable this option if you do not run a slurm daemon on this host
           (i.e. <literal>server.enable</literal> and <literal>client.enable</literal> are <literal>false</literal>)
           but you still want to run slurm commands from this host.
diff --git a/nixos/modules/services/computing/torque/mom.nix b/nixos/modules/services/computing/torque/mom.nix
index 83772539a7a..0c5f43cf3e6 100644
--- a/nixos/modules/services/computing/torque/mom.nix
+++ b/nixos/modules/services/computing/torque/mom.nix
@@ -60,4 +60,4 @@ in
     };
 
   };
-}      
+}
diff --git a/nixos/modules/services/computing/torque/server.nix b/nixos/modules/services/computing/torque/server.nix
index 655d1500497..21c5a4f4672 100644
--- a/nixos/modules/services/computing/torque/server.nix
+++ b/nixos/modules/services/computing/torque/server.nix
@@ -93,4 +93,4 @@ in
     };
 
   };
-}      
+}
diff --git a/nixos/modules/services/continuous-integration/buildbot/master.nix b/nixos/modules/services/continuous-integration/buildbot/master.nix
index e3da3092d45..e1950b91382 100644
--- a/nixos/modules/services/continuous-integration/buildbot/master.nix
+++ b/nixos/modules/services/continuous-integration/buildbot/master.nix
@@ -16,7 +16,7 @@ let
     factory = util.BuildFactory()
     c = BuildmasterConfig = dict(
      workers       = [${concatStringsSep "," cfg.workers}],
-     protocols     = { 'pb': {'port': ${toString cfg.bpPort} } },
+     protocols     = { 'pb': {'port': ${toString cfg.pbPort} } },
      title         = '${escapeStr cfg.title}',
      titleURL      = '${escapeStr cfg.titleUrl}',
      buildbotURL   = '${escapeStr cfg.buildbotUrl}',
@@ -25,7 +25,7 @@ let
      change_source = [ ${concatStringsSep "," cfg.changeSource} ],
      schedulers    = [ ${concatStringsSep "," cfg.schedulers} ],
      builders      = [ ${concatStringsSep "," cfg.builders} ],
-     status        = [ ${concatStringsSep "," cfg.status} ],
+     services      = [ ${concatStringsSep "," cfg.reporters} ],
     )
     for step in [ ${concatStringsSep "," cfg.factorySteps} ]:
       factory.addStep(step)
@@ -119,10 +119,10 @@ in {
         default = [ "worker.Worker('example-worker', 'pass')" ];
       };
 
-      status = mkOption {
+      reporters = mkOption {
         default = [];
         type = types.listOf types.str;
-        description = "List of status notification endpoints.";
+        description = "List of reporter objects used to present build status to various users.";
       };
 
       user = mkOption {
@@ -155,10 +155,20 @@ in {
         description = "Specifies the Buildbot directory.";
       };
 
-      bpPort = mkOption {
+      pbPort = mkOption {
         default = 9989;
-        type = types.int;
-        description = "Port where the master will listen to Buildbot Worker.";
+        type = types.either types.str types.int;
+        example = "'tcp:9990:interface=127.0.0.1'";
+        description = ''
+          The buildmaster will listen on a TCP port of your choosing
+          for connections from workers.
+          It can also use this port for connections from remote Change Sources,
+          status clients, and debug tools.
+          This port should be visible to the outside world, and you’ll need to tell
+          your worker admins about your choice.
+          If put in (single) quotes, this can also be used as a connection string,
+          as defined in the <link xlink:href="https://twistedmatrix.com/documents/current/core/howto/endpoints.html">ConnectionStrings guide</link>.
+        '';
       };
 
       listenAddress = mkOption {
@@ -264,5 +274,13 @@ in {
     };
   };
 
+  imports = [
+    (mkRenamedOptionModule [ "services" "buildbot-master" "bpPort" ] [ "services" "buildbot-master" "pbPort" ])
+    (mkRemovedOptionModule [ "services" "buildbot-master" "status" ] ''
+      Since Buildbot 0.9.0, status targets are deprecated and ignored.
+      Review your configuration and migrate to reporters (available at services.buildbot-master.reporters).
+    '')
+  ];
+
   meta.maintainers = with lib.maintainers; [ nand0p mic92 ];
 }
diff --git a/nixos/modules/services/continuous-integration/buildbot/worker.nix b/nixos/modules/services/continuous-integration/buildbot/worker.nix
index 52f24b8cee3..7b8a35f54bf 100644
--- a/nixos/modules/services/continuous-integration/buildbot/worker.nix
+++ b/nixos/modules/services/continuous-integration/buildbot/worker.nix
@@ -29,7 +29,7 @@ let
 
     with open('${cfg.workerPassFile}', 'r', encoding='utf-8') as passwd_file:
         passwd = passwd_file.read().strip('\r\n')
-    keepalive = 600
+    keepalive = ${toString cfg.keepalive}
     umask = None
     maxdelay = 300
     numcpus = None
@@ -116,6 +116,15 @@ in {
         description = "Specifies the Buildbot Worker connection string.";
       };
 
+      keepalive = mkOption {
+        default = 600;
+        type = types.int;
+        description = "
+          This is a number that indicates how frequently keepalive messages should be sent
+          from the worker to the buildmaster, expressed in seconds.
+        ";
+      };
+
       package = mkOption {
         type = types.package;
         default = pkgs.python3Packages.buildbot-worker;
diff --git a/nixos/modules/services/continuous-integration/gitlab-runner.nix b/nixos/modules/services/continuous-integration/gitlab-runner.nix
index eacfed85ddf..431555309cc 100644
--- a/nixos/modules/services/continuous-integration/gitlab-runner.nix
+++ b/nixos/modules/services/continuous-integration/gitlab-runner.nix
@@ -1,14 +1,16 @@
 { config, lib, pkgs, ... }:
+with builtins;
 with lib;
 let
   cfg = config.services.gitlab-runner;
   hasDocker = config.virtualisation.docker.enable;
-  hashedServices = with builtins; (mapAttrs' (name: service: nameValuePair
-    "${name}_${config.networking.hostName}_${
+  hashedServices = mapAttrs'
+    (name: service: nameValuePair
+      "${name}_${config.networking.hostName}_${
         substring 0 12
         (hashString "md5" (unsafeDiscardStringContext (toJSON service)))}"
       service)
-    cfg.services);
+    cfg.services;
   configPath = "$HOME/.gitlab-runner/config.toml";
   configureScript = pkgs.writeShellScriptBin "gitlab-runner-configure" (
     if (cfg.configFile != null) then ''
@@ -47,6 +49,8 @@ let
           ] ++ service.registrationFlags
             ++ optional (service.buildsDir != null)
             "--builds-dir ${service.buildsDir}"
+            ++ optional (service.cloneUrl != null)
+            "--clone-url ${service.cloneUrl}"
             ++ optional (service.preCloneScript != null)
             "--pre-clone-script ${service.preCloneScript}"
             ++ optional (service.preBuildScript != null)
@@ -76,7 +80,7 @@ let
               ++ map (v: "--docker-allowed-images ${escapeShellArg v}") service.dockerAllowedImages
               ++ map (v: "--docker-allowed-services ${escapeShellArg v}") service.dockerAllowedServices
             )
-          ))} && sleep 1
+          ))} && sleep 1 || exit 1
         fi
       '') hashedServices)}
 
@@ -89,8 +93,17 @@ let
 
       # update global options
       remarshal --if toml --of json ${configPath} \
-        | jq -cM '.check_interval = ${toString cfg.checkInterval} |
-                  .concurrent = ${toString cfg.concurrent}' \
+        | jq -cM ${escapeShellArg (concatStringsSep " | " [
+            ".check_interval = ${toJSON cfg.checkInterval}"
+            ".concurrent = ${toJSON cfg.concurrent}"
+            ".sentry_dsn = ${toJSON cfg.sentryDSN}"
+            ".listen_address = ${toJSON cfg.prometheusListenAddress}"
+            ".session_server.listen_address = ${toJSON cfg.sessionServer.listenAddress}"
+            ".session_server.advertise_address = ${toJSON cfg.sessionServer.advertiseAddress}"
+            ".session_server.session_timeout = ${toJSON cfg.sessionServer.sessionTimeout}"
+            "del(.[] | nulls)"
+            "del(.session_server[] | nulls)"
+          ])} \
         | remarshal --if json --of toml \
         | sponge ${configPath}
 
@@ -141,6 +154,66 @@ in
         0 does not mean unlimited.
       '';
     };
+    sentryDSN = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "https://public:private@host:port/1";
+      description = ''
+        Data Source Name for tracking of all system level errors to Sentry.
+      '';
+    };
+    prometheusListenAddress = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "localhost:8080";
+      description = ''
+        Address (&lt;host&gt;:&lt;port&gt;) on which the Prometheus metrics HTTP server
+        should be listening.
+      '';
+    };
+    sessionServer = mkOption {
+      type = types.submodule {
+        options = {
+          listenAddress = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            example = "0.0.0.0:8093";
+            description = ''
+              An internal URL to be used for the session server.
+            '';
+          };
+          advertiseAddress = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            example = "runner-host-name.tld:8093";
+            description = ''
+              The URL that the Runner will expose to GitLab to be used
+              to access the session server.
+              Fallbacks to <option>listenAddress</option> if not defined.
+            '';
+          };
+          sessionTimeout = mkOption {
+            type = types.int;
+            default = 1800;
+            description = ''
+              How long in seconds the session can stay active after
+              the job completes (which will block the job from finishing).
+            '';
+          };
+        };
+      };
+      default = { };
+      example = literalExample ''
+        {
+          listenAddress = "0.0.0.0:8093";
+        }
+      '';
+      description = ''
+        The session server allows the user to interact with jobs
+        that the Runner is responsible for. A good example of this is the
+        <link xlink:href="https://docs.gitlab.com/ee/ci/interactive_web_terminal/index.html">interactive web terminal</link>.
+      '';
+    };
     gracefulTermination = mkOption {
       type = types.bool;
       default = false;
@@ -306,6 +379,14 @@ in
               in context of selected executor (Locally, Docker, SSH).
             '';
           };
+          cloneUrl = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            example = "http://gitlab.example.local";
+            description = ''
+              Overwrite the URL for the GitLab instance. Used if the Runner can’t connect to GitLab on the URL GitLab exposes itself.
+            '';
+          };
           dockerImage = mkOption {
             type = types.nullOr types.str;
             default = null;
diff --git a/nixos/modules/services/databases/mysql.nix b/nixos/modules/services/databases/mysql.nix
index f9e657f5774..7d0a3f9afc4 100644
--- a/nixos/modules/services/databases/mysql.nix
+++ b/nixos/modules/services/databases/mysql.nix
@@ -6,12 +6,10 @@ let
 
   cfg = config.services.mysql;
 
-  mysql = cfg.package;
-
-  isMariaDB = lib.getName mysql == lib.getName pkgs.mariadb;
+  isMariaDB = lib.getName cfg.package == lib.getName pkgs.mariadb;
 
   mysqldOptions =
-    "--user=${cfg.user} --datadir=${cfg.dataDir} --basedir=${mysql}";
+    "--user=${cfg.user} --datadir=${cfg.dataDir} --basedir=${cfg.package}";
 
   settingsFile = pkgs.writeText "my.cnf" (
     generators.toINI { listsAsDuplicateKeys = true; } cfg.settings +
@@ -22,7 +20,7 @@ in
 
 {
   imports = [
-    (mkRemovedOptionModule [ "services" "mysql" "pidDir" ] "Don't wait for pidfiles, describe dependencies through systemd")
+    (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.")
   ];
 
@@ -32,13 +30,7 @@ in
 
     services.mysql = {
 
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = "
-          Whether to enable the MySQL server.
-        ";
-      };
+      enable = mkEnableOption "MySQL server";
 
       package = mkOption {
         type = types.package;
@@ -52,25 +44,31 @@ in
         type = types.nullOr types.str;
         default = null;
         example = literalExample "0.0.0.0";
-        description = "Address to bind to. The default is to bind to all addresses";
+        description = "Address to bind to. The default is to bind to all addresses.";
       };
 
       port = mkOption {
         type = types.int;
         default = 3306;
-        description = "Port of MySQL";
+        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.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "mysql";
+        description = "Group under which MySQL runs.";
       };
 
       dataDir = mkOption {
         type = types.path;
         example = "/var/lib/mysql";
-        description = "Location where MySQL stores its table files";
+        description = "Location where MySQL stores its table files.";
       };
 
       configFile = mkOption {
@@ -87,7 +85,6 @@ in
             datadir = /var/lib/mysql
             bind-address = 127.0.0.1
             port = 3336
-            plugin-load-add = auth_socket.so
 
             !includedir /etc/mysql/conf.d/
           ''';
@@ -178,7 +175,7 @@ in
       initialScript = mkOption {
         type = types.nullOr types.path;
         default = null;
-        description = "A file containing SQL statements to be executed on the first startup. Can be used for granting certain permissions on the database";
+        description = "A file containing SQL statements to be executed on the first startup. Can be used for granting certain permissions on the database.";
       };
 
       ensureDatabases = mkOption {
@@ -266,33 +263,33 @@ in
         serverId = mkOption {
           type = types.int;
           default = 1;
-          description = "Id of the MySQL server instance. This number must be unique for each instance";
+          description = "Id of the MySQL server instance. This number must be unique for each instance.";
         };
 
         masterHost = mkOption {
           type = types.str;
-          description = "Hostname of the MySQL master server";
+          description = "Hostname of the MySQL master server.";
         };
 
         slaveHost = mkOption {
           type = types.str;
-          description = "Hostname of the MySQL slave server";
+          description = "Hostname of the MySQL slave server.";
         };
 
         masterUser = mkOption {
           type = types.str;
-          description = "Username of the MySQL replication user";
+          description = "Username of the MySQL replication user.";
         };
 
         masterPassword = mkOption {
           type = types.str;
-          description = "Password of the MySQL replication user";
+          description = "Password of the MySQL replication user.";
         };
 
         masterPort = mkOption {
           type = types.int;
           default = 3306;
-          description = "Port number on which the MySQL master server runs";
+          description = "Port number on which the MySQL master server runs.";
         };
       };
     };
@@ -315,34 +312,42 @@ in
         datadir = cfg.dataDir;
         bind-address = mkIf (cfg.bind != null) cfg.bind;
         port = cfg.port;
-        plugin-load-add = optional (cfg.ensureUsers != []) "auth_socket.so";
       }
       (mkIf (cfg.replication.role == "master" || cfg.replication.role == "slave") {
         log-bin = "mysql-bin-${toString cfg.replication.serverId}";
         log-bin-index = "mysql-bin-${toString cfg.replication.serverId}.index";
         relay-log = "mysql-relay-bin";
         server-id = cfg.replication.serverId;
+        binlog-ignore-db = [ "information_schema" "performance_schema" "mysql" ];
+      })
+      (mkIf (!isMariaDB) {
+        plugin-load-add = "auth_socket.so";
       })
     ];
 
-    users.users.mysql = {
-      description = "MySQL server user";
-      group = "mysql";
-      uid = config.ids.uids.mysql;
+    users.users = optionalAttrs (cfg.user == "mysql") {
+      mysql = {
+        description = "MySQL server user";
+        group = cfg.group;
+        uid = config.ids.uids.mysql;
+      };
     };
 
-    users.groups.mysql.gid = config.ids.gids.mysql;
+    users.groups = optionalAttrs (cfg.group == "mysql") {
+      mysql.gid = config.ids.gids.mysql;
+    };
 
-    environment.systemPackages = [mysql];
+    environment.systemPackages = [ cfg.package ];
 
     environment.etc."my.cnf".source = cfg.configFile;
 
     systemd.tmpfiles.rules = [
-      "d '${cfg.dataDir}' 0700 ${cfg.user} mysql -"
+      "d '${cfg.dataDir}' 0700 '${cfg.user}' '${cfg.group}' - -"
+      "z '${cfg.dataDir}' 0700 '${cfg.user}' '${cfg.group}' - -"
     ];
 
     systemd.services.mysql = let
-      hasNotify = (cfg.package == pkgs.mariadb);
+      hasNotify = isMariaDB;
     in {
         description = "MySQL Server";
 
@@ -360,127 +365,153 @@ in
 
         preStart = if isMariaDB then ''
           if ! test -e ${cfg.dataDir}/mysql; then
-            ${mysql}/bin/mysql_install_db --defaults-file=/etc/my.cnf ${mysqldOptions}
-            touch /tmp/mysql_init
+            ${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
-            ${mysql}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} --initialize-insecure
-            touch /tmp/mysql_init
+            ${cfg.package}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} --initialize-insecure
+            touch ${cfg.dataDir}/mysql_init
           fi
         '';
 
-        serviceConfig = {
-          User = cfg.user;
-          Group = "mysql";
-          Type = if hasNotify then "notify" else "simple";
-          RuntimeDirectory = "mysqld";
-          RuntimeDirectoryMode = "0755";
-          Restart = "on-abort";
-          RestartSec = "5s";
-          # The last two environment variables are used for starting Galera clusters
-          ExecStart = "${mysql}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} $_WSREP_NEW_CLUSTER $_WSREP_START_POSITION";
-          ExecStartPost =
-            let
-              setupScript = pkgs.writeScript "mysql-setup" ''
-                #!${pkgs.runtimeShell} -e
-
-                ${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 /tmp/mysql_init ]
+        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
-                    ${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
-                            ''}
-                          ) | ${mysql}/bin/mysql -u root -N
-                      fi
-                    '') cfg.initialDatabases}
-
-                    ${optionalString (cfg.replication.role == "master")
-                      ''
-                        # Set up the replication master
+                    echo "Tried 30 times, giving up..."
+                    exit 1
+                fi
 
-                        ( 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}';"
-                        ) | ${mysql}/bin/mysql -u root -N
+                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 == "slave")
-                      ''
-                        # Set up the replication slave
+              ${optionalString (cfg.replication.role == "master")
+                ''
+                  # Set up the replication master
 
-                        ( echo "stop slave;"
-                          echo "change master to master_host='${cfg.replication.masterHost}', master_user='${cfg.replication.masterUser}', master_password='${cfg.replication.masterPassword}';"
-                          echo "set global slave_exec_mode='IDEMPOTENT';"
-                          echo "start slave;"
-                        ) | ${mysql}/bin/mysql -u root -N
-                      ''}
+                  ( 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.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} | ${mysql}/bin/mysql -u root -N
-                      ''}
+              ${optionalString (cfg.replication.role == "slave")
+                ''
+                  # Set up the replication slave
 
-                    rm /tmp/mysql_init
-                fi
+                  ( 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.ensureDatabases != []) ''
-                  (
-                  ${concatMapStrings (database: ''
-                    echo "CREATE DATABASE IF NOT EXISTS \`${database}\`;"
-                  '') cfg.ensureDatabases}
-                  ) | ${mysql}/bin/mysql -u root -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
                 ''}
 
-                ${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)}
-                    ) | ${mysql}/bin/mysql -u root -N
-                  '') cfg.ensureUsers}
-              '';
-            in
-              # ensureDatbases & ensureUsers depends on this script being run as root
-              # when the user has secured their mysql install
-              "+${setupScript}";
+              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
+            '') cfg.ensureUsers}
+        '';
+
+        serviceConfig = {
+          Type = if hasNotify then "notify" else "simple";
+          Restart = "on-abort";
+          RestartSec = "5s";
+          # The last two environment variables are used for starting Galera clusters
+          ExecStart = "${cfg.package}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} $_WSREP_NEW_CLUSTER $_WSREP_START_POSITION";
+          # User and group
+          User = cfg.user;
+          Group = cfg.group;
+          # Runtime directory and mode
+          RuntimeDirectory = "mysqld";
+          RuntimeDirectoryMode = "0755";
+          # Access write directories
+          ReadWritePaths = [ cfg.dataDir ];
+          # Capabilities
+          CapabilityBoundingSet = "";
+          # Security
+          NoNewPrivileges = true;
+          # Sandboxing
+          ProtectSystem = "strict";
+          ProtectHome = true;
+          PrivateTmp = true;
+          PrivateDevices = true;
+          ProtectHostname = true;
+          ProtectKernelTunables = true;
+          ProtectKernelModules = true;
+          ProtectControlGroups = true;
+          RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+          LockPersonality = true;
+          MemoryDenyWriteExecute = true;
+          RestrictRealtime = true;
+          RestrictSUIDSGID = true;
+          PrivateMounts = true;
+          # System Call Filtering
+          SystemCallArchitectures = "native";
         };
       };
 
diff --git a/nixos/modules/services/databases/openldap.nix b/nixos/modules/services/databases/openldap.nix
index 8c2851c37ac..7472538b887 100644
--- a/nixos/modules/services/databases/openldap.nix
+++ b/nixos/modules/services/databases/openldap.nix
@@ -5,14 +5,14 @@ with lib;
 let
 
   cfg = config.services.openldap;
-  openldap = pkgs.openldap;
+  openldap = cfg.package;
 
   dataFile = pkgs.writeText "ldap-contents.ldif" cfg.declarativeContents;
   configFile = pkgs.writeText "slapd.conf" ((optionalString cfg.defaultSchemas ''
-    include ${pkgs.openldap.out}/etc/schema/core.schema
-    include ${pkgs.openldap.out}/etc/schema/cosine.schema
-    include ${pkgs.openldap.out}/etc/schema/inetorgperson.schema
-    include ${pkgs.openldap.out}/etc/schema/nis.schema
+    include ${openldap.out}/etc/schema/core.schema
+    include ${openldap.out}/etc/schema/cosine.schema
+    include ${openldap.out}/etc/schema/inetorgperson.schema
+    include ${openldap.out}/etc/schema/nis.schema
   '') + ''
     ${cfg.extraConfig}
     database ${cfg.database}
@@ -46,6 +46,18 @@ in
         ";
       };
 
+      package = mkOption {
+        type = types.package;
+        default = pkgs.openldap;
+        description = ''
+          OpenLDAP package to use.
+
+          This can be used to, for example, set an OpenLDAP package
+          with custom overrides to enable modules or other
+          functionality.
+        '';
+      };
+
       user = mkOption {
         type = types.str;
         default = "openldap";
@@ -152,10 +164,10 @@ in
         ";
         example = literalExample ''
             '''
-            include ${pkgs.openldap.out}/etc/schema/core.schema
-            include ${pkgs.openldap.out}/etc/schema/cosine.schema
-            include ${pkgs.openldap.out}/etc/schema/inetorgperson.schema
-            include ${pkgs.openldap.out}/etc/schema/nis.schema
+            include ${openldap.out}/etc/schema/core.schema
+            include ${openldap.out}/etc/schema/cosine.schema
+            include ${openldap.out}/etc/schema/inetorgperson.schema
+            include ${openldap.out}/etc/schema/nis.schema
 
             database bdb
             suffix dc=example,dc=org
@@ -232,7 +244,7 @@ in
   };
 
   meta = {
-    maintainers = lib.maintainers.mic92;
+    maintainers = [ lib.maintainers.mic92 ];
   };
 
 
diff --git a/nixos/modules/services/databases/postgresql.nix b/nixos/modules/services/databases/postgresql.nix
index 93f5c1ca5f5..c726a08e34f 100644
--- a/nixos/modules/services/databases/postgresql.nix
+++ b/nixos/modules/services/databases/postgresql.nix
@@ -17,10 +17,11 @@ let
       hba_file = '${pkgs.writeText "pg_hba.conf" cfg.authentication}'
       ident_file = '${pkgs.writeText "pg_ident.conf" cfg.identMap}'
       log_destination = 'stderr'
+      log_line_prefix = '${cfg.logLinePrefix}'
       listen_addresses = '${if cfg.enableTCPIP then "*" else "localhost"}'
       port = ${toString cfg.port}
       ${cfg.extraConfig}
-    ''; 
+    '';
 
   groupAccessAvailable = versionAtLeast postgresql.version "11.0";
 
@@ -54,9 +55,13 @@ in
 
       dataDir = mkOption {
         type = types.path;
+        defaultText = "/var/lib/postgresql/\${config.services.postgresql.package.psqlSchema}";
         example = "/var/lib/postgresql/11";
         description = ''
-          Data directory for PostgreSQL.
+          The data directory for PostgreSQL. If left as the default value
+          this directory will automatically be created before the PostgreSQL server starts, otherwise
+          the sysadmin is responsible for ensuring the directory exists with appropriate ownership
+          and permissions.
         '';
       };
 
@@ -186,6 +191,17 @@ in
         '';
       };
 
+      logLinePrefix = mkOption {
+        type = types.str;
+        default = "[%p] ";
+        example = "%m [%p] ";
+        description = ''
+          A printf-style string that is output at the beginning of each log line.
+          Upstream default is <literal>'%m [%p] '</literal>, i.e. it includes the timestamp. We do
+          not include the timestamp, because journal has it anyway.
+        '';
+      };
+
       extraPlugins = mkOption {
         type = types.listOf types.path;
         default = [];
@@ -209,14 +225,15 @@ in
           Contents of the <filename>recovery.conf</filename> file.
         '';
       };
+
       superUser = mkOption {
         type = types.str;
-        default= if versionAtLeast config.system.stateVersion "17.09" then "postgres" else "root";
+        default = "postgres";
         internal = true;
+        readOnly = true;
         description = ''
-          NixOS traditionally used 'root' as superuser, most other distros use 'postgres'.
-          From 17.09 we also try to follow this standard. Internal since changing this value
-          would lead to breakage while setting up databases.
+          PostgreSQL superuser account to use for various operations. Internal since changing
+          this value would lead to breakage while setting up databases.
         '';
         };
     };
@@ -237,10 +254,7 @@ in
             else if versionAtLeast config.system.stateVersion "16.03" then pkgs.postgresql_9_5
             else throw "postgresql_9_4 was removed, please upgrade your postgresql version.");
 
-    services.postgresql.dataDir =
-      mkDefault (if versionAtLeast config.system.stateVersion "17.09"
-                  then "/var/lib/postgresql/${cfg.package.psqlSchema}"
-                  else "/var/db/postgresql");
+    services.postgresql.dataDir = mkDefault "/var/lib/postgresql/${cfg.package.psqlSchema}";
 
     services.postgresql.authentication = mkAfter
       ''
@@ -279,59 +293,28 @@ in
 
         preStart =
           ''
-            # Create data directory.
             if ! test -e ${cfg.dataDir}/PG_VERSION; then
-              mkdir -m 0700 -p ${cfg.dataDir}
+              # Cleanup the data directory.
               rm -f ${cfg.dataDir}/*.conf
-              chown -R postgres:postgres ${cfg.dataDir}
-            fi
-          ''; # */
 
-        script =
-          ''
-            # Initialise the database.
-            if ! test -e ${cfg.dataDir}/PG_VERSION; then
+              # Initialise the database.
               initdb -U ${cfg.superUser} ${concatStringsSep " " cfg.initdbArgs}
+
               # See postStart!
               touch "${cfg.dataDir}/.first_startup"
             fi
+
             ln -sfn "${configFile}" "${cfg.dataDir}/postgresql.conf"
             ${optionalString (cfg.recoveryConfig != null) ''
               ln -sfn "${pkgs.writeText "recovery.conf" cfg.recoveryConfig}" \
                 "${cfg.dataDir}/recovery.conf"
             ''}
-            ${optionalString (!groupAccessAvailable) ''
-              # postgresql pre 11.0 doesn't start if state directory mode is group accessible
-              chmod 0700 "${cfg.dataDir}"
-            ''}
-
-            exec postgres
           '';
 
-        serviceConfig =
-          { ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
-            User = "postgres";
-            Group = "postgres";
-            PermissionsStartOnly = true;
-            RuntimeDirectory = "postgresql";
-            Type = if versionAtLeast cfg.package.version "9.6"
-                   then "notify"
-                   else "simple";
-
-            # Shut down Postgres using SIGINT ("Fast Shutdown mode").  See
-            # http://www.postgresql.org/docs/current/static/server-shutdown.html
-            KillSignal = "SIGINT";
-            KillMode = "mixed";
-
-            # Give Postgres a decent amount of time to clean up after
-            # receiving systemd's SIGINT.
-            TimeoutSec = 120;
-          };
-
         # Wait for PostgreSQL to be ready to accept connections.
         postStart =
           ''
-            PSQL="${pkgs.sudo}/bin/sudo -u ${cfg.superUser} psql --port=${toString cfg.port}"
+            PSQL="psql --port=${toString cfg.port}"
 
             while ! $PSQL -d postgres -c "" 2> /dev/null; do
                 if ! kill -0 "$MAINPID"; then exit 1; fi
@@ -357,6 +340,32 @@ in
             '') cfg.ensureUsers}
           '';
 
+        serviceConfig = mkMerge [
+          { ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+            User = "postgres";
+            Group = "postgres";
+            RuntimeDirectory = "postgresql";
+            Type = if versionAtLeast cfg.package.version "9.6"
+                   then "notify"
+                   else "simple";
+
+            # Shut down Postgres using SIGINT ("Fast Shutdown mode").  See
+            # http://www.postgresql.org/docs/current/static/server-shutdown.html
+            KillSignal = "SIGINT";
+            KillMode = "mixed";
+
+            # Give Postgres a decent amount of time to clean up after
+            # receiving systemd's SIGINT.
+            TimeoutSec = 120;
+
+            ExecStart = "${postgresql}/bin/postgres";
+          }
+          (mkIf (cfg.dataDir == "/var/lib/postgresql/${cfg.package.psqlSchema}") {
+            StateDirectory = "postgresql postgresql/${cfg.package.psqlSchema}";
+            StateDirectoryMode = if groupAccessAvailable then "0750" else "0700";
+          })
+        ];
+
         unitConfig.RequiresMountsFor = "${cfg.dataDir}";
       };
 
diff --git a/nixos/modules/services/databases/redis.nix b/nixos/modules/services/databases/redis.nix
index 5c817422aae..f1777854e14 100644
--- a/nixos/modules/services/databases/redis.nix
+++ b/nixos/modules/services/databases/redis.nix
@@ -11,12 +11,11 @@ let
     port ${toString cfg.port}
     ${condOption "bind" cfg.bind}
     ${condOption "unixsocket" cfg.unixSocket}
-    daemonize yes
+    daemonize no
     supervised systemd
     loglevel ${cfg.logLevel}
     logfile ${cfg.logfile}
     syslog-enabled ${redisBool cfg.syslog}
-    pidfile /run/redis/redis.pid
     databases ${toString cfg.databases}
     ${concatMapStrings (d: "save ${toString (builtins.elemAt d 0)} ${toString (builtins.elemAt d 1)}\n") cfg.save}
     dbfilename dump.rdb
@@ -219,6 +218,7 @@ in
       description = "Redis database user";
       isSystemUser = true;
     };
+    users.groups.redis = {};
 
     environment.systemPackages = [ cfg.package ];
 
@@ -241,6 +241,7 @@ in
         StateDirectory = "redis";
         Type = "notify";
         User = "redis";
+        Group = "redis";
       };
     };
   };
diff --git a/nixos/modules/services/databases/rethinkdb.nix b/nixos/modules/services/databases/rethinkdb.nix
index f18fbaf5b06..c764d6c21c6 100644
--- a/nixos/modules/services/databases/rethinkdb.nix
+++ b/nixos/modules/services/databases/rethinkdb.nix
@@ -15,10 +15,7 @@ in
 
     services.rethinkdb = {
 
-      enable = mkOption {
-        default = false;
-        description = "Whether to enable the RethinkDB server.";
-      };
+      enable = mkEnableOption "RethinkDB server";
 
       #package = mkOption {
       #  default = pkgs.rethinkdb;
diff --git a/nixos/modules/services/databases/riak-cs.nix b/nixos/modules/services/databases/riak-cs.nix
index 2cb204f729a..fa6ac886331 100644
--- a/nixos/modules/services/databases/riak-cs.nix
+++ b/nixos/modules/services/databases/riak-cs.nix
@@ -35,7 +35,7 @@ in
           Name of the Erlang node.
         '';
       };
-      
+
       anonymousUserCreation = mkOption {
         type = types.bool;
         default = false;
diff --git a/nixos/modules/services/desktops/deepin/deepin.nix b/nixos/modules/services/desktops/deepin/deepin.nix
index 931bac58ace..f8fb73701af 100644
--- a/nixos/modules/services/desktops/deepin/deepin.nix
+++ b/nixos/modules/services/desktops/deepin/deepin.nix
@@ -41,7 +41,6 @@
         pkgs.deepin.dde-session-ui
         pkgs.deepin.deepin-anything
         pkgs.deepin.deepin-image-viewer
-        pkgs.deepin.deepin-screenshot
       ];
 
       services.dbus.packages = [
@@ -55,7 +54,6 @@
         pkgs.deepin.dde-session-ui
         pkgs.deepin.deepin-anything
         pkgs.deepin.deepin-image-viewer
-        pkgs.deepin.deepin-screenshot
       ];
 
       systemd.packages = [
diff --git a/nixos/modules/services/desktops/espanso.nix b/nixos/modules/services/desktops/espanso.nix
new file mode 100644
index 00000000000..cd2eadf8816
--- /dev/null
+++ b/nixos/modules/services/desktops/espanso.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let cfg = config.services.espanso;
+in {
+  meta = { maintainers = with lib.maintainers; [ numkem ]; };
+
+  options = {
+    services.espanso = { enable = options.mkEnableOption "Espanso"; };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.user.services.espanso = {
+      description = "Espanso daemon";
+      path = with pkgs; [ espanso libnotify xclip ];
+      serviceConfig = {
+        ExecStart = "${pkgs.espanso}/bin/espanso daemon";
+        Restart = "on-failure";
+      };
+      wantedBy = [ "default.target" ];
+    };
+
+    environment.systemPackages = [ pkgs.espanso ];
+  };
+}
diff --git a/nixos/modules/services/desktops/flatpak.nix b/nixos/modules/services/desktops/flatpak.nix
index 7fb0024f37d..7da92cc9f26 100644
--- a/nixos/modules/services/desktops/flatpak.nix
+++ b/nixos/modules/services/desktops/flatpak.nix
@@ -42,6 +42,7 @@ in {
     # It has been possible since https://github.com/flatpak/flatpak/releases/tag/1.3.2
     # to build a SELinux policy module.
 
+    # TODO: use sysusers.d
     users.users.flatpak = {
       description = "Flatpak system helper";
       group = "flatpak";
diff --git a/nixos/modules/services/desktops/malcontent.nix b/nixos/modules/services/desktops/malcontent.nix
index 5d6912595b5..1fbeb17e6ae 100644
--- a/nixos/modules/services/desktops/malcontent.nix
+++ b/nixos/modules/services/desktops/malcontent.nix
@@ -28,7 +28,10 @@ with lib;
       malcontent-ui
     ];
 
-    services.dbus.packages = [ pkgs.malcontent ];
+    services.dbus.packages = [
+      # D-Bus services are in `out`, not the default `bin` output that would be picked up by `makeDbusConf`.
+      pkgs.malcontent.out
+    ];
 
     services.accounts-daemon.enable = true;
 
diff --git a/nixos/modules/services/development/jupyter/default.nix b/nixos/modules/services/development/jupyter/default.nix
index e598b018645..6a5fd6b2940 100644
--- a/nixos/modules/services/development/jupyter/default.nix
+++ b/nixos/modules/services/development/jupyter/default.nix
@@ -6,10 +6,7 @@ let
 
   cfg = config.services.jupyter;
 
-  # NOTE: We don't use top-level jupyter because we don't
-  # want to pass in JUPYTER_PATH but use .environment instead,
-  # saving a rebuild.
-  package = pkgs.python3.pkgs.notebook;
+  package = cfg.package;
 
   kernels = (pkgs.jupyter-kernel.create  {
     definitions = if cfg.kernels != null
@@ -37,6 +34,27 @@ in {
       '';
     };
 
+    package = mkOption {
+      type = types.package;
+      # NOTE: We don't use top-level jupyter because we don't
+      # want to pass in JUPYTER_PATH but use .environment instead,
+      # saving a rebuild.
+      default = pkgs.python3.pkgs.notebook;
+      description = ''
+        Jupyter package to use.
+      '';
+    };
+
+    command = mkOption {
+      type = types.str;
+      default = "jupyter-notebook";
+      example = "jupyter-lab";
+      description = ''
+        Which command the service runs. Note that not all jupyter packages
+        have all commands, e.g. jupyter-lab isn't present in the default package.
+       '';
+    };
+
     port = mkOption {
       type = types.int;
       default = 8888;
@@ -157,7 +175,7 @@ in {
 
         serviceConfig = {
           Restart = "always";
-          ExecStart = ''${package}/bin/jupyter-notebook \
+          ExecStart = ''${package}/bin/${cfg.command} \
             --no-browser \
             --ip=${cfg.ip} \
             --port=${toString cfg.port} --port-retries 0 \
diff --git a/nixos/modules/services/development/jupyterhub/default.nix b/nixos/modules/services/development/jupyterhub/default.nix
new file mode 100644
index 00000000000..f1dcab68b00
--- /dev/null
+++ b/nixos/modules/services/development/jupyterhub/default.nix
@@ -0,0 +1,190 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.jupyterhub;
+
+  kernels = (pkgs.jupyter-kernel.create  {
+    definitions = if cfg.kernels != null
+      then cfg.kernels
+      else  pkgs.jupyter-kernel.default;
+  });
+
+  jupyterhubConfig = pkgs.writeText "jupyterhub_config.py" ''
+    c.JupyterHub.bind_url = "http://${cfg.host}:${toString cfg.port}"
+
+    c.JupyterHub.authenticator_class = "${cfg.authentication}"
+    c.JupyterHub.spawner_class = "${cfg.spawner}"
+
+    c.SystemdSpawner.default_url = '/lab'
+    c.SystemdSpawner.cmd = "${cfg.jupyterlabEnv}/bin/jupyterhub-singleuser"
+    c.SystemdSpawner.environment = {
+      'JUPYTER_PATH': '${kernels}'
+    }
+
+    ${cfg.extraConfig}
+  '';
+in {
+  meta.maintainers = with maintainers; [ costrouc ];
+
+  options.services.jupyterhub = {
+    enable = mkEnableOption "Jupyterhub development server";
+
+    authentication = mkOption {
+      type = types.str;
+      default = "jupyterhub.auth.PAMAuthenticator";
+      description = ''
+        Jupyterhub authentication to use
+
+        There are many authenticators available including: oauth, pam,
+        ldap, kerberos, etc.
+      '';
+    };
+
+    spawner = mkOption {
+      type = types.str;
+      default = "systemdspawner.SystemdSpawner";
+      description = ''
+        Jupyterhub spawner to use
+
+        There are many spawners available including: local process,
+        systemd, docker, kubernetes, yarn, batch, etc.
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        Extra contents appended to the jupyterhub configuration
+
+        Jupyterhub configuration is a normal python file using
+        Traitlets. https://jupyterhub.readthedocs.io/en/stable/getting-started/config-basics.html. The
+        base configuration of this module was designed to have sane
+        defaults for configuration but you can override anything since
+        this is a python file.
+      '';
+      example = literalExample ''
+         c.SystemdSpawner.mem_limit = '8G'
+         c.SystemdSpawner.cpu_limit = 2.0
+      '';
+    };
+
+    jupyterhubEnv = mkOption {
+      type = types.package;
+      default = (pkgs.python3.withPackages (p: with p; [
+        jupyterhub
+        jupyterhub-systemdspawner
+      ]));
+      description = ''
+        Python environment to run jupyterhub
+
+        Customizing will affect the packages available in the hub and
+        proxy. This will allow packages to be available for the
+        extraConfig that you may need. This will not normally need to
+        be changed.
+      '';
+    };
+
+    jupyterlabEnv = mkOption {
+      type = types.package;
+      default = (pkgs.python3.withPackages (p: with p; [
+        jupyterhub
+        jupyterlab
+      ]));
+      description = ''
+        Python environment to run jupyterlab
+
+        Customizing will affect the packages available in the
+        jupyterlab server and the default kernel provided. This is the
+        way to customize the jupyterlab extensions and jupyter
+        notebook extensions. This will not normally need to
+        be changed.
+      '';
+    };
+
+    kernels = mkOption {
+      type = types.nullOr (types.attrsOf(types.submodule (import ../jupyter/kernel-options.nix {
+        inherit lib;
+      })));
+
+      default = null;
+      example = literalExample ''
+        {
+          python3 = let
+            env = (pkgs.python3.withPackages (pythonPackages: with pythonPackages; [
+                    ipykernel
+                    pandas
+                    scikitlearn
+                  ]));
+          in {
+            displayName = "Python 3 for machine learning";
+            argv = [
+              "''${env.interpreter}"
+              "-m"
+              "ipykernel_launcher"
+              "-f"
+              "{connection_file}"
+            ];
+            language = "python";
+            logo32 = "''${env}/''${env.sitePackages}/ipykernel/resources/logo-32x32.png";
+            logo64 = "''${env}/''${env.sitePackages}/ipykernel/resources/logo-64x64.png";
+          };
+        }
+      '';
+      description = ''
+        Declarative kernel config
+
+        Kernels can be declared in any language that supports and has
+        the required dependencies to communicate with a jupyter server.
+        In python's case, it means that ipykernel package must always be
+        included in the list of packages of the targeted environment.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 8000;
+      description = ''
+        Port number Jupyterhub will be listening on
+      '';
+    };
+
+    host = mkOption {
+      type = types.str;
+      default = "0.0.0.0";
+      description = ''
+        Bind IP JupyterHub will be listening on
+      '';
+    };
+
+    stateDirectory = mkOption {
+      type = types.str;
+      default = "jupyterhub";
+      description = ''
+        Directory for jupyterhub state (token + database)
+      '';
+    };
+  };
+
+  config = mkMerge [
+    (mkIf cfg.enable  {
+      systemd.services.jupyterhub = {
+        description = "Jupyterhub development server";
+
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+
+        serviceConfig = {
+          Restart = "always";
+          ExecStart = "${cfg.jupyterhubEnv}/bin/jupyterhub --config ${jupyterhubConfig}";
+          User = "root";
+          StateDirectory = cfg.stateDirectory;
+          WorkingDirectory = "/var/lib/${cfg.stateDirectory}";
+        };
+      };
+    })
+  ];
+}
diff --git a/nixos/modules/services/editors/emacs.nix b/nixos/modules/services/editors/emacs.nix
index d791b387665..00d9eaad9eb 100644
--- a/nixos/modules/services/editors/emacs.nix
+++ b/nixos/modules/services/editors/emacs.nix
@@ -15,26 +15,27 @@ let
     fi
   '';
 
-desktopApplicationFile = pkgs.writeTextFile {
-  name = "emacsclient.desktop";
-  destination = "/share/applications/emacsclient.desktop";
-  text = ''
-[Desktop Entry]
-Name=Emacsclient
-GenericName=Text Editor
-Comment=Edit text
-MimeType=text/english;text/plain;text/x-makefile;text/x-c++hdr;text/x-c++src;text/x-chdr;text/x-csrc;text/x-java;text/x-moc;text/x-pascal;text/x-tcl;text/x-tex;application/x-shellscript;text/x-c;text/x-c++;
-Exec=emacseditor %F
-Icon=emacs
-Type=Application
-Terminal=false
-Categories=Development;TextEditor;
-StartupWMClass=Emacs
-Keywords=Text;Editor;
-'';
-};
-
-in {
+  desktopApplicationFile = pkgs.writeTextFile {
+    name = "emacsclient.desktop";
+    destination = "/share/applications/emacsclient.desktop";
+    text = ''
+      [Desktop Entry]
+      Name=Emacsclient
+      GenericName=Text Editor
+      Comment=Edit text
+      MimeType=text/english;text/plain;text/x-makefile;text/x-c++hdr;text/x-c++src;text/x-chdr;text/x-csrc;text/x-java;text/x-moc;text/x-pascal;text/x-tcl;text/x-tex;application/x-shellscript;text/x-c;text/x-c++;
+      Exec=emacseditor %F
+      Icon=emacs
+      Type=Application
+      Terminal=false
+      Categories=Development;TextEditor;
+      StartupWMClass=Emacs
+      Keywords=Text;Editor;
+    '';
+  };
+
+in
+{
 
   options.services.emacs = {
     enable = mkOption {
@@ -86,10 +87,10 @@ in {
       description = "Emacs: the extensible, self-documenting text editor";
 
       serviceConfig = {
-        Type      = "forking";
+        Type = "forking";
         ExecStart = "${pkgs.bash}/bin/bash -c 'source ${config.system.build.setEnvironment}; exec ${cfg.package}/bin/emacs --daemon'";
-        ExecStop  = "${cfg.package}/bin/emacsclient --eval (kill-emacs)";
-        Restart   = "always";
+        ExecStop = "${cfg.package}/bin/emacsclient --eval (kill-emacs)";
+        Restart = "always";
       };
     } // optionalAttrs cfg.enable { wantedBy = [ "default.target" ]; };
 
diff --git a/nixos/modules/services/editors/emacs.xml b/nixos/modules/services/editors/emacs.xml
index 74c60014dce..05f87df43bc 100644
--- a/nixos/modules/services/editors/emacs.xml
+++ b/nixos/modules/services/editors/emacs.xml
@@ -53,11 +53,11 @@
        <varname>emacs</varname>
       </term>
       <term>
-       <varname>emacs25</varname>
+       <varname>emacs</varname>
       </term>
       <listitem>
        <para>
-        The latest stable version of Emacs 25 using the
+        The latest stable version of Emacs using the
         <link
                 xlink:href="http://www.gtk.org">GTK 2</link>
         widget toolkit.
@@ -66,11 +66,11 @@
      </varlistentry>
      <varlistentry>
       <term>
-       <varname>emacs25-nox</varname>
+       <varname>emacs-nox</varname>
       </term>
       <listitem>
        <para>
-        Emacs 25 built without any dependency on X11 libraries.
+        Emacs built without any dependency on X11 libraries.
        </para>
       </listitem>
      </varlistentry>
@@ -79,11 +79,11 @@
        <varname>emacsMacport</varname>
       </term>
       <term>
-       <varname>emacs25Macport</varname>
+       <varname>emacsMacport</varname>
       </term>
       <listitem>
        <para>
-        Emacs 25 with the "Mac port" patches, providing a more native look and
+        Emacs with the "Mac port" patches, providing a more native look and
         feel under macOS.
        </para>
       </listitem>
diff --git a/nixos/modules/services/games/minetest-server.nix b/nixos/modules/services/games/minetest-server.nix
index 98e69c6dc0e..f52079fc1ef 100644
--- a/nixos/modules/services/games/minetest-server.nix
+++ b/nixos/modules/services/games/minetest-server.nix
@@ -5,12 +5,12 @@ with lib;
 let
   cfg   = config.services.minetest-server;
   flag  = val: name: if val != null then "--${name} ${val} " else "";
-  flags = [ 
-    (flag cfg.gameId "gameid") 
-    (flag cfg.world "world") 
-    (flag cfg.configPath "config") 
-    (flag cfg.logPath "logfile") 
-    (flag cfg.port "port") 
+  flags = [
+    (flag cfg.gameId "gameid")
+    (flag cfg.world "world")
+    (flag cfg.configPath "config")
+    (flag cfg.logPath "logfile")
+    (flag cfg.port "port")
   ];
 in
 {
@@ -26,7 +26,7 @@ in
         type        = types.nullOr types.str;
         default     = null;
         description = ''
-          Id of the game to use. To list available games run 
+          Id of the game to use. To list available games run
           `minetestserver --gameid list`.
 
           If only one game exists, this option can be null.
@@ -59,7 +59,7 @@ in
         type        = types.nullOr types.path;
         default     = null;
         description = ''
-          Path to logfile for logging. 
+          Path to logfile for logging.
 
           If set to null, logging will be output to stdout which means
           all output will be catched by systemd.
diff --git a/nixos/modules/services/games/teeworlds.nix b/nixos/modules/services/games/teeworlds.nix
new file mode 100644
index 00000000000..babf989c98c
--- /dev/null
+++ b/nixos/modules/services/games/teeworlds.nix
@@ -0,0 +1,119 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.teeworlds;
+  register = cfg.register;
+
+  teeworldsConf = pkgs.writeText "teeworlds.cfg" ''
+    sv_port ${toString cfg.port}
+    sv_register ${if cfg.register then "1" else "0"}
+    ${optionalString (cfg.name != null) "sv_name ${cfg.name}"}
+    ${optionalString (cfg.motd != null) "sv_motd ${cfg.motd}"}
+    ${optionalString (cfg.password != null) "password ${cfg.password}"}
+    ${optionalString (cfg.rconPassword != null) "sv_rcon_password ${cfg.rconPassword}"}
+    ${concatStringsSep "\n" cfg.extraOptions}
+  '';
+
+in
+{
+  options = {
+    services.teeworlds = {
+      enable = mkEnableOption "Teeworlds Server";
+
+      openPorts = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to open firewall ports for Teeworlds";
+      };
+
+      name = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Name of the server. Defaults to 'unnamed server'.
+        '';
+      };
+
+      register = mkOption {
+        type = types.bool;
+        example = true;
+        default = false;
+        description = ''
+          Whether the server registers as public server in the global server list. This is disabled by default because of privacy.
+        '';
+      };
+
+      motd = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Set the server message of the day text.
+        '';
+      };
+
+      password = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Password to connect to the server.
+        '';
+      };
+
+      rconPassword = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Password to access the remote console. If not set, a randomly generated one is displayed in the server log.
+        '';
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 8303;
+        description = ''
+          Port the server will listen on.
+        '';
+      };
+
+      extraOptions = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          Extra configuration lines for the <filename>teeworlds.cfg</filename>. See <link xlink:href="https://www.teeworlds.com/?page=docs&amp;wiki=server_settings">Teeworlds Documentation</link>.
+        '';
+        example = [ "sv_map dm1" "sv_gametype dm" ];
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    networking.firewall = mkIf cfg.openPorts {
+      allowedUDPPorts = [ cfg.port ];
+    };
+
+    systemd.services.teeworlds = {
+      description = "Teeworlds Server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        DynamicUser = true;
+        ExecStart = "${pkgs.teeworlds}/bin/teeworlds_srv -f ${teeworldsConf}";
+
+        # Hardening
+        CapabilityBoundingSet = false;
+        PrivateDevices = true;
+        PrivateUsers = true;
+        ProtectHome = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        SystemCallArchitectures = "native";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/games/terraria.nix b/nixos/modules/services/games/terraria.nix
index a59b74c0b4c..413660321ec 100644
--- a/nixos/modules/services/games/terraria.nix
+++ b/nixos/modules/services/games/terraria.nix
@@ -7,7 +7,7 @@ let
   worldSizeMap = { small = 1; medium = 2; large = 3; };
   valFlag = name: val: optionalString (val != null) "-${name} \"${escape ["\\" "\""] (toString val)}\"";
   boolFlag = name: val: optionalString val "-${name}";
-  flags = [ 
+  flags = [
     (valFlag "port" cfg.port)
     (valFlag "maxPlayers" cfg.maxPlayers)
     (valFlag "password" cfg.password)
diff --git a/nixos/modules/services/hardware/fwupd.nix b/nixos/modules/services/hardware/fwupd.nix
index e586af25c2b..222ac8e487e 100644
--- a/nixos/modules/services/hardware/fwupd.nix
+++ b/nixos/modules/services/hardware/fwupd.nix
@@ -6,6 +6,23 @@ with lib;
 
 let
   cfg = config.services.fwupd;
+
+  customEtc = {
+    "fwupd/daemon.conf" = {
+      source = pkgs.writeText "daemon.conf" ''
+        [fwupd]
+        BlacklistDevices=${lib.concatStringsSep ";" cfg.blacklistDevices}
+        BlacklistPlugins=${lib.concatStringsSep ";" cfg.blacklistPlugins}
+      '';
+    };
+    "fwupd/uefi.conf" = {
+      source = pkgs.writeText "uefi.conf" ''
+        [uefi]
+        OverrideESPMountPoint=${config.boot.loader.efi.efiSysMountPoint}
+      '';
+    };
+  };
+
   originalEtc =
     let
       mkEtcFile = n: nameValuePair n { source = "${cfg.package}/etc/${n}"; };
@@ -96,22 +113,8 @@ in {
 
     environment.systemPackages = [ cfg.package ];
 
-    environment.etc = {
-      "fwupd/daemon.conf" = {
-        source = pkgs.writeText "daemon.conf" ''
-          [fwupd]
-          BlacklistDevices=${lib.concatStringsSep ";" cfg.blacklistDevices}
-          BlacklistPlugins=${lib.concatStringsSep ";" cfg.blacklistPlugins}
-        '';
-      };
-      "fwupd/uefi.conf" = {
-        source = pkgs.writeText "uefi.conf" ''
-          [uefi]
-          OverrideESPMountPoint=${config.boot.loader.efi.efiSysMountPoint}
-        '';
-      };
-
-    } // originalEtc // extraTrustedKeys // testRemote;
+    # customEtc overrides some files from the package
+    environment.etc = originalEtc // customEtc // extraTrustedKeys // testRemote;
 
     services.dbus.packages = [ cfg.package ];
 
diff --git a/nixos/modules/services/hardware/thinkfan.nix b/nixos/modules/services/hardware/thinkfan.nix
index 7c105e99ca5..3bda61ed1a9 100644
--- a/nixos/modules/services/hardware/thinkfan.nix
+++ b/nixos/modules/services/hardware/thinkfan.nix
@@ -67,7 +67,7 @@ in {
         type = types.bool;
         default = false;
         description = ''
-          Whether to build thinkfan with SMART support to read temperatures 
+          Whether to build thinkfan with SMART support to read temperatures
           directly from hard disks.
         '';
       };
diff --git a/nixos/modules/services/hardware/tlp.nix b/nixos/modules/services/hardware/tlp.nix
index 3962d7b1598..4230f2edd27 100644
--- a/nixos/modules/services/hardware/tlp.nix
+++ b/nixos/modules/services/hardware/tlp.nix
@@ -8,12 +8,8 @@ let
   mkTlpConfig = tlpConfig: generators.toKeyValue {
     mkKeyValue = generators.mkKeyValueDefault {
       mkValueString = val:
-        if isInt val then toString val
-        else if isString val then val
-        else if true == val then "1"
-        else if false == val then "0"
-        else if isList val then "\"" + (concatStringsSep " " val) + "\""
-        else err "invalid value provided to mkTlpConfig:" (toString val);
+        if isList val then "\"" + (toString val) + "\""
+        else toString val;
     } "=";
   } tlpConfig;
 in
@@ -27,10 +23,24 @@ in
         description = "Whether to enable the TLP power management daemon.";
       };
 
+      settings = mkOption {type = with types; attrsOf (oneOf [bool int float str (listOf str)]);
+        default = {};
+        example = {
+          SATA_LINKPWR_ON_BAT = "med_power_with_dipm";
+          USB_BLACKLIST_PHONE = 1;
+        };
+        description = ''
+          Options passed to TLP. See https://linrunner.de/tlp for all supported options..
+        '';
+      };
+
       extraConfig = mkOption {
         type = types.lines;
         default = "";
-        description = "Additional configuration variables for TLP";
+        description = ''
+          Verbatim additional configuration variables for TLP.
+          DEPRECATED: use services.tlp.config instead.
+        '';
       };
     };
   };
@@ -39,8 +49,20 @@ in
   config = mkIf cfg.enable {
     boot.kernelModules = [ "msr" ];
 
+    warnings = optional (cfg.extraConfig != "") ''
+      Using config.services.tlp.extraConfig is deprecated and will become unsupported in a future release. Use config.services.tlp.settings instead.
+    '';
+
+    assertions = [{
+      assertion = cfg.enable -> config.powerManagement.scsiLinkPolicy == null;
+      message = ''
+        `services.tlp.enable` and `config.powerManagement.scsiLinkPolicy` cannot be set both.
+        Set `services.tlp.settings.SATA_LINKPWR_ON_AC` and `services.tlp.settings.SATA_LINKPWR_ON_BAT` instead.
+      '';
+    }];
+
     environment.etc = {
-      "tlp.conf".text = cfg.extraConfig;
+      "tlp.conf".text = (mkTlpConfig cfg.settings) + cfg.extraConfig;
     } // optionalAttrs enableRDW {
       "NetworkManager/dispatcher.d/99tlp-rdw-nm".source =
         "${tlp}/etc/NetworkManager/dispatcher.d/99tlp-rdw-nm";
@@ -48,18 +70,25 @@ in
 
     environment.systemPackages = [ tlp ];
 
-    # FIXME: When the config is parametrized we need to move these into a
-    # conditional on the relevant options being enabled.
-    powerManagement = {
-      scsiLinkPolicy = null;
-      cpuFreqGovernor = null;
-      cpufreq.max = null;
-      cpufreq.min = null;
+
+    services.tlp.settings = let
+      cfg = config.powerManagement;
+      maybeDefault = val: lib.mkIf (val != null) (lib.mkDefault val);
+    in {
+      CPU_SCALING_GOVERNOR_ON_AC = maybeDefault cfg.cpuFreqGovernor;
+      CPU_SCALING_GOVERNOR_ON_BAT = maybeDefault cfg.cpuFreqGovernor;
+      CPU_SCALING_MIN_FREQ_ON_AC = maybeDefault cfg.cpufreq.min;
+      CPU_SCALING_MAX_FREQ_ON_AC = maybeDefault cfg.cpufreq.max;
+      CPU_SCALING_MIN_FREQ_ON_BAT = maybeDefault cfg.cpufreq.min;
+      CPU_SCALING_MAX_FREQ_ON_BAT = maybeDefault cfg.cpufreq.max;
     };
 
     services.udev.packages = [ tlp ];
 
     systemd = {
+      # use native tlp instead because it can also differentiate between AC/BAT
+      services.cpufreq.enable = false;
+
       packages = [ tlp ];
       # XXX: These must always be disabled/masked according to [1].
       #
diff --git a/nixos/modules/services/hardware/trezord.nix b/nixos/modules/services/hardware/trezord.nix
index c517e9fbb2b..2594ac74371 100644
--- a/nixos/modules/services/hardware/trezord.nix
+++ b/nixos/modules/services/hardware/trezord.nix
@@ -10,7 +10,7 @@ in {
   meta = {
     doc = ./trezord.xml;
   };
-  
+
   ### interface
 
   options = {
@@ -40,7 +40,7 @@ in {
       };
     };
   };
-  
+
   ### implementation
 
   config = mkIf cfg.enable {
diff --git a/nixos/modules/services/hardware/u2f.nix b/nixos/modules/services/hardware/u2f.nix
deleted file mode 100644
index bb4b2f05f89..00000000000
--- a/nixos/modules/services/hardware/u2f.nix
+++ /dev/null
@@ -1,23 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-let
-  cfg = config.hardware.u2f;
-in {
-  options = {
-    hardware.u2f = {
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Enable U2F hardware support.
-        '';
-      };
-    };
-  };
-
-  config = mkIf cfg.enable {
-    services.udev.packages = [ pkgs.libu2f-host ];
-  };
-}
-
diff --git a/nixos/modules/services/hardware/udev.nix b/nixos/modules/services/hardware/udev.nix
index 168056a475e..587b9b0234a 100644
--- a/nixos/modules/services/hardware/udev.nix
+++ b/nixos/modules/services/hardware/udev.nix
@@ -83,6 +83,10 @@ let
       run_progs=$(grep -v '^[[:space:]]*#' $out/* | grep 'RUN+="/' |
         sed -e 's/.*RUN+="\([^ "]*\)[ "].*/\1/' | uniq)
       for i in $import_progs $run_progs; do
+        # if the path refers to /run/current-system/systemd, replace with config.systemd.package
+        if [[ $i == /run/current-system/systemd* ]]; then
+          i="${config.systemd.package}/''${i#/run/current-system/systemd/}"
+        fi
         if [[ ! -x $i ]]; then
           echo "FAIL"
           echo "$i is called in udev rules but is not executable or does not exist"
diff --git a/nixos/modules/services/hardware/undervolt.nix b/nixos/modules/services/hardware/undervolt.nix
index e5ef0601de3..054ffa35050 100644
--- a/nixos/modules/services/hardware/undervolt.nix
+++ b/nixos/modules/services/hardware/undervolt.nix
@@ -1,18 +1,35 @@
 { config, pkgs, lib, ... }:
 
 with lib;
-
 let
   cfg = config.services.undervolt;
-in {
+  cliArgs = lib.cli.toGNUCommandLineShell {} {
+    inherit (cfg)
+      verbose
+      temp
+      ;
+    # `core` and `cache` are both intentionally set to `cfg.coreOffset` as according to the undervolt docs:
+    #
+    #     Core or Cache offsets have no effect. It is not possible to set different offsets for
+    #     CPU Core and Cache. The CPU will take the smaller of the two offsets, and apply that to
+    #     both CPU and Cache. A warning message will be displayed if you attempt to set different offsets.
+    core = cfg.coreOffset;
+    cache = cfg.coreOffset;
+    gpu = cfg.gpuOffset;
+    uncore = cfg.uncoreOffset;
+    analogio = cfg.analogioOffset;
+
+    temp-bat = cfg.tempBat;
+    temp-ac = cfg.tempAc;
+  };
+in
+{
   options.services.undervolt = {
-    enable = mkOption {
-      type = types.bool;
-      default = false;
-      description = ''
-        Whether to undervolt intel cpus.
-      '';
-    };
+    enable = mkEnableOption ''
+       Undervolting service for Intel CPUs.
+
+       Warning: This service is not endorsed by Intel and may permanently damage your hardware. Use at your own risk!
+    '';
 
     verbose = mkOption {
       type = types.bool;
@@ -32,58 +49,69 @@ in {
     };
 
     coreOffset = mkOption {
-      type = types.nullOr types.str;
+      type = types.nullOr types.int;
       default = null;
       description = ''
-        The amount of voltage to offset the CPU cores by. Accepts a floating point number.
+        The amount of voltage in mV to offset the CPU cores by.
       '';
     };
 
     gpuOffset = mkOption {
-      type = types.nullOr types.str;
+      type = types.nullOr types.int;
       default = null;
       description = ''
-        The amount of voltage to offset the GPU by. Accepts a floating point number.
+        The amount of voltage in mV to offset the GPU by.
       '';
     };
 
     uncoreOffset = mkOption {
-      type = types.nullOr types.str;
+      type = types.nullOr types.int;
       default = null;
       description = ''
-        The amount of voltage to offset uncore by. Accepts a floating point number.
+        The amount of voltage in mV to offset uncore by.
       '';
     };
 
     analogioOffset = mkOption {
-      type = types.nullOr types.str;
+      type = types.nullOr types.int;
       default = null;
       description = ''
-        The amount of voltage to offset analogio by. Accepts a floating point number.
+        The amount of voltage in mV to offset analogio by.
       '';
     };
 
     temp = mkOption {
-      type = types.nullOr types.str;
+      type = types.nullOr types.int;
       default = null;
       description = ''
-        The temperature target. Accepts a floating point number.
+        The temperature target in Celsius degrees.
       '';
     };
 
     tempAc = mkOption {
-      type = types.nullOr types.str;
+      type = types.nullOr types.int;
       default = null;
       description = ''
-        The temperature target on AC power. Accepts a floating point number.
+        The temperature target on AC power in Celsius degrees.
       '';
     };
 
     tempBat = mkOption {
-      type = types.nullOr types.str;
+      type = types.nullOr types.int;
       default = null;
       description = ''
-        The temperature target on battery power. Accepts a floating point number.
+        The temperature target on battery power in Celsius degrees.
+      '';
+    };
+
+    useTimer = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to set a timer that applies the undervolt settings every 30s.
+        This will cause spam in the journal but might be required for some
+        hardware under specific conditions.
+        Enable this if your undervolt settings don't hold.
       '';
     };
   };
@@ -97,31 +125,19 @@ in {
       path = [ pkgs.undervolt ];
 
       description = "Intel Undervolting Service";
+
+      # Apply undervolt on boot, nixos generation switch and resume
+      wantedBy = [ "multi-user.target" "post-resume.target" ];
+      after = [ "post-resume.target" ]; # Not sure why but it won't work without this
+
       serviceConfig = {
         Type = "oneshot";
         Restart = "no";
-
-        # `core` and `cache` are both intentionally set to `cfg.coreOffset` as according to the undervolt docs:
-        #
-        #     Core or Cache offsets have no effect. It is not possible to set different offsets for
-        #     CPU Core and Cache. The CPU will take the smaller of the two offsets, and apply that to
-        #     both CPU and Cache. A warning message will be displayed if you attempt to set different offsets.
-        ExecStart = ''
-          ${pkgs.undervolt}/bin/undervolt \
-            ${optionalString cfg.verbose "--verbose"} \
-            ${optionalString (cfg.coreOffset != null) "--core ${cfg.coreOffset}"} \
-            ${optionalString (cfg.coreOffset != null) "--cache ${cfg.coreOffset}"} \
-            ${optionalString (cfg.gpuOffset != null) "--gpu ${cfg.gpuOffset}"} \
-            ${optionalString (cfg.uncoreOffset != null) "--uncore ${cfg.uncoreOffset}"} \
-            ${optionalString (cfg.analogioOffset != null) "--analogio ${cfg.analogioOffset}"} \
-            ${optionalString (cfg.temp != null) "--temp ${cfg.temp}"} \
-            ${optionalString (cfg.tempAc != null) "--temp-ac ${cfg.tempAc}"} \
-            ${optionalString (cfg.tempBat != null) "--temp-bat ${cfg.tempBat}"}
-        '';
+        ExecStart = "${pkgs.undervolt}/bin/undervolt ${cliArgs}";
       };
     };
 
-    systemd.timers.undervolt = {
+    systemd.timers.undervolt = mkIf cfg.useTimer {
       description = "Undervolt timer to ensure voltage settings are always applied";
       partOf = [ "undervolt.service" ];
       wantedBy = [ "multi-user.target" ];
diff --git a/nixos/modules/services/logging/logrotate.nix b/nixos/modules/services/logging/logrotate.nix
index fdd9f0f3e5c..7d6102b8255 100644
--- a/nixos/modules/services/logging/logrotate.nix
+++ b/nixos/modules/services/logging/logrotate.nix
@@ -5,42 +5,165 @@ with lib;
 let
   cfg = config.services.logrotate;
 
-  configFile = pkgs.writeText "logrotate.conf"
-    cfg.config;
+  pathOpts = {
+    options = {
+      enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable log rotation for this path. This can be used to explicitly disable
+          logging that has been configured by NixOS.
+        '';
+      };
+
+      path = mkOption {
+        type = types.str;
+        description = ''
+          The path to log files to be rotated.
+        '';
+      };
+
+      user = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          The user account to use for rotation.
+        '';
+      };
+
+      group = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          The group to use for rotation.
+        '';
+      };
+
+      frequency = mkOption {
+        type = types.enum [ "daily" "weekly" "monthly" "yearly" ];
+        default = "daily";
+        description = ''
+          How often to rotate the logs.
+        '';
+      };
+
+      keep = mkOption {
+        type = types.int;
+        default = 20;
+        description = ''
+          How many rotations to keep.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra logrotate config options for this path. Refer to
+          <link xlink:href="https://linux.die.net/man/8/logrotate"/> for details.
+        '';
+      };
+
+      priority = mkOption {
+        type = types.int;
+        default = 1000;
+        description = ''
+          Order of this logrotate block in relation to the others. The semantics are
+          the same as with `lib.mkOrder`. Smaller values have a greater priority.
+        '';
+      };
+    };
+
+    config.extraConfig = ''
+      missingok
+      notifempty
+    '';
+  };
+
+  mkConf = pathOpts: ''
+    # generated by NixOS using the `services.logrotate.paths.${pathOpts.name}` attribute set
+    "${pathOpts.path}" {
+      ${optionalString (pathOpts.user != null || pathOpts.group != null) "su ${pathOpts.user} ${pathOpts.group}"}
+      ${pathOpts.frequency}
+      rotate ${toString pathOpts.keep}
+      ${pathOpts.extraConfig}
+    }
+  '';
+
+  paths = sortProperties (mapAttrsToList (name: pathOpts: pathOpts // { name = name; }) (filterAttrs (_: pathOpts: pathOpts.enable) cfg.paths));
+  configFile = pkgs.writeText "logrotate.conf" (concatStringsSep "\n" ((map mkConf paths) ++ [ cfg.extraConfig ]));
 
 in
 {
+  imports = [
+    (mkRenamedOptionModule [ "services" "logrotate" "config" ] [ "services" "logrotate" "extraConfig" ])
+  ];
+
   options = {
     services.logrotate = {
-      enable = mkOption {
-        type = lib.types.bool;
-        default = false;
+      enable = mkEnableOption "the logrotate systemd service";
+
+      paths = mkOption {
+        type = with types; attrsOf (submodule pathOpts);
+        default = {};
         description = ''
-          Enable the logrotate cron job
+          Attribute set of paths to rotate. The order each block appears in the generated configuration file
+          can be controlled by the <link linkend="opt-services.logrotate.paths._name_.priority">priority</link> option
+          using the same semantics as `lib.mkOrder`. Smaller values have a greater priority.
+        '';
+        example = literalExample ''
+          {
+            httpd = {
+              path = "/var/log/httpd/*.log";
+              user = config.services.httpd.user;
+              group = config.services.httpd.group;
+              keep = 7;
+            };
+
+            myapp = {
+              path = "/var/log/myapp/*.log";
+              user = "myuser";
+              group = "mygroup";
+              frequency = "weekly";
+              keep = 5;
+              priority = 1;
+            };
+          }
         '';
       };
 
-      config = mkOption {
+      extraConfig = mkOption {
         default = "";
         type = types.lines;
         description = ''
-          The contents of the logrotate config file
+          Extra contents to append to the logrotate configuration file. Refer to
+          <link xlink:href="https://linux.die.net/man/8/logrotate"/> for details.
         '';
       };
     };
   };
 
   config = mkIf cfg.enable {
-    systemd.services.logrotate = {
-      description   = "Logrotate Service";
-      wantedBy      = [ "multi-user.target" ];
-      startAt       = "*-*-* *:05:00";
+    assertions = mapAttrsToList (name: pathOpts:
+      { assertion = (pathOpts.user != null) == (pathOpts.group != null);
+        message = ''
+          If either of `services.logrotate.paths.${name}.user` or `services.logrotate.paths.${name}.group` are specified then *both* must be specified.
+        '';
+      }
+    ) cfg.paths;
 
-      serviceConfig.Restart = "no";
-      serviceConfig.User    = "root";
+    systemd.services.logrotate = {
+      description = "Logrotate Service";
+      wantedBy = [ "multi-user.target" ];
+      startAt = "*-*-* *:05:00";
       script = ''
         exec ${pkgs.logrotate}/sbin/logrotate ${configFile}
       '';
+
+      serviceConfig = {
+        Restart = "no";
+        User = "root";
+      };
     };
   };
 }
diff --git a/nixos/modules/services/logging/logstash.nix b/nixos/modules/services/logging/logstash.nix
index 21a83803fd8..bf92425f998 100644
--- a/nixos/modules/services/logging/logstash.nix
+++ b/nixos/modules/services/logging/logstash.nix
@@ -4,13 +4,9 @@ with lib;
 
 let
   cfg = config.services.logstash;
-  pluginPath = lib.concatStringsSep ":" cfg.plugins;
-  havePluginPath = lib.length cfg.plugins > 0;
   ops = lib.optionalString;
   verbosityFlag = "--log.level " + cfg.logLevel;
 
-  pluginsPath = "--path.plugins ${pluginPath}";
-
   logstashConf = pkgs.writeText "logstash.conf" ''
     input {
       ${cfg.inputConfig}
@@ -173,7 +169,7 @@ in
         ExecStart = concatStringsSep " " (filter (s: stringLength s != 0) [
           "${cfg.package}/bin/logstash"
           "-w ${toString cfg.filterWorkers}"
-          (ops havePluginPath pluginsPath)
+          (concatMapStringsSep " " (x: "--path.plugins ${x}") cfg.plugins)
           "${verbosityFlag}"
           "-f ${logstashConf}"
           "--path.settings ${logstashSettingsDir}"
diff --git a/nixos/modules/services/mail/dovecot.nix b/nixos/modules/services/mail/dovecot.nix
index 9fbf0c19752..c166ef68f29 100644
--- a/nixos/modules/services/mail/dovecot.nix
+++ b/nixos/modules/services/mail/dovecot.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, ... }:
+{ options, config, lib, pkgs, ... }:
 
 with lib;
 
@@ -83,11 +83,11 @@ let
     )
 
     (
-      optionalString (cfg.mailboxes != []) ''
+      optionalString (cfg.mailboxes != {}) ''
         protocol imap {
           namespace inbox {
             inbox=yes
-            ${concatStringsSep "\n" (map mailboxConfig cfg.mailboxes)}
+            ${concatStringsSep "\n" (map mailboxConfig (attrValues cfg.mailboxes))}
           }
         }
       ''
@@ -125,15 +125,19 @@ let
   mailboxConfig = mailbox: ''
     mailbox "${mailbox.name}" {
       auto = ${toString mailbox.auto}
+  '' + optionalString (mailbox.autoexpunge != null) ''
+    autoexpunge = ${mailbox.autoexpunge}
   '' + optionalString (mailbox.specialUse != null) ''
     special_use = \${toString mailbox.specialUse}
   '' + "}";
 
-  mailboxes = { ... }: {
+  mailboxes = { name, ... }: {
     options = {
       name = mkOption {
         type = types.strMatching ''[^"]+'';
         example = "Spam";
+        default = name;
+        readOnly = true;
         description = "The name of the mailbox.";
       };
       auto = mkOption {
@@ -148,6 +152,15 @@ let
         example = "Junk";
         description = "Null if no special use flag is set. Other than that every use flag mentioned in the RFC is valid.";
       };
+      autoexpunge = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "60d";
+        description = ''
+          To automatically remove all email from the mailbox which is older than the
+          specified time.
+        '';
+      };
     };
   };
 in
@@ -323,9 +336,16 @@ in
     };
 
     mailboxes = mkOption {
-      type = types.listOf (types.submodule mailboxes);
-      default = [];
-      example = [ { name = "Spam"; specialUse = "Junk"; auto = "create"; } ];
+      type = with types; coercedTo
+        (listOf unspecified)
+        (list: listToAttrs (map (entry: { name = entry.name; value = removeAttrs entry ["name"]; }) list))
+        (attrsOf (submodule mailboxes));
+      default = {};
+      example = literalExample ''
+        {
+          Spam = { specialUse = "Junk"; auto = "create"; };
+        }
+      '';
       description = "Configure mailboxes and auto create or subscribe them.";
     };
 
@@ -444,6 +464,10 @@ in
 
     environment.systemPackages = [ dovecotPkg ];
 
+    warnings = mkIf (any isList options.services.dovecot2.mailboxes.definitions) [
+      "Declaring `services.dovecot2.mailboxes' as a list is deprecated and will break eval in 21.03! See the release notes for more info for migration."
+    ];
+
     assertions = [
       {
         assertion = intersectLists cfg.protocols [ "pop3" "imap" ] != [];
diff --git a/nixos/modules/services/mail/mailman.nix b/nixos/modules/services/mail/mailman.nix
index f5e78b18293..5c61cfbebf6 100644
--- a/nixos/modules/services/mail/mailman.nix
+++ b/nixos/modules/services/mail/mailman.nix
@@ -6,42 +6,46 @@ let
 
   cfg = config.services.mailman;
 
+  pythonEnv = pkgs.python3.withPackages (ps:
+    [ps.mailman ps.mailman-web]
+    ++ lib.optional cfg.hyperkitty.enable ps.mailman-hyperkitty
+    ++ cfg.extraPythonPackages);
+
   # This deliberately doesn't use recursiveUpdate so users can
   # override the defaults.
-  settings = {
+  webSettings = {
     DEFAULT_FROM_EMAIL = cfg.siteOwner;
     SERVER_EMAIL = cfg.siteOwner;
     ALLOWED_HOSTS = [ "localhost" "127.0.0.1" ] ++ cfg.webHosts;
     COMPRESS_OFFLINE = true;
-    STATIC_ROOT = "/var/lib/mailman-web/static";
+    STATIC_ROOT = "/var/lib/mailman-web-static";
     MEDIA_ROOT = "/var/lib/mailman-web/media";
+    LOGGING = {
+      version = 1;
+      disable_existing_loggers = true;
+      handlers.console.class = "logging.StreamHandler";
+      loggers.django = {
+        handlers = [ "console" ];
+        level = "INFO";
+      };
+    };
+    HAYSTACK_CONNECTIONS.default = {
+      ENGINE = "haystack.backends.whoosh_backend.WhooshEngine";
+      PATH = "/var/lib/mailman-web/fulltext-index";
+    };
   } // cfg.webSettings;
 
-  settingsJSON = pkgs.writeText "settings.json" (builtins.toJSON settings);
-
-  mailmanCfg = ''
-    [mailman]
-    site_owner: ${cfg.siteOwner}
-    layout: fhs
-
-    [paths.fhs]
-    bin_dir: ${pkgs.python3Packages.mailman}/bin
-    var_dir: /var/lib/mailman
-    queue_dir: $var_dir/queue
-    template_dir: $var_dir/templates
-    log_dir: $var_dir/log
-    lock_dir: $var_dir/lock
-    etc_dir: /etc
-    ext_dir: $etc_dir/mailman.d
-    pid_file: /run/mailman/master.pid
-  '' + optionalString cfg.hyperkitty.enable ''
-
-    [archiver.hyperkitty]
-    class: mailman_hyperkitty.Archiver
-    enable: yes
-    configuration: /var/lib/mailman/mailman-hyperkitty.cfg
+  webSettingsJSON = pkgs.writeText "settings.json" (builtins.toJSON webSettings);
+
+  # TODO: Should this be RFC42-ised so that users can set additional options without modifying the module?
+  mtaConfig = pkgs.writeText "mailman-postfix.cfg" ''
+    [postfix]
+    postmap_command: ${pkgs.postfix}/bin/postmap
+    transport_file_type: hash
   '';
 
+  mailmanCfg = lib.generators.toINI {} cfg.settings;
+
   mailmanHyperkittyCfg = pkgs.writeText "mailman-hyperkitty.cfg" ''
     [general]
     # This is your HyperKitty installation, preferably on the localhost. This
@@ -84,7 +88,7 @@ in {
         type = types.package;
         default = pkgs.mailman;
         defaultText = "pkgs.mailman";
-        example = "pkgs.mailman.override { archivers = []; }";
+        example = literalExample "pkgs.mailman.override { archivers = []; }";
         description = "Mailman package to use";
       };
 
@@ -98,18 +102,6 @@ in {
         '';
       };
 
-      webRoot = mkOption {
-        type = types.path;
-        default = "${pkgs.mailman-web}/${pkgs.python3.sitePackages}";
-        defaultText = "\${pkgs.mailman-web}/\${pkgs.python3.sitePackages}";
-        description = ''
-          The web root for the Hyperkity + Postorius apps provided by Mailman.
-          This variable can be set, of course, but it mainly exists so that site
-          admins can refer to it in their own hand-written web server
-          configuration files.
-        '';
-      };
-
       webHosts = mkOption {
         type = types.listOf types.str;
         default = [];
@@ -124,7 +116,7 @@ in {
 
       webUser = mkOption {
         type = types.str;
-        default = config.services.httpd.user;
+        default = "mailman-web";
         description = ''
           User to run mailman-web as
         '';
@@ -138,6 +130,22 @@ in {
         '';
       };
 
+      serve = {
+        enable = mkEnableOption "Automatic nginx and uwsgi setup for mailman-web";
+      };
+
+      extraPythonPackages = mkOption {
+        description = "Packages to add to the python environment used by mailman and mailman-web";
+        type = types.listOf types.package;
+        default = [];
+      };
+
+      settings = mkOption {
+        description = "Settings for mailman.cfg";
+        type = types.attrsOf (types.attrsOf types.str);
+        default = {};
+      };
+
       hyperkitty = {
         enable = mkEnableOption "the Hyperkitty archiver for Mailman";
 
@@ -158,6 +166,35 @@ in {
 
   config = mkIf cfg.enable {
 
+    services.mailman.settings = {
+      mailman.site_owner = lib.mkDefault cfg.siteOwner;
+      mailman.layout = "fhs";
+
+      "paths.fhs" = {
+        bin_dir = "${pkgs.python3Packages.mailman}/bin";
+        var_dir = "/var/lib/mailman";
+        queue_dir = "$var_dir/queue";
+        template_dir = "$var_dir/templates";
+        log_dir = "/var/log/mailman";
+        lock_dir = "$var_dir/lock";
+        etc_dir = "/etc";
+        ext_dir = "$etc_dir/mailman.d";
+        pid_file = "/run/mailman/master.pid";
+      };
+
+      mta.configuration = lib.mkDefault "${mtaConfig}";
+
+      "archiver.hyperkitty" = lib.mkIf cfg.hyperkitty.enable {
+        class = "mailman_hyperkitty.Archiver";
+        enable = "yes";
+        configuration = "/var/lib/mailman/mailman-hyperkitty.cfg";
+      };
+    } // (let
+      loggerNames = ["root" "archiver" "bounce" "config" "database" "debug" "error" "fromusenet" "http" "locks" "mischief" "plugins" "runner" "smtp"];
+      loggerSectionNames = map (n: "logging.${n}") loggerNames;
+      in lib.genAttrs loggerSectionNames(name: { handler = "stderr"; })
+    );
+
     assertions = let
       inherit (config.services) postfix;
 
@@ -183,7 +220,17 @@ in {
       (requirePostfixHash [ "config" "local_recipient_maps" ] "postfix_lmtp")
     ];
 
-    users.users.mailman = { description = "GNU Mailman"; isSystemUser = true; };
+    users.users.mailman = {
+      description = "GNU Mailman";
+      isSystemUser = true;
+      group = "mailman";
+    };
+    users.users.mailman-web = lib.mkIf (cfg.webUser == "mailman-web") {
+      description = "GNU Mailman web interface";
+      isSystemUser = true;
+      group = "mailman";
+    };
+    users.groups.mailman = {};
 
     environment.etc."mailman.cfg".text = mailmanCfg;
 
@@ -198,197 +245,193 @@ in {
 
       import json
 
-      with open('${settingsJSON}') as f:
+      with open('${webSettingsJSON}') as f:
           globals().update(json.load(f))
 
       with open('/var/lib/mailman-web/settings_local.json') as f:
           globals().update(json.load(f))
     '';
 
-    environment.systemPackages = [ cfg.package ] ++ (with pkgs; [ mailman-web ]);
-
-    services.postfix = {
-      recipientDelimiter = "+";         # bake recipient addresses in mail envelopes via VERP
-      config = {
-        owner_request_special = "no";   # Mailman handles -owner addresses on its own
-      };
-    };
-
-    systemd.services.mailman = {
-      description = "GNU Mailman Master Process";
-      after = [ "network.target" ];
-      restartTriggers = [ config.environment.etc."mailman.cfg".source ];
-      wantedBy = [ "multi-user.target" ];
-      serviceConfig = {
-        ExecStart = "${cfg.package}/bin/mailman start";
-        ExecStop = "${cfg.package}/bin/mailman stop";
-        User = "mailman";
-        Type = "forking";
-        RuntimeDirectory = "mailman";
-        PIDFile = "/run/mailman/master.pid";
-      };
-    };
-
-    systemd.services.mailman-settings = {
-      description = "Generate settings files (including secrets) for Mailman";
-      before = [ "mailman.service" "mailman-web.service" "hyperkitty.service" "httpd.service" "uwsgi.service" ];
-      requiredBy = [ "mailman.service" "mailman-web.service" "hyperkitty.service" "httpd.service" "uwsgi.service" ];
-      path = with pkgs; [ jq ];
-      script = ''
-        mailmanDir=/var/lib/mailman
-        mailmanWebDir=/var/lib/mailman-web
-
-        mailmanCfg=$mailmanDir/mailman-hyperkitty.cfg
-        mailmanWebCfg=$mailmanWebDir/settings_local.json
-
-        install -m 0700 -o mailman -g nogroup -d $mailmanDir
-        install -m 0700 -o ${cfg.webUser} -g nogroup -d $mailmanWebDir
-
-        if [ ! -e $mailmanWebCfg ]; then
-            hyperkittyApiKey=$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 64)
-            secretKey=$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 64)
-
-            mailmanWebCfgTmp=$(mktemp)
-            jq -n '.MAILMAN_ARCHIVER_KEY=$archiver_key | .SECRET_KEY=$secret_key' \
-                --arg archiver_key "$hyperkittyApiKey" \
-                --arg secret_key "$secretKey" \
-                >"$mailmanWebCfgTmp"
-            chown ${cfg.webUser} "$mailmanWebCfgTmp"
-            mv -n "$mailmanWebCfgTmp" $mailmanWebCfg
-        fi
-
-        hyperkittyApiKey="$(jq -r .MAILMAN_ARCHIVER_KEY $mailmanWebCfg)"
-        mailmanCfgTmp=$(mktemp)
-        sed "s/@API_KEY@/$hyperkittyApiKey/g" ${mailmanHyperkittyCfg} >"$mailmanCfgTmp"
-        chown mailman "$mailmanCfgTmp"
-        mv "$mailmanCfgTmp" $mailmanCfg
-      '';
-      serviceConfig = {
-        Type = "oneshot";
-        # RemainAfterExit makes restartIfChanged work for this service, so
-        # downstream services will get updated automatically when things like
-        # services.mailman.hyperkitty.baseUrl change.  Otherwise users have to
-        # restart things manually, which is confusing.
-        RemainAfterExit = "yes";
+    services.nginx = mkIf cfg.serve.enable {
+      enable = mkDefault true;
+      virtualHosts."${lib.head cfg.webHosts}" = {
+        serverAliases = cfg.webHosts;
+        locations = {
+          "/".extraConfig = "uwsgi_pass unix:/run/mailman-web.socket;";
+          "/static/".alias = webSettings.STATIC_ROOT + "/";
+        };
       };
     };
 
-    systemd.services.mailman-web = {
-      description = "Init Postorius DB";
-      before = [ "httpd.service" "uwsgi.service" ];
-      requiredBy = [ "httpd.service" "uwsgi.service" ];
-      restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
-      script = ''
-        ${pkgs.mailman-web}/bin/mailman-web migrate
-        rm -rf static
-        ${pkgs.mailman-web}/bin/mailman-web collectstatic
-        ${pkgs.mailman-web}/bin/mailman-web compress
+    environment.systemPackages = [ (pkgs.buildEnv {
+      name = "mailman-tools";
+      # We don't want to pollute the system PATH with a python
+      # interpreter etc. so let's pick only the stuff we actually
+      # want from pythonEnv
+      pathsToLink = ["/bin"];
+      paths = [pythonEnv];
+      postBuild = ''
+        find $out/bin/ -mindepth 1 -not -name "mailman*" -delete
       '';
-      serviceConfig = {
-        User = cfg.webUser;
-        Type = "oneshot";
-        # Similar to mailman-settings.service, this makes restartTriggers work
-        # properly for this service.
-        RemainAfterExit = "yes";
-        WorkingDirectory = "/var/lib/mailman-web";
-      };
-    };
+    }) ];
 
-    systemd.services.mailman-daily = {
-      description = "Trigger daily Mailman events";
-      startAt = "daily";
-      restartTriggers = [ config.environment.etc."mailman.cfg".source ];
-      serviceConfig = {
-        ExecStart = "${cfg.package}/bin/mailman digests --send";
-        User = "mailman";
-      };
-    };
-
-    systemd.services.hyperkitty = {
-      inherit (cfg.hyperkitty) enable;
-      description = "GNU Hyperkitty QCluster Process";
-      after = [ "network.target" ];
-      restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
-      wantedBy = [ "mailman.service" "multi-user.target" ];
-      serviceConfig = {
-        ExecStart = "${pkgs.mailman-web}/bin/mailman-web qcluster";
-        User = cfg.webUser;
-        WorkingDirectory = "/var/lib/mailman-web";
+    services.postfix = {
+      recipientDelimiter = "+";         # bake recipient addresses in mail envelopes via VERP
+      config = {
+        owner_request_special = "no";   # Mailman handles -owner addresses on its own
       };
     };
 
-    systemd.services.hyperkitty-minutely = {
-      inherit (cfg.hyperkitty) enable;
-      description = "Trigger minutely Hyperkitty events";
-      startAt = "minutely";
-      restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
-      serviceConfig = {
-        ExecStart = "${pkgs.mailman-web}/bin/mailman-web runjobs minutely";
-        User = cfg.webUser;
-        WorkingDirectory = "/var/lib/mailman-web";
-      };
+    systemd.sockets.mailman-uwsgi = lib.mkIf cfg.serve.enable {
+      wantedBy = ["sockets.target"];
+      before = ["nginx.service"];
+      socketConfig.ListenStream = "/run/mailman-web.socket";
     };
-
-    systemd.services.hyperkitty-quarter-hourly = {
-      inherit (cfg.hyperkitty) enable;
-      description = "Trigger quarter-hourly Hyperkitty events";
-      startAt = "*:00/15";
-      restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
-      serviceConfig = {
-        ExecStart = "${pkgs.mailman-web}/bin/mailman-web runjobs quarter_hourly";
-        User = cfg.webUser;
-        WorkingDirectory = "/var/lib/mailman-web";
+    systemd.services = {
+      mailman = {
+        description = "GNU Mailman Master Process";
+        after = [ "network.target" ];
+        restartTriggers = [ config.environment.etc."mailman.cfg".source ];
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          ExecStart = "${pythonEnv}/bin/mailman start";
+          ExecStop = "${pythonEnv}/bin/mailman stop";
+          User = "mailman";
+          Group = "mailman";
+          Type = "forking";
+          RuntimeDirectory = "mailman";
+          LogsDirectory = "mailman";
+          PIDFile = "/run/mailman/master.pid";
+        };
       };
-    };
 
-    systemd.services.hyperkitty-hourly = {
-      inherit (cfg.hyperkitty) enable;
-      description = "Trigger hourly Hyperkitty events";
-      startAt = "hourly";
-      restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
-      serviceConfig = {
-        ExecStart = "${pkgs.mailman-web}/bin/mailman-web runjobs hourly";
-        User = cfg.webUser;
-        WorkingDirectory = "/var/lib/mailman-web";
+      mailman-settings = {
+        description = "Generate settings files (including secrets) for Mailman";
+        before = [ "mailman.service" "mailman-web-setup.service" "mailman-uwsgi.service" "hyperkitty.service" ];
+        requiredBy = [ "mailman.service" "mailman-web-setup.service" "mailman-uwsgi.service" "hyperkitty.service" ];
+        path = with pkgs; [ jq ];
+        script = ''
+          mailmanDir=/var/lib/mailman
+          mailmanWebDir=/var/lib/mailman-web
+
+          mailmanCfg=$mailmanDir/mailman-hyperkitty.cfg
+          mailmanWebCfg=$mailmanWebDir/settings_local.json
+
+          install -m 0775 -o mailman -g mailman -d /var/lib/mailman-web-static
+          install -m 0770 -o mailman -g mailman -d $mailmanDir
+          install -m 0770 -o ${cfg.webUser} -g mailman -d $mailmanWebDir
+
+          if [ ! -e $mailmanWebCfg ]; then
+              hyperkittyApiKey=$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 64)
+              secretKey=$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 64)
+
+              mailmanWebCfgTmp=$(mktemp)
+              jq -n '.MAILMAN_ARCHIVER_KEY=$archiver_key | .SECRET_KEY=$secret_key' \
+                  --arg archiver_key "$hyperkittyApiKey" \
+                  --arg secret_key "$secretKey" \
+                  >"$mailmanWebCfgTmp"
+              chown root:mailman "$mailmanWebCfgTmp"
+              chmod 440 "$mailmanWebCfgTmp"
+              mv -n "$mailmanWebCfgTmp" "$mailmanWebCfg"
+          fi
+
+          hyperkittyApiKey="$(jq -r .MAILMAN_ARCHIVER_KEY "$mailmanWebCfg")"
+          mailmanCfgTmp=$(mktemp)
+          sed "s/@API_KEY@/$hyperkittyApiKey/g" ${mailmanHyperkittyCfg} >"$mailmanCfgTmp"
+          chown mailman:mailman "$mailmanCfgTmp"
+          mv "$mailmanCfgTmp" "$mailmanCfg"
+        '';
       };
-    };
 
-    systemd.services.hyperkitty-daily = {
-      inherit (cfg.hyperkitty) enable;
-      description = "Trigger daily Hyperkitty events";
-      startAt = "daily";
-      restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
-      serviceConfig = {
-        ExecStart = "${pkgs.mailman-web}/bin/mailman-web runjobs daily";
-        User = cfg.webUser;
-        WorkingDirectory = "/var/lib/mailman-web";
+      mailman-web-setup = {
+        description = "Prepare mailman-web files and database";
+        before = [ "uwsgi.service" "mailman-uwsgi.service" ];
+        requiredBy = [ "mailman-uwsgi.service" ];
+        restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
+        script = ''
+          [[ -e "${webSettings.STATIC_ROOT}" ]] && find "${webSettings.STATIC_ROOT}/" -mindepth 1 -delete
+          ${pythonEnv}/bin/mailman-web migrate
+          ${pythonEnv}/bin/mailman-web collectstatic
+          ${pythonEnv}/bin/mailman-web compress
+        '';
+        serviceConfig = {
+          User = cfg.webUser;
+          Group = "mailman";
+          Type = "oneshot";
+          WorkingDirectory = "/var/lib/mailman-web";
+        };
       };
-    };
 
-    systemd.services.hyperkitty-weekly = {
-      inherit (cfg.hyperkitty) enable;
-      description = "Trigger weekly Hyperkitty events";
-      startAt = "weekly";
-      restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
-      serviceConfig = {
-        ExecStart = "${pkgs.mailman-web}/bin/mailman-web runjobs weekly";
-        User = cfg.webUser;
-        WorkingDirectory = "/var/lib/mailman-web";
+      mailman-uwsgi = mkIf cfg.serve.enable (let
+        uwsgiConfig.uwsgi = {
+          type = "normal";
+          plugins = ["python3"];
+          home = pythonEnv;
+          module = "mailman_web.wsgi";
+        };
+        uwsgiConfigFile = pkgs.writeText "uwsgi-mailman.json" (builtins.toJSON uwsgiConfig);
+      in {
+        wantedBy = ["multi-user.target"];
+        requires = ["mailman-uwsgi.socket" "mailman-web-setup.service"];
+        restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
+        serviceConfig = {
+          # Since the mailman-web settings.py obstinately creates a logs
+          # dir in the cwd, change to the (writable) runtime directory before
+          # starting uwsgi.
+          ExecStart = "${pkgs.coreutils}/bin/env -C $RUNTIME_DIRECTORY ${pkgs.uwsgi.override { plugins = ["python3"]; }}/bin/uwsgi --json ${uwsgiConfigFile}";
+          User = cfg.webUser;
+          Group = "mailman";
+          RuntimeDirectory = "mailman-uwsgi";
+        };
+      });
+
+      mailman-daily = {
+        description = "Trigger daily Mailman events";
+        startAt = "daily";
+        restartTriggers = [ config.environment.etc."mailman.cfg".source ];
+        serviceConfig = {
+          ExecStart = "${pythonEnv}/bin/mailman digests --send";
+          User = "mailman";
+          Group = "mailman";
+        };
       };
-    };
 
-    systemd.services.hyperkitty-yearly = {
-      inherit (cfg.hyperkitty) enable;
-      description = "Trigger yearly Hyperkitty events";
-      startAt = "yearly";
-      restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
-      serviceConfig = {
-        ExecStart = "${pkgs.mailman-web}/bin/mailman-web runjobs yearly";
-        User = cfg.webUser;
-        WorkingDirectory = "/var/lib/mailman-web";
+      hyperkitty = lib.mkIf cfg.hyperkitty.enable {
+        description = "GNU Hyperkitty QCluster Process";
+        after = [ "network.target" ];
+        restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
+        wantedBy = [ "mailman.service" "multi-user.target" ];
+        serviceConfig = {
+          ExecStart = "${pythonEnv}/bin/mailman-web qcluster";
+          User = cfg.webUser;
+          Group = "mailman";
+          WorkingDirectory = "/var/lib/mailman-web";
+        };
       };
-    };
+    } // flip lib.mapAttrs' {
+      "minutely" = "minutely";
+      "quarter_hourly" = "*:00/15";
+      "hourly" = "hourly";
+      "daily" = "daily";
+      "weekly" = "weekly";
+      "yearly" = "yearly";
+    } (name: startAt:
+      lib.nameValuePair "hyperkitty-${name}" (lib.mkIf cfg.hyperkitty.enable {
+        description = "Trigger ${name} Hyperkitty events";
+        inherit startAt;
+        restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
+        serviceConfig = {
+          ExecStart = "${pythonEnv}/bin/mailman-web runjobs minutely";
+          User = cfg.webUser;
+          Group = "mailman";
+          WorkingDirectory = "/var/lib/mailman-web";
+        };
+      }));
+  };
 
+  meta = {
+    maintainers = with lib.maintainers; [ lheckemann ];
+    doc = ./mailman.xml;
   };
 
 }
diff --git a/nixos/modules/services/mail/mailman.xml b/nixos/modules/services/mail/mailman.xml
new file mode 100644
index 00000000000..cbe50ed0b91
--- /dev/null
+++ b/nixos/modules/services/mail/mailman.xml
@@ -0,0 +1,59 @@
+<chapter xmlns="http://docbook.org/ns/docbook"
+         xmlns:xlink="http://www.w3.org/1999/xlink"
+         xmlns:xi="http://www.w3.org/2001/XInclude"
+         version="5.0"
+         xml:id="module-services-mailman">
+  <title>Mailman</title>
+  <para>
+    <link xlink:href="https://www.list.org">Mailman</link> is free
+    software for managing electronic mail discussion and e-newsletter
+    lists. Mailman and its web interface can be configured using the
+    corresponding NixOS module. Note that this service is best used with
+    an existing, securely configured Postfix setup, as it does not automatically configure this.
+  </para>
+
+  <section xml:id="module-services-mailman-basic-usage">
+    <title>Basic usage</title>
+    <para>
+      For a basic configuration, the following settings are suggested:
+      <programlisting>{ config, ... }: {
+  services.postfix = {
+    enable = true;
+    relayDomains = ["hash:/var/lib/mailman/data/postfix_domains"];
+    sslCert = config.security.acme.certs."lists.example.org".directory + "/full.pem";
+    sslKey = config.security.acme.certs."lists.example.org".directory + "/key.pem";
+    config = {
+      transport_maps = ["hash:/var/lib/mailman/data/postfix_lmtp"];
+      local_recipient_maps = ["hash:/var/lib/mailman/data/postfix_lmtp"];
+    };
+  };
+  services.mailman = {
+    <link linkend="opt-services.mailman.enable">enable</link> = true;
+    <link linkend="opt-services.mailman.serve.enable">serve.enable</link> = true;
+    <link linkend="opt-services.mailman.hyperkitty.enable">hyperkitty.enable</link> = true;
+    <link linkend="opt-services.mailman.hyperkitty.enable">webHosts</link> = ["lists.example.org"];
+    <link linkend="opt-services.mailman.hyperkitty.enable">siteOwner</link> = "mailman@example.org";
+  };
+  <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">services.nginx.virtualHosts."lists.example.org".enableACME</link> = true;
+  <link linkend="opt-services.mailman.hyperkitty.enable">networking.firewall.allowedTCPPorts</link> = [ 25 80 443 ];
+}</programlisting>
+    </para>
+    <para>
+      DNS records will also be required:
+      <itemizedlist>
+        <listitem><para><literal>AAAA</literal> and <literal>A</literal> records pointing to the host in question, in order for browsers to be able to discover the address of the web server;</para></listitem>
+        <listitem><para>An <literal>MX</literal> record pointing to a domain name at which the host is reachable, in order for other mail servers to be able to deliver emails to the mailing lists it hosts.</para></listitem>
+      </itemizedlist>
+    </para>
+    <para>
+      After this has been done and appropriate DNS records have been
+      set up, the Postorius mailing list manager and the Hyperkitty
+      archive browser will be available at
+      https://lists.example.org/. Note that this setup is not
+      sufficient to deliver emails to most email providers nor to
+      avoid spam -- a number of additional measures for authenticating
+      incoming and outgoing mails, such as SPF, DMARC and DKIM are
+      necessary, but outside the scope of the Mailman module.
+    </para>
+  </section>
+</chapter>
diff --git a/nixos/modules/services/mail/opensmtpd.nix b/nixos/modules/services/mail/opensmtpd.nix
index 1fabe2da45c..c838d3b949d 100644
--- a/nixos/modules/services/mail/opensmtpd.nix
+++ b/nixos/modules/services/mail/opensmtpd.nix
@@ -17,6 +17,10 @@ in {
 
   ###### interface
 
+  imports = [
+    (mkRenamedOptionModule [ "services" "opensmtpd" "addSendmailToSystemPath" ] [ "services" "opensmtpd" "setSendmail" ])
+  ];
+
   options = {
 
     services.opensmtpd = {
@@ -34,13 +38,10 @@ in {
         description = "The OpenSMTPD package to use.";
       };
 
-      addSendmailToSystemPath = mkOption {
+      setSendmail = mkOption {
         type = types.bool;
         default = true;
-        description = ''
-          Whether to add OpenSMTPD's sendmail binary to the
-          system path or not.
-        '';
+        description = "Whether to set the system sendmail to OpenSMTPD's.";
       };
 
       extraServerArgs = mkOption {
@@ -82,7 +83,7 @@ in {
 
   ###### implementation
 
-  config = mkIf cfg.enable {
+  config = mkIf cfg.enable rec {
     users.groups = {
       smtpd.gid = config.ids.gids.smtpd;
       smtpq.gid = config.ids.gids.smtpq;
@@ -101,6 +102,14 @@ in {
       };
     };
 
+    security.wrappers.smtpctl = {
+      group = "smtpq";
+      setgid = true;
+      source = "${cfg.package}/bin/smtpctl";
+    };
+
+    services.mail.sendmailSetuidWrapper = mkIf cfg.setSendmail security.wrappers.smtpctl;
+
     systemd.tmpfiles.rules = [
       "d /var/spool/smtpd 711 root - - -"
       "d /var/spool/smtpd/offline 770 root smtpq - -"
@@ -119,7 +128,5 @@ in {
       serviceConfig.ExecStart = "${cfg.package}/sbin/smtpd -d -f ${conf} ${args}";
       environment.OPENSMTPD_PROC_PATH = "${procEnv}/libexec/opensmtpd";
     };
-
-    environment.systemPackages = mkIf cfg.addSendmailToSystemPath [ sendmail ];
   };
 }
diff --git a/nixos/modules/services/mail/pfix-srsd.nix b/nixos/modules/services/mail/pfix-srsd.nix
index 38984f896d6..e3dbf2a014f 100644
--- a/nixos/modules/services/mail/pfix-srsd.nix
+++ b/nixos/modules/services/mail/pfix-srsd.nix
@@ -53,4 +53,4 @@ with lib;
       };
     };
   };
-}
\ No newline at end of file
+}
diff --git a/nixos/modules/services/mail/postfix.nix b/nixos/modules/services/mail/postfix.nix
index 608f64a68fb..fd4d16cdc37 100644
--- a/nixos/modules/services/mail/postfix.nix
+++ b/nixos/modules/services/mail/postfix.nix
@@ -25,6 +25,8 @@ let
 
   clientRestrictions = concatStringsSep ", " (clientAccess ++ dnsBl);
 
+  smtpTlsSecurityLevel = if cfg.useDane then "dane" else "may";
+
   mainCf = let
     escape = replaceStrings ["$"] ["$$"];
     mkList = items: "\n  " + concatStringsSep ",\n  " items;
@@ -280,6 +282,17 @@ in
         description = "Whether to enable smtp submission.";
       };
 
+      enableSubmissions = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable smtp submission via smtps.
+
+          According to RFC 8314 this should be preferred
+          over STARTTLS for submission of messages by end user clients.
+        '';
+      };
+
       submissionOptions = mkOption {
         type = types.attrs;
         default = {
@@ -298,6 +311,29 @@ in
         description = "Options for the submission config in master.cf";
       };
 
+      submissionsOptions = mkOption {
+        type = types.attrs;
+        default = {
+          smtpd_sasl_auth_enable = "yes";
+          smtpd_client_restrictions = "permit_sasl_authenticated,reject";
+          milter_macro_daemon_name = "ORIGINATING";
+        };
+        example = {
+          smtpd_sasl_auth_enable = "yes";
+          smtpd_sasl_type = "dovecot";
+          smtpd_client_restrictions = "permit_sasl_authenticated,reject";
+          milter_macro_daemon_name = "ORIGINATING";
+        };
+        description = ''
+          Options for the submission config via smtps in master.cf.
+
+          smtpd_tls_security_level will be set to encrypt, if it is missing
+          or has one of the values "may" or "none".
+
+          smtpd_tls_wrappermode with value "yes" will be added automatically.
+        '';
+      };
+
       setSendmail = mkOption {
         type = types.bool;
         default = true;
@@ -454,7 +490,7 @@ in
         '';
         example = {
           mail_owner = "postfix";
-          smtp_use_tls = true;
+          smtp_tls_security_level = "may";
         };
       };
 
@@ -466,16 +502,26 @@ in
         ";
       };
 
-      sslCert = mkOption {
+      tlsTrustedAuthorities = mkOption {
         type = types.str;
-        default = "";
-        description = "SSL certificate to use.";
+        default = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
+        description = ''
+          File containing trusted certification authorities (CA) to verify certificates of mailservers contacted for mail delivery. This basically sets smtp_tls_CAfile and enables opportunistic tls. Defaults to NixOS trusted certification authorities.
+        '';
+      };
+
+      useDane = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Sets smtp_tls_security_level to "dane" rather than "may". See postconf(5) for details.
+        '';
       };
 
-      sslCACert = mkOption {
+      sslCert = mkOption {
         type = types.str;
         default = "";
-        description = "SSL certificate of CA.";
+        description = "SSL certificate to use.";
       };
 
       sslKey = mkOption {
@@ -771,18 +817,20 @@ in
         recipient_canonical_classes = [ "envelope_recipient" ];
       }
       // optionalAttrs cfg.enableHeaderChecks { header_checks = [ "regexp:/etc/postfix/header_checks" ]; }
+      // optionalAttrs (cfg.tlsTrustedAuthorities != "") {
+        smtp_tls_CAfile = cfg.tlsTrustedAuthorities;
+        smtp_tls_security_level = smtpTlsSecurityLevel;
+      }
       // optionalAttrs (cfg.sslCert != "") {
-        smtp_tls_CAfile = cfg.sslCACert;
         smtp_tls_cert_file = cfg.sslCert;
         smtp_tls_key_file = cfg.sslKey;
 
-        smtp_use_tls = true;
+        smtp_tls_security_level = smtpTlsSecurityLevel;
 
-        smtpd_tls_CAfile = cfg.sslCACert;
         smtpd_tls_cert_file = cfg.sslCert;
         smtpd_tls_key_file = cfg.sslKey;
 
-        smtpd_use_tls = true;
+        smtpd_tls_security_level = "may";
       };
 
       services.postfix.masterConfig = {
@@ -878,6 +926,23 @@ in
           command = "smtp";
           args = [ "-o" "smtp_fallback_relay=" ];
         };
+      } // optionalAttrs cfg.enableSubmissions {
+        submissions = {
+          type = "inet";
+          private = false;
+          command = "smtpd";
+          args = let
+            mkKeyVal = opt: val: [ "-o" (opt + "=" + val) ];
+            adjustSmtpTlsSecurityLevel = !(cfg.submissionsOptions ? smtpd_tls_security_level) ||
+                                      cfg.submissionsOptions.smtpd_tls_security_level == "none" ||
+                                      cfg.submissionsOptions.smtpd_tls_security_level == "may";
+            submissionsOptions = cfg.submissionsOptions // {
+              smtpd_tls_wrappermode = "yes";
+            } // optionalAttrs adjustSmtpTlsSecurityLevel {
+              smtpd_tls_security_level = "encrypt";
+            };
+          in concatLists (mapAttrsToList mkKeyVal submissionsOptions);
+        };
       };
     }
 
@@ -900,4 +965,9 @@ in
       services.postfix.mapFiles.client_access = checkClientAccessFile;
     })
   ]);
+
+  imports = [
+   (mkRemovedOptionModule [ "services" "postfix" "sslCACert" ]
+     "services.postfix.sslCACert was replaced by services.postfix.tlsTrustedAuthorities. In case you intend that your server should validate requested client certificates use services.postfix.extraConfig.")
+  ];
 }
diff --git a/nixos/modules/services/mail/roundcube.nix b/nixos/modules/services/mail/roundcube.nix
index ed1439745ac..a0bbab64985 100644
--- a/nixos/modules/services/mail/roundcube.nix
+++ b/nixos/modules/services/mail/roundcube.nix
@@ -95,6 +95,18 @@ in
       '';
     };
 
+    maxAttachmentSize = mkOption {
+      type = types.int;
+      default = 18;
+      description = ''
+        The maximum attachment size in MB.
+
+        Note: Since roundcube only uses 70% of max upload values configured in php
+        30% is added automatically to <xref linkend="opt-services.roundcube.maxAttachmentSize"/>.
+      '';
+      apply = configuredMaxAttachmentSize: "${toString (configuredMaxAttachmentSize * 1.3)}M";
+    };
+
     extraConfig = mkOption {
       type = types.lines;
       default = "";
@@ -115,7 +127,7 @@ in
       $config = array();
       $config['db_dsnw'] = 'pgsql://${cfg.database.username}${lib.optionalString (!localDB) ":' . $password . '"}@${if localDB then "unix(/run/postgresql)" else cfg.database.host}/${cfg.database.dbname}';
       $config['log_driver'] = 'syslog';
-      $config['max_message_size'] = '25M';
+      $config['max_message_size'] =  '${cfg.maxAttachmentSize}';
       $config['plugins'] = [${concatMapStringsSep "," (p: "'${p}'") cfg.plugins}];
       $config['des_key'] = file_get_contents('/var/lib/roundcube/des_key');
       $config['mime_types'] = '${pkgs.nginx}/conf/mime.types';
@@ -172,8 +184,8 @@ in
       phpOptions = ''
         error_log = 'stderr'
         log_errors = on
-        post_max_size = 25M
-        upload_max_filesize = 25M
+        post_max_size = ${cfg.maxAttachmentSize}
+        upload_max_filesize = ${cfg.maxAttachmentSize}
       '';
       settings = mapAttrs (name: mkDefault) {
         "listen.owner" = "nginx";
diff --git a/nixos/modules/services/misc/bazarr.nix b/nixos/modules/services/misc/bazarr.nix
new file mode 100644
index 00000000000..d3fd5b08cc8
--- /dev/null
+++ b/nixos/modules/services/misc/bazarr.nix
@@ -0,0 +1,76 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.bazarr;
+in
+{
+  options = {
+    services.bazarr = {
+      enable = mkEnableOption "bazarr, a subtitle manager for Sonarr and Radarr";
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Open ports in the firewall for the bazarr web interface.";
+      };
+
+      listenPort = mkOption {
+        type = types.port;
+        default = 6767;
+        description = "Port on which the bazarr web interface should listen";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "bazarr";
+        description = "User account under which bazarr runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "bazarr";
+        description = "Group under which bazarr runs.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.bazarr = {
+      description = "bazarr";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = rec {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+        StateDirectory = "bazarr";
+        SyslogIdentifier = "bazarr";
+        ExecStart = pkgs.writeShellScript "start-bazarr" ''
+          ${pkgs.bazarr}/bin/bazarr \
+            --config '/var/lib/${StateDirectory}' \
+            --port ${toString cfg.listenPort} \
+            --no-update True
+        '';
+        Restart = "on-failure";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.listenPort ];
+    };
+
+    users.users = mkIf (cfg.user == "bazarr") {
+      bazarr = {
+        group = cfg.group;
+        home = "/var/lib/${config.systemd.services.bazarr.serviceConfig.StateDirectory}";
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "bazarr") {
+      bazarr = {};
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/docker-registry.nix b/nixos/modules/services/misc/docker-registry.nix
index 89bac4f47d7..1c2e2cc5359 100644
--- a/nixos/modules/services/misc/docker-registry.nix
+++ b/nixos/modules/services/misc/docker-registry.nix
@@ -138,7 +138,7 @@ in {
 
       script = ''
         ${pkgs.docker-distribution}/bin/registry garbage-collect ${configFile}
-        ${pkgs.systemd}/bin/systemctl restart docker-registry.service
+        /run/current-system/systemd/bin/systemctl restart docker-registry.service
       '';
 
       startAt = optional cfg.enableGarbageCollect cfg.garbageCollectDates;
diff --git a/nixos/modules/services/misc/freeswitch.nix b/nixos/modules/services/misc/freeswitch.nix
index 0de5ba42811..b42f36e8663 100644
--- a/nixos/modules/services/misc/freeswitch.nix
+++ b/nixos/modules/services/misc/freeswitch.nix
@@ -78,7 +78,7 @@ in {
       wantedBy = [ "multi-user.target" ];
       restartTriggers = [ configDirectory ];
       serviceConfig = {
-        ExecStart = "${pkgs.systemd}/bin/systemctl try-reload-or-restart freeswitch.service";
+        ExecStart = "/run/current-system/systemd/bin/systemctl try-reload-or-restart freeswitch.service";
         RemainAfterExit = true;
         Type = "oneshot";
       };
@@ -95,9 +95,11 @@ in {
           -conf ${configPath} \\
           -base /var/lib/freeswitch";
         ExecReload = "${pkg}/bin/fs_cli -x reloadxml";
-        Restart = "always";
+        Restart = "on-failure";
         RestartSec = "5s";
+        CPUSchedulingPolicy = "fifo";
       };
     };
+    environment.systemPackages = [ pkg ];
   };
 }
diff --git a/nixos/modules/services/misc/gitea.nix b/nixos/modules/services/misc/gitea.nix
index f8bcedc94fe..af80e99746b 100644
--- a/nixos/modules/services/misc/gitea.nix
+++ b/nixos/modules/services/misc/gitea.nix
@@ -162,6 +162,45 @@ in
             <manvolnum>7</manvolnum></citerefentry>.
           '';
         };
+
+        backupDir = mkOption {
+          type = types.str;
+          default = "${cfg.stateDir}/dump";
+          description = "Path to the dump files.";
+        };
+      };
+
+      ssh = {
+        enable = mkOption {
+          type = types.bool;
+          default = true;
+          description = "Enable external SSH feature.";
+        };
+
+        clonePort = mkOption {
+          type = types.int;
+          default = 22;
+          example = 2222;
+          description = ''
+            SSH port displayed in clone URL.
+            The option is required to configure a service when the external visible port
+            differs from the local listening port i.e. if port forwarding is used.
+          '';
+        };
+      };
+
+      lfs = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Enables git-lfs support.";
+        };
+
+        contentDir = mkOption {
+          type = types.str;
+          default = "${cfg.stateDir}/data/lfs";
+          description = "Where to store LFS files.";
+        };
       };
 
       appName = mkOption {
@@ -200,6 +239,12 @@ in
         description = "HTTP listen port.";
       };
 
+      enableUnixSocket = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Configure Gitea to listen on a unix socket instead of the default TCP port.";
+      };
+
       cookieSecure = mkOption {
         type = types.bool;
         default = false;
@@ -300,14 +345,34 @@ in
         ROOT = cfg.repositoryRoot;
       };
 
-      server = {
-        DOMAIN = cfg.domain;
-        HTTP_ADDR = cfg.httpAddress;
-        HTTP_PORT = cfg.httpPort;
-        ROOT_URL = cfg.rootUrl;
-        STATIC_ROOT_PATH = cfg.staticRootPath;
-        LFS_JWT_SECRET = "#jwtsecret#";
-      };
+      server = mkMerge [
+        {
+          DOMAIN = cfg.domain;
+          STATIC_ROOT_PATH = cfg.staticRootPath;
+          LFS_JWT_SECRET = "#jwtsecret#";
+          ROOT_URL = cfg.rootUrl;
+        }
+        (mkIf cfg.enableUnixSocket {
+          PROTOCOL = "unix";
+          HTTP_ADDR = "/run/gitea/gitea.sock";
+        })
+        (mkIf (!cfg.enableUnixSocket) {
+          HTTP_ADDR = cfg.httpAddress;
+          HTTP_PORT = cfg.httpPort;
+        })
+        (mkIf cfg.ssh.enable {
+          DISABLE_SSH = false;
+          SSH_PORT = cfg.ssh.clonePort;
+        })
+        (mkIf (!cfg.ssh.enable) {
+          DISABLE_SSH = true;
+        })
+        (mkIf cfg.lfs.enable {
+          LFS_START_SERVER = true;
+          LFS_CONTENT_PATH = cfg.lfs.contentDir;
+        })
+
+      ];
 
       session = {
         COOKIE_NAME = "session";
@@ -357,12 +422,26 @@ in
     };
 
     systemd.tmpfiles.rules = [
-      "d '${cfg.stateDir}' - ${cfg.user} gitea - -"
-      "d '${cfg.stateDir}/conf' - ${cfg.user} gitea - -"
-      "d '${cfg.stateDir}/custom' - ${cfg.user} gitea - -"
-      "d '${cfg.stateDir}/custom/conf' - ${cfg.user} gitea - -"
-      "d '${cfg.stateDir}/log' - ${cfg.user} gitea - -"
-      "d '${cfg.repositoryRoot}' - ${cfg.user} gitea - -"
+      "d '${cfg.dump.backupDir}' 0750 ${cfg.user} gitea - -"
+      "z '${cfg.dump.backupDir}' 0750 ${cfg.user} gitea - -"
+      "Z '${cfg.dump.backupDir}' - ${cfg.user} gitea - -"
+      "d '${cfg.lfs.contentDir}' 0750 ${cfg.user} gitea - -"
+      "z '${cfg.lfs.contentDir}' 0750 ${cfg.user} gitea - -"
+      "Z '${cfg.lfs.contentDir}' - ${cfg.user} gitea - -"
+      "d '${cfg.repositoryRoot}' 0750 ${cfg.user} gitea - -"
+      "z '${cfg.repositoryRoot}' 0750 ${cfg.user} gitea - -"
+      "Z '${cfg.repositoryRoot}' - ${cfg.user} gitea - -"
+      "d '${cfg.stateDir}' 0750 ${cfg.user} gitea - -"
+      "d '${cfg.stateDir}/conf' 0750 ${cfg.user} gitea - -"
+      "d '${cfg.stateDir}/custom' 0750 ${cfg.user} gitea - -"
+      "d '${cfg.stateDir}/custom/conf' 0750 ${cfg.user} gitea - -"
+      "d '${cfg.stateDir}/log' 0750 ${cfg.user} gitea - -"
+      "z '${cfg.stateDir}' 0750 ${cfg.user} gitea - -"
+      "z '${cfg.stateDir}/.ssh' 0700 ${cfg.user} gitea - -"
+      "z '${cfg.stateDir}/conf' 0750 ${cfg.user} gitea - -"
+      "z '${cfg.stateDir}/custom' 0750 ${cfg.user} gitea - -"
+      "z '${cfg.stateDir}/custom/conf' 0750 ${cfg.user} gitea - -"
+      "z '${cfg.stateDir}/log' 0750 ${cfg.user} gitea - -"
       "Z '${cfg.stateDir}' - ${cfg.user} gitea - -"
 
       # If we have a folder or symlink with gitea locales, remove it
@@ -431,28 +510,39 @@ in
         User = cfg.user;
         Group = "gitea";
         WorkingDirectory = cfg.stateDir;
-        ExecStart = "${gitea}/bin/gitea web";
+        ExecStart = "${gitea}/bin/gitea web --pid /run/gitea/gitea.pid";
         Restart = "always";
-
-        # Filesystem
+        # Runtime directory and mode
+        RuntimeDirectory = "gitea";
+        RuntimeDirectoryMode = "0755";
+        # Access write directories
+        ReadWritePaths = [ cfg.dump.backupDir cfg.repositoryRoot cfg.stateDir cfg.lfs.contentDir ];
+        UMask = "0027";
+        # Capabilities
+        CapabilityBoundingSet = "";
+        # Security
+        NoNewPrivileges = true;
+        # Sandboxing
+        ProtectSystem = "strict";
         ProtectHome = true;
+        PrivateTmp = true;
         PrivateDevices = true;
+        PrivateUsers = true;
+        ProtectHostname = true;
+        ProtectClock = true;
         ProtectKernelTunables = true;
         ProtectKernelModules = true;
+        ProtectKernelLogs = true;
         ProtectControlGroups = true;
-        ReadWritePaths = cfg.stateDir;
-        # Caps
-        CapabilityBoundingSet = "";
-        NoNewPrivileges = true;
-        # Misc.
+        RestrictAddressFamilies = [ "AF_UNIX AF_INET AF_INET6" ];
         LockPersonality = true;
+        MemoryDenyWriteExecute = true;
         RestrictRealtime = true;
+        RestrictSUIDSGID = true;
         PrivateMounts = true;
-        PrivateUsers = true;
-        MemoryDenyWriteExecute = true;
-        SystemCallFilter = "~@clock @cpu-emulation @debug @keyring @memlock @module @mount @obsolete @raw-io @reboot @resources @setuid @swap";
+        # System Call Filtering
         SystemCallArchitectures = "native";
-        RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
+        SystemCallFilter = "~@clock @cpu-emulation @debug @keyring @memlock @module @mount @obsolete @raw-io @reboot @resources @setuid @swap";
       };
 
       environment = {
@@ -504,7 +594,7 @@ in
          Type = "oneshot";
          User = cfg.user;
          ExecStart = "${gitea}/bin/gitea dump";
-         WorkingDirectory = cfg.stateDir;
+         WorkingDirectory = cfg.dump.backupDir;
        };
     };
 
diff --git a/nixos/modules/services/misc/gitlab.nix b/nixos/modules/services/misc/gitlab.nix
index 730166b04d2..425f35f37cb 100644
--- a/nixos/modules/services/misc/gitlab.nix
+++ b/nixos/modules/services/misc/gitlab.nix
@@ -43,6 +43,9 @@ let
 
     [gitlab-shell]
     dir = "${cfg.packages.gitlab-shell}"
+    secret_file = "${cfg.statePath}/gitlab_shell_secret"
+    gitlab_url = "http+unix://${pathUrlQuote gitlabSocket}"
+    http_settings = { self_signed_cert = false }
 
     ${concatStringsSep "\n" (attrValues (mapAttrs (k: v: ''
     [[storage]]
@@ -51,7 +54,7 @@ let
     '') gitlabConfig.production.repositories.storages))}
   '';
 
-  gitlabShellConfig = {
+  gitlabShellConfig = flip recursiveUpdate cfg.extraShellConfig {
     user = cfg.user;
     gitlab_url = "http+unix://${pathUrlQuote gitlabSocket}";
     http_settings.self_signed_cert = false;
@@ -68,7 +71,7 @@ let
     };
   };
 
-  redisConfig.production.url = "redis://localhost:6379/";
+  redisConfig.production.url = cfg.redisUrl;
 
   gitlabConfig = {
     # These are the default settings from config/gitlab.example.yml
@@ -129,7 +132,7 @@ let
     HOME = "${cfg.statePath}/home";
     UNICORN_PATH = "${cfg.statePath}/";
     GITLAB_PATH = "${cfg.packages.gitlab}/share/gitlab/";
-    SCHEMA = "${cfg.statePath}/db/schema.rb";
+    SCHEMA = "${cfg.statePath}/db/structure.sql";
     GITLAB_UPLOADS_PATH = "${cfg.statePath}/uploads";
     GITLAB_LOG_PATH = "${cfg.statePath}/log";
     GITLAB_REDIS_CONFIG_FILE = pkgs.writeText "redis.yml" (builtins.toJSON redisConfig);
@@ -308,6 +311,12 @@ in {
         description = "Extra configuration in config/database.yml.";
       };
 
+      redisUrl = mkOption {
+        type = types.str;
+        default = "redis://localhost:6379/";
+        description = "Redis URL for all GitLab services except gitlab-shell";
+      };
+
       extraGitlabRb = mkOption {
         type = types.str;
         default = "";
@@ -508,6 +517,12 @@ in {
         '';
       };
 
+      extraShellConfig = mkOption {
+        type = types.attrs;
+        default = {};
+        description = "Extra configuration to merge into shell-config.yml";
+      };
+
       extraConfig = mkOption {
         type = types.attrs;
         default = {};
@@ -609,26 +624,38 @@ in {
       enable = true;
       ensureUsers = singleton { name = cfg.databaseUsername; };
     };
+
     # The postgresql module doesn't currently support concepts like
     # objects owners and extensions; for now we tack on what's needed
     # here.
-    systemd.services.postgresql.postStart = mkAfter (optionalString databaseActuallyCreateLocally ''
-      set -eu
-
-      $PSQL -tAc "SELECT 1 FROM pg_database WHERE datname = '${cfg.databaseName}'" | grep -q 1 || $PSQL -tAc 'CREATE DATABASE "${cfg.databaseName}" OWNER "${cfg.databaseUsername}"'
-      current_owner=$($PSQL -tAc "SELECT pg_catalog.pg_get_userbyid(datdba) FROM pg_catalog.pg_database WHERE datname = '${cfg.databaseName}'")
-      if [[ "$current_owner" != "${cfg.databaseUsername}" ]]; then
-          $PSQL -tAc 'ALTER DATABASE "${cfg.databaseName}" OWNER TO "${cfg.databaseUsername}"'
-          if [[ -e "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}" ]]; then
-              echo "Reassigning ownership of database ${cfg.databaseName} to user ${cfg.databaseUsername} failed on last boot. Failing..."
-              exit 1
-          fi
-          touch "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}"
-          $PSQL "${cfg.databaseName}" -tAc "REASSIGN OWNED BY \"$current_owner\" TO \"${cfg.databaseUsername}\""
-          rm "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}"
-      fi
-      $PSQL '${cfg.databaseName}' -tAc "CREATE EXTENSION IF NOT EXISTS pg_trgm"
-    '');
+    systemd.services.gitlab-postgresql = let pgsql = config.services.postgresql; in mkIf databaseActuallyCreateLocally {
+      after = [ "postgresql.service" ];
+      wantedBy = [ "multi-user.target" ];
+      path = [ pgsql.package ];
+      script = ''
+        set -eu
+
+        PSQL="${pkgs.utillinux}/bin/runuser -u ${pgsql.superUser} -- psql --port=${toString pgsql.port}"
+
+        $PSQL -tAc "SELECT 1 FROM pg_database WHERE datname = '${cfg.databaseName}'" | grep -q 1 || $PSQL -tAc 'CREATE DATABASE "${cfg.databaseName}" OWNER "${cfg.databaseUsername}"'
+        current_owner=$($PSQL -tAc "SELECT pg_catalog.pg_get_userbyid(datdba) FROM pg_catalog.pg_database WHERE datname = '${cfg.databaseName}'")
+        if [[ "$current_owner" != "${cfg.databaseUsername}" ]]; then
+            $PSQL -tAc 'ALTER DATABASE "${cfg.databaseName}" OWNER TO "${cfg.databaseUsername}"'
+            if [[ -e "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}" ]]; then
+                echo "Reassigning ownership of database ${cfg.databaseName} to user ${cfg.databaseUsername} failed on last boot. Failing..."
+                exit 1
+            fi
+            touch "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}"
+            $PSQL "${cfg.databaseName}" -tAc "REASSIGN OWNED BY \"$current_owner\" TO \"${cfg.databaseUsername}\""
+            rm "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}"
+        fi
+        $PSQL '${cfg.databaseName}' -tAc "CREATE EXTENSION IF NOT EXISTS pg_trgm"
+      '';
+
+      serviceConfig = {
+        Type = "oneshot";
+      };
+    };
 
     # Use postfix to send out mails.
     services.postfix.enable = mkDefault true;
@@ -675,7 +702,6 @@ in {
       "L+ /run/gitlab/shell-config.yml - - - - ${pkgs.writeText "config.yml" (builtins.toJSON gitlabShellConfig)}"
 
       "L+ ${cfg.statePath}/config/unicorn.rb - - - - ${./defaultUnicornConfig.rb}"
-      "L+ ${cfg.statePath}/config/initializers/extra-gitlab.rb - - - - ${extraGitlabRb}"
     ];
 
     systemd.services.gitlab-sidekiq = {
@@ -701,7 +727,7 @@ in {
         TimeoutSec = "infinity";
         Restart = "on-failure";
         WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab";
-        ExecStart="${cfg.packages.gitlab.rubyEnv}/bin/sidekiq -C \"${cfg.packages.gitlab}/share/gitlab/config/sidekiq_queues.yml\" -e production -P ${cfg.statePath}/tmp/sidekiq.pid";
+        ExecStart="${cfg.packages.gitlab.rubyEnv}/bin/sidekiq -C \"${cfg.packages.gitlab}/share/gitlab/config/sidekiq_queues.yml\" -e production";
       };
     };
 
@@ -758,7 +784,7 @@ in {
     };
 
     systemd.services.gitlab = {
-      after = [ "gitlab-workhorse.service" "gitaly.service" "network.target" "postgresql.service" "redis.service" ];
+      after = [ "gitlab-workhorse.service" "gitaly.service" "network.target" "gitlab-postgresql.service" "redis.service" ];
       requires = [ "gitlab-sidekiq.service" ];
       wantedBy = [ "multi-user.target" ];
       environment = gitlabEnv;
@@ -795,6 +821,7 @@ in {
             rm -f ${cfg.statePath}/lib
             cp -rf --no-preserve=mode ${cfg.packages.gitlab}/share/gitlab/config.dist/* ${cfg.statePath}/config
             cp -rf --no-preserve=mode ${cfg.packages.gitlab}/share/gitlab/db/* ${cfg.statePath}/db
+            ln -sf ${extraGitlabRb} ${cfg.statePath}/config/initializers/extra-gitlab.rb
 
             ${cfg.packages.gitlab-shell}/bin/install
 
diff --git a/nixos/modules/services/misc/gitolite.nix b/nixos/modules/services/misc/gitolite.nix
index cc69f81bbcc..59cbdac319c 100644
--- a/nixos/modules/services/misc/gitolite.nix
+++ b/nixos/modules/services/misc/gitolite.nix
@@ -27,7 +27,10 @@ in
         type = types.str;
         default = "/var/lib/gitolite";
         description = ''
-          Gitolite home directory (used to store all the repositories).
+          The gitolite home directory used to store all repositories. If left as the default value
+          this directory will automatically be created before the gitolite server starts, otherwise
+          the sysadmin is responsible for ensuring the directory exists with appropriate ownership
+          and permissions.
         '';
       };
 
@@ -149,14 +152,6 @@ in
     };
     users.groups.${cfg.group}.gid = config.ids.gids.gitolite;
 
-    systemd.tmpfiles.rules = [
-      "d '${cfg.dataDir}' 0750 ${cfg.user} ${cfg.group} - -"
-      "d '${cfg.dataDir}'/.gitolite - ${cfg.user} ${cfg.group} - -"
-      "d '${cfg.dataDir}'/.gitolite/logs - ${cfg.user} ${cfg.group} - -"
-
-      "Z ${cfg.dataDir} 0750 ${cfg.user} ${cfg.group} - -"
-    ];
-
     systemd.services.gitolite-init = {
       description = "Gitolite initialization";
       wantedBy    = [ "multi-user.target" ];
@@ -167,13 +162,19 @@ in
         GITOLITE_RC_DEFAULT = "${rcDir}/gitolite.rc.default";
       };
 
-      serviceConfig = {
-        Type = "oneshot";
-        User = cfg.user;
-        Group = cfg.group;
-        WorkingDirectory = "~";
-        RemainAfterExit = true;
-      };
+      serviceConfig = mkMerge [
+        (mkIf (cfg.dataDir == "/var/lib/gitolite") {
+          StateDirectory = "gitolite gitolite/.gitolite gitolite/.gitolite/logs";
+          StateDirectoryMode = "0750";
+        })
+        {
+          Type = "oneshot";
+          User = cfg.user;
+          Group = cfg.group;
+          WorkingDirectory = "~";
+          RemainAfterExit = true;
+        }
+      ];
 
       path = [ pkgs.gitolite pkgs.git pkgs.perl pkgs.bash pkgs.diffutils config.programs.ssh.package ];
       script =
diff --git a/nixos/modules/services/misc/gollum.nix b/nixos/modules/services/misc/gollum.nix
index f4a9c72b154..0c9c7548305 100644
--- a/nixos/modules/services/misc/gollum.nix
+++ b/nixos/modules/services/misc/gollum.nix
@@ -50,6 +50,12 @@ in
       description = "Parse and interpret emoji tags";
     };
 
+    h1-title = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Use the first h1 as page title";
+    };
+
     branch = mkOption {
       type = types.str;
       default = "master";
@@ -98,10 +104,11 @@ in
           ${pkgs.gollum}/bin/gollum \
             --port ${toString cfg.port} \
             --host ${cfg.address} \
-            --config ${builtins.toFile "gollum-config.rb" cfg.extraConfig} \
+            --config ${pkgs.writeText "gollum-config.rb" cfg.extraConfig} \
             --ref ${cfg.branch} \
             ${optionalString cfg.mathjax "--mathjax"} \
             ${optionalString cfg.emoji "--emoji"} \
+            ${optionalString cfg.h1-title "--h1-title"} \
             ${optionalString (cfg.allowUploads != null) "--allow-uploads ${cfg.allowUploads}"} \
             ${cfg.stateDir}
         '';
diff --git a/nixos/modules/services/misc/home-assistant.nix b/nixos/modules/services/misc/home-assistant.nix
index 86033d02bf3..0477254e7c1 100644
--- a/nixos/modules/services/misc/home-assistant.nix
+++ b/nixos/modules/services/misc/home-assistant.nix
@@ -11,9 +11,9 @@ let
     (recursiveUpdate defaultConfig cfg.config) else cfg.config));
   configFile = pkgs.runCommand "configuration.yaml" { preferLocalBuild = true; } ''
     ${pkgs.remarshal}/bin/json2yaml -i ${configJSON} -o $out
-    # Hack to support secrets, that are encoded as custom yaml objects,
-    # https://www.home-assistant.io/docs/configuration/secrets/
-    sed -i -e "s/'\!secret \(.*\)'/\!secret \1/" $out
+    # Hack to support custom yaml objects,
+    # i.e. secrets: https://www.home-assistant.io/docs/configuration/secrets/
+    sed -i -e "s/'\!\([a-z_]\+\) \(.*\)'/\!\1 \2/;s/^\!\!/\!/;" $out
   '';
 
   lovelaceConfigJSON = pkgs.writeText "ui-lovelace.json"
@@ -120,7 +120,9 @@ in {
             unit_system = "metric";
             time_zone = "UTC";
           };
-          frontend = { };
+          frontend = {
+            themes = "!include_dir_merge_named themes";
+          };
           http = { };
           feedreader.urls = [ "https://nixos.org/blogs.xml" ];
         }
@@ -238,6 +240,7 @@ in {
       '');
       serviceConfig = {
         ExecStart = "${package}/bin/hass --config '${cfg.configDir}'";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
         User = "hass";
         Group = "hass";
         Restart = "on-failure";
diff --git a/nixos/modules/services/misc/jellyfin.nix b/nixos/modules/services/misc/jellyfin.nix
index 6ecdfb57dc3..0493dadea94 100644
--- a/nixos/modules/services/misc/jellyfin.nix
+++ b/nixos/modules/services/misc/jellyfin.nix
@@ -16,6 +16,14 @@ in
         description = "User account under which Jellyfin runs.";
       };
 
+      package = mkOption {
+        type = types.package;
+        example = literalExample "pkgs.jellyfin";
+        description = ''
+          Jellyfin package to use.
+        '';
+      };
+
       group = mkOption {
         type = types.str;
         default = "jellyfin";
@@ -35,11 +43,16 @@ in
         Group = cfg.group;
         StateDirectory = "jellyfin";
         CacheDirectory = "jellyfin";
-        ExecStart = "${pkgs.jellyfin}/bin/jellyfin --datadir '/var/lib/${StateDirectory}' --cachedir '/var/cache/${CacheDirectory}'";
+        ExecStart = "${cfg.package}/bin/jellyfin --datadir '/var/lib/${StateDirectory}' --cachedir '/var/cache/${CacheDirectory}'";
         Restart = "on-failure";
       };
     };
 
+    services.jellyfin.package = mkDefault (
+      if versionAtLeast config.system.stateVersion "20.09" then pkgs.jellyfin
+        else pkgs.jellyfin_10_5
+    );
+
     users.users = mkIf (cfg.user == "jellyfin") {
       jellyfin = {
         group = cfg.group;
diff --git a/nixos/modules/services/misc/mathics.nix b/nixos/modules/services/misc/mathics.nix
deleted file mode 100644
index c588a30d76c..00000000000
--- a/nixos/modules/services/misc/mathics.nix
+++ /dev/null
@@ -1,54 +0,0 @@
-{ pkgs, lib, config, ... }:
-
-with lib;
-
-let
-  cfg = config.services.mathics;
-
-in {
-  options = {
-    services.mathics = {
-      enable = mkEnableOption "Mathics notebook service";
-
-      external = mkOption {
-        type = types.bool;
-        default = false;
-        description = "Listen on all interfaces, rather than just localhost?";
-      };
-
-      port = mkOption {
-        type = types.int;
-        default = 8000;
-        description = "TCP port to listen on.";
-      };
-    };
-  };
-
-  config = mkIf cfg.enable {
-
-    users.users.mathics = {
-      group = config.users.groups.mathics.name;
-      description = "Mathics user";
-      home = "/var/lib/mathics";
-      createHome = true;
-      uid = config.ids.uids.mathics;
-    };
-
-    users.groups.mathics.gid = config.ids.gids.mathics;
-
-    systemd.services.mathics = {
-      description = "Mathics notebook server";
-      wantedBy = [ "multi-user.target" ];
-      after = [ "network.target" ];
-      serviceConfig = {
-        User = config.users.users.mathics.name;
-        Group = config.users.groups.mathics.name;
-        ExecStart = concatStringsSep " " [
-          "${pkgs.mathics}/bin/mathicsserver"
-          "--port" (toString cfg.port)
-          (if cfg.external then "--external" else "")
-        ];
-      };
-    };
-  };
-}
diff --git a/nixos/modules/services/misc/matrix-appservice-discord.nix b/nixos/modules/services/misc/matrix-appservice-discord.nix
new file mode 100644
index 00000000000..49c41ff637a
--- /dev/null
+++ b/nixos/modules/services/misc/matrix-appservice-discord.nix
@@ -0,0 +1,162 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  dataDir = "/var/lib/matrix-appservice-discord";
+  registrationFile = "${dataDir}/discord-registration.yaml";
+  appDir = "${pkgs.matrix-appservice-discord}/lib/node_modules/matrix-appservice-discord";
+  cfg = config.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);
+
+in {
+  options = {
+    services.matrix-appservice-discord = {
+      enable = mkEnableOption "a bridge between Matrix and Discord";
+
+      settings = mkOption rec {
+        # TODO: switch to types.config.json as prescribed by RFC42 once it's implemented
+        type = types.attrs;
+        apply = recursiveUpdate default;
+        default = {
+          database = {
+            filename = "${dataDir}/discord.db";
+
+            # TODO: remove those old config keys once the following issues are solved:
+            # * https://github.com/Half-Shot/matrix-appservice-discord/issues/490
+            # * https://github.com/Half-Shot/matrix-appservice-discord/issues/498
+            userStorePath = "${dataDir}/user-store.db";
+            roomStorePath = "${dataDir}/room-store.db";
+          };
+
+          # empty values necessary for registration file generation
+          # actual values defined in environmentFile
+          auth = {
+            clientID = "";
+            botToken = "";
+          };
+        };
+        example = literalExample ''
+          {
+            bridge = {
+              domain = "public-domain.tld";
+              homeserverUrl = "http://public-domain.tld:8008";
+            };
+          }
+        '';
+        description = ''
+          <filename>config.yaml</filename> configuration as a Nix attribute set.
+          </para>
+
+          <para>
+          Configuration options should match those described in
+          <link xlink:href="https://github.com/Half-Shot/matrix-appservice-discord/blob/master/config/config.sample.yaml">
+          config.sample.yaml</link>.
+          </para>
+
+          <para>
+          <option>config.bridge.domain</option> and <option>config.bridge.homeserverUrl</option>
+          should be set to match the public host name of the Matrix homeserver for webhooks and avatars to work.
+          </para>
+
+          <para>
+          Secret tokens should be specified using <option>environmentFile</option>
+          instead of this world-readable attribute set.
+        '';
+      };
+
+      environmentFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          File containing environment variables to be passed to the matrix-appservice-discord service,
+          in which secret tokens can be specified securely by defining values for
+          <literal>APPSERVICE_DISCORD_AUTH_CLIENT_I_D</literal> and
+          <literal>APPSERVICE_DISCORD_AUTH_BOT_TOKEN</literal>.
+        '';
+      };
+
+      url = mkOption {
+        type = types.str;
+        default = "http://localhost:${toString cfg.port}";
+        description = ''
+          The URL where the application service is listening for HS requests.
+        '';
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 9005; # from https://github.com/Half-Shot/matrix-appservice-discord/blob/master/package.json#L11
+        description = ''
+          Port number on which the bridge should listen for internal communication with the Matrix homeserver.
+        '';
+      };
+
+      localpart = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          The user_id localpart to assign to the AS.
+        '';
+      };
+
+      serviceDependencies = mkOption {
+        type = with types; listOf str;
+        default = optional config.services.matrix-synapse.enable "matrix-synapse.service";
+        description = ''
+          List of Systemd services to require and wait for when starting the application service,
+          such as the Matrix homeserver if it's running on the same host.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.matrix-appservice-discord = {
+      description = "A bridge between Matrix and Discord.";
+
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ] ++ cfg.serviceDependencies;
+      after = [ "network-online.target" ] ++ cfg.serviceDependencies;
+
+      preStart = ''
+        if [ ! -f '${registrationFile}' ]; then
+          ${pkgs.matrix-appservice-discord}/bin/matrix-appservice-discord \
+            --generate-registration \
+            --url=${escapeShellArg cfg.url} \
+            ${optionalString (cfg.localpart != null) "--localpart=${escapeShellArg cfg.localpart}"} \
+            --config='${settingsFile}' \
+            --file='${registrationFile}'
+        fi
+      '';
+
+      serviceConfig = {
+        Type = "simple";
+        Restart = "always";
+
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+
+        DynamicUser = true;
+        PrivateTmp = true;
+        WorkingDirectory = appDir;
+        StateDirectory = baseNameOf dataDir;
+        UMask = 0027;
+        EnvironmentFile = cfg.environmentFile;
+
+        ExecStart = ''
+          ${pkgs.matrix-appservice-discord}/bin/matrix-appservice-discord \
+            --file='${registrationFile}' \
+            --config='${settingsFile}' \
+            --port='${toString cfg.port}'
+        '';
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ pacien ];
+}
diff --git a/nixos/modules/services/misc/matrix-synapse.nix b/nixos/modules/services/misc/matrix-synapse.nix
index 703bc9416f8..3eb1073387f 100644
--- a/nixos/modules/services/misc/matrix-synapse.nix
+++ b/nixos/modules/services/misc/matrix-synapse.nix
@@ -9,6 +9,9 @@ let
   logConfigFile = pkgs.writeText "log_config.yaml" cfg.logConfig;
   mkResource = r: ''{names: ${builtins.toJSON r.names}, compress: ${boolToString r.compress}}'';
   mkListener = l: ''{port: ${toString l.port}, bind_address: "${l.bind_address}", type: ${l.type}, tls: ${boolToString l.tls}, x_forwarded: ${boolToString l.x_forwarded}, resources: [${concatStringsSep "," (map mkResource l.resources)}]}'';
+  pluginsEnv = cfg.package.python.buildEnv.override {
+    extraLibs = cfg.plugins;
+  };
   configFile = pkgs.writeText "homeserver.yaml" ''
 ${optionalString (cfg.tls_certificate_path != null) ''
 tls_certificate_path: "${cfg.tls_certificate_path}"
@@ -125,6 +128,14 @@ in {
           Overridable attribute of the matrix synapse server package to use.
         '';
       };
+      plugins = mkOption {
+        type = types.listOf types.package;
+        default = [ ];
+        defaultText = "with config.services.matrix-synapse.package.plugins [ matrix-synapse-ldap3 matrix-synapse-pam ]";
+        description = ''
+          List of additional Matrix plugins to make available.
+        '';
+      };
       no_tls = mkOption {
         type = types.bool;
         default = false;
@@ -664,7 +675,7 @@ in {
       }
     ];
 
-    users.users.matrix-synapse = { 
+    users.users.matrix-synapse = {
         group = "matrix-synapse";
         home = cfg.dataDir;
         createHome = true;
@@ -686,6 +697,7 @@ in {
           --keys-directory ${cfg.dataDir} \
           --generate-keys
       '';
+      environment.PYTHONPATH = makeSearchPathOutput "lib" cfg.package.python.sitePackages [ pluginsEnv ];
       serviceConfig = {
         Type = "notify";
         User = "matrix-synapse";
@@ -715,5 +727,6 @@ in {
   ];
 
   meta.doc = ./matrix-synapse.xml;
+  meta.maintainers = teams.matrix.members;
 
 }
diff --git a/nixos/modules/services/misc/matrix-synapse.xml b/nixos/modules/services/misc/matrix-synapse.xml
index 2f2ac27eeb9..fbfa838b168 100644
--- a/nixos/modules/services/misc/matrix-synapse.xml
+++ b/nixos/modules/services/misc/matrix-synapse.xml
@@ -14,9 +14,9 @@
  <para>
   This chapter will show you how to set up your own, self-hosted Matrix
   homeserver using the Synapse reference homeserver, and how to serve your own
-  copy of the Riot web client. See the
+  copy of the Element web client. See the
   <link xlink:href="https://matrix.org/docs/projects/try-matrix-now.html">Try
-  Matrix Now!</link> overview page for links to Riot Apps for Android and iOS,
+  Matrix Now!</link> overview page for links to Element Apps for Android and iOS,
   desktop clients, as well as bridges to other networks and other projects
   around Matrix.
  </para>
@@ -84,7 +84,7 @@ in {
               "m.homeserver" =  { "base_url" = "https://${fqdn}"; };
               "m.identity_server" =  { "base_url" = "https://vector.im"; };
             };
-          # ACAO required to allow riot-web on any URL to request this json file
+          # ACAO required to allow element-web on any URL to request this json file
           in ''
             add_header Content-Type application/json;
             add_header Access-Control-Allow-Origin *;
@@ -98,7 +98,7 @@ in {
         <link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true;
 
         # Or do a redirect instead of the 404, or whatever is appropriate for you.
-        # But do not put a Matrix Web client here! See the Riot Web section below.
+        # But do not put a Matrix Web client here! See the Element web section below.
         <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.extraConfig">locations."/".extraConfig</link> = ''
           return 404;
         '';
@@ -171,17 +171,19 @@ Success!
    option until a better solution for NixOS is in place.
   </para>
  </section>
- <section xml:id="module-services-matrix-riot-web">
-  <title>Riot Web Client</title>
+ <section xml:id="module-services-matrix-element-web">
+  <title>Element (formerly known as Riot) Web Client</title>
 
   <para>
-   <link xlink:href="https://github.com/vector-im/riot-web/">Riot Web</link> is
+   <link xlink:href="https://github.com/vector-im/riot-web/">Element Web</link> is
    the reference web client for Matrix and developed by the core team at
-   matrix.org. The following snippet can be optionally added to the code before
+   matrix.org. Element was formerly known as Riot.im, see the
+   <link xlink:href="https://element.io/blog/welcome-to-element/">Element introductory blog post</link>
+   for more information. The following snippet can be optionally added to the code before
    to complete the synapse installation with a web client served at
-   <code>https://riot.myhostname.example.org</code> and
-   <code>https://riot.example.org</code>. Alternatively, you can use the hosted
-   copy at <link xlink:href="https://riot.im/app">https://riot.im/app</link>,
+   <code>https://element.myhostname.example.org</code> and
+   <code>https://element.example.org</code>. Alternatively, you can use the hosted
+   copy at <link xlink:href="https://app.element.io/">https://app.element.io/</link>,
    or use other web clients or native client applications. Due to the
    <literal>/.well-known</literal> urls set up done above, many clients should
    fill in the required connection details automatically when you enter your
@@ -191,14 +193,14 @@ Success!
    featureset.
 <programlisting>
 {
-  services.nginx.virtualHosts."riot.${fqdn}" = {
+  services.nginx.virtualHosts."element.${fqdn}" = {
     <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true;
     <link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true;
     <link linkend="opt-services.nginx.virtualHosts._name_.serverAliases">serverAliases</link> = [
-      "riot.${config.networking.domain}"
+      "element.${config.networking.domain}"
     ];
 
-    <link linkend="opt-services.nginx.virtualHosts._name_.root">root</link> = pkgs.riot-web.override {
+    <link linkend="opt-services.nginx.virtualHosts._name_.root">root</link> = pkgs.element-web.override {
       conf = {
         default_server_config."m.homeserver" = {
           "base_url" = "${config.networking.domain}";
@@ -212,13 +214,13 @@ Success!
   </para>
 
   <para>
-   Note that the Riot developers do not recommend running Riot and your Matrix
+   Note that the Element developers do not recommend running Element and your Matrix
    homeserver on the same fully-qualified domain name for security reasons. In
    the example, this means that you should not reuse the
-   <literal>myhostname.example.org</literal> virtualHost to also serve Riot,
+   <literal>myhostname.example.org</literal> virtualHost to also serve Element,
    but instead serve it on a different subdomain, like
-   <literal>riot.example.org</literal> in the example. See the
-   <link xlink:href="https://github.com/vector-im/riot-web#important-security-note">Riot
+   <literal>element.example.org</literal> in the example. See the
+   <link xlink:href="https://github.com/vector-im/riot-web#important-security-note">Element
    Important Security Notes</link> for more information on this subject.
   </para>
  </section>
diff --git a/nixos/modules/services/misc/mautrix-telegram.nix b/nixos/modules/services/misc/mautrix-telegram.nix
new file mode 100644
index 00000000000..c5e8a5b85ec
--- /dev/null
+++ b/nixos/modules/services/misc/mautrix-telegram.nix
@@ -0,0 +1,163 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  dataDir = "/var/lib/mautrix-telegram";
+  registrationFile = "${dataDir}/telegram-registration.yaml";
+  cfg = config.services.mautrix-telegram;
+  # TODO: switch to configGen.json once RFC42 is implemented
+  settingsFile = pkgs.writeText "mautrix-telegram-settings.json" (builtins.toJSON cfg.settings);
+
+in {
+  options = {
+    services.mautrix-telegram = {
+      enable = mkEnableOption "Mautrix-Telegram, a Matrix-Telegram hybrid puppeting/relaybot bridge";
+
+      settings = mkOption rec {
+        # TODO: switch to types.config.json as prescribed by RFC42 once it's implemented
+        type = types.attrs;
+        apply = recursiveUpdate default;
+        default = {
+          appservice = rec {
+            database = "sqlite:///${dataDir}/mautrix-telegram.db";
+            hostname = "0.0.0.0";
+            port = 8080;
+            address = "http://localhost:${toString port}";
+          };
+
+          bridge = {
+            permissions."*" = "relaybot";
+            relaybot.whitelist = [ ];
+          };
+
+          logging = {
+            version = 1;
+
+            formatters.precise.format = "[%(levelname)s@%(name)s] %(message)s";
+
+            handlers.console = {
+              class = "logging.StreamHandler";
+              formatter = "precise";
+            };
+
+            loggers = {
+              mau.level = "INFO";
+              telethon.level = "INFO";
+
+              # prevent tokens from leaking in the logs:
+              # https://github.com/tulir/mautrix-telegram/issues/351
+              aiohttp.level = "WARNING";
+            };
+
+            # log to console/systemd instead of file
+            root = {
+              level = "INFO";
+              handlers = [ "console" ];
+            };
+          };
+        };
+        example = literalExample ''
+          {
+            homeserver = {
+              address = "http://localhost:8008";
+              domain = "public-domain.tld";
+            };
+
+            appservice.public = {
+              prefix = "/public";
+              external = "https://public-appservice-address/public";
+            };
+
+            bridge.permissions = {
+              "example.com" = "full";
+              "@admin:example.com" = "admin";
+            };
+          }
+        '';
+        description = ''
+          <filename>config.yaml</filename> configuration as a Nix attribute set.
+          Configuration options should match those described in
+          <link xlink:href="https://github.com/tulir/mautrix-telegram/blob/master/example-config.yaml">
+          example-config.yaml</link>.
+          </para>
+
+          <para>
+          Secret tokens should be specified using <option>environmentFile</option>
+          instead of this world-readable attribute set.
+        '';
+      };
+
+      environmentFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          File containing environment variables to be passed to the mautrix-telegram service,
+          in which secret tokens can be specified securely by defining values for
+          <literal>MAUTRIX_TELEGRAM_APPSERVICE_AS_TOKEN</literal>,
+          <literal>MAUTRIX_TELEGRAM_APPSERVICE_HS_TOKEN</literal>,
+          <literal>MAUTRIX_TELEGRAM_TELEGRAM_API_ID</literal>,
+          <literal>MAUTRIX_TELEGRAM_TELEGRAM_API_HASH</literal> and optionally
+          <literal>MAUTRIX_TELEGRAM_TELEGRAM_BOT_TOKEN</literal>.
+        '';
+      };
+
+      serviceDependencies = mkOption {
+        type = with types; listOf str;
+        default = optional config.services.matrix-synapse.enable "matrix-synapse.service";
+        description = ''
+          List of Systemd services to require and wait for when starting the application service.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.mautrix-telegram = {
+      description = "Mautrix-Telegram, a Matrix-Telegram hybrid puppeting/relaybot bridge.";
+
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ] ++ cfg.serviceDependencies;
+      after = [ "network-online.target" ] ++ cfg.serviceDependencies;
+
+      preStart = ''
+        # generate the appservice's registration file if absent
+        if [ ! -f '${registrationFile}' ]; then
+          ${pkgs.mautrix-telegram}/bin/mautrix-telegram \
+            --generate-registration \
+            --base-config='${pkgs.mautrix-telegram}/${pkgs.mautrix-telegram.pythonModule.sitePackages}/mautrix_telegram/example-config.yaml' \
+            --config='${settingsFile}' \
+            --registration='${registrationFile}'
+        fi
+
+        # run automatic database init and migration scripts
+        ${pkgs.mautrix-telegram.alembic}/bin/alembic -x config='${settingsFile}' upgrade head
+      '';
+
+      serviceConfig = {
+        Type = "simple";
+        Restart = "always";
+
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+
+        DynamicUser = true;
+        PrivateTmp = true;
+        WorkingDirectory = pkgs.mautrix-telegram; # necessary for the database migration scripts to be found
+        StateDirectory = baseNameOf dataDir;
+        UMask = 0027;
+        EnvironmentFile = cfg.environmentFile;
+
+        ExecStart = ''
+          ${pkgs.mautrix-telegram}/bin/mautrix-telegram \
+            --config='${settingsFile}'
+        '';
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ pacien vskilet ];
+}
diff --git a/nixos/modules/services/misc/mesos-master.nix b/nixos/modules/services/misc/mesos-master.nix
deleted file mode 100644
index 572a9847e46..00000000000
--- a/nixos/modules/services/misc/mesos-master.nix
+++ /dev/null
@@ -1,125 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-  cfg = config.services.mesos.master;
-
-in {
-
-  options.services.mesos = {
-
-    master = {
-      enable = mkOption {
-        description = "Whether to enable the Mesos Master.";
-        default = false;
-        type = types.bool;
-      };
-
-      ip = mkOption {
-        description = "IP address to listen on.";
-        default = "0.0.0.0";
-        type = types.str;
-      };
-
-      port = mkOption {
-        description = "Mesos Master port";
-        default = 5050;
-        type = types.int;
-      };
-
-      advertiseIp = mkOption {
-        description = "IP address advertised to reach this master.";
-        default = null;
-        type = types.nullOr types.str;
-      };
-
-      advertisePort = mkOption {
-        description = "Port advertised to reach this Mesos master.";
-        default = null;
-        type = types.nullOr types.int;
-      };
-
-      zk = mkOption {
-        description = ''
-          ZooKeeper URL (used for leader election amongst masters).
-          May be one of:
-            zk://host1:port1,host2:port2,.../mesos
-            zk://username:password@host1:port1,host2:port2,.../mesos
-        '';
-        type = types.str;
-      };
-
-      workDir = mkOption {
-        description = "The Mesos work directory.";
-        default = "/var/lib/mesos/master";
-        type = types.str;
-      };
-
-      extraCmdLineOptions = mkOption {
-        description = ''
-          Extra command line options for Mesos Master.
-
-          See https://mesos.apache.org/documentation/latest/configuration/
-        '';
-        default = [ "" ];
-        type = types.listOf types.str;
-        example = [ "--credentials=VALUE" ];
-      };
-
-      quorum = mkOption {
-        description = ''
-          The size of the quorum of replicas when using 'replicated_log' based
-          registry. It is imperative to set this value to be a majority of
-          masters i.e., quorum > (number of masters)/2.
-
-          If 0 will fall back to --registry=in_memory.
-        '';
-        default = 0;
-        type = types.int;
-      };
-
-      logLevel = mkOption {
-        description = ''
-          The logging level used. Possible values:
-            'INFO', 'WARNING', 'ERROR'
-        '';
-        default = "INFO";
-        type = types.str;
-      };
-
-    };
-
-
-  };
-
-
-  config = mkIf cfg.enable {
-    systemd.tmpfiles.rules = [
-      "d '${cfg.workDir}' 0700 - - - -"
-    ];
-    systemd.services.mesos-master = {
-      description = "Mesos Master";
-      wantedBy = [ "multi-user.target" ];
-      after = [ "network.target" ];
-      serviceConfig = {
-        ExecStart = ''
-          ${pkgs.mesos}/bin/mesos-master \
-            --ip=${cfg.ip} \
-            --port=${toString cfg.port} \
-            ${optionalString (cfg.advertiseIp != null) "--advertise_ip=${cfg.advertiseIp}"} \
-            ${optionalString (cfg.advertisePort  != null) "--advertise_port=${toString cfg.advertisePort}"} \
-            ${if cfg.quorum == 0
-              then "--registry=in_memory"
-              else "--zk=${cfg.zk} --registry=replicated_log --quorum=${toString cfg.quorum}"} \
-            --work_dir=${cfg.workDir} \
-            --logging_level=${cfg.logLevel} \
-            ${toString cfg.extraCmdLineOptions}
-        '';
-        Restart = "on-failure";
-      };
-    };
-  };
-
-}
-
diff --git a/nixos/modules/services/misc/mesos-slave.nix b/nixos/modules/services/misc/mesos-slave.nix
deleted file mode 100644
index 170065d0065..00000000000
--- a/nixos/modules/services/misc/mesos-slave.nix
+++ /dev/null
@@ -1,220 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-  cfg = config.services.mesos.slave;
-
-  mkAttributes =
-    attrs: concatStringsSep ";" (mapAttrsToList
-                                   (k: v: "${k}:${v}")
-                                   (filterAttrs (k: v: v != null) attrs));
-  attribsArg = optionalString (cfg.attributes != {})
-                              "--attributes=${mkAttributes cfg.attributes}";
-
-  containerizersArg = concatStringsSep "," (
-    lib.unique (
-      cfg.containerizers ++ (optional cfg.withDocker "docker")
-    )
-  );
-
-  imageProvidersArg = concatStringsSep "," (
-    lib.unique (
-      cfg.imageProviders ++ (optional cfg.withDocker "docker")
-    )
-  );
-
-  isolationArg = concatStringsSep "," (
-    lib.unique (
-      cfg.isolation ++ (optionals cfg.withDocker [ "filesystem/linux" "docker/runtime"])
-    )
-  );
-
-in {
-
-  options.services.mesos = {
-    slave = {
-      enable = mkOption {
-        description = "Whether to enable the Mesos Slave.";
-        default = false;
-        type = types.bool;
-      };
-
-      ip = mkOption {
-        description = "IP address to listen on.";
-        default = "0.0.0.0";
-        type = types.str;
-      };
-
-      port = mkOption {
-        description = "Port to listen on.";
-        default = 5051;
-        type = types.int;
-      };
-
-      advertiseIp = mkOption {
-        description = "IP address advertised to reach this agent.";
-        default = null;
-        type = types.nullOr types.str;
-      };
-
-      advertisePort = mkOption {
-        description = "Port advertised to reach this agent.";
-        default = null;
-        type = types.nullOr types.int;
-      };
-
-      containerizers = mkOption {
-        description = ''
-          List of containerizer implementations to compose in order to provide
-          containerization. Available options are mesos and docker.
-          The order the containerizers are specified is the order they are tried.
-        '';
-        default = [ "mesos" ];
-        type = types.listOf types.str;
-      };
-
-      imageProviders = mkOption {
-        description = "List of supported image providers, e.g., APPC,DOCKER.";
-        default = [ ];
-        type = types.listOf types.str;
-      };
-
-      imageProvisionerBackend = mkOption {
-        description = ''
-          Strategy for provisioning container rootfs from images,
-          e.g., aufs, bind, copy, overlay.
-        '';
-        default = "copy";
-        type = types.str;
-      };
-
-      isolation = mkOption {
-        description = ''
-          Isolation mechanisms to use, e.g., posix/cpu,posix/mem, or
-          cgroups/cpu,cgroups/mem, or network/port_mapping, or `gpu/nvidia` for nvidia
-          specific gpu isolation.
-        '';
-        default = [ "posix/cpu" "posix/mem" ];
-        type = types.listOf types.str;
-      };
-
-      master = mkOption {
-        description = ''
-          May be one of:
-            zk://host1:port1,host2:port2,.../path
-            zk://username:password@host1:port1,host2:port2,.../path
-        '';
-        type = types.str;
-      };
-
-      withHadoop = mkOption {
-        description = "Add the HADOOP_HOME to the slave.";
-        default = false;
-        type = types.bool;
-      };
-
-      withDocker = mkOption {
-        description = "Enable the docker containerizer.";
-        default = config.virtualisation.docker.enable;
-        type = types.bool;
-      };
-
-      dockerRegistry = mkOption {
-        description = ''
-          The default url for pulling Docker images.
-          It could either be a Docker registry server url,
-          or a local path in which Docker image archives are stored.
-        '';
-        default = null;
-        type = types.nullOr (types.either types.str types.path);
-      };
-
-      workDir = mkOption {
-        description = "The Mesos work directory.";
-        default = "/var/lib/mesos/slave";
-        type = types.str;
-      };
-
-      extraCmdLineOptions = mkOption {
-        description = ''
-          Extra command line options for Mesos Slave.
-
-          See https://mesos.apache.org/documentation/latest/configuration/
-        '';
-        default = [ "" ];
-        type = types.listOf types.str;
-        example = [ "--gc_delay=3days" ];
-      };
-
-      logLevel = mkOption {
-        description = ''
-          The logging level used. Possible values:
-            'INFO', 'WARNING', 'ERROR'
-        '';
-        default = "INFO";
-        type = types.str;
-      };
-
-      attributes = mkOption {
-        description = ''
-          Machine attributes for the slave instance.
-
-          Use caution when changing this; you may need to manually reset slave
-          metadata before the slave can re-register.
-        '';
-        default = {};
-        type = types.attrsOf types.str;
-        example = { rack = "aa";
-                    host = "aabc123";
-                    os = "nixos"; };
-      };
-
-      executorEnvironmentVariables = mkOption {
-        description = ''
-          The environment variables that should be passed to the executor, and thus subsequently task(s).
-        '';
-        default = {
-          PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
-        };
-        type = types.attrsOf types.str;
-      };
-    };
-
-  };
-
-  config = mkIf cfg.enable {
-    systemd.tmpfiles.rules = [
-      "d '${cfg.workDir}' 0701 - - - -"
-    ];
-    systemd.services.mesos-slave = {
-      description = "Mesos Slave";
-      wantedBy = [ "multi-user.target" ];
-      after = [ "network.target" ] ++ optionals cfg.withDocker [ "docker.service" ] ;
-      path = [ pkgs.runtimeShellPackage ];
-      serviceConfig = {
-        ExecStart = ''
-          ${pkgs.mesos}/bin/mesos-slave \
-            --containerizers=${containerizersArg} \
-            --image_providers=${imageProvidersArg} \
-            --image_provisioner_backend=${cfg.imageProvisionerBackend} \
-            --isolation=${isolationArg} \
-            --ip=${cfg.ip} \
-            --port=${toString cfg.port} \
-            ${optionalString (cfg.advertiseIp != null) "--advertise_ip=${cfg.advertiseIp}"} \
-            ${optionalString (cfg.advertisePort  != null) "--advertise_port=${toString cfg.advertisePort}"} \
-            --master=${cfg.master} \
-            --work_dir=${cfg.workDir} \
-            --logging_level=${cfg.logLevel} \
-            ${attribsArg} \
-            ${optionalString cfg.withHadoop "--hadoop-home=${pkgs.hadoop}"} \
-            ${optionalString cfg.withDocker "--docker=${pkgs.docker}/libexec/docker/docker"} \
-            ${optionalString (cfg.dockerRegistry != null) "--docker_registry=${cfg.dockerRegistry}"} \
-            --executor_environment_variables=${lib.escapeShellArg (builtins.toJSON cfg.executorEnvironmentVariables)} \
-            ${toString cfg.extraCmdLineOptions}
-        '';
-      };
-    };
-  };
-
-}
diff --git a/nixos/modules/services/misc/nix-daemon.nix b/nixos/modules/services/misc/nix-daemon.nix
index 2577cb78e96..924a007efc6 100644
--- a/nixos/modules/services/misc/nix-daemon.nix
+++ b/nixos/modules/services/misc/nix-daemon.nix
@@ -193,50 +193,111 @@ in
       };
 
       buildMachines = mkOption {
-        type = types.listOf types.attrs;
+        type = types.listOf (types.submodule ({
+          options = {
+            hostName = mkOption {
+              type = types.str;
+              example = "nixbuilder.example.org";
+              description = ''
+                The hostname of the build machine.
+              '';
+            };
+            system = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              example = "x86_64-linux";
+              description = ''
+                The system type the build machine can execute derivations on.
+                Either this attribute or <varname>systems</varname> must be
+                present, where <varname>system</varname> takes precedence if
+                both are set.
+              '';
+            };
+            systems = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              example = [ "x86_64-linux" "aarch64-linux" ];
+              description = ''
+                The system types the build machine can execute derivations on.
+                Either this attribute or <varname>system</varname> must be
+                present, where <varname>system</varname> takes precedence if
+                both are set.
+              '';
+            };
+            sshUser = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              example = "builder";
+              description = ''
+                The username to log in as on the remote host. This user must be
+                able to log in and run nix commands non-interactively. It must
+                also be privileged to build derivations, so must be included in
+                <option>nix.trustedUsers</option>.
+              '';
+            };
+            sshKey = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              example = "/root/.ssh/id_buildhost_builduser";
+              description = ''
+                The path to the SSH private key with which to authenticate on
+                the build machine. The private key must not have a passphrase.
+                If null, the building user (root on NixOS machines) must have an
+                appropriate ssh configuration to log in non-interactively.
+
+                Note that for security reasons, this path must point to a file
+                in the local filesystem, *not* to the nix store.
+              '';
+            };
+            maxJobs = mkOption {
+              type = types.int;
+              default = 1;
+              description = ''
+                The number of concurrent jobs the build machine supports. The
+                build machine will enforce its own limits, but this allows hydra
+                to schedule better since there is no work-stealing between build
+                machines.
+              '';
+            };
+            speedFactor = mkOption {
+              type = types.int;
+              default = 1;
+              description = ''
+                The relative speed of this builder. This is an arbitrary integer
+                that indicates the speed of this builder, relative to other
+                builders. Higher is faster.
+              '';
+            };
+            mandatoryFeatures = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              example = [ "big-parallel" ];
+              description = ''
+                A list of features mandatory for this builder. The builder will
+                be ignored for derivations that don't require all features in
+                this list. All mandatory features are automatically included in
+                <varname>supportedFeatures</varname>.
+              '';
+            };
+            supportedFeatures = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              example = [ "kvm" "big-parallel" ];
+              description = ''
+                A list of features supported by this builder. The builder will
+                be ignored for derivations that require features not in this
+                list.
+              '';
+            };
+          };
+        }));
         default = [];
-        example = literalExample ''
-          [ { hostName = "voila.labs.cs.uu.nl";
-              sshUser = "nix";
-              sshKey = "/root/.ssh/id_buildfarm";
-              system = "powerpc-darwin";
-              maxJobs = 1;
-            }
-            { hostName = "linux64.example.org";
-              sshUser = "buildfarm";
-              sshKey = "/root/.ssh/id_buildfarm";
-              system = "x86_64-linux";
-              maxJobs = 2;
-              speedFactor = 2;
-              supportedFeatures = [ "kvm" ];
-              mandatoryFeatures = [ "perf" ];
-            }
-          ]
-        '';
         description = ''
-          This option lists the machines to be used if distributed
-          builds are enabled (see
-          <option>nix.distributedBuilds</option>).  Nix will perform
-          derivations on those machines via SSH by copying the inputs
-          to the Nix store on the remote machine, starting the build,
-          then copying the output back to the local Nix store.  Each
-          element of the list should be an attribute set containing
-          the machine's host name (<varname>hostname</varname>), the
-          user name to be used for the SSH connection
-          (<varname>sshUser</varname>), the Nix system type
-          (<varname>system</varname>, e.g.,
-          <literal>"i686-linux"</literal>), the maximum number of
-          jobs to be run in parallel on that machine
-          (<varname>maxJobs</varname>), the path to the SSH private
-          key to be used to connect (<varname>sshKey</varname>), a
-          list of supported features of the machine
-          (<varname>supportedFeatures</varname>) and a list of
-          mandatory features of the machine
-          (<varname>mandatoryFeatures</varname>). The SSH private key
-          should not have a passphrase, and the corresponding public
-          key should be added to
-          <filename>~<replaceable>sshUser</replaceable>/authorized_keys</filename>
-          on the remote machine.
+          This option lists the machines to be used if distributed builds are
+          enabled (see <option>nix.distributedBuilds</option>).
+          Nix will perform derivations on those machines via SSH by copying the
+          inputs to the Nix store on the remote machine, starting the build,
+          then copying the output back to the local Nix store.
         '';
       };
 
@@ -439,9 +500,22 @@ in
 
   config = {
 
+    assertions = [
+      {
+        assertion = config.nix.distributedBuilds || config.nix.buildMachines == [];
+        message = "You must set `nix.distributedBuilds = true` to use nix.buildMachines";
+      }
+    ];
+
     nix.binaryCachePublicKeys = [ "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" ];
     nix.binaryCaches = [ "https://cache.nixos.org/" ];
 
+    environment.systemPackages =
+      [ nix
+        pkgs.nix-info
+      ]
+      ++ optional (config.programs.bash.enableCompletion && !versionAtLeast nixVersion "2.4pre") pkgs.nix-bash-completions;
+
     environment.etc."nix/nix.conf".source = nixConf;
 
     environment.etc."nix/registry.json".text = builtins.toJSON {
@@ -455,14 +529,14 @@ in
       { enable = cfg.buildMachines != [];
         text =
           concatMapStrings (machine:
-            "${if machine ? sshUser then "${machine.sshUser}@" else ""}${machine.hostName} "
-            + machine.system or (concatStringsSep "," machine.systems)
-            + " ${machine.sshKey or "-"} ${toString machine.maxJobs or 1} "
-            + toString (machine.speedFactor or 1)
+            "${if machine.sshUser != null then "${machine.sshUser}@" else ""}${machine.hostName} "
+            + (if machine.system != null then machine.system else concatStringsSep "," machine.systems)
+            + " ${if machine.sshKey != null then machine.sshKey else "-"} ${toString machine.maxJobs} "
+            + toString (machine.speedFactor)
             + " "
-            + concatStringsSep "," (machine.mandatoryFeatures or [] ++ machine.supportedFeatures or [])
+            + concatStringsSep "," (machine.mandatoryFeatures ++ machine.supportedFeatures)
             + " "
-            + concatStringsSep "," machine.mandatoryFeatures or []
+            + concatStringsSep "," machine.mandatoryFeatures
             + "\n"
           ) cfg.buildMachines;
       };
diff --git a/nixos/modules/services/misc/octoprint.nix b/nixos/modules/services/misc/octoprint.nix
index 7a71d2c8c6a..e2fbd3b401c 100644
--- a/nixos/modules/services/misc/octoprint.nix
+++ b/nixos/modules/services/misc/octoprint.nix
@@ -68,8 +68,8 @@ in
       plugins = mkOption {
         default = plugins: [];
         defaultText = "plugins: []";
-        example = literalExample "plugins: [ m3d-fio ]";
-        description = "Additional plugins.";
+        example = literalExample "plugins: with plugins; [ m33-fio stlviewer ]";
+        description = "Additional plugins to be used. Available plugins are passed through the plugins input.";
       };
 
       extraConfig = mkOption {
diff --git a/nixos/modules/services/misc/pinnwand.nix b/nixos/modules/services/misc/pinnwand.nix
new file mode 100644
index 00000000000..aa1ee5cfaa7
--- /dev/null
+++ b/nixos/modules/services/misc/pinnwand.nix
@@ -0,0 +1,78 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.pinnwand;
+
+  format = pkgs.formats.toml {};
+  configFile = format.generate "pinnwand.toml" cfg.settings;
+in
+{
+  options.services.pinnwand = {
+    enable = mkEnableOption "Pinnwand";
+
+    port = mkOption {
+      type = types.port;
+      description = "The port to listen on.";
+      default = 8000;
+    };
+
+    settings = mkOption {
+      type = format.type;
+      description = ''
+        Your <filename>pinnwand.toml</filename> as a Nix attribute set. Look up
+        possible options in the <link xlink:href="https://github.com/supakeen/pinnwand/blob/master/pinnwand.toml-example">pinnwand.toml-example</link>.
+      '';
+      default = {
+        # https://github.com/supakeen/pinnwand/blob/master/pinnwand.toml-example
+        database_uri = "sqlite:///var/lib/pinnwand/pinnwand.db";
+        preferred_lexeres = [];
+        paste_size = 262144;
+        paste_help = ''
+          <p>Welcome to pinnwand, this site is a pastebin. It allows you to share code with others. If you write code in the text area below and press the paste button you will be given a link you can share with others so they can view your code as well.</p><p>People with the link can view your pasted code, only you can remove your paste and it expires automatically. Note that anyone could guess the URI to your paste so don't rely on it being private.</p>
+        '';
+        footer = ''
+          View <a href="//github.com/supakeen/pinnwand" target="_BLANK">source code</a>, the <a href="/removal">removal</a> or <a href="/expiry">expiry</a> stories, or read the <a href="/about">about</a> page.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.pinnwand = {
+      description = "Pinnwannd HTTP Server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      unitConfig.Documentation = "https://pinnwand.readthedocs.io/en/latest/";
+      serviceConfig = {
+        ExecStart = "${pkgs.pinnwand}/bin/pinnwand --configuration-path ${configFile} http --port ${toString(cfg.port)}";
+        StateDirectory = "pinnwand";
+        StateDirectoryMode = "0700";
+
+        AmbientCapabilities = [];
+        CapabilityBoundingSet = "";
+        DevicePolicy = "closed";
+        DynamicUser = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        PrivateDevices = true;
+        PrivateUsers = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectKernelLogs = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = "@system-service";
+        UMask = "0077";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/redmine.nix b/nixos/modules/services/misc/redmine.nix
index 1febdba0c8f..0e71cf92569 100644
--- a/nixos/modules/services/misc/redmine.nix
+++ b/nixos/modules/services/misc/redmine.nix
@@ -26,7 +26,7 @@ let
       scm_mercurial_command: ${pkgs.mercurial}/bin/hg
       scm_git_command: ${pkgs.gitAndTools.git}/bin/git
       scm_cvs_command: ${pkgs.cvs}/bin/cvs
-      scm_bazaar_command: ${pkgs.bazaar}/bin/bzr
+      scm_bazaar_command: ${pkgs.breezy}/bin/bzr
       scm_darcs_command: ${pkgs.darcs}/bin/darcs
 
     ${cfg.extraConfig}
@@ -297,7 +297,7 @@ in
       environment.SCHEMA = "${cfg.stateDir}/cache/schema.db";
       path = with pkgs; [
         imagemagick
-        bazaar
+        breezy
         cvs
         darcs
         gitAndTools.git
diff --git a/nixos/modules/services/misc/siproxd.nix b/nixos/modules/services/misc/siproxd.nix
index ae7b27de8e7..0e87fc461d3 100644
--- a/nixos/modules/services/misc/siproxd.nix
+++ b/nixos/modules/services/misc/siproxd.nix
@@ -38,7 +38,7 @@ in
         type = types.bool;
         default = false;
         description = ''
-          Whether to enable the Siproxd SIP 
+          Whether to enable the Siproxd SIP
 	  proxy/masquerading daemon.
         '';
       };
@@ -111,7 +111,7 @@ in
         type = types.int;
         default = 300;
         description = ''
-          Timeout for an RTP stream. If for the specified 
+          Timeout for an RTP stream. If for the specified
           number of seconds no data is relayed on an active
           stream, it is considered dead and will be killed.
         '';
@@ -122,7 +122,7 @@ in
         default = 46;
         description = ''
           DSCP (differentiated services) value to be assigned
-          to RTP packets. Allows QOS aware routers to handle 
+          to RTP packets. Allows QOS aware routers to handle
           different types traffic with different priorities.
         '';
       };
@@ -132,7 +132,7 @@ in
         default = 0;
         description = ''
           DSCP (differentiated services) value to be assigned
-          to SIP packets. Allows QOS aware routers to handle 
+          to SIP packets. Allows QOS aware routers to handle
           different types traffic with different priorities.
         '';
       };
diff --git a/nixos/modules/services/misc/ssm-agent.nix b/nixos/modules/services/misc/ssm-agent.nix
index f7c05deeecb..00e806695fd 100644
--- a/nixos/modules/services/misc/ssm-agent.nix
+++ b/nixos/modules/services/misc/ssm-agent.nix
@@ -29,13 +29,15 @@ in {
 
   config = mkIf cfg.enable {
     systemd.services.ssm-agent = {
+      users.extraUsers.ssm-user = {};
+
       inherit (cfg.package.meta) description;
       after    = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
 
-      path = [ fake-lsb-release ];
+      path = [ fake-lsb-release pkgs.coreutils ];
       serviceConfig = {
-        ExecStart = "${cfg.package}/bin/agent";
+        ExecStart = "${cfg.package}/bin/amazon-ssm-agent";
         KillMode = "process";
         Restart = "on-failure";
         RestartSec = "15min";
diff --git a/nixos/modules/services/misc/sssd.nix b/nixos/modules/services/misc/sssd.nix
index 77f6ccfe64f..3da99a3b38c 100644
--- a/nixos/modules/services/misc/sssd.nix
+++ b/nixos/modules/services/misc/sssd.nix
@@ -42,11 +42,6 @@ in {
   };
   config = mkMerge [
     (mkIf cfg.enable {
-      assertions = singleton {
-        assertion = nscd.enable;
-        message = "nscd must be enabled through `services.nscd.enable` for SSSD to work.";
-      };
-
       systemd.services.sssd = {
         description = "System Security Services Daemon";
         wantedBy    = [ "multi-user.target" ];
@@ -74,11 +69,12 @@ in {
         mode = "0400";
       };
 
-      system.nssModules = optional cfg.enable pkgs.sssd;
+      system.nssModules = pkgs.sssd;
       system.nssDatabases = {
+        group = [ "sss" ];
         passwd = [ "sss" ];
-        shadow = [ "sss" ];
         services = [ "sss" ];
+        shadow = [ "sss" ];
       };
       services.dbus.packages = [ pkgs.sssd ];
     })
diff --git a/nixos/modules/services/misc/tzupdate.nix b/nixos/modules/services/misc/tzupdate.nix
index 570982ced29..eac1e1112a5 100644
--- a/nixos/modules/services/misc/tzupdate.nix
+++ b/nixos/modules/services/misc/tzupdate.nix
@@ -11,7 +11,7 @@ in {
       default = false;
       description = ''
         Enable the tzupdate timezone updating service. This provides
-        a one-shot service which can be activated with systemctl to 
+        a one-shot service which can be activated with systemctl to
         update the timezone.
       '';
     };
@@ -21,7 +21,7 @@ in {
     # We need to have imperative time zone management for this to work.
     # This will give users an error if they have set an explicit time
     # zone, which is better than silently overriding it.
-    time.timeZone = null; 
+    time.timeZone = null;
 
     # We provide a one-shot service which can be manually run. We could
     # provide a service that runs on startup, but it's tricky to get
diff --git a/nixos/modules/services/misc/zigbee2mqtt.nix b/nixos/modules/services/misc/zigbee2mqtt.nix
new file mode 100644
index 00000000000..0957920f1a0
--- /dev/null
+++ b/nixos/modules/services/misc/zigbee2mqtt.nix
@@ -0,0 +1,98 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.zigbee2mqtt;
+
+  configJSON = pkgs.writeText "configuration.json"
+    (builtins.toJSON (recursiveUpdate defaultConfig cfg.config));
+  configFile = pkgs.runCommand "configuration.yaml" { preferLocalBuild = true; } ''
+    ${pkgs.remarshal}/bin/json2yaml -i ${configJSON} -o $out
+  '';
+
+  # the default config contains all required settings,
+  # so the service starts up without crashing.
+  defaultConfig = {
+    homeassistant = false;
+    permit_join = false;
+    mqtt = {
+      base_topic = "zigbee2mqtt";
+      server = "mqtt://localhost:1883";
+    };
+    serial.port = "/dev/ttyACM0";
+    # put device configuration into separate file because configuration.yaml
+    # is copied from the store on startup
+    devices = "devices.yaml";
+  };
+in
+{
+  meta.maintainers = with maintainers; [ sweber ];
+
+  options.services.zigbee2mqtt = {
+    enable = mkEnableOption "enable zigbee2mqtt service";
+
+    package = mkOption {
+      description = "Zigbee2mqtt package to use";
+      default = pkgs.zigbee2mqtt.override {
+        dataDir = cfg.dataDir;
+      };
+      defaultText = "pkgs.zigbee2mqtt";
+      type = types.package;
+    };
+
+    dataDir = mkOption {
+      description = "Zigbee2mqtt data directory";
+      default = "/var/lib/zigbee2mqtt";
+      type = types.path;
+    };
+
+    config = mkOption {
+      default = {};
+      type = with types; nullOr attrs;
+      example = literalExample ''
+        {
+          homeassistant = config.services.home-assistant.enable;
+          permit_join = true;
+          serial = {
+            port = "/dev/ttyACM1";
+          };
+        }
+      '';
+      description = ''
+        Your <filename>configuration.yaml</filename> as a Nix attribute set.
+      '';
+    };
+  };
+
+  config = mkIf (cfg.enable) {
+    systemd.services.zigbee2mqtt = {
+      description = "Zigbee2mqtt Service";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/zigbee2mqtt";
+        User = "zigbee2mqtt";
+        WorkingDirectory = cfg.dataDir;
+        Restart = "on-failure";
+        ProtectSystem = "strict";
+        ReadWritePaths = cfg.dataDir;
+        PrivateTmp = true;
+        RemoveIPC = true;
+      };
+      preStart = ''
+        cp --no-preserve=mode ${configFile} "${cfg.dataDir}/configuration.yaml"
+      '';
+    };
+
+    users.users.zigbee2mqtt = {
+      home = cfg.dataDir;
+      createHome = true;
+      group = "zigbee2mqtt";
+      extraGroups = [ "dialout" ];
+      uid = config.ids.uids.zigbee2mqtt;
+    };
+
+    users.groups.zigbee2mqtt.gid = config.ids.gids.zigbee2mqtt;
+  };
+}
diff --git a/nixos/modules/services/misc/zoneminder.nix b/nixos/modules/services/misc/zoneminder.nix
index d5b3537068d..d9d34b7fac9 100644
--- a/nixos/modules/services/misc/zoneminder.nix
+++ b/nixos/modules/services/misc/zoneminder.nix
@@ -63,10 +63,6 @@ let
     ${cfg.extraConfig}
   '';
 
-  phpExtensions = with pkgs.phpPackages; [
-    { pkg = apcu; name = "apcu"; }
-  ];
-
 in {
   options = {
     services.zoneminder = with lib; {
@@ -289,11 +285,9 @@ in {
       phpfpm = lib.mkIf useNginx {
         pools.zoneminder = {
           inherit user group;
+          phpPackage = pkgs.php.withExtensions ({ enabled, all }: enabled ++ [ all.apcu ]);
           phpOptions = ''
             date.timezone = "${config.time.timeZone}"
-
-            ${lib.concatStringsSep "\n" (map (e:
-            "extension=${e.pkg}/lib/php/extensions/${e.name}.so") phpExtensions)}
           '';
           settings = lib.mapAttrs (name: lib.mkDefault) {
             "listen.owner" = user;
diff --git a/nixos/modules/services/monitoring/cadvisor.nix b/nixos/modules/services/monitoring/cadvisor.nix
index 655a6934a26..da051dbe465 100644
--- a/nixos/modules/services/monitoring/cadvisor.nix
+++ b/nixos/modules/services/monitoring/cadvisor.nix
@@ -90,7 +90,7 @@ in {
         default = [];
         description = ''
           Additional cadvisor options.
-          
+
           See <link xlink:href='https://github.com/google/cadvisor/blob/master/docs/runtime_options.md'/> for available options.
         '';
       };
diff --git a/nixos/modules/services/monitoring/dd-agent/dd-agent.nix b/nixos/modules/services/monitoring/dd-agent/dd-agent.nix
index e91717fb205..a290dae8d4b 100644
--- a/nixos/modules/services/monitoring/dd-agent/dd-agent.nix
+++ b/nixos/modules/services/monitoring/dd-agent/dd-agent.nix
@@ -97,11 +97,11 @@ let
         "dd-agent/conf.d/nginx.yaml".source = nginxConfig;
       }) //
       (optionalAttrs (cfg.mongoConfig != null)
-      { 
+      {
         "dd-agent/conf.d/mongo.yaml".source = mongoConfig;
       }) //
       (optionalAttrs (cfg.processConfig != null)
-      { 
+      {
         "dd-agent/conf.d/process.yaml".source = processConfig;
       }) //
       (optionalAttrs (cfg.jmxConfig != null)
diff --git a/nixos/modules/services/monitoring/do-agent.nix b/nixos/modules/services/monitoring/do-agent.nix
index 2d3fe2f7976..4dfb6236727 100644
--- a/nixos/modules/services/monitoring/do-agent.nix
+++ b/nixos/modules/services/monitoring/do-agent.nix
@@ -4,6 +4,7 @@ with lib;
 
 let
   cfg = config.services.do-agent;
+
 in
 {
   options.services.do-agent = {
@@ -11,23 +12,13 @@ in
   };
 
   config = mkIf cfg.enable {
-    environment.systemPackages = [ pkgs.do-agent ];
+    systemd.packages = [ pkgs.do-agent ];
 
     systemd.services.do-agent = {
-      description = "DigitalOcean Droplet Metrics Agent";
       wantedBy = [ "multi-user.target" ];
-      after = [ "network-online.target" ];
-      wants = [ "network-online.target" ];
       serviceConfig = {
-        ExecStart = "${pkgs.do-agent}/bin/do-agent --syslog";
-        Restart = "always";
-        OOMScoreAdjust = -900;
-        SyslogIdentifier = "DigitalOceanAgent";
-        PrivateTmp = "yes";
-        ProtectSystem = "full";
-        ProtectHome = "yes";
-        NoNewPrivileges = "yes";
-        DynamicUser = "yes";
+        ExecStart = [ "" "${pkgs.do-agent}/bin/do-agent --syslog" ];
+        DynamicUser = true;
       };
     };
   };
diff --git a/nixos/modules/services/monitoring/monit.nix b/nixos/modules/services/monitoring/monit.nix
index ca935227217..aa51b83912c 100644
--- a/nixos/modules/services/monitoring/monit.nix
+++ b/nixos/modules/services/monitoring/monit.nix
@@ -4,19 +4,29 @@ with lib;
 
 let
   cfg = config.services.monit;
+  extraConfig = pkgs.writeText "monitConfig" cfg.extraConfig;
 in
 
 {
+  imports = [
+    (mkRenamedOptionModule [ "services" "monit" "config" ] ["services" "monit" "extraConfig" ])
+  ];
+
   options.services.monit = {
 
     enable = mkEnableOption "Monit";
 
-    config = mkOption {
+    configFiles = mkOption {
+      type = types.listOf types.path;
+      default = [];
+      description = "List of paths to be included in the monitrc file";
+    };
+
+    extraConfig = mkOption {
       type = types.lines;
       default = "";
-      description = "monitrc content";
+      description = "Additional monit config as string";
     };
-
   };
 
   config = mkIf cfg.enable {
@@ -24,7 +34,7 @@ in
     environment.systemPackages = [ pkgs.monit ];
 
     environment.etc.monitrc = {
-      text = cfg.config;
+      text = concatMapStringsSep "\n" (path: "include ${path}")  (cfg.configFiles ++ [extraConfig]);
       mode = "0400";
     };
 
diff --git a/nixos/modules/services/monitoring/netdata.nix b/nixos/modules/services/monitoring/netdata.nix
index a5233a46e34..2e73e15d3a8 100644
--- a/nixos/modules/services/monitoring/netdata.nix
+++ b/nixos/modules/services/monitoring/netdata.nix
@@ -133,16 +133,6 @@ in {
         }
       ];
 
-    systemd.tmpfiles.rules = [
-      "d /var/cache/netdata 0755 ${cfg.user} ${cfg.group} -"
-      "Z /var/cache/netdata - ${cfg.user} ${cfg.group} -"
-      "d /var/log/netdata 0755 ${cfg.user} ${cfg.group} -"
-      "Z /var/log/netdata - ${cfg.user} ${cfg.group} -"
-      "d /var/lib/netdata 0755 ${cfg.user} ${cfg.group} -"
-      "Z /var/lib/netdata - ${cfg.user} ${cfg.group} -"
-      "d /etc/netdata 0755 ${cfg.user} ${cfg.group} -"
-      "Z /etc/netdata - ${cfg.user} ${cfg.group} -"
-    ];
     systemd.services.netdata = {
       description = "Real time performance monitoring";
       after = [ "network.target" ];
@@ -158,11 +148,40 @@ in {
         # User and group
         User = cfg.user;
         Group = cfg.group;
-        # Runtime directory and mode
-        RuntimeDirectory = "netdata";
-        RuntimeDirectoryMode = "0755";
         # Performance
         LimitNOFILE = "30000";
+        # Runtime directory and mode
+        RuntimeDirectory = "netdata";
+        RuntimeDirectoryMode = "0750";
+        # State directory and mode
+        StateDirectory = "netdata";
+        StateDirectoryMode = "0750";
+        # Cache directory and mode
+        CacheDirectory = "netdata";
+        CacheDirectoryMode = "0750";
+        # Logs directory and mode
+        LogsDirectory = "netdata";
+        LogsDirectoryMode = "0750";
+        # Configuration directory and mode
+        ConfigurationDirectory = "netdata";
+        ConfigurationDirectoryMode = "0755";
+        # Capabilities
+        CapabilityBoundingSet = [
+          "CAP_DAC_OVERRIDE"      # is required for freeipmi and slabinfo plugins
+          "CAP_DAC_READ_SEARCH"   # is required for apps plugin
+          "CAP_FOWNER"            # is required for freeipmi plugin
+          "CAP_SETPCAP"           # is required for apps, perf and slabinfo plugins
+          "CAP_SYS_ADMIN"         # is required for perf plugin
+          "CAP_SYS_PTRACE"        # is required for apps plugin
+          "CAP_SYS_RESOURCE"      # is required for ebpf plugin
+          "CAP_NET_RAW"           # is required for fping app
+        ];
+        # Sandboxing
+        ProtectSystem = "full";
+        ProtectHome = "read-only";
+        PrivateTmp = true;
+        ProtectControlGroups = true;
+        PrivateMounts = true;
       };
     };
 
diff --git a/nixos/modules/services/monitoring/prometheus/default.nix b/nixos/modules/services/monitoring/prometheus/default.nix
index 84a72afac2f..d7e06484b69 100644
--- a/nixos/modules/services/monitoring/prometheus/default.nix
+++ b/nixos/modules/services/monitoring/prometheus/default.nix
@@ -46,7 +46,7 @@ let
   cmdlineArgs = cfg.extraFlags ++ [
     "--storage.tsdb.path=${workingDir}/data/"
     "--config.file=${prometheusYml}"
-    "--web.listen-address=${cfg.listenAddress}"
+    "--web.listen-address=${cfg.listenAddress}:${builtins.toString cfg.port}"
     "--alertmanager.notification-queue-capacity=${toString cfg.alertmanagerNotificationQueueCapacity}"
     "--alertmanager.timeout=${toString cfg.alertmanagerTimeout}s"
   ] ++
@@ -489,9 +489,17 @@ in {
       '';
     };
 
+    port = mkOption {
+      type = types.port;
+      default = 9090;
+      description = ''
+        Port to listen on.
+      '';
+    };
+
     listenAddress = mkOption {
       type = types.str;
-      default = "0.0.0.0:9090";
+      default = "0.0.0.0";
       description = ''
         Address to listen on for the web interface, API, and telemetry.
       '';
@@ -619,6 +627,21 @@ in {
   };
 
   config = mkIf cfg.enable {
+    assertions = [
+      ( let
+          legacy = builtins.match "(.*):(.*)" cfg.listenAddress;
+        in {
+          assertion = legacy == null;
+          message = ''
+            Do not specify the port for Prometheus to listen on in the
+            listenAddress option; use the port option instead:
+              services.prometheus.listenAddress = ${builtins.elemAt legacy 0};
+              services.prometheus.port = ${builtins.elemAt legacy 1};
+          '';
+        }
+      )
+    ];
+
     users.groups.prometheus.gid = config.ids.gids.prometheus;
     users.users.prometheus = {
       description = "Prometheus daemon user";
diff --git a/nixos/modules/services/monitoring/prometheus/exporters.nix b/nixos/modules/services/monitoring/prometheus/exporters.nix
index f9ad1457fc8..59748efe0de 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters.nix
@@ -21,6 +21,7 @@ let
   #  `serviceOpts.script` or `serviceOpts.serviceConfig.ExecStart`
 
   exporterOpts = genAttrs [
+    "apcupsd"
     "bind"
     "blackbox"
     "collectd"
@@ -28,14 +29,18 @@ let
     "dovecot"
     "fritzbox"
     "json"
+    "keylight"
+    "lnd"
     "mail"
     "mikrotik"
     "minio"
+    "modemmanager"
     "nextcloud"
     "nginx"
     "node"
     "postfix"
     "postgres"
+    "redis"
     "rspamd"
     "snmp"
     "surfboard"
@@ -168,15 +173,6 @@ in
        (opt: lib.mkRemovedOptionModule [ "services" "prometheus" "${opt}" ] ''
          The prometheus exporters are now configured using `services.prometheus.exporters'.
          See the 18.03 release notes for more information.
-       '' ))
-
-    ++ (lib.forEach [ "enable" "substitutions" "preset" ]
-       (opt: lib.mkRemovedOptionModule [ "fonts" "fontconfig" "ultimate" "${opt}" ] ''
-         The fonts.fontconfig.ultimate module and configuration is obsolete.
-         The repository has since been archived and activity has ceased.
-         https://github.com/bohoomil/fontconfig-ultimate/issues/171.
-         No action should be needed for font configuration, as the fonts.fontconfig
-         module is already used by default.
        '' ));
 
   options.services.prometheus.exporters = mkOption {
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/apcupsd.nix b/nixos/modules/services/monitoring/prometheus/exporters/apcupsd.nix
new file mode 100644
index 00000000000..57c35a742c5
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/apcupsd.nix
@@ -0,0 +1,38 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.apcupsd;
+in
+{
+  port = 9162;
+  extraOpts = {
+    apcupsdAddress = mkOption {
+      type = types.str;
+      default = ":3551";
+      description = ''
+        Address of the apcupsd Network Information Server (NIS).
+      '';
+    };
+
+    apcupsdNetwork = mkOption {
+      type = types.enum ["tcp" "tcp4" "tcp6"];
+      default = "tcp";
+      description = ''
+        Network of the apcupsd Network Information Server (NIS): one of "tcp", "tcp4", or "tcp6".
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-apcupsd-exporter}/bin/apcupsd_exporter \
+          -telemetry.addr ${cfg.listenAddress}:${toString cfg.port} \
+          -apcupsd.addr ${cfg.apcupsdAddress} \
+          -apcupsd.network ${cfg.apcupsdNetwork} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/keylight.nix b/nixos/modules/services/monitoring/prometheus/exporters/keylight.nix
new file mode 100644
index 00000000000..dfa56343b87
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/keylight.nix
@@ -0,0 +1,19 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.keylight;
+in
+{
+  port = 9288;
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-keylight-exporter}/bin/keylight_exporter \
+          -metrics.addr ${cfg.listenAddress}:${toString cfg.port} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/lnd.nix b/nixos/modules/services/monitoring/prometheus/exporters/lnd.nix
new file mode 100644
index 00000000000..35f97202057
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/lnd.nix
@@ -0,0 +1,46 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.lnd;
+in
+{
+  port = 9092;
+  extraOpts = {
+    lndHost = mkOption {
+      type = types.str;
+      default = "localhost:10009";
+      description = ''
+        lnd instance gRPC address:port.
+      '';
+    };
+
+    lndTlsPath = mkOption {
+      type = types.path;
+      description = ''
+        Path to lnd TLS certificate.
+      '';
+    };
+
+    lndMacaroonDir = mkOption {
+      type = types.path;
+      description = ''
+        Path to lnd macaroons.
+      '';
+    };
+  };
+  serviceOpts.serviceConfig = {
+    ExecStart = ''
+      ${pkgs.prometheus-lnd-exporter}/bin/lndmon \
+        --prometheus.listenaddr=${cfg.listenAddress}:${toString cfg.port} \
+        --prometheus.logdir=/var/log/prometheus-lnd-exporter \
+        --lnd.host=${cfg.lndHost} \
+        --lnd.tlspath=${cfg.lndTlsPath} \
+        --lnd.macaroondir=${cfg.lndMacaroonDir} \
+        ${concatStringsSep " \\\n  " cfg.extraFlags}
+    '';
+    LogsDirectory = "prometheus-lnd-exporter";
+    ReadOnlyPaths = [ cfg.lndTlsPath cfg.lndMacaroonDir ];
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/modemmanager.nix b/nixos/modules/services/monitoring/prometheus/exporters/modemmanager.nix
new file mode 100644
index 00000000000..86ea98b94e4
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/modemmanager.nix
@@ -0,0 +1,33 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.modemmanager;
+in
+{
+  port = 9539;
+  extraOpts = {
+    refreshRate = mkOption {
+      type = types.str;
+      default = "5s";
+      description = ''
+        How frequently ModemManager will refresh the extended signal quality
+        information for each modem. The duration should be specified in seconds
+        ("5s"), minutes ("1m"), or hours ("1h").
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      # Required in order to authenticate with ModemManager via D-Bus.
+      SupplementaryGroups = "networkmanager";
+      ExecStart = ''
+        ${pkgs.prometheus-modemmanager-exporter}/bin/modemmanager_exporter \
+          -addr ${cfg.listenAddress}:${toString cfg.port} \
+          -rate ${cfg.refreshRate} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/redis.nix b/nixos/modules/services/monitoring/prometheus/exporters/redis.nix
new file mode 100644
index 00000000000..befbcb21f76
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/redis.nix
@@ -0,0 +1,19 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.redis;
+in
+{
+  port = 9121;
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-redis-exporter}/bin/redis_exporter \
+          -web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/smartd.nix b/nixos/modules/services/monitoring/smartd.nix
index c345ec48a01..c72b4abfcdc 100644
--- a/nixos/modules/services/monitoring/smartd.nix
+++ b/nixos/modules/services/monitoring/smartd.nix
@@ -18,9 +18,9 @@ let
     ${optionalString nm.enable ''
       {
       ${pkgs.coreutils}/bin/cat << EOF
-      From: smartd on ${host} <root>
+      From: smartd on ${host} <${nm.sender}>
       To: undisclosed-recipients:;
-      Subject: SMART error on $SMARTD_DEVICESTRING: $SMARTD_FAILTYPE
+      Subject: $SMARTD_SUBJECT
 
       $SMARTD_FULLMESSAGE
       EOF
@@ -129,6 +129,16 @@ in
             description = "Whenever to send e-mail notifications.";
           };
 
+          sender = mkOption {
+            default = "root";
+            example = "example@domain.tld";
+            type = types.str;
+            description = ''
+              Sender of the notification messages.
+              Acts as the value of <literal>email</literal> in the emails' <literal>From: ... </literal> field.
+            '';
+          };
+
           recipient = mkOption {
             default = "root";
             type = types.str;
@@ -229,11 +239,7 @@ in
 
     systemd.services.smartd = {
       description = "S.M.A.R.T. Daemon";
-
       wantedBy = [ "multi-user.target" ];
-
-      path = [ pkgs.nettools ]; # for hostname and dnsdomanname calls in smartd
-
       serviceConfig.ExecStart = "${pkgs.smartmontools}/sbin/smartd ${lib.concatStringsSep " " cfg.extraOptions} --no-fork --configfile=${smartdConf}";
     };
 
diff --git a/nixos/modules/services/monitoring/teamviewer.nix b/nixos/modules/services/monitoring/teamviewer.nix
index dd98ecab828..8d781d82d08 100644
--- a/nixos/modules/services/monitoring/teamviewer.nix
+++ b/nixos/modules/services/monitoring/teamviewer.nix
@@ -15,7 +15,7 @@ in
   options = {
 
     services.teamviewer.enable = mkEnableOption "TeamViewer daemon";
-      
+
   };
 
   ###### implementation
diff --git a/nixos/modules/services/monitoring/tuptime.nix b/nixos/modules/services/monitoring/tuptime.nix
index 731260a5c20..8f79d916599 100644
--- a/nixos/modules/services/monitoring/tuptime.nix
+++ b/nixos/modules/services/monitoring/tuptime.nix
@@ -32,7 +32,10 @@ in {
 
     environment.systemPackages = [ pkgs.tuptime ];
 
-    users.users.tuptime.description = "tuptime database owner";
+    users = {
+      groups._tuptime.members = [ "_tuptime" ];
+      users._tuptime.description = "tuptime database owner";
+    };
 
     systemd = {
       services = {
@@ -45,7 +48,7 @@ in {
           serviceConfig = {
             StateDirectory = "tuptime";
             Type = "oneshot";
-            User = "tuptime";
+            User = "_tuptime";
             RemainAfterExit = true;
             ExecStart = "${pkgs.tuptime}/bin/tuptime -x";
             ExecStop = "${pkgs.tuptime}/bin/tuptime -xg";
@@ -57,7 +60,7 @@ in {
           serviceConfig = {
             StateDirectory = "tuptime";
             Type = "oneshot";
-            User = "tuptime";
+            User = "_tuptime";
             ExecStart = "${pkgs.tuptime}/bin/tuptime -x";
           };
         };
diff --git a/nixos/modules/services/monitoring/zabbix-agent.nix b/nixos/modules/services/monitoring/zabbix-agent.nix
index b3383ed628b..73eed7aa66a 100644
--- a/nixos/modules/services/monitoring/zabbix-agent.nix
+++ b/nixos/modules/services/monitoring/zabbix-agent.nix
@@ -3,8 +3,9 @@
 let
   cfg = config.services.zabbixAgent;
 
-  inherit (lib) mkDefault mkEnableOption mkIf mkOption;
+  inherit (lib) mkDefault mkEnableOption mkIf mkMerge mkOption;
   inherit (lib) attrValues concatMapStringsSep literalExample optionalString types;
+  inherit (lib.generators) toKeyValue;
 
   user = "zabbix-agent";
   group = "zabbix-agent";
@@ -14,19 +15,15 @@ let
     paths = attrValues cfg.modules;
   };
 
-  configFile = pkgs.writeText "zabbix_agent.conf" ''
-    LogType = console
-    Server = ${cfg.server}
-    ListenIP = ${cfg.listen.ip}
-    ListenPort = ${toString cfg.listen.port}
-    ${optionalString (cfg.modules != {}) "LoadModulePath = ${moduleEnv}/lib"}
-    ${concatMapStringsSep "\n" (name: "LoadModule = ${name}") (builtins.attrNames cfg.modules)}
-    ${cfg.extraConfig}
-  '';
+  configFile = pkgs.writeText "zabbix_agent.conf" (toKeyValue { listsAsDuplicateKeys = true; } cfg.settings);
 
 in
 
 {
+  imports = [
+    (lib.mkRemovedOptionModule [ "services" "zabbixAgent" "extraConfig" ] "Use services.zabbixAgent.settings instead.")
+  ];
+
   # interface
 
   options = {
@@ -105,15 +102,18 @@ in
         '';
       };
 
-      # TODO: for bonus points migrate this to https://github.com/NixOS/rfcs/pull/42
-      extraConfig = mkOption {
-        default = "";
-        type = types.lines;
+      settings = mkOption {
+        type = with types; attrsOf (oneOf [ int str (listOf str) ]);
+        default = {};
         description = ''
-          Configuration that is injected verbatim into the configuration file. Refer to
+          Zabbix Agent configuration. Refer to
           <link xlink:href="https://www.zabbix.com/documentation/current/manual/appendix/config/zabbix_agentd"/>
           for details on supported values.
         '';
+        example = {
+          Hostname = "example.org";
+          DebugLevel = 4;
+        };
       };
 
     };
@@ -124,6 +124,17 @@ in
 
   config = mkIf cfg.enable {
 
+    services.zabbixAgent.settings = mkMerge [
+      {
+        LogType = "console";
+        Server = cfg.server;
+        ListenIP = cfg.listen.ip;
+        ListenPort = cfg.listen.port;
+        LoadModule = builtins.attrNames cfg.modules;
+      }
+      (mkIf (cfg.modules != {}) { LoadModulePath = "${moduleEnv}/lib"; })
+    ];
+
     networking.firewall = mkIf cfg.openFirewall {
       allowedTCPPorts = [ cfg.listen.port ];
     };
diff --git a/nixos/modules/services/monitoring/zabbix-proxy.nix b/nixos/modules/services/monitoring/zabbix-proxy.nix
index 9d214469c3b..2c8b8b92cb3 100644
--- a/nixos/modules/services/monitoring/zabbix-proxy.nix
+++ b/nixos/modules/services/monitoring/zabbix-proxy.nix
@@ -5,8 +5,9 @@ let
   pgsql = config.services.postgresql;
   mysql = config.services.mysql;
 
-  inherit (lib) mkDefault mkEnableOption mkIf mkOption;
-  inherit (lib) attrValues concatMapStringsSep literalExample optional optionalAttrs optionalString types;
+  inherit (lib) mkAfter mkDefault mkEnableOption mkIf mkMerge mkOption;
+  inherit (lib) attrValues concatMapStringsSep getName literalExample optional optionalAttrs optionalString types;
+  inherit (lib.generators) toKeyValue;
 
   user = "zabbix";
   group = "zabbix";
@@ -19,24 +20,7 @@ let
     paths = attrValues cfg.modules;
   };
 
-  configFile = pkgs.writeText "zabbix_proxy.conf" ''
-    LogType = console
-    ListenIP = ${cfg.listen.ip}
-    ListenPort = ${toString cfg.listen.port}
-    Server = ${cfg.server}
-    # TODO: set to cfg.database.socket if database type is pgsql?
-    DBHost = ${optionalString (cfg.database.createLocally != true) cfg.database.host}
-    ${optionalString (cfg.database.createLocally != true) "DBPort = ${cfg.database.port}"}
-    DBName = ${cfg.database.name}
-    DBUser = ${cfg.database.user}
-    ${optionalString (cfg.database.passwordFile != null) "Include ${passwordFile}"}
-    ${optionalString (mysqlLocal && cfg.database.socket != null) "DBSocket = ${cfg.database.socket}"}
-    SocketDir = ${runtimeDir}
-    FpingLocation = /run/wrappers/bin/fping
-    ${optionalString (cfg.modules != {}) "LoadModulePath = ${moduleEnv}/lib"}
-    ${concatMapStringsSep "\n" (name: "LoadModule = ${name}") (builtins.attrNames cfg.modules)}
-    ${cfg.extraConfig}
-  '';
+  configFile = pkgs.writeText "zabbix_proxy.conf" (toKeyValue { listsAsDuplicateKeys = true; } cfg.settings);
 
   mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql";
   pgsqlLocal = cfg.database.createLocally && cfg.database.type == "pgsql";
@@ -44,6 +28,10 @@ let
 in
 
 {
+  imports = [
+    (lib.mkRemovedOptionModule [ "services" "zabbixProxy" "extraConfig" ] "Use services.zabbixProxy.settings instead.")
+  ];
+
   # interface
 
   options = {
@@ -182,15 +170,19 @@ in
         '';
       };
 
-      # TODO: for bonus points migrate this to https://github.com/NixOS/rfcs/pull/42
-      extraConfig = mkOption {
-        default = "";
-        type = types.lines;
+      settings = mkOption {
+        type = with types; attrsOf (oneOf [ int str (listOf str) ]);
+        default = {};
         description = ''
-          Configuration that is injected verbatim into the configuration file. Refer to
+          Zabbix Proxy configuration. Refer to
           <link xlink:href="https://www.zabbix.com/documentation/current/manual/appendix/config/zabbix_proxy"/>
           for details on supported values.
         '';
+        example = {
+          CacheSize = "1G";
+          SSHKeyLocation = "/var/lib/zabbix/.ssh";
+          StartPingers = 32;
+        };
       };
 
     };
@@ -213,6 +205,26 @@ in
       }
     ];
 
+    services.zabbixProxy.settings = mkMerge [
+      {
+        LogType = "console";
+        ListenIP = cfg.listen.ip;
+        ListenPort = cfg.listen.port;
+        Server = cfg.server;
+        # TODO: set to cfg.database.socket if database type is pgsql?
+        DBHost = optionalString (cfg.database.createLocally != true) cfg.database.host;
+        DBName = cfg.database.name;
+        DBUser = cfg.database.user;
+        SocketDir = runtimeDir;
+        FpingLocation = "/run/wrappers/bin/fping";
+        LoadModule = builtins.attrNames cfg.modules;
+      }
+      (mkIf (cfg.database.createLocally != true) { DBPort = cfg.database.port; })
+      (mkIf (cfg.database.passwordFile != null) { Include = [ "${passwordFile}" ]; })
+      (mkIf (mysqlLocal && cfg.database.socket != null) { DBSocket = cfg.database.socket; })
+      (mkIf (cfg.modules != {}) { LoadModulePath = "${moduleEnv}/lib"; })
+    ];
+
     networking.firewall = mkIf cfg.openFirewall {
       allowedTCPPorts = [ cfg.listen.port ];
     };
@@ -220,14 +232,15 @@ in
     services.mysql = optionalAttrs mysqlLocal {
       enable = true;
       package = mkDefault pkgs.mariadb;
-      ensureDatabases = [ cfg.database.name ];
-      ensureUsers = [
-        { name = cfg.database.user;
-          ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
-        }
-      ];
     };
 
+    systemd.services.mysql.postStart = mkAfter (optionalString mysqlLocal ''
+      ( echo "CREATE DATABASE IF NOT EXISTS \`${cfg.database.name}\` CHARACTER SET utf8 COLLATE utf8_bin;"
+        echo "CREATE USER IF NOT EXISTS '${cfg.database.user}'@'localhost' IDENTIFIED WITH ${if (getName config.services.mysql.package == getName pkgs.mariadb) then "unix_socket" else "auth_socket"};"
+        echo "GRANT ALL PRIVILEGES ON \`${cfg.database.name}\`.* TO '${cfg.database.user}'@'localhost';"
+      ) | ${config.services.mysql.package}/bin/mysql -N
+    '');
+
     services.postgresql = optionalAttrs pgsqlLocal {
       enable = true;
       ensureDatabases = [ cfg.database.name ];
diff --git a/nixos/modules/services/monitoring/zabbix-server.nix b/nixos/modules/services/monitoring/zabbix-server.nix
index b4e4378ce1e..c8658634ecb 100644
--- a/nixos/modules/services/monitoring/zabbix-server.nix
+++ b/nixos/modules/services/monitoring/zabbix-server.nix
@@ -5,8 +5,9 @@ let
   pgsql = config.services.postgresql;
   mysql = config.services.mysql;
 
-  inherit (lib) mkDefault mkEnableOption mkIf mkOption;
-  inherit (lib) attrValues concatMapStringsSep literalExample optional optionalAttrs optionalString types;
+  inherit (lib) mkAfter mkDefault mkEnableOption mkIf mkMerge mkOption;
+  inherit (lib) attrValues concatMapStringsSep getName literalExample optional optionalAttrs optionalString types;
+  inherit (lib.generators) toKeyValue;
 
   user = "zabbix";
   group = "zabbix";
@@ -19,24 +20,7 @@ let
     paths = attrValues cfg.modules;
   };
 
-  configFile = pkgs.writeText "zabbix_server.conf" ''
-    LogType = console
-    ListenIP = ${cfg.listen.ip}
-    ListenPort = ${toString cfg.listen.port}
-    # TODO: set to cfg.database.socket if database type is pgsql?
-    DBHost = ${optionalString (cfg.database.createLocally != true) cfg.database.host}
-    ${optionalString (cfg.database.createLocally != true) "DBPort = ${cfg.database.port}"}
-    DBName = ${cfg.database.name}
-    DBUser = ${cfg.database.user}
-    ${optionalString (cfg.database.passwordFile != null) "Include ${passwordFile}"}
-    ${optionalString (mysqlLocal && cfg.database.socket != null) "DBSocket = ${cfg.database.socket}"}
-    PidFile = ${runtimeDir}/zabbix_server.pid
-    SocketDir = ${runtimeDir}
-    FpingLocation = /run/wrappers/bin/fping
-    ${optionalString (cfg.modules != {}) "LoadModulePath = ${moduleEnv}/lib"}
-    ${concatMapStringsSep "\n" (name: "LoadModule = ${name}") (builtins.attrNames cfg.modules)}
-    ${cfg.extraConfig}
-  '';
+  configFile = pkgs.writeText "zabbix_server.conf" (toKeyValue { listsAsDuplicateKeys = true; } cfg.settings);
 
   mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql";
   pgsqlLocal = cfg.database.createLocally && cfg.database.type == "pgsql";
@@ -47,6 +31,7 @@ in
   imports = [
     (lib.mkRenamedOptionModule [ "services" "zabbixServer" "dbServer" ] [ "services" "zabbixServer" "database" "host" ])
     (lib.mkRemovedOptionModule [ "services" "zabbixServer" "dbPassword" ] "Use services.zabbixServer.database.passwordFile instead.")
+    (lib.mkRemovedOptionModule [ "services" "zabbixServer" "extraConfig" ] "Use services.zabbixServer.settings instead.")
   ];
 
   # interface
@@ -176,15 +161,19 @@ in
         '';
       };
 
-      # TODO: for bonus points migrate this to https://github.com/NixOS/rfcs/pull/42
-      extraConfig = mkOption {
-        default = "";
-        type = types.lines;
+      settings = mkOption {
+        type = with types; attrsOf (oneOf [ int str (listOf str) ]);
+        default = {};
         description = ''
-          Configuration that is injected verbatim into the configuration file. Refer to
+          Zabbix Server configuration. Refer to
           <link xlink:href="https://www.zabbix.com/documentation/current/manual/appendix/config/zabbix_server"/>
           for details on supported values.
         '';
+        example = {
+          CacheSize = "1G";
+          SSHKeyLocation = "/var/lib/zabbix/.ssh";
+          StartPingers = 32;
+        };
       };
 
     };
@@ -204,6 +193,26 @@ in
       }
     ];
 
+    services.zabbixServer.settings = mkMerge [
+      {
+        LogType = "console";
+        ListenIP = cfg.listen.ip;
+        ListenPort = cfg.listen.port;
+        # TODO: set to cfg.database.socket if database type is pgsql?
+        DBHost = optionalString (cfg.database.createLocally != true) cfg.database.host;
+        DBName = cfg.database.name;
+        DBUser = cfg.database.user;
+        PidFile = "${runtimeDir}/zabbix_server.pid";
+        SocketDir = runtimeDir;
+        FpingLocation = "/run/wrappers/bin/fping";
+        LoadModule = builtins.attrNames cfg.modules;
+      }
+      (mkIf (cfg.database.createLocally != true) { DBPort = cfg.database.port; })
+      (mkIf (cfg.database.passwordFile != null) { Include = [ "${passwordFile}" ]; })
+      (mkIf (mysqlLocal && cfg.database.socket != null) { DBSocket = cfg.database.socket; })
+      (mkIf (cfg.modules != {}) { LoadModulePath = "${moduleEnv}/lib"; })
+    ];
+
     networking.firewall = mkIf cfg.openFirewall {
       allowedTCPPorts = [ cfg.listen.port ];
     };
@@ -211,14 +220,15 @@ in
     services.mysql = optionalAttrs mysqlLocal {
       enable = true;
       package = mkDefault pkgs.mariadb;
-      ensureDatabases = [ cfg.database.name ];
-      ensureUsers = [
-        { name = cfg.database.user;
-          ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
-        }
-      ];
     };
 
+    systemd.services.mysql.postStart = mkAfter (optionalString mysqlLocal ''
+      ( echo "CREATE DATABASE IF NOT EXISTS \`${cfg.database.name}\` CHARACTER SET utf8 COLLATE utf8_bin;"
+        echo "CREATE USER IF NOT EXISTS '${cfg.database.user}'@'localhost' IDENTIFIED WITH ${if (getName config.services.mysql.package == getName pkgs.mariadb) then "unix_socket" else "auth_socket"};"
+        echo "GRANT ALL PRIVILEGES ON \`${cfg.database.name}\`.* TO '${cfg.database.user}'@'localhost';"
+      ) | ${config.services.mysql.package}/bin/mysql -N
+    '');
+
     services.postgresql = optionalAttrs pgsqlLocal {
       enable = true;
       ensureDatabases = [ cfg.database.name ];
diff --git a/nixos/modules/services/network-filesystems/ipfs.nix b/nixos/modules/services/network-filesystems/ipfs.nix
index 880f70ae141..f298f831fa7 100644
--- a/nixos/modules/services/network-filesystems/ipfs.nix
+++ b/nixos/modules/services/network-filesystems/ipfs.nix
@@ -1,69 +1,39 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, pkgs, options, ... }:
 with lib;
 let
-  inherit (pkgs) ipfs runCommand makeWrapper;
-
   cfg = config.services.ipfs;
+  opt = options.services.ipfs;
 
   ipfsFlags = toString ([
     (optionalString  cfg.autoMount                   "--mount")
-    #(optionalString  cfg.autoMigrate                 "--migrate")
     (optionalString  cfg.enableGC                    "--enable-gc")
     (optionalString (cfg.serviceFdlimit != null)     "--manage-fdlimit=false")
     (optionalString (cfg.defaultMode == "offline")   "--offline")
     (optionalString (cfg.defaultMode == "norouting") "--routing=none")
   ] ++ cfg.extraFlags);
 
-  defaultDataDir = if versionAtLeast config.system.stateVersion "17.09" then
-    "/var/lib/ipfs" else
-    "/var/lib/ipfs/.ipfs";
-
-  # Wrapping the ipfs binary with the environment variable IPFS_PATH set to dataDir because we can't set it in the user environment
-  wrapped = runCommand "ipfs" { buildInputs = [ makeWrapper ]; preferLocalBuild = true; } ''
-    mkdir -p "$out/bin"
-    makeWrapper "${ipfs}/bin/ipfs" "$out/bin/ipfs" \
-      --set IPFS_PATH ${cfg.dataDir} \
-      --prefix PATH : /run/wrappers/bin
-  '';
+  splitMulitaddr = addrRaw: lib.tail (lib.splitString "/" addrRaw);
+
+  multiaddrToListenStream = addrRaw: let
+      addr = splitMulitaddr addrRaw;
+      s = builtins.elemAt addr;
+    in if s 0 == "ip4" && s 2 == "tcp"
+      then "${s 1}:${s 3}"
+    else if s 0 == "ip6" && s 2 == "tcp"
+      then "[${s 1}]:${s 3}"
+    else if s 0 == "unix"
+      then "/${lib.concatStringsSep "/" (lib.tail addr)}"
+    else null; # not valid for listen stream, skip
+
+  multiaddrToListenDatagram = addrRaw: let
+      addr = splitMulitaddr addrRaw;
+      s = builtins.elemAt addr;
+    in if s 0 == "ip4" && s 2 == "udp"
+      then "${s 1}:${s 3}"
+    else if s 0 == "ip6" && s 2 == "udp"
+      then "[${s 1}]:${s 3}"
+    else null; # not valid for listen datagram, skip
 
-
-  commonEnv = {
-    environment.IPFS_PATH = cfg.dataDir;
-    path = [ wrapped ];
-    serviceConfig.User = cfg.user;
-    serviceConfig.Group = cfg.group;
-  };
-
-  baseService = recursiveUpdate commonEnv {
-    wants = [ "ipfs-init.service" ];
-    # NB: migration must be performed prior to pre-start, else we get the failure message!
-    preStart = optionalString cfg.autoMount ''
-      ipfs --local config Mounts.FuseAllowOther --json true
-      ipfs --local config Mounts.IPFS ${cfg.ipfsMountDir}
-      ipfs --local config Mounts.IPNS ${cfg.ipnsMountDir}
-    '' + concatStringsSep "\n" (collect
-          isString
-          (mapAttrsRecursive
-            (path: value:
-            # Using heredoc below so that the value is never improperly quoted
-            ''
-              read value <<EOF
-              ${builtins.toJSON value}
-              EOF
-              ipfs --local config --json "${concatStringsSep "." path}" "$value"
-            '')
-            ({ Addresses.API = cfg.apiAddress;
-               Addresses.Gateway = cfg.gatewayAddress;
-               Addresses.Swarm = cfg.swarmAddress;
-            } //
-            cfg.extraConfig))
-        );
-    serviceConfig = {
-      ExecStart = "${wrapped}/bin/ipfs daemon ${ipfsFlags}";
-      Restart = "on-failure";
-      RestartSec = 1;
-    } // optionalAttrs (cfg.serviceFdlimit != null) { LimitNOFILE = cfg.serviceFdlimit; };
-  };
 in {
 
   ###### interface
@@ -88,7 +58,9 @@ in {
 
       dataDir = mkOption {
         type = types.str;
-        default = defaultDataDir;
+        default = if versionAtLeast config.system.stateVersion "17.09"
+                  then "/var/lib/ipfs"
+                  else "/var/lib/ipfs/.ipfs";
         description = "The data dir for IPFS";
       };
 
@@ -98,18 +70,6 @@ in {
         description = "systemd service that is enabled by default";
       };
 
-      /*
-      autoMigrate = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Whether IPFS should try to migrate the file system automatically.
-
-          The daemon will need to be able to download a binary from https://ipfs.io to perform the migration.
-        '';
-      };
-      */
-
       autoMount = mkOption {
         type = types.bool;
         default = false;
@@ -142,7 +102,12 @@ in {
 
       swarmAddress = mkOption {
         type = types.listOf types.str;
-        default = [ "/ip4/0.0.0.0/tcp/4001" "/ip6/::/tcp/4001" ];
+        default = [
+          "/ip4/0.0.0.0/tcp/4001"
+          "/ip6/::/tcp/4001"
+          "/ip4/0.0.0.0/udp/4001/quic"
+          "/ip6/::/udp/4001/quic"
+        ];
         description = "Where IPFS listens for incoming p2p connections";
       };
 
@@ -199,13 +164,21 @@ in {
         example = 64*1024;
       };
 
+      startWhenNeeded = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to use socket activation to start IPFS when needed.";
+      };
+
     };
   };
 
   ###### implementation
 
   config = mkIf cfg.enable {
-    environment.systemPackages = [ wrapped ];
+    environment.systemPackages = [ pkgs.ipfs ];
+    environment.variables.IPFS_PATH = cfg.dataDir;
+
     programs.fuse = mkIf cfg.autoMount {
       userAllowOther = true;
     };
@@ -217,6 +190,9 @@ in {
         createHome = false;
         uid = config.ids.uids.ipfs;
         description = "IPFS daemon user";
+        packages = [
+          pkgs.ipfs-migrator
+        ];
       };
     };
 
@@ -231,10 +207,14 @@ in {
       "d '${cfg.ipnsMountDir}' - ${cfg.user} ${cfg.group} - -"
     ];
 
-    systemd.services.ipfs-init = recursiveUpdate commonEnv {
+    systemd.packages = [ pkgs.ipfs ];
+
+    systemd.services.ipfs-init = {
       description = "IPFS Initializer";
 
-      before = [ "ipfs.service" "ipfs-offline.service" "ipfs-norouting.service" ];
+      environment.IPFS_PATH = cfg.dataDir;
+
+      path = [ pkgs.ipfs ];
 
       script = ''
         if [[ ! -f ${cfg.dataDir}/config ]]; then
@@ -248,34 +228,72 @@ in {
         fi
       '';
 
+      wantedBy = [ "default.target" ];
+
       serviceConfig = {
         Type = "oneshot";
         RemainAfterExit = true;
+        User = cfg.user;
+        Group = cfg.group;
       };
     };
 
-    # TODO These 3 definitions possibly be further abstracted through use of a function
-    # like: mutexServices "ipfs" [ "", "offline", "norouting" ] { ... shared conf here ... }
+    systemd.services.ipfs = {
+      path = [ "/run/wrappers" pkgs.ipfs ];
+      environment.IPFS_PATH = cfg.dataDir;
 
-    systemd.services.ipfs = recursiveUpdate baseService {
-      description = "IPFS Daemon";
-      wantedBy = mkIf (cfg.defaultMode == "online") [ "multi-user.target" ];
-      after = [ "network.target" "ipfs-init.service" ];
-      conflicts = [ "ipfs-offline.service" "ipfs-norouting.service"];
+      wants = [ "ipfs-init.service" ];
+      after = [ "ipfs-init.service" ];
+
+      preStart = optionalString cfg.autoMount ''
+        ipfs --local config Mounts.FuseAllowOther --json true
+        ipfs --local config Mounts.IPFS ${cfg.ipfsMountDir}
+        ipfs --local config Mounts.IPNS ${cfg.ipnsMountDir}
+      '' + concatStringsSep "\n" (collect
+            isString
+            (mapAttrsRecursive
+              (path: value:
+              # Using heredoc below so that the value is never improperly quoted
+              ''
+                read value <<EOF
+                ${builtins.toJSON value}
+                EOF
+                ipfs --local config --json "${concatStringsSep "." path}" "$value"
+              '')
+              ({ Addresses.API = cfg.apiAddress;
+                 Addresses.Gateway = cfg.gatewayAddress;
+                 Addresses.Swarm = cfg.swarmAddress;
+              } //
+              cfg.extraConfig))
+          );
+      serviceConfig = {
+        ExecStart = ["" "${pkgs.ipfs}/bin/ipfs daemon ${ipfsFlags}"];
+        User = cfg.user;
+        Group = cfg.group;
+      } // optionalAttrs (cfg.serviceFdlimit != null) { LimitNOFILE = cfg.serviceFdlimit; };
+    } // optionalAttrs (!cfg.startWhenNeeded) {
+      wantedBy = [ "default.target" ];
     };
 
-    systemd.services.ipfs-offline = recursiveUpdate baseService {
-      description = "IPFS Daemon (offline mode)";
-      wantedBy = mkIf (cfg.defaultMode == "offline") [ "multi-user.target" ];
-      after = [ "ipfs-init.service" ];
-      conflicts = [ "ipfs.service" "ipfs-norouting.service"];
+    systemd.sockets.ipfs-gateway = {
+      wantedBy = [ "sockets.target" ];
+      socketConfig = {
+        ListenStream = let
+            fromCfg = multiaddrToListenStream cfg.gatewayAddress;
+          in [ "" ] ++ lib.optional (fromCfg != null) fromCfg;
+        ListenDatagram = let
+            fromCfg = multiaddrToListenDatagram cfg.gatewayAddress;
+          in [ "" ] ++ lib.optional (fromCfg != null) fromCfg;
+      };
     };
 
-    systemd.services.ipfs-norouting = recursiveUpdate baseService {
-      description = "IPFS Daemon (no routing mode)";
-      wantedBy = mkIf (cfg.defaultMode == "norouting") [ "multi-user.target" ];
-      after = [ "ipfs-init.service" ];
-      conflicts = [ "ipfs.service" "ipfs-offline.service"];
+    systemd.sockets.ipfs-api = {
+      wantedBy = [ "sockets.target" ];
+      # We also include "%t/ipfs.sock" because tere is no way to put the "%t"
+      # in the multiaddr.
+      socketConfig.ListenStream = let
+          fromCfg = multiaddrToListenStream cfg.apiAddress;
+        in [ "" "%t/ipfs.sock" ] ++ lib.optional (fromCfg != null) fromCfg;
     };
 
   };
diff --git a/nixos/modules/services/network-filesystems/samba.nix b/nixos/modules/services/network-filesystems/samba.nix
index a115590ccaa..08c912e0fcd 100644
--- a/nixos/modules/services/network-filesystems/samba.nix
+++ b/nixos/modules/services/network-filesystems/samba.nix
@@ -224,6 +224,7 @@ in
       (mkIf cfg.enable {
 
         system.nssModules = optional cfg.nsswins samba;
+        system.nssDatabases.hosts = optional cfg.nsswins "wins";
 
         systemd = {
           targets.samba = {
diff --git a/nixos/modules/services/networking/3proxy.nix b/nixos/modules/services/networking/3proxy.nix
index ae8a4958ca9..37a48657c1c 100644
--- a/nixos/modules/services/networking/3proxy.nix
+++ b/nixos/modules/services/networking/3proxy.nix
@@ -124,7 +124,7 @@ in {
                   <literal>"iponly"</literal>: specifies no authentication. ACLs authorization is used.
                 </para></listitem>
                 <listitem><para>
-                  <literal>"strong"</literal>: authentication by username/password. If user is not registered his access is denied regardless of ACLs.
+                  <literal>"strong"</literal>: authentication by username/password. If user is not registered their access is denied regardless of ACLs.
                 </para></listitem>
               </itemizedlist>
 
diff --git a/nixos/modules/services/networking/avahi-daemon.nix b/nixos/modules/services/networking/avahi-daemon.nix
index ddcfe3d77e2..c876b252e8c 100644
--- a/nixos/modules/services/networking/avahi-daemon.nix
+++ b/nixos/modules/services/networking/avahi-daemon.nix
@@ -238,6 +238,10 @@ in
     users.groups.avahi = {};
 
     system.nssModules = optional cfg.nssmdns pkgs.nssmdns;
+    system.nssDatabases.hosts = optionals cfg.nssmdns (mkMerge [
+      [ "mdns_minimal [NOTFOUND=return]" ]
+      (mkOrder 1501 [ "mdns" ]) # 1501 to ensure it's after dns
+    ]);
 
     environment.systemPackages = [ pkgs.avahi ];
 
diff --git a/nixos/modules/services/networking/bitcoind.nix b/nixos/modules/services/networking/bitcoind.nix
index 4e00a886547..bc9aa53f49a 100644
--- a/nixos/modules/services/networking/bitcoind.nix
+++ b/nixos/modules/services/networking/bitcoind.nix
@@ -3,31 +3,8 @@
 with lib;
 
 let
-  cfg = config.services.bitcoind;
-  pidFile = "${cfg.dataDir}/bitcoind.pid";
-  configFile = pkgs.writeText "bitcoin.conf" ''
-    ${optionalString cfg.testnet "testnet=1"}
-    ${optionalString (cfg.dbCache != null) "dbcache=${toString cfg.dbCache}"}
-    ${optionalString (cfg.prune != null) "prune=${toString cfg.prune}"}
-
-    # Connection options
-    ${optionalString (cfg.port != null) "port=${toString cfg.port}"}
-
-    # RPC server options
-    ${optionalString (cfg.rpc.port != null) "rpcport=${toString cfg.rpc.port}"}
-    ${concatMapStringsSep  "\n"
-      (rpcUser: "rpcauth=${rpcUser.name}:${rpcUser.passwordHMAC}")
-      (attrValues cfg.rpc.users)
-    }
 
-    # Extra config options (from bitcoind nixos service)
-    ${cfg.extraConfig}
-  '';
-  cmdlineOptions = escapeShellArgs [
-    "-conf=${cfg.configFile}"
-    "-datadir=${cfg.dataDir}"
-    "-pid=${pidFile}"
-  ];
+  eachBitcoind = config.services.bitcoind;
 
   rpcUserOpts = { name, ... }: {
     options = {
@@ -39,11 +16,14 @@ let
         '';
       };
       passwordHMAC = mkOption {
-        type = with types; uniq (strMatching "[0-9a-f]+\\$[0-9a-f]{64}");
+        type = types.uniq (types.strMatching "[0-9a-f]+\\$[0-9a-f]{64}");
         example = "f7efda5c189b999524f151318c0c86$d5b51b3beffbc02b724e5d095828e0bc8b2456e9ac8757ae3211a5d9b16a22ae";
         description = ''
           Password HMAC-SHA-256 for JSON-RPC connections. Must be a string of the
           format &lt;SALT-HEX&gt;$&lt;HMAC-HEX&gt;.
+
+          Tool (Python script) for HMAC generation is available here:
+          <link xlink:href="https://github.com/bitcoin/bitcoin/blob/master/share/rpcauth/rpcauth.py"/>
         '';
       };
     };
@@ -51,10 +31,10 @@ let
       name = mkDefault name;
     };
   };
-in {
-  options = {
 
-    services.bitcoind = {
+  bitcoindOpts = { config, lib, name, ...}: {
+    options = {
+
       enable = mkEnableOption "Bitcoin daemon";
 
       package = mkOption {
@@ -63,12 +43,14 @@ in {
         defaultText = "pkgs.bitcoind";
         description = "The package providing bitcoin binaries.";
       };
+
       configFile = mkOption {
-        type = types.path;
-        default = configFile;
-        example = "/etc/bitcoind.conf";
+        type = types.nullOr types.path;
+        default = null;
+        example = "/var/lib/${name}/bitcoin.conf";
         description = "The configuration file path to supply bitcoind.";
       };
+
       extraConfig = mkOption {
         type = types.lines;
         default = "";
@@ -79,20 +61,22 @@ in {
         '';
         description = "Additional configurations to be appended to <filename>bitcoin.conf</filename>.";
       };
+
       dataDir = mkOption {
         type = types.path;
-        default = "/var/lib/bitcoind";
+        default = "/var/lib/bitcoind-${name}";
         description = "The data directory for bitcoind.";
       };
 
       user = mkOption {
         type = types.str;
-        default = "bitcoin";
+        default = "bitcoind-${name}";
         description = "The user as which to run bitcoind.";
       };
+
       group = mkOption {
         type = types.str;
-        default = cfg.user;
+        default = config.user;
         description = "The group as which to run bitcoind.";
       };
 
@@ -110,29 +94,36 @@ in {
               bob.passwordHMAC = "b2dd077cb54591a2f3139e69a897ac$4e71f08d48b4347cf8eff3815c0e25ae2e9a4340474079f55705f40574f4ec99";
             }
           '';
-          type = with types; loaOf (submodule rpcUserOpts);
-          description = ''
-            RPC user information for JSON-RPC connnections.
-          '';
+          type = types.attrsOf (types.submodule rpcUserOpts);
+          description = "RPC user information for JSON-RPC connnections.";
         };
       };
 
+      pidFile = mkOption {
+        type = types.path;
+        default = "${config.dataDir}/bitcoind.pid";
+        description = "Location of bitcoind pid file.";
+      };
+
       testnet = mkOption {
         type = types.bool;
         default = false;
-        description = "Whether to use the test chain.";
+        description = "Whether to use the testnet instead of mainnet.";
       };
+
       port = mkOption {
         type = types.nullOr types.port;
         default = null;
         description = "Override the default port on which to listen for connections.";
       };
+
       dbCache = mkOption {
         type = types.nullOr (types.ints.between 4 16384);
         default = null;
         example = 4000;
-        description = "Override the default database cache size in megabytes.";
+        description = "Override the default database cache size in MiB.";
       };
+
       prune = mkOption {
         type = types.nullOr (types.coercedTo
           (types.enum [ "disable" "manual" ])
@@ -149,45 +140,122 @@ in {
           and -rescan. Warning: Reverting this setting requires re-downloading
           the entire blockchain. ("disable" = disable pruning blocks, "manual"
           = allow manual pruning via RPC, >=550 = automatically prune block files
-          to stay under the specified target size in MiB)
+          to stay under the specified target size in MiB).
+        '';
+      };
+
+      extraCmdlineOptions = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          Extra command line options to pass to bitcoind.
+          Run bitcoind --help to list all available options.
         '';
       };
     };
   };
+in
+{
 
-  config = mkIf cfg.enable {
-    environment.systemPackages = [ cfg.package ];
-    systemd.tmpfiles.rules = [
-      "d '${cfg.dataDir}' 0770 '${cfg.user}' '${cfg.group}' - -"
-      "L '${cfg.dataDir}/bitcoin.conf' - - - - '${cfg.configFile}'"
-    ];
-    systemd.services.bitcoind = {
-      description = "Bitcoin daemon";
-      after = [ "network.target" ];
-      wantedBy = [ "multi-user.target" ];
-      serviceConfig = {
-        User = cfg.user;
-        Group = cfg.group;
-        ExecStart = "${cfg.package}/bin/bitcoind ${cmdlineOptions}";
-        Restart = "on-failure";
-
-        # Hardening measures
-        PrivateTmp = "true";
-        ProtectSystem = "full";
-        NoNewPrivileges = "true";
-        PrivateDevices = "true";
-        MemoryDenyWriteExecute = "true";
-      };
+  options = {
+    services.bitcoind = mkOption {
+      type = types.attrsOf (types.submodule bitcoindOpts);
+      default = {};
+      description = "Specification of one or more bitcoind instances.";
     };
-    users.users.${cfg.user} = {
+  };
+
+  config = mkIf (eachBitcoind != {}) {
+
+    assertions = flatten (mapAttrsToList (bitcoindName: cfg: [
+    {
+      assertion = (cfg.prune != null) -> (builtins.elem cfg.prune [ "disable" "manual" 0 1 ] || (builtins.isInt cfg.prune && cfg.prune >= 550));
+      message = ''
+        If set, services.bitcoind.${bitcoindName}.prune has to be "disable", "manual", 0 , 1 or >= 550.
+      '';
+    }
+    {
+      assertion = (cfg.rpc.users != {}) -> (cfg.configFile == null);
+      message = ''
+        You cannot set both services.bitcoind.${bitcoindName}.rpc.users and services.bitcoind.${bitcoindName}.configFile
+        as they are exclusive. RPC user setting would have no effect if custom configFile would be used.
+      '';
+    }
+    ]) eachBitcoind);
+
+    environment.systemPackages = flatten (mapAttrsToList (bitcoindName: cfg: [
+      cfg.package
+    ]) eachBitcoind);
+
+    systemd.services = mapAttrs' (bitcoindName: cfg: (
+      nameValuePair "bitcoind-${bitcoindName}" (
+      let
+        configFile = pkgs.writeText "bitcoin.conf" ''
+          # If Testnet is enabled, we need to add [test] section
+          # otherwise, some options (e.g.: custom RPC port) will not work
+          ${optionalString cfg.testnet "[test]"}
+          # RPC users
+          ${concatMapStringsSep  "\n"
+            (rpcUser: "rpcauth=${rpcUser.name}:${rpcUser.passwordHMAC}")
+            (attrValues cfg.rpc.users)
+          }
+          # Extra config options (from bitcoind nixos service)
+          ${cfg.extraConfig}
+        '';
+      in {
+        description = "Bitcoin daemon";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          User = cfg.user;
+          Group = cfg.group;
+          ExecStart = ''
+            ${cfg.package}/bin/bitcoind \
+            ${if (cfg.configFile != null) then
+              "-conf=${cfg.configFile}"
+            else
+              "-conf=${configFile}"
+            } \
+            -datadir=${cfg.dataDir} \
+            -pid=${cfg.pidFile} \
+            ${optionalString cfg.testnet "-testnet"}\
+            ${optionalString (cfg.port != null) "-port=${toString cfg.port}"}\
+            ${optionalString (cfg.prune != null) "-prune=${toString cfg.prune}"}\
+            ${optionalString (cfg.dbCache != null) "-dbcache=${toString cfg.dbCache}"}\
+            ${optionalString (cfg.rpc.port != null) "-rpcport=${toString cfg.rpc.port}"}\
+            ${toString cfg.extraCmdlineOptions}
+          '';
+          Restart = "on-failure";
+
+          # Hardening measures
+          PrivateTmp = "true";
+          ProtectSystem = "full";
+          NoNewPrivileges = "true";
+          PrivateDevices = "true";
+          MemoryDenyWriteExecute = "true";
+        };
+      }
+    ))) eachBitcoind;
+
+    systemd.tmpfiles.rules = flatten (mapAttrsToList (bitcoindName: cfg: [
+      "d '${cfg.dataDir}' 0770 '${cfg.user}' '${cfg.group}' - -"
+    ]) eachBitcoind);
+
+    users.users = mapAttrs' (bitcoindName: cfg: (
+      nameValuePair "bitcoind-${bitcoindName}" {
       name = cfg.user;
       group = cfg.group;
       description = "Bitcoin daemon user";
       home = cfg.dataDir;
       isSystemUser = true;
-    };
-    users.groups.${cfg.group} = {
-      name = cfg.group;
-    };
+    })) eachBitcoind;
+
+    users.groups = mapAttrs' (bitcoindName: cfg: (
+      nameValuePair "${cfg.group}" { }
+    )) eachBitcoind;
+
   };
+
+  meta.maintainers = with maintainers; [ _1000101 ];
+
 }
diff --git a/nixos/modules/services/networking/blockbook-frontend.nix b/nixos/modules/services/networking/blockbook-frontend.nix
new file mode 100644
index 00000000000..dde24522756
--- /dev/null
+++ b/nixos/modules/services/networking/blockbook-frontend.nix
@@ -0,0 +1,275 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  eachBlockbook = config.services.blockbook-frontend;
+
+  blockbookOpts = { config, lib, name, ...}: {
+
+    options = {
+
+      enable = mkEnableOption "blockbook-frontend application.";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.blockbook;
+        description = "Which blockbook package to use.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "blockbook-frontend-${name}";
+        description = "The user as which to run blockbook-frontend-${name}.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "${config.user}";
+        description = "The group as which to run blockbook-frontend-${name}.";
+      };
+
+      certFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/etc/secrets/blockbook-frontend-${name}/certFile";
+        description = ''
+          To enable SSL, specify path to the name of certificate files without extension.
+          Expecting <filename>certFile.crt</filename> and <filename>certFile.key</filename>.
+        '';
+      };
+
+      configFile = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        example = "${config.dataDir}/config.json";
+        description = "Location of the blockbook configuration file.";
+      };
+
+      coinName = mkOption {
+        type = types.str;
+        default = "Bitcoin";
+        example = "Bitcoin";
+        description = ''
+          See <link xlink:href="https://github.com/trezor/blockbook/blob/master/bchain/coins/blockchain.go#L61"/>
+          for current of coins supported in master (Note: may differ from release).
+        '';
+      };
+
+      cssDir = mkOption {
+        type = types.path;
+        default = "${config.package}/share/css/";
+        example = "${config.dataDir}/static/css/";
+        description = ''
+          Location of the dir with <filename>main.css</filename> CSS file.
+          By default, the one shipped with the package is used.
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/blockbook-frontend-${name}";
+        description = "Location of blockbook-frontend-${name} data directory.";
+      };
+
+      debug = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Debug mode, return more verbose errors, reload templates on each request.";
+      };
+
+      internal = mkOption {
+        type = types.nullOr types.str;
+        default = ":9030";
+        example = ":9030";
+        description = "Internal http server binding <literal>[address]:port</literal>.";
+      };
+
+      messageQueueBinding = mkOption {
+        type = types.str;
+        default = "tcp://127.0.0.1:38330";
+        example = "tcp://127.0.0.1:38330";
+        description = "Message Queue Binding <literal>address:port</literal>.";
+      };
+
+      public = mkOption {
+        type = types.nullOr types.str;
+        default = ":9130";
+        example = ":9130";
+        description = "Public http server binding <literal>[address]:port</literal>.";
+      };
+
+      rpc = {
+        url = mkOption {
+          type = types.str;
+          default = "http://127.0.0.1";
+          description = "URL for JSON-RPC connections.";
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = 8030;
+          description = "Port for JSON-RPC connections.";
+        };
+
+        user = mkOption {
+          type = types.str;
+          default = "rpc";
+          example = "rpc";
+          description = "Username for JSON-RPC connections.";
+        };
+
+        password = mkOption {
+          type = types.str;
+          default = "rpc";
+          example = "rpc";
+          description = ''
+            RPC password for JSON-RPC connections.
+            Warning: this is stored in cleartext in the Nix store!!!
+            Use <literal>configFile</literal> or <literal>passwordFile</literal> if needed.
+          '';
+        };
+
+        passwordFile = mkOption {
+          type = types.nullOr types.path;
+          default = null;
+          description = ''
+            File containing password of the RPC user.
+            Note: This options is ignored when <literal>configFile</literal> is used.
+          '';
+        };
+      };
+
+      sync = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Synchronizes until tip, if together with zeromq, keeps index synchronized.";
+      };
+
+      templateDir = mkOption {
+        type = types.path;
+        default = "${config.package}/share/templates/";
+        example = "${config.dataDir}/templates/static/";
+        description = "Location of the HTML templates. By default, ones shipped with the package are used.";
+      };
+
+      extraConfig = mkOption {
+        type = types.attrs;
+        default = {};
+        example = literalExample '' {
+          alternative_estimate_fee = "whatthefee-disabled";
+          alternative_estimate_fee_params = "{\"url\": \"https://whatthefee.io/data.json\", \"periodSeconds\": 60}";
+          fiat_rates = "coingecko";
+          fiat_rates_params = "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"bitcoin\", \"periodSeconds\": 60}";
+          coin_shortcut = "BTC";
+          coin_label = "Bitcoin";
+          xpub_magic = 76067358;
+          xpub_magic_segwit_p2sh = 77429938;
+          xpub_magic_segwit_native = 78792518;
+        }'';
+        description = ''
+          Additional configurations to be appended to <filename>coin.conf</filename>.
+          Overrides any already defined configuration options.
+          See <link xlink:href="https://github.com/trezor/blockbook/tree/master/configs/coins"/>
+          for current configuration options supported in master (Note: may differ from release).
+        '';
+      };
+
+      extraCmdLineOptions = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "-workers=1" "-dbcache=0" "-logtosderr" ];
+        description = ''
+          Extra command line options to pass to Blockbook.
+          Run blockbook --help to list all available options.
+        '';
+      };
+    };
+  };
+in
+{
+  # interface
+
+  options = {
+    services.blockbook-frontend = mkOption {
+      type = types.attrsOf (types.submodule blockbookOpts);
+      default = {};
+      description = "Specification of one or more blockbook-frontend instances.";
+    };
+  };
+
+  # implementation
+
+  config = mkIf (eachBlockbook != {}) {
+
+    systemd.services = mapAttrs' (blockbookName: cfg: (
+      nameValuePair "blockbook-frontend-${blockbookName}" (
+        let
+          configFile = if cfg.configFile != null then cfg.configFile else
+            pkgs.writeText "config.conf" (builtins.toJSON ( {
+                coin_name = "${cfg.coinName}";
+                rpc_user = "${cfg.rpc.user}";
+                rpc_pass = "${cfg.rpc.password}";
+                rpc_url = "${cfg.rpc.url}:${toString cfg.rpc.port}";
+                message_queue_binding = "${cfg.messageQueueBinding}";
+              } // cfg.extraConfig)
+            );
+        in {
+          description = "blockbook-frontend-${blockbookName} daemon";
+          after = [ "network.target" ];
+          wantedBy = [ "multi-user.target" ];
+          preStart = ''
+            ln -sf ${cfg.templateDir} ${cfg.dataDir}/static/
+            ln -sf ${cfg.cssDir} ${cfg.dataDir}/static/
+            ${optionalString (cfg.rpc.passwordFile != null && cfg.configFile == null) ''
+              CONFIGTMP=$(mktemp)
+              ${pkgs.jq}/bin/jq ".rpc_pass = \"$(cat ${cfg.rpc.passwordFile})\"" ${configFile} > $CONFIGTMP
+              mv $CONFIGTMP ${cfg.dataDir}/${blockbookName}-config.json
+            ''}
+          '';
+          serviceConfig = {
+            User = cfg.user;
+            Group = cfg.group;
+            ExecStart = ''
+               ${cfg.package}/bin/blockbook \
+               ${if (cfg.rpc.passwordFile != null && cfg.configFile == null) then
+               "-blockchaincfg=${cfg.dataDir}/${blockbookName}-config.json"
+               else
+               "-blockchaincfg=${configFile}"
+               } \
+               -datadir=${cfg.dataDir} \
+               ${optionalString (cfg.sync != false) "-sync"} \
+               ${optionalString (cfg.certFile != null) "-certfile=${toString cfg.certFile}"} \
+               ${optionalString (cfg.debug != false) "-debug"} \
+               ${optionalString (cfg.internal != null) "-internal=${toString cfg.internal}"} \
+               ${optionalString (cfg.public != null) "-public=${toString cfg.public}"} \
+               ${toString cfg.extraCmdLineOptions}
+            '';
+            Restart = "on-failure";
+            WorkingDirectory = cfg.dataDir;
+            LimitNOFILE = 65536;
+          };
+        }
+    ) )) eachBlockbook;
+
+    systemd.tmpfiles.rules = flatten (mapAttrsToList (blockbookName: cfg: [
+      "d ${cfg.dataDir} 0750 ${cfg.user} ${cfg.group} - -"
+      "d ${cfg.dataDir}/static 0750 ${cfg.user} ${cfg.group} - -"
+    ]) eachBlockbook);
+
+    users.users = mapAttrs' (blockbookName: cfg: (
+      nameValuePair "blockbook-frontend-${blockbookName}" {
+      name = cfg.user;
+      group = cfg.group;
+      home = cfg.dataDir;
+      isSystemUser = true;
+    })) eachBlockbook;
+
+    users.groups = mapAttrs' (instanceName: cfg: (
+      nameValuePair "${cfg.group}" { })) eachBlockbook;
+  };
+
+  meta.maintainers = with maintainers; [ _1000101 ];
+
+}
diff --git a/nixos/modules/services/networking/corerad.nix b/nixos/modules/services/networking/corerad.nix
index 1a2c4aec665..d90a5923bc6 100644
--- a/nixos/modules/services/networking/corerad.nix
+++ b/nixos/modules/services/networking/corerad.nix
@@ -4,14 +4,50 @@ with lib;
 
 let
   cfg = config.services.corerad;
+
+  writeTOML = name: x:
+    pkgs.runCommandNoCCLocal name {
+      passAsFile = ["config"];
+      config = builtins.toJSON x;
+      buildInputs = [ pkgs.go-toml ];
+    } "jsontoml < $configPath > $out";
+
 in {
-  meta = {
-    maintainers = with maintainers; [ mdlayher ];
-  };
+  meta.maintainers = with maintainers; [ mdlayher ];
 
   options.services.corerad = {
     enable = mkEnableOption "CoreRAD IPv6 NDP RA daemon";
 
+    settings = mkOption {
+      type = types.uniq types.attrs;
+      example = literalExample ''
+        {
+          interfaces = [
+            # eth0 is an upstream interface monitoring for IPv6 router advertisements.
+            {
+              name = "eth0";
+              monitor = true;
+            }
+            # eth1 is a downstream interface advertising IPv6 prefixes for SLAAC.
+            {
+              name = "eth1";
+              advertise = true;
+              prefix = [{ prefix = "::/64"; }];
+            }
+          ];
+          # Optionally enable Prometheus metrics.
+          debug = {
+            address = "localhost:9430";
+            prometheus = true;
+          };
+        }
+      '';
+      description = ''
+        Configuration for CoreRAD, see <link xlink:href="https://github.com/mdlayher/corerad/blob/master/internal/config/default.toml"/>
+        for supported values. Ignored if configFile is set.
+      '';
+    };
+
     configFile = mkOption {
       type = types.path;
       example = literalExample "\"\${pkgs.corerad}/etc/corerad/corerad.toml\"";
@@ -27,6 +63,9 @@ in {
   };
 
   config = mkIf cfg.enable {
+    # Prefer the config file over settings if both are set.
+    services.corerad.configFile = mkDefault (writeTOML "corerad.toml" cfg.settings);
+
     systemd.services.corerad = {
       description = "CoreRAD IPv6 NDP RA daemon";
       after = [ "network.target" ];
@@ -38,8 +77,11 @@ in {
         AmbientCapabilities = "CAP_NET_ADMIN CAP_NET_RAW";
         NoNewPrivileges = true;
         DynamicUser = true;
+        Type = "notify";
+        NotifyAccess = "main";
         ExecStart = "${getBin cfg.package}/bin/corerad -c=${cfg.configFile}";
         Restart = "on-failure";
+        RestartKillSignal = "SIGHUP";
       };
     };
   };
diff --git a/nixos/modules/services/networking/dhcpcd.nix b/nixos/modules/services/networking/dhcpcd.nix
index c0619211c2f..0507b739d49 100644
--- a/nixos/modules/services/networking/dhcpcd.nix
+++ b/nixos/modules/services/networking/dhcpcd.nix
@@ -81,7 +81,7 @@ let
           # anything ever again ("couldn't resolve ..., giving up on
           # it"), so we silently lose time synchronisation. This also
           # applies to openntpd.
-          ${config.systemd.package}/bin/systemctl try-reload-or-restart ntpd.service openntpd.service chronyd.service || true
+          /run/current-system/systemd/bin/systemctl try-reload-or-restart ntpd.service openntpd.service chronyd.service || true
       fi
 
       ${cfg.runHook}
@@ -217,7 +217,7 @@ in
     powerManagement.resumeCommands = mkIf config.systemd.services.dhcpcd.enable
       ''
         # Tell dhcpcd to rebind its interfaces if it's running.
-        ${config.systemd.package}/bin/systemctl reload dhcpcd.service
+        /run/current-system/systemd/bin/systemctl reload dhcpcd.service
       '';
 
   };
diff --git a/nixos/modules/services/networking/dhcpd.nix b/nixos/modules/services/networking/dhcpd.nix
index 67f7d811887..8966deac76c 100644
--- a/nixos/modules/services/networking/dhcpd.nix
+++ b/nixos/modules/services/networking/dhcpd.nix
@@ -11,7 +11,7 @@ let
     ''
       default-lease-time 600;
       max-lease-time 7200;
-      authoritative;
+      ${optionalString (!cfg.authoritative) "not "}authoritative;
       ddns-update-style interim;
       log-facility local1; # see dhcpd.nix
 
@@ -176,6 +176,16 @@ let
       '';
     };
 
+    authoritative = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether the DHCP server shall send DHCPNAK messages to misconfigured
+        clients. If this is not done, clients may be unable to get a correct
+        IP address after changing subnets until their old lease has expired.
+      '';
+    };
+
   };
 
 in
diff --git a/nixos/modules/services/networking/dnschain.nix b/nixos/modules/services/networking/dnschain.nix
deleted file mode 100644
index 003609ea705..00000000000
--- a/nixos/modules/services/networking/dnschain.nix
+++ /dev/null
@@ -1,184 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-  cfgs = config.services;
-  cfg  = cfgs.dnschain;
-
-  dataDir  = "/var/lib/dnschain";
-  username = "dnschain";
-
-  configFile = pkgs.writeText "dnschain.conf" ''
-    [log]
-    level = info
-
-    [dns]
-    host = ${cfg.dns.address}
-    port = ${toString cfg.dns.port}
-    oldDNSMethod = NO_OLD_DNS
-    externalIP = ${cfg.dns.externalAddress}
-
-    [http]
-    host = ${cfg.api.hostname}
-    port = ${toString cfg.api.port}
-    tlsPort = ${toString cfg.api.tlsPort}
-
-    ${cfg.extraConfig}
-  '';
-
-in
-
-{
-
-  ###### interface
-
-  options = {
-
-    services.dnschain = {
-
-      enable = mkEnableOption ''
-        DNSChain, a blockchain based DNS + HTTP server.
-        To resolve .bit domains set <literal>services.namecoind.enable = true;</literal>
-        and an RPC username/password.
-      '';
-
-      dns.address = mkOption {
-        type = types.str;
-        default = "127.0.0.1";
-        description = ''
-          The IP address the DNSChain resolver will bind to.
-          Leave this unchanged if you do not wish to directly expose the resolver.
-        '';
-      };
-
-      dns.externalAddress = mkOption {
-        type = types.str;
-        default = cfg.dns.address;
-        description = ''
-           The IP address used by clients to reach the resolver and the value of
-           the <literal>namecoin.dns</literal> record. Set this in case the bind address
-           is not the actual IP address (e.g. the machine is behind a NAT).
-        '';
-      };
-
-      dns.port = mkOption {
-        type = types.int;
-        default = 5333;
-        description = ''
-          The port the DNSChain resolver will bind to.
-        '';
-      };
-
-      api.hostname = mkOption {
-        type = types.str;
-        default = "0.0.0.0";
-        description = ''
-          The hostname (or IP address) the DNSChain API server will bind to.
-        '';
-      };
-
-      api.port = mkOption {
-        type = types.int;
-        default = 8080;
-        description = ''
-          The port the DNSChain API server (HTTP) will bind to.
-        '';
-      };
-
-      api.tlsPort = mkOption {
-        type = types.int;
-        default = 4433;
-        description = ''
-          The port the DNSChain API server (HTTPS) will bind to.
-        '';
-      };
-
-      extraConfig = mkOption {
-        type = types.lines;
-        default = "";
-        example = ''
-          [log]
-          level = debug
-        '';
-        description = ''
-          Additional options that will be appended to the configuration file.
-        '';
-      };
-
-    };
-
-    services.dnsmasq.resolveDNSChainQueries = mkOption {
-      type = types.bool;
-      default = false;
-      description = ''
-        Resolve <literal>.bit</literal> top-level domains using DNSChain and namecoin.
-      '';
-    };
-
-    services.pdns-recursor.resolveDNSChainQueries = mkOption {
-      type = types.bool;
-      default = false;
-      description = ''
-        Resolve <literal>.bit</literal> top-level domains using DNSChain and namecoin.
-      '';
-    };
-
-  };
-
-
-  ###### implementation
-
-  config = mkIf cfg.enable {
-
-    services.dnsmasq.servers = optionals cfgs.dnsmasq.resolveDNSChainQueries
-      [ "/.bit/127.0.0.1#${toString cfg.dns.port}"
-        "/.dns/127.0.0.1#${toString cfg.dns.port}"
-      ];
-
-    services.pdns-recursor = mkIf cfgs.pdns-recursor.resolveDNSChainQueries {
-      forwardZonesRecurse =
-        { bit = "127.0.0.1:${toString cfg.dns.port}";
-          dns = "127.0.0.1:${toString cfg.dns.port}";
-        };
-      luaConfig =''
-        addNTA("bit", "namecoin doesn't support DNSSEC")
-        addNTA("dns", "namecoin doesn't support DNSSEC")
-      '';
-    };
-
-    users.users.${username} = {
-      description = "DNSChain daemon user";
-      home = dataDir;
-      createHome = true;
-      uid = config.ids.uids.dnschain;
-      extraGroups = optional cfgs.namecoind.enable "namecoin";
-    };
-
-    systemd.services.dnschain = {
-      description = "DNSChain daemon";
-      after    = optional cfgs.namecoind.enable "namecoind.target";
-      wantedBy = [ "multi-user.target" ];
-
-      serviceConfig = {
-        User = "dnschain";
-        Restart = "on-failure";
-        ExecStart = "${pkgs.nodePackages.dnschain}/bin/dnschain";
-      };
-
-      preStart = ''
-        # Link configuration file into dnschain home directory
-        configPath=${dataDir}/.dnschain/dnschain.conf
-        mkdir -p ${dataDir}/.dnschain
-        if [ "$(realpath $configPath)" != "${configFile}" ]; then
-          rm -f $configPath
-          ln -s ${configFile} $configPath
-        fi
-      '';
-    };
-
-  };
-
-  meta.maintainers = with lib.maintainers; [ rnhmjoj ];
-
-}
diff --git a/nixos/modules/services/networking/dnscrypt-proxy2.nix b/nixos/modules/services/networking/dnscrypt-proxy2.nix
index e48eb729103..28691e83827 100644
--- a/nixos/modules/services/networking/dnscrypt-proxy2.nix
+++ b/nixos/modules/services/networking/dnscrypt-proxy2.nix
@@ -55,6 +55,7 @@ in
         AmbientCapabilities = "CAP_NET_BIND_SERVICE";
         DynamicUser = true;
         ExecStart = "${pkgs.dnscrypt-proxy2}/bin/dnscrypt-proxy -config ${cfg.configFile}";
+        Restart = "always";
       };
     };
   };
diff --git a/nixos/modules/services/networking/dnscrypt-wrapper.nix b/nixos/modules/services/networking/dnscrypt-wrapper.nix
index e53fb7a1578..b9333cd19a2 100644
--- a/nixos/modules/services/networking/dnscrypt-wrapper.nix
+++ b/nixos/modules/services/networking/dnscrypt-wrapper.nix
@@ -5,12 +5,20 @@ let
   cfg     = config.services.dnscrypt-wrapper;
   dataDir = "/var/lib/dnscrypt-wrapper";
 
+  mkPath = path: default:
+    if path != null
+      then toString path
+      else default;
+
+  publicKey = mkPath cfg.providerKey.public "${dataDir}/public.key";
+  secretKey = mkPath cfg.providerKey.secret "${dataDir}/secret.key";
+
   daemonArgs = with cfg; [
     "--listen-address=${address}:${toString port}"
     "--resolver-address=${upstream.address}:${toString upstream.port}"
     "--provider-name=${providerName}"
-    "--provider-publickey-file=public.key"
-    "--provider-secretkey-file=secret.key"
+    "--provider-publickey-file=${publicKey}"
+    "--provider-secretkey-file=${secretKey}"
     "--provider-cert-file=${providerName}.crt"
     "--crypt-secretkey-file=${providerName}.key"
   ];
@@ -24,17 +32,19 @@ let
       dnscrypt-wrapper --gen-cert-file \
         --crypt-secretkey-file=${cfg.providerName}.key \
         --provider-cert-file=${cfg.providerName}.crt \
-        --provider-publickey-file=public.key \
-        --provider-secretkey-file=secret.key \
+        --provider-publickey-file=${publicKey} \
+        --provider-secretkey-file=${secretKey} \
         --cert-file-expire-days=${toString cfg.keys.expiration}
     }
 
     cd ${dataDir}
 
     # generate provider keypair (first run only)
-    if [ ! -f public.key ] || [ ! -f secret.key ]; then
-      dnscrypt-wrapper --gen-provider-keypair
-    fi
+    ${optionalString (cfg.providerKey.public == null || cfg.providerKey.secret == null) ''
+      if [ ! -f ${publicKey} ] || [ ! -f ${secretKey} ]; then
+        dnscrypt-wrapper --gen-provider-keypair
+      fi
+    ''}
 
     # generate new keys for rotation
     if [ ! -f ${cfg.providerName}.key ] || [ ! -f ${cfg.providerName}.crt ]; then
@@ -64,6 +74,47 @@ let
     fi
   '';
 
+
+  # This is the fork of the original dnscrypt-proxy maintained by Dyne.org.
+  # dnscrypt-proxy2 doesn't provide the `--test` feature that is needed to
+  # correctly implement key rotation of dnscrypt-wrapper ephemeral keys.
+  dnscrypt-proxy1 = pkgs.callPackage
+    ({ stdenv, fetchFromGitHub, autoreconfHook
+    , pkgconfig, libsodium, ldns, openssl, systemd }:
+
+    stdenv.mkDerivation rec {
+      pname = "dnscrypt-proxy";
+      version = "2019-08-20";
+
+      src = fetchFromGitHub {
+        owner = "dyne";
+        repo = "dnscrypt-proxy";
+        rev = "07ac3825b5069adc28e2547c16b1d983a8ed8d80";
+        sha256 = "0c4mq741q4rpmdn09agwmxap32kf0vgfz7pkhcdc5h54chc3g3xy";
+      };
+
+      configureFlags = optional stdenv.isLinux "--with-systemd";
+
+      nativeBuildInputs = [ autoreconfHook pkgconfig ];
+
+      # <ldns/ldns.h> depends on <openssl/ssl.h>
+      buildInputs = [ libsodium openssl.dev ldns ] ++ optional stdenv.isLinux systemd;
+
+      postInstall = ''
+        # Previous versions required libtool files to load plugins; they are
+        # now strictly optional.
+        rm $out/lib/dnscrypt-proxy/*.la
+      '';
+
+      meta = {
+        description = "A tool for securing communications between a client and a DNS resolver";
+        homepage = "https://github.com/dyne/dnscrypt-proxy";
+        license = licenses.isc;
+        maintainers = with maintainers; [ rnhmjoj ];
+        platforms = platforms.linux;
+      };
+    }) { };
+
 in {
 
 
@@ -98,6 +149,26 @@ in {
       '';
     };
 
+    providerKey.public = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = "/etc/secrets/public.key";
+      description = ''
+        The filepath to the provider public key. If not given a new
+        provider key pair will be generated on the first run.
+      '';
+    };
+
+    providerKey.secret = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = "/etc/secrets/secret.key";
+      description = ''
+        The filepath to the provider secret key. If not given a new
+        provider key pair will be generated on the first run.
+      '';
+    };
+
     upstream.address = mkOption {
       type = types.str;
       default = "127.0.0.1";
@@ -179,7 +250,7 @@ in {
       requires = [ "dnscrypt-wrapper.service" ];
       description = "Rotates DNSCrypt wrapper keys if soon to expire";
 
-      path   = with pkgs; [ dnscrypt-wrapper dnscrypt-proxy gawk ];
+      path   = with pkgs; [ dnscrypt-wrapper dnscrypt-proxy1 gawk ];
       script = rotateKeys;
       serviceConfig.User = "dnscrypt-wrapper";
     };
@@ -196,6 +267,13 @@ in {
       };
     };
 
+    assertions = with cfg; [
+      { assertion = (providerKey.public == null && providerKey.secret == null) ||
+                    (providerKey.secret != null && providerKey.public != null);
+        message = "The secret and public provider key must be set together.";
+      }
+    ];
+
   };
 
   meta.maintainers = with lib.maintainers; [ rnhmjoj ];
diff --git a/nixos/modules/services/networking/ergo.nix b/nixos/modules/services/networking/ergo.nix
new file mode 100644
index 00000000000..c52de30dc36
--- /dev/null
+++ b/nixos/modules/services/networking/ergo.nix
@@ -0,0 +1,141 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.ergo;
+
+  inherit (lib) mkEnableOption mkIf mkOption optionalString types;
+
+  configFile = pkgs.writeText "ergo.conf" (''
+ergo {
+  directory = "${cfg.dataDir}"
+  node {
+    mining = false
+  }
+  wallet.secretStorage.secretDir = "${cfg.dataDir}/wallet/keystore"
+}
+
+scorex {
+  network {
+    bindAddress = "${cfg.listen.ip}:${toString cfg.listen.port}"
+  }
+'' + optionalString (cfg.api.keyHash != null) ''
+ restApi {
+    apiKeyHash = "${cfg.api.keyHash}"
+    bindAddress = "${cfg.api.listen.ip}:${toString cfg.api.listen.port}"
+ }
+'' + ''
+}
+'');
+
+in {
+
+  options = {
+
+    services.ergo = {
+      enable = mkEnableOption "Ergo service";
+
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/ergo";
+        description = "The data directory for the Ergo node.";
+      };
+
+      listen = {
+        ip = mkOption {
+          type = types.str;
+          default = "0.0.0.0";
+          description = "IP address on which the Ergo node should listen.";
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = 9006;
+          description = "Listen port for the Ergo node.";
+        };
+      };
+
+      api = {
+       keyHash = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "324dcf027dd4a30a932c441f365a25e86b173defa4b8e58948253471b81b72cf";
+        description = "Hex-encoded Blake2b256 hash of an API key as a 64-chars long Base16 string.";
+       };
+
+       listen = {
+        ip = mkOption {
+          type = types.str;
+          default = "0.0.0.0";
+          description = "IP address that the Ergo node API should listen on if <option>api.keyHash</option> is defined.";
+          };
+
+        port = mkOption {
+          type = types.port;
+          default = 9052;
+          description = "Listen port for the API endpoint if <option>api.keyHash</option> is defined.";
+        };
+       };
+      };
+
+      testnet = mkOption {
+         type = types.bool;
+         default = false;
+         description = "Connect to testnet network instead of the default mainnet.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "ergo";
+        description = "The user as which to run the Ergo node.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = cfg.user;
+        description = "The group as which to run the Ergo node.";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Open ports in the firewall for the Ergo node as well as the API.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' 0770 '${cfg.user}' '${cfg.group}' - -"
+    ];
+
+    systemd.services.ergo = {
+      description = "ergo server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = ''${pkgs.ergo}/bin/ergo \
+                      ${optionalString (!cfg.testnet)
+                      "--mainnet"} \
+                      -c ${configFile}'';
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.listen.port ] ++ [ cfg.api.listen.port ];
+    };
+
+    users.users.${cfg.user} = {
+      name = cfg.user;
+      group = cfg.group;
+      description = "Ergo daemon user";
+      home = cfg.dataDir;
+      isSystemUser = true;
+    };
+
+    users.groups.${cfg.group} = {};
+
+  };
+}
diff --git a/nixos/modules/services/networking/gateone.nix b/nixos/modules/services/networking/gateone.nix
index 4456a95402e..56f2ba21a12 100644
--- a/nixos/modules/services/networking/gateone.nix
+++ b/nixos/modules/services/networking/gateone.nix
@@ -56,4 +56,4 @@ config = mkIf cfg.enable {
   };
 };
 }
-  
+
diff --git a/nixos/modules/services/networking/go-neb.nix b/nixos/modules/services/networking/go-neb.nix
new file mode 100644
index 00000000000..991ae38f30a
--- /dev/null
+++ b/nixos/modules/services/networking/go-neb.nix
@@ -0,0 +1,53 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.go-neb;
+
+  configFile = pkgs.writeText "config.yml" (builtins.toJSON cfg.config);
+in {
+  options.services.go-neb = {
+    enable = mkEnableOption "Extensible matrix bot written in Go";
+
+    bindAddress = mkOption {
+      type = types.str;
+      description = "Port (and optionally address) to listen on.";
+      default = ":4050";
+    };
+
+    baseUrl = mkOption {
+      type = types.str;
+      description = "Public-facing endpoint that can receive webhooks.";
+    };
+
+    config = mkOption {
+      type = types.uniq types.attrs;
+      description = ''
+        Your <filename>config.yaml</filename> as a Nix attribute set.
+        See <link xlink:href="https://github.com/matrix-org/go-neb/blob/master/config.sample.yaml">config.sample.yaml</link>
+        for possible options.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.go-neb = {
+      description = "Extensible matrix bot written in Go";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      environment = {
+        BASE_URL = cfg.baseUrl;
+        BIND_ADDRESS = cfg.bindAddress;
+        CONFIG_FILE = configFile;
+      };
+
+      serviceConfig = {
+        ExecStart = "${pkgs.go-neb}/bin/go-neb";
+        DynamicUser = true;
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ hexa maralorn ];
+}
diff --git a/nixos/modules/services/networking/haproxy.nix b/nixos/modules/services/networking/haproxy.nix
index 4678829986c..e9d72b35499 100644
--- a/nixos/modules/services/networking/haproxy.nix
+++ b/nixos/modules/services/networking/haproxy.nix
@@ -56,6 +56,9 @@ with lib;
       message = "You must provide services.haproxy.config.";
     }];
 
+    # configuration file indirection is needed to support reloading
+    environment.etc."haproxy.cfg".source = haproxyCfg;
+
     systemd.services.haproxy = {
       description = "HAProxy";
       after = [ "network.target" ];
@@ -64,11 +67,32 @@ with lib;
         User = cfg.user;
         Group = cfg.group;
         Type = "notify";
-        # when running the config test, don't be quiet so we can see what goes wrong
-        ExecStartPre = "${pkgs.haproxy}/sbin/haproxy -c -f ${haproxyCfg}";
-        ExecStart = "${pkgs.haproxy}/sbin/haproxy -Ws -f ${haproxyCfg}";
-        Restart = "on-failure";
+        ExecStartPre = [
+          # when the master process receives USR2, it reloads itself using exec(argv[0]),
+          # so we create a symlink there and update it before reloading
+          "${pkgs.coreutils}/bin/ln -sf ${pkgs.haproxy}/sbin/haproxy /run/haproxy/haproxy"
+          # when running the config test, don't be quiet so we can see what goes wrong
+          "/run/haproxy/haproxy -c -f ${haproxyCfg}"
+        ];
+        ExecStart = "/run/haproxy/haproxy -Ws -f /etc/haproxy.cfg -p /run/haproxy/haproxy.pid";
+        # support reloading
+        ExecReload = [
+          "${pkgs.haproxy}/sbin/haproxy -c -f ${haproxyCfg}"
+          "${pkgs.coreutils}/bin/ln -sf ${pkgs.haproxy}/sbin/haproxy /run/haproxy/haproxy"
+          "${pkgs.coreutils}/bin/kill -USR2 $MAINPID"
+        ];
+        KillMode = "mixed";
+        SuccessExitStatus = "143";
+        Restart = "always";
         RuntimeDirectory = "haproxy";
+        # upstream hardening options
+        NoNewPrivileges = true;
+        ProtectHome = true;
+        ProtectSystem = "strict";
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        SystemCallFilter= "~@cpu-emulation @keyring @module @obsolete @raw-io @reboot @swap @sync";
         # needed in case we bind to port < 1024
         AmbientCapabilities = "CAP_NET_BIND_SERVICE";
       };
diff --git a/nixos/modules/services/networking/jicofo.nix b/nixos/modules/services/networking/jicofo.nix
new file mode 100644
index 00000000000..160a5fea91a
--- /dev/null
+++ b/nixos/modules/services/networking/jicofo.nix
@@ -0,0 +1,152 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.jicofo;
+in
+{
+  options.services.jicofo = with types; {
+    enable = mkEnableOption "Jitsi Conference Focus - component of Jitsi Meet";
+
+    xmppHost = mkOption {
+      type = str;
+      example = "localhost";
+      description = ''
+        Hostname of the XMPP server to connect to.
+      '';
+    };
+
+    xmppDomain = mkOption {
+      type = nullOr str;
+      example = "meet.example.org";
+      description = ''
+        Domain name of the XMMP server to which to connect as a component.
+
+        If null, <option>xmppHost</option> is used.
+      '';
+    };
+
+    componentPasswordFile = mkOption {
+      type = str;
+      example = "/run/keys/jicofo-component";
+      description = ''
+        Path to file containing component secret.
+      '';
+    };
+
+    userName = mkOption {
+      type = str;
+      default = "focus";
+      description = ''
+        User part of the JID for XMPP user connection.
+      '';
+    };
+
+    userDomain = mkOption {
+      type = str;
+      example = "auth.meet.example.org";
+      description = ''
+        Domain part of the JID for XMPP user connection.
+      '';
+    };
+
+    userPasswordFile = mkOption {
+      type = str;
+      example = "/run/keys/jicofo-user";
+      description = ''
+        Path to file containing password for XMPP user connection.
+      '';
+    };
+
+    bridgeMuc = mkOption {
+      type = str;
+      example = "jvbbrewery@internal.meet.example.org";
+      description = ''
+        JID of the internal MUC used to communicate with Videobridges.
+      '';
+    };
+
+    config = mkOption {
+      type = attrsOf str;
+      default = { };
+      example = literalExample ''
+        {
+          "org.jitsi.jicofo.auth.URL" = "XMPP:jitsi-meet.example.com";
+        }
+      '';
+      description = ''
+        Contents of the <filename>sip-communicator.properties</filename> configuration file for jicofo.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.jicofo.config = mapAttrs (_: v: mkDefault v) {
+      "org.jitsi.jicofo.BRIDGE_MUC" = cfg.bridgeMuc;
+    };
+
+    users.groups.jitsi-meet = {};
+
+    systemd.services.jicofo = let
+      jicofoProps = {
+        "-Dnet.java.sip.communicator.SC_HOME_DIR_LOCATION" = "/etc/jitsi";
+        "-Dnet.java.sip.communicator.SC_HOME_DIR_NAME" = "jicofo";
+        "-Djava.util.logging.config.file" = "/etc/jitsi/jicofo/logging.properties";
+      };
+    in
+    {
+      description = "JItsi COnference FOcus";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      restartTriggers = [
+        config.environment.etc."jitsi/jicofo/sip-communicator.properties".source
+      ];
+      environment.JAVA_SYS_PROPS = concatStringsSep " " (mapAttrsToList (k: v: "${k}=${toString v}") jicofoProps);
+
+      script = ''
+        ${pkgs.jicofo}/bin/jicofo \
+          --host=${cfg.xmppHost} \
+          --domain=${if cfg.xmppDomain == null then cfg.xmppHost else cfg.xmppDomain} \
+          --secret=$(cat ${cfg.componentPasswordFile}) \
+          --user_name=${cfg.userName} \
+          --user_domain=${cfg.userDomain} \
+          --user_password=$(cat ${cfg.userPasswordFile})
+      '';
+
+      serviceConfig = {
+        Type = "exec";
+
+        DynamicUser = true;
+        User = "jicofo";
+        Group = "jitsi-meet";
+
+        CapabilityBoundingSet = "";
+        NoNewPrivileges = true;
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectHostname = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+        RestrictNamespaces = true;
+        LockPersonality = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+      };
+    };
+
+    environment.etc."jitsi/jicofo/sip-communicator.properties".source =
+      pkgs.writeText "sip-communicator.properties" (
+        generators.toKeyValue {} cfg.config
+      );
+    environment.etc."jitsi/jicofo/logging.properties".source =
+      mkDefault "${pkgs.jicofo}/etc/jitsi/jicofo/logging.properties-journal";
+  };
+
+  meta.maintainers = lib.teams.jitsi.members;
+}
diff --git a/nixos/modules/services/networking/jitsi-videobridge.nix b/nixos/modules/services/networking/jitsi-videobridge.nix
new file mode 100644
index 00000000000..5482e997a40
--- /dev/null
+++ b/nixos/modules/services/networking/jitsi-videobridge.nix
@@ -0,0 +1,276 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.jitsi-videobridge;
+  attrsToArgs = a: concatStringsSep " " (mapAttrsToList (k: v: "${k}=${toString v}") a);
+
+  # HOCON is a JSON superset that videobridge2 uses for configuration.
+  # It can substitute environment variables which we use for passwords here.
+  # https://github.com/lightbend/config/blob/master/README.md
+  #
+  # Substitution for environment variable FOO is represented as attribute set
+  # { __hocon_envvar = "FOO"; }
+  toHOCON = x: if isAttrs x && x ? __hocon_envvar then ("\${" + x.__hocon_envvar + "}")
+    else if isAttrs x then "{${ concatStringsSep "," (mapAttrsToList (k: v: ''"${k}":${toHOCON v}'') x) }}"
+    else if isList x then "[${ concatMapStringsSep "," toHOCON x }]"
+    else builtins.toJSON x;
+
+  # We're passing passwords in environment variables that have names generated
+  # from an attribute name, which may not be a valid bash identifier.
+  toVarName = s: "XMPP_PASSWORD_" + stringAsChars (c: if builtins.match "[A-Za-z0-9]" c != null then c else "_") s;
+
+  defaultJvbConfig = {
+    videobridge = {
+      ice = {
+        tcp = {
+          enabled = true;
+          port = 4443;
+        };
+        udp.port = 10000;
+      };
+      stats = {
+        enabled = true;
+        transports = [ { type = "muc"; } ];
+      };
+      apis.xmpp-client.configs = flip mapAttrs cfg.xmppConfigs (name: xmppConfig: {
+        hostname = xmppConfig.hostName;
+        domain = xmppConfig.domain;
+        username = xmppConfig.userName;
+        password = { __hocon_envvar = toVarName name; };
+        muc_jids = xmppConfig.mucJids;
+        muc_nickname = xmppConfig.mucNickname;
+        disable_certificate_verification = xmppConfig.disableCertificateVerification;
+      });
+    };
+  };
+
+  # Allow overriding leaves of the default config despite types.attrs not doing any merging.
+  jvbConfig = recursiveUpdate defaultJvbConfig cfg.config;
+in
+{
+  options.services.jitsi-videobridge = with types; {
+    enable = mkEnableOption "Jitsi Videobridge, a WebRTC compatible video router";
+
+    config = mkOption {
+      type = attrs;
+      default = { };
+      example = literalExample ''
+        {
+          videobridge = {
+            ice.udp.port = 5000;
+            websockets = {
+              enabled = true;
+              server-id = "jvb1";
+            };
+          };
+        }
+      '';
+      description = ''
+        Videobridge configuration.
+
+        See <link xlink:href="https://github.com/jitsi/jitsi-videobridge/blob/master/src/main/resources/reference.conf" />
+        for default configuration with comments.
+      '';
+    };
+
+    xmppConfigs = mkOption {
+      description = ''
+        XMPP servers to connect to.
+
+        See <link xlink:href="https://github.com/jitsi/jitsi-videobridge/blob/master/doc/muc.md" /> for more information.
+      '';
+      default = { };
+      example = literalExample ''
+        {
+          "localhost" = {
+            hostName = "localhost";
+            userName = "jvb";
+            domain = "auth.xmpp.example.org";
+            passwordFile = "/var/lib/jitsi-meet/videobridge-secret";
+            mucJids = "jvbbrewery@internal.xmpp.example.org";
+          };
+        }
+      '';
+      type = attrsOf (submodule ({ name, ... }: {
+        options = {
+          hostName = mkOption {
+            type = str;
+            example = "xmpp.example.org";
+            description = ''
+              Hostname of the XMPP server to connect to. Name of the attribute set is used by default.
+            '';
+          };
+          domain = mkOption {
+            type = nullOr str;
+            default = null;
+            example = "auth.xmpp.example.org";
+            description = ''
+              Domain part of JID of the XMPP user, if it is different from hostName.
+            '';
+          };
+          userName = mkOption {
+            type = str;
+            default = "jvb";
+            description = ''
+              User part of the JID.
+            '';
+          };
+          passwordFile = mkOption {
+            type = str;
+            example = "/run/keys/jitsi-videobridge-xmpp1";
+            description = ''
+              File containing the password for the user.
+            '';
+          };
+          mucJids = mkOption {
+            type = str;
+            example = "jvbbrewery@internal.xmpp.example.org";
+            description = ''
+              JID of the MUC to join. JiCoFo needs to be configured to join the same MUC.
+            '';
+          };
+          mucNickname = mkOption {
+            # Upstream DEBs use UUID, let's use hostname instead.
+            type = str;
+            description = ''
+              Videobridges use the same XMPP account and need to be distinguished by the
+              nickname (aka resource part of the JID). By default, system hostname is used.
+            '';
+          };
+          disableCertificateVerification = mkOption {
+            type = bool;
+            default = false;
+            description = ''
+              Whether to skip validation of the server's certificate.
+            '';
+          };
+        };
+        config = {
+          hostName = mkDefault name;
+          mucNickname = mkDefault (builtins.replaceStrings [ "." ] [ "-" ] (
+            config.networking.hostName + optionalString (config.networking.domain != null) ".${config.networking.domain}"
+          ));
+        };
+      }));
+    };
+
+    nat = {
+      localAddress = mkOption {
+        type = nullOr str;
+        default = null;
+        example = "192.168.1.42";
+        description = ''
+          Local address when running behind NAT.
+        '';
+      };
+
+      publicAddress = mkOption {
+        type = nullOr str;
+        default = null;
+        example = "1.2.3.4";
+        description = ''
+          Public address when running behind NAT.
+        '';
+      };
+    };
+
+    extraProperties = mkOption {
+      type = attrsOf str;
+      default = { };
+      description = ''
+        Additional Java properties passed to jitsi-videobridge.
+      '';
+    };
+
+    openFirewall = mkOption {
+      type = bool;
+      default = false;
+      description = ''
+        Whether to open ports in the firewall for the videobridge.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.groups.jitsi-meet = {};
+
+    services.jitsi-videobridge.extraProperties = optionalAttrs (cfg.nat.localAddress != null) {
+      "org.ice4j.ice.harvest.NAT_HARVESTER_LOCAL_ADDRESS" = cfg.nat.localAddress;
+      "org.ice4j.ice.harvest.NAT_HARVESTER_PUBLIC_ADDRESS" = cfg.nat.publicAddress;
+    };
+
+    systemd.services.jitsi-videobridge2 = let
+      jvbProps = {
+        "-Dnet.java.sip.communicator.SC_HOME_DIR_LOCATION" = "/etc/jitsi";
+        "-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);
+      } // (mapAttrs' (k: v: nameValuePair "-D${k}" v) cfg.extraProperties);
+    in
+    {
+      aliases = [ "jitsi-videobridge.service" ];
+      description = "Jitsi Videobridge";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      environment.JAVA_SYS_PROPS = attrsToArgs jvbProps;
+
+      script = (concatStrings (mapAttrsToList (name: xmppConfig:
+        "export ${toVarName name}=$(cat ${xmppConfig.passwordFile})\n"
+      ) cfg.xmppConfigs))
+      + ''
+        ${pkgs.jitsi-videobridge}/bin/jitsi-videobridge --apis=none
+      '';
+
+      serviceConfig = {
+        Type = "exec";
+
+        DynamicUser = true;
+        User = "jitsi-videobridge";
+        Group = "jitsi-meet";
+
+        CapabilityBoundingSet = "";
+        NoNewPrivileges = true;
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectHostname = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+        RestrictNamespaces = true;
+        LockPersonality = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+
+        TasksMax = 65000;
+        LimitNPROC = 65000;
+        LimitNOFILE = 65000;
+      };
+    };
+
+    environment.etc."jitsi/videobridge/logging.properties".source =
+      mkDefault "${pkgs.jitsi-videobridge}/etc/jitsi/videobridge/logging.properties-journal";
+
+    # (from videobridge2 .deb)
+    # this sets the max, so that we can bump the JVB UDP single port buffer size.
+    boot.kernel.sysctl."net.core.rmem_max" = mkDefault 10485760;
+    boot.kernel.sysctl."net.core.netdev_max_backlog" = mkDefault 100000;
+
+    networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall
+      [ jvbConfig.videobridge.ice.tcp.port ];
+    networking.firewall.allowedUDPPorts = mkIf cfg.openFirewall
+      [ jvbConfig.videobridge.ice.udp.port ];
+
+    assertions = [{
+      message = "publicAddress must be set if and only if localAddress is set";
+      assertion = (cfg.nat.publicAddress == null) == (cfg.nat.localAddress == null);
+    }];
+  };
+
+  meta.maintainers = lib.teams.jitsi.members;
+}
diff --git a/nixos/modules/services/networking/kresd.nix b/nixos/modules/services/networking/kresd.nix
index c5a84eebd46..ccb34163d5f 100644
--- a/nixos/modules/services/networking/kresd.nix
+++ b/nixos/modules/services/networking/kresd.nix
@@ -129,14 +129,17 @@ in {
     systemd.services."kresd@".serviceConfig = {
       ExecStart = "${package}/bin/kresd --noninteractive "
         + "-c ${package}/lib/knot-resolver/distro-preconfig.lua -c ${configFile}";
-      # Ensure correct ownership in case UID or GID changes.
+      # Ensure /run/knot-resolver exists
+      RuntimeDirectory = "knot-resolver";
+      RuntimeDirectoryMode = "0770";
+      # Ensure /var/lib/knot-resolver exists
+      StateDirectory = "knot-resolver";
+      StateDirectoryMode = "0770";
+      # Ensure /var/cache/knot-resolver exists
       CacheDirectory = "knot-resolver";
-      CacheDirectoryMode = "0750";
+      CacheDirectoryMode = "0770";
     };
 
-    environment.etc."tmpfiles.d/knot-resolver.conf".source =
-      "${package}/lib/tmpfiles.d/knot-resolver.conf";
-
     # Try cleaning up the previously default location of cache file.
     # Note that /var/cache/* should always be safe to remove.
     # TODO: remove later, probably between 20.09 and 21.03
diff --git a/nixos/modules/services/networking/mstpd.nix b/nixos/modules/services/networking/mstpd.nix
index 5d1fc4a6542..bd71010ce54 100644
--- a/nixos/modules/services/networking/mstpd.nix
+++ b/nixos/modules/services/networking/mstpd.nix
@@ -5,7 +5,7 @@ in
 with lib;
 {
   options.services.mstpd = {
-    
+
     enable = mkOption {
       default = false;
       type = types.bool;
diff --git a/nixos/modules/services/networking/namecoind.nix b/nixos/modules/services/networking/namecoind.nix
index ead7f085943..6ca99e1321b 100644
--- a/nixos/modules/services/networking/namecoind.nix
+++ b/nixos/modules/services/networking/namecoind.nix
@@ -149,11 +149,6 @@ in
 
   config = mkIf cfg.enable {
 
-    services.dnschain.extraConfig = ''
-      [namecoin]
-      config = ${configFile}
-    '';
-
     users.users.namecoin = {
       uid  = config.ids.uids.namecoin;
       description = "Namecoin daemon user";
diff --git a/nixos/modules/services/networking/ncdns.nix b/nixos/modules/services/networking/ncdns.nix
new file mode 100644
index 00000000000..c1832ad1752
--- /dev/null
+++ b/nixos/modules/services/networking/ncdns.nix
@@ -0,0 +1,278 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfgs = config.services;
+  cfg  = cfgs.ncdns;
+
+  dataDir  = "/var/lib/ncdns";
+  username = "ncdns";
+
+  valueType = with types; oneOf [ int str bool path ]
+    // { description = "setting type (integer, string, bool or path)"; };
+
+  configType = with types; attrsOf (nullOr (either valueType configType))
+    // { description = ''
+          ncdns.conf configuration type. The format consists of an
+          attribute set of settings. Each setting can be either `null`,
+          a value or an attribute set. The allowed values are integers,
+          strings, booleans or paths.
+         '';
+       };
+
+  configFile = pkgs.runCommand "ncdns.conf"
+    { json = builtins.toJSON cfg.settings;
+      passAsFile = [ "json" ];
+    }
+    "${pkgs.remarshal}/bin/json2toml < $jsonPath > $out";
+
+  defaultFiles = {
+    public  = "${dataDir}/bit.key";
+    private = "${dataDir}/bit.private";
+    zonePublic  = "${dataDir}/bit-zone.key";
+    zonePrivate = "${dataDir}/bit-zone.private";
+  };
+
+  # if all keys are the default value
+  needsKeygen = all id (flip mapAttrsToList cfg.dnssec.keys
+    (n: v: v == getAttr n defaultFiles));
+
+  mkDefaultAttrs = mapAttrs (n: v: mkDefault v);
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.ncdns = {
+
+      enable = mkEnableOption ''
+        ncdns, a Go daemon to bridge Namecoin to DNS.
+        To resolve .bit domains set <literal>services.namecoind.enable = true;</literal>
+        and an RPC username/password
+      '';
+
+      address = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        description = ''
+          The IP address the ncdns resolver will bind to.  Leave this unchanged
+          if you do not wish to directly expose the resolver.
+        '';
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 5333;
+        description = ''
+          The port the ncdns resolver will bind to.
+        '';
+      };
+
+      identity.hostname = mkOption {
+        type = types.str;
+        default = config.networking.hostName;
+        example = "example.com";
+        description = ''
+          The hostname of this ncdns instance, which defaults to the machine
+          hostname. If specified, ncdns lists the hostname as an NS record at
+          the zone apex:
+          <programlisting>
+          bit. IN NS ns1.example.com.
+          </programlisting>
+          If unset ncdns will generate an internal psuedo-hostname under the
+          zone, which will resolve to the value of
+          <option>services.ncdns.identity.address</option>.
+          If you are only using ncdns locally you can ignore this.
+        '';
+      };
+
+      identity.hostmaster = mkOption {
+        type = types.str;
+        default = "";
+        example = "root@example.com";
+        description = ''
+          An email address for the SOA record at the bit zone.
+          If you are only using ncdns locally you can ignore this.
+        '';
+      };
+
+      identity.address = mkOption {
+        type = types.str;
+        default = "127.127.127.127";
+        description = ''
+          The IP address the hostname specified in
+          <option>services.ncdns.identity.hostname</option> should resolve to.
+          If you are only using ncdns locally you can ignore this.
+        '';
+      };
+
+      dnssec.enable = mkEnableOption ''
+        DNSSEC support in ncdns. This will generate KSK and ZSK keypairs
+        (unless provided via the options
+        <option>services.ncdns.dnssec.publicKey</option>,
+        <option>services.ncdns.dnssec.privateKey</option> etc.) and add a trust
+        anchor to recursive resolvers
+      '';
+
+      dnssec.keys.public = mkOption {
+        type = types.path;
+        default = defaultFiles.public;
+        description = ''
+          Path to the file containing the KSK public key.
+          The key can be generated using the <literal>dnssec-keygen</literal>
+          command, provided by the package <package>bind</package> as follows:
+          <programlisting>
+          $ dnssec-keygen -a RSASHA256 -3 -b 2048 -f KSK bit
+          </programlisting>
+        '';
+      };
+
+      dnssec.keys.private = mkOption {
+        type = types.path;
+        default = defaultFiles.private;
+        description = ''
+          Path to the file containing the KSK private key.
+        '';
+      };
+
+      dnssec.keys.zonePublic = mkOption {
+        type = types.path;
+        default = defaultFiles.zonePublic;
+        description = ''
+          Path to the file containing the ZSK public key.
+          The key can be generated using the <literal>dnssec-keygen</literal>
+          command, provided by the package <package>bind</package> as follows:
+          <programlisting>
+          $ dnssec-keygen -a RSASHA256 -3 -b 2048 bit
+          </programlisting>
+        '';
+      };
+
+      dnssec.keys.zonePrivate = mkOption {
+        type = types.path;
+        default = defaultFiles.zonePrivate;
+        description = ''
+          Path to the file containing the ZSK private key.
+        '';
+      };
+
+      settings = mkOption {
+        type = configType;
+        default = { };
+        example = literalExample ''
+          { # enable webserver
+            ncdns.httplistenaddr = ":8202";
+
+            # synchronize TLS certs
+            certstore.nss = true;
+            # note: all paths are relative to the config file
+            certstore.nsscertdir =  "../../var/lib/ncdns";
+            certstore.nssdbdir = "../../home/alice/.pki/nssdb";
+          }
+        '';
+        description = ''
+          ncdns settings. Use this option to configure ncds
+          settings not exposed in a NixOS option or to bypass one.
+          See the example ncdns.conf file at <link xlink:href="
+          https://git.io/JfX7g"/> for the available options.
+        '';
+      };
+
+    };
+
+    services.pdns-recursor.resolveNamecoin = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Resolve <literal>.bit</literal> top-level domains using ncdns and namecoin.
+      '';
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    services.pdns-recursor = mkIf cfgs.pdns-recursor.resolveNamecoin {
+      forwardZonesRecurse.bit = "127.0.0.1:${toString cfg.port}";
+      luaConfig =
+        if cfg.dnssec.enable
+          then ''readTrustAnchorsFromFile("${cfg.dnssec.keys.public}")''
+          else ''addNTA("bit", "namecoin DNSSEC disabled")'';
+    };
+
+    # Avoid pdns-recursor not finding the DNSSEC keys
+    systemd.services.pdns-recursor = mkIf cfgs.pdns-recursor.resolveNamecoin {
+      after = [ "ncdns.service" ];
+      wants = [ "ncdns.service" ];
+    };
+
+    services.ncdns.settings = mkDefaultAttrs {
+      ncdns =
+        { # Namecoin RPC
+          namecoinrpcaddress =
+            "${cfgs.namecoind.rpc.address}:${toString cfgs.namecoind.rpc.port}";
+          namecoinrpcusername = cfgs.namecoind.rpc.user;
+          namecoinrpcpassword = cfgs.namecoind.rpc.password;
+
+          # Identity
+          selfname = cfg.identity.hostname;
+          hostmaster = cfg.identity.hostmaster;
+          selfip = cfg.identity.address;
+
+          # Other
+          bind = "${cfg.address}:${toString cfg.port}";
+        }
+        // optionalAttrs cfg.dnssec.enable
+        { # DNSSEC
+          publickey  = "../.." + cfg.dnssec.keys.public;
+          privatekey = "../.." + cfg.dnssec.keys.private;
+          zonepublickey  = "../.." + cfg.dnssec.keys.zonePublic;
+          zoneprivatekey = "../.." + cfg.dnssec.keys.zonePrivate;
+        };
+
+        # Daemon
+        service.daemon = true;
+        xlog.journal = true;
+    };
+
+    users.users.ncdns =
+      { description = "ncdns daemon user"; };
+
+    systemd.services.ncdns = {
+      description = "ncdns daemon";
+      after    = [ "namecoind.service" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        User = "ncdns";
+        StateDirectory = "ncdns";
+        Restart = "on-failure";
+        ExecStart = "${pkgs.ncdns}/bin/ncdns -conf=${configFile}";
+      };
+
+      preStart = optionalString (cfg.dnssec.enable && needsKeygen) ''
+        cd ${dataDir}
+        if [ ! -e bit.key ]; then
+          ${pkgs.bind}/bin/dnssec-keygen -a RSASHA256 -3 -b 2048 bit
+          mv Kbit.*.key bit-zone.key
+          mv Kbit.*.private bit-zone.private
+          ${pkgs.bind}/bin/dnssec-keygen -a RSASHA256 -3 -b 2048 -f KSK bit
+          mv Kbit.*.key bit.key
+          mv Kbit.*.private bit.private
+        fi
+      '';
+    };
+
+  };
+
+  meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+
+}
diff --git a/nixos/modules/services/networking/networkmanager.nix b/nixos/modules/services/networking/networkmanager.nix
index 6f24141b33c..cc789897b29 100644
--- a/nixos/modules/services/networking/networkmanager.nix
+++ b/nixos/modules/services/networking/networkmanager.nix
@@ -449,6 +449,13 @@ in {
 
     systemd.services.ModemManager.aliases = [ "dbus-org.freedesktop.ModemManager1.service" ];
 
+    # override unit as recommended by upstream - see https://github.com/NixOS/nixpkgs/issues/88089
+    # TODO: keep an eye on modem-manager releases as this will eventually be added to the upstream unit
+    systemd.services.ModemManager.serviceConfig.ExecStart = [
+      ""
+      "${pkgs.modemmanager}/sbin/ModemManager --filter-policy=STRICT"
+    ];
+
     systemd.services.NetworkManager-dispatcher = {
       wantedBy = [ "network.target" ];
       restartTriggers = [ configFile ];
diff --git a/nixos/modules/services/networking/nextdns.nix b/nixos/modules/services/networking/nextdns.nix
new file mode 100644
index 00000000000..a633bff62ec
--- /dev/null
+++ b/nixos/modules/services/networking/nextdns.nix
@@ -0,0 +1,44 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.nextdns;
+in {
+  options = {
+    services.nextdns = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the NextDNS DNS/53 to DoH Proxy service.";
+      };
+      arguments = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "-config" "10.0.3.0/24=abcdef" ];
+        description = "Additional arguments to be passed to nextdns run.";
+      };
+    };
+  };
+
+  # https://github.com/nextdns/nextdns/blob/628ea509eaaccd27adb66337db03e5b56f6f38a8/host/service/systemd/service.go
+  config = mkIf cfg.enable {
+    systemd.services.nextdns = {
+      description = "NextDNS DNS/53 to DoH Proxy";
+      environment = {
+        SERVICE_RUN_MODE = "1";
+      };
+      serviceConfig = {
+        StartLimitInterval = 5;
+        StartLimitBurst = 10;
+        ExecStart = "${pkgs.nextdns}/bin/nextdns run ${escapeShellArgs config.services.nextdns.arguments}";
+        RestartSec = 120;
+        LimitMEMLOCK = "infinity";
+      };
+      after = [ "network.target" ];
+      before = [ "nss-lookup.target" ];
+      wants = [ "nss-lookup.target" ];
+      wantedBy = [ "multi-user.target" ];
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/nghttpx/default.nix b/nixos/modules/services/networking/nghttpx/default.nix
index 881a2670f5d..b8a0a24e3aa 100644
--- a/nixos/modules/services/networking/nghttpx/default.nix
+++ b/nixos/modules/services/networking/nghttpx/default.nix
@@ -60,7 +60,7 @@ let
       # NB: nghttpx doesn't accept "tls", you must omit "no-tls" for
       # the default behavior of turning on TLS.
       params1 = lib.remove "tls" params0;
-          
+
       sections          = [ host] ++ params1;
       formattedSections = lib.concatStringsSep ";" sections;
     in
@@ -90,7 +90,7 @@ in
 { imports = [
     ./nghttpx-options.nix
   ];
-  
+
   config = lib.mkIf cfg.enable {
 
     users.groups.nghttpx = { };
@@ -98,7 +98,7 @@ in
       group = config.users.groups.nghttpx.name;
       isSystemUser = true;
     };
-      
+
 
     systemd.services = {
       nghttpx = {
diff --git a/nixos/modules/services/networking/nsd.nix b/nixos/modules/services/networking/nsd.nix
index 429580e5c6c..3ecbd06ee41 100644
--- a/nixos/modules/services/networking/nsd.nix
+++ b/nixos/modules/services/networking/nsd.nix
@@ -11,8 +11,6 @@ let
 
   # build nsd with the options needed for the given config
   nsdPkg = pkgs.nsd.override {
-    configFile = "${configFile}/nsd.conf";
-
     bind8Stats = cfg.bind8Stats;
     ipv6 = cfg.ipv6;
     ratelimit = cfg.ratelimit.enable;
@@ -252,7 +250,7 @@ let
           Use imports or pkgs.lib.readFile if you don't want this data in your config file.
         '';
       };
-      
+
       dnssec = mkEnableOption "DNSSEC";
 
       dnssecPolicy = {
@@ -897,7 +895,10 @@ in
               + "want, please enable 'services.nsd.rootServer'.";
     };
 
-    environment.systemPackages = [ nsdPkg ];
+    environment = {
+      systemPackages = [ nsdPkg ];
+      etc."nsd/nsd.conf".source = "${configFile}/nsd.conf";
+    };
 
     users.groups.${username}.gid = config.ids.gids.nsd;
 
@@ -970,7 +971,7 @@ in
       script = signZones;
 
       postStop = ''
-        ${pkgs.systemd}/bin/systemctl kill -s SIGHUP nsd.service
+        /run/current-system/systemd/bin/systemctl kill -s SIGHUP nsd.service
       '';
     };
 
diff --git a/nixos/modules/services/networking/onedrive.nix b/nixos/modules/services/networking/onedrive.nix
new file mode 100644
index 00000000000..210d2217b27
--- /dev/null
+++ b/nixos/modules/services/networking/onedrive.nix
@@ -0,0 +1,72 @@
+{ config, lib, pkgs, ... }:
+let
+  cfg = config.services.onedrive;
+
+  onedriveLauncher =  pkgs.writeShellScriptBin
+    "onedrive-launcher"
+    ''
+      # XDG_CONFIG_HOME is not recognized in the environment here.
+      if [ -f $HOME/.config/onedrive-launcher ]
+      then
+        # Hopefully using underscore boundary helps locate variables
+        for _onedrive_config_dirname_ in $(cat $HOME/.config/onedrive-launcher | grep -v '[ \t]*#' )
+        do
+          systemctl --user start onedrive@$_onedrive_config_dirname_
+        done
+      else
+        systemctl --user start onedrive@onedrive
+      fi
+    ''
+  ;
+
+in {
+  ### Documentation
+  # meta.doc = ./onedrive.xml;
+
+  ### Interface
+
+  options.services.onedrive = {
+    enable = lib.mkOption {
+      type = lib.types.bool;
+      default = false;
+      description = "Enable OneDrive service";
+    };
+
+     package = lib.mkOption {
+       type = lib.types.package;
+       default = pkgs.onedrive;
+       defaultText = "pkgs.onedrive";
+       example = lib.literalExample "pkgs.onedrive";
+       description = ''
+         OneDrive package to use.
+       '';
+     };
+  };
+### Implementation
+
+  config = lib.mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+
+    systemd.user.services."onedrive@" = {
+      description = "Onedrive sync service";
+
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = ''
+          ${cfg.package}/bin/onedrive --monitor --verbose --confdir=%h/.config/%i
+        '';
+        Restart="on-failure";
+        RestartSec=3;
+        RestartPreventExitStatus=3;
+      };
+    };
+
+    systemd.user.services.onedrive-launcher = {
+      wantedBy = [ "default.target" ];
+      serviceConfig = {
+        Type = "oneshot";
+        ExecStart = "${onedriveLauncher}/bin/onedrive-launcher";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/onedrive.xml b/nixos/modules/services/networking/onedrive.xml
new file mode 100644
index 00000000000..5a9dcf01aee
--- /dev/null
+++ b/nixos/modules/services/networking/onedrive.xml
@@ -0,0 +1,34 @@
+<chapter xmlns="http://docbook.org/ns/docbook"
+         xmlns:xlink="http://www.w3.org/1999/xlink"
+         xmlns:xi="http://www.w3.org/2001/XInclude"
+         version="5.0"
+         xml:id="onedrive">
+ <title>Microsoft OneDrive</title>
+ <para>
+  Microsoft Onedrive is a popular cloud file-hosting service, used by 85% of Fortune 500 companies. NixOS uses a popular OneDrive client for Linux maintained by github user abraunegg. The Linux client is excellent and allows customization of which files or paths to download, not much unlike the default Windows OneDrive client by Microsoft itself. The client allows syncing with multiple onedrive accounts at the same time, of any type- OneDrive personal, OneDrive business, Office365 and Sharepoint libraries, without any additional charge.
+ </para>
+ <para>
+  For more information, guides and documentation, see <link xlink:href="https://abraunegg.github.io/"/>.
+ </para>
+ <para>
+  To enable OneDrive support, add the following to your <filename>configuration.nix</filename>:
+<programlisting>
+<xref linkend="opt-services.onedrive.enable"/> = true;
+</programlisting>
+  This installs the <literal>onedrive</literal> package and a service <literal>onedriveLauncher</literal> which will instantiate a <literal>onedrive</literal> service for all your OneDrive accounts. Follow the steps in documentation of the onedrive client to setup your accounts. To use the service with multiple accounts, create a file named <filename>onedrive-launcher</filename> in <filename>~/.config</filename> and add the filename of the config directory, relative to <filename>~/.config</filename>. For example, if you have two OneDrive accounts with configs in <filename>~/.config/onedrive_bob_work</filename> and <filename>~/.config/onedrive_bob_personal</filename>, add the following lines:
+<programlisting>
+onedrive_bob_work
+# Not in use:
+# onedrive_bob_office365
+onedrive_bob_personal
+</programlisting>
+  No such file needs to be created if you are using only a single OneDrive account with config in the default location <filename>~/.config/onedrive</filename>, in the absence of <filename>~/.config/onedrive-launcher</filename>, only a single service is instantiated, with default config path.
+</para>
+
+  <para>
+  If you wish to use a custom OneDrive package, say from another channel, add the following line:
+<programlisting>
+<xref linkend="opt-services.onedrive.package"/> = pkgs.unstable.onedrive;
+</programlisting>
+ </para>
+</chapter>
diff --git a/nixos/modules/services/networking/prosody.nix b/nixos/modules/services/networking/prosody.nix
index cdd341c9fb6..e53d7093be8 100644
--- a/nixos/modules/services/networking/prosody.nix
+++ b/nixos/modules/services/networking/prosody.nix
@@ -772,7 +772,7 @@ in
       };
 
       disco_items = {
-      ${ lib.concatStringsSep "\n" (builtins.map (x: ''{ "${x.url}", "${x.description}"};'') discoItems)} 
+      ${ lib.concatStringsSep "\n" (builtins.map (x: ''{ "${x.url}", "${x.description}"};'') discoItems)}
       };
 
       allow_registration = ${toLua cfg.allowRegistration}
diff --git a/nixos/modules/services/networking/radicale.nix b/nixos/modules/services/networking/radicale.nix
index 30bf22586f8..5af035fd59e 100644
--- a/nixos/modules/services/networking/radicale.nix
+++ b/nixos/modules/services/networking/radicale.nix
@@ -8,8 +8,10 @@ let
 
   confFile = pkgs.writeText "radicale.conf" cfg.config;
 
-  # This enables us to default to version 2 while still not breaking configurations of people with version 1
-  defaultPackage = if versionAtLeast config.system.stateVersion "17.09" then {
+  defaultPackage = if versionAtLeast config.system.stateVersion "20.09" then {
+    pkg = pkgs.radicale3;
+    text = "pkgs.radicale3";
+  } else if versionAtLeast config.system.stateVersion "17.09" then {
     pkg = pkgs.radicale2;
     text = "pkgs.radicale2";
   } else {
@@ -35,8 +37,9 @@ in
       defaultText = defaultPackage.text;
       description = ''
         Radicale package to use. This defaults to version 1.x if
-        <literal>system.stateVersion &lt; 17.09</literal> and version 2.x
-        otherwise.
+        <literal>system.stateVersion &lt; 17.09</literal>, version 2.x if
+        <literal>17.09 ≤ system.stateVersion &lt; 20.09</literal>, and
+        version 3.x otherwise.
       '';
     };
 
diff --git a/nixos/modules/services/networking/resilio.nix b/nixos/modules/services/networking/resilio.nix
index e74e03fc0b0..6193d7340fc 100644
--- a/nixos/modules/services/networking/resilio.nix
+++ b/nixos/modules/services/networking/resilio.nix
@@ -30,12 +30,12 @@ let
     download_limit = cfg.downloadLimit;
     upload_limit = cfg.uploadLimit;
     lan_encrypt_data = cfg.encryptLAN;
-  } // optionalAttrs cfg.enableWebUI {
+  } // optionalAttrs (cfg.directoryRoot != "") { directory_root = cfg.directoryRoot; }
+    // optionalAttrs cfg.enableWebUI {
     webui = { listen = "${cfg.httpListenAddr}:${toString cfg.httpListenPort}"; } //
       (optionalAttrs (cfg.httpLogin != "") { login = cfg.httpLogin; }) //
       (optionalAttrs (cfg.httpPass != "") { password = cfg.httpPass; }) //
-      (optionalAttrs (cfg.apiKey != "") { api_key = cfg.apiKey; }) //
-      (optionalAttrs (cfg.directoryRoot != "") { directory_root = cfg.directoryRoot; });
+      (optionalAttrs (cfg.apiKey != "") { api_key = cfg.apiKey; });
   } // optionalAttrs (sharedFoldersRecord != []) {
     shared_folders = sharedFoldersRecord;
   }));
@@ -109,8 +109,8 @@ in
 
       httpListenAddr = mkOption {
         type = types.str;
-        default = "0.0.0.0";
-        example = "1.2.3.4";
+        default = "[::1]";
+        example = "0.0.0.0";
         description = ''
           HTTP address to bind to.
         '';
@@ -206,16 +206,16 @@ in
 
           If you would like to be able to modify the contents of this
           directories, it is recommended that you make your user a
-          member of the <literal>resilio</literal> group.
+          member of the <literal>rslsync</literal> group.
 
           Directories in this list should be in the
-          <literal>resilio</literal> group, and that group must have
+          <literal>rslsync</literal> group, and that group must have
           write access to the directory. It is also recommended that
           <literal>chmod g+s</literal> is applied to the directory
           so that any sub directories created will also belong to
-          the <literal>resilio</literal> group. Also,
-          <literal>setfacl -d -m group:resilio:rwx</literal> and
-          <literal>setfacl -m group:resilio:rwx</literal> should also
+          the <literal>rslsync</literal> group. Also,
+          <literal>setfacl -d -m group:rslsync:rwx</literal> and
+          <literal>setfacl -m group:rslsync:rwx</literal> should also
           be applied so that the sub directories are writable by
           the group.
         '';
diff --git a/nixos/modules/services/networking/skydns.nix b/nixos/modules/services/networking/skydns.nix
index e79d6de9264..ea466de9327 100644
--- a/nixos/modules/services/networking/skydns.nix
+++ b/nixos/modules/services/networking/skydns.nix
@@ -64,7 +64,7 @@ in {
     extraConfig = mkOption {
       default = {};
       type = types.attrsOf types.str;
-      description = "Skydns attribute set of extra config options passed as environemnt variables.";
+      description = "Skydns attribute set of extra config options passed as environment variables.";
     };
   };
 
diff --git a/nixos/modules/services/networking/sslh.nix b/nixos/modules/services/networking/sslh.nix
index c4fa370a5fe..0921febba66 100644
--- a/nixos/modules/services/networking/sslh.nix
+++ b/nixos/modules/services/networking/sslh.nix
@@ -15,7 +15,11 @@ let
 
     listen:
     (
-      { host: "${cfg.listenAddress}"; port: "${toString cfg.port}"; }
+      ${
+        concatMapStringsSep ",\n"
+        (addr: ''{ host: "${addr}"; port: "${toString cfg.port}"; }'')
+        cfg.listenAddresses
+      }
     );
 
     ${cfg.appendConfig}
@@ -33,6 +37,10 @@ let
   '';
 in
 {
+  imports = [
+    (mkRenamedOptionModule [ "services" "sslh" "listenAddress" ] [ "services" "sslh" "listenAddresses" ])
+  ];
+
   options = {
     services.sslh = {
       enable = mkEnableOption "sslh";
@@ -55,10 +63,10 @@ in
         description = "Will the services behind sslh (Apache, sshd and so on) see the external IP and ports as if the external world connected directly to them";
       };
 
-      listenAddress = mkOption {
-        type = types.str;
-        default = "0.0.0.0";
-        description = "Listening address or hostname.";
+      listenAddresses = mkOption {
+        type = types.coercedTo types.str singleton (types.listOf types.str);
+        default = [ "0.0.0.0" "[::]" ];
+        description = "Listening addresses or hostnames.";
       };
 
       port = mkOption {
diff --git a/nixos/modules/services/networking/supplicant.nix b/nixos/modules/services/networking/supplicant.nix
index b5b9989ce18..20704be9b36 100644
--- a/nixos/modules/services/networking/supplicant.nix
+++ b/nixos/modules/services/networking/supplicant.nix
@@ -76,9 +76,9 @@ in
     networking.supplicant = mkOption {
       type = with types; attrsOf (submodule {
         options = {
-  
+
           configFile = {
-  
+
             path = mkOption {
               type = types.nullOr types.path;
               default = null;
@@ -89,7 +89,7 @@ in
                 precedence over options defined in <literal>configFile</literal>.
               '';
             };
-  
+
             writable = mkOption {
               type = types.bool;
               default = false;
@@ -98,9 +98,9 @@ in
                 <literal>wpa_supplicant</literal>.
               '';
             };
-  
+
           };
-  
+
           extraConf = mkOption {
             type = types.lines;
             default = "";
@@ -126,7 +126,7 @@ in
               use the <literal>configFile</literal> instead.
             '';
           };
-  
+
           extraCmdArgs = mkOption {
             type = types.str;
             default = "";
@@ -134,21 +134,21 @@ in
             description =
               "Command line arguments to add when executing <literal>wpa_supplicant</literal>.";
           };
-  
+
           driver = mkOption {
             type = types.nullOr types.str;
             default = "nl80211,wext";
             description = "Force a specific wpa_supplicant driver.";
           };
-  
+
           bridge = mkOption {
             type = types.str;
             default = "";
             description = "Name of the bridge interface that wpa_supplicant should listen at.";
           };
-  
+
           userControlled = {
-  
+
             enable = mkOption {
               type = types.bool;
               default = false;
@@ -159,20 +159,20 @@ in
                 access points.
               '';
             };
-  
+
             socketDir = mkOption {
               type = types.str;
               default = "/run/wpa_supplicant";
               description = "Directory of sockets for controlling wpa_supplicant.";
             };
-  
+
             group = mkOption {
               type = types.str;
               default = "wheel";
               example = "network";
               description = "Members of this group can control wpa_supplicant.";
             };
-  
+
           };
         };
       });
diff --git a/nixos/modules/services/networking/tinc.nix b/nixos/modules/services/networking/tinc.nix
index e98aafc2093..725bd9bf940 100644
--- a/nixos/modules/services/networking/tinc.nix
+++ b/nixos/modules/services/networking/tinc.nix
@@ -48,6 +48,14 @@ in
               '';
             };
 
+            rsaPrivateKeyFile = mkOption {
+              default = null;
+              type = types.nullOr types.path;
+              description = ''
+                Path of the private RSA keyfile.
+              '';
+            };
+
             debugLevel = mkOption {
               default = 0;
               type = types.addCheck types.int (l: l >= 0 && l <= 5);
@@ -139,6 +147,7 @@ in
               Name = ${if data.name == null then "$HOST" else data.name}
               DeviceType = ${data.interfaceType}
               ${optionalString (data.ed25519PrivateKeyFile != null) "Ed25519PrivateKeyFile = ${data.ed25519PrivateKeyFile}"}
+              ${optionalString (data.rsaPrivateKeyFile != null) "PrivateKeyFile = ${data.rsaPrivateKeyFile}"}
               ${optionalString (data.listenAddress != null) "ListenAddress = ${data.listenAddress}"}
               ${optionalString (data.bindToAddress != null) "BindToAddress = ${data.bindToAddress}"}
               Interface = tinc.${network}
@@ -170,12 +179,15 @@ in
           # Determine how we should generate our keys
           if type tinc >/dev/null 2>&1; then
             # Tinc 1.1+ uses the tinc helper application for key generation
-          ${if data.ed25519PrivateKeyFile != null then "  # Keyfile managed by nix" else ''
+          ${if data.ed25519PrivateKeyFile != null then "  # ed25519 Keyfile managed by nix" else ''
             # Prefer ED25519 keys (only in 1.1+)
             [ -f "/etc/tinc/${network}/ed25519_key.priv" ] || tinc -n ${network} generate-ed25519-keys
           ''}
-            # Otherwise use RSA keys
+          ${if data.rsaPrivateKeyFile != null then "  # RSA Keyfile managed by nix" else ''
             [ -f "/etc/tinc/${network}/rsa_key.priv" ] || tinc -n ${network} generate-rsa-keys 4096
+          ''}
+            # In case there isn't anything to do
+            true
           else
             # Tinc 1.0 uses the tincd application
             [ -f "/etc/tinc/${network}/rsa_key.priv" ] || tincd -n ${network} -K 4096
diff --git a/nixos/modules/services/networking/trickster.nix b/nixos/modules/services/networking/trickster.nix
index 8760dd5a938..49c945adb80 100644
--- a/nixos/modules/services/networking/trickster.nix
+++ b/nixos/modules/services/networking/trickster.nix
@@ -106,7 +106,8 @@ in
         Restart = "always";
       };
     };
+  };
 
-  };  
-}
+  meta.maintainers = with maintainers; [ _1000101 ];
 
+}
diff --git a/nixos/modules/services/networking/unifi.nix b/nixos/modules/services/networking/unifi.nix
index 4bdfa8143dc..62bcf7a1497 100644
--- a/nixos/modules/services/networking/unifi.nix
+++ b/nixos/modules/services/networking/unifi.nix
@@ -162,6 +162,8 @@ in
       unitConfig.RequiresMountsFor = stateDir;
       # This a HACK to fix missing dependencies of dynamic libs extracted from jars
       environment.LD_LIBRARY_PATH = with pkgs.stdenv; "${cc.cc.lib}/lib";
+      # Make sure package upgrades trigger a service restart
+      restartTriggers = [ cfg.unifiPackage cfg.mongodbPackage ];
 
       serviceConfig = {
         Type = "simple";
diff --git a/nixos/modules/services/networking/v2ray.nix b/nixos/modules/services/networking/v2ray.nix
index a1774cdffbb..6a924a16449 100644
--- a/nixos/modules/services/networking/v2ray.nix
+++ b/nixos/modules/services/networking/v2ray.nix
@@ -58,7 +58,13 @@ with lib;
     cfg = config.services.v2ray;
     configFile = if cfg.configFile != null
       then cfg.configFile
-      else (pkgs.writeText "v2ray.json" (builtins.toJSON cfg.config));
+      else pkgs.writeTextFile {
+        name = "v2ray.json";
+        text = builtins.toJSON cfg.config;
+        checkPhase = ''
+          ${pkgs.v2ray}/bin/v2ray -test -config $out
+        '';
+      };
 
   in mkIf cfg.enable {
     assertions = [
diff --git a/nixos/modules/services/networking/vsftpd.nix b/nixos/modules/services/networking/vsftpd.nix
index b3e20184423..c57994533c1 100644
--- a/nixos/modules/services/networking/vsftpd.nix
+++ b/nixos/modules/services/networking/vsftpd.nix
@@ -116,7 +116,8 @@ let
         userlist_file=${cfg.userlistFile}
       ''}
       background=YES
-      listen=YES
+      listen=NO
+      listen_ipv6=YES
       nopriv_user=vsftpd
       secure_chroot_dir=/var/empty
       ${optionalString (cfg.localRoot != null) ''
diff --git a/nixos/modules/services/networking/wasabibackend.nix b/nixos/modules/services/networking/wasabibackend.nix
new file mode 100644
index 00000000000..6eacffe709b
--- /dev/null
+++ b/nixos/modules/services/networking/wasabibackend.nix
@@ -0,0 +1,158 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.wasabibackend;
+
+  inherit (lib) mkEnableOption mkIf mkOption optionalAttrs optionalString types;
+
+  confOptions = {
+      BitcoinRpcConnectionString = "${cfg.rpc.user}:${cfg.rpc.password}";
+  } // optionalAttrs (cfg.network == "mainnet") {
+      Network = "Main";
+      MainNetBitcoinP2pEndPoint = "${cfg.endpoint.ip}:${toString cfg.endpoint.port}";
+      MainNetBitcoinCoreRpcEndPoint = "${cfg.rpc.ip}:${toString cfg.rpc.port}";
+  } // optionalAttrs (cfg.network == "testnet") {
+      Network = "TestNet";
+      TestNetBitcoinP2pEndPoint = "${cfg.endpoint.ip}:${toString cfg.endpoint.port}";
+      TestNetBitcoinCoreRpcEndPoint = "${cfg.rpc.ip}:${toString cfg.rpc.port}";
+  } // optionalAttrs (cfg.network == "regtest") {
+      Network = "RegTest";
+      RegTestBitcoinP2pEndPoint = "${cfg.endpoint.ip}:${toString cfg.endpoint.port}";
+      RegTestBitcoinCoreRpcEndPoint = "${cfg.rpc.ip}:${toString cfg.rpc.port}";
+  };
+
+	configFile = pkgs.writeText "wasabibackend.conf" (builtins.toJSON confOptions);
+
+in {
+
+  options = {
+
+    services.wasabibackend = {
+      enable = mkEnableOption "Wasabi backend service";
+
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/wasabibackend";
+        description = "The data directory for the Wasabi backend node.";
+      };
+
+      customConfigFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = "Defines the path to a custom configuration file that is copied to the user's directory. Overrides any config options.";
+      };
+
+      network = mkOption {
+        type = types.enum [ "mainnet" "testnet" "regtest" ];
+        default = "mainnet";
+        description = "The network to use for the Wasabi backend service.";
+      };
+
+      endpoint = {
+        ip = mkOption {
+          type = types.str;
+          default = "127.0.0.1";
+          description = "IP address for P2P connection to bitcoind.";
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = 8333;
+          description = "Port for P2P connection to bitcoind.";
+        };
+      };
+
+      rpc = {
+        ip = mkOption {
+          type = types.str;
+          default = "127.0.0.1";
+          description = "IP address for RPC connection to bitcoind.";
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = 8332;
+          description = "Port for RPC connection to bitcoind.";
+        };
+
+        user = mkOption {
+          type = types.str;
+          default = "bitcoin";
+          description = "RPC user for the bitcoin endpoint.";
+        };
+
+        password = mkOption {
+          type = types.str;
+          default = "password";
+          description = "RPC password for the bitcoin endpoint. Warning: this is stored in cleartext in the Nix store! Use <literal>configFile</literal> or <literal>passwordFile</literal> if needed.";
+        };
+
+        passwordFile = mkOption {
+          type = types.nullOr types.path;
+          default = null;
+          description = "File that contains the password of the RPC user.";
+        };
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "wasabibackend";
+        description = "The user as which to run the wasabibackend node.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = cfg.user;
+        description = "The group as which to run the wasabibackend node.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' 0770 '${cfg.user}' '${cfg.group}' - -"
+    ];
+
+    systemd.services.wasabibackend = {
+      description = "wasabibackend server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      environment = {
+        DOTNET_PRINT_TELEMETRY_MESSAGE = "false";
+        DOTNET_CLI_TELEMETRY_OPTOUT = "true";
+      };
+      preStart = ''
+        mkdir -p ${cfg.dataDir}/.walletwasabi/backend
+        ${if cfg.customConfigFile != null then ''
+          cp -v ${cfg.customConfigFile} ${cfg.dataDir}/.walletwasabi/backend/Config.json
+        '' else ''
+          cp -v ${configFile} ${cfg.dataDir}/.walletwasabi/backend/Config.json
+          ${optionalString (cfg.rpc.passwordFile != null) ''
+            CONFIGTMP=$(mktemp)
+            cat ${cfg.dataDir}/.walletwasabi/backend/Config.json | ${pkgs.jq}/bin/jq --arg rpconnection "${cfg.rpc.user}:$(cat "${cfg.rpc.passwordFile}")" '. + { BitcoinRpcConnectionString: $rpconnection }' > $CONFIGTMP
+            mv $CONFIGTMP ${cfg.dataDir}/.walletwasabi/backend/Config.json
+          ''}
+        ''}
+        chmod ug+w ${cfg.dataDir}/.walletwasabi/backend/Config.json
+      '';
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${pkgs.wasabibackend}/bin/WasabiBackend";
+        ProtectSystem = "full";
+      };
+    };
+
+    users.users.${cfg.user} = {
+      name = cfg.user;
+      group = cfg.group;
+      description = "wasabibackend daemon user";
+      home = cfg.dataDir;
+      isSystemUser = true;
+    };
+
+    users.groups.${cfg.group} = {};
+
+  };
+}
diff --git a/nixos/modules/services/networking/websockify.nix b/nixos/modules/services/networking/websockify.nix
index d9177df65bd..27cb47be12f 100644
--- a/nixos/modules/services/networking/websockify.nix
+++ b/nixos/modules/services/networking/websockify.nix
@@ -5,12 +5,12 @@ with lib;
 let cfg = config.services.networking.websockify; in {
   options = {
     services.networking.websockify = {
-      enable = mkOption {  
+      enable = mkOption {
         description = "Whether to enable websockify to forward websocket connections to TCP connections.";
 
-        default = false;   
+        default = false;
 
-        type = types.bool; 
+        type = types.bool;
       };
 
       sslCert = mkOption {
diff --git a/nixos/modules/services/networking/wg-quick.nix b/nixos/modules/services/networking/wg-quick.nix
index ff1bdeed9f4..02fe40a22a1 100644
--- a/nixos/modules/services/networking/wg-quick.nix
+++ b/nixos/modules/services/networking/wg-quick.nix
@@ -29,7 +29,7 @@ let
         type = with types; nullOr str;
         default = null;
         description = ''
-          Base64 private key generated by wg genkey.
+          Base64 private key generated by <command>wg genkey</command>.
 
           Warning: Consider using privateKeyFile instead if you do not
           want to store the key in the world-readable Nix store.
@@ -41,7 +41,7 @@ let
         type = with types; nullOr str;
         default = null;
         description = ''
-          Private key file as generated by wg genkey.
+          Private key file as generated by <command>wg genkey</command>.
         '';
       };
 
@@ -106,9 +106,9 @@ let
         description = ''
           The kernel routing table to add this interface's
           associated routes to. Setting this is useful for e.g. policy routing
-          ("ip rule") or virtual routing and forwarding ("ip vrf"). Both numeric
-          table IDs and table names (/etc/rt_tables) can be used. Defaults to
-          "main".
+          ("ip rule") or virtual routing and forwarding ("ip vrf"). Both
+          numeric table IDs and table names (/etc/rt_tables) can be used.
+          Defaults to "main".
         '';
       };
 
@@ -139,7 +139,7 @@ let
       publicKey = mkOption {
         example = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
         type = types.str;
-        description = "The base64 public key the peer.";
+        description = "The base64 public key to the peer.";
       };
 
       presharedKey = mkOption {
@@ -147,8 +147,8 @@ let
         example = "rVXs/Ni9tu3oDBLS4hOyAUAa1qTWVA3loR8eL20os3I=";
         type = with types; nullOr str;
         description = ''
-          Base64 preshared key generated by wg genpsk. Optional,
-          and may be omitted. This option adds an additional layer of
+          Base64 preshared key generated by <command>wg genpsk</command>.
+          Optional, and may be omitted. This option adds an additional layer of
           symmetric-key cryptography to be mixed into the already existing
           public-key cryptography, for post-quantum resistance.
 
@@ -162,8 +162,8 @@ let
         example = "/private/wireguard_psk";
         type = with types; nullOr str;
         description = ''
-          File pointing to preshared key as generated by wg pensk. Optional,
-          and may be omitted. This option adds an additional layer of
+          File pointing to preshared key as generated by <command>wg genpsk</command>.
+          Optional, and may be omitted. This option adds an additional layer of
           symmetric-key cryptography to be mixed into the already existing
           public-key cryptography, for post-quantum resistance.
         '';
diff --git a/nixos/modules/services/networking/wireguard.nix b/nixos/modules/services/networking/wireguard.nix
index e8f83f6dd8b..e07020349cf 100644
--- a/nixos/modules/services/networking/wireguard.nix
+++ b/nixos/modules/services/networking/wireguard.nix
@@ -91,11 +91,13 @@ let
       table = mkOption {
         default = "main";
         type = types.str;
-        description = ''The kernel routing table to add this interface's
-        associated routes to. Setting this is useful for e.g. policy routing
-        ("ip rule") or virtual routing and forwarding ("ip vrf"). Both numeric
-        table IDs and table names (/etc/rt_tables) can be used. Defaults to
-        "main".'';
+        description = ''
+          The kernel routing table to add this interface's
+          associated routes to. Setting this is useful for e.g. policy routing
+          ("ip rule") or virtual routing and forwarding ("ip vrf"). Both
+          numeric table IDs and table names (/etc/rt_tables) can be used.
+          Defaults to "main".
+        '';
       };
 
       peers = mkOption {
@@ -174,7 +176,7 @@ let
         example = "/private/wireguard_psk";
         type = with types; nullOr str;
         description = ''
-          File pointing to preshared key as generated by <command>wg pensk</command>.
+          File pointing to preshared key as generated by <command>wg genpsk</command>.
           Optional, and may be omitted. This option adds an additional layer of
           symmetric-key cryptography to be mixed into the already existing
           public-key cryptography, for post-quantum resistance.
@@ -217,7 +219,6 @@ let
 
   };
 
-
   generatePathUnit = name: values:
     assert (values.privateKey == null);
     assert (values.privateKeyFile != null);
diff --git a/nixos/modules/services/networking/wpa_supplicant.nix b/nixos/modules/services/networking/wpa_supplicant.nix
index de0f11595a9..08a17d20ed7 100644
--- a/nixos/modules/services/networking/wpa_supplicant.nix
+++ b/nixos/modules/services/networking/wpa_supplicant.nix
@@ -4,7 +4,7 @@ with lib;
 
 let
   cfg = config.networking.wireless;
-  configFile = if cfg.networks != {} then pkgs.writeText "wpa_supplicant.conf" ''
+  configFile = if cfg.networks != {} || cfg.extraConfig != "" || cfg.userControlled.enable then pkgs.writeText "wpa_supplicant.conf" ''
     ${optionalString cfg.userControlled.enable ''
       ctrl_interface=DIR=/run/wpa_supplicant GROUP=${cfg.userControlled.group}
       update_config=1''}
@@ -253,12 +253,12 @@ in {
     };
 
     powerManagement.resumeCommands = ''
-      ${config.systemd.package}/bin/systemctl try-restart wpa_supplicant
+      /run/current-system/systemd/bin/systemctl try-restart wpa_supplicant
     '';
 
     # Restart wpa_supplicant when a wlan device appears or disappears.
     services.udev.extraRules = ''
-      ACTION=="add|remove", SUBSYSTEM=="net", ENV{DEVTYPE}=="wlan", RUN+="${config.systemd.package}/bin/systemctl try-restart wpa_supplicant.service"
+      ACTION=="add|remove", SUBSYSTEM=="net", ENV{DEVTYPE}=="wlan", RUN+="/run/current-system/systemd/bin/systemctl try-restart wpa_supplicant.service"
     '';
   };
 
diff --git a/nixos/modules/services/networking/xandikos.nix b/nixos/modules/services/networking/xandikos.nix
index 87c029156b9..3c40bb956f5 100644
--- a/nixos/modules/services/networking/xandikos.nix
+++ b/nixos/modules/services/networking/xandikos.nix
@@ -90,7 +90,7 @@ in
   config = mkIf cfg.enable (
     mkMerge [
       {
-        meta.maintainers = [ lib.maintainers."0x4A6F" ];
+        meta.maintainers = with lib.maintainers; [ _0x4A6F ];
 
         systemd.services.xandikos = {
           description = "A Simple Calendar and Contact Server";
@@ -122,7 +122,7 @@ in
             ExecStart = ''
               ${cfg.package}/bin/xandikos \
                 --directory /var/lib/xandikos \
-                --listen_address ${cfg.address} \
+                --listen-address ${cfg.address} \
                 --port ${toString cfg.port} \
                 --route-prefix ${cfg.routePrefix} \
                 ${lib.concatStringsSep " " cfg.extraOptions}
diff --git a/nixos/modules/services/networking/yggdrasil.nix b/nixos/modules/services/networking/yggdrasil.nix
index ecd1406b483..a71c635c9f6 100644
--- a/nixos/modules/services/networking/yggdrasil.nix
+++ b/nixos/modules/services/networking/yggdrasil.nix
@@ -1,55 +1,17 @@
 { config, lib, pkgs, ... }:
 with lib;
 let
+  keysPath = "/var/lib/yggdrasil/keys.json";
+
   cfg = config.services.yggdrasil;
-  configProvided = (cfg.config != {});
-  configAsFile = (if configProvided then
-                   toString (pkgs.writeTextFile {
-                     name = "yggdrasil-conf";
-                     text = builtins.toJSON cfg.config;
-                   })
-                   else null);
-  configFileProvided = (cfg.configFile != null);
-  generateConfig = (
-    if configProvided && configFileProvided then
-      "${pkgs.jq}/bin/jq -s add ${configAsFile} ${cfg.configFile}"
-    else if configProvided then
-      "cat ${configAsFile}"
-    else if configFileProvided then
-      "cat ${cfg.configFile}"
-    else
-      "${cfg.package}/bin/yggdrasil -genconf"
-  );
+  configProvided = cfg.config != { };
+  configFileProvided = cfg.configFile != null;
 
 in {
   options = with types; {
     services.yggdrasil = {
       enable = mkEnableOption "the yggdrasil system service";
 
-      configFile = mkOption {
-        type =  nullOr str;
-        default = null;
-        example = "/run/keys/yggdrasil.conf";
-        description = ''
-          A file which contains JSON configuration for yggdrasil.
-
-          You do not have to supply a complete configuration, as
-          yggdrasil will use default values for anything which is
-          omitted.  If the encryption and signing keys are omitted,
-          yggdrasil will generate new ones each time the service is
-          started, resulting in a random IPv6 address on the yggdrasil
-          network each time.
-
-          If both this option and <option>config</option> are
-          supplied, they will be combined, with values from
-          <option>config</option> taking precedence.
-
-          You can use the command <code>nix-shell -p yggdrasil --run
-          "yggdrasil -genconf -json"</code> to generate a default
-          JSON configuration.
-        '';
-      };
-
       config = mkOption {
         type = attrs;
         default = {};
@@ -66,16 +28,21 @@ in {
           Configuration for yggdrasil, as a Nix attribute set.
 
           Warning: this is stored in the WORLD-READABLE Nix store!
-          Therefore, it is not appropriate for private keys.  If you
-          do not specify the keys, yggdrasil will generate a new set
-          each time the service is started, creating a random IPv6
-          address on the yggdrasil network each time.
+          Therefore, it is not appropriate for private keys. If you
+          wish to specify the keys, use <option>configFile</option>.
+
+          If the <option>persistentKeys</option> is enabled then the
+          keys that are generated during activation will override
+          those in <option>config</option> or
+          <option>configFile</option>.
 
-          If you wish to specify the keys, use
-          <option>configFile</option>.  If both
-          <option>configFile</option> and <option>config</option> are
-          supplied, they will be combined, with values from
-          <option>config</option> taking precedence.
+          If no keys are specified then ephemeral keys are generated
+          and the Yggdrasil interface will have a random IPv6 address
+          each time the service is started, this is the default.
+
+          If both <option>configFile</option> and <option>config</option>
+          are supplied, they will be combined, with values from
+          <option>configFile</option> taking precedence.
 
           You can use the command <code>nix-shell -p yggdrasil --run
           "yggdrasil -genconf"</code> to generate default
@@ -83,12 +50,21 @@ in {
         '';
       };
 
+      configFile = mkOption {
+        type = nullOr path;
+        default = null;
+        example = "/run/keys/yggdrasil.conf";
+        description = ''
+          A file which contains JSON configuration for yggdrasil.
+          See the <option>config</option> option for more information.
+        '';
+      };
+
       group = mkOption {
         type = types.str;
         default = "root";
         example = "wheel";
-        description =
-          "Group to grant acces to the Yggdrasil control socket.";
+        description = "Group to grant acces to the Yggdrasil control socket.";
       };
 
       openMulticastPort = mkOption {
@@ -126,37 +102,64 @@ in {
         defaultText = "pkgs.yggdrasil";
         description = "Yggdrasil package to use.";
       };
+
+      persistentKeys = mkEnableOption ''
+        If enabled then keys will be generated once and Yggdrasil
+        will retain the same IPv6 address when the service is
+        restarted. Keys are stored at ${keysPath}.
+      '';
+
     };
   };
 
-  config = mkIf cfg.enable {
-    assertions = [
-      { assertion = config.networking.enableIPv6;
-        message = "networking.enableIPv6 must be true for yggdrasil to work";
-      }
-    ];
+  config = mkIf cfg.enable (let binYggdrasil = cfg.package + "/bin/yggdrasil";
+  in {
+    assertions = [{
+      assertion = config.networking.enableIPv6;
+      message = "networking.enableIPv6 must be true for yggdrasil to work";
+    }];
+
+    system.activationScripts.yggdrasil = mkIf cfg.persistentKeys ''
+      if [ ! -e ${keysPath} ]
+      then
+        mkdir -p ${builtins.dirOf keysPath}
+        ${binYggdrasil} -genconf -json \
+          | ${pkgs.jq}/bin/jq \
+              'to_entries|map(select(.key|endswith("Key")))|from_entries' \
+          > ${keysPath}
+        chmod 600 ${keysPath}
+      fi
+    '';
 
     systemd.services.yggdrasil = {
       description = "Yggdrasil Network Service";
-      path = [ cfg.package ] ++ optional (configProvided && configFileProvided) pkgs.jq;
       bindsTo = [ "network-online.target" ];
       after = [ "network-online.target" ];
       wantedBy = [ "multi-user.target" ];
 
-      preStart = ''
-        ${generateConfig} | yggdrasil -normaliseconf -useconf > /run/yggdrasil/yggdrasil.conf
-      '';
+      preStart =
+        (if configProvided || configFileProvided || cfg.persistentKeys then
+          "echo "
+
+          + (lib.optionalString configProvided
+            "'${builtins.toJSON cfg.config}'")
+          + (lib.optionalString configFileProvided "$(cat ${cfg.configFile})")
+          + (lib.optionalString cfg.persistentKeys "$(cat ${keysPath})")
+          + " | ${pkgs.jq}/bin/jq -s add | ${binYggdrasil} -normaliseconf -useconf"
+        else
+          "${binYggdrasil} -genconf") + " > /run/yggdrasil/yggdrasil.conf";
 
       serviceConfig = {
-        ExecStart = "${cfg.package}/bin/yggdrasil -useconffile /run/yggdrasil/yggdrasil.conf";
+        ExecStart =
+          "${binYggdrasil} -useconffile /run/yggdrasil/yggdrasil.conf";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
         Restart = "always";
 
         Group = cfg.group;
         RuntimeDirectory = "yggdrasil";
         RuntimeDirectoryMode = "0750";
-        BindReadOnlyPaths = mkIf configFileProvided
-          [ "${cfg.configFile}" ];
+        BindReadOnlyPaths = lib.optional configFileProvided cfg.configFile
+          ++ lib.optional cfg.persistentKeys keysPath;
 
         # TODO: as of yggdrasil 0.3.8 and systemd 243, yggdrasil fails
         # to set up the network adapter when DynamicUser is set.  See
@@ -191,6 +194,9 @@ in {
 
     # Make yggdrasilctl available on the command line.
     environment.systemPackages = [ cfg.package ];
+  });
+  meta = {
+    doc = ./yggdrasil.xml;
+    maintainers = with lib.maintainers; [ gazally ehmry ];
   };
-  meta.maintainers = with lib.maintainers; [ gazally ];
 }
diff --git a/nixos/modules/services/networking/yggdrasil.xml b/nixos/modules/services/networking/yggdrasil.xml
new file mode 100644
index 00000000000..c012cd4a929
--- /dev/null
+++ b/nixos/modules/services/networking/yggdrasil.xml
@@ -0,0 +1,157 @@
+<?xml version="1.0"?>
+<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xi="http://www.w3.org/2001/XInclude" version="5.0" xml:id="module-services-networking-yggdrasil">
+  <title>Yggdrasil</title>
+  <para>
+    <emphasis>Source:</emphasis>
+    <filename>modules/services/networking/yggdrasil/default.nix</filename>
+  </para>
+  <para>
+    <emphasis>Upstream documentation:</emphasis>
+    <link xlink:href="https://yggdrasil-network.github.io/"/>
+  </para>
+  <para>
+Yggdrasil is an early-stage implementation of a fully end-to-end encrypted,
+self-arranging IPv6 network.
+</para>
+  <section xml:id="module-services-networking-yggdrasil-configuration">
+    <title>Configuration</title>
+    <section xml:id="module-services-networking-yggdrasil-configuration-simple">
+      <title>Simple ephemeral node</title>
+      <para>
+An annotated example of a simple configuration:
+<programlisting>
+{
+  services.yggdrasil = {
+    enable = true;
+    persistentKeys = false;
+      # The NixOS module will generate new keys and a new IPv6 address each time
+      # it is started if persistentKeys is not enabled.
+
+    config = {
+      Peers = [
+        # Yggdrasil will automatically connect and "peer" with other nodes it
+        # discovers via link-local multicast annoucements. Unless this is the
+        # case (it probably isn't) a node needs peers within the existing
+        # network that it can tunnel to.
+        "tcp://1.2.3.4:1024"
+        "tcp://1.2.3.5:1024"
+        # Public peers can be found at
+        # https://github.com/yggdrasil-network/public-peers
+      ];
+    };
+  };
+}
+</programlisting>
+   </para>
+    </section>
+    <section xml:id="module-services-networking-yggdrasil-configuration-prefix">
+      <title>Persistent node with prefix</title>
+      <para>
+A node with a fixed address that announces a prefix:
+<programlisting>
+let
+  address = "210:5217:69c0:9afc:1b95:b9f:8718:c3d2";
+  prefix = "310:5217:69c0:9afc";
+  # taken from the output of "yggdrasilctl getself".
+in {
+
+  services.yggdrasil = {
+    enable = true;
+    persistentKeys = true; # Maintain a fixed public key and IPv6 address.
+    config = {
+      Peers = [ "tcp://1.2.3.4:1024" "tcp://1.2.3.5:1024" ];
+      NodeInfo = {
+        # This information is visible to the network.
+        name = config.networking.hostName;
+        location = "The North Pole";
+      };
+    };
+  };
+
+  boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = 1;
+    # Forward traffic under the prefix.
+
+  networking.interfaces.${eth0}.ipv6.addresses = [{
+    # Set a 300::/8 address on the local physical device.
+    address = prefix + "::1";
+    prefixLength = 64;
+  }];
+
+  services.radvd = {
+    # Annouce the 300::/8 prefix to eth0.
+    enable = true;
+    config = ''
+      interface eth0
+      {
+        AdvSendAdvert on;
+        AdvDefaultLifetime 0;
+        prefix ${prefix}::/64 {
+          AdvOnLink on;
+          AdvAutonomous on;
+        };
+        route 200::/8 {};
+      };
+    '';
+  };
+}
+</programlisting>
+  </para>
+    </section>
+    <section xml:id="module-services-networking-yggdrasil-configuration-container">
+      <title>Yggdrasil attached Container</title>
+      <para>
+A NixOS container attached to the Yggdrasil network via a node running on the
+host:
+        <programlisting>
+let
+  yggPrefix64 = "310:5217:69c0:9afc";
+    # Again, taken from the output of "yggdrasilctl getself".
+in
+{
+  boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = 1;
+  # Enable IPv6 forwarding.
+
+  networking = {
+    bridges.br0.interfaces = [ ];
+    # A bridge only to containers&#x2026;
+
+    interfaces.br0 = {
+      # &#x2026; configured with a prefix address.
+      ipv6.addresses = [{
+        address = "${yggPrefix64}::1";
+        prefixLength = 64;
+      }];
+    };
+  };
+
+  containers.foo = {
+    autoStart = true;
+    privateNetwork = true;
+    hostBridge = "br0";
+    # Attach the container to the bridge only.
+    config = { config, pkgs, ... }: {
+      networking.interfaces.eth0.ipv6 = {
+        addresses = [{
+          # Configure a prefix address.
+          address = "${yggPrefix64}::2";
+          prefixLength = 64;
+        }];
+        routes = [{
+          # Configure the prefix route.
+          address = "200::";
+          prefixLength = 7;
+          via = "${yggPrefix64}::1";
+        }];
+      };
+
+      services.httpd.enable = true;
+      networking.firewall.allowedTCPPorts = [ 80 ];
+    };
+  };
+
+}
+</programlisting>
+      </para>
+    </section>
+  </section>
+</chapter>
diff --git a/nixos/modules/services/scheduling/chronos.nix b/nixos/modules/services/scheduling/chronos.nix
deleted file mode 100644
index 9a8ed4c09ac..00000000000
--- a/nixos/modules/services/scheduling/chronos.nix
+++ /dev/null
@@ -1,54 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-  cfg = config.services.chronos;
-
-in {
-
-  ###### interface
-
-  options.services.chronos = {
-    enable = mkOption {
-      description = "Whether to enable graphite web frontend.";
-      default = false;
-      type = types.bool;
-    };
-
-    httpPort = mkOption {
-      description = "Chronos listening port";
-      default = 4400;
-      type = types.int;
-    };
-
-    master = mkOption {
-      description = "Chronos mesos master zookeeper address";
-      default = "zk://${head cfg.zookeeperHosts}/mesos";
-      type = types.str;
-    };
-
-    zookeeperHosts = mkOption {
-      description = "Chronos mesos zookepper addresses";
-      default = [ "localhost:2181" ];
-      type = types.listOf types.str;
-    };
-  };
-
-  ###### implementation
-
-  config = mkIf cfg.enable {
-    systemd.services.chronos = {
-      description = "Chronos Service";
-      wantedBy = [ "multi-user.target" ];
-      after = [ "network.target" "zookeeper.service" ];
-
-      serviceConfig = {
-        ExecStart = "${pkgs.chronos}/bin/chronos --master ${cfg.master} --zk_hosts ${concatStringsSep "," cfg.zookeeperHosts} --http_port ${toString cfg.httpPort}";
-        User = "chronos";
-      };
-    };
-
-    users.users.chronos.uid = config.ids.uids.chronos;
-  };
-}
diff --git a/nixos/modules/services/scheduling/marathon.nix b/nixos/modules/services/scheduling/marathon.nix
deleted file mode 100644
index 2e0d20c64b2..00000000000
--- a/nixos/modules/services/scheduling/marathon.nix
+++ /dev/null
@@ -1,98 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-
-  cfg = config.services.marathon;
-
-in {
-
-  ###### interface
-
-  options.services.marathon = {
-    enable = mkOption {
-      type = types.bool;
-      default = false;
-      description = ''
-	Whether to enable the marathon mesos framework.
-      '';
-    };
-
-    master = mkOption {
-      type = types.str;
-      default = "zk://${concatStringsSep "," cfg.zookeeperHosts}/mesos";
-      example = "zk://1.2.3.4:2181,2.3.4.5:2181,3.4.5.6:2181/mesos";
-      description = ''
-	Mesos master address. See <link xlink:href="https://mesosphere.github.io/marathon/docs/"/> for details.
-      '';
-    };
-
-    zookeeperHosts = mkOption {
-      type = types.listOf types.str;
-      default = [ "localhost:2181" ];
-      example = [ "1.2.3.4:2181" "2.3.4.5:2181" "3.4.5.6:2181" ];
-      description = ''
-	ZooKeeper hosts' addresses.
-      '';
-    };
-
-    user = mkOption {
-      type = types.str;
-      default = "marathon";
-      example = "root";
-      description = ''
-	The user that the Marathon framework will be launched as. If the user doesn't exist it will be created.
-	If you want to run apps that require root access or you want to launch apps using arbitrary users, that
-	is using the `--mesos_user` flag then you need to change this to `root`.
-      '';
-    };
-
-    httpPort = mkOption {
-      type = types.int;
-      default = 8080;
-      description = ''
-	Marathon listening port for HTTP connections.
-      '';
-    };
-
-    extraCmdLineOptions = mkOption {
-      type = types.listOf types.str;
-      default = [ ];
-      example = [ "--https_port=8443" "--zk_timeout=10000" "--marathon_store_timeout=2000" ];
-      description = ''
-	Extra command line options to pass to Marathon.
-	See <link xlink:href="https://mesosphere.github.io/marathon/docs/command-line-flags.html"/> for all possible flags.
-      '';
-    };
-
-    environment = mkOption {
-      default = { };
-      type = types.attrs;
-      example = { JAVA_OPTS = "-Xmx512m"; MESOSPHERE_HTTP_CREDENTIALS = "username:password"; };
-      description = ''
-	Environment variables passed to Marathon.
-      '';
-    };
-  };
-
-  ###### implementation
-
-  config = mkIf cfg.enable {
-    systemd.services.marathon = {
-      description = "Marathon Service";
-      environment = cfg.environment;
-      wantedBy = [ "multi-user.target" ];
-      after = [ "network.target" "zookeeper.service" "mesos-master.service" "mesos-slave.service" ];
-
-      serviceConfig = {
-        ExecStart = "${pkgs.marathon}/bin/marathon --master ${cfg.master} --zk zk://${concatStringsSep "," cfg.zookeeperHosts}/marathon --http_port ${toString cfg.httpPort} ${concatStringsSep " " cfg.extraCmdLineOptions}";
-        User = cfg.user;
-        Restart = "always";
-        RestartSec = "2";
-      };
-    };
-
-    users.users.${cfg.user}.isSystemUser = true;
-  };
-}
diff --git a/nixos/modules/services/security/fprintd.nix b/nixos/modules/services/security/fprintd.nix
index 8ece1ca1901..cbac4ef05b8 100644
--- a/nixos/modules/services/security/fprintd.nix
+++ b/nixos/modules/services/security/fprintd.nix
@@ -29,7 +29,6 @@ in
         type = types.package;
         default = pkgs.fprintd;
         defaultText = "pkgs.fprintd";
-        example = "pkgs.fprintd-thinkpad";
         description = ''
           fprintd package to use.
         '';
diff --git a/nixos/modules/services/security/haveged.nix b/nixos/modules/services/security/haveged.nix
index eca52918881..22ece188344 100644
--- a/nixos/modules/services/security/haveged.nix
+++ b/nixos/modules/services/security/haveged.nix
@@ -21,11 +21,11 @@ in
         type = types.bool;
         default = false;
         description = ''
-          Whether to enable to haveged entropy daemon, which refills 
+          Whether to enable to haveged entropy daemon, which refills
           /dev/random when low.
         '';
       };
-      
+
       refill_threshold = mkOption {
         type = types.int;
         default = 1024;
@@ -34,16 +34,16 @@ in
           haveged should refill the entropy pool.
         '';
       };
-      
+
     };
-    
+
   };
-  
-  
+
+
   ###### implementation
-  
+
   config = mkIf cfg.enable {
-  
+
     systemd.services.haveged =
       { description = "Entropy Harvesting Daemon";
         unitConfig.Documentation = "man:haveged(8)";
@@ -63,5 +63,5 @@ in
       };
 
   };
-  
+
 }
diff --git a/nixos/modules/services/security/nginx-sso.nix b/nixos/modules/services/security/nginx-sso.nix
index d792f90abe6..50d250fc4d7 100644
--- a/nixos/modules/services/security/nginx-sso.nix
+++ b/nixos/modules/services/security/nginx-sso.nix
@@ -4,12 +4,21 @@ with lib;
 
 let
   cfg = config.services.nginx.sso;
-  pkg = getBin pkgs.nginx-sso;
+  pkg = getBin cfg.package;
   configYml = pkgs.writeText "nginx-sso.yml" (builtins.toJSON cfg.configuration);
 in {
   options.services.nginx.sso = {
     enable = mkEnableOption "nginx-sso service";
 
+    package = mkOption {
+      type = types.package;
+      default = pkgs.nginx-sso;
+      defaultText = "pkgs.nginx-sso";
+      description = ''
+        The nginx-sso package that should be used.
+      '';
+    };
+
     configuration = mkOption {
       type = types.attrsOf types.unspecified;
       default = {};
diff --git a/nixos/modules/services/security/oauth2_proxy.nix b/nixos/modules/services/security/oauth2_proxy.nix
index d5c5437329e..2f9e94bd77b 100644
--- a/nixos/modules/services/security/oauth2_proxy.nix
+++ b/nixos/modules/services/security/oauth2_proxy.nix
@@ -99,7 +99,7 @@ in
 
     ##############################################
     # PROVIDER configuration
-    # Taken from: https://github.com/pusher/oauth2_proxy/blob/master/providers/providers.go
+    # Taken from: https://github.com/oauth2-proxy/oauth2-proxy/blob/master/providers/providers.go
     provider = mkOption {
       type = types.enum [
         "google"
@@ -346,7 +346,9 @@ in
         type = types.nullOr types.str;
         default = null;
         description = ''
-          An optional cookie domain to force cookies to.
+          Optional cookie domains to force cookies to (ie: `.yourcompany.com`).
+          The longest domain matching the request's host will be used (or the shortest
+          cookie domain if there is no match).
         '';
         example = ".yourcompany.com";
       };
@@ -537,7 +539,7 @@ in
     extraConfig = mkOption {
       default = {};
       description = ''
-        Extra config to pass to oauth2_proxy.
+        Extra config to pass to oauth2-proxy.
       '';
     };
 
@@ -545,7 +547,7 @@ in
       type = types.nullOr types.path;
       default = null;
       description = ''
-        oauth2_proxy allows passing sensitive configuration via environment variables.
+        oauth2-proxy allows passing sensitive configuration via environment variables.
         Make a file that contains lines like
         OAUTH2_PROXY_CLIENT_SECRET=asdfasdfasdf.apps.googleuserscontent.com
         and specify the path here.
@@ -577,7 +579,7 @@ in
       serviceConfig = {
         User = "oauth2_proxy";
         Restart = "always";
-        ExecStart = "${cfg.package}/bin/oauth2_proxy ${configString}";
+        ExecStart = "${cfg.package}/bin/oauth2-proxy ${configString}";
         EnvironmentFile = mkIf (cfg.keyFile != null) cfg.keyFile;
       };
     };
diff --git a/nixos/modules/services/security/physlock.nix b/nixos/modules/services/security/physlock.nix
index 61bcd84f2e6..690eb70079d 100644
--- a/nixos/modules/services/security/physlock.nix
+++ b/nixos/modules/services/security/physlock.nix
@@ -107,6 +107,7 @@ in
                 ++ cfg.lockOn.extraTargets;
         before   = optional cfg.lockOn.suspend   "systemd-suspend.service"
                 ++ optional cfg.lockOn.hibernate "systemd-hibernate.service"
+                ++ optional (cfg.lockOn.hibernate || cfg.lockOn.suspend) "systemd-suspend-then-hibernate.service"
                 ++ cfg.lockOn.extraTargets;
         serviceConfig = {
           Type = "forking";
diff --git a/nixos/modules/services/security/privacyidea.nix b/nixos/modules/services/security/privacyidea.nix
new file mode 100644
index 00000000000..c2988858e56
--- /dev/null
+++ b/nixos/modules/services/security/privacyidea.nix
@@ -0,0 +1,278 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.privacyidea;
+
+  uwsgi = pkgs.uwsgi.override { plugins = [ "python3" ]; };
+  python = uwsgi.python3;
+  penv = python.withPackages (ps: [ ps.privacyidea ]);
+  logCfg = pkgs.writeText "privacyidea-log.cfg" ''
+    [formatters]
+    keys=detail
+
+    [handlers]
+    keys=stream
+
+    [formatter_detail]
+    class=privacyidea.lib.log.SecureFormatter
+    format=[%(asctime)s][%(process)d][%(thread)d][%(levelname)s][%(name)s:%(lineno)d] %(message)s
+
+    [handler_stream]
+    class=StreamHandler
+    level=NOTSET
+    formatter=detail
+    args=(sys.stdout,)
+
+    [loggers]
+    keys=root,privacyidea
+
+    [logger_privacyidea]
+    handlers=stream
+    qualname=privacyidea
+    level=INFO
+
+    [logger_root]
+    handlers=stream
+    level=ERROR
+  '';
+
+  piCfgFile = pkgs.writeText "privacyidea.cfg" ''
+    SUPERUSER_REALM = [ '${concatStringsSep "', '" cfg.superuserRealm}' ]
+    SQLALCHEMY_DATABASE_URI = 'postgresql:///privacyidea'
+    SECRET_KEY = '${cfg.secretKey}'
+    PI_PEPPER = '${cfg.pepper}'
+    PI_ENCFILE = '${cfg.encFile}'
+    PI_AUDIT_KEY_PRIVATE = '${cfg.auditKeyPrivate}'
+    PI_AUDIT_KEY_PUBLIC = '${cfg.auditKeyPublic}'
+    PI_LOGCONFIG = '${logCfg}'
+    ${cfg.extraConfig}
+  '';
+
+in
+
+{
+  options = {
+    services.privacyidea = {
+      enable = mkEnableOption "PrivacyIDEA";
+
+      stateDir = mkOption {
+        type = types.str;
+        default = "/var/lib/privacyidea";
+        description = ''
+          Directory where all PrivacyIDEA files will be placed by default.
+        '';
+      };
+
+      superuserRealm = mkOption {
+        type = types.listOf types.str;
+        default = [ "super" "administrators" ];
+        description = ''
+          The realm where users are allowed to login as administrators.
+        '';
+      };
+
+      secretKey = mkOption {
+        type = types.str;
+        example = "t0p s3cr3t";
+        description = ''
+          This is used to encrypt the auth_token.
+        '';
+      };
+
+      pepper = mkOption {
+        type = types.str;
+        example = "Never know...";
+        description = ''
+          This is used to encrypt the admin passwords.
+        '';
+      };
+
+      encFile = mkOption {
+        type = types.str;
+        default = "${cfg.stateDir}/enckey";
+        description = ''
+          This is used to encrypt the token data and token passwords
+        '';
+      };
+
+      auditKeyPrivate = mkOption {
+        type = types.str;
+        default = "${cfg.stateDir}/private.pem";
+        description = ''
+          Private Key for signing the audit log.
+        '';
+      };
+
+      auditKeyPublic = mkOption {
+        type = types.str;
+        default = "${cfg.stateDir}/public.pem";
+        description = ''
+          Public key for checking signatures of the audit log.
+        '';
+      };
+
+      adminPasswordFile = mkOption {
+        type = types.path;
+        description = "File containing password for the admin user";
+      };
+
+      adminEmail = mkOption {
+        type = types.str;
+        example = "admin@example.com";
+        description = "Mail address for the admin user";
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra configuration options for pi.cfg.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "privacyidea";
+        description = "User account under which PrivacyIDEA runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "privacyidea";
+        description = "Group account under which PrivacyIDEA runs.";
+      };
+
+      ldap-proxy = {
+        enable = mkEnableOption "PrivacyIDEA LDAP Proxy";
+
+        configFile = mkOption {
+          type = types.path;
+          default = "";
+          description = ''
+            Path to PrivacyIDEA LDAP Proxy configuration (proxy.ini).
+          '';
+        };
+
+        user = mkOption {
+          type = types.str;
+          default = "pi-ldap-proxy";
+          description = "User account under which PrivacyIDEA LDAP proxy runs.";
+        };
+
+        group = mkOption {
+          type = types.str;
+          default = "pi-ldap-proxy";
+          description = "Group account under which PrivacyIDEA LDAP proxy runs.";
+        };
+      };
+    };
+  };
+
+  config = mkMerge [
+
+    (mkIf cfg.enable {
+
+      environment.systemPackages = [ python.pkgs.privacyidea ];
+
+      services.postgresql.enable = mkDefault true;
+
+      systemd.services.privacyidea = let
+        piuwsgi = pkgs.writeText "uwsgi.json" (builtins.toJSON {
+          uwsgi = {
+            plugins = [ "python3" ];
+            pythonpath = "${penv}/${uwsgi.python3.sitePackages}";
+            socket = "/run/privacyidea/socket";
+            uid = cfg.user;
+            gid = cfg.group;
+            chmod-socket = 770;
+            chown-socket = "${cfg.user}:nginx";
+            chdir = cfg.stateDir;
+            wsgi-file = "${penv}/etc/privacyidea/privacyideaapp.wsgi";
+            processes = 4;
+            harakiri = 60;
+            reload-mercy = 8;
+            stats = "/run/privacyidea/stats.socket";
+            max-requests = 2000;
+            limit-as = 1024;
+            reload-on-as = 512;
+            reload-on-rss = 256;
+            no-orphans = true;
+            vacuum = true;
+          };
+        });
+      in {
+        wantedBy = [ "multi-user.target" ];
+        after = [ "postgresql.service" ];
+        path = with pkgs; [ openssl ];
+        environment.PRIVACYIDEA_CONFIGFILE = piCfgFile;
+        preStart = let
+          pi-manage = "${pkgs.sudo}/bin/sudo -u privacyidea -HE ${penv}/bin/pi-manage";
+          pgsu = config.services.postgresql.superUser;
+          psql = config.services.postgresql.package;
+        in ''
+          mkdir -p ${cfg.stateDir} /run/privacyidea
+          chown ${cfg.user}:${cfg.group} -R ${cfg.stateDir} /run/privacyidea
+          if ! test -e "${cfg.stateDir}/db-created"; then
+            ${pkgs.sudo}/bin/sudo -u ${pgsu} ${psql}/bin/createuser --no-superuser --no-createdb --no-createrole ${cfg.user}
+            ${pkgs.sudo}/bin/sudo -u ${pgsu} ${psql}/bin/createdb --owner ${cfg.user} privacyidea
+            ${pi-manage} create_enckey
+            ${pi-manage} create_audit_keys
+            ${pi-manage} createdb
+            ${pi-manage} admin add admin -e ${cfg.adminEmail} -p "$(cat ${cfg.adminPasswordFile})"
+            ${pi-manage} db stamp head -d ${penv}/lib/privacyidea/migrations
+            touch "${cfg.stateDir}/db-created"
+            chmod g+r "${cfg.stateDir}/enckey" "${cfg.stateDir}/private.pem"
+          fi
+          ${pi-manage} db upgrade -d ${penv}/lib/privacyidea/migrations
+        '';
+        serviceConfig = {
+          Type = "notify";
+          ExecStart = "${uwsgi}/bin/uwsgi --json ${piuwsgi}";
+          ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+          ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID";
+          NotifyAccess = "main";
+          KillSignal = "SIGQUIT";
+        };
+      };
+
+      users.users.privacyidea = mkIf (cfg.user == "privacyidea") {
+        group = cfg.group;
+      };
+
+      users.groups.privacyidea = mkIf (cfg.group == "privacyidea") {};
+    })
+
+    (mkIf cfg.ldap-proxy.enable {
+
+      systemd.services.privacyidea-ldap-proxy = let
+        ldap-proxy-env = pkgs.python2.withPackages (ps: [ ps.privacyidea-ldap-proxy ]);
+      in {
+        description = "privacyIDEA LDAP proxy";
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          User = cfg.ldap-proxy.user;
+          Group = cfg.ldap-proxy.group;
+          ExecStart = ''
+            ${ldap-proxy-env}/bin/twistd \
+              --nodaemon \
+              --pidfile= \
+              -u ${cfg.ldap-proxy.user} \
+              -g ${cfg.ldap-proxy.group} \
+              ldap-proxy \
+              -c ${cfg.ldap-proxy.configFile}
+          '';
+          Restart = "always";
+        };
+      };
+
+      users.users.pi-ldap-proxy = mkIf (cfg.ldap-proxy.user == "pi-ldap-proxy") {
+        group = cfg.ldap-proxy.group;
+      };
+
+      users.groups.pi-ldap-proxy = mkIf (cfg.ldap-proxy.group == "pi-ldap-proxy") {};
+    })
+  ];
+
+}
diff --git a/nixos/modules/services/security/tor.nix b/nixos/modules/services/security/tor.nix
index 18c105b2f57..b33e905c67d 100644
--- a/nixos/modules/services/security/tor.nix
+++ b/nixos/modules/services/security/tor.nix
@@ -159,7 +159,7 @@ in
           type = types.bool;
           default = false;
           description = ''
-            Wheter to enable Tor control socket. Control socket is created
+            Whether to enable Tor control socket. Control socket is created
             in <literal>${torRunDirectory}/control</literal>
           '';
         };
diff --git a/nixos/modules/services/security/usbguard.nix b/nixos/modules/services/security/usbguard.nix
index 4ced5acd9bd..16a90da5231 100644
--- a/nixos/modules/services/security/usbguard.nix
+++ b/nixos/modules/services/security/usbguard.nix
@@ -1,37 +1,39 @@
-{config, lib, pkgs, ... }:
+{ config, lib, pkgs, ... }:
 
 with lib;
-
 let
-
   cfg = config.services.usbguard;
 
   # valid policy options
   policy = (types.enum [ "allow" "block" "reject" "keep" "apply-policy" ]);
 
+  defaultRuleFile = "/var/lib/usbguard/rules.conf";
+
   # decide what file to use for rules
-  ruleFile = if cfg.rules != null then pkgs.writeText "usbguard-rules" cfg.rules else cfg.ruleFile;
+  ruleFile = if cfg.rules != null then pkgs.writeText "usbguard-rules" cfg.rules else defaultRuleFile;
 
   daemonConf = ''
-      # generated by nixos/modules/services/security/usbguard.nix
-      RuleFile=${ruleFile}
-      ImplicitPolicyTarget=${cfg.implictPolicyTarget}
-      PresentDevicePolicy=${cfg.presentDevicePolicy}
-      PresentControllerPolicy=${cfg.presentControllerPolicy}
-      InsertedDevicePolicy=${cfg.insertedDevicePolicy}
-      RestoreControllerDeviceState=${if cfg.restoreControllerDeviceState then "true" else "false"}
-      # this does not seem useful for endusers to change
-      DeviceManagerBackend=uevent
-      IPCAllowedUsers=${concatStringsSep " " cfg.IPCAllowedUsers}
-      IPCAllowedGroups=${concatStringsSep " " cfg.IPCAllowedGroups}
-      IPCAccessControlFiles=${cfg.IPCAccessControlFiles}
-      DeviceRulesWithPort=${if cfg.deviceRulesWithPort then "true" else "false"}
-      AuditFilePath=${cfg.auditFilePath}
-    '';
-
-    daemonConfFile = pkgs.writeText "usbguard-daemon-conf" daemonConf;
-
-in {
+    # generated by nixos/modules/services/security/usbguard.nix
+    RuleFile=${ruleFile}
+    ImplicitPolicyTarget=${cfg.implictPolicyTarget}
+    PresentDevicePolicy=${cfg.presentDevicePolicy}
+    PresentControllerPolicy=${cfg.presentControllerPolicy}
+    InsertedDevicePolicy=${cfg.insertedDevicePolicy}
+    RestoreControllerDeviceState=${if cfg.restoreControllerDeviceState then "true" else "false"}
+    # this does not seem useful for endusers to change
+    DeviceManagerBackend=uevent
+    IPCAllowedUsers=${concatStringsSep " " cfg.IPCAllowedUsers}
+    IPCAllowedGroups=${concatStringsSep " " cfg.IPCAllowedGroups}
+    IPCAccessControlFiles=/var/lib/usbguard/IPCAccessControl.d/
+    DeviceRulesWithPort=${if cfg.deviceRulesWithPort then "true" else "false"}
+    # HACK: that way audit logs still land in the journal
+    AuditFilePath=/dev/null
+  '';
+
+  daemonConfFile = pkgs.writeText "usbguard-daemon-conf" daemonConf;
+
+in
+{
 
   ###### interface
 
@@ -49,22 +51,6 @@ in {
         '';
       };
 
-      ruleFile = mkOption {
-        type = types.path;
-        default = "/var/lib/usbguard/rules.conf";
-        description = ''
-          The USBGuard daemon will use this file to load the policy rule set
-          from it and to write new rules received via the IPC interface.
-
-          Running the command <literal>usbguard generate-policy</literal> as
-          root will generate a config for your currently plugged in devices.
-          For a in depth guide consult the official documentation.
-
-          Setting the <literal>rules</literal> option will ignore the
-          <literal>ruleFile</literal> option.
-        '';
-      };
-
       rules = mkOption {
         type = types.nullOr types.lines;
         default = null;
@@ -72,16 +58,20 @@ in {
           allow with-interface equals { 08:*:* }
         '';
         description = ''
-          The USBGuard daemon will load this policy rule set. Modifying it via
-          the IPC interface won't work if you use this option, since the
-          contents of this option will be written into the nix-store it will be
-          read-only.
+          The USBGuard daemon will load this as the policy rule set.
+          As these rules are NixOS managed they are immutable and can't
+          be changed by the IPC interface.
+
+          If you do not set this option, the USBGuard daemon will load
+          it's policy rule set from <literal>${defaultRuleFile}</literal>.
+          This file can be changed manually or via the IPC interface.
 
-          You can still use <literal> usbguard generate-policy</literal> to
-          generate rules, but you would have to insert them here.
+          Running <literal>usbguard generate-policy</literal> as root will
+          generate a config for your currently plugged in devices.
 
-          Setting the <literal>rules</literal> option will ignore the
-          <literal>ruleFile</literal> option.
+          For more details see <citerefentry>
+          <refentrytitle>usbguard-rules.conf</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry>.
         '';
       };
 
@@ -155,17 +145,6 @@ in {
         '';
       };
 
-      IPCAccessControlFiles = mkOption {
-        type = types.path;
-        default = "/var/lib/usbguard/IPCAccessControl.d/";
-        description = ''
-          The files at this location will be interpreted by the daemon as IPC
-          access control definition files. See the IPC ACCESS CONTROL section
-          in <citerefentry><refentrytitle>usbguard-daemon.conf</refentrytitle>
-          <manvolnum>5</manvolnum></citerefentry> for more details.
-        '';
-      };
-
       deviceRulesWithPort = mkOption {
         type = types.bool;
         default = false;
@@ -173,14 +152,6 @@ in {
           Generate device specific rules including the "via-port" attribute.
         '';
       };
-
-      auditFilePath = mkOption {
-        type = types.path;
-        default = "/var/log/usbguard/usbguard-audit.log";
-        description = ''
-          USBGuard audit events log file path.
-        '';
-      };
     };
   };
 
@@ -197,17 +168,47 @@ in {
       wantedBy = [ "basic.target" ];
       wants = [ "systemd-udevd.service" ];
 
-      # make sure an empty rule file and required directories exist
-      preStart = ''
-        mkdir -p $(dirname "${cfg.ruleFile}") $(dirname "${cfg.auditFilePath}") "${cfg.IPCAccessControlFiles}" \
-          && ([ -f "${cfg.ruleFile}" ] || touch ${cfg.ruleFile})
-      '';
+      # make sure an empty rule file exists
+      preStart = ''[ -f "${ruleFile}" ] || touch ${ruleFile}'';
 
       serviceConfig = {
         Type = "simple";
         ExecStart = ''${cfg.package}/bin/usbguard-daemon -P -k -c ${daemonConfFile}'';
         Restart = "on-failure";
+
+        StateDirectory = [
+          "usbguard"
+          "usbguard/IPCAccessControl.d"
+        ];
+
+        AmbientCapabilities = "";
+        CapabilityBoundingSet = "CAP_CHOWN CAP_FOWNER";
+        DeviceAllow = "/dev/null rw";
+        DevicePolicy = "strict";
+        IPAddressDeny = "any";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateTmp = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectKernelModules = true;
+        ProtectSystem = true;
+        ReadOnlyPaths = "-/";
+        ReadWritePaths = "-/dev/shm -/tmp";
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_NETLINK" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = "@system-service";
+        UMask = "0077";
       };
     };
   };
+  imports = [
+    (mkRemovedOptionModule [ "services" "usbguard" "ruleFile" ] "The usbguard module now uses ${defaultRuleFile} as ruleFile. Alternatively, use services.usbguard.rules to configure rules.")
+    (mkRemovedOptionModule [ "services" "usbguard" "IPCAccessControlFiles" ] "The usbguard module now hardcodes IPCAccessControlFiles to /var/lib/usbguard/IPCAccessControl.d.")
+    (mkRemovedOptionModule [ "services" "usbguard" "auditFilePath" ] "Removed usbguard module audit log files. Audit logs can be found in the systemd journal.")
+  ];
 }
diff --git a/nixos/modules/services/security/yubikey-agent.nix b/nixos/modules/services/security/yubikey-agent.nix
new file mode 100644
index 00000000000..2972c64a364
--- /dev/null
+++ b/nixos/modules/services/security/yubikey-agent.nix
@@ -0,0 +1,60 @@
+# Global configuration for yubikey-agent.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.yubikey-agent;
+
+  # reuse the pinentryFlavor option from the gnupg module
+  pinentryFlavor = config.programs.gnupg.agent.pinentryFlavor;
+in
+{
+  ###### interface
+
+  meta.maintainers = with maintainers; [ philandstuff rawkode ];
+
+  options = {
+
+    services.yubikey-agent = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to start yubikey-agent when you log in.  Also sets
+          SSH_AUTH_SOCK to point at yubikey-agent.
+
+          Note that yubikey-agent will use whatever pinentry is
+          specified in programs.gnupg.agent.pinentryFlavor.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.yubikey-agent;
+        defaultText = "pkgs.yubikey-agent";
+        description = ''
+          The package used for the yubikey-agent daemon.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+    systemd.packages = [ cfg.package ];
+
+    # This overrides the systemd user unit shipped with the
+    # yubikey-agent package
+    systemd.user.services.yubikey-agent = mkIf (pinentryFlavor != null) {
+      path = [ pkgs.pinentry.${pinentryFlavor} ];
+    };
+
+    environment.extraInit = ''
+      if [ -z "$SSH_AUTH_SOCK" -a -n "$XDG_RUNTIME_DIR" ]; then
+        export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/yubikey-agent/yubikey-agent.sock"
+      fi
+    '';
+  };
+}
diff --git a/nixos/modules/services/system/cgmanager.nix b/nixos/modules/services/system/cgmanager.nix
deleted file mode 100644
index d3d57aa7692..00000000000
--- a/nixos/modules/services/system/cgmanager.nix
+++ /dev/null
@@ -1,26 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-  cfg = config.services.cgmanager;
-in {
-  meta.maintainers = [ maintainers.mic92 ];
-
-  ###### interface
-  options.services.cgmanager.enable = mkEnableOption "cgmanager";
-
-  ###### implementation
-  config = mkIf cfg.enable {
-    systemd.services.cgmanager = {
-      wantedBy = [ "multi-user.target" ];
-      description = "Cgroup management daemon";
-      restartIfChanged = false;
-      serviceConfig = {
-        ExecStart = "${pkgs.cgmanager}/bin/cgmanager -m name=systemd";
-        KillMode = "process";
-        Restart = "on-failure";
-      };
-    };
-  };
-}
diff --git a/nixos/modules/services/system/earlyoom.nix b/nixos/modules/services/system/earlyoom.nix
index 39d1bf274bd..e29bdbe264c 100644
--- a/nixos/modules/services/system/earlyoom.nix
+++ b/nixos/modules/services/system/earlyoom.nix
@@ -67,9 +67,19 @@ in
       notificationsCommand = mkOption {
         type = types.nullOr types.str;
         default = null;
-        example = "sudo -u example_user DISPLAY=:0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus notify-send";
         description = ''
-          Command used to send notifications.
+          This option is deprecated and ignored by earlyoom since 1.6.
+          Use <option>services.earlyoom.enableNotifications</option> instead.
+        '';
+      };
+
+      enableNotifications = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Send notifications about killed processes via the system d-bus.
+          To actually see the notifications in your GUI session, you need to have
+          <literal>systembus-notify</literal> running as your user.
 
           See <link xlink:href="https://github.com/rfjakob/earlyoom#notifications">README</link> for details.
         '';
@@ -87,12 +97,15 @@ in
         message = "Both options in conjunction do not make sense"; }
     ];
 
+    warnings = optional (ecfg.notificationsCommand != null)
+      "`services.earlyoom.notificationsCommand` is deprecated and ignored by earlyoom since 1.6.";
+
     systemd.services.earlyoom = {
       description = "Early OOM Daemon for Linux";
       wantedBy = [ "multi-user.target" ];
+      path = optional ecfg.enableNotifications pkgs.dbus;
       serviceConfig = {
         StandardOutput = "null";
-        StandardError = "syslog";
         ExecStart = ''
           ${pkgs.earlyoom}/bin/earlyoom \
           -m ${toString ecfg.freeMemThreshold} \
@@ -100,10 +113,11 @@ in
           ${optionalString ecfg.useKernelOOMKiller "-k"} \
           ${optionalString ecfg.ignoreOOMScoreAdjust "-i"} \
           ${optionalString ecfg.enableDebugInfo "-d"} \
-          ${optionalString (ecfg.notificationsCommand != null)
-            "-N ${escapeShellArg ecfg.notificationsCommand}"}
+          ${optionalString ecfg.enableNotifications "-n"}
         '';
       };
     };
+
+    environment.systemPackages = optional ecfg.enableNotifications pkgs.systembus-notify;
   };
 }
diff --git a/nixos/modules/services/system/nscd.conf b/nixos/modules/services/system/nscd.conf
index 2b7523a7346..722b883ba42 100644
--- a/nixos/modules/services/system/nscd.conf
+++ b/nixos/modules/services/system/nscd.conf
@@ -24,7 +24,7 @@ negative-time-to-live   netgroup        0
 shared                  netgroup        yes
 
 enable-cache            hosts           yes
-positive-time-to-live   hosts           600
+positive-time-to-live   hosts           0
 negative-time-to-live   hosts           0
 shared                  hosts           yes
 
diff --git a/nixos/modules/services/torrent/rtorrent.nix b/nixos/modules/services/torrent/rtorrent.nix
new file mode 100644
index 00000000000..be57c03b172
--- /dev/null
+++ b/nixos/modules/services/torrent/rtorrent.nix
@@ -0,0 +1,209 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.rtorrent;
+
+in {
+  options.services.rtorrent = {
+    enable = mkEnableOption "rtorrent";
+
+    dataDir = mkOption {
+      type = types.str;
+      default = "/var/lib/rtorrent";
+      description = ''
+        The directory where rtorrent stores its data files.
+      '';
+    };
+
+    downloadDir = mkOption {
+      type = types.str;
+      default = "${cfg.dataDir}/download";
+      description = ''
+        Where to put downloaded files.
+      '';
+    };
+
+    user = mkOption {
+      type = types.str;
+      default = "rtorrent";
+      description = ''
+        User account under which rtorrent runs.
+      '';
+    };
+
+    group = mkOption {
+      type = types.str;
+      default = "rtorrent";
+      description = ''
+        Group under which rtorrent runs.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.rtorrent;
+      defaultText = "pkgs.rtorrent";
+      description = ''
+        The rtorrent package to use.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 50000;
+      description = ''
+        The rtorrent port.
+      '';
+    };
+
+    openFirewall = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to open the firewall for the port in <option>services.rtorrent.port</option>.
+      '';
+    };
+
+    rpcSocket = mkOption {
+      type = types.str;
+      readOnly = true;
+      default = "/run/rtorrent/rpc.sock";
+      description = ''
+        RPC socket path.
+      '';
+    };
+
+    configText = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        The content of <filename>rtorrent.rc</filename>. The <link xlink:href="https://rtorrent-docs.readthedocs.io/en/latest/cookbook.html#modernized-configuration-template">modernized configuration template</link> with the values specified in this module will be prepended using mkBefore. You can use mkForce to overwrite the config completly.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    users.groups = mkIf (cfg.group == "rtorrent") {
+      rtorrent = {};
+    };
+
+    users.users = mkIf (cfg.user == "rtorrent") {
+      rtorrent = {
+        group = cfg.group;
+        shell = pkgs.bashInteractive;
+        home = cfg.dataDir;
+        description = "rtorrent Daemon user";
+        isSystemUser = true;
+      };
+    };
+
+    networking.firewall.allowedTCPPorts = mkIf (cfg.openFirewall) [ cfg.port ];
+
+    services.rtorrent.configText = mkBefore ''
+      # Instance layout (base paths)
+      method.insert = cfg.basedir, private|const|string, (cat,"${cfg.dataDir}/")
+      method.insert = cfg.watch,   private|const|string, (cat,(cfg.basedir),"watch/")
+      method.insert = cfg.logs,    private|const|string, (cat,(cfg.basedir),"log/")
+      method.insert = cfg.logfile, private|const|string, (cat,(cfg.logs),(system.time),".log")
+      method.insert = cfg.rpcsock, private|const|string, (cat,"${cfg.rpcSocket}")
+
+      # Create instance directories
+      execute.throw = sh, -c, (cat, "mkdir -p ", (cfg.basedir), "/session ", (cfg.watch), " ", (cfg.logs))
+
+      # Listening port for incoming peer traffic (fixed; you can also randomize it)
+      network.port_range.set = ${toString cfg.port}-${toString cfg.port}
+      network.port_random.set = no
+
+      # Tracker-less torrent and UDP tracker support
+      # (conservative settings for 'private' trackers, change for 'public')
+      dht.mode.set = disable
+      protocol.pex.set = no
+      trackers.use_udp.set = no
+
+      # Peer settings
+      throttle.max_uploads.set = 100
+      throttle.max_uploads.global.set = 250
+
+      throttle.min_peers.normal.set = 20
+      throttle.max_peers.normal.set = 60
+      throttle.min_peers.seed.set = 30
+      throttle.max_peers.seed.set = 80
+      trackers.numwant.set = 80
+
+      protocol.encryption.set = allow_incoming,try_outgoing,enable_retry
+
+      # Limits for file handle resources, this is optimized for
+      # an `ulimit` of 1024 (a common default). You MUST leave
+      # a ceiling of handles reserved for rTorrent's internal needs!
+      network.http.max_open.set = 50
+      network.max_open_files.set = 600
+      network.max_open_sockets.set = 3000
+
+      # Memory resource usage (increase if you have a large number of items loaded,
+      # and/or the available resources to spend)
+      pieces.memory.max.set = 1800M
+      network.xmlrpc.size_limit.set = 4M
+
+      # Basic operational settings (no need to change these)
+      session.path.set = (cat, (cfg.basedir), "session/")
+      directory.default.set = "${cfg.downloadDir}"
+      log.execute = (cat, (cfg.logs), "execute.log")
+      ##log.xmlrpc = (cat, (cfg.logs), "xmlrpc.log")
+      execute.nothrow = sh, -c, (cat, "echo >", (session.path), "rtorrent.pid", " ", (system.pid))
+
+      # Other operational settings (check & adapt)
+      encoding.add = utf8
+      system.umask.set = 0027
+      system.cwd.set = (cfg.basedir)
+      network.http.dns_cache_timeout.set = 25
+      schedule2 = monitor_diskspace, 15, 60, ((close_low_diskspace, 1000M))
+
+      # Watch directories (add more as you like, but use unique schedule names)
+      #schedule2 = watch_start, 10, 10, ((load.start, (cat, (cfg.watch), "start/*.torrent")))
+      #schedule2 = watch_load, 11, 10, ((load.normal, (cat, (cfg.watch), "load/*.torrent")))
+
+      # Logging:
+      #   Levels = critical error warn notice info debug
+      #   Groups = connection_* dht_* peer_* rpc_* storage_* thread_* tracker_* torrent_*
+      print = (cat, "Logging to ", (cfg.logfile))
+      log.open_file = "log", (cfg.logfile)
+      log.add_output = "info", "log"
+      ##log.add_output = "tracker_debug", "log"
+
+      # XMLRPC
+      scgi_local = (cfg.rpcsock)
+      schedule = scgi_group,0,0,"execute.nothrow=chown,\":rtorrent\",(cfg.rpcsock)"
+      schedule = scgi_permission,0,0,"execute.nothrow=chmod,\"g+w,o=\",(cfg.rpcsock)"
+    '';
+
+    systemd = {
+      services = {
+        rtorrent = let
+          rtorrentConfigFile = pkgs.writeText "rtorrent.rc" cfg.configText;
+        in {
+          description = "rTorrent system service";
+          after = [ "network.target" ];
+          path = [ cfg.package pkgs.bash ];
+          wantedBy = [ "multi-user.target" ];
+          serviceConfig = {
+            User = cfg.user;
+            Group = cfg.group;
+            Type = "simple";
+            Restart = "on-failure";
+            WorkingDirectory = cfg.dataDir;
+            ExecStartPre=''${pkgs.bash}/bin/bash -c "if test -e ${cfg.dataDir}/session/rtorrent.lock && test -z $(${pkgs.procps}/bin/pidof rtorrent); then rm -f ${cfg.dataDir}/session/rtorrent.lock; fi"'';
+            ExecStart="${cfg.package}/bin/rtorrent -n -o system.daemon.set=true -o import=${rtorrentConfigFile}";
+            RuntimeDirectory = "rtorrent";
+            RuntimeDirectoryMode = 755;
+          };
+        };
+      };
+
+      tmpfiles.rules = [ "d '${cfg.dataDir}' 0750 ${cfg.user} ${cfg.group} -" ];
+    };
+  };
+}
diff --git a/nixos/modules/services/torrent/transmission.nix b/nixos/modules/services/torrent/transmission.nix
index fd28b94f7be..014a22bb5a8 100644
--- a/nixos/modules/services/torrent/transmission.nix
+++ b/nixos/modules/services/torrent/transmission.nix
@@ -1,58 +1,54 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, pkgs, options, ... }:
 
 with lib;
 
 let
   cfg = config.services.transmission;
+  inherit (config.environment) etc;
   apparmor = config.security.apparmor.enable;
-
-  homeDir = cfg.home;
-  downloadDirPermissions = cfg.downloadDirPermissions;
-  downloadDir = "${homeDir}/Downloads";
-  incompleteDir = "${homeDir}/.incomplete";
-
-  settingsDir = "${homeDir}/.config/transmission-daemon";
-  settingsFile = pkgs.writeText "settings.json" (builtins.toJSON fullSettings);
-
-  # for users in group "transmission" to have access to torrents
-  fullSettings = { umask = 2; download-dir = downloadDir; incomplete-dir = incompleteDir; } // cfg.settings;
-
-  preStart = pkgs.writeScript "transmission-pre-start" ''
-    #!${pkgs.runtimeShell}
-    set -ex
-    for DIR in "${homeDir}" "${settingsDir}" "${fullSettings.download-dir}" "${fullSettings.incomplete-dir}"; do
-      mkdir -p "$DIR"
-    done
-    chmod 755 "${homeDir}"
-    chmod 700 "${settingsDir}"
-    chmod ${downloadDirPermissions} "${fullSettings.download-dir}" "${fullSettings.incomplete-dir}"
-    cp -f ${settingsFile} ${settingsDir}/settings.json
-  '';
+  rootDir = "/run/transmission";
+  homeDir = "/var/lib/transmission";
+  settingsDir = ".config/transmission-daemon";
+  downloadsDir = "Downloads";
+  incompleteDir = ".incomplete";
+  watchDir = "watchdir";
+  # TODO: switch to configGen.json once RFC0042 is implemented
+  settingsFile = pkgs.writeText "settings.json" (builtins.toJSON cfg.settings);
 in
 {
   options = {
     services.transmission = {
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Whether or not to enable the headless Transmission BitTorrent daemon.
+      enable = mkEnableOption ''the headless Transmission BitTorrent daemon.
 
-          Transmission daemon can be controlled via the RPC interface using
-          transmission-remote or the WebUI (http://localhost:9091/ by default).
+        Transmission daemon can be controlled via the RPC interface using
+        transmission-remote, the WebUI (http://127.0.0.1:9091/ by default),
+        or other clients like stig or tremc.
 
-          Torrents are downloaded to ${downloadDir} by default and are
-          accessible to users in the "transmission" group.
-        '';
-      };
+        Torrents are downloaded to ${homeDir}/${downloadsDir} by default and are
+        accessible to users in the "transmission" group'';
 
-      settings = mkOption {
+      settings = mkOption rec {
+        # TODO: switch to types.config.json as prescribed by RFC0042 once it's implemented
         type = types.attrs;
+        apply = recursiveUpdate default;
         default =
           {
-            download-dir = downloadDir;
-            incomplete-dir = incompleteDir;
+            download-dir = "${cfg.home}/${downloadsDir}";
+            incomplete-dir = "${cfg.home}/${incompleteDir}";
             incomplete-dir-enabled = true;
+            watch-dir = "${cfg.home}/${watchDir}";
+            watch-dir-enabled = false;
+            message-level = 1;
+            peer-port = 51413;
+            peer-port-random-high = 65535;
+            peer-port-random-low = 49152;
+            peer-port-random-on-start = false;
+            rpc-bind-address = "127.0.0.1";
+            rpc-port = 9091;
+            script-torrent-done-enabled = false;
+            script-torrent-done-filename = "";
+            umask = 2; # 0o002 in decimal as expected by Transmission
+            utp-enabled = true;
           };
         example =
           {
@@ -62,11 +58,12 @@ in
             rpc-whitelist = "127.0.0.1,192.168.*.*";
           };
         description = ''
-          Attribute set whos fields overwrites fields in settings.json (each
-          time the service starts). String values must be quoted, integer and
+          Attribute set whose fields overwrites fields in
+          <literal>.config/transmission-daemon/settings.json</literal>
+          (each time the service starts). String values must be quoted, integer and
           boolean values must not.
 
-          See https://github.com/transmission/transmission/wiki/Editing-Configuration-Files
+          See <link xlink:href="https://github.com/transmission/transmission/wiki/Editing-Configuration-Files">Transmission's Wiki</link>
           for documentation.
         '';
       };
@@ -76,22 +73,32 @@ in
         default = "770";
         example = "775";
         description = ''
-          The permissions to set for download-dir and incomplete-dir.
-          They will be applied on every service start.
+          The permissions set by <literal>systemd.activationScripts.transmission-daemon</literal>
+          on the directories <link linkend="opt-services.transmission.settings">settings.download-dir</link>
+          and <link linkend="opt-services.transmission.settings">settings.incomplete-dir</link>.
+          Note that you may also want to change
+          <link linkend="opt-services.transmission.settings">settings.umask</link>.
         '';
       };
 
       port = mkOption {
-        type = types.int;
-        default = 9091;
-        description = "TCP port number to run the RPC/web interface.";
+        type = types.port;
+        description = ''
+          TCP port number to run the RPC/web interface.
+
+          If instead you want to change the peer port,
+          use <link linkend="opt-services.transmission.settings">settings.peer-port</link>
+          or <link linkend="opt-services.transmission.settings">settings.peer-port-random-on-start</link>.
+        '';
       };
 
       home = mkOption {
         type = types.path;
-        default = "/var/lib/transmission";
+        default = homeDir;
         description = ''
-          The directory where transmission will create files.
+          The directory where Transmission will create <literal>${settingsDir}</literal>.
+          as well as <literal>${downloadsDir}/</literal> unless <link linkend="opt-services.transmission.settings">settings.download-dir</link> is changed,
+          and <literal>${incompleteDir}/</literal> unless <link linkend="opt-services.transmission.settings">settings.incomplete-dir</link> is changed.
         '';
       };
 
@@ -106,25 +113,179 @@ in
         default = "transmission";
         description = "Group account under which Transmission runs.";
       };
+
+      credentialsFile = mkOption {
+        type = types.path;
+        description = ''
+          Path to a JSON file to be merged with the settings.
+          Useful to merge a file which is better kept out of the Nix store
+          because it contains sensible data like <link linkend="opt-services.transmission.settings">settings.rpc-password</link>.
+        '';
+        default = "/dev/null";
+        example = "/var/lib/secrets/transmission/settings.json";
+      };
+
+      openFirewall = mkEnableOption "opening of the peer port(s) in the firewall";
+
+      performanceNetParameters = mkEnableOption ''tweaking of kernel parameters
+        to open many more connections at the same time.
+
+        Note that you may also want to increase
+        <link linkend="opt-services.transmission.settings">settings.peer-limit-global</link>.
+        And be aware that these settings are quite aggressive
+        and might not suite your regular desktop use.
+        For instance, SSH sessions may time out more easily'';
     };
   };
 
   config = mkIf cfg.enable {
+    # Note that using systemd.tmpfiles would not work here
+    # because it would fail when creating a directory
+    # with a different owner than its parent directory, by saying:
+    # Detected unsafe path transition /home/foo → /home/foo/Downloads during canonicalization of /home/foo/Downloads
+    # 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}'
+      '';
+
+    assertions = [
+      { assertion = builtins.match "^/.*" cfg.home != null;
+        message = "`services.transmission.home' must be an absolute path.";
+      }
+      { assertion = types.path.check cfg.settings.download-dir;
+        message = "`services.transmission.settings.download-dir' must be an absolute path.";
+      }
+      { assertion = types.path.check cfg.settings.incomplete-dir;
+        message = "`services.transmission.settings.incomplete-dir' must be an absolute path.";
+      }
+      { assertion = types.path.check cfg.settings.watch-dir;
+        message = "`services.transmission.settings.watch-dir' must be an absolute path.";
+      }
+      { assertion = cfg.settings.script-torrent-done-filename == "" || types.path.check cfg.settings.script-torrent-done-filename;
+        message = "`services.transmission.settings.script-torrent-done-filename' must be an absolute path.";
+      }
+      { assertion = types.port.check cfg.settings.rpc-port;
+        message = "${toString cfg.settings.rpc-port} is not a valid port number for `services.transmission.settings.rpc-port`.";
+      }
+      # In case both port and settings.rpc-port are explicitely defined: they must be the same.
+      { assertion = !options.services.transmission.port.isDefined || cfg.port == cfg.settings.rpc-port;
+        message = "`services.transmission.port' is not equal to `services.transmission.settings.rpc-port'";
+      }
+    ];
+
+    services.transmission.settings =
+      optionalAttrs options.services.transmission.port.isDefined { rpc-port = cfg.port; };
+
     systemd.services.transmission = {
       description = "Transmission BitTorrent Service";
       after = [ "network.target" ] ++ optional apparmor "apparmor.service";
-      requires = mkIf apparmor [ "apparmor.service" ];
+      requires = optional apparmor "apparmor.service";
       wantedBy = [ "multi-user.target" ];
+      environment.CURL_CA_BUNDLE = etc."ssl/certs/ca-certificates.crt".source;
 
-      # 1) Only the "transmission" user and group have access to torrents.
-      # 2) Optionally update/force specific fields into the configuration file.
-      serviceConfig.ExecStartPre = preStart;
-      serviceConfig.ExecStart = "${pkgs.transmission}/bin/transmission-daemon -f --port ${toString config.services.transmission.port} --config-dir ${settingsDir}";
-      serviceConfig.ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
-      serviceConfig.User = cfg.user;
-      serviceConfig.Group = cfg.group;
-      # NOTE: transmission has an internal umask that also must be set (in settings.json)
-      serviceConfig.UMask = "0002";
+      serviceConfig = {
+        # Use "+" because credentialsFile may not be accessible to User= or Group=.
+        ExecStartPre = [("+" + pkgs.writeShellScript "transmission-prestart" ''
+          set -eu${lib.optionalString (cfg.settings.message-level >= 3) "x"}
+          ${pkgs.jq}/bin/jq --slurp add ${settingsFile} '${cfg.credentialsFile}' |
+          install -D -m 600 -o '${cfg.user}' -g '${cfg.group}' /dev/stdin \
+           '${cfg.home}/${settingsDir}/settings.json'
+        '')];
+        ExecStart="${pkgs.transmission}/bin/transmission-daemon -f";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        User = cfg.user;
+        Group = cfg.group;
+        # Create rootDir in the host's mount namespace.
+        RuntimeDirectory = [(baseNameOf rootDir)];
+        RuntimeDirectoryMode = "755";
+        # Avoid mounting rootDir in the own rootDir of ExecStart='s mount namespace.
+        InaccessiblePaths = ["-+${rootDir}"];
+        # This is for BindPaths= and BindReadOnlyPaths=
+        # to allow traversal of directories they create in RootDirectory=.
+        UMask = "0066";
+        # Using RootDirectory= makes it possible
+        # to use the same paths download-dir/incomplete-dir
+        # (which appear in user's interfaces) without requiring cfg.user
+        # to have access to their parent directories,
+        # by using BindPaths=/BindReadOnlyPaths=.
+        # Note that TemporaryFileSystem= could have been used instead
+        # but not without adding some BindPaths=/BindReadOnlyPaths=
+        # that would only be needed for ExecStartPre=,
+        # because RootDirectoryStartOnly=true would not help.
+        RootDirectory = rootDir;
+        RootDirectoryStartOnly = true;
+        MountAPIVFS = true;
+        BindPaths =
+          [ "${cfg.home}/${settingsDir}"
+            cfg.settings.download-dir
+          ] ++
+          optional cfg.settings.incomplete-dir-enabled
+            cfg.settings.incomplete-dir
+          ++
+          optional cfg.settings.watch-dir-enabled
+            cfg.settings.watch-dir
+          ;
+        BindReadOnlyPaths = [
+          # No confinement done of /nix/store here like in systemd-confinement.nix,
+          # an AppArmor profile is provided to get a confinement based upon paths and rights.
+          builtins.storeDir
+          "/etc"
+          ] ++
+          optional (cfg.settings.script-torrent-done-enabled &&
+                    cfg.settings.script-torrent-done-filename != "")
+            cfg.settings.script-torrent-done-filename;
+        # The following options are only for optimizing:
+        # systemd-analyze security transmission
+        AmbientCapabilities = "";
+        CapabilityBoundingSet = "";
+        # ProtectClock= adds DeviceAllow=char-rtc r
+        DeviceAllow = "";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
+        PrivateNetwork = mkDefault false;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        # ProtectHome=true would not allow BindPaths= to work accross /home,
+        # and ProtectHome=tmpfs would break statfs(),
+        # preventing transmission-daemon to report the available free space.
+        # However, RootDirectory= is used, so this is not a security concern
+        # since there would be nothing in /home but any BindPaths= wanted by the user.
+        ProtectHome = "read-only";
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectSystem = "strict";
+        RemoveIPC = true;
+        # AF_UNIX may become usable one day:
+        # https://github.com/transmission/transmission/issues/441
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallFilter = [
+          "@system-service"
+          # Groups in @system-service which do not contain a syscall
+          # listed by perf stat -e 'syscalls:sys_enter_*' transmission-daemon -f
+          # in tests, and seem likely not necessary for transmission-daemon.
+          "~@aio" "~@chown" "~@keyring" "~@memlock" "~@resources" "~@setuid" "~@timer"
+          # In the @privileged group, but reached when querying infos through RPC (eg. with stig).
+          "quotactl"
+        ];
+        SystemCallArchitectures = "native";
+        SystemCallErrorNumber = "EPERM";
+      };
     };
 
     # It's useful to have transmission in path, e.g. for remote control
@@ -132,68 +293,159 @@ in
 
     users.users = optionalAttrs (cfg.user == "transmission") ({
       transmission = {
-        name = "transmission";
         group = cfg.group;
         uid = config.ids.uids.transmission;
         description = "Transmission BitTorrent user";
-        home = homeDir;
-        createHome = true;
+        home = cfg.home;
       };
     });
 
     users.groups = optionalAttrs (cfg.group == "transmission") ({
       transmission = {
-        name = "transmission";
         gid = config.ids.gids.transmission;
       };
     });
 
-    # AppArmor profile
+    networking.firewall = mkIf cfg.openFirewall (
+      if cfg.settings.peer-port-random-on-start
+      then
+        { allowedTCPPortRanges =
+            [ { from = cfg.settings.peer-port-random-low;
+                to   = cfg.settings.peer-port-random-high;
+              }
+            ];
+          allowedUDPPortRanges =
+            [ { from = cfg.settings.peer-port-random-low;
+                to   = cfg.settings.peer-port-random-high;
+              }
+            ];
+        }
+      else
+        { allowedTCPPorts = [ cfg.settings.peer-port ];
+          allowedUDPPorts = [ cfg.settings.peer-port ];
+        }
+    );
+
+    boot.kernel.sysctl = mkMerge [
+      # Transmission uses a single UDP socket in order to implement multiple uTP sockets,
+      # and thus expects large kernel buffers for the UDP socket,
+      # https://trac.transmissionbt.com/browser/trunk/libtransmission/tr-udp.c?rev=11956.
+      # at least up to the values hardcoded here:
+      (mkIf cfg.settings.utp-enabled {
+        "net.core.rmem_max" = mkDefault "4194304"; # 4MB
+        "net.core.wmem_max" = mkDefault "1048576"; # 1MB
+      })
+      (mkIf cfg.performanceNetParameters {
+        # Increase the number of available source (local) TCP and UDP ports to 49151.
+        # Usual default is 32768 60999, ie. 28231 ports.
+        # Find out your current usage with: ss -s
+        "net.ipv4.ip_local_port_range" = "16384 65535";
+        # Timeout faster generic TCP states.
+        # Usual default is 600.
+        # Find out your current usage with: watch -n 1 netstat -nptuo
+        "net.netfilter.nf_conntrack_generic_timeout" = 60;
+        # Timeout faster established but inactive connections.
+        # Usual default is 432000.
+        "net.netfilter.nf_conntrack_tcp_timeout_established" = 600;
+        # Clear immediately TCP states after timeout.
+        # Usual default is 120.
+        "net.netfilter.nf_conntrack_tcp_timeout_time_wait" = 1;
+        # Increase the number of trackable connections.
+        # Usual default is 262144.
+        # Find out your current usage with: conntrack -C
+        "net.netfilter.nf_conntrack_max" = 1048576;
+      })
+    ];
+
     security.apparmor.profiles = mkIf apparmor [
       (pkgs.writeText "apparmor-transmission-daemon" ''
-        #include <tunables/global>
+        include <tunables/global>
 
         ${pkgs.transmission}/bin/transmission-daemon {
-          #include <abstractions/base>
-          #include <abstractions/nameservice>
-
-          ${getLib pkgs.glibc}/lib/*.so                    mr,
-          ${getLib pkgs.libevent}/lib/libevent*.so*        mr,
-          ${getLib pkgs.curl}/lib/libcurl*.so*             mr,
-          ${getLib pkgs.openssl}/lib/libssl*.so*           mr,
-          ${getLib pkgs.openssl}/lib/libcrypto*.so*        mr,
-          ${getLib pkgs.zlib}/lib/libz*.so*                mr,
-          ${getLib pkgs.libssh2}/lib/libssh2*.so*          mr,
-          ${getLib pkgs.systemd}/lib/libsystemd*.so*       mr,
-          ${getLib pkgs.xz}/lib/liblzma*.so*               mr,
-          ${getLib pkgs.libgcrypt}/lib/libgcrypt*.so*      mr,
-          ${getLib pkgs.libgpgerror}/lib/libgpg-error*.so* mr,
-          ${getLib pkgs.nghttp2}/lib/libnghttp2*.so*       mr,
-          ${getLib pkgs.c-ares}/lib/libcares*.so*          mr,
-          ${getLib pkgs.libcap}/lib/libcap*.so*            mr,
-          ${getLib pkgs.attr}/lib/libattr*.so*             mr,
-          ${getLib pkgs.lz4}/lib/liblz4*.so*               mr,
-          ${getLib pkgs.libkrb5}/lib/lib*.so*              mr,
-          ${getLib pkgs.keyutils}/lib/libkeyutils*.so*     mr,
-          ${getLib pkgs.utillinuxMinimal.out}/lib/libblkid.so.* mr,
-          ${getLib pkgs.utillinuxMinimal.out}/lib/libmount.so.* mr,
-          ${getLib pkgs.utillinuxMinimal.out}/lib/libuuid.so.* mr,
-
-          @{PROC}/sys/kernel/random/uuid   r,
-          @{PROC}/sys/vm/overcommit_memory r,
-
-          ${pkgs.openssl.out}/etc/**                     r,
-          ${pkgs.transmission}/share/transmission/** r,
-
-          owner ${settingsDir}/** rw,
-
-          ${fullSettings.download-dir}/** rw,
-          ${optionalString fullSettings.incomplete-dir-enabled ''
-            ${fullSettings.incomplete-dir}/** rw,
+          include <abstractions/base>
+          include <abstractions/nameservice>
+
+          # NOTE: https://github.com/NixOS/nixpkgs/pull/93457
+          # will remove the need for these by fixing <abstractions/base>
+          r ${etc."hosts".source},
+          r /etc/ld-nix.so.preload,
+          ${lib.optionalString (builtins.hasAttr "ld-nix.so.preload" etc) ''
+            r ${etc."ld-nix.so.preload".source},
+            ${concatMapStrings (p: optionalString (p != "") ("mr ${p},\n"))
+              (splitString "\n" config.environment.etc."ld-nix.so.preload".text)}
           ''}
+          r ${etc."ssl/certs/ca-certificates.crt".source},
+          r ${pkgs.tzdata}/share/zoneinfo/**,
+          r ${pkgs.stdenv.cc.libc}/share/i18n/**,
+          r ${pkgs.stdenv.cc.libc}/share/locale/**,
+
+          mr ${getLib pkgs.stdenv.cc.cc}/lib/*.so*,
+          mr ${getLib pkgs.stdenv.cc.libc}/lib/*.so*,
+          mr ${getLib pkgs.attr}/lib/libattr*.so*,
+          mr ${getLib pkgs.c-ares}/lib/libcares*.so*,
+          mr ${getLib pkgs.curl}/lib/libcurl*.so*,
+          mr ${getLib pkgs.keyutils}/lib/libkeyutils*.so*,
+          mr ${getLib pkgs.libcap}/lib/libcap*.so*,
+          mr ${getLib pkgs.libevent}/lib/libevent*.so*,
+          mr ${getLib pkgs.libgcrypt}/lib/libgcrypt*.so*,
+          mr ${getLib pkgs.libgpgerror}/lib/libgpg-error*.so*,
+          mr ${getLib pkgs.libkrb5}/lib/lib*.so*,
+          mr ${getLib pkgs.libssh2}/lib/libssh2*.so*,
+          mr ${getLib pkgs.lz4}/lib/liblz4*.so*,
+          mr ${getLib pkgs.nghttp2}/lib/libnghttp2*.so*,
+          mr ${getLib pkgs.openssl}/lib/libcrypto*.so*,
+          mr ${getLib pkgs.openssl}/lib/libssl*.so*,
+          mr ${getLib pkgs.systemd}/lib/libsystemd*.so*,
+          mr ${getLib pkgs.utillinuxMinimal.out}/lib/libblkid.so*,
+          mr ${getLib pkgs.utillinuxMinimal.out}/lib/libmount.so*,
+          mr ${getLib pkgs.utillinuxMinimal.out}/lib/libuuid.so*,
+          mr ${getLib pkgs.xz}/lib/liblzma*.so*,
+          mr ${getLib pkgs.zlib}/lib/libz*.so*,
+
+          r @{PROC}/sys/kernel/random/uuid,
+          r @{PROC}/sys/vm/overcommit_memory,
+          # @{pid} is not a kernel variable yet but a regexp
+          #r @{PROC}/@{pid}/environ,
+          r @{PROC}/@{pid}/mounts,
+          rwk /tmp/tr_session_id_*,
+
+          r ${pkgs.openssl.out}/etc/**,
+          r ${config.systemd.services.transmission.environment.CURL_CA_BUNDLE},
+          r ${pkgs.transmission}/share/transmission/**,
+
+          owner rw ${cfg.home}/${settingsDir}/**,
+          rw ${cfg.settings.download-dir}/**,
+          ${optionalString cfg.settings.incomplete-dir-enabled ''
+            rw ${cfg.settings.incomplete-dir}/**,
+          ''}
+          ${optionalString cfg.settings.watch-dir-enabled ''
+            rw ${cfg.settings.watch-dir}/**,
+          ''}
+          profile dirs {
+            rw ${cfg.settings.download-dir}/**,
+            ${optionalString cfg.settings.incomplete-dir-enabled ''
+              rw ${cfg.settings.incomplete-dir}/**,
+            ''}
+            ${optionalString cfg.settings.watch-dir-enabled ''
+              rw ${cfg.settings.watch-dir}/**,
+            ''}
+          }
+
+          ${optionalString (cfg.settings.script-torrent-done-enabled &&
+                            cfg.settings.script-torrent-done-filename != "") ''
+            # Stack transmission_directories profile on top of
+            # any existing profile for script-torrent-done-filename
+            # FIXME: to be tested as I'm not sure it works well with NoNewPrivileges=
+            # https://gitlab.com/apparmor/apparmor/-/wikis/AppArmorStacking#seccomp-and-no_new_privs
+            px ${cfg.settings.script-torrent-done-filename} -> &@{dirs},
+          ''}
+
+          # FIXME: enable customizing using https://github.com/NixOS/nixpkgs/pull/93457
+          # include <local/transmission-daemon>
         }
       '')
     ];
   };
 
+  meta.maintainers = with lib.maintainers; [ julm ];
 }
diff --git a/nixos/modules/services/video/mirakurun.nix b/nixos/modules/services/video/mirakurun.nix
new file mode 100644
index 00000000000..675b67f6ebf
--- /dev/null
+++ b/nixos/modules/services/video/mirakurun.nix
@@ -0,0 +1,165 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.mirakurun;
+  mirakurun = pkgs.mirakurun;
+  username = config.users.users.mirakurun.name;
+  groupname = config.users.users.mirakurun.group;
+  settingsFmt = pkgs.formats.yaml {};
+in
+  {
+    options = {
+      services.mirakurun = {
+        enable = mkEnableOption mirakurun.meta.description;
+
+        port = mkOption {
+          type = with types; nullOr port;
+          default = 40772;
+          description = ''
+            Port to listen on. If null, it won't listen on any port.
+          '';
+        };
+
+        openFirewall = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Open ports in the firewall for Mirakurun.
+          '';
+        };
+
+        serverSettings = mkOption {
+          type = settingsFmt.type;
+          default = {};
+          example = literalExample ''
+            {
+              highWaterMark = 25165824;
+              overflowTimeLimit = 30000;
+            };
+          '';
+          description = ''
+            Options for server.yml.
+
+            Documentation:
+            <link xlink:href="https://github.com/Chinachu/Mirakurun/blob/master/doc/Configuration.md"/>
+          '';
+        };
+
+        tunerSettings = mkOption {
+          type = with types; nullOr settingsFmt.type;
+          default = null;
+          example = literalExample ''
+            [
+              {
+                name = "tuner-name";
+                types = [ "GR" "BS" "CS" "SKY" ];
+                dvbDevicePath = "/dev/dvb/adapterX/dvrX";
+              }
+            ];
+          '';
+          description = ''
+            Options which are added to tuners.yml. If none is specified, it will
+            automatically be generated at runtime.
+
+            Documentation:
+            <link xlink:href="https://github.com/Chinachu/Mirakurun/blob/master/doc/Configuration.md"/>
+          '';
+        };
+
+        channelSettings = mkOption {
+          type = with types; nullOr settingsFmt.type;
+          default = null;
+          example = literalExample ''
+            [
+              {
+                name = "channel";
+                types = "GR";
+                channel = "0";
+              }
+            ];
+          '';
+          description = ''
+            Options which are added to channels.yml. If none is specified, it
+            will automatically be generated at runtime.
+
+            Documentation:
+            <link xlink:href="https://github.com/Chinachu/Mirakurun/blob/master/doc/Configuration.md"/>
+          '';
+        };
+      };
+    };
+
+    config = mkIf cfg.enable {
+      environment.systemPackages = [ mirakurun ];
+      environment.etc = {
+        "mirakurun/server.yml".source = settingsFmt.generate "server.yml" cfg.serverSettings;
+        "mirakurun/tuners.yml" = mkIf (cfg.tunerSettings != null) {
+          source = settingsFmt.generate "tuners.yml" cfg.tunerSettings;
+          mode = "0644";
+          user = username;
+          group = groupname;
+        };
+        "mirakurun/channels.yml" = mkIf (cfg.channelSettings != null) {
+          source = settingsFmt.generate "channels.yml" cfg.channelSettings;
+          mode = "0644";
+          user = username;
+          group = groupname;
+        };
+      };
+
+      networking.firewall = mkIf cfg.openFirewall {
+        allowedTCPPorts = mkIf (cfg.port != null) [ cfg.port ];
+      };
+
+      users.users.mirakurun = {
+        description = "Mirakurun user";
+        group = "video";
+        isSystemUser = true;
+      };
+
+      services.mirakurun.serverSettings = {
+        logLevel = mkDefault 2;
+        path = mkDefault "/var/run/mirakurun/mirakurun.sock";
+        port = mkIf (cfg.port != null) (mkDefault cfg.port);
+      };
+
+      systemd.tmpfiles.rules = [
+        "d '/etc/mirakurun' - ${username} ${groupname} - -"
+      ];
+
+      systemd.services.mirakurun = {
+        description = mirakurun.meta.description;
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+        serviceConfig = {
+          ExecStart = "${mirakurun}/bin/mirakurun";
+          User = username;
+          Group = groupname;
+          RuntimeDirectory="mirakurun";
+          StateDirectory="mirakurun";
+          Nice = -10;
+          IOSchedulingClass = "realtime";
+          IOSchedulingPriority = 7;
+        };
+
+        environment = {
+          SERVER_CONFIG_PATH = "/etc/mirakurun/server.yml";
+          TUNERS_CONFIG_PATH = "/etc/mirakurun/tuners.yml";
+          CHANNELS_CONFIG_PATH = "/etc/mirakurun/channels.yml";
+          SERVICES_DB_PATH = "/var/lib/mirakurun/services.json";
+          PROGRAMS_DB_PATH = "/var/lib/mirakurun/programs.json";
+          NODE_ENV = "production";
+        };
+
+        restartTriggers = let
+          getconf = target: config.environment.etc."mirakurun/${target}.yml".source;
+          targets = [
+            "server"
+          ] ++ optional (cfg.tunerSettings != null) "tuners"
+            ++ optional (cfg.channelSettings != null) "channels";
+        in (map getconf targets);
+      };
+    };
+  }
diff --git a/nixos/modules/services/wayland/cage.nix b/nixos/modules/services/wayland/cage.nix
index c59ca9983a6..50e424fccbf 100644
--- a/nixos/modules/services/wayland/cage.nix
+++ b/nixos/modules/services/wayland/cage.nix
@@ -73,8 +73,6 @@ in {
         TTYVTDisallocate = "yes";
         # Fail to start if not controlling the virtual terminal.
         StandardInput = "tty-fail";
-        StandardOutput = "syslog";
-        StandardError = "syslog";
         # Set up a full (custom) user session for the user, required by Cage.
         PAMName = "cage";
       };
diff --git a/nixos/modules/services/web-apps/codimd.nix b/nixos/modules/services/web-apps/codimd.nix
index 751f81649dd..ab922a38e5c 100644
--- a/nixos/modules/services/web-apps/codimd.nix
+++ b/nixos/modules/services/web-apps/codimd.nix
@@ -93,7 +93,7 @@ in
           type = types.bool;
           default = true;
           description = ''
-            Wheter to enable HSTS if HTTPS is also enabled.
+            Whether to enable HSTS if HTTPS is also enabled.
           '';
         };
         maxAgeSeconds = mkOption {
@@ -385,7 +385,7 @@ in
         type = types.bool;
         default = true;
         description = ''
-          Wether to enable email registration.
+          Whether to enable email registration.
         '';
       };
       allowGravatar = mkOption {
diff --git a/nixos/modules/services/web-apps/convos.nix b/nixos/modules/services/web-apps/convos.nix
new file mode 100644
index 00000000000..8be11eec9f3
--- /dev/null
+++ b/nixos/modules/services/web-apps/convos.nix
@@ -0,0 +1,72 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.convos;
+in
+{
+  options.services.convos = {
+    enable = mkEnableOption "Convos";
+    listenPort = mkOption {
+      type = types.port;
+      default = 3000;
+      example = 8080;
+      description = "Port the web interface should listen on";
+    };
+    listenAddress = mkOption {
+      type = types.str;
+      default = "*";
+      example = "127.0.0.1";
+      description = "Address or host the web interface should listen on";
+    };
+    reverseProxy = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enables reverse proxy support. This will allow Convos to automatically
+        pick up the <literal>X-Forwarded-For</literal> and
+        <literal>X-Request-Base</literal> HTTP headers set in your reverse proxy
+        web server. Note that enabling this option without a reverse proxy in
+        front will be a security issue.
+      '';
+    };
+  };
+  config = mkIf cfg.enable {
+    systemd.services.convos = {
+      description = "Convos Service";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "networking.target" ];
+      environment = {
+        CONVOS_HOME = "%S/convos";
+        CONVOS_REVERSE_PROXY = if cfg.reverseProxy then "1" else "0";
+        MOJO_LISTEN = "http://${toString cfg.listenAddress}:${toString cfg.listenPort}";
+      };
+      serviceConfig = {
+        ExecStart = "${pkgs.convos}/bin/convos daemon";
+        Restart = "on-failure";
+        StateDirectory = "convos";
+        WorkingDirectory = "%S/convos";
+        DynamicUser = true;
+        MemoryDenyWriteExecute = true;
+        ProtectHome = true;
+        ProtectClock = true;
+        ProtectHostname = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectKernelLogs = true;
+        ProtectControlGroups = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
+        PrivateUsers = true;
+        LockPersonality = true;
+        RestrictRealtime = true;
+        RestrictNamespaces = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6"];
+        SystemCallFilter = "@system-service";
+        SystemCallArchitectures = "native";
+        CapabilityBoundingSet = "";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/dokuwiki.nix b/nixos/modules/services/web-apps/dokuwiki.nix
index 33a828fa2cb..d9ebb3a9880 100644
--- a/nixos/modules/services/web-apps/dokuwiki.nix
+++ b/nixos/modules/services/web-apps/dokuwiki.nix
@@ -2,7 +2,7 @@
 
 let
 
-  inherit (lib) mkEnableOption mkForce mkIf mkMerge mkOption optionalAttrs recursiveUpdate types;
+  inherit (lib) mkEnableOption mkForce mkIf mkMerge mkOption optionalAttrs recursiveUpdate types maintainers;
   inherit (lib) concatMapStringsSep flatten mapAttrs mapAttrs' mapAttrsToList nameValuePair concatMapStringSep;
 
   eachSite = config.services.dokuwiki;
@@ -95,7 +95,7 @@ let
 
       aclFile = mkOption {
         type = with types; nullOr str;
-        default = if (config.aclUse && config.acl == null) then "/var/lib/dokuwiki/${name}/users.auth.php" else null;
+        default = if (config.aclUse && config.acl == null) then "/var/lib/dokuwiki/${name}/acl.auth.php" else null;
         description = ''
           Location of the dokuwiki acl rules. Mutually exclusive with services.dokuwiki.acl
           Mutually exclusive with services.dokuwiki.acl which is preferred.
@@ -249,22 +249,19 @@ let
       nginx = mkOption {
         type = types.submodule (
           recursiveUpdate
-            (import ../web-servers/nginx/vhost-options.nix { inherit config lib; })
-            {
-              # Enable encryption by default,
-              options.forceSSL.default = true;
-              options.enableACME.default = true;
-            }
+            (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) {}
         );
-        default = {forceSSL = true; enableACME = true;};
+        default = {};
         example = {
           serverAliases = [
             "wiki.\${config.networking.domain}"
           ];
-          enableACME = false;
+          # To enable encryption and let let's encrypt take care of certificate
+          forceSSL = true;
+          enableACME = true;
         };
         description = ''
-          With this option, you can customize the nginx virtualHost which already has sensible defaults for DokuWiki.
+          With this option, you can customize the nginx virtualHost settings.
         '';
       };
     };
@@ -276,7 +273,7 @@ in
     services.dokuwiki = mkOption {
       type = types.attrsOf (types.submodule siteOpts);
       default = {};
-      description = "Sepcification of one or more dokuwiki sites to service.";
+      description = "Sepcification of one or more dokuwiki sites to serve.";
     };
   };
 
@@ -321,7 +318,7 @@ in
       enable = true;
       virtualHosts = mapAttrs (hostName: cfg:  mkMerge [ cfg.nginx {
         root = mkForce "${pkg hostName cfg}/share/dokuwiki";
-        extraConfig = "fastcgi_param HTTPS on;";
+        extraConfig = lib.optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "fastcgi_param HTTPS on;";
 
         locations."~ /(conf/|bin/|inc/|install.php)" = {
           extraConfig = "deny all;";
@@ -359,7 +356,7 @@ in
               fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
               fastcgi_param REDIRECT_STATUS 200;
               fastcgi_pass unix:${config.services.phpfpm.pools."dokuwiki-${hostName}".socket};
-              fastcgi_param HTTPS on;
+              ${lib.optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "fastcgi_param HTTPS on;"}
           '';
         };
       }]) eachSite;
@@ -385,4 +382,7 @@ in
       isSystemUser = true;
     };
   };
+
+  meta.maintainers = with maintainers; [ _1000101 ];
+
 }
diff --git a/nixos/modules/services/web-apps/engelsystem.nix b/nixos/modules/services/web-apps/engelsystem.nix
new file mode 100644
index 00000000000..899582a2030
--- /dev/null
+++ b/nixos/modules/services/web-apps/engelsystem.nix
@@ -0,0 +1,186 @@
+{ config, lib, pkgs, utils, ... }:
+
+let
+  inherit (lib) mkDefault mkEnableOption mkIf mkOption types literalExample;
+  cfg = config.services.engelsystem;
+in {
+  options = {
+    services.engelsystem = {
+      enable = mkOption {
+        default = false;
+        example = true;
+        description = ''
+          Whether to enable engelsystem, an online tool for coordinating helpers
+          and shifts on large events.
+        '';
+        type = lib.types.bool;
+      };
+
+      domain = mkOption {
+        type = types.str;
+        example = "engelsystem.example.com";
+        description = "Domain to serve on.";
+      };
+
+      package = mkOption {
+        type = types.package;
+        example = literalExample "pkgs.engelsystem";
+        description = "Engelsystem package used for the service.";
+        default = pkgs.engelsystem;
+      };
+
+      createDatabase = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to create a local database automatically.
+          This will override every database setting in <option>services.engelsystem.config</option>.
+        '';
+      };
+    };
+
+    services.engelsystem.config = mkOption {
+      type = types.attrs;
+      default = {
+        database = {
+          host = "localhost";
+          database = "engelsystem";
+          username = "engelsystem";
+        };
+      };
+      example = {
+        maintenance = false;
+        database = {
+          host = "database.example.com";
+          database = "engelsystem";
+          username = "engelsystem";
+          password._secret = "/var/keys/engelsystem/database";
+        };
+        email = {
+          driver = "smtp";
+          host = "smtp.example.com";
+          port = 587;
+          from.address = "engelsystem@example.com";
+          from.name = "example engelsystem";
+          encryption = "tls";
+          username = "engelsystem@example.com";
+          password._secret = "/var/keys/engelsystem/mail";
+        };
+        autoarrive = true;
+        min_password_length = 6;
+        default_locale = "de_DE";
+      };
+      description = ''
+        Options to be added to config.php, as a nix attribute set. Options containing secret data
+        should be set to an attribute set containing the attribute _secret - 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 config.php file, the email.password key will be set to
+        the contents of the /var/keys/engelsystem/mail file.
+
+        See https://engelsystem.de/doc/admin/configuration/ for available options.
+
+        Note that the admin user login credentials cannot be set here - they always default to
+        admin:asdfasdf. Log in and change them immediately.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    # create database
+    services.mysql = mkIf cfg.createDatabase {
+      enable = true;
+      package = mkDefault pkgs.mysql;
+      ensureUsers = [{
+        name = "engelsystem";
+        ensurePermissions = { "engelsystem.*" = "ALL PRIVILEGES"; };
+      }];
+      ensureDatabases = [ "engelsystem" ];
+    };
+
+    environment.etc."engelsystem/config.php".source =
+      pkgs.writeText "config.php" ''
+        <?php
+        return json_decode(file_get_contents("/var/lib/engelsystem/config.json"), true);
+      '';
+
+    services.phpfpm.pools.engelsystem = {
+      user = "engelsystem";
+      settings = {
+        "listen.owner" = config.services.nginx.user;
+        "pm" = "dynamic";
+        "pm.max_children" = 32;
+        "pm.max_requests" = 500;
+        "pm.start_servers" = 2;
+        "pm.min_spare_servers" = 2;
+        "pm.max_spare_servers" = 5;
+        "php_admin_value[error_log]" = "stderr";
+        "php_admin_flag[log_errors]" = true;
+        "catch_workers_output" = true;
+      };
+    };
+
+    services.nginx = {
+      enable = true;
+      virtualHosts."${cfg.domain}".locations = {
+        "/" = {
+          root = "${cfg.package}/share/engelsystem/public";
+          extraConfig = ''
+            index index.php;
+            try_files $uri $uri/ /index.php?$args;
+            autoindex off;
+          '';
+        };
+        "~ \\.php$" = {
+          root = "${cfg.package}/share/engelsystem/public";
+          extraConfig = ''
+            fastcgi_pass unix:${config.services.phpfpm.pools.engelsystem.socket};
+            fastcgi_index index.php;
+            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+            include ${config.services.nginx.package}/conf/fastcgi_params;
+            include ${config.services.nginx.package}/conf/fastcgi.conf;
+          '';
+        };
+      };
+    };
+
+    systemd.services."engelsystem-init" = {
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = { Type = "oneshot"; };
+      script =
+        let
+          genConfigScript = pkgs.writeScript "engelsystem-gen-config.sh"
+            (utils.genJqSecretsReplacementSnippet cfg.config "config.json");
+        in ''
+          umask 077
+          mkdir -p /var/lib/engelsystem/storage/app
+          mkdir -p /var/lib/engelsystem/storage/cache/views
+          cd /var/lib/engelsystem
+          ${genConfigScript}
+          chmod 400 config.json
+          chown -R engelsystem .
+      '';
+    };
+    systemd.services."engelsystem-migrate" = {
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "oneshot";
+        User = "engelsystem";
+        Group = "engelsystem";
+      };
+      script = ''
+        ${cfg.package}/bin/migrate
+      '';
+      after = [ "engelsystem-init.service" "mysql.service" ];
+    };
+    systemd.services."phpfpm-engelsystem".after =
+      [ "engelsystem-migrate.service" ];
+
+    users.users.engelsystem = {
+      isSystemUser = true;
+      createHome = true;
+      home = "/var/lib/engelsystem/storage";
+      group = "engelsystem";
+    };
+    users.groups.engelsystem = { };
+  };
+}
diff --git a/nixos/modules/services/web-apps/gerrit.nix b/nixos/modules/services/web-apps/gerrit.nix
index b184c0754d4..657b1a4fc5b 100644
--- a/nixos/modules/services/web-apps/gerrit.nix
+++ b/nixos/modules/services/web-apps/gerrit.nix
@@ -17,6 +17,10 @@ let
     lib.generators.toGitINI cfg.settings
   );
 
+  replicationConfig = pkgs.writeText "replication.conf" (
+    lib.generators.toGitINI cfg.replicationSettings
+  );
+
   # Wrap the gerrit java with all the java options so it can be called
   # like a normal CLI app
   gerrit-cli = pkgs.writeShellScriptBin "gerrit" ''
@@ -106,6 +110,15 @@ in
         '';
       };
 
+      replicationSettings = mkOption {
+        type = gitIniType;
+        default = {};
+        description = ''
+          Replication configuration. This will be generated to the
+          <literal>etc/replication.config</literal> file.
+        '';
+      };
+
       plugins = mkOption {
         type = types.listOf types.package;
         default = [];
@@ -138,6 +151,13 @@ in
 
   config = mkIf cfg.enable {
 
+    assertions = [
+      {
+        assertion = cfg.replicationSettings != {} -> elem "replication" cfg.builtinPlugins;
+        message = "Gerrit replicationSettings require enabling the replication plugin";
+      }
+    ];
+
     services.gerrit.settings = {
       cache.directory = "/var/cache/gerrit";
       container.heapLimit = cfg.jvmHeapLimit;
@@ -194,6 +214,7 @@ in
 
         # copy the config, keep it mutable because Gerrit
         ln -sfv ${gerritConfig} etc/gerrit.config
+        ln -sfv ${replicationConfig} etc/replication.config
 
         # install the plugins
         rm -rf plugins
diff --git a/nixos/modules/services/web-apps/jitsi-meet.nix b/nixos/modules/services/web-apps/jitsi-meet.nix
new file mode 100644
index 00000000000..3b2b2440491
--- /dev/null
+++ b/nixos/modules/services/web-apps/jitsi-meet.nix
@@ -0,0 +1,333 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.jitsi-meet;
+
+  # The configuration files are JS of format "var <<string>> = <<JSON>>;". In order to
+  # override only some settings, we need to extract the JSON, use jq to merge it with
+  # the config provided by user, and then reconstruct the file.
+  overrideJs =
+    source: varName: userCfg: appendExtra:
+    let
+      extractor = pkgs.writeText "extractor.js" ''
+        var fs = require("fs");
+        eval(fs.readFileSync(process.argv[2], 'utf8'));
+        process.stdout.write(JSON.stringify(eval(process.argv[3])));
+      '';
+      userJson = pkgs.writeText "user.json" (builtins.toJSON userCfg);
+    in (pkgs.runCommand "${varName}.js" { } ''
+      ${pkgs.nodejs}/bin/node ${extractor} ${source} ${varName} > default.json
+      (
+        echo "var ${varName} = "
+        ${pkgs.jq}/bin/jq -s '.[0] * .[1]' default.json ${userJson}
+        echo ";"
+        echo ${escapeShellArg appendExtra}
+      ) > $out
+    '');
+
+  # Essential config - it's probably not good to have these as option default because
+  # types.attrs doesn't do merging. Let's merge explicitly, can still be overriden if
+  # user desires.
+  defaultCfg = {
+    hosts = {
+      domain = cfg.hostName;
+      muc = "conference.${cfg.hostName}";
+      focus = "focus.${cfg.hostName}";
+    };
+    bosh = "//${cfg.hostName}/http-bind";
+  };
+in
+{
+  options.services.jitsi-meet = with types; {
+    enable = mkEnableOption "Jitsi Meet - Secure, Simple and Scalable Video Conferences";
+
+    hostName = mkOption {
+      type = str;
+      example = "meet.example.org";
+      description = ''
+        Hostname of the Jitsi Meet instance.
+      '';
+    };
+
+    config = mkOption {
+      type = attrs;
+      default = { };
+      example = literalExample ''
+        {
+          enableWelcomePage = false;
+          defaultLang = "fi";
+        }
+      '';
+      description = ''
+        Client-side web application settings that override the defaults in <filename>config.js</filename>.
+
+        See <link xlink:href="https://github.com/jitsi/jitsi-meet/blob/master/config.js" /> for default
+        configuration with comments.
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = lines;
+      default = "";
+      description = ''
+        Text to append to <filename>config.js</filename> web application config file.
+
+        Can be used to insert JavaScript logic to determine user's region in cascading bridges setup.
+      '';
+    };
+
+    interfaceConfig = mkOption {
+      type = attrs;
+      default = { };
+      example = literalExample ''
+        {
+          SHOW_JITSI_WATERMARK = false;
+          SHOW_WATERMARK_FOR_GUESTS = false;
+        }
+      '';
+      description = ''
+        Client-side web-app interface settings that override the defaults in <filename>interface_config.js</filename>.
+
+        See <link xlink:href="https://github.com/jitsi/jitsi-meet/blob/master/interface_config.js" /> for
+        default configuration with comments.
+      '';
+    };
+
+    videobridge = {
+      enable = mkOption {
+        type = bool;
+        default = true;
+        description = ''
+          Whether to enable Jitsi Videobridge instance and configure it to connect to Prosody.
+
+          Additional configuration is possible with <option>services.jitsi-videobridge</option>.
+        '';
+      };
+
+      passwordFile = mkOption {
+        type = nullOr str;
+        default = null;
+        example = "/run/keys/videobridge";
+        description = ''
+          File containing password to the Prosody account for videobridge.
+
+          If <literal>null</literal>, a file with password will be generated automatically. Setting
+          this option is useful if you plan to connect additional videobridges to the XMPP server.
+        '';
+      };
+    };
+
+    jicofo.enable = mkOption {
+      type = bool;
+      default = true;
+      description = ''
+        Whether to enable JiCoFo instance and configure it to connect to Prosody.
+
+        Additional configuration is possible with <option>services.jicofo</option>.
+      '';
+    };
+
+    nginx.enable = mkOption {
+      type = bool;
+      default = true;
+      description = ''
+        Whether to enable nginx virtual host that will serve the javascript application and act as
+        a proxy for the XMPP server. Further nginx configuration can be done by adapting
+        <option>services.nginx.virtualHosts.&lt;hostName&gt;</option>.
+        When this is enabled, ACME will be used to retrieve a TLS certificate by default. To disable
+        this, set the <option>services.nginx.virtualHosts.&lt;hostName&gt;.enableACME</option> to
+        <literal>false</literal> and if appropriate do the same for
+        <option>services.nginx.virtualHosts.&lt;hostName&gt;.forceSSL</option>.
+      '';
+    };
+
+    prosody.enable = mkOption {
+      type = bool;
+      default = true;
+      description = ''
+        Whether to configure Prosody to relay XMPP messages between Jitsi Meet components. Turn this
+        off if you want to configure it manually.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.prosody = mkIf cfg.prosody.enable {
+      enable = mkDefault true;
+      xmppComplianceSuite = mkDefault false;
+      modules = {
+        admin_adhoc = mkDefault false;
+        bosh = mkDefault true;
+        ping = mkDefault true;
+        roster = mkDefault true;
+        saslauth = mkDefault true;
+        tls = mkDefault true;
+      };
+      muc = [
+        {
+          domain = "conference.${cfg.hostName}";
+          name = "Jitsi Meet MUC";
+          roomLocking = false;
+          roomDefaultPublicJids = true;
+          extraConfig = ''
+            storage = "memory"
+          '';
+        }
+        {
+          domain = "internal.${cfg.hostName}";
+          name = "Jitsi Meet Videobridge MUC";
+          extraConfig = ''
+            storage = "memory"
+            admins = { "focus@auth.${cfg.hostName}", "jvb@auth.${cfg.hostName}" }
+          '';
+          #-- muc_room_cache_size = 1000
+        }
+      ];
+      extraModules = [ "pubsub" ];
+      extraConfig = mkAfter ''
+        Component "focus.${cfg.hostName}"
+          component_secret = os.getenv("JICOFO_COMPONENT_SECRET")
+      '';
+      virtualHosts.${cfg.hostName} = {
+        enabled = true;
+        domain = cfg.hostName;
+        extraConfig = ''
+          authentication = "anonymous"
+          c2s_require_encryption = false
+          admins = { "focus@auth.${cfg.hostName}" }
+        '';
+        ssl = {
+          cert = "/var/lib/jitsi-meet/jitsi-meet.crt";
+          key = "/var/lib/jitsi-meet/jitsi-meet.key";
+        };
+      };
+      virtualHosts."auth.${cfg.hostName}" = {
+        enabled = true;
+        domain = "auth.${cfg.hostName}";
+        extraConfig = ''
+          authentication = "internal_plain"
+        '';
+        ssl = {
+          cert = "/var/lib/jitsi-meet/jitsi-meet.crt";
+          key = "/var/lib/jitsi-meet/jitsi-meet.key";
+        };
+      };
+    };
+    systemd.services.prosody.serviceConfig = mkIf cfg.prosody.enable {
+      EnvironmentFile = [ "/var/lib/jitsi-meet/secrets-env" ];
+      SupplementaryGroups = [ "jitsi-meet" ];
+    };
+
+    users.groups.jitsi-meet = {};
+    systemd.tmpfiles.rules = [
+      "d '/var/lib/jitsi-meet' 0750 root jitsi-meet - -"
+    ];
+
+    systemd.services.jitsi-meet-init-secrets = {
+      wantedBy = [ "multi-user.target" ];
+      before = [ "jicofo.service" "jitsi-videobridge2.service" ] ++ (optional cfg.prosody.enable "prosody.service");
+      serviceConfig = {
+        Type = "oneshot";
+      };
+
+      script = let
+        secrets = [ "jicofo-component-secret" "jicofo-user-secret" ] ++ (optional (cfg.videobridge.passwordFile == null) "videobridge-secret");
+        videobridgeSecret = if cfg.videobridge.passwordFile != null then cfg.videobridge.passwordFile else "/var/lib/jitsi-meet/videobridge-secret";
+      in
+      ''
+        cd /var/lib/jitsi-meet
+        ${concatMapStringsSep "\n" (s: ''
+          if [ ! -f ${s} ]; then
+            tr -dc a-zA-Z0-9 </dev/urandom | head -c 64 > ${s}
+            chown root:jitsi-meet ${s}
+            chmod 640 ${s}
+          fi
+        '') secrets}
+
+        # for easy access in prosody
+        echo "JICOFO_COMPONENT_SECRET=$(cat jicofo-component-secret)" > secrets-env
+        chown root:jitsi-meet secrets-env
+        chmod 640 secrets-env
+      ''
+      + optionalString cfg.prosody.enable ''
+        ${config.services.prosody.package}/bin/prosodyctl register focus auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jicofo-user-secret)"
+        ${config.services.prosody.package}/bin/prosodyctl register jvb auth.${cfg.hostName} "$(cat ${videobridgeSecret})"
+
+        # generate self-signed certificates
+        if [ ! -f /var/lib/jitsi-meet.crt ]; then
+          ${getBin pkgs.openssl}/bin/openssl req \
+            -x509 \
+            -newkey rsa:4096 \
+            -keyout /var/lib/jitsi-meet/jitsi-meet.key \
+            -out /var/lib/jitsi-meet/jitsi-meet.crt \
+            -days 36500 \
+            -nodes \
+            -subj '/CN=${cfg.hostName}/CN=auth.${cfg.hostName}'
+          chmod 640 /var/lib/jitsi-meet/jitsi-meet.{crt,key}
+          chown root:jitsi-meet /var/lib/jitsi-meet/jitsi-meet.{crt,key}
+        fi
+      '';
+    };
+
+    services.nginx = mkIf cfg.nginx.enable {
+      enable = mkDefault true;
+      virtualHosts.${cfg.hostName} = {
+        enableACME = mkDefault true;
+        forceSSL = mkDefault true;
+        root = pkgs.jitsi-meet;
+        extraConfig = ''
+          ssi on;
+        '';
+        locations."@root_path".extraConfig = ''
+          rewrite ^/(.*)$ / break;
+        '';
+        locations."~ ^/([^/\\?&:'\"]+)$".tryFiles = "$uri @root_path";
+        locations."=/http-bind" = {
+          proxyPass = "http://localhost:5280/http-bind";
+          extraConfig = ''
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+            proxy_set_header Host $host;
+          '';
+        };
+        locations."=/external_api.js" = mkDefault {
+          alias = "${pkgs.jitsi-meet}/libs/external_api.min.js";
+        };
+        locations."=/config.js" = mkDefault {
+          alias = overrideJs "${pkgs.jitsi-meet}/config.js" "config" (recursiveUpdate defaultCfg cfg.config) cfg.extraConfig;
+        };
+        locations."=/interface_config.js" = mkDefault {
+          alias = overrideJs "${pkgs.jitsi-meet}/interface_config.js" "interfaceConfig" cfg.interfaceConfig "";
+        };
+      };
+    };
+
+    services.jitsi-videobridge = mkIf cfg.videobridge.enable {
+      enable = true;
+      xmppConfigs."localhost" = {
+        userName = "jvb";
+        domain = "auth.${cfg.hostName}";
+        passwordFile = "/var/lib/jitsi-meet/videobridge-secret";
+        mucJids = "jvbbrewery@internal.${cfg.hostName}";
+        disableCertificateVerification = true;
+      };
+    };
+
+    services.jicofo = mkIf cfg.jicofo.enable {
+      enable = true;
+      xmppHost = "localhost";
+      xmppDomain = cfg.hostName;
+      userDomain = "auth.${cfg.hostName}";
+      userName = "focus";
+      userPasswordFile = "/var/lib/jitsi-meet/jicofo-user-secret";
+      componentPasswordFile = "/var/lib/jitsi-meet/jicofo-component-secret";
+      bridgeMuc = "jvbbrewery@internal.${cfg.hostName}";
+      config = {
+        "org.jitsi.jicofo.ALWAYS_TRUST_MODE_ENABLED" = "true";
+      };
+    };
+  };
+
+  meta.maintainers = lib.teams.jitsi.members;
+}
diff --git a/nixos/modules/services/web-apps/moodle.nix b/nixos/modules/services/web-apps/moodle.nix
index 1196780cf6e..f45eaa24d54 100644
--- a/nixos/modules/services/web-apps/moodle.nix
+++ b/nixos/modules/services/web-apps/moodle.nix
@@ -40,7 +40,7 @@ let
   $CFG->disableupdateautodeploy = true;
 
   $CFG->pathtogs = '${pkgs.ghostscript}/bin/gs';
-  $CFG->pathtophp = '${pkgs.php}/bin/php';
+  $CFG->pathtophp = '${phpExt}/bin/php';
   $CFG->pathtodu = '${pkgs.coreutils}/bin/du';
   $CFG->aspellpath = '${pkgs.aspell}/bin/aspell';
   $CFG->pathtodot = '${pkgs.graphviz}/bin/dot';
@@ -55,6 +55,9 @@ let
 
   mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql";
   pgsqlLocal = cfg.database.createLocally && cfg.database.type == "pgsql";
+
+  phpExt = pkgs.php.withExtensions
+        ({ enabled, all }: with all; [ iconv mbstring curl openssl tokenizer xmlrpc soap ctype zip gd simplexml dom  intl json sqlite3 pgsql pdo_sqlite pdo_pgsql pdo_odbc pdo_mysql pdo mysqli session zlib xmlreader fileinfo ]);
 in
 {
   # interface
@@ -222,6 +225,7 @@ in
 
     services.phpfpm.pools.moodle = {
       inherit user group;
+      phpPackage = phpExt;
       phpEnv.MOODLE_CONFIG = "${moodleConfig}";
       phpOptions = ''
         zend_extension = opcache.so
@@ -263,13 +267,13 @@ in
       after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
       environment.MOODLE_CONFIG = moodleConfig;
       script = ''
-        ${pkgs.php}/bin/php ${cfg.package}/share/moodle/admin/cli/check_database_schema.php && rc=$? || rc=$?
+        ${phpExt}/bin/php ${cfg.package}/share/moodle/admin/cli/check_database_schema.php && rc=$? || rc=$?
 
-        [ "$rc" == 1 ] && ${pkgs.php}/bin/php ${cfg.package}/share/moodle/admin/cli/upgrade.php \
+        [ "$rc" == 1 ] && ${phpExt}/bin/php ${cfg.package}/share/moodle/admin/cli/upgrade.php \
           --non-interactive \
           --allow-unstable
 
-        [ "$rc" == 2 ] && ${pkgs.php}/bin/php ${cfg.package}/share/moodle/admin/cli/install_database.php \
+        [ "$rc" == 2 ] && ${phpExt}/bin/php ${cfg.package}/share/moodle/admin/cli/install_database.php \
           --agree-license \
           --adminpass=${cfg.initialPassword}
 
@@ -289,7 +293,7 @@ in
       serviceConfig = {
         User = user;
         Group = group;
-        ExecStart = "${pkgs.php}/bin/php ${cfg.package}/share/moodle/admin/cli/cron.php";
+        ExecStart = "${phpExt}/bin/php ${cfg.package}/share/moodle/admin/cli/cron.php";
       };
     };
 
diff --git a/nixos/modules/services/web-apps/nextcloud.nix b/nixos/modules/services/web-apps/nextcloud.nix
index f826096bf60..7da119758fc 100644
--- a/nixos/modules/services/web-apps/nextcloud.nix
+++ b/nixos/modules/services/web-apps/nextcloud.nix
@@ -34,7 +34,7 @@ let
     cd ${cfg.package}
     sudo=exec
     if [[ "$USER" != nextcloud ]]; then
-      sudo='exec /run/wrappers/bin/sudo -u nextcloud --preserve-env=NEXTCLOUD_CONFIG_DIR'
+      sudo='exec /run/wrappers/bin/sudo -u nextcloud --preserve-env=NEXTCLOUD_CONFIG_DIR --preserve-env=OC_PASS'
     fi
     export NEXTCLOUD_CONFIG_DIR="${cfg.home}/config"
     $sudo \
@@ -45,6 +45,22 @@ let
   inherit (config.system) stateVersion;
 
 in {
+
+  imports = [
+    (mkRemovedOptionModule [ "services" "nextcloud" "nginx" "enable" ] ''
+      The nextcloud module supports `nginx` as 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
+        * setting `listen.owner` & `listen.group` in the phpfpm-pool to a different value
+
+      Further details about this can be found in the `Nextcloud`-section of the NixOS-manual
+      (which can be openend e.g. by running `nixos-help`).
+    '')
+  ];
+
   options.services.nextcloud = {
     enable = mkEnableOption "nextcloud";
     hostName = mkOption {
@@ -69,7 +85,7 @@ in {
     package = mkOption {
       type = types.package;
       description = "Which package to use for the Nextcloud instance.";
-      relatedPackages = [ "nextcloud17" "nextcloud18" ];
+      relatedPackages = [ "nextcloud17" "nextcloud18" "nextcloud19" ];
     };
 
     maxUploadSize = mkOption {
@@ -91,16 +107,6 @@ in {
       '';
     };
 
-    nginx.enable = mkOption {
-      type = types.bool;
-      default = false;
-      description = ''
-        Whether to enable nginx virtual host management.
-        Further nginx configuration can be done by adapting <literal>services.nginx.virtualHosts.&lt;name&gt;</literal>.
-        See <xref linkend="opt-services.nginx.virtualHosts"/> for further information.
-      '';
-    };
-
     webfinger = mkOption {
       type = types.bool;
       default = false;
@@ -303,6 +309,14 @@ in {
         '';
       };
     };
+    occ = mkOption {
+      type = types.package;
+      default = occ;
+      internal = true;
+      description = ''
+        The nextcloud-occ program preconfigured to target this Nextcloud instance.
+      '';
+    };
   };
 
   config = mkIf cfg.enable (mkMerge [
@@ -336,7 +350,16 @@ in {
              server, and wait until the upgrade to 17 is finished.
 
           Then, set `services.nextcloud.package` to `pkgs.nextcloud18` to upgrade to
-          Nextcloud version 18.
+          Nextcloud version 18. Please note that Nextcloud 19 is already out and it's
+          recommended to upgrade to nextcloud19 after that.
+        '')
+        ++ (optional (versionOlder cfg.package.version "19") ''
+          A legacy Nextcloud install (from before NixOS 20.09/unstable) may be installed.
+
+          If/After nextcloud18 is installed successfully, you can safely upgrade to
+          nextcloud19. If not, please upgrade to nextcloud18 first since Nextcloud doesn't
+          support upgrades that skip multiple versions (i.e. an upgrade from 17 to 19 isn't
+          possible, but an upgrade from 18 to 19).
         '');
 
       services.nextcloud.package = with pkgs;
@@ -348,7 +371,8 @@ in {
               `pkgs.nextcloud`.
             ''
           else if versionOlder stateVersion "20.03" then nextcloud17
-          else nextcloud18
+          else if versionOlder stateVersion "20.09" then nextcloud18
+          else nextcloud19
         );
     }
 
@@ -360,6 +384,11 @@ in {
       };
 
       systemd.services = {
+        # When upgrading the Nextcloud package, Nextcloud can report errors such as
+        # "The files of the app [all apps in /var/lib/nextcloud/apps] were not replaced correctly"
+        # Restarting phpfpm on Nextcloud package update fixes these issues (but this is a workaround).
+        phpfpm-nextcloud.restartTriggers = [ cfg.package ];
+
         nextcloud-setup = let
           c = cfg.config;
           writePhpArrary = a: "[${concatMapStringsSep "," (val: ''"${toString val}"'') a}]";
@@ -445,10 +474,18 @@ in {
           script = ''
             chmod og+x ${cfg.home}
             ln -sf ${cfg.package}/apps ${cfg.home}/
-            mkdir -p ${cfg.home}/config ${cfg.home}/data ${cfg.home}/store-apps
-            ln -sf ${overrideConfig} ${cfg.home}/config/override.config.php
 
-            chown -R nextcloud:nginx ${cfg.home}/config ${cfg.home}/data ${cfg.home}/store-apps
+            # create nextcloud directories.
+            # if the directories exist already with wrong permissions, we fix that
+            for dir in ${cfg.home}/config ${cfg.home}/data ${cfg.home}/store-apps; do
+              if [ ! -e $dir ]; then
+                install -o nextcloud -g nextcloud -d $dir
+              elif [ $(stat -c "%G" $dir) != "nextcloud" ]; then
+                chgrp -R nextcloud $dir
+              fi
+            done
+
+            ln -sf ${overrideConfig} ${cfg.home}/config/override.config.php
 
             # Do not install if already installed
             if [[ ! -e ${cfg.home}/config/config.php ]]; then
@@ -461,6 +498,7 @@ in {
             ${occSetTrustedDomainsCmd}
           '';
           serviceConfig.Type = "oneshot";
+          serviceConfig.User = "nextcloud";
         };
         nextcloud-cron = {
           environment.NEXTCLOUD_CONFIG_DIR = "${cfg.home}/config";
@@ -479,7 +517,7 @@ in {
       services.phpfpm = {
         pools.nextcloud = {
           user = "nextcloud";
-          group = "nginx";
+          group = "nextcloud";
           phpOptions = phpOptionsStr;
           phpPackage = phpPackage;
           phpEnv = {
@@ -487,128 +525,121 @@ in {
             PATH = "/run/wrappers/bin:/nix/var/nix/profiles/default/bin:/run/current-system/sw/bin:/usr/bin:/bin";
           };
           settings = mapAttrs (name: mkDefault) {
-            "listen.owner" = "nginx";
-            "listen.group" = "nginx";
+            "listen.owner" = config.services.nginx.user;
+            "listen.group" = config.services.nginx.group;
           } // cfg.poolSettings;
           extraConfig = cfg.poolConfig;
         };
       };
 
-      users.extraUsers.nextcloud = {
+      users.users.nextcloud = {
         home = "${cfg.home}";
-        group = "nginx";
+        group = "nextcloud";
         createHome = true;
       };
+      users.groups.nextcloud.members = [ "nextcloud" config.services.nginx.user ];
 
       environment.systemPackages = [ occ ];
-    }
 
-    (mkIf cfg.nginx.enable {
-      services.nginx = {
-        enable = true;
-        virtualHosts = {
-          ${cfg.hostName} = {
-            root = cfg.package;
-            locations = {
-              "= /robots.txt" = {
-                priority = 100;
-                extraConfig = ''
-                  allow all;
-                  log_not_found off;
-                  access_log off;
-                '';
-              };
-              "/" = {
-                priority = 200;
-                extraConfig = "rewrite ^ /index.php;";
-              };
-              "~ ^/store-apps" = {
-                priority = 201;
-                extraConfig = "root ${cfg.home};";
-              };
-              "= /.well-known/carddav" = {
-                priority = 210;
-                extraConfig = "return 301 $scheme://$host/remote.php/dav;";
-              };
-              "= /.well-known/caldav" = {
-                priority = 210;
-                extraConfig = "return 301 $scheme://$host/remote.php/dav;";
-              };
-              "~ ^\\/(?:build|tests|config|lib|3rdparty|templates|data)\\/" = {
-                priority = 300;
-                extraConfig = "deny all;";
-              };
-              "~ ^\\/(?:\\.|autotest|occ|issue|indie|db_|console)" = {
-                priority = 300;
-                extraConfig = "deny all;";
-              };
-              "~ ^\\/(?:index|remote|public|cron|core/ajax\\/update|status|ocs\\/v[12]|updater\\/.+|ocs-provider\\/.+|ocm-provider\\/.+)\\.php(?:$|\\/)" = {
-                priority = 500;
-                extraConfig = ''
-                  include ${config.services.nginx.package}/conf/fastcgi.conf;
-                  fastcgi_split_path_info ^(.+\.php)(\\/.*)$;
-                  try_files $fastcgi_script_name =404;
-                  fastcgi_param PATH_INFO $fastcgi_path_info;
-                  fastcgi_param HTTPS ${if cfg.https then "on" else "off"};
-                  fastcgi_param modHeadersAvailable true;
-                  fastcgi_param front_controller_active true;
-                  fastcgi_pass unix:${fpm.socket};
-                  fastcgi_intercept_errors on;
-                  fastcgi_request_buffering off;
-                  fastcgi_read_timeout 120s;
-                '';
-              };
-              "~ ^\\/(?:updater|ocs-provider|ocm-provider)(?:$|\\/)".extraConfig = ''
-                try_files $uri/ =404;
-                index index.php;
-              '';
-              "~ \\.(?:css|js|woff2?|svg|gif)$".extraConfig = ''
-                try_files $uri /index.php$request_uri;
-                add_header Cache-Control "public, max-age=15778463";
-                add_header X-Content-Type-Options nosniff;
-                add_header X-XSS-Protection "1; mode=block";
-                add_header X-Robots-Tag none;
-                add_header X-Download-Options noopen;
-                add_header X-Permitted-Cross-Domain-Policies none;
-                add_header X-Frame-Options sameorigin;
-                add_header Referrer-Policy no-referrer;
-                access_log off;
-              '';
-              "~ \\.(?:png|html|ttf|ico|jpg|jpeg|bcmap|mp4|webm)$".extraConfig = ''
-                try_files $uri /index.php$request_uri;
-                access_log off;
-              '';
-            };
+      services.nginx.enable = mkDefault true;
+      services.nginx.virtualHosts.${cfg.hostName} = {
+        root = cfg.package;
+        locations = {
+          "= /robots.txt" = {
+            priority = 100;
+            extraConfig = ''
+              allow all;
+              log_not_found off;
+              access_log off;
+            '';
+          };
+          "/" = {
+            priority = 900;
+            extraConfig = "try_files $uri $uri/ /index.php$request_uri;";
+          };
+          "~ ^/store-apps" = {
+            priority = 201;
+            extraConfig = "root ${cfg.home};";
+          };
+          "^~ /.well-known" = {
+            priority = 210;
+            extraConfig = ''
+              location = /.well-known/carddav {
+                return 301 $scheme://$host/remote.php/dav;
+              }
+              location = /.well-known/caldav {
+                return 301 $scheme://$host/remote.php/dav;
+              }
+              try_files $uri $uri/ =404;
+            '';
+          };
+          "~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/)".extraConfig = ''
+            return 404;
+          '';
+          "~ ^/(?:\\.|autotest|occ|issue|indie|db_|console)".extraConfig = ''
+            return 404;
+          '';
+          "~ \\.php(?:$|/)" = {
+            priority = 500;
             extraConfig = ''
-              add_header X-Content-Type-Options nosniff;
-              add_header X-XSS-Protection "1; mode=block";
-              add_header X-Robots-Tag none;
-              add_header X-Download-Options noopen;
-              add_header X-Permitted-Cross-Domain-Policies none;
-              add_header X-Frame-Options sameorigin;
-              add_header Referrer-Policy no-referrer;
-              add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
-              error_page 403 /core/templates/403.php;
-              error_page 404 /core/templates/404.php;
-              client_max_body_size ${cfg.maxUploadSize};
-              fastcgi_buffers 64 4K;
-              fastcgi_hide_header X-Powered-By;
-              gzip on;
-              gzip_vary on;
-              gzip_comp_level 4;
-              gzip_min_length 256;
-              gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;
-              gzip_types application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy;
-
-              ${optionalString cfg.webfinger ''
-                rewrite ^/.well-known/host-meta /public.php?service=host-meta last;
-                rewrite ^/.well-known/host-meta.json /public.php?service=host-meta-json last;
-              ''}
+              include ${config.services.nginx.package}/conf/fastcgi.conf;
+              fastcgi_split_path_info ^(.+?\.php)(\\/.*)$;
+              set $path_info $fastcgi_path_info;
+              try_files $fastcgi_script_name =404;
+              fastcgi_param PATH_INFO $path_info;
+              fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+              fastcgi_param HTTPS ${if cfg.https then "on" else "off"};
+              fastcgi_param modHeadersAvailable true;
+              fastcgi_param front_controller_active true;
+              fastcgi_pass unix:${fpm.socket};
+              fastcgi_intercept_errors on;
+              fastcgi_request_buffering off;
+              fastcgi_read_timeout 120s;
             '';
           };
+          "~ \\.(?:css|js|svg|gif|map)$".extraConfig = ''
+            try_files $uri /index.php$request_uri;
+            expires 6M;
+            access_log off;
+          '';
+          "~ \\.woff2?$".extraConfig = ''
+            try_files $uri /index.php$request_uri;
+            expires 7d;
+            access_log off;
+          '';
+          "~ ^\\/(?:updater|ocs-provider|ocm-provider)(?:$|\\/)".extraConfig = ''
+            try_files $uri/ =404;
+            index index.php;
+          '';
         };
+        extraConfig = ''
+          index index.php index.html /index.php$request_uri;
+          expires 1m;
+          add_header X-Content-Type-Options nosniff;
+          add_header X-XSS-Protection "1; mode=block";
+          add_header X-Robots-Tag none;
+          add_header X-Download-Options noopen;
+          add_header X-Permitted-Cross-Domain-Policies none;
+          add_header X-Frame-Options sameorigin;
+          add_header Referrer-Policy no-referrer;
+          add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
+          client_max_body_size ${cfg.maxUploadSize};
+          fastcgi_buffers 64 4K;
+          fastcgi_hide_header X-Powered-By;
+          gzip on;
+          gzip_vary on;
+          gzip_comp_level 4;
+          gzip_min_length 256;
+          gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;
+          gzip_types application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy;
+
+          ${optionalString cfg.webfinger ''
+            rewrite ^/.well-known/host-meta /public.php?service=host-meta last;
+            rewrite ^/.well-known/host-meta.json /public.php?service=host-meta-json last;
+          ''}
+        '';
       };
-    })
+    }
   ]);
 
   meta.doc = ./nextcloud.xml;
diff --git a/nixos/modules/services/web-apps/nextcloud.xml b/nixos/modules/services/web-apps/nextcloud.xml
index fc454f8ba25..02e4dba2861 100644
--- a/nixos/modules/services/web-apps/nextcloud.xml
+++ b/nixos/modules/services/web-apps/nextcloud.xml
@@ -29,7 +29,6 @@
   services.nextcloud = {
     <link linkend="opt-services.nextcloud.enable">enable</link> = true;
     <link linkend="opt-services.nextcloud.hostName">hostName</link> = "nextcloud.tld";
-    <link linkend="opt-services.nextcloud.nginx.enable">nginx.enable</link> = true;
     config = {
       <link linkend="opt-services.nextcloud.config.dbtype">dbtype</link> = "pgsql";
       <link linkend="opt-services.nextcloud.config.dbuser">dbuser</link> = "nextcloud";
@@ -61,9 +60,8 @@
   </para>
 
   <para>
-   The options <literal>hostName</literal> and <literal>nginx.enable</literal>
-   are used internally to configure an HTTP server using
-   <literal><link xlink:href="https://php-fpm.org/">PHP-FPM</link></literal>
+   The <literal>hostName</literal> option is used internally to configure an HTTP
+   server using <literal><link xlink:href="https://php-fpm.org/">PHP-FPM</link></literal>
    and <literal>nginx</literal>. The <literal>config</literal> attribute set is
    used by the imperative installer and all values are written to an additional file
    to ensure that changes can be applied by changing the module's options.
@@ -125,6 +123,61 @@
   </para>
  </section>
 
+ <section xml:id="module-services-nextcloud-httpd">
+  <title>Using an alternative webserver as reverse-proxy (e.g. <literal>httpd</literal>)</title>
+  <para>
+   By default, <package>nginx</package> is used as reverse-proxy for <package>nextcloud</package>.
+   However, it's possible to use e.g. <package>httpd</package> by explicitly disabling
+   <package>nginx</package> using <xref linkend="opt-services.nginx.enable" /> and fixing the
+   settings <literal>listen.owner</literal> &amp; <literal>listen.group</literal> in the
+   <link linkend="opt-services.phpfpm.pools">corresponding <literal>phpfpm</literal> pool</link>.
+  </para>
+  <para>
+   An exemplary configuration may look like this:
+<programlisting>{ config, lib, pkgs, ... }: {
+  <link linkend="opt-services.nginx.enable">services.nginx.enable</link> = false;
+  services.nextcloud = {
+    <link linkend="opt-services.nextcloud.enable">enable</link> = true;
+    <link linkend="opt-services.nextcloud.hostName">hostName</link> = "localhost";
+
+    /* further, required options */
+  };
+  <link linkend="opt-services.phpfpm.pools._name_.settings">services.phpfpm.pools.nextcloud.settings</link> = {
+    "listen.owner" = config.services.httpd.user;
+    "listen.group" = config.services.httpd.group;
+  };
+  services.httpd = {
+    <link linkend="opt-services.httpd.enable">enable</link> = true;
+    <link linkend="opt-services.httpd.adminAddr">adminAddr</link> = "webmaster@localhost";
+    <link linkend="opt-services.httpd.extraModules">extraModules</link> = [ "proxy_fcgi" ];
+    virtualHosts."localhost" = {
+      <link linkend="opt-services.httpd.virtualHosts._name_.documentRoot">documentRoot</link> = config.services.nextcloud.package;
+      <link linkend="opt-services.httpd.virtualHosts._name_.extraConfig">extraConfig</link> = ''
+        &lt;Directory "${config.services.nextcloud.package}"&gt;
+          &lt;FilesMatch "\.php$"&gt;
+            &lt;If "-f %{REQUEST_FILENAME}"&gt;
+              SetHandler "proxy:unix:${config.services.phpfpm.pools.nextcloud.socket}|fcgi://localhost/"
+            &lt;/If&gt;
+          &lt;/FilesMatch&gt;
+          &lt;IfModule mod_rewrite.c&gt;
+            RewriteEngine On
+            RewriteBase /
+            RewriteRule ^index\.php$ - [L]
+            RewriteCond %{REQUEST_FILENAME} !-f
+            RewriteCond %{REQUEST_FILENAME} !-d
+            RewriteRule . /index.php [L]
+          &lt;/IfModule&gt;
+          DirectoryIndex index.php
+          Require all granted
+          Options +FollowSymLinks
+        &lt;/Directory&gt;
+      '';
+    };
+  };
+}</programlisting>
+  </para>
+ </section>
+
  <section xml:id="module-services-nextcloud-maintainer-info">
   <title>Maintainer information</title>
 
@@ -161,5 +214,11 @@
   };
 }</programlisting>
   </para>
+
+  <para>
+   Ideally we should make sure that it's possible to jump two NixOS versions forward:
+   i.e. the warnings and the logic in the module should guard a user to upgrade from a
+   Nextcloud on e.g. 19.09 to a Nextcloud on 20.09.
+  </para>
  </section>
 </chapter>
diff --git a/nixos/modules/services/web-apps/pgpkeyserver-lite.nix b/nixos/modules/services/web-apps/pgpkeyserver-lite.nix
index ad70ba70bbe..838fd19ad29 100644
--- a/nixos/modules/services/web-apps/pgpkeyserver-lite.nix
+++ b/nixos/modules/services/web-apps/pgpkeyserver-lite.nix
@@ -33,7 +33,7 @@ in
         description = "
           Which hostname to set the vHost to that is proxying to sks.
         ";
-      };     
+      };
 
       hkpAddress = mkOption {
         default = builtins.head sksCfg.hkpAddress;
diff --git a/nixos/modules/services/web-apps/rss-bridge.nix b/nixos/modules/services/web-apps/rss-bridge.nix
new file mode 100644
index 00000000000..f1d5b7660f3
--- /dev/null
+++ b/nixos/modules/services/web-apps/rss-bridge.nix
@@ -0,0 +1,127 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.rss-bridge;
+
+  poolName = "rss-bridge";
+
+  whitelist = pkgs.writeText "rss-bridge_whitelist.txt"
+    (concatStringsSep "\n" cfg.whitelist);
+in
+{
+  options = {
+    services.rss-bridge = {
+      enable = mkEnableOption "rss-bridge";
+
+      user = mkOption {
+        type = types.str;
+        default = "nginx";
+        example = "nginx";
+        description = ''
+          User account under which both the service and the web-application run.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "nginx";
+        example = "nginx";
+        description = ''
+          Group under which the web-application run.
+        '';
+      };
+
+      pool = mkOption {
+        type = types.str;
+        default = poolName;
+        description = ''
+          Name of existing phpfpm pool that is used to run web-application.
+          If not specified a pool will be created automatically with
+          default values.
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/rss-bridge";
+        description = ''
+          Location in which cache directory will be created.
+          You can put <literal>config.ini.php</literal> in here.
+        '';
+      };
+
+      virtualHost = mkOption {
+        type = types.nullOr types.str;
+        default = "rss-bridge";
+        description = ''
+          Name of the nginx virtualhost to use and setup. If null, do not setup any virtualhost.
+        '';
+      };
+
+      whitelist = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = options.literalExample ''
+          [
+            "Facebook"
+            "Instagram"
+            "Twitter"
+          ]
+        '';
+        description = ''
+          List of bridges to be whitelisted.
+          If the list is empty, rss-bridge will use whitelist.default.txt.
+          Use <literal>[ "*" ]</literal> to whitelist all.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.phpfpm.pools = mkIf (cfg.pool == poolName) {
+      ${poolName} = {
+        user = cfg.user;
+        settings = mapAttrs (name: mkDefault) {
+          "listen.owner" = cfg.user;
+          "listen.group" = cfg.user;
+          "listen.mode" = "0600";
+          "pm" = "dynamic";
+          "pm.max_children" = 75;
+          "pm.start_servers" = 10;
+          "pm.min_spare_servers" = 5;
+          "pm.max_spare_servers" = 20;
+          "pm.max_requests" = 500;
+          "catch_workers_output" = 1;
+        };
+      };
+    };
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}/cache' 0750 ${cfg.user} ${cfg.group} - -"
+      (mkIf (cfg.whitelist != []) "L+ ${cfg.dataDir}/whitelist.txt - - - - ${whitelist}")
+      "z '${cfg.dataDir}/config.ini.php' 0750 ${cfg.user} ${cfg.group} - -"
+    ];
+
+    services.nginx = mkIf (cfg.virtualHost != null) {
+      enable = true;
+      virtualHosts = {
+        ${cfg.virtualHost} = {
+          root = "${pkgs.rss-bridge}";
+
+          locations."/" = {
+            tryFiles = "$uri /index.php$is_args$args";
+          };
+
+          locations."~ ^/index.php(/|$)" = {
+            extraConfig = ''
+              include ${pkgs.nginx}/conf/fastcgi_params;
+              fastcgi_split_path_info ^(.+\.php)(/.+)$;
+              fastcgi_pass unix:${config.services.phpfpm.pools.${cfg.pool}.socket};
+              fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+              fastcgi_param RSSBRIDGE_DATA ${cfg.dataDir};
+            '';
+          };
+        };
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/sogo.nix b/nixos/modules/services/web-apps/sogo.nix
new file mode 100644
index 00000000000..5f30124dd68
--- /dev/null
+++ b/nixos/modules/services/web-apps/sogo.nix
@@ -0,0 +1,272 @@
+{ config, pkgs, lib, ... }: with lib; let
+  cfg = config.services.sogo;
+
+  preStart = pkgs.writeShellScriptBin "sogo-prestart" ''
+    touch /etc/sogo/sogo.conf
+    chown sogo:sogo /etc/sogo/sogo.conf
+    chmod 640 /etc/sogo/sogo.conf
+
+    ${if (cfg.configReplaces != {}) then ''
+      # Insert secrets
+      ${concatStringsSep "\n" (mapAttrsToList (k: v: ''export ${k}="$(cat "${v}" | tr -d '\n')"'') cfg.configReplaces)}
+
+      ${pkgs.perl}/bin/perl -p ${concatStringsSep " " (mapAttrsToList (k: v: '' -e 's/${k}/''${ENV{"${k}"}}/g;' '') cfg.configReplaces)} /etc/sogo/sogo.conf.raw > /etc/sogo/sogo.conf
+    '' else ''
+      cp /etc/sogo/sogo.conf.raw /etc/sogo/sogo.conf
+    ''}
+  '';
+
+in {
+  options.services.sogo = with types; {
+    enable = mkEnableOption "SOGo groupware";
+
+    vhostName = mkOption {
+      description = "Name of the nginx vhost";
+      type = str;
+      default = "sogo";
+    };
+
+    timezone = mkOption {
+      description = "Timezone of your SOGo instance";
+      type = str;
+      example = "America/Montreal";
+    };
+
+    language = mkOption {
+      description = "Language of SOGo";
+      type = str;
+      default = "English";
+    };
+
+    ealarmsCredFile = mkOption {
+      description = "Optional path to a credentials file for email alarms";
+      type = nullOr str;
+      default = null;
+    };
+
+    configReplaces = mkOption {
+      description = ''
+        Replacement-filepath mapping for sogo.conf.
+        Every key is replaced with the contents of the file specified as value.
+
+        In the example, every occurence of LDAP_BINDPW will be replaced with the text of the
+        specified file.
+      '';
+      type = attrsOf str;
+      default = {};
+      example = {
+        LDAP_BINDPW = "/var/lib/secrets/sogo/ldappw";
+      };
+    };
+
+    extraConfig = mkOption {
+      description = "Extra sogo.conf configuration lines";
+      type = lines;
+      default = "";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.sogo ];
+
+    environment.etc."sogo/sogo.conf.raw".text = ''
+      {
+        // Mandatory parameters
+        SOGoTimeZone = "${cfg.timezone}";
+        SOGoLanguage = "${cfg.language}";
+        // Paths
+        WOSendMail = "/run/wrappers/bin/sendmail";
+        SOGoMailSpoolPath = "/var/lib/sogo/spool";
+        SOGoZipPath = "${pkgs.zip}/bin/zip";
+        // Enable CSRF protection
+        SOGoXSRFValidationEnabled = YES;
+        // Remove dates from log (jornald does that)
+        NGLogDefaultLogEventFormatterClass = "NGLogEventFormatter";
+        // Extra config
+        ${cfg.extraConfig}
+      }
+    '';
+
+    systemd.services.sogo = {
+      description = "SOGo groupware";
+      after = [ "postgresql.service" "mysql.service" "memcached.service" "openldap.service" "dovecot2.service" ];
+      wantedBy = [ "multi-user.target" ];
+      restartTriggers = [ config.environment.etc."sogo/sogo.conf.raw".source ];
+
+      environment.LDAPTLS_CACERT = "/etc/ssl/certs/ca-certificates.crt";
+
+      serviceConfig = {
+        Type = "forking";
+        ExecStartPre = "+" + preStart + "/bin/sogo-prestart";
+        ExecStart = "${pkgs.sogo}/bin/sogod -WOLogFile - -WOPidFile /run/sogo/sogo.pid";
+
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        RuntimeDirectory = "sogo";
+        StateDirectory = "sogo/spool";
+
+        User = "sogo";
+        Group = "sogo";
+
+        CapabilityBoundingSet = "";
+        NoNewPrivileges = true;
+
+        LockPersonality = true;
+        RestrictRealtime = true;
+        PrivateMounts = true;
+        PrivateUsers = true;
+        MemoryDenyWriteExecute = true;
+        SystemCallFilter = "@basic-io @file-system @network-io @system-service @timer";
+        SystemCallArchitectures = "native";
+        RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
+      };
+    };
+
+    systemd.services.sogo-tmpwatch = {
+      description = "SOGo tmpwatch";
+
+      startAt = [ "hourly" ];
+      script = ''
+        SOGOSPOOL=/var/lib/sogo/spool
+
+        find "$SOGOSPOOL" -type f -user sogo -atime +23 -delete > /dev/null
+        find "$SOGOSPOOL" -mindepth 1 -type d -user sogo -empty -delete > /dev/null
+      '';
+
+      serviceConfig = {
+        Type = "oneshot";
+
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        StateDirectory = "sogo/spool";
+
+        User = "sogo";
+        Group = "sogo";
+
+        CapabilityBoundingSet = "";
+        NoNewPrivileges = true;
+
+        LockPersonality = true;
+        RestrictRealtime = true;
+        PrivateMounts = true;
+        PrivateUsers = true;
+        PrivateNetwork = true;
+        SystemCallFilter = "@basic-io @file-system @system-service";
+        SystemCallArchitectures = "native";
+        RestrictAddressFamilies = "";
+      };
+    };
+
+    systemd.services.sogo-ealarms = {
+      description = "SOGo email alarms";
+
+      after = [ "postgresql.service" "mysqld.service" "memcached.service" "openldap.service" "dovecot2.service" "sogo.service" ];
+      restartTriggers = [ config.environment.etc."sogo/sogo.conf.raw".source ];
+
+      startAt = [ "minutely" ];
+
+      serviceConfig = {
+        Type = "oneshot";
+        ExecStart = "${pkgs.sogo}/bin/sogo-ealarms-notify${optionalString (cfg.ealarmsCredFile != null) " -p ${cfg.ealarmsCredFile}"}";
+
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        StateDirectory = "sogo/spool";
+
+        User = "sogo";
+        Group = "sogo";
+
+        CapabilityBoundingSet = "";
+        NoNewPrivileges = true;
+
+        LockPersonality = true;
+        RestrictRealtime = true;
+        PrivateMounts = true;
+        PrivateUsers = true;
+        MemoryDenyWriteExecute = true;
+        SystemCallFilter = "@basic-io @file-system @network-io @system-service";
+        SystemCallArchitectures = "native";
+        RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
+      };
+    };
+
+    # nginx vhost
+    services.nginx.virtualHosts."${cfg.vhostName}" = {
+      locations."/".extraConfig = ''
+        rewrite ^ https://$server_name/SOGo;
+        allow all;
+      '';
+
+      # For iOS 7
+      locations."/principals/".extraConfig = ''
+        rewrite ^ https://$server_name/SOGo/dav;
+        allow all;
+      '';
+
+      locations."^~/SOGo".extraConfig = ''
+        proxy_pass http://127.0.0.1:20000;
+        proxy_redirect http://127.0.0.1:20000 default;
+
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header Host $host;
+        proxy_set_header x-webobjects-server-protocol HTTP/1.0;
+        proxy_set_header x-webobjects-remote-host 127.0.0.1;
+        proxy_set_header x-webobjects-server-port $server_port;
+        proxy_set_header x-webobjects-server-name $server_name;
+        proxy_set_header x-webobjects-server-url $scheme://$host;
+        proxy_connect_timeout 90;
+        proxy_send_timeout 90;
+        proxy_read_timeout 90;
+        proxy_buffer_size 4k;
+        proxy_buffers 4 32k;
+        proxy_busy_buffers_size 64k;
+        proxy_temp_file_write_size 64k;
+        client_max_body_size 50m;
+        client_body_buffer_size 128k;
+        break;
+      '';
+
+      locations."/SOGo.woa/WebServerResources/".extraConfig = ''
+        alias ${pkgs.sogo}/lib/GNUstep/SOGo/WebServerResources/;
+        allow all;
+      '';
+
+      locations."/SOGo/WebServerResources/".extraConfig = ''
+        alias ${pkgs.sogo}/lib/GNUstep/SOGo/WebServerResources/;
+        allow all;
+      '';
+
+      locations."~ ^/SOGo/so/ControlPanel/Products/([^/]*)/Resources/(.*)$".extraConfig = ''
+        alias ${pkgs.sogo}/lib/GNUstep/SOGo/$1.SOGo/Resources/$2;
+      '';
+
+      locations."~ ^/SOGo/so/ControlPanel/Products/[^/]*UI/Resources/.*\\.(jpg|png|gif|css|js)$".extraConfig = ''
+        alias ${pkgs.sogo}/lib/GNUstep/SOGo/$1.SOGo/Resources/$2;
+      '';
+    };
+
+    # User and group
+    users.groups.sogo = {};
+    users.users.sogo = {
+      group = "sogo";
+      isSystemUser = true;
+      description = "SOGo service user";
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/trilium.nix b/nixos/modules/services/web-apps/trilium.nix
index 6f47193c62b..3fa8dad0490 100644
--- a/nixos/modules/services/web-apps/trilium.nix
+++ b/nixos/modules/services/web-apps/trilium.nix
@@ -83,7 +83,7 @@ in
     };
   };
 
-  config = lib.mkIf cfg.enable (lib.mkMerge [ 
+  config = lib.mkIf cfg.enable (lib.mkMerge [
   {
     meta.maintainers = with lib.maintainers; [ kampka ];
 
diff --git a/nixos/modules/services/web-apps/tt-rss.nix b/nixos/modules/services/web-apps/tt-rss.nix
index b92e3449894..6a29f10d119 100644
--- a/nixos/modules/services/web-apps/tt-rss.nix
+++ b/nixos/modules/services/web-apps/tt-rss.nix
@@ -631,9 +631,10 @@ let
         serviceConfig = {
           User = "${cfg.user}";
           Group = "tt_rss";
-          ExecStart = "${pkgs.php}/bin/php ${cfg.root}/update.php --daemon";
-          StandardOutput = "syslog";
-          StandardError = "syslog";
+          ExecStart = "${pkgs.php}/bin/php ${cfg.root}/update.php --daemon --quiet";
+          Restart = "on-failure";
+          RestartSec = "60";
+          SyslogIdentifier = "tt-rss";
         };
 
         wantedBy = [ "multi-user.target" ];
diff --git a/nixos/modules/services/web-servers/apache-httpd/default.nix b/nixos/modules/services/web-servers/apache-httpd/default.nix
index 8abee7130d7..fc4c2945394 100644
--- a/nixos/modules/services/web-servers/apache-httpd/default.nix
+++ b/nixos/modules/services/web-servers/apache-httpd/default.nix
@@ -10,6 +10,12 @@ let
 
   pkg = cfg.package.out;
 
+  apachectl = pkgs.runCommand "apachectl" { meta.priority = -1; } ''
+    mkdir -p $out/bin
+    cp ${pkg}/bin/apachectl $out/bin/apachectl
+    sed -i $out/bin/apachectl -e 's|$HTTPD -t|$HTTPD -t -f ${httpdConf}|'
+  '';
+
   httpdConf = cfg.configFile;
 
   php = cfg.phpPackage.override { apacheHttpd = pkg; };
@@ -650,10 +656,29 @@ in
       postRun = "systemctl reload httpd.service";
     }) (filterAttrs (name: hostOpts: hostOpts.enableACME) cfg.virtualHosts);
 
-    environment.systemPackages = [ pkg ];
+    environment.systemPackages = [
+      apachectl
+      pkg
+    ];
 
-    # required for "apachectl configtest"
-    environment.etc."httpd/httpd.conf".source = httpdConf;
+    services.logrotate = optionalAttrs (cfg.logFormat != "none") {
+      enable = mkDefault true;
+      paths.httpd = {
+        path = "${cfg.logDir}/*.log";
+        user = cfg.user;
+        group = cfg.group;
+        frequency = "daily";
+        keep = 28;
+        extraConfig = ''
+          sharedscripts
+          compress
+          delaycompress
+          postrotate
+            systemctl reload httpd.service > /dev/null 2>/dev/null || true
+          endscript
+        '';
+      };
+    };
 
     services.httpd.phpOptions =
       ''
@@ -708,6 +733,7 @@ in
         wantedBy = [ "multi-user.target" ];
         wants = concatLists (map (hostOpts: [ "acme-${hostOpts.hostName}.service" "acme-selfsigned-${hostOpts.hostName}.service" ]) vhostsACME);
         after = [ "network.target" "fs.target" ] ++ map (hostOpts: "acme-selfsigned-${hostOpts.hostName}.service") vhostsACME;
+        before = map (hostOpts: "acme-${hostOpts.hostName}.service") vhostsACME;
 
         path = [ pkg pkgs.coreutils pkgs.gnugrep ];
 
diff --git a/nixos/modules/services/web-servers/jboss/builder.sh b/nixos/modules/services/web-servers/jboss/builder.sh
index 2eb89a90f67..0e5af324c13 100644
--- a/nixos/modules/services/web-servers/jboss/builder.sh
+++ b/nixos/modules/services/web-servers/jboss/builder.sh
@@ -28,11 +28,11 @@ stop()
 if test "\$1" = start
 then
   trap stop 15
-  
+
   start
 elif test "\$1" = stop
 then
-  stop  
+  stop
 elif test "\$1" = init
 then
   echo "Are you sure you want to create a new server instance (old server instance will be lost!)?"
@@ -42,21 +42,21 @@ then
   then
     exit 1
   fi
-  
+
   rm -rf $serverDir
   mkdir -p $serverDir
   cd $serverDir
   cp -av $jboss/server/default .
   sed -i -e "s|deploy/|$deployDir|" default/conf/jboss-service.xml
-  
+
   if ! test "$useJK" = ""
   then
     sed -i -e 's|<attribute name="UseJK">false</attribute>|<attribute name="UseJK">true</attribute>|' default/deploy/jboss-web.deployer/META-INF/jboss-service.xml
     sed -i -e 's|<Engine name="jboss.web" defaultHost="localhost">|<Engine name="jboss.web" defaultHost="localhost" jvmRoute="node1">|' default/deploy/jboss-web.deployer/server.xml
   fi
-  
+
   # Make files accessible for the server user
-  
+
   chown -R $user $serverDir
   for i in \`find $serverDir -type d\`
   do
diff --git a/nixos/modules/services/web-servers/meguca.nix b/nixos/modules/services/web-servers/meguca.nix
deleted file mode 100644
index 5a00070dc94..00000000000
--- a/nixos/modules/services/web-servers/meguca.nix
+++ /dev/null
@@ -1,174 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-let
-  cfg = config.services.meguca;
-  postgres = config.services.postgresql;
-in with lib; {
-  options.services.meguca = {
-    enable = mkEnableOption "meguca";
-
-    dataDir = mkOption {
-      type = types.path;
-      default = "/var/lib/meguca";
-      example = "/home/okina/meguca";
-      description = "Location where meguca stores it's database and links.";
-    };
-
-    password = mkOption {
-      type = types.str;
-      default = "meguca";
-      example = "dumbpass";
-      description = "Password for the meguca database.";
-    };
-
-    passwordFile = mkOption {
-      type = types.path;
-      default = "/run/keys/meguca-password-file";
-      example = "/home/okina/meguca/keys/pass";
-      description = "Password file for the meguca database.";
-    };
-
-    reverseProxy = mkOption {
-      type = types.nullOr types.str;
-      default = null;
-      example = "192.168.1.5";
-      description = "Reverse proxy IP.";
-    };
-
-    sslCertificate = mkOption {
-      type = types.nullOr types.str;
-      default = null;
-      example = "/home/okina/meguca/ssl.cert";
-      description = "Path to the SSL certificate.";
-    };
-
-    listenAddress = mkOption {
-      type = types.nullOr types.str;
-      default = null;
-      example = "127.0.0.1:8000";
-      description = "Listen on a specific IP address and port.";
-    };
-
-    cacheSize = mkOption {
-      type = types.nullOr types.int;
-      default = null;
-      example = 256;
-      description = "Cache size in MB.";
-    };
-
-    postgresArgs = mkOption {
-      type = types.str;
-      example = "user=meguca password=dumbpass dbname=meguca sslmode=disable";
-      description = "Postgresql connection arguments.";
-    };
-
-    postgresArgsFile = mkOption {
-      type = types.path;
-      default = "/run/keys/meguca-postgres-args";
-      example = "/home/okina/meguca/keys/postgres";
-      description = "Postgresql connection arguments file.";
-    };
-
-    compressTraffic = mkOption {
-      type = types.bool;
-      default = false;
-      description = "Compress all traffic with gzip.";
-    };
-
-    assumeReverseProxy = mkOption {
-      type = types.bool;
-      default = false;
-      description = "Assume the server is behind a reverse proxy, when resolving client IPs.";
-    };
-
-    httpsOnly = mkOption {
-      type = types.bool;
-      default = false;
-      description = "Serve and listen only through HTTPS.";
-    };
-
-    videoPaths = mkOption {
-      type = types.listOf types.path;
-      default = [];
-      example = [ "/home/okina/Videos/tehe_pero.webm" ];
-      description = "Videos that will be symlinked into www/videos.";
-    };
-  };
-
-  config = mkIf cfg.enable {
-    security.sudo.enable = cfg.enable;
-    services.postgresql.enable = cfg.enable;
-    services.postgresql.package = pkgs.postgresql_11;
-    services.meguca.passwordFile = mkDefault (pkgs.writeText "meguca-password-file" cfg.password);
-    services.meguca.postgresArgsFile = mkDefault (pkgs.writeText "meguca-postgres-args" cfg.postgresArgs);
-    services.meguca.postgresArgs = mkDefault "user=meguca password=${cfg.password} dbname=meguca sslmode=disable";
-
-    systemd.services.meguca = {
-      description = "meguca";
-      after = [ "network.target" "postgresql.service" ];
-      wantedBy = [ "multi-user.target" ];
-
-      preStart = ''
-        # Ensure folder exists or create it and links and permissions are correct
-        mkdir -p ${escapeShellArg cfg.dataDir}/www
-        rm -rf ${escapeShellArg cfg.dataDir}/www/videos
-        ln -sf ${pkgs.meguca}/share/meguca/www/* ${escapeShellArg cfg.dataDir}/www
-        unlink ${escapeShellArg cfg.dataDir}/www/videos
-        mkdir -p ${escapeShellArg cfg.dataDir}/www/videos
-
-        for vid in ${escapeShellArg cfg.videoPaths}; do
-          ln -sf $vid ${escapeShellArg cfg.dataDir}/www/videos
-        done
-
-        chmod 750 ${escapeShellArg cfg.dataDir}
-        chown -R meguca:meguca ${escapeShellArg cfg.dataDir}
-
-        # Ensure the database is correct or create it
-        ${pkgs.sudo}/bin/sudo -u ${postgres.superUser} ${postgres.package}/bin/createuser \
-          -SDR meguca || true
-        ${pkgs.sudo}/bin/sudo -u ${postgres.superUser} ${postgres.package}/bin/createdb \
-          -T template0 -E UTF8 -O meguca meguca || true
-        ${pkgs.sudo}/bin/sudo -u meguca ${postgres.package}/bin/psql \
-          -c "ALTER ROLE meguca WITH PASSWORD '$(cat ${escapeShellArg cfg.passwordFile})';" || true
-      '';
-
-    script = ''
-      cd ${escapeShellArg cfg.dataDir}
-
-      ${pkgs.meguca}/bin/meguca -d "$(cat ${escapeShellArg cfg.postgresArgsFile})"''
-      + optionalString (cfg.reverseProxy != null) " -R ${cfg.reverseProxy}"
-      + optionalString (cfg.sslCertificate != null) " -S ${cfg.sslCertificate}"
-      + optionalString (cfg.listenAddress != null) " -a ${cfg.listenAddress}"
-      + optionalString (cfg.cacheSize != null) " -c ${toString cfg.cacheSize}"
-      + optionalString (cfg.compressTraffic) " -g"
-      + optionalString (cfg.assumeReverseProxy) " -r"
-      + optionalString (cfg.httpsOnly) " -s" + " start";
-
-      serviceConfig = {
-        PermissionsStartOnly = true;
-        Type = "forking";
-        User = "meguca";
-        Group = "meguca";
-        ExecStop = "${pkgs.meguca}/bin/meguca stop";
-      };
-    };
-
-    users = {
-      groups.meguca.gid = config.ids.gids.meguca;
-
-      users.meguca = {
-        description = "meguca server service user";
-        home = cfg.dataDir;
-        createHome = true;
-        group = "meguca";
-        uid = config.ids.uids.meguca;
-      };
-    };
-  };
-
-  imports = [
-    (mkRenamedOptionModule [ "services" "meguca" "baseDir" ] [ "services" "meguca" "dataDir" ])
-  ];
-
-  meta.maintainers = with maintainers; [ chiiruno ];
-}
diff --git a/nixos/modules/services/web-servers/molly-brown.nix b/nixos/modules/services/web-servers/molly-brown.nix
new file mode 100644
index 00000000000..e9052a184b2
--- /dev/null
+++ b/nixos/modules/services/web-servers/molly-brown.nix
@@ -0,0 +1,117 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.molly-brown;
+
+  settingsType = with types;
+    attrsOf (oneOf [
+      int
+      str
+      (listOf str)
+      (attrsOf (oneOf [ int str (listOf str) (attrsOf str) ]))
+    ]) // {
+      description = "primitive expression convertable to TOML";
+    };
+
+  configFile = pkgs.runCommand "molly-brown.toml" {
+    buildInputs = [ pkgs.remarshal ];
+    preferLocalBuild = true;
+    passAsFile = [ "settings" ];
+    settings = builtins.toJSON cfg.settings;
+  } "remarshal -if json -of toml < $settingsPath > $out";
+in {
+
+  options.services.molly-brown = {
+
+    enable = mkEnableOption "Molly-Brown Gemini server";
+
+    port = mkOption {
+      default = 1965;
+      type = types.port;
+      description = ''
+        TCP port for molly-brown to bind to.
+      '';
+    };
+
+    hostName = mkOption {
+      type = types.str;
+      example = literalExample "config.networking.hostName";
+      default = config.networking.hostName;
+      description = ''
+        The hostname to respond to requests for. Requests for URLs with
+        other hosts will result in a status 53 (PROXY REQUEST REFUSED)
+        response.
+      '';
+    };
+
+    certPath = mkOption {
+      type = types.path;
+      example = "/var/lib/acme/example.com/cert.pem";
+      description = ''
+        Path to TLS certificate. An ACME certificate and key may be
+        shared with an HTTP server, but only if molly-brown has
+        permissions allowing it to read such keys.
+
+        As an example:
+        <programlisting>
+        security.acme.certs."example.com".allowKeysForGroup = true;
+        systemd.services.molly-brown.serviceConfig.SupplementaryGroups =
+          [ config.security.acme.certs."example.com".group ];
+        </programlisting>
+      '';
+    };
+
+    keyPath = mkOption {
+      type = types.path;
+      example = "/var/lib/acme/example.com/key.pem";
+      description = "Path to TLS key. See <option>CertPath</option>.";
+    };
+
+    docBase = mkOption {
+      type = types.path;
+      example = "/var/lib/molly-brown";
+      description = "Base directory for Gemini content.";
+    };
+
+    settings = mkOption {
+      type = settingsType;
+      default = { };
+      description = ''
+        molly-brown configuration. Refer to
+        <link xlink:href="https://tildegit.org/solderpunk/molly-brown/src/branch/master/example.conf"/>
+        for details on supported values.
+      '';
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    services.molly-brown.settings = let logDir = "/var/log/molly-brown";
+    in {
+      Port = cfg.port;
+      Hostname = cfg.hostName;
+      CertPath = cfg.certPath;
+      KeyPath = cfg.keyPath;
+      DocBase = cfg.docBase;
+      AccessLog = "${logDir}/access.log";
+      ErrorLog = "${logDir}/error.log";
+    };
+
+    systemd.services.molly-brown = {
+      description = "Molly Brown gemini server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        DynamicUser = true;
+        LogsDirectory = "molly-brown";
+        ExecStart = "${pkgs.molly-brown}/bin/molly-brown -c ${configFile}";
+        Restart = "always";
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/web-servers/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix
index 1e9cda7e478..461888c4cc4 100644
--- a/nixos/modules/services/web-servers/nginx/default.nix
+++ b/nixos/modules/services/web-servers/nginx/default.nix
@@ -463,6 +463,14 @@ in
         '';
       };
 
+      enableSandbox = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Starting Nginx web server with additional sandbox/hardening options.
+        '';
+      };
+
       user = mkOption {
         type = types.str;
         default = "nginx";
@@ -685,6 +693,10 @@ in
       wantedBy = [ "multi-user.target" ];
       wants = concatLists (map (vhostConfig: ["acme-${vhostConfig.serverName}.service" "acme-selfsigned-${vhostConfig.serverName}.service"]) acmeEnabledVhosts);
       after = [ "network.target" ] ++ map (vhostConfig: "acme-selfsigned-${vhostConfig.serverName}.service") acmeEnabledVhosts;
+      # Nginx needs to be started in order to be able to request certificates
+      # (it's hosting the acme challenge after all)
+      # This fixes https://github.com/NixOS/nixpkgs/issues/81842
+      before = map (vhostConfig: "acme-${vhostConfig.serverName}.service") acmeEnabledVhosts;
       stopIfChanged = false;
       preStart = ''
         ${cfg.preStart}
@@ -692,7 +704,10 @@ in
       '';
       serviceConfig = {
         ExecStart = execCommand;
-        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        ExecReload = [
+          "${execCommand} -t"
+          "${pkgs.coreutils}/bin/kill -HUP $MAINPID"
+        ];
         Restart = "always";
         RestartSec = "10s";
         StartLimitInterval = "1min";
@@ -710,6 +725,27 @@ in
         LogsDirectoryMode = "0750";
         # Capabilities
         AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" "CAP_SYS_RESOURCE" ];
+        CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" "CAP_SYS_RESOURCE" ];
+        # Security
+        NoNewPrivileges = true;
+      } // optionalAttrs cfg.enableSandbox {
+        # Sandboxing
+        ProtectSystem = "strict";
+        ProtectHome = mkDefault true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectHostname = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+        LockPersonality = true;
+        MemoryDenyWriteExecute = !(builtins.any (mod: (mod.allowMemoryWriteExecute or false)) pkgs.nginx.modules);
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        PrivateMounts = true;
+        # System Call Filtering
+        SystemCallArchitectures = "native";
       };
     };
 
@@ -727,9 +763,8 @@ in
       serviceConfig.Type = "oneshot";
       serviceConfig.TimeoutSec = 60;
       script = ''
-        if ${pkgs.systemd}/bin/systemctl -q is-active nginx.service ; then
-          ${execCommand} -t && \
-            ${pkgs.systemd}/bin/systemctl reload nginx.service
+        if /run/current-system/systemd/bin/systemctl -q is-active nginx.service ; then
+          /run/current-system/systemd/bin/systemctl reload nginx.service
         fi
       '';
       serviceConfig.RemainAfterExit = true;
@@ -743,7 +778,7 @@ in
             webroot = vhostConfig.acmeRoot;
             extraDomains = genAttrs vhostConfig.serverAliases (alias: null);
             postRun = ''
-              systemctl reload nginx
+              /run/current-system/systemd/bin/systemctl reload nginx
             '';
           }; }) acmeEnabledVhosts;
       in
diff --git a/nixos/modules/services/web-servers/shellinabox.nix b/nixos/modules/services/web-servers/shellinabox.nix
index 58a02ac59c3..c7c51f873eb 100644
--- a/nixos/modules/services/web-servers/shellinabox.nix
+++ b/nixos/modules/services/web-servers/shellinabox.nix
@@ -51,7 +51,7 @@ in
           Whether or not to enable SSL (https) support.
         '';
       };
-        
+
       certDirectory = mkOption {
         type = types.nullOr types.path;
         default = null;
diff --git a/nixos/modules/services/web-servers/unit/default.nix b/nixos/modules/services/web-servers/unit/default.nix
index 989866144e1..894271d1e55 100644
--- a/nixos/modules/services/web-servers/unit/default.nix
+++ b/nixos/modules/services/web-servers/unit/default.nix
@@ -102,7 +102,7 @@ in {
         PIDFile = "/run/unit/unit.pid";
         ExecStart = ''
           ${cfg.package}/bin/unitd --control 'unix:/run/unit/control.unit.sock' --pid '/run/unit/unit.pid' \
-                                   --log '${cfg.logDir}/unit.log' --state '${cfg.stateDir}' \
+                                   --log '${cfg.logDir}/unit.log' --state '${cfg.stateDir}' --tmp '/tmp' \
                                    --user ${cfg.user} --group ${cfg.group}
         '';
         ExecStop = ''
@@ -120,9 +120,12 @@ in {
         ProtectHome = true;
         PrivateTmp = true;
         PrivateDevices = true;
+        PrivateUsers = false;
         ProtectHostname = true;
+        ProtectClock = true;
         ProtectKernelTunables = true;
         ProtectKernelModules = true;
+        ProtectKernelLogs = true;
         ProtectControlGroups = true;
         RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
         LockPersonality = true;
diff --git a/nixos/modules/services/web-servers/uwsgi.nix b/nixos/modules/services/web-servers/uwsgi.nix
index 4b74c329e3d..936e211ec71 100644
--- a/nixos/modules/services/web-servers/uwsgi.nix
+++ b/nixos/modules/services/web-servers/uwsgi.nix
@@ -79,7 +79,25 @@ in {
       };
 
       instance = mkOption {
-        type = types.attrs;
+        type =  with lib.types; let
+          valueType = nullOr (oneOf [
+            bool
+            int
+            float
+            str
+            (lazyAttrsOf valueType)
+            (listOf valueType)
+            (mkOptionType {
+              name = "function";
+              description = "function";
+              check = x: isFunction x;
+              merge = mergeOneOption;
+            })
+          ]) // {
+            description = "Json value or lambda";
+            emptyValue.value = {};
+          };
+        in valueType;
         default = {
           type = "normal";
         };
diff --git a/nixos/modules/services/x11/colord.nix b/nixos/modules/services/x11/colord.nix
index cf113ad2af8..31ccee6aa33 100644
--- a/nixos/modules/services/x11/colord.nix
+++ b/nixos/modules/services/x11/colord.nix
@@ -26,7 +26,7 @@ in {
 
     systemd.packages = [ pkgs.colord ];
 
-    environment.etc."tmpfiles.d/colord.conf".source = "${pkgs.colord}/lib/tmpfiles.d/colord.conf";
+    systemd.tmpfiles.packages = [ pkgs.colord ];
 
     users.users.colord = {
       isSystemUser = true;
diff --git a/nixos/modules/services/x11/desktop-managers/cde.nix b/nixos/modules/services/x11/desktop-managers/cde.nix
index c1b6d3bf064..2d9504fb5f1 100644
--- a/nixos/modules/services/x11/desktop-managers/cde.nix
+++ b/nixos/modules/services/x11/desktop-managers/cde.nix
@@ -8,9 +8,26 @@ let
 in {
   options.services.xserver.desktopManager.cde = {
     enable = mkEnableOption "Common Desktop Environment";
+
+    extraPackages = mkOption {
+      type = with types; listOf package;
+      default = with pkgs.xorg; [
+        xclock bitmap xlsfonts xfd xrefresh xload xwininfo xdpyinfo xwd xwud
+      ];
+      example = literalExample ''
+        with pkgs.xorg; [
+          xclock bitmap xlsfonts xfd xrefresh xload xwininfo xdpyinfo xwd xwud
+        ]
+      '';
+      description = ''
+        Extra packages to be installed system wide.
+      '';
+    };
   };
 
   config = mkIf (xcfg.enable && cfg.enable) {
+    environment.systemPackages = cfg.extraPackages;
+
     services.rpcbind.enable = true;
 
     services.xinetd.enable = true;
diff --git a/nixos/modules/services/x11/desktop-managers/default.nix b/nixos/modules/services/x11/desktop-managers/default.nix
index ea6aac9f6c9..5d3a84d7139 100644
--- a/nixos/modules/services/x11/desktop-managers/default.nix
+++ b/nixos/modules/services/x11/desktop-managers/default.nix
@@ -19,7 +19,7 @@ in
   # E.g., if Plasma 5 is enabled, it supersedes xterm.
   imports = [
     ./none.nix ./xterm.nix ./xfce.nix ./plasma5.nix ./lumina.nix
-    ./lxqt.nix ./enlightenment.nix ./gnome3.nix ./kodi.nix ./maxx.nix
+    ./lxqt.nix ./enlightenment.nix ./gnome3.nix ./kodi.nix
     ./mate.nix ./pantheon.nix ./surf-display.nix ./cde.nix
   ];
 
diff --git a/nixos/modules/services/x11/desktop-managers/enlightenment.nix b/nixos/modules/services/x11/desktop-managers/enlightenment.nix
index 1690a7d51a8..3a7ab64510b 100644
--- a/nixos/modules/services/x11/desktop-managers/enlightenment.nix
+++ b/nixos/modules/services/x11/desktop-managers/enlightenment.nix
@@ -32,15 +32,14 @@ in
 
   config = mkIf cfg.enable {
 
-    environment.systemPackages = [
-      e.efl e.enlightenment
-      e.terminology e.econnman
-      pkgs.xorg.xauth # used by kdesu
-      pkgs.gtk2 # To get GTK's themes.
-      pkgs.tango-icon-theme
-
-      pkgs.gnome-icon-theme
-      pkgs.xorg.xcursorthemes
+    environment.systemPackages = with pkgs; [
+      enlightenment.econnman
+      enlightenment.efl
+      enlightenment.enlightenment
+      enlightenment.ephoto
+      enlightenment.rage
+      enlightenment.terminology
+      xorg.xcursorthemes
     ];
 
     environment.pathsToLink = [
@@ -50,11 +49,10 @@ in
       "/share/locale"
     ];
 
-    services.xserver.desktopManager.session = [
-    { name = "Enlightenment";
-      start = ''
-        export XDG_MENU_PREFIX=e-
+    services.xserver.displayManager.sessionPackages = [ pkgs.enlightenment.enlightenment ];
 
+    services.xserver.displayManager.sessionCommands = ''
+      if test "$XDG_CURRENT_DESKTOP" = "Enlightenment"; then
         export GST_PLUGIN_PATH="${GST_PLUGIN_PATH}"
 
         # make available for D-BUS user services
@@ -62,12 +60,15 @@ in
 
         # Update user dirs as described in http://freedesktop.org/wiki/Software/xdg-user-dirs/
         ${pkgs.xdg-user-dirs}/bin/xdg-user-dirs-update
-
-        exec ${e.enlightenment}/bin/enlightenment_start
-      '';
-    }];
-
-    security.wrappers = (import "${e.enlightenment}/e-wrappers.nix").security.wrappers;
+      fi
+    '';
+
+    # Wrappers for programs installed by enlightenment that should be setuid
+    security.wrappers = {
+      enlightenment_ckpasswd.source = "${pkgs.enlightenment.enlightenment}/lib/enlightenment/utils/enlightenment_ckpasswd";
+      enlightenment_sys.source = "${pkgs.enlightenment.enlightenment}/lib/enlightenment/utils/enlightenment_sys";
+      enlightenment_system.source = "${pkgs.enlightenment.enlightenment}/lib/enlightenment/utils/enlightenment_system";
+    };
 
     environment.etc."X11/xkb".source = xcfg.xkbDir;
 
diff --git a/nixos/modules/services/x11/desktop-managers/gnome3.nix b/nixos/modules/services/x11/desktop-managers/gnome3.nix
index bbc7feb2d04..69cf9832172 100644
--- a/nixos/modules/services/x11/desktop-managers/gnome3.nix
+++ b/nixos/modules/services/x11/desktop-managers/gnome3.nix
@@ -37,10 +37,10 @@ let
      chmod -R a+w $out/share/gsettings-schemas/nixos-gsettings-overrides
      cat - > $out/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas/nixos-defaults.gschema.override <<- EOF
        [org.gnome.desktop.background]
-       picture-uri='file://${pkgs.nixos-artwork.wallpapers.simple-dark-gray}/share/artwork/gnome/nix-wallpaper-simple-dark-gray.png'
+       picture-uri='file://${pkgs.nixos-artwork.wallpapers.simple-dark-gray.gnomeFilePath}'
 
        [org.gnome.desktop.screensaver]
-       picture-uri='file://${pkgs.nixos-artwork.wallpapers.simple-dark-gray-bottom}/share/artwork/gnome/nix-wallpaper-simple-dark-gray_bottom.png'
+       picture-uri='file://${pkgs.nixos-artwork.wallpapers.simple-dark-gray-bottom.gnomeFilePath}'
 
        [org.gnome.shell]
        favorite-apps=[ 'org.gnome.Epiphany.desktop', 'org.gnome.Geary.desktop', 'org.gnome.Music.desktop', 'org.gnome.Photos.desktop', 'org.gnome.Nautilus.desktop', 'org.gnome.Software.desktop' ]
@@ -320,6 +320,8 @@ in
         gnome-shell
         gnome-shell-extensions
         gnome-themes-extra
+        pkgs.nixos-artwork.wallpapers.simple-dark-gray
+        pkgs.nixos-artwork.wallpapers.simple-dark-gray-bottom
         pkgs.gnome-user-docs
         pkgs.orca
         pkgs.glib # for gsettings
diff --git a/nixos/modules/services/x11/desktop-managers/lumina.nix b/nixos/modules/services/x11/desktop-managers/lumina.nix
index 2224bcd5a2a..419f5055d8b 100644
--- a/nixos/modules/services/x11/desktop-managers/lumina.nix
+++ b/nixos/modules/services/x11/desktop-managers/lumina.nix
@@ -23,12 +23,9 @@ in
 
   config = mkIf cfg.enable {
 
-    services.xserver.desktopManager.session = singleton {
-      name = "lumina";
-      start = ''
-        exec ${pkgs.lumina.lumina}/bin/start-lumina-desktop
-      '';
-    };
+    services.xserver.displayManager.sessionPackages = [
+      pkgs.lumina.lumina
+    ];
 
     environment.systemPackages =
       pkgs.lumina.preRequisitePackages ++
diff --git a/nixos/modules/services/x11/desktop-managers/maxx.nix b/nixos/modules/services/x11/desktop-managers/maxx.nix
deleted file mode 100644
index 1c04104df41..00000000000
--- a/nixos/modules/services/x11/desktop-managers/maxx.nix
+++ /dev/null
@@ -1,31 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-  xcfg = config.services.xserver;
-  cfg = xcfg.desktopManager.maxx;
-in {
-  options.services.xserver.desktopManager.maxx = {
-    enable = mkEnableOption "MaXX desktop environment";
-  };
-
-  config = mkIf cfg.enable {
-    environment.systemPackages = [ pkgs.maxx ];
-
-    # there is hardcoded path in binaries
-    system.activationScripts.setup-maxx = ''
-      mkdir -p /opt
-      ln -sfn ${pkgs.maxx}/opt/MaXX /opt
-    '';
-
-    services.xserver.desktopManager.session = [
-    { name = "MaXX";
-      start = ''
-        exec ${pkgs.maxx}/opt/MaXX/etc/skel/Xsession.dt
-      '';
-    }];
-  };
-
-  meta.maintainers = [ maintainers.gnidorah ];
-}
diff --git a/nixos/modules/services/x11/desktop-managers/pantheon.nix b/nixos/modules/services/x11/desktop-managers/pantheon.nix
index 5fcc8590232..6dabca6bf09 100644
--- a/nixos/modules/services/x11/desktop-managers/pantheon.nix
+++ b/nixos/modules/services/x11/desktop-managers/pantheon.nix
@@ -180,6 +180,7 @@ in
         gtk3.out
         hicolor-icon-theme
         lightlocker
+        nixos-artwork.wallpapers.simple-dark-gray
         onboard
         qgnomeplatform
         shared-mime-info
diff --git a/nixos/modules/services/x11/desktop-managers/plasma5.nix b/nixos/modules/services/x11/desktop-managers/plasma5.nix
index 60ef0159ff1..75bf55a2639 100644
--- a/nixos/modules/services/x11/desktop-managers/plasma5.nix
+++ b/nixos/modules/services/x11/desktop-managers/plasma5.nix
@@ -158,6 +158,19 @@ in
         example = "vlc";
         description = "Phonon audio backend to install.";
       };
+
+      supportDDC = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Support setting monitor brightness via DDC.
+          </para>
+          <para>
+          This is not needed for controlling brightness of the internal monitor
+          of a laptop and as it is considered experimental by upstream, it is
+          disabled by default.
+        '';
+      };
     };
 
   };
@@ -184,6 +197,12 @@ in
         };
       };
 
+      # DDC support
+      boot.kernelModules = lib.optional cfg.supportDDC "i2c_dev";
+      services.udev.extraRules = lib.optionalString cfg.supportDDC ''
+        KERNEL=="i2c-[0-9]*", TAG+="uaccess"
+      '';
+
       environment.systemPackages = with pkgs; with qt5; with libsForQt5; with plasma5; with kdeApplications;
         [
           frameworkintegration
@@ -302,7 +321,7 @@ in
 
       fonts.fonts = with pkgs; [ noto-fonts hack-font ];
       fonts.fontconfig.defaultFonts = {
-        monospace = [ "Hack" "Noto Mono" ];
+        monospace = [ "Hack" "Noto Sans Mono" ];
         sansSerif = [ "Noto Sans" ];
         serif = [ "Noto Serif" ];
       };
diff --git a/nixos/modules/services/x11/display-managers/default.nix b/nixos/modules/services/x11/display-managers/default.nix
index 2a7a19e7695..b8b36aa0532 100644
--- a/nixos/modules/services/x11/display-managers/default.nix
+++ b/nixos/modules/services/x11/display-managers/default.nix
@@ -39,7 +39,8 @@ let
 
       ${optionalString cfg.startDbusSession ''
         if test -z "$DBUS_SESSION_BUS_ADDRESS"; then
-          exec ${pkgs.dbus.dbus-launch} --exit-with-session "$0" "$@"
+          /run/current-system/systemd/bin/systemctl --user start dbus.socket
+          export `/run/current-system/systemd/bin/systemctl --user show-environment | grep '^DBUS_SESSION_BUS_ADDRESS'`
         fi
       ''}
 
@@ -59,7 +60,7 @@ let
       #
       # Also tell systemd about the dbus session bus address.
       # This is required by user units using the session bus.
-      ${config.systemd.package}/bin/systemctl --user import-environment DISPLAY XAUTHORITY DBUS_SESSION_BUS_ADDRESS
+      /run/current-system/systemd/bin/systemctl --user import-environment DISPLAY XAUTHORITY DBUS_SESSION_BUS_ADDRESS
 
       # Load X defaults. This should probably be safe on wayland too.
       ${xorg.xrdb}/bin/xrdb -merge ${xresourcesXft}
@@ -88,7 +89,7 @@ let
       fi
 
       # Start systemd user services for graphical sessions
-      ${config.systemd.package}/bin/systemctl --user start graphical-session.target
+      /run/current-system/systemd/bin/systemctl --user start graphical-session.target
 
       # Allow the user to setup a custom session type.
       if test -x ~/.xsession; then
@@ -331,12 +332,45 @@ in
 
       };
 
+      # Configuration for automatic login. Common for all DM.
+      autoLogin = mkOption {
+        type = types.submodule {
+          options = {
+            enable = mkOption {
+              type = types.bool;
+              default = cfg.displayManager.autoLogin.user != null;
+              description = ''
+                Automatically log in as <option>autoLogin.user</option>.
+              '';
+            };
+
+            user = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              description = ''
+                User to be used for the automatic login.
+              '';
+            };
+          };
+        };
+
+        default = {};
+        description = ''
+          Auto login configuration attrset.
+        '';
+      };
+
     };
 
   };
 
   config = {
     assertions = [
+      { assertion = cfg.displayManager.autoLogin.enable -> cfg.displayManager.autoLogin.user != null;
+        message = ''
+          services.xserver.displayManager.autoLogin.enable requires services.xserver.displayManager.autoLogin.user to be set
+        '';
+      }
       {
         assertion = cfg.desktopManager.default != null || cfg.windowManager.default != null -> cfg.displayManager.defaultSession == defaultSessionFromLegacyOptions;
         message = "You cannot use both services.xserver.displayManager.defaultSession option and legacy options (services.xserver.desktopManager.default and services.xserver.windowManager.default).";
@@ -393,7 +427,7 @@ in
 
           test -n "$waitPID" && wait "$waitPID"
 
-          ${config.systemd.package}/bin/systemctl --user stop graphical-session.target
+          /run/current-system/systemd/bin/systemctl --user stop graphical-session.target
 
           exit 0
         '';
diff --git a/nixos/modules/services/x11/display-managers/gdm.nix b/nixos/modules/services/x11/display-managers/gdm.nix
index 622ea62f3a9..23ab7f2ae43 100644
--- a/nixos/modules/services/x11/display-managers/gdm.nix
+++ b/nixos/modules/services/x11/display-managers/gdm.nix
@@ -37,6 +37,22 @@ let
 in
 
 {
+  imports = [
+    (mkRenamedOptionModule [ "services" "xserver" "displayManager" "gdm" "autoLogin" "enable" ] [
+      "services"
+      "xserver"
+      "displayManager"
+      "autoLogin"
+      "enable"
+    ])
+    (mkRenamedOptionModule [ "services" "xserver" "displayManager" "gdm" "autoLogin" "user" ] [
+      "services"
+      "xserver"
+      "displayManager"
+      "autoLogin"
+      "user"
+    ])
+  ];
 
   meta = {
     maintainers = teams.gnome.members;
@@ -56,40 +72,13 @@ in
         debugging messages in GDM
       '';
 
-      autoLogin = mkOption {
-        default = {};
+      # Auto login options specific to GDM
+      autoLogin.delay = mkOption {
+        type = types.int;
+        default = 0;
         description = ''
-          Auto login configuration attrset.
+          Seconds of inactivity after which the autologin will be performed.
         '';
-
-        type = types.submodule {
-          options = {
-            enable = mkOption {
-              type = types.bool;
-              default = false;
-              description = ''
-                Automatically log in as the sepecified <option>autoLogin.user</option>.
-              '';
-            };
-
-            user = mkOption {
-              type = types.nullOr types.str;
-              default = null;
-              description = ''
-                User to be used for the autologin.
-              '';
-            };
-
-            delay = mkOption {
-              type = types.int;
-              default = 0;
-              description = ''
-                Seconds of inactivity after which the autologin will be performed.
-              '';
-            };
-
-          };
-        };
       };
 
       wayland = mkOption {
@@ -128,12 +117,6 @@ in
 
   config = mkIf cfg.gdm.enable {
 
-    assertions = [
-      { assertion = cfg.gdm.autoLogin.enable -> cfg.gdm.autoLogin.user != null;
-        message = "GDM auto-login requires services.xserver.displayManager.gdm.autoLogin.user to be set";
-      }
-    ];
-
     services.xserver.displayManager.lightdm.enable = false;
 
     users.users.gdm =
@@ -217,7 +200,6 @@ in
       KillMode = "mixed";
       IgnoreSIGPIPE = "no";
       BusName = "org.gnome.DisplayManager";
-      StandardOutput = "syslog";
       StandardError = "inherit";
       ExecReload = "${pkgs.coreutils}/bin/kill -SIGHUP $MAINPID";
       KeyringMode = "shared";
@@ -287,14 +269,14 @@ in
     environment.etc."gdm/custom.conf".text = ''
       [daemon]
       WaylandEnable=${if cfg.gdm.wayland then "true" else "false"}
-      ${optionalString cfg.gdm.autoLogin.enable (
+      ${optionalString cfg.autoLogin.enable (
         if cfg.gdm.autoLogin.delay > 0 then ''
           TimedLoginEnable=true
-          TimedLogin=${cfg.gdm.autoLogin.user}
+          TimedLogin=${cfg.autoLogin.user}
           TimedLoginDelay=${toString cfg.gdm.autoLogin.delay}
         '' else ''
           AutomaticLoginEnable=true
-          AutomaticLogin=${cfg.gdm.autoLogin.user}
+          AutomaticLogin=${cfg.autoLogin.user}
         '')
       }
 
diff --git a/nixos/modules/services/x11/display-managers/lightdm-greeters/pantheon.nix b/nixos/modules/services/x11/display-managers/lightdm-greeters/pantheon.nix
index 087c6b9c38a..9bc9e2bf616 100644
--- a/nixos/modules/services/x11/display-managers/lightdm-greeters/pantheon.nix
+++ b/nixos/modules/services/x11/display-managers/lightdm-greeters/pantheon.nix
@@ -43,7 +43,7 @@ in
     services.xserver.displayManager.lightdm.extraSeatDefaults = "greeter-show-manual-login=true";
 
     environment.etc."lightdm/io.elementary.greeter.conf".source = "${pkgs.pantheon.elementary-greeter}/etc/lightdm/io.elementary.greeter.conf";
-    environment.etc."wingpanel.d/io.elementary.greeter.whitelist".source = "${pkgs.pantheon.elementary-default-settings}/etc/wingpanel.d/io.elementary.greeter.whitelist";
+    environment.etc."wingpanel.d/io.elementary.greeter.allowed".source = "${pkgs.pantheon.elementary-default-settings}/etc/wingpanel.d/io.elementary.greeter.allowed";
 
   };
 }
diff --git a/nixos/modules/services/x11/display-managers/lightdm.nix b/nixos/modules/services/x11/display-managers/lightdm.nix
index 479548863b4..143785db0b4 100644
--- a/nixos/modules/services/x11/display-managers/lightdm.nix
+++ b/nixos/modules/services/x11/display-managers/lightdm.nix
@@ -53,8 +53,8 @@ let
       ${optionalString cfg.greeter.enable ''
         greeter-session = ${cfg.greeter.name}
       ''}
-      ${optionalString cfg.autoLogin.enable ''
-        autologin-user = ${cfg.autoLogin.user}
+      ${optionalString dmcfg.autoLogin.enable ''
+        autologin-user = ${dmcfg.autoLogin.user}
         autologin-user-timeout = ${toString cfg.autoLogin.timeout}
         autologin-session = ${sessionData.autologinSession}
       ''}
@@ -82,6 +82,20 @@ in
     ./lightdm-greeters/enso-os.nix
     ./lightdm-greeters/pantheon.nix
     ./lightdm-greeters/tiny.nix
+    (mkRenamedOptionModule [ "services" "xserver" "displayManager" "lightdm" "autoLogin" "enable" ] [
+      "services"
+      "xserver"
+      "displayManager"
+      "autoLogin"
+      "enable"
+    ])
+    (mkRenamedOptionModule [ "services" "xserver" "displayManager" "lightdm" "autoLogin" "user" ] [
+     "services"
+     "xserver"
+     "displayManager"
+     "autoLogin"
+     "user"
+    ])
   ];
 
   options = {
@@ -132,8 +146,9 @@ in
       };
 
       background = mkOption {
-        type = types.str;
-        default = "${pkgs.nixos-artwork.wallpapers.simple-dark-gray-bottom}/share/artwork/gnome/nix-wallpaper-simple-dark-gray_bottom.png";
+        type = types.path;
+        # Manual cannot depend on packages, we are actually setting the default in config below.
+        defaultText = "pkgs.nixos-artwork.wallpapers.simple-dark-gray-bottom.gnomeFilePath";
         description = ''
           The background image or color to use.
         '';
@@ -148,39 +163,13 @@ in
         description = "Extra lines to append to SeatDefaults section.";
       };
 
-      autoLogin = mkOption {
-        default = {};
+      # Configuration for automatic login specific to LightDM
+      autoLogin.timeout = mkOption {
+        type = types.int;
+        default = 0;
         description = ''
-          Configuration for automatic login.
+          Show the greeter for this many seconds before automatic login occurs.
         '';
-
-        type = types.submodule {
-          options = {
-            enable = mkOption {
-              type = types.bool;
-              default = false;
-              description = ''
-                Automatically log in as the specified <option>autoLogin.user</option>.
-              '';
-            };
-
-            user = mkOption {
-              type = types.nullOr types.str;
-              default = null;
-              description = ''
-                User to be used for the automatic login.
-              '';
-            };
-
-            timeout = mkOption {
-              type = types.int;
-              default = 0;
-              description = ''
-                Show the greeter for this many seconds before automatic login occurs.
-              '';
-            };
-          };
-        };
       };
 
     };
@@ -194,17 +183,12 @@ in
           LightDM requires services.xserver.enable to be true
         '';
       }
-      { assertion = cfg.autoLogin.enable -> cfg.autoLogin.user != null;
-        message = ''
-          LightDM auto-login requires services.xserver.displayManager.lightdm.autoLogin.user to be set
-        '';
-      }
-      { assertion = cfg.autoLogin.enable -> sessionData.autologinSession != null;
+      { assertion = dmcfg.autoLogin.enable -> sessionData.autologinSession != null;
         message = ''
           LightDM auto-login requires that services.xserver.displayManager.defaultSession is set.
         '';
       }
-      { assertion = !cfg.greeter.enable -> (cfg.autoLogin.enable && cfg.autoLogin.timeout == 0);
+      { assertion = !cfg.greeter.enable -> (dmcfg.autoLogin.enable && cfg.autoLogin.timeout == 0);
         message = ''
           LightDM can only run without greeter if automatic login is enabled and the timeout for it
           is set to zero.
@@ -212,9 +196,12 @@ in
       }
     ];
 
+    # Keep in sync with the defaultText value from the option definition.
+    services.xserver.displayManager.lightdm.background = mkDefault pkgs.nixos-artwork.wallpapers.simple-dark-gray-bottom.gnomeFilePath;
+
     # Set default session in session chooser to a specified values – basically ignore session history.
     # Auto-login is already covered by a config value.
-    services.xserver.displayManager.job.preStart = optionalString (!cfg.autoLogin.enable && dmcfg.defaultSession != null) ''
+    services.xserver.displayManager.job.preStart = optionalString (!dmcfg.autoLogin.enable && dmcfg.defaultSession != null) ''
       ${setSessionScript}/bin/set-session ${dmcfg.defaultSession}
     '';
 
@@ -266,7 +253,6 @@ in
       KeyringMode = "shared";
       KillMode = "mixed";
       StandardError = "inherit";
-      StandardOutput = "syslog";
     };
 
     environment.etc."lightdm/lightdm.conf".source = lightdmConf;
diff --git a/nixos/modules/services/x11/display-managers/sddm.nix b/nixos/modules/services/x11/display-managers/sddm.nix
index 4224c557ed6..e63bb2e4453 100644
--- a/nixos/modules/services/x11/display-managers/sddm.nix
+++ b/nixos/modules/services/x11/display-managers/sddm.nix
@@ -30,8 +30,8 @@ let
 
   cfgFile = pkgs.writeText "sddm.conf" ''
     [General]
-    HaltCommand=${pkgs.systemd}/bin/systemctl poweroff
-    RebootCommand=${pkgs.systemd}/bin/systemctl reboot
+    HaltCommand=/run/current-system/systemd/bin/systemctl poweroff
+    RebootCommand=/run/current-system/systemd/bin/systemctl reboot
     ${optionalString cfg.autoNumlock ''
     Numlock=on
     ''}
@@ -61,9 +61,9 @@ let
     EnableHidpi=${if cfg.enableHidpi then "true" else "false"}
     SessionDir=${dmcfg.sessionData.desktops}/share/wayland-sessions
 
-    ${optionalString cfg.autoLogin.enable ''
+    ${optionalString dmcfg.autoLogin.enable ''
     [Autologin]
-    User=${cfg.autoLogin.user}
+    User=${dmcfg.autoLogin.user}
     Session=${autoLoginSessionName}.desktop
     Relogin=${boolToString cfg.autoLogin.relogin}
     ''}
@@ -78,6 +78,20 @@ in
   imports = [
     (mkRemovedOptionModule [ "services" "xserver" "displayManager" "sddm" "themes" ]
       "Set the option `services.xserver.displayManager.sddm.package' instead.")
+    (mkRenamedOptionModule [ "services" "xserver" "displayManager" "sddm" "autoLogin" "enable" ] [
+      "services"
+      "xserver"
+      "displayManager"
+      "autoLogin"
+      "enable"
+    ])
+    (mkRenamedOptionModule [ "services" "xserver" "displayManager" "sddm" "autoLogin" "user" ] [
+      "services"
+      "xserver"
+      "displayManager"
+      "autoLogin"
+      "user"
+    ])
   ];
 
   options = {
@@ -153,40 +167,14 @@ in
         '';
       };
 
-      autoLogin = mkOption {
-        default = {};
+      # Configuration for automatic login specific to SDDM
+      autoLogin.relogin = mkOption {
+        type = types.bool;
+        default = false;
         description = ''
-          Configuration for automatic login.
+          If true automatic login will kick in again on session exit (logout), otherwise it
+          will only log in automatically when the display-manager is started.
         '';
-
-        type = types.submodule {
-          options = {
-            enable = mkOption {
-              type = types.bool;
-              default = false;
-              description = ''
-                Automatically log in as <option>autoLogin.user</option>.
-              '';
-            };
-
-            user = mkOption {
-              type = types.nullOr types.str;
-              default = null;
-              description = ''
-                User to be used for the automatic login.
-              '';
-            };
-
-            relogin = mkOption {
-              type = types.bool;
-              default = false;
-              description = ''
-                If true automatic login will kick in again on session exit (logout), otherwise it
-                will only log in automatically when the display-manager is started.
-              '';
-            };
-          };
-        };
       };
 
     };
@@ -201,12 +189,7 @@ in
           SDDM requires services.xserver.enable to be true
         '';
       }
-      { assertion = cfg.autoLogin.enable -> cfg.autoLogin.user != null;
-        message = ''
-          SDDM auto-login requires services.xserver.displayManager.sddm.autoLogin.user to be set
-        '';
-      }
-      { assertion = cfg.autoLogin.enable -> autoLoginSessionName != null;
+      { assertion = dmcfg.autoLogin.enable -> autoLoginSessionName != null;
         message = ''
           SDDM auto-login requires that services.xserver.displayManager.defaultSession is set.
         '';
diff --git a/nixos/modules/services/x11/imwheel.nix b/nixos/modules/services/x11/imwheel.nix
index 3923df498e7..51f72dadbd4 100644
--- a/nixos/modules/services/x11/imwheel.nix
+++ b/nixos/modules/services/x11/imwheel.nix
@@ -61,7 +61,8 @@ in
             "--kill"
           ] ++ cfg.extraOptions);
           ExecStop = "${pkgs.procps}/bin/pkill imwheel";
-          Restart = "on-failure";
+          RestartSec = 3;
+          Restart = "always";
         };
       };
     };
diff --git a/nixos/modules/services/x11/urserver.nix b/nixos/modules/services/x11/urserver.nix
new file mode 100644
index 00000000000..0beb62eb766
--- /dev/null
+++ b/nixos/modules/services/x11/urserver.nix
@@ -0,0 +1,38 @@
+# urserver service
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.urserver;
+in {
+
+  options.services.urserver.enable = lib.mkEnableOption "urserver";
+
+  config = lib.mkIf cfg.enable {
+
+    networking.firewall = {
+      allowedTCPPorts = [ 9510 9512 ];
+      allowedUDPPorts = [ 9511 9512 ];
+    };
+
+    systemd.user.services.urserver =  {
+      description = ''
+        Server for Unified Remote: The one-and-only remote for your computer.
+      '';
+      wantedBy = [ "graphical-session.target" ];
+      partOf = [ "graphical-session.target" ];
+      after = [ "network.target" ];
+      serviceConfig = {
+        Type = "forking";
+        ExecStart = ''
+          ${pkgs.urserver}/bin/urserver --daemon
+        '';
+        ExecStop = ''
+          ${pkgs.procps}/bin/pkill urserver
+        '';
+        RestartSec = 3;
+        Restart = "on-failure";
+      };
+    };
+  };
+
+}
diff --git a/nixos/modules/services/x11/window-managers/berry.nix b/nixos/modules/services/x11/window-managers/berry.nix
new file mode 100644
index 00000000000..0d2285e7a60
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/berry.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.berry;
+in
+{
+  ###### interface
+  options = {
+    services.xserver.windowManager.berry.enable = mkEnableOption "berry";
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton {
+      name = "berry";
+      start = ''
+        ${pkgs.berry}/bin/berry &
+        waitPID=$!
+      '';
+    };
+    environment.systemPackages = [ pkgs.berry ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/default.nix b/nixos/modules/services/x11/window-managers/default.nix
index b815c5f16a1..87702c58727 100644
--- a/nixos/modules/services/x11/window-managers/default.nix
+++ b/nixos/modules/services/x11/window-managers/default.nix
@@ -10,6 +10,7 @@ in
   imports = [
     ./2bwm.nix
     ./afterstep.nix
+    ./berry.nix
     ./bspwm.nix
     ./cwm.nix
     ./dwm.nix
@@ -21,6 +22,7 @@ in
     ./i3.nix
     ./jwm.nix
     ./leftwm.nix
+    ./lwm.nix
     ./metacity.nix
     ./mwm.nix
     ./openbox.nix
@@ -28,6 +30,7 @@ in
     ./notion.nix
     ./ratpoison.nix
     ./sawfish.nix
+    ./smallwm.nix
     ./stumpwm.nix
     ./spectrwm.nix
     ./tinywm.nix
@@ -35,6 +38,7 @@ in
     ./windowmaker.nix
     ./wmii.nix
     ./xmonad.nix
+    ./yeahwm.nix
     ./qtile.nix
     ./none.nix ];
 
diff --git a/nixos/modules/services/x11/window-managers/lwm.nix b/nixos/modules/services/x11/window-managers/lwm.nix
new file mode 100644
index 00000000000..e2aa062fd13
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/lwm.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.lwm;
+in
+{
+  ###### interface
+  options = {
+    services.xserver.windowManager.lwm.enable = mkEnableOption "lwm";
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton {
+      name = "lwm";
+      start = ''
+        ${pkgs.lwm}/bin/lwm &
+        waitPID=$!
+      '';
+    };
+    environment.systemPackages = [ pkgs.lwm ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/qtile.nix b/nixos/modules/services/x11/window-managers/qtile.nix
index ad3b65150b0..cadc316bbc4 100644
--- a/nixos/modules/services/x11/window-managers/qtile.nix
+++ b/nixos/modules/services/x11/window-managers/qtile.nix
@@ -19,7 +19,7 @@ in
         waitPID=$!
       '';
     }];
-    
+
     environment.systemPackages = [ pkgs.qtile ];
   };
 }
diff --git a/nixos/modules/services/x11/window-managers/smallwm.nix b/nixos/modules/services/x11/window-managers/smallwm.nix
new file mode 100644
index 00000000000..091ba4f92b9
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/smallwm.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.smallwm;
+in
+{
+  ###### interface
+  options = {
+    services.xserver.windowManager.smallwm.enable = mkEnableOption "smallwm";
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton {
+      name = "smallwm";
+      start = ''
+        ${pkgs.smallwm}/bin/smallwm &
+        waitPID=$!
+      '';
+    };
+    environment.systemPackages = [ pkgs.smallwm ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/xmonad.nix b/nixos/modules/services/x11/window-managers/xmonad.nix
index 30c59b88f82..070758720fe 100644
--- a/nixos/modules/services/x11/window-managers/xmonad.nix
+++ b/nixos/modules/services/x11/window-managers/xmonad.nix
@@ -82,12 +82,11 @@ in
     services.xserver.windowManager = {
       session = [{
         name = "xmonad";
-        start = if (cfg.config != null) then ''
-          ${xmonadBin}
-          waitPID=$!
-        '' else ''
-          systemd-cat -t xmonad ${xmonad}/bin/xmonad &
-          waitPID=$!
+        start = let
+          xmonadCommand = if (cfg.config != null) then xmonadBin else "${xmonad}/bin/xmonad";
+        in ''
+           systemd-cat -t xmonad ${xmonadCommand} &
+           waitPID=$!
         '';
       }];
     };
diff --git a/nixos/modules/services/x11/window-managers/yeahwm.nix b/nixos/modules/services/x11/window-managers/yeahwm.nix
new file mode 100644
index 00000000000..351bd7dfe48
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/yeahwm.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.yeahwm;
+in
+{
+  ###### interface
+  options = {
+    services.xserver.windowManager.yeahwm.enable = mkEnableOption "yeahwm";
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton {
+      name = "yeahwm";
+      start = ''
+        ${pkgs.yeahwm}/bin/yeahwm &
+        waitPID=$!
+      '';
+    };
+    environment.systemPackages = [ pkgs.yeahwm ];
+  };
+}
diff --git a/nixos/modules/services/x11/xautolock.nix b/nixos/modules/services/x11/xautolock.nix
index 3e03131ca11..5ce08fce7c4 100644
--- a/nixos/modules/services/x11/xautolock.nix
+++ b/nixos/modules/services/x11/xautolock.nix
@@ -66,7 +66,7 @@ in
 
         killer = mkOption {
           default = null; # default according to `man xautolock` is none
-          example = "${pkgs.systemd}/bin/systemctl suspend";
+          example = "/run/current-system/systemd/bin/systemctl suspend";
           type = types.nullOr types.str;
 
           description = ''
diff --git a/nixos/modules/services/x11/xserver.nix b/nixos/modules/services/x11/xserver.nix
index 6aec1c0753a..400173745d3 100644
--- a/nixos/modules/services/x11/xserver.nix
+++ b/nixos/modules/services/x11/xserver.nix
@@ -246,7 +246,7 @@ in
       videoDrivers = mkOption {
         type = types.listOf types.str;
         # !!! We'd like "nv" here, but it segfaults the X server.
-        default = [ "radeon" "cirrus" "vesa" "vmware" "modesetting" ];
+        default = [ "radeon" "cirrus" "vesa" "modesetting" ];
         example = [
           "ati_unfree" "amdgpu" "amdgpu-pro"
           "nv" "nvidia" "nvidiaLegacy390" "nvidiaLegacy340" "nvidiaLegacy304"
diff --git a/nixos/modules/system/activation/top-level.nix b/nixos/modules/system/activation/top-level.nix
index f6739977fa4..fb8644dd13a 100644
--- a/nixos/modules/system/activation/top-level.nix
+++ b/nixos/modules/system/activation/top-level.nix
@@ -92,9 +92,7 @@ let
   # `switch-to-configuration' that activates the configuration and
   # makes it bootable.
   baseSystem = pkgs.stdenvNoCC.mkDerivation {
-    name = let hn = config.networking.hostName;
-               nn = if (hn != "") then hn else "unnamed";
-        in "nixos-system-${nn}-${config.system.nixos.label}";
+    name = "nixos-system-${config.system.name}-${config.system.nixos.label}";
     preferLocalBuild = true;
     allowSubstitutes = false;
     buildCommand = systemBuilder;
@@ -265,6 +263,21 @@ in
       '';
     };
 
+    system.name = mkOption {
+      type = types.str;
+      default =
+        if config.networking.hostName == ""
+        then "unnamed"
+        else config.networking.hostName;
+      defaultText = '''networking.hostName' if non empty else "unnamed"'';
+      description = ''
+        The name of the system used in the <option>system.build.toplevel</option> derivation.
+        </para><para>
+        That derivation has the following name:
+        <literal>"nixos-system-''${config.system.name}-''${config.system.nixos.label}"</literal>
+      '';
+    };
+
   };
 
 
diff --git a/nixos/modules/system/boot/binfmt.nix b/nixos/modules/system/boot/binfmt.nix
index a677ab4cb71..9eeae0c3ef4 100644
--- a/nixos/modules/system/boot/binfmt.nix
+++ b/nixos/modules/system/boot/binfmt.nix
@@ -268,9 +268,10 @@ in {
       mkdir -p -m 0755 /run/binfmt
       ${lib.concatStringsSep "\n" (lib.mapAttrsToList activationSnippet config.boot.binfmt.registrations)}
     '';
-    systemd.additionalUpstreamSystemUnits = lib.mkIf (config.boot.binfmt.registrations != {})
-      [ "proc-sys-fs-binfmt_misc.automount"
-        "proc-sys-fs-binfmt_misc.mount"
-      ];
+    systemd.additionalUpstreamSystemUnits = lib.mkIf (config.boot.binfmt.registrations != {}) [
+      "proc-sys-fs-binfmt_misc.automount"
+      "proc-sys-fs-binfmt_misc.mount"
+      "systemd-binfmt.service"
+    ];
   };
 }
diff --git a/nixos/modules/system/boot/emergency-mode.nix b/nixos/modules/system/boot/emergency-mode.nix
index 9cdab841619..ec697bcee26 100644
--- a/nixos/modules/system/boot/emergency-mode.nix
+++ b/nixos/modules/system/boot/emergency-mode.nix
@@ -34,4 +34,4 @@ with lib;
 
   };
 
-}
\ No newline at end of file
+}
diff --git a/nixos/modules/system/boot/initrd-network.nix b/nixos/modules/system/boot/initrd-network.nix
index 0ab6e626b34..ec794d6eb01 100644
--- a/nixos/modules/system/boot/initrd-network.nix
+++ b/nixos/modules/system/boot/initrd-network.nix
@@ -139,7 +139,7 @@ in
     boot.initrd.postMountCommands = mkIf cfg.flushBeforeStage2 ''
       for iface in $ifaces; do
         ip address flush "$iface"
-        ip link down "$iface"
+        ip link set "$iface" down
       done
     '';
 
diff --git a/nixos/modules/system/boot/initrd-openvpn.nix b/nixos/modules/system/boot/initrd-openvpn.nix
new file mode 100644
index 00000000000..e59bc7b6678
--- /dev/null
+++ b/nixos/modules/system/boot/initrd-openvpn.nix
@@ -0,0 +1,81 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.boot.initrd.network.openvpn;
+
+in
+
+{
+
+  options = {
+
+    boot.initrd.network.openvpn.enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Starts an OpenVPN client during initrd boot. It can be used to e.g.
+        remotely accessing the SSH service controlled by
+        <option>boot.initrd.network.ssh</option> or other network services
+        included. Service is killed when stage-1 boot is finished.
+      '';
+    };
+
+    boot.initrd.network.openvpn.configuration = mkOption {
+      type = types.path; # Same type as boot.initrd.secrets
+      description = ''
+        The configuration file for OpenVPN.
+
+        <warning>
+          <para>
+            Unless your bootloader supports initrd secrets, this configuration
+            is stored insecurely in the global Nix store.
+          </para>
+        </warning>
+      '';
+      example = "./configuration.ovpn";
+    };
+
+  };
+
+  config = mkIf (config.boot.initrd.network.enable && cfg.enable) {
+    assertions = [
+      {
+        assertion = cfg.configuration != null;
+        message = "You should specify a configuration for initrd OpenVPN";
+      }
+    ];
+
+    # Add kernel modules needed for OpenVPN
+    boot.initrd.kernelModules = [ "tun" "tap" ];
+
+    # Add openvpn and ip binaries to the initrd
+    # The shared libraries are required for DNS resolution
+    boot.initrd.extraUtilsCommands = ''
+      copy_bin_and_libs ${pkgs.openvpn}/bin/openvpn
+      copy_bin_and_libs ${pkgs.iproute}/bin/ip
+
+      cp -pv ${pkgs.glibc}/lib/libresolv.so.2 $out/lib
+      cp -pv ${pkgs.glibc}/lib/libnss_dns.so.2 $out/lib
+    '';
+
+    boot.initrd.secrets = {
+      "/etc/initrd.ovpn" = cfg.configuration;
+    };
+
+    # openvpn --version would exit with 1 instead of 0
+    boot.initrd.extraUtilsCommandsTest = ''
+      $out/bin/openvpn --show-gateway
+    '';
+
+    # Add `iproute /bin/ip` to the config, to ensure that openvpn
+    # is able to set the routes
+    boot.initrd.network.postCommands = ''
+      (cat /etc/initrd.ovpn; echo -e '\niproute /bin/ip') | \
+        openvpn /dev/stdin &
+    '';
+  };
+
+}
diff --git a/nixos/modules/system/boot/kernel_config.nix b/nixos/modules/system/boot/kernel_config.nix
index a316782dfc5..783685c9dfe 100644
--- a/nixos/modules/system/boot/kernel_config.nix
+++ b/nixos/modules/system/boot/kernel_config.nix
@@ -22,7 +22,7 @@ let
 
   mergeFalseByDefault = locs: defs:
     if defs == [] then abort "This case should never happen."
-    else if any (x: x == false) defs then false
+    else if any (x: x == false) (getValues defs) then false
     else true;
 
   kernelItem = types.submodule {
@@ -54,7 +54,8 @@ let
         type = types.bool // { merge = mergeFalseByDefault; };
         default = false;
         description = ''
-          Wether option should generate a failure when unused.
+          Whether option should generate a failure when unused.
+          Upon merging values, mandatory wins over optional.
         '';
       };
     };
@@ -121,7 +122,7 @@ in
       type = types.attrsOf kernelItem;
       example = literalExample '' with lib.kernel; {
         "9P_NET" = yes;
-        USB = optional yes;
+        USB = option yes;
         MMC_BLOCK_MINORS = freeform "32";
       }'';
       description = ''
diff --git a/nixos/modules/system/boot/loader/generations-dir/generations-dir-builder.sh b/nixos/modules/system/boot/loader/generations-dir/generations-dir-builder.sh
index e723b9eb7cb..8ae23dc988c 100644
--- a/nixos/modules/system/boot/loader/generations-dir/generations-dir-builder.sh
+++ b/nixos/modules/system/boot/loader/generations-dir/generations-dir-builder.sh
@@ -63,7 +63,7 @@ addEntry() {
         copyToKernelsDir $kernel; kernel=$result
         copyToKernelsDir $initrd; initrd=$result
     fi
-    
+
     mkdir -p $outdir
     ln -sf $(readlink -f $path) $outdir/system
     ln -sf $(readlink -f $path/init) $outdir/init
diff --git a/nixos/modules/system/boot/loader/generic-extlinux-compatible/default.nix b/nixos/modules/system/boot/loader/generic-extlinux-compatible/default.nix
index af39c7bb684..bd508bbe8ea 100644
--- a/nixos/modules/system/boot/loader/generic-extlinux-compatible/default.nix
+++ b/nixos/modules/system/boot/loader/generic-extlinux-compatible/default.nix
@@ -4,11 +4,15 @@ with lib;
 
 let
   blCfg = config.boot.loader;
+  dtCfg = config.hardware.deviceTree;
   cfg = blCfg.generic-extlinux-compatible;
 
   timeoutStr = if blCfg.timeout == null then "-1" else toString blCfg.timeout;
 
+  # The builder used to write during system activation
   builder = import ./extlinux-conf-builder.nix { inherit pkgs; };
+  # The builder exposed in populateCmd, which runs on the build architecture
+  populateBuilder = import ./extlinux-conf-builder.nix { pkgs = pkgs.buildPackages; };
 in
 {
   options = {
@@ -34,11 +38,28 @@ in
           Maximum number of configurations in the boot menu.
         '';
       };
+
+      populateCmd = mkOption {
+        type = types.str;
+        readOnly = true;
+        description = ''
+          Contains the builder command used to populate an image,
+          honoring all options except the <literal>-c &lt;path-to-default-configuration&gt;</literal>
+          argument.
+          Useful to have for sdImage.populateRootCommands
+        '';
+      };
+
     };
   };
 
-  config = mkIf cfg.enable {
-    system.build.installBootLoader = "${builder} -g ${toString cfg.configurationLimit} -t ${timeoutStr} -c";
-    system.boot.loader.id = "generic-extlinux-compatible";
-  };
+  config = let
+    builderArgs = "-g ${toString cfg.configurationLimit} -t ${timeoutStr}" + lib.optionalString (dtCfg.name != null) " -n ${dtCfg.name}";
+  in
+    mkIf cfg.enable {
+      system.build.installBootLoader = "${builder} ${builderArgs} -c";
+      system.boot.loader.id = "generic-extlinux-compatible";
+
+      boot.loader.generic-extlinux-compatible.populateCmd = "${populateBuilder} ${builderArgs}";
+    };
 }
diff --git a/nixos/modules/system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.sh b/nixos/modules/system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.sh
index 0092ee92b62..854684b87fa 100644
--- a/nixos/modules/system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.sh
+++ b/nixos/modules/system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.sh
@@ -6,7 +6,7 @@ export PATH=/empty
 for i in @path@; do PATH=$PATH:$i/bin; done
 
 usage() {
-    echo "usage: $0 -t <timeout> -c <path-to-default-configuration> [-d <boot-dir>] [-g <num-generations>]" >&2
+    echo "usage: $0 -t <timeout> -c <path-to-default-configuration> [-d <boot-dir>] [-g <num-generations>] [-n <dtbName>]" >&2
     exit 1
 }
 
@@ -15,7 +15,7 @@ default=                # Default configuration
 target=/boot            # Target directory
 numGenerations=0        # Number of other generations to include in the menu
 
-while getopts "t:c:d:g:" opt; do
+while getopts "t:c:d:g:n:" opt; do
     case "$opt" in
         t) # U-Boot interprets '0' as infinite and negative as instant boot
             if [ "$OPTARG" -lt 0 ]; then
@@ -29,6 +29,7 @@ while getopts "t:c:d:g:" opt; do
         c) default="$OPTARG" ;;
         d) target="$OPTARG" ;;
         g) numGenerations="$OPTARG" ;;
+        n) dtbName="$OPTARG" ;;
         \?) usage ;;
     esac
 done
@@ -96,7 +97,17 @@ addEntry() {
     echo "  LINUX ../nixos/$(basename $kernel)"
     echo "  INITRD ../nixos/$(basename $initrd)"
     if [ -d "$dtbDir" ]; then
-        echo "  FDTDIR ../nixos/$(basename $dtbs)"
+        # if a dtbName was specified explicitly, use that, else use FDTDIR
+        if [ -n "$dtbName" ]; then
+            echo "  FDT ../nixos/$(basename $dtbs)/${dtbName}"
+        else
+            echo "  FDTDIR ../nixos/$(basename $dtbs)"
+        fi
+    else
+        if [ -n "$dtbName" ]; then
+            echo "Explicitly requested dtbName $dtbName, but there's no FDTDIR - bailing out." >&2
+            exit 1
+        fi
     fi
     echo "  APPEND systemConfig=$path init=$path/init $extraParams"
 }
diff --git a/nixos/modules/system/boot/loader/grub/grub.nix b/nixos/modules/system/boot/loader/grub/grub.nix
index c775632a4aa..20e39628eab 100644
--- a/nixos/modules/system/boot/loader/grub/grub.nix
+++ b/nixos/modules/system/boot/loader/grub/grub.nix
@@ -55,12 +55,15 @@ let
       storePath = config.boot.loader.grub.storePath;
       bootloaderId = if args.efiBootloaderId == null then "NixOS${efiSysMountPoint'}" else args.efiBootloaderId;
       timeout = if config.boot.loader.timeout == null then -1 else config.boot.loader.timeout;
+      users = if cfg.users == {} || cfg.version != 1 then cfg.users else throw "GRUB version 1 does not support user accounts.";
+      theme = f cfg.theme;
       inherit efiSysMountPoint;
       inherit (args) devices;
       inherit (efi) canTouchEfiVariables;
       inherit (cfg)
         version extraConfig extraPerEntryConfig extraEntries forceInstall useOSProber
-        extraEntriesBeforeNixOS extraPrepareConfig extraInitrd configurationLimit copyKernels
+        extraGrubInstallArgs
+        extraEntriesBeforeNixOS extraPrepareConfig configurationLimit copyKernels
         default fsIdentifier efiSupport efiInstallAsRemovable gfxmodeEfi gfxmodeBios gfxpayloadEfi gfxpayloadBios;
       path = with pkgs; makeBinPath (
         [ coreutils gnused gnugrep findutils diffutils btrfs-progs utillinux mdadm ]
@@ -83,7 +86,7 @@ let
              ] ++ (optional (cfg.fontSize!=null) "--size ${toString cfg.fontSize}")))
          );
 
-  defaultSplash = "${pkgs.nixos-artwork.wallpapers.simple-dark-gray-bootloader}/share/artwork/gnome/nix-wallpaper-simple-dark-gray_bootloader.png";
+  defaultSplash = pkgs.nixos-artwork.wallpapers.simple-dark-gray-bootloader.gnomeFilePath;
 in
 
 {
@@ -137,6 +140,67 @@ in
         '';
       };
 
+      users = mkOption {
+        default = {};
+        example = {
+          root = { hashedPasswordFile = "/path/to/file"; };
+        };
+        description = ''
+          User accounts for GRUB. When specified, the GRUB command line and
+          all boot options except the default are password-protected.
+          All passwords and hashes provided will be stored in /boot/grub/grub.cfg,
+          and will be visible to any local user who can read this file. Additionally,
+          any passwords and hashes provided directly in a Nix configuration
+          (as opposed to external files) will be copied into the Nix store, and
+          will be visible to all local users.
+        '';
+        type = with types; attrsOf (submodule {
+          options = {
+            hashedPasswordFile = mkOption {
+              example = "/path/to/file";
+              default = null;
+              type = with types; uniq (nullOr str);
+              description = ''
+                Specifies the path to a file containing the password hash
+                for the account, generated with grub-mkpasswd-pbkdf2.
+                This hash will be stored in /boot/grub/grub.cfg, and will
+                be visible to any local user who can read this file.
+              '';
+            };
+            hashedPassword = mkOption {
+              example = "grub.pbkdf2.sha512.10000.674DFFDEF76E13EA...2CC972B102CF4355";
+              default = null;
+              type = with types; uniq (nullOr str);
+              description = ''
+                Specifies the password hash for the account,
+                generated with grub-mkpasswd-pbkdf2.
+                This hash will be copied to the Nix store, and will be visible to all local users.
+              '';
+            };
+            passwordFile = mkOption {
+              example = "/path/to/file";
+              default = null;
+              type = with types; uniq (nullOr str);
+              description = ''
+                Specifies the path to a file containing the
+                clear text password for the account.
+                This password will be stored in /boot/grub/grub.cfg, and will
+                be visible to any local user who can read this file.
+              '';
+            };
+            password = mkOption {
+              example = "Pa$$w0rd!";
+              default = null;
+              type = with types; uniq (nullOr str);
+              description = ''
+                Specifies the clear text password for the account.
+                This password will be copied to the Nix store, and will be visible to all local users.
+              '';
+            };
+          };
+        });
+      };
+
       mirroredBoots = mkOption {
         default = [ ];
         example = [
@@ -236,6 +300,33 @@ in
         '';
       };
 
+      extraGrubInstallArgs = mkOption {
+        default = [ ];
+        example = [ "--modules=nativedisk ahci pata part_gpt part_msdos diskfilter mdraid1x lvm ext2" ];
+        type = types.listOf types.str;
+        description = ''
+          Additional arguments passed to <literal>grub-install</literal>.
+
+          A use case for this is to build specific GRUB2 modules
+          directly into the GRUB2 kernel image, so that they are available
+          and activated even in the <literal>grub rescue</literal> shell.
+
+          They are also necessary when the BIOS/UEFI is bugged and cannot
+          correctly read large disks (e.g. above 2 TB), so GRUB2's own
+          <literal>nativedisk</literal> and related modules can be used
+          to use its own disk drivers. The example shows one such case.
+          This is also useful for booting from USB.
+          See the
+          <link xlink:href="http://git.savannah.gnu.org/cgit/grub.git/tree/grub-core/commands/nativedisk.c?h=grub-2.04#n326">
+          GRUB source code
+          </link>
+          for which disk modules are available.
+
+          The list elements are passed directly as <literal>argv</literal>
+          arguments to the <literal>grub-install</literal> program, in order.
+        '';
+      };
+
       extraPerEntryConfig = mkOption {
         default = "";
         example = "root (hd0)";
@@ -292,19 +383,6 @@ in
         '';
       };
 
-      extraInitrd = mkOption {
-        type = types.nullOr types.path;
-        default = null;
-        example = "/boot/extra_initramfs.gz";
-        description = ''
-          The path to a second initramfs to be supplied to the kernel.
-          This ramfs will not be copied to the store, so that it can
-          contain secrets such as LUKS keyfiles or ssh keys.
-          This implies that rolling back to a previous configuration
-          won't rollback the state of this file.
-        '';
-      };
-
       useOSProber = mkOption {
         default = false;
         type = types.bool;
@@ -349,6 +427,19 @@ in
         '';
       };
 
+      theme = mkOption {
+        type = types.nullOr types.path;
+        example = literalExample "pkgs.nixos-grub2-theme";
+        default = null;
+        description = ''
+          Grub theme to be used.
+
+          <note><para>
+          This options has no effect for GRUB 1.
+          </para></note>
+        '';
+      };
+
       splashMode = mkOption {
         type = types.enum [ "normal" "stretch" ];
         default = "stretch";
@@ -608,6 +699,8 @@ in
         { path = "/boot"; inherit (cfg) devices; inherit (efi) efiSysMountPoint; }
       ];
 
+      boot.loader.supportsInitrdSecrets = true;
+
       system.build.installBootLoader =
         let
           install-grub-pl = pkgs.substituteAll {
@@ -618,7 +711,7 @@ in
         in pkgs.writeScript "install-grub.sh" (''
         #!${pkgs.runtimeShell}
         set -e
-        export PERL5LIB=${with pkgs.perlPackages; makePerlPath [ FileSlurp XMLLibXML XMLSAX XMLSAXBase ListCompare ]}
+        export PERL5LIB=${with pkgs.perlPackages; makePerlPath [ FileSlurp FileCopyRecursive XMLLibXML XMLSAX XMLSAXBase ListCompare JSON ]}
         ${optionalString cfg.enableCryptodisk "export GRUB_ENABLE_CRYPTODISK=y"}
       '' + flip concatMapStrings cfg.mirroredBoots (args: ''
         ${pkgs.perl}/bin/perl ${install-grub-pl} ${grubConfig args} $@
@@ -705,6 +798,24 @@ in
       (mkRenamedOptionModule [ "boot" "grubDevice" ] [ "boot" "loader" "grub" "device" ])
       (mkRenamedOptionModule [ "boot" "bootMount" ] [ "boot" "loader" "grub" "bootDevice" ])
       (mkRenamedOptionModule [ "boot" "grubSplashImage" ] [ "boot" "loader" "grub" "splashImage" ])
+      (mkRemovedOptionModule [ "boot" "loader" "grub" "extraInitrd" ] ''
+        This option has been replaced with the bootloader agnostic
+        boot.initrd.secrets option. To migrate to the initrd secrets system,
+        extract the extraInitrd archive into your main filesystem:
+
+          # zcat /boot/extra_initramfs.gz | cpio -idvmD /etc/secrets/initrd
+          /path/to/secret1
+          /path/to/secret2
+
+        then replace boot.loader.grub.extraInitrd with boot.initrd.secrets:
+
+          boot.initrd.secrets = {
+            "/path/to/secret1" = "/etc/secrets/initrd/path/to/secret1";
+            "/path/to/secret2" = "/etc/secrets/initrd/path/to/secret2";
+          };
+
+        See the boot.initrd.secrets option documentation for more information.
+      '')
     ];
 
 }
diff --git a/nixos/modules/system/boot/loader/grub/install-grub.pl b/nixos/modules/system/boot/loader/grub/install-grub.pl
index 8df18cbd901..59f5638044f 100644
--- a/nixos/modules/system/boot/loader/grub/install-grub.pl
+++ b/nixos/modules/system/boot/loader/grub/install-grub.pl
@@ -6,8 +6,11 @@ use File::Basename;
 use File::Path;
 use File::stat;
 use File::Copy;
+use File::Copy::Recursive qw(rcopy pathrm);
 use File::Slurp;
 use File::Temp;
+use JSON;
+use File::Find;
 require List::Compare;
 use POSIX;
 use Cwd;
@@ -20,6 +23,16 @@ my $dom = XML::LibXML->load_xml(location => $ARGV[0]);
 
 sub get { my ($name) = @_; return $dom->findvalue("/expr/attrs/attr[\@name = '$name']/*/\@value"); }
 
+sub getList {
+    my ($name) = @_;
+    my @list = ();
+    foreach my $entry ($dom->findnodes("/expr/attrs/attr[\@name = '$name']/list/string/\@value")) {
+        $entry = $entry->findvalue(".") or die;
+        push(@list, $entry);
+    }
+    return @list;
+}
+
 sub readFile {
     my ($fn) = @_; local $/ = undef;
     open FILE, "<$fn" or return undef; my $s = <FILE>; close FILE;
@@ -49,7 +62,6 @@ my $extraPrepareConfig = get("extraPrepareConfig");
 my $extraPerEntryConfig = get("extraPerEntryConfig");
 my $extraEntries = get("extraEntries");
 my $extraEntriesBeforeNixOS = get("extraEntriesBeforeNixOS") eq "true";
-my $extraInitrd = get("extraInitrd");
 my $splashImage = get("splashImage");
 my $splashMode = get("splashMode");
 my $backgroundColor = get("backgroundColor");
@@ -72,6 +84,7 @@ my $gfxpayloadBios = get("gfxpayloadBios");
 my $bootloaderId = get("bootloaderId");
 my $forceInstall = get("forceInstall");
 my $font = get("font");
+my $theme = get("theme");
 $ENV{'PATH'} = get("path");
 
 die "unsupported GRUB version\n" if $grubVersion != 1 && $grubVersion != 2;
@@ -232,13 +245,6 @@ my $grubStore;
 if ($copyKernels == 0) {
     $grubStore = GrubFs($storePath);
 }
-my $extraInitrdPath;
-if ($extraInitrd) {
-    if (! -f $extraInitrd) {
-        print STDERR "Warning: the specified extraInitrd " . $extraInitrd . " doesn't exist. Your system won't boot without it.\n";
-    }
-    $extraInitrdPath = GrubFs($extraInitrd);
-}
 
 # Generate the header.
 my $conf .= "# Automatically generated.  DO NOT EDIT THIS FILE!\n";
@@ -249,12 +255,51 @@ if ($grubVersion == 1) {
         timeout $timeout
     ";
     if ($splashImage) {
-        copy $splashImage, "$bootPath/background.xpm.gz" or die "cannot copy $splashImage to $bootPath\n";
+        copy $splashImage, "$bootPath/background.xpm.gz" or die "cannot copy $splashImage to $bootPath: $!\n";
         $conf .= "splashimage " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/background.xpm.gz\n";
     }
 }
 
 else {
+    my @users = ();
+    foreach my $user ($dom->findnodes('/expr/attrs/attr[@name = "users"]/attrs/attr')) {
+        my $name = $user->findvalue('@name') or die;
+        my $hashedPassword = $user->findvalue('./attrs/attr[@name = "hashedPassword"]/string/@value');
+        my $hashedPasswordFile = $user->findvalue('./attrs/attr[@name = "hashedPasswordFile"]/string/@value');
+        my $password = $user->findvalue('./attrs/attr[@name = "password"]/string/@value');
+        my $passwordFile = $user->findvalue('./attrs/attr[@name = "passwordFile"]/string/@value');
+
+        if ($hashedPasswordFile) {
+            open(my $f, '<', $hashedPasswordFile) or die "Can't read file '$hashedPasswordFile'!";
+            $hashedPassword = <$f>;
+            chomp $hashedPassword;
+        }
+        if ($passwordFile) {
+            open(my $f, '<', $passwordFile) or die "Can't read file '$passwordFile'!";
+            $password = <$f>;
+            chomp $password;
+        }
+
+        if ($hashedPassword) {
+            if (index($hashedPassword, "grub.pbkdf2.") == 0) {
+                $conf .= "\npassword_pbkdf2 $name $hashedPassword";
+            }
+            else {
+                die "Password hash for GRUB user '$name' is not valid!";
+            }
+        }
+        elsif ($password) {
+            $conf .= "\npassword $name $password";
+        }
+        else {
+            die "GRUB user '$name' has no password!";
+        }
+        push(@users, $name);
+    }
+    if (@users) {
+        $conf .= "\nset superusers=\"" . join(' ',@users) . "\"\n";
+    }
+
     if ($copyKernels == 0) {
         $conf .= "
             " . $grubStore->search;
@@ -288,7 +333,7 @@ else {
     ";
 
     if ($font) {
-        copy $font, "$bootPath/converted-font.pf2" or die "cannot copy $font to $bootPath\n";
+        copy $font, "$bootPath/converted-font.pf2" or die "cannot copy $font to $bootPath: $!\n";
         $conf .= "
             insmod font
             if loadfont " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/converted-font.pf2; then
@@ -316,7 +361,7 @@ else {
 		    background_color '$backgroundColor'
 		    ";
 		}
-        copy $splashImage, "$bootPath/background$suffix" or die "cannot copy $splashImage to $bootPath\n";
+        copy $splashImage, "$bootPath/background$suffix" or die "cannot copy $splashImage to $bootPath: $!\n";
         $conf .= "
             insmod " . substr($suffix, 1) . "
             if background_image --mode '$splashMode' " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/background$suffix; then
@@ -328,6 +373,28 @@ else {
             fi
         ";
     }
+
+    rmtree("$bootPath/theme") or die "cannot clean up theme folder in $bootPath\n" if -e "$bootPath/theme";
+
+    if ($theme) {
+        # Copy theme
+        rcopy($theme, "$bootPath/theme") or die "cannot copy $theme to $bootPath\n";
+        $conf .= "
+            # Sets theme.
+            set theme=" . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/theme/theme.txt
+            export theme
+            # Load theme fonts, if any
+         ";
+
+         find( { wanted => sub {
+             if ($_ =~ /\.pf2$/i) {
+                 $font = File::Spec->abs2rel($File::Find::name, $theme);
+                 $conf .= "
+                     loadfont " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/theme/$font
+                 ";
+             }
+         }, no_chdir => 1 }, $theme );
+     }
 }
 
 $conf .= "$extraConfig\n";
@@ -350,22 +417,43 @@ sub copyToKernelsDir {
     # kernels or initrd if this script is ever interrupted.
     if (! -e $dst) {
         my $tmp = "$dst.tmp";
-        copy $path, $tmp or die "cannot copy $path to $tmp\n";
-        rename $tmp, $dst or die "cannot rename $tmp to $dst\n";
+        copy $path, $tmp or die "cannot copy $path to $tmp: $!\n";
+        rename $tmp, $dst or die "cannot rename $tmp to $dst: $!\n";
     }
     $copied{$dst} = 1;
     return ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/kernels/$name";
 }
 
 sub addEntry {
-    my ($name, $path) = @_;
+    my ($name, $path, $options) = @_;
     return unless -e "$path/kernel" && -e "$path/initrd";
 
     my $kernel = copyToKernelsDir(Cwd::abs_path("$path/kernel"));
     my $initrd = copyToKernelsDir(Cwd::abs_path("$path/initrd"));
-    if ($extraInitrd) {
-        $initrd .= " " .$extraInitrdPath->path;
+
+    # Include second initrd with secrets
+    if (-e -x "$path/append-initrd-secrets") {
+      my $initrdName = basename($initrd);
+      my $initrdSecretsPath = "$bootPath/kernels/$initrdName-secrets";
+
+      mkpath(dirname($initrdSecretsPath), 0, 0755);
+      my $oldUmask = umask;
+      # Make sure initrd is not world readable (won't work if /boot is FAT)
+      umask 0137;
+      my $initrdSecretsPathTemp = File::Temp::mktemp("$initrdSecretsPath.XXXXXXXX");
+      system("$path/append-initrd-secrets", $initrdSecretsPathTemp) == 0 or die "failed to create initrd secrets: $!\n";
+      # Check whether any secrets were actually added
+      if (-e $initrdSecretsPathTemp && ! -z _) {
+        rename $initrdSecretsPathTemp, $initrdSecretsPath or die "failed to move initrd secrets into place: $!\n";
+        $copied{$initrdSecretsPath} = 1;
+        $initrd .= " " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/kernels/$initrdName-secrets";
+      } else {
+        unlink $initrdSecretsPathTemp;
+        rmdir dirname($initrdSecretsPathTemp);
+      }
+      umask $oldUmask;
     }
+
     my $xen = -e "$path/xen.gz" ? copyToKernelsDir(Cwd::abs_path("$path/xen.gz")) : undef;
 
     # FIXME: $confName
@@ -383,14 +471,11 @@ sub addEntry {
         $conf .= "  " . ($xen ? "module" : "kernel") . " $kernel $kernelParams\n";
         $conf .= "  " . ($xen ? "module" : "initrd") . " $initrd\n\n";
     } else {
-        $conf .= "menuentry \"$name\" {\n";
+        $conf .= "menuentry \"$name\" " . ($options||"") . " {\n";
         $conf .= $grubBoot->search . "\n";
         if ($copyKernels == 0) {
             $conf .= $grubStore->search . "\n";
         }
-        if ($extraInitrd) {
-            $conf .= $extraInitrdPath->search . "\n";
-        }
         $conf .= "  $extraPerEntryConfig\n" if $extraPerEntryConfig;
         $conf .= "  multiboot $xen $xenParams\n" if $xen;
         $conf .= "  " . ($xen ? "module" : "linux") . " $kernel $kernelParams\n";
@@ -403,7 +488,7 @@ sub addEntry {
 # Add default entries.
 $conf .= "$extraEntries\n" if $extraEntriesBeforeNixOS;
 
-addEntry("NixOS - Default", $defaultConfig);
+addEntry("NixOS - Default", $defaultConfig, "--unrestricted");
 
 $conf .= "$extraEntries\n" unless $extraEntriesBeforeNixOS;
 
@@ -526,7 +611,7 @@ if (get("useOSProber") eq "true") {
 }
 
 # Atomically switch to the new config
-rename $tmpFile, $confFile or die "cannot rename $tmpFile to $confFile\n";
+rename $tmpFile, $confFile or die "cannot rename $tmpFile to $confFile: $!\n";
 
 
 # Remove obsolete files from $bootPath/kernels.
@@ -547,9 +632,12 @@ struct(GrubState => {
     efi => '$',
     devices => '$',
     efiMountPoint => '$',
+    extraGrubInstallArgs => '@',
 });
+# If you add something to the state file, only add it to the end
+# because it is read line-by-line.
 sub readGrubState {
-    my $defaultGrubState = GrubState->new(name => "", version => "", efi => "", devices => "", efiMountPoint => "" );
+    my $defaultGrubState = GrubState->new(name => "", version => "", efi => "", devices => "", efiMountPoint => "", extraGrubInstallArgs => () );
     open FILE, "<$bootPath/grub/state" or return $defaultGrubState;
     local $/ = "\n";
     my $name = <FILE>;
@@ -562,24 +650,37 @@ sub readGrubState {
     chomp($devices);
     my $efiMountPoint = <FILE>;
     chomp($efiMountPoint);
+    # Historically, arguments in the state file were one per each line, but that
+    # gets really messy when newlines are involved, structured arguments
+    # like lists are needed (they have to have a separator encoding), or even worse,
+    # when we need to remove a setting in the future. Thus, the 6th line is a JSON
+    # object that can store structured data, with named keys, and all new state
+    # should go in there.
+    my $jsonStateLine = <FILE>;
+    # For historical reasons we do not check the values above for un-definedness
+    # (that is, when the state file has too few lines and EOF is reached),
+    # because the above come from the first version of this logic and are thus
+    # guaranteed to be present.
+    $jsonStateLine = defined $jsonStateLine ? $jsonStateLine : '{}'; # empty JSON object
+    chomp($jsonStateLine);
+    if ($jsonStateLine eq "") {
+        $jsonStateLine = '{}'; # empty JSON object
+    }
+    my %jsonState = %{decode_json($jsonStateLine)};
+    my @extraGrubInstallArgs = exists($jsonState{'extraGrubInstallArgs'}) ? @{$jsonState{'extraGrubInstallArgs'}} : ();
     close FILE;
-    my $grubState = GrubState->new(name => $name, version => $version, efi => $efi, devices => $devices, efiMountPoint => $efiMountPoint );
+    my $grubState = GrubState->new(name => $name, version => $version, efi => $efi, devices => $devices, efiMountPoint => $efiMountPoint, extraGrubInstallArgs => \@extraGrubInstallArgs );
     return $grubState
 }
 
-sub getDeviceTargets {
-    my @devices = ();
-    foreach my $dev ($dom->findnodes('/expr/attrs/attr[@name = "devices"]/list/string/@value')) {
-        $dev = $dev->findvalue(".") or die;
-        push(@devices, $dev);
-    }
-    return @devices;
-}
-my @deviceTargets = getDeviceTargets();
+my @deviceTargets = getList('devices');
 my $prevGrubState = readGrubState();
 my @prevDeviceTargets = split/,/, $prevGrubState->devices;
+my @extraGrubInstallArgs = getList('extraGrubInstallArgs');
+my @prevExtraGrubInstallArgs = @{$prevGrubState->extraGrubInstallArgs};
 
 my $devicesDiffer = scalar (List::Compare->new( '-u', '-a', \@deviceTargets, \@prevDeviceTargets)->get_symmetric_difference());
+my $extraGrubInstallArgsDiffer = scalar (List::Compare->new( '-u', '-a', \@extraGrubInstallArgs, \@prevExtraGrubInstallArgs)->get_symmetric_difference());
 my $nameDiffer = get("fullName") ne $prevGrubState->name;
 my $versionDiffer = get("fullVersion") ne $prevGrubState->version;
 my $efiDiffer = $efiTarget ne $prevGrubState->efi;
@@ -588,25 +689,25 @@ if (($ENV{'NIXOS_INSTALL_GRUB'} // "") eq "1") {
     warn "NIXOS_INSTALL_GRUB env var deprecated, use NIXOS_INSTALL_BOOTLOADER";
     $ENV{'NIXOS_INSTALL_BOOTLOADER'} = "1";
 }
-my $requireNewInstall = $devicesDiffer || $nameDiffer || $versionDiffer || $efiDiffer || $efiMountPointDiffer || (($ENV{'NIXOS_INSTALL_BOOTLOADER'} // "") eq "1");
+my $requireNewInstall = $devicesDiffer || $extraGrubInstallArgsDiffer || $nameDiffer || $versionDiffer || $efiDiffer || $efiMountPointDiffer || (($ENV{'NIXOS_INSTALL_BOOTLOADER'} // "") eq "1");
 
 # install a symlink so that grub can detect the boot drive
-my $tmpDir = File::Temp::tempdir(CLEANUP => 1) or die "Failed to create temporary space";
-symlink "$bootPath", "$tmpDir/boot" or die "Failed to symlink $tmpDir/boot";
+my $tmpDir = File::Temp::tempdir(CLEANUP => 1) or die "Failed to create temporary space: $!";
+symlink "$bootPath", "$tmpDir/boot" or die "Failed to symlink $tmpDir/boot: $!";
 
 # install non-EFI GRUB
 if (($requireNewInstall != 0) && ($efiTarget eq "no" || $efiTarget eq "both")) {
     foreach my $dev (@deviceTargets) {
         next if $dev eq "nodev";
         print STDERR "installing the GRUB $grubVersion boot loader on $dev...\n";
-        my @command = ("$grub/sbin/grub-install", "--recheck", "--root-directory=$tmpDir", Cwd::abs_path($dev));
+        my @command = ("$grub/sbin/grub-install", "--recheck", "--root-directory=$tmpDir", Cwd::abs_path($dev), @extraGrubInstallArgs);
         if ($forceInstall eq "true") {
             push @command, "--force";
         }
         if ($grubTarget ne "") {
             push @command, "--target=$grubTarget";
         }
-        (system @command) == 0 or die "$0: installation of GRUB on $dev failed\n";
+        (system @command) == 0 or die "$0: installation of GRUB on $dev failed: $!\n";
     }
 }
 
@@ -614,7 +715,7 @@ if (($requireNewInstall != 0) && ($efiTarget eq "no" || $efiTarget eq "both")) {
 # install EFI GRUB
 if (($requireNewInstall != 0) && ($efiTarget eq "only" || $efiTarget eq "both")) {
     print STDERR "installing the GRUB $grubVersion EFI boot loader into $efiSysMountPoint...\n";
-    my @command = ("$grubEfi/sbin/grub-install", "--recheck", "--target=$grubTargetEfi", "--boot-directory=$bootPath", "--efi-directory=$efiSysMountPoint");
+    my @command = ("$grubEfi/sbin/grub-install", "--recheck", "--target=$grubTargetEfi", "--boot-directory=$bootPath", "--efi-directory=$efiSysMountPoint", @extraGrubInstallArgs);
     if ($forceInstall eq "true") {
         push @command, "--force";
     }
@@ -625,17 +726,29 @@ if (($requireNewInstall != 0) && ($efiTarget eq "only" || $efiTarget eq "both"))
         push @command, "--removable" if $efiInstallAsRemovable eq "true";
     }
 
-    (system @command) == 0 or die "$0: installation of GRUB EFI into $efiSysMountPoint failed\n";
+    (system @command) == 0 or die "$0: installation of GRUB EFI into $efiSysMountPoint failed: $!\n";
 }
 
 
 # update GRUB state file
 if ($requireNewInstall != 0) {
-    open FILE, ">$bootPath/grub/state" or die "cannot create $bootPath/grub/state: $!\n";
+    # Temp file for atomic rename.
+    my $stateFile = "$bootPath/grub/state";
+    my $stateFileTmp = $stateFile . ".tmp";
+
+    open FILE, ">$stateFileTmp" or die "cannot create $stateFileTmp: $!\n";
     print FILE get("fullName"), "\n" or die;
     print FILE get("fullVersion"), "\n" or die;
     print FILE $efiTarget, "\n" or die;
     print FILE join( ",", @deviceTargets ), "\n" or die;
     print FILE $efiSysMountPoint, "\n" or die;
+    my %jsonState = (
+        extraGrubInstallArgs => \@extraGrubInstallArgs
+    );
+    my $jsonStateLine = encode_json(\%jsonState);
+    print FILE $jsonStateLine, "\n" or die;
     close FILE or die;
+
+    # Atomically switch to the new state file
+    rename $stateFileTmp, $stateFile or die "cannot rename $stateFileTmp to $stateFile: $!\n";
 }
diff --git a/nixos/modules/system/boot/loader/init-script/init-script-builder.sh b/nixos/modules/system/boot/loader/init-script/init-script-builder.sh
index 6f48d2539ac..2a1ec479fea 100644
--- a/nixos/modules/system/boot/loader/init-script/init-script-builder.sh
+++ b/nixos/modules/system/boot/loader/init-script/init-script-builder.sh
@@ -53,7 +53,7 @@ addEntry() {
       echo "exec $stage2"
     )"
 
-    [ "$path" != "$defaultConfig" ] || { 
+    [ "$path" != "$defaultConfig" ] || {
       echo "$content" > $tmp
       echo "# older configurations: $targetOther" >> $tmp
       chmod +x $tmp
diff --git a/nixos/modules/system/boot/loader/raspberrypi/raspberrypi-builder.sh b/nixos/modules/system/boot/loader/raspberrypi/raspberrypi-builder.sh
index c8b5bf2e61a..0541ca1ba62 100644
--- a/nixos/modules/system/boot/loader/raspberrypi/raspberrypi-builder.sh
+++ b/nixos/modules/system/boot/loader/raspberrypi/raspberrypi-builder.sh
@@ -1,4 +1,7 @@
-#! @bash@/bin/sh -e
+#! @bash@/bin/sh
+
+# This can end up being called disregarding the shebang.
+set -e
 
 shopt -s nullglob
 
diff --git a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py
index f48a085ce57..97e824fe629 100644
--- a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py
+++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py
@@ -47,9 +47,9 @@ def write_loader_conf(profile, generation):
         if "@timeout@" != "":
             f.write("timeout @timeout@\n")
         if profile:
-            f.write("default nixos-%s-generation-%d\n" % (profile, generation))
+            f.write("default nixos-%s-generation-%d.conf\n" % (profile, generation))
         else:
-            f.write("default nixos-generation-%d\n" % (generation))
+            f.write("default nixos-generation-%d.conf\n" % (generation))
         if not @editor@:
             f.write("editor 0\n");
         f.write("console-mode @consoleMode@\n");
@@ -197,6 +197,22 @@ def main():
             subprocess.check_call(["@systemd@/bin/bootctl", "--path=@efiSysMountPoint@", "install"])
         else:
             subprocess.check_call(["@systemd@/bin/bootctl", "--path=@efiSysMountPoint@", "--no-variables", "install"])
+    else:
+        # Update bootloader to latest if needed
+        systemd_version = subprocess.check_output(["@systemd@/bin/bootctl", "--version"], universal_newlines=True).split()[1]
+        sdboot_status = subprocess.check_output(["@systemd@/bin/bootctl", "--path=@efiSysMountPoint@", "status"], universal_newlines=True)
+
+        # See status_binaries() in systemd bootctl.c for code which generates this
+        m = re.search("^\W+File:.*/EFI/(BOOT|systemd)/.*\.efi \(systemd-boot (\d+)\)$",
+                      sdboot_status, re.IGNORECASE | re.MULTILINE)
+        if m is None:
+            print("could not find any previously installed systemd-boot")
+        else:
+            sdboot_version = m.group(2)
+            if systemd_version > sdboot_version:
+                print("updating systemd-boot from %s to %s" % (sdboot_version, systemd_version))
+                subprocess.check_call(["@systemd@/bin/bootctl", "--path=@efiSysMountPoint@", "update"])
+
 
     mkdir_p("@efiSysMountPoint@/efi/nixos")
     mkdir_p("@efiSysMountPoint@/loader/entries")
diff --git a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix
index 22d459ceb04..f0bd76a3c1d 100644
--- a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix
+++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix
@@ -64,10 +64,10 @@ in {
       example = 120;
       type = types.nullOr types.int;
       description = ''
-        Maximum number of latest generations in the boot menu. 
+        Maximum number of latest generations in the boot menu.
         Useful to prevent boot partition running out of disk space.
 
-        <literal>null</literal> means no limit i.e. all generations 
+        <literal>null</literal> means no limit i.e. all generations
         that were not garbage collected yet.
       '';
     };
diff --git a/nixos/modules/system/boot/luksroot.nix b/nixos/modules/system/boot/luksroot.nix
index 31f1e22cda3..166f89c7066 100644
--- a/nixos/modules/system/boot/luksroot.nix
+++ b/nixos/modules/system/boot/luksroot.nix
@@ -140,7 +140,7 @@ let
     umount /crypt-ramfs 2>/dev/null
   '';
 
-  openCommand = name': { name, device, header, keyFile, keyFileSize, keyFileOffset, allowDiscards, yubikey, gpgCard, fido2, fallbackToPassword, ... }: assert name' == name;
+  openCommand = name': { name, device, header, keyFile, keyFileSize, keyFileOffset, allowDiscards, yubikey, gpgCard, fido2, fallbackToPassword, preOpenCommands, postOpenCommands,... }: assert name' == name;
   let
     csopen   = "cryptsetup luksOpen ${device} ${name} ${optionalString allowDiscards "--allow-discards"} ${optionalString (header != null) "--header=${header}"}";
     cschange = "cryptsetup luksChangeKey ${device} ${optionalString (header != null) "--header=${header}"}";
@@ -412,11 +412,17 @@ let
     }
     ''}
 
+    # commands to run right before we mount our device
+    ${preOpenCommands}
+
     ${if (luks.yubikeySupport && (yubikey != null)) || (luks.gpgSupport && (gpgCard != null)) || (luks.fido2Support && (fido2.credential != null)) then ''
     open_with_hardware
     '' else ''
     open_normally
     ''}
+
+    # commands to run right after we mounted our device
+    ${postOpenCommands}
   '';
 
   askPass = pkgs.writeScriptBin "cryptsetup-askpass" ''
@@ -467,8 +473,6 @@ in
         [ "aes" "aes_generic" "blowfish" "twofish"
           "serpent" "cbc" "xts" "lrw" "sha1" "sha256" "sha512"
           "af_alg" "algif_skcipher"
-
-          (if pkgs.stdenv.hostPlatform.system == "x86_64-linux" then "aes_x86_64" else "aes_i586")
         ];
       description = ''
         A list of cryptographic kernel modules needed to decrypt the root device(s).
@@ -735,6 +739,30 @@ in
               };
             });
           };
+
+          preOpenCommands = mkOption {
+            type = types.lines;
+            default = "";
+            example = ''
+              mkdir -p /tmp/persistent
+              mount -t zfs rpool/safe/persistent /tmp/persistent
+            '';
+            description = ''
+              Commands that should be run right before we try to mount our LUKS device.
+              This can be useful, if the keys needed to open the drive is on another partion.
+            '';
+          };
+
+          postOpenCommands = mkOption {
+            type = types.lines;
+            default = "";
+            example = ''
+              umount /tmp/persistent
+            '';
+            description = ''
+              Commands that should be run right after we have mounted our LUKS device.
+            '';
+          };
         };
       }));
     };
diff --git a/nixos/modules/system/boot/modprobe.nix b/nixos/modules/system/boot/modprobe.nix
index dee0ab470c9..c75f32c4d99 100644
--- a/nixos/modules/system/boot/modprobe.nix
+++ b/nixos/modules/system/boot/modprobe.nix
@@ -28,7 +28,7 @@ with lib;
         Any additional configuration to be appended to the generated
         <filename>modprobe.conf</filename>.  This is typically used to
         specify module options.  See
-        <citerefentry><refentrytitle>modprobe.conf</refentrytitle>
+        <citerefentry><refentrytitle>modprobe.d</refentrytitle>
         <manvolnum>5</manvolnum></citerefentry> for details.
       '';
       type = types.lines;
diff --git a/nixos/modules/system/boot/networkd.nix b/nixos/modules/system/boot/networkd.nix
index 9b34b12e73a..47689b2a470 100644
--- a/nixos/modules/system/boot/networkd.nix
+++ b/nixos/modules/system/boot/networkd.nix
@@ -8,359 +8,714 @@ let
 
   cfg = config.systemd.network;
 
-  checkLink = checkUnitConfig "Link" [
-    (assertOnlyFields [
-      "Description" "Alias" "MACAddressPolicy" "MACAddress" "NamePolicy" "Name" "OriginalName"
-      "MTUBytes" "BitsPerSecond" "Duplex" "AutoNegotiation" "WakeOnLan" "Port" "Advertise"
-      "TCPSegmentationOffload" "TCP6SegmentationOffload" "GenericSegmentationOffload"
-      "GenericReceiveOffload" "LargeReceiveOffload" "RxChannels" "TxChannels"
-      "OtherChannels" "CombinedChannels"
-    ])
-    (assertValueOneOf "MACAddressPolicy" ["persistent" "random" "none"])
-    (assertMacAddress "MACAddress")
-    (assertByteFormat "MTUBytes")
-    (assertByteFormat "BitsPerSecond")
-    (assertValueOneOf "Duplex" ["half" "full"])
-    (assertValueOneOf "AutoNegotiation" boolValues)
-    (assertValueOneOf "WakeOnLan" ["phy" "unicast" "multicast" "broadcast" "arp" "magic" "secureon" "off"])
-    (assertValueOneOf "Port" ["tp" "aui" "bnc" "mii" "fibre"])
-    (assertValueOneOf "TCPSegmentationOffload" boolValues)
-    (assertValueOneOf "TCP6SegmentationOffload" boolValues)
-    (assertValueOneOf "GenericSegmentationOffload" boolValues)
-    (assertValueOneOf "UDPSegmentationOffload" boolValues)
-    (assertValueOneOf "GenericReceiveOffload" boolValues)
-    (assertValueOneOf "LargeReceiveOffload" boolValues)
-    (assertInt "RxChannels")
-    (assertMinimum "RxChannels" 1)
-    (assertInt "TxChannels")
-    (assertMinimum "TxChannels" 1)
-    (assertInt "OtherChannels")
-    (assertMinimum "OtherChannels" 1)
-    (assertInt "CombinedChannels")
-    (assertMinimum "CombinedChannels" 1)
-  ];
-
-  checkNetdev = checkUnitConfig "Netdev" [
-    (assertOnlyFields [
-      "Description" "Name" "Kind" "MTUBytes" "MACAddress"
-    ])
-    (assertHasField "Name")
-    (assertHasField "Kind")
-    (assertValueOneOf "Kind" [
-      "bond" "bridge" "dummy" "gre" "gretap" "ip6gre" "ip6tnl" "ip6gretap" "ipip"
-      "ipvlan" "macvlan" "macvtap" "sit" "tap" "tun" "veth" "vlan" "vti" "vti6"
-      "vxlan" "geneve" "vrf" "vcan" "vxcan" "wireguard" "netdevsim" "xfrm"
-    ])
-    (assertByteFormat "MTUBytes")
-    (assertMacAddress "MACAddress")
-  ];
-
-  checkVRF = checkUnitConfig "VRF" [
-    (assertOnlyFields [ "Table" ])
-    (assertMinimum "Table" 0)
-  ];
+  check = {
+
+    link = {
+
+      sectionLink = checkUnitConfig "Link" [
+        (assertOnlyFields [
+          "Description"
+          "Alias"
+          "MACAddressPolicy"
+          "MACAddress"
+          "NamePolicy"
+          "Name"
+          "AlternativeNamesPolicy"
+          "AlternativeName"
+          "MTUBytes"
+          "BitsPerSecond"
+          "Duplex"
+          "AutoNegotiation"
+          "WakeOnLan"
+          "Port"
+          "Advertise"
+          "ReceiveChecksumOffload"
+          "TransmitChecksumOffload"
+          "TCPSegmentationOffload"
+          "TCP6SegmentationOffload"
+          "GenericSegmentationOffload"
+          "GenericReceiveOffload"
+          "LargeReceiveOffload"
+          "RxChannels"
+          "TxChannels"
+          "OtherChannels"
+          "CombinedChannels"
+          "RxBufferSize"
+          "TxBufferSize"
+        ])
+        (assertValueOneOf "MACAddressPolicy" ["persistent" "random" "none"])
+        (assertMacAddress "MACAddress")
+        (assertByteFormat "MTUBytes")
+        (assertByteFormat "BitsPerSecond")
+        (assertValueOneOf "Duplex" ["half" "full"])
+        (assertValueOneOf "AutoNegotiation" boolValues)
+        (assertValueOneOf "WakeOnLan" ["phy" "unicast" "multicast" "broadcast" "arp" "magic" "secureon" "off"])
+        (assertValueOneOf "Port" ["tp" "aui" "bnc" "mii" "fibre"])
+        (assertValueOneOf "ReceiveChecksumOffload" boolValues)
+        (assertValueOneOf "TransmitChecksumOffload" boolValues)
+        (assertValueOneOf "TCPSegmentationOffload" boolValues)
+        (assertValueOneOf "TCP6SegmentationOffload" boolValues)
+        (assertValueOneOf "GenericSegmentationOffload" boolValues)
+        (assertValueOneOf "GenericReceiveOffload" boolValues)
+        (assertValueOneOf "LargeReceiveOffload" boolValues)
+        (assertInt "RxChannels")
+        (assertRange "RxChannels" 1 4294967295)
+        (assertInt "TxChannels")
+        (assertRange "TxChannels" 1 4294967295)
+        (assertInt "OtherChannels")
+        (assertRange "OtherChannels" 1 4294967295)
+        (assertInt "CombinedChannels")
+        (assertRange "CombinedChannels" 1 4294967295)
+        (assertInt "RxBufferSize")
+        (assertInt "TxBufferSize")
+      ];
+    };
 
-  # NOTE The PrivateKey directive is missing on purpose here, please
-  # do not add it to this list. The nix store is world-readable let's
-  # refrain ourselves from providing a footgun.
-  checkWireGuard = checkUnitConfig "WireGuard" [
-    (assertOnlyFields [
-      "PrivateKeyFile" "ListenPort" "FwMark"
-    ])
-    # The following check won't work on nix <= 2.2
-    # see https://github.com/NixOS/nix/pull/2378
-    #
-    # Add this again when we'll have drop the
-    # nix < 2.2 support.
-    # (assertRange "FwMark" 1 4294967295)
-  ];
+    netdev = let
+
+      tunChecks = [
+        (assertOnlyFields [
+          "MultiQueue"
+          "PacketInfo"
+          "VNetHeader"
+          "User"
+          "Group"
+        ])
+        (assertValueOneOf "MultiQueue" boolValues)
+        (assertValueOneOf "PacketInfo" boolValues)
+        (assertValueOneOf "VNetHeader" boolValues)
+      ];
+    in {
+
+      sectionNetdev = checkUnitConfig "Netdev" [
+        (assertOnlyFields [
+          "Description"
+          "Name"
+          "Kind"
+          "MTUBytes"
+          "MACAddress"
+        ])
+        (assertHasField "Name")
+        (assertHasField "Kind")
+        (assertValueOneOf "Kind" [
+          "bond"
+          "bridge"
+          "dummy"
+          "gre"
+          "gretap"
+          "erspan"
+          "ip6gre"
+          "ip6tnl"
+          "ip6gretap"
+          "ipip"
+          "ipvlan"
+          "macvlan"
+          "macvtap"
+          "sit"
+          "tap"
+          "tun"
+          "veth"
+          "vlan"
+          "vti"
+          "vti6"
+          "vxlan"
+          "geneve"
+          "l2tp"
+          "macsec"
+          "vrf"
+          "vcan"
+          "vxcan"
+          "wireguard"
+          "netdevsim"
+          "nlmon"
+          "fou"
+          "xfrm"
+          "ifb"
+        ])
+        (assertByteFormat "MTUBytes")
+        (assertMacAddress "MACAddress")
+      ];
 
-  # NOTE The PresharedKey directive is missing on purpose here, please
-  # do not add it to this list. The nix store is world-readable,let's
-  # refrain ourselves from providing a footgun.
-  checkWireGuardPeer = checkUnitConfig "WireGuardPeer" [
-    (assertOnlyFields [
-      "PublicKey" "PresharedKeyFile" "AllowedIPs"
-      "Endpoint" "PersistentKeepalive"
-    ])
-    (assertRange "PersistentKeepalive" 1 65535)
-  ];
+      sectionVLAN = checkUnitConfig "VLAN" [
+        (assertOnlyFields [
+          "Id"
+          "GVRP"
+          "MVRP"
+          "LooseBinding"
+          "ReorderHeader"
+        ])
+        (assertInt "Id")
+        (assertRange "Id" 0 4094)
+        (assertValueOneOf "GVRP" boolValues)
+        (assertValueOneOf "MVRP" boolValues)
+        (assertValueOneOf "LooseBinding" boolValues)
+        (assertValueOneOf "ReorderHeader" boolValues)
+      ];
 
-  checkVlan = checkUnitConfig "VLAN" [
-    (assertOnlyFields ["Id" "GVRP" "MVRP" "LooseBinding" "ReorderHeader"])
-    (assertRange "Id" 0 4094)
-    (assertValueOneOf "GVRP" boolValues)
-    (assertValueOneOf "MVRP" boolValues)
-    (assertValueOneOf "LooseBinding" boolValues)
-    (assertValueOneOf "ReorderHeader" boolValues)
-  ];
+      sectionMACVLAN = checkUnitConfig "MACVLAN" [
+        (assertOnlyFields [
+          "Mode"
+        ])
+        (assertValueOneOf "Mode" ["private" "vepa" "bridge" "passthru"])
+      ];
 
-  checkMacvlan = checkUnitConfig "MACVLAN" [
-    (assertOnlyFields ["Mode"])
-    (assertValueOneOf "Mode" ["private" "vepa" "bridge" "passthru"])
-  ];
+      sectionVXLAN = checkUnitConfig "VXLAN" [
+        (assertOnlyFields [
+          "VNI"
+          "Remote"
+          "Local"
+          "Group"
+          "TOS"
+          "TTL"
+          "MacLearning"
+          "FDBAgeingSec"
+          "MaximumFDBEntries"
+          "ReduceARPProxy"
+          "L2MissNotification"
+          "L3MissNotification"
+          "RouteShortCircuit"
+          "UDPChecksum"
+          "UDP6ZeroChecksumTx"
+          "UDP6ZeroChecksumRx"
+          "RemoteChecksumTx"
+          "RemoteChecksumRx"
+          "GroupPolicyExtension"
+          "GenericProtocolExtension"
+          "DestinationPort"
+          "PortRange"
+          "FlowLabel"
+          "IPDoNotFragment"
+        ])
+        (assertInt "VNI")
+        (assertRange "VNI" 1 16777215)
+        (assertValueOneOf "MacLearning" boolValues)
+        (assertInt "MaximumFDBEntries")
+        (assertValueOneOf "ReduceARPProxy" boolValues)
+        (assertValueOneOf "L2MissNotification" boolValues)
+        (assertValueOneOf "L3MissNotification" boolValues)
+        (assertValueOneOf "RouteShortCircuit" boolValues)
+        (assertValueOneOf "UDPChecksum" boolValues)
+        (assertValueOneOf "UDP6ZeroChecksumTx" boolValues)
+        (assertValueOneOf "UDP6ZeroChecksumRx" boolValues)
+        (assertValueOneOf "RemoteChecksumTx" boolValues)
+        (assertValueOneOf "RemoteChecksumRx" boolValues)
+        (assertValueOneOf "GroupPolicyExtension" boolValues)
+        (assertValueOneOf "GenericProtocolExtension" boolValues)
+        (assertInt "FlowLabel")
+        (assertRange "FlowLabel" 0 1048575)
+        (assertValueOneOf "IPDoNotFragment" (boolValues + ["inherit"]))
+      ];
 
-  checkVxlan = checkUnitConfig "VXLAN" [
-    (assertOnlyFields [
-      "Id" "Remote" "Local" "TOS" "TTL" "MacLearning" "FDBAgeingSec"
-      "MaximumFDBEntries" "ReduceARPProxy" "L2MissNotification"
-      "L3MissNotification" "RouteShortCircuit" "UDPChecksum"
-      "UDP6ZeroChecksumTx" "UDP6ZeroChecksumRx" "RemoteChecksumTx"
-      "RemoteChecksumRx" "GroupPolicyExtension" "DestinationPort" "PortRange"
-      "FlowLabel"
-    ])
-    (assertRange "TTL" 0 255)
-    (assertValueOneOf "MacLearning" boolValues)
-    (assertValueOneOf "ReduceARPProxy" boolValues)
-    (assertValueOneOf "L2MissNotification" boolValues)
-    (assertValueOneOf "L3MissNotification" boolValues)
-    (assertValueOneOf "RouteShortCircuit" boolValues)
-    (assertValueOneOf "UDPChecksum" boolValues)
-    (assertValueOneOf "UDP6ZeroChecksumTx" boolValues)
-    (assertValueOneOf "UDP6ZeroChecksumRx" boolValues)
-    (assertValueOneOf "RemoteChecksumTx" boolValues)
-    (assertValueOneOf "RemoteChecksumRx" boolValues)
-    (assertValueOneOf "GroupPolicyExtension" boolValues)
-    (assertRange "FlowLabel" 0 1048575)
-  ];
+      sectionTunnel = checkUnitConfig "Tunnel" [
+        (assertOnlyFields [
+          "Local"
+          "Remote"
+          "TOS"
+          "TTL"
+          "DiscoverPathMTU"
+          "IPv6FlowLabel"
+          "CopyDSCP"
+          "EncapsulationLimit"
+          "Key"
+          "InputKey"
+          "OutputKey"
+          "Mode"
+          "Independent"
+          "AssignToLoopback"
+          "AllowLocalRemote"
+          "FooOverUDP"
+          "FOUDestinationPort"
+          "FOUSourcePort"
+          "Encapsulation"
+          "IPv6RapidDeploymentPrefix"
+          "ISATAP"
+          "SerializeTunneledPackets"
+          "ERSPANIndex"
+        ])
+        (assertInt "TTL")
+        (assertRange "TTL" 0 255)
+        (assertValueOneOf "DiscoverPathMTU" boolValues)
+        (assertValueOneOf "CopyDSCP" boolValues)
+        (assertValueOneOf "Mode" ["ip6ip6" "ipip6" "any"])
+        (assertValueOneOf "Independent" boolValues)
+        (assertValueOneOf "AssignToLoopback" boolValues)
+        (assertValueOneOf "AllowLocalRemote" boolValues)
+        (assertValueOneOf "FooOverUDP" boolValues)
+        (assertPort "FOUDestinationPort")
+        (assertPort "FOUSourcePort")
+        (assertValueOneOf "Encapsulation" ["FooOverUDP" "GenericUDPEncapsulation"])
+        (assertValueOneOf "ISATAP" boolValues)
+        (assertValueOneOf "SerializeTunneledPackets" boolValues)
+        (assertInt "ERSPANIndex")
+        (assertRange "ERSPANIndex" 1 1048575)
+      ];
 
-  checkTunnel = checkUnitConfig "Tunnel" [
-    (assertOnlyFields [
-      "Local" "Remote" "TOS" "TTL" "DiscoverPathMTU" "IPv6FlowLabel" "CopyDSCP"
-      "EncapsulationLimit" "Key" "InputKey" "OutputKey" "Mode" "Independent"
-      "AllowLocalRemote"
-    ])
-    (assertRange "TTL" 0 255)
-    (assertValueOneOf "DiscoverPathMTU" boolValues)
-    (assertValueOneOf "CopyDSCP" boolValues)
-    (assertValueOneOf "Mode" ["ip6ip6" "ipip6" "any"])
-    (assertValueOneOf "Independent" boolValues)
-    (assertValueOneOf "AllowLocalRemote" boolValues)
-  ];
+      sectionPeer = checkUnitConfig "Peer" [
+        (assertOnlyFields [
+          "Name"
+          "MACAddress"
+        ])
+        (assertMacAddress "MACAddress")
+      ];
 
-  checkPeer = checkUnitConfig "Peer" [
-    (assertOnlyFields ["Name" "MACAddress"])
-    (assertMacAddress "MACAddress")
-  ];
+      sectionTun = checkUnitConfig "Tun" tunChecks;
+
+      sectionTap = checkUnitConfig "Tap" tunChecks;
+
+      # NOTE The PrivateKey directive is missing on purpose here, please
+      # do not add it to this list. The nix store is world-readable let's
+      # refrain ourselves from providing a footgun.
+      sectionWireGuard = checkUnitConfig "WireGuard" [
+        (assertOnlyFields [
+          "PrivateKeyFile"
+          "ListenPort"
+          "FirewallMark"
+        ])
+        (assertInt "FirewallMark")
+        (assertRange "FirewallMark" 1 4294967295)
+      ];
 
-  tunTapChecks = [
-    (assertOnlyFields ["OneQueue" "MultiQueue" "PacketInfo" "VNetHeader" "User" "Group"])
-    (assertValueOneOf "OneQueue" boolValues)
-    (assertValueOneOf "MultiQueue" boolValues)
-    (assertValueOneOf "PacketInfo" boolValues)
-    (assertValueOneOf "VNetHeader" boolValues)
-  ];
+      # NOTE The PresharedKey directive is missing on purpose here, please
+      # do not add it to this list. The nix store is world-readable,let's
+      # refrain ourselves from providing a footgun.
+      sectionWireGuardPeer = checkUnitConfig "WireGuardPeer" [
+        (assertOnlyFields [
+          "PublicKey"
+          "PresharedKeyFile"
+          "AllowedIPs"
+          "Endpoint"
+          "PersistentKeepalive"
+        ])
+        (assertInt "PersistentKeepalive")
+        (assertRange "PersistentKeepalive" 0 65535)
+      ];
 
-  checkTun = checkUnitConfig "Tun" tunTapChecks;
-
-  checkTap = checkUnitConfig "Tap" tunTapChecks;
-
-  checkBond = checkUnitConfig "Bond" [
-    (assertOnlyFields [
-      "Mode" "TransmitHashPolicy" "LACPTransmitRate" "MIIMonitorSec"
-      "UpDelaySec" "DownDelaySec" "LearnPacketIntervalSec" "AdSelect"
-      "FailOverMACPolicy" "ARPValidate" "ARPIntervalSec" "ARPIPTargets"
-      "ARPAllTargets" "PrimaryReselectPolicy" "ResendIGMP" "PacketsPerSlave"
-      "GratuitousARP" "AllSlavesActive" "MinLinks"
-    ])
-    (assertValueOneOf "Mode" [
-      "balance-rr" "active-backup" "balance-xor"
-      "broadcast" "802.3ad" "balance-tlb" "balance-alb"
-    ])
-    (assertValueOneOf "TransmitHashPolicy" [
-      "layer2" "layer3+4" "layer2+3" "encap2+3" "encap3+4"
-    ])
-    (assertValueOneOf "LACPTransmitRate" ["slow" "fast"])
-    (assertValueOneOf "AdSelect" ["stable" "bandwidth" "count"])
-    (assertValueOneOf "FailOverMACPolicy" ["none" "active" "follow"])
-    (assertValueOneOf "ARPValidate" ["none" "active" "backup" "all"])
-    (assertValueOneOf "ARPAllTargets" ["any" "all"])
-    (assertValueOneOf "PrimaryReselectPolicy" ["always" "better" "failure"])
-    (assertRange "ResendIGMP" 0 255)
-    (assertRange "PacketsPerSlave" 0 65535)
-    (assertRange "GratuitousARP" 0 255)
-    (assertValueOneOf "AllSlavesActive" boolValues)
-  ];
+      sectionBond = checkUnitConfig "Bond" [
+        (assertOnlyFields [
+          "Mode"
+          "TransmitHashPolicy"
+          "LACPTransmitRate"
+          "MIIMonitorSec"
+          "UpDelaySec"
+          "DownDelaySec"
+          "LearnPacketIntervalSec"
+          "AdSelect"
+          "AdActorSystemPriority"
+          "AdUserPortKey"
+          "AdActorSystem"
+          "FailOverMACPolicy"
+          "ARPValidate"
+          "ARPIntervalSec"
+          "ARPIPTargets"
+          "ARPAllTargets"
+          "PrimaryReselectPolicy"
+          "ResendIGMP"
+          "PacketsPerSlave"
+          "GratuitousARP"
+          "AllSlavesActive"
+          "DynamicTransmitLoadBalancing"
+          "MinLinks"
+        ])
+        (assertValueOneOf "Mode" [
+          "balance-rr"
+          "active-backup"
+          "balance-xor"
+          "broadcast"
+          "802.3ad"
+          "balance-tlb"
+          "balance-alb"
+        ])
+        (assertValueOneOf "TransmitHashPolicy" [
+          "layer2"
+          "layer3+4"
+          "layer2+3"
+          "encap2+3"
+          "encap3+4"
+        ])
+        (assertValueOneOf "LACPTransmitRate" ["slow" "fast"])
+        (assertValueOneOf "AdSelect" ["stable" "bandwidth" "count"])
+        (assertInt "AdActorSystemPriority")
+        (assertRange "AdActorSystemPriority" 1 65535)
+        (assertInt "AdUserPortKey")
+        (assertRange "AdUserPortKey" 0 1023)
+        (assertValueOneOf "FailOverMACPolicy" ["none" "active" "follow"])
+        (assertValueOneOf "ARPValidate" ["none" "active" "backup" "all"])
+        (assertValueOneOf "ARPAllTargets" ["any" "all"])
+        (assertValueOneOf "PrimaryReselectPolicy" ["always" "better" "failure"])
+        (assertInt "ResendIGMP")
+        (assertRange "ResendIGMP" 0 255)
+        (assertInt "PacketsPerSlave")
+        (assertRange "PacketsPerSlave" 0 65535)
+        (assertInt "GratuitousARP")
+        (assertRange "GratuitousARP" 0 255)
+        (assertValueOneOf "AllSlavesActive" boolValues)
+        (assertValueOneOf "DynamicTransmitLoadBalancing" boolValues)
+        (assertInt "MinLinks")
+        (assertMinimum "MinLinks" 0)
+      ];
 
-  checkXfrm = checkUnitConfig "Xfrm" [
-    (assertOnlyFields [
-      "InterfaceId" "Independent"
-    ])
-    # The following check won't work on nix <= 2.2
-    # see https://github.com/NixOS/nix/pull/2378
-    #
-    # Add this again when we'll have drop the
-    # nix < 2.2 support.
-    # (assertRange "InterfaceId" 1 4294967295)
-    (assertValueOneOf "Independent" boolValues)
-  ];
+      sectionXfrm = checkUnitConfig "Xfrm" [
+        (assertOnlyFields [
+          "InterfaceId"
+          "Independent"
+        ])
+        (assertInt "InterfaceId")
+        (assertRange "InterfaceId" 1 4294967295)
+        (assertValueOneOf "Independent" boolValues)
+      ];
 
-  checkNetwork = checkUnitConfig "Network" [
-    (assertOnlyFields [
-      "Description" "DHCP" "DHCPServer" "LinkLocalAddressing" "IPv4LLRoute"
-      "IPv6Token" "LLMNR" "MulticastDNS" "DNSOverTLS" "DNSSEC"
-      "DNSSECNegativeTrustAnchors" "LLDP" "EmitLLDP" "BindCarrier" "Address"
-      "Gateway" "DNS" "Domains" "NTP" "IPForward" "IPMasquerade"
-      "IPv6PrivacyExtensions" "IPv6AcceptRA" "IPv6DuplicateAddressDetection"
-      "IPv6HopLimit" "IPv4ProxyARP" "IPv6ProxyNDP" "IPv6ProxyNDPAddress"
-      "IPv6PrefixDelegation" "IPv6MTUBytes" "Bridge" "Bond" "VRF" "VLAN"
-      "IPVLAN" "MACVLAN" "VXLAN" "Tunnel" "ActiveSlave" "PrimarySlave"
-      "ConfigureWithoutCarrier" "Xfrm" "KeepConfiguration"
-    ])
-    # Note: For DHCP the values both, none, v4, v6 are deprecated
-    (assertValueOneOf "DHCP" ["yes" "no" "ipv4" "ipv6" "both" "none" "v4" "v6"])
-    (assertValueOneOf "DHCPServer" boolValues)
-    (assertValueOneOf "LinkLocalAddressing" ["yes" "no" "ipv4" "ipv6" "ipv4-fallback" "fallback"])
-    (assertValueOneOf "IPv4LLRoute" boolValues)
-    (assertValueOneOf "LLMNR" ["yes" "resolve" "no"])
-    (assertValueOneOf "MulticastDNS" ["yes" "resolve" "no"])
-    (assertValueOneOf "DNSOverTLS" ["opportunistic" "no"])
-    (assertValueOneOf "DNSSEC" ["yes" "allow-downgrade" "no"])
-    (assertValueOneOf "LLDP" ["yes" "routers-only" "no"])
-    (assertValueOneOf "EmitLLDP" ["yes" "no" "nearest-bridge" "non-tpmr-bridge" "customer-bridge"])
-    (assertValueOneOf "IPForward" ["yes" "no" "ipv4" "ipv6"])
-    (assertValueOneOf "IPMasquerade" boolValues)
-    (assertValueOneOf "IPv6PrivacyExtensions" ["yes" "no" "prefer-public" "kernel"])
-    (assertValueOneOf "IPv6AcceptRA" boolValues)
-    (assertValueOneOf "IPv4ProxyARP" boolValues)
-    (assertValueOneOf "IPv6ProxyNDP" boolValues)
-    (assertValueOneOf "IPv6PrefixDelegation" (boolValues ++ [ "dhcpv6" "static" ]))
-    (assertValueOneOf "ActiveSlave" boolValues)
-    (assertValueOneOf "PrimarySlave" boolValues)
-    (assertValueOneOf "ConfigureWithoutCarrier" boolValues)
-    (assertValueOneOf "KeepConfiguration" (boolValues ++ ["static" "dhcp-on-stop" "dhcp"]))
-  ];
+      sectionVRF = checkUnitConfig "VRF" [
+        (assertOnlyFields [
+          "Table"
+        ])
+        (assertInt "Table")
+        (assertMinimum "Table" 0)
+      ];
+    };
 
-  checkAddress = checkUnitConfig "Address" [
-    (assertOnlyFields [
-      "Address" "Peer" "Broadcast" "Label" "PreferredLifetime" "Scope"
-      "HomeAddress" "DuplicateAddressDetection" "ManageTemporaryAddress"
-      "PrefixRoute" "AutoJoin"
-    ])
-    (assertHasField "Address")
-    (assertValueOneOf "PreferredLifetime" ["forever" "infinity" "0" 0])
-    (assertValueOneOf "HomeAddress" boolValues)
-    (assertValueOneOf "DuplicateAddressDetection" boolValues)
-    (assertValueOneOf "ManageTemporaryAddress" boolValues)
-    (assertValueOneOf "PrefixRoute" boolValues)
-    (assertValueOneOf "AutoJoin" boolValues)
-  ];
+    network = {
+
+      sectionLink = checkUnitConfig "Link" [
+        (assertOnlyFields [
+          "MACAddress"
+          "MTUBytes"
+          "ARP"
+          "Multicast"
+          "AllMulticast"
+          "Unmanaged"
+          "RequiredForOnline"
+        ])
+        (assertMacAddress "MACAddress")
+        (assertByteFormat "MTUBytes")
+        (assertValueOneOf "ARP" boolValues)
+        (assertValueOneOf "Multicast" boolValues)
+        (assertValueOneOf "AllMulticast" boolValues)
+        (assertValueOneOf "Unmanaged" boolValues)
+        (assertValueOneOf "RequiredForOnline" (boolValues ++ [
+          "missing"
+          "off"
+          "no-carrier"
+          "dormant"
+          "degraded-carrier"
+          "carrier"
+          "degraded"
+          "enslaved"
+          "routable"
+        ]))
+      ];
 
-  checkRoutingPolicyRule = checkUnitConfig "RoutingPolicyRule" [
-    (assertOnlyFields [
-      "TypeOfService" "From" "To" "FirewallMark" "Table" "Priority"
-      "IncomingInterface" "OutgoingInterface" "SourcePort" "DestinationPort"
-      "IPProtocol" "InvertRule" "Family"
-    ])
-    (assertRange "TypeOfService" 0 255)
-    # The following check won't work on nix <= 2.2
-    # see https://github.com/NixOS/nix/pull/2378
-    #
-    # Add this again when we'll have drop the
-    # nix < 2.2 support.
-    #  (assertRange "FirewallMark" 1 4294967295)
-    (assertInt "Priority")
-    (assertPort "SourcePort")
-    (assertPort "DestinationPort")
-    (assertValueOneOf "InvertRule" boolValues)
-    (assertValueOneOf "Family" ["ipv4" "ipv6" "both"])
-  ];
+      sectionNetwork = checkUnitConfig "Network" [
+        (assertOnlyFields [
+          "Description"
+          "DHCP"
+          "DHCPServer"
+          "LinkLocalAddressing"
+          "IPv4LLRoute"
+          "DefaultRouteOnDevice"
+          "IPv6Token"
+          "LLMNR"
+          "MulticastDNS"
+          "DNSOverTLS"
+          "DNSSEC"
+          "DNSSECNegativeTrustAnchors"
+          "LLDP"
+          "EmitLLDP"
+          "BindCarrier"
+          "Address"
+          "Gateway"
+          "DNS"
+          "Domains"
+          "DNSDefaultRoute"
+          "NTP"
+          "IPForward"
+          "IPMasquerade"
+          "IPv6PrivacyExtensions"
+          "IPv6AcceptRA"
+          "IPv6DuplicateAddressDetection"
+          "IPv6HopLimit"
+          "IPv4ProxyARP"
+          "IPv6ProxyNDP"
+          "IPv6ProxyNDPAddress"
+          "IPv6PrefixDelegation"
+          "IPv6MTUBytes"
+          "Bridge"
+          "Bond"
+          "VRF"
+          "VLAN"
+          "IPVLAN"
+          "MACVLAN"
+          "VXLAN"
+          "Tunnel"
+          "MACsec"
+          "ActiveSlave"
+          "PrimarySlave"
+          "ConfigureWithoutCarrier"
+          "IgnoreCarrierLoss"
+          "Xfrm"
+          "KeepConfiguration"
+        ])
+        # Note: For DHCP the values both, none, v4, v6 are deprecated
+        (assertValueOneOf "DHCP" ["yes" "no" "ipv4" "ipv6"])
+        (assertValueOneOf "DHCPServer" boolValues)
+        (assertValueOneOf "LinkLocalAddressing" ["yes" "no" "ipv4" "ipv6" "fallback" "ipv4-fallback"])
+        (assertValueOneOf "IPv4LLRoute" boolValues)
+        (assertValueOneOf "DefaultRouteOnDevice" boolValues)
+        (assertValueOneOf "LLMNR" (boolValues ++ ["resolve"]))
+        (assertValueOneOf "MulticastDNS" (boolValues ++ ["resolve"]))
+        (assertValueOneOf "DNSOverTLS" (boolValues ++ ["opportunistic"]))
+        (assertValueOneOf "DNSSEC" (boolValues ++ ["allow-downgrade"]))
+        (assertValueOneOf "LLDP" (boolValues ++ ["routers-only"]))
+        (assertValueOneOf "EmitLLDP" (boolValues ++ ["nearest-bridge" "non-tpmr-bridge" "customer-bridge"]))
+        (assertValueOneOf "DNSDefaultRoute" boolValues)
+        (assertValueOneOf "IPForward" (boolValues ++ ["ipv4" "ipv6"]))
+        (assertValueOneOf "IPMasquerade" boolValues)
+        (assertValueOneOf "IPv6PrivacyExtensions" (boolValues ++ ["prefer-public" "kernel"]))
+        (assertValueOneOf "IPv6AcceptRA" boolValues)
+        (assertInt "IPv6DuplicateAddressDetection")
+        (assertMinimum "IPv6DuplicateAddressDetection" 0)
+        (assertInt "IPv6HopLimit")
+        (assertMinimum "IPv6HopLimit" 0)
+        (assertValueOneOf "IPv4ProxyARP" boolValues)
+        (assertValueOneOf "IPv6ProxyNDP" boolValues)
+        (assertValueOneOf "IPv6PrefixDelegation" ["static" "dhcpv6" "yes" "false"])
+        (assertByteFormat "IPv6MTUBytes")
+        (assertValueOneOf "ActiveSlave" boolValues)
+        (assertValueOneOf "PrimarySlave" boolValues)
+        (assertValueOneOf "ConfigureWithoutCarrier" boolValues)
+        (assertValueOneOf "IgnoreCarrierLoss" boolValues)
+        (assertValueOneOf "KeepConfiguration" (boolValues ++ ["static" "dhcp-on-stop" "dhcp"]))
+      ];
 
-  checkRoute = checkUnitConfig "Route" [
-    (assertOnlyFields [
-      "Gateway" "GatewayOnLink" "Destination" "Source" "Metric"
-      "IPv6Preference" "Scope" "PreferredSource" "Table" "Protocol" "Type"
-      "InitialCongestionWindow" "InitialAdvertisedReceiveWindow" "QuickAck"
-      "MTUBytes"
-    ])
-  ];
+      sectionAddress = checkUnitConfig "Address" [
+        (assertOnlyFields [
+          "Address"
+          "Peer"
+          "Broadcast"
+          "Label"
+          "PreferredLifetime"
+          "Scope"
+          "HomeAddress"
+          "DuplicateAddressDetection"
+          "ManageTemporaryAddress"
+          "AddPrefixRoute"
+          "AutoJoin"
+        ])
+        (assertHasField "Address")
+        (assertValueOneOf "PreferredLifetime" ["forever" "infinity" "0" 0])
+        (assertValueOneOf "HomeAddress" boolValues)
+        (assertValueOneOf "DuplicateAddressDetection" ["ipv4" "ipv6" "both" "none"])
+        (assertValueOneOf "ManageTemporaryAddress" boolValues)
+        (assertValueOneOf "AddPrefixRoute" boolValues)
+        (assertValueOneOf "AutoJoin" boolValues)
+      ];
 
-  checkDhcpV4 = checkUnitConfig "DHCPv4" [
-    (assertOnlyFields [
-      "UseDNS" "RoutesToDNS" "UseNTP" "UseMTU" "Anonymize" "SendHostname" "UseHostname"
-      "Hostname" "UseDomains" "UseRoutes" "UseTimezone"
-      "ClientIdentifier" "VendorClassIdentifier" "UserClass" "MaxAttempts"
-      "DUIDType" "DUIDRawData" "IAID" "RequestBroadcast" "RouteMetric" "RouteTable"
-      "ListenPort" "SendRelease"
-    ])
-    (assertValueOneOf "UseDNS" boolValues)
-    (assertValueOneOf "RoutesToDNS" boolValues)
-    (assertValueOneOf "UseNTP" boolValues)
-    (assertValueOneOf "UseMTU" boolValues)
-    (assertValueOneOf "Anonymize" boolValues)
-    (assertValueOneOf "SendHostname" boolValues)
-    (assertValueOneOf "UseHostname" boolValues)
-    (assertValueOneOf "UseDomains" ["yes" "no" "route"])
-    (assertValueOneOf "UseRoutes" boolValues)
-    (assertValueOneOf "UseTimezone" boolValues)
-    (assertMinimum "MaxAttempts" 0)
-    (assertValueOneOf "RequestBroadcast" boolValues)
-    (assertInt "RouteTable")
-    (assertMinimum "RouteTable" 0)
-    (assertValueOneOf "SendRelease" boolValues)
-  ];
+      sectionRoutingPolicyRule = checkUnitConfig "RoutingPolicyRule" [
+        (assertOnlyFields [
+          "TypeOfService"
+          "From"
+          "To"
+          "FirewallMark"
+          "Table"
+          "Priority"
+          "IncomingInterface"
+          "OutgoingInterface"
+          "SourcePort"
+          "DestinationPort"
+          "IPProtocol"
+          "InvertRule"
+          "Family"
+          "User"
+          "SuppressPrefixLength"
+        ])
+        (assertInt "TypeOfService")
+        (assertRange "TypeOfService" 0 255)
+        (assertInt "FirewallMark")
+        (assertRange "FirewallMark" 1 4294967295)
+        (assertInt "Priority")
+        (assertPort "SourcePort")
+        (assertPort "DestinationPort")
+        (assertValueOneOf "InvertRule" boolValues)
+        (assertValueOneOf "Family" ["ipv4" "ipv6" "both"])
+        (assertInt "SuppressPrefixLength")
+        (assertRange "SuppressPrefixLength" 0 128)
+      ];
 
-  checkDhcpV6 = checkUnitConfig "DHCPv6" [
-    (assertOnlyFields [
-      "UseDns" "UseNTP" "RapidCommit" "ForceDHCPv6PDOtherInformation"
-      "PrefixDelegationHint"
-    ])
-    (assertValueOneOf "UseDNS" boolValues)
-    (assertValueOneOf "UseNTP" boolValues)
-    (assertValueOneOf "RapidCommit" boolValues)
-    (assertValueOneOf "ForceDHCPv6PDOtherInformation" boolValues)
-  ];
+      sectionRoute = checkUnitConfig "Route" [
+        (assertOnlyFields [
+          "Gateway"
+          "GatewayOnLink"
+          "Destination"
+          "Source"
+          "Metric"
+          "IPv6Preference"
+          "Scope"
+          "PreferredSource"
+          "Table"
+          "Protocol"
+          "Type"
+          "InitialCongestionWindow"
+          "InitialAdvertisedReceiveWindow"
+          "QuickAck"
+          "FastOpenNoCookie"
+          "TTLPropagate"
+          "MTUBytes"
+          "IPServiceType"
+          "MultiPathRoute"
+        ])
+        (assertValueOneOf "GatewayOnLink" boolValues)
+        (assertInt "Metric")
+        (assertValueOneOf "IPv6Preference" ["low" "medium" "high"])
+        (assertValueOneOf "Scope" ["global" "site" "link" "host" "nowhere"])
+        (assertValueOneOf "Type" [
+          "unicast"
+          "local"
+          "broadcast"
+          "anycast"
+          "multicast"
+          "blackhole"
+          "unreachable"
+          "prohibit"
+          "throw"
+          "nat"
+          "xresolve"
+        ])
+        (assertValueOneOf "QuickAck" boolValues)
+        (assertValueOneOf "FastOpenNoCookie" boolValues)
+        (assertValueOneOf "TTLPropagate" boolValues)
+        (assertByteFormat "MTUBytes")
+        (assertValueOneOf "IPServiceType" ["CS6" "CS4"])
+      ];
 
-  checkIpv6PrefixDelegation = checkUnitConfig "IPv6PrefixDelegation" [
-    (assertOnlyFields [
-      "Managed"  "OtherInformation"  "RouterLifetimeSec"
-      "RouterPreference"  "EmitDNS"  "DNS"  "EmitDomains"  "Domains"
-      "DNSLifetimeSec"
-    ])
-    (assertValueOneOf "Managed" boolValues)
-    (assertValueOneOf "OtherInformation" boolValues)
-    (assertValueOneOf "RouterPreference" ["high" "medium" "low" "normal" "default"])
-    (assertValueOneOf "EmitDNS" boolValues)
-    (assertValueOneOf "EmitDomains" boolValues)
-    (assertMinimum "DNSLifetimeSec" 0)
-  ];
+      sectionDHCPv4 = checkUnitConfig "DHCPv4" [
+        (assertOnlyFields [
+          "UseDNS"
+          "RoutesToDNS"
+          "UseNTP"
+          "UseSIP"
+          "UseMTU"
+          "Anonymize"
+          "SendHostname"
+          "UseHostname"
+          "Hostname"
+          "UseDomains"
+          "UseRoutes"
+          "UseTimezone"
+          "ClientIdentifier"
+          "VendorClassIdentifier"
+          "UserClass"
+          "MaxAttempts"
+          "DUIDType"
+          "DUIDRawData"
+          "IAID"
+          "RequestBroadcast"
+          "RouteMetric"
+          "RouteTable"
+          "RouteMTUBytes"
+          "ListenPort"
+          "SendRelease"
+          "SendDecline"
+          "BlackList"
+          "RequestOptions"
+          "SendOption"
+        ])
+        (assertValueOneOf "UseDNS" boolValues)
+        (assertValueOneOf "RoutesToDNS" boolValues)
+        (assertValueOneOf "UseNTP" boolValues)
+        (assertValueOneOf "UseSIP" boolValues)
+        (assertValueOneOf "UseMTU" boolValues)
+        (assertValueOneOf "Anonymize" boolValues)
+        (assertValueOneOf "SendHostname" boolValues)
+        (assertValueOneOf "UseHostname" boolValues)
+        (assertValueOneOf "UseDomains" (boolValues ++ ["route"]))
+        (assertValueOneOf "UseRoutes" boolValues)
+        (assertValueOneOf "UseTimezone" boolValues)
+        (assertValueOneOf "ClientIdentifier" ["mac" "duid" "duid-only"])
+        (assertInt "IAID")
+        (assertValueOneOf "RequestBroadcast" boolValues)
+        (assertInt "RouteMetric")
+        (assertInt "RouteTable")
+        (assertRange "RouteTable" 0 4294967295)
+        (assertByteFormat "RouteMTUBytes")
+        (assertPort "ListenPort")
+        (assertValueOneOf "SendRelease" boolValues)
+        (assertValueOneOf "SendDecline" boolValues)
+      ];
 
-  checkIpv6Prefix = checkUnitConfig "IPv6Prefix" [
-    (assertOnlyFields [
-      "AddressAutoconfiguration"  "OnLink"  "Prefix"
-      "PreferredLifetimeSec" "ValidLifetimeSec"
-    ])
-    (assertValueOneOf "AddressAutoconfiguration" boolValues)
-    (assertValueOneOf "OnLink" boolValues)
-    (assertMinimum "PreferredLifetimeSec" 0)
-    (assertMinimum "ValidLifetimeSec" 0)
-  ];
+      sectionDHCPv6 = checkUnitConfig "DHCPv6" [
+        (assertOnlyFields [
+          "UseDNS"
+          "UseNTP"
+          "RapidCommit"
+          "ForceDHCPv6PDOtherInformation"
+          "PrefixDelegationHint"
+        ])
+        (assertValueOneOf "UseDNS" boolValues)
+        (assertValueOneOf "UseNTP" boolValues)
+        (assertValueOneOf "RapidCommit" boolValues)
+        (assertValueOneOf "ForceDHCPv6PDOtherInformation" boolValues)
+      ];
 
+      sectionDHCPServer = checkUnitConfig "DHCPServer" [
+        (assertOnlyFields [
+          "PoolOffset"
+          "PoolSize"
+          "DefaultLeaseTimeSec"
+          "MaxLeaseTimeSec"
+          "EmitDNS"
+          "DNS"
+          "EmitNTP"
+          "NTP"
+          "EmitSIP"
+          "SIP"
+          "EmitRouter"
+          "EmitTimezone"
+          "Timezone"
+          "SendOption"
+        ])
+        (assertInt "PoolOffset")
+        (assertMinimum "PoolOffset" 0)
+        (assertInt "PoolSize")
+        (assertMinimum "PoolSize" 0)
+        (assertValueOneOf "EmitDNS" boolValues)
+        (assertValueOneOf "EmitNTP" boolValues)
+        (assertValueOneOf "EmitSIP" boolValues)
+        (assertValueOneOf "EmitRouter" boolValues)
+        (assertValueOneOf "EmitTimezone" boolValues)
+      ];
 
-  checkDhcpServer = checkUnitConfig "DHCPServer" [
-    (assertOnlyFields [
-      "PoolOffset" "PoolSize" "DefaultLeaseTimeSec" "MaxLeaseTimeSec"
-      "EmitDNS" "DNS" "EmitNTP" "NTP" "EmitRouter" "EmitTimezone" "Timezone"
-    ])
-    (assertValueOneOf "EmitDNS" boolValues)
-    (assertValueOneOf "EmitNTP" boolValues)
-    (assertValueOneOf "EmitRouter" boolValues)
-    (assertValueOneOf "EmitTimezone" boolValues)
-  ];
+      sectionIPv6PrefixDelegation = checkUnitConfig "IPv6PrefixDelegation" [
+        (assertOnlyFields [
+          "Managed"
+          "OtherInformation"
+          "RouterLifetimeSec"
+          "RouterPreference"
+          "EmitDNS"
+          "DNS"
+          "EmitDomains"
+          "Domains"
+          "DNSLifetimeSec"
+        ])
+        (assertValueOneOf "Managed" boolValues)
+        (assertValueOneOf "OtherInformation" boolValues)
+        (assertValueOneOf "RouterPreference" ["high" "medium" "low" "normal" "default"])
+        (assertValueOneOf "EmitDNS" boolValues)
+        (assertValueOneOf "EmitDomains" boolValues)
+      ];
 
-  # .network files have a [Link] section with different options than in .netlink files
-  checkNetworkLink = checkUnitConfig "Link" [
-    (assertOnlyFields [
-      "MACAddress" "MTUBytes" "ARP" "Multicast" "Unmanaged" "RequiredForOnline"
-    ])
-    (assertMacAddress "MACAddress")
-    (assertByteFormat "MTUBytes")
-    (assertValueOneOf "ARP" boolValues)
-    (assertValueOneOf "Multicast" boolValues)
-    (assertValueOneOf "Unmanaged" boolValues)
-    (assertValueOneOf "RequiredForOnline" (boolValues ++ ["off" "no-carrier" "dormant" "degraded-carrier" "carrier" "degraded" "enslaved" "routable"]))
-  ];
+      sectionIPv6Prefix = checkUnitConfig "IPv6Prefix" [
+        (assertOnlyFields [
+          "AddressAutoconfiguration"
+          "OnLink"
+          "Prefix"
+          "PreferredLifetimeSec"
+          "ValidLifetimeSec"
+        ])
+        (assertValueOneOf "AddressAutoconfiguration" boolValues)
+        (assertValueOneOf "OnLink" boolValues)
+      ];
 
+    };
+  };
 
   commonNetworkOptions = {
 
@@ -406,7 +761,7 @@ let
     linkConfig = mkOption {
       default = {};
       example = { MACAddress = "00:ff:ee:aa:cc:dd"; };
-      type = types.addCheck (types.attrsOf unitOption) checkLink;
+      type = types.addCheck (types.attrsOf unitOption) check.link.sectionLink;
       description = ''
         Each attribute in this set specifies an option in the
         <literal>[Link]</literal> section of the unit.  See
@@ -417,12 +772,28 @@ let
 
   };
 
+  wireguardPeerOptions = {
+    options = {
+      wireguardPeerConfig = mkOption {
+        default = {};
+        example = { };
+        type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionWireGuardPeer;
+        description = ''
+          Each attribute in this set specifies an option in the
+          <literal>[WireGuardPeer]</literal> section of the unit.  See
+          <citerefentry><refentrytitle>systemd.network</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry> for details.
+        '';
+      };
+    };
+  };
+
   netdevOptions = commonNetworkOptions // {
 
     netdevConfig = mkOption {
       default = {};
       example = { Name = "mybridge"; Kind = "bridge"; };
-      type = types.addCheck (types.attrsOf unitOption) checkNetdev;
+      type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionNetdev;
       description = ''
         Each attribute in this set specifies an option in the
         <literal>[Netdev]</literal> section of the unit.  See
@@ -431,65 +802,10 @@ let
       '';
     };
 
-    vrfConfig = mkOption {
-      default = {};
-      example = { Table = 2342; };
-      type = types.addCheck (types.attrsOf unitOption) checkVRF;
-      description = ''
-        Each attribute in this set specifies an option in the
-        <literal>[VRF]</literal> section of the unit. See
-        <citerefentry><refentrytitle>systemd.netdev</refentrytitle>
-        <manvolnum>5</manvolnum></citerefentry> for details.
-        A detailed explanation about how VRFs work can be found in the
-        <link xlink:href="https://www.kernel.org/doc/Documentation/networking/vrf.txt">kernel
-        docs</link>.
-      '';
-    };
-
-    wireguardConfig = mkOption {
-      default = {};
-      example = {
-        PrivateKeyFile = "/etc/wireguard/secret.key";
-        ListenPort = 51820;
-        FwMark = 42;
-      };
-      type = types.addCheck (types.attrsOf unitOption) checkWireGuard;
-      description = ''
-        Each attribute in this set specifies an option in the
-        <literal>[WireGuard]</literal> section of the unit. See
-        <citerefentry><refentrytitle>systemd.netdev</refentrytitle>
-        <manvolnum>5</manvolnum></citerefentry> for details.
-        Use <literal>PrivateKeyFile</literal> instead of
-        <literal>PrivateKey</literal>: the nix store is
-        world-readable.
-      '';
-    };
-
-    wireguardPeers = mkOption {
-      default = [];
-      example = [ { wireguardPeerConfig={
-        Endpoint = "192.168.1.1:51820";
-        PublicKey = "27s0OvaBBdHoJYkH9osZpjpgSOVNw+RaKfboT/Sfq0g=";
-        PresharedKeyFile = "/etc/wireguard/psk.key";
-        AllowedIPs = [ "10.0.0.1/32" ];
-        PersistentKeepalive = 15;
-      };}];
-      type = with types; listOf (submodule wireguardPeerOptions);
-      description = ''
-        Each item in this array specifies an option in the
-        <literal>[WireGuardPeer]</literal> section of the unit. See
-        <citerefentry><refentrytitle>systemd.netdev</refentrytitle>
-        <manvolnum>5</manvolnum></citerefentry> for details.
-        Use <literal>PresharedKeyFile</literal> instead of
-        <literal>PresharedKey</literal>: the nix store is
-        world-readable.
-      '';
-    };
-
     vlanConfig = mkOption {
       default = {};
-      example = { Id = "4"; };
-      type = types.addCheck (types.attrsOf unitOption) checkVlan;
+      example = { Id = 4; };
+      type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionVLAN;
       description = ''
         Each attribute in this set specifies an option in the
         <literal>[VLAN]</literal> section of the unit.  See
@@ -501,7 +817,7 @@ let
     macvlanConfig = mkOption {
       default = {};
       example = { Mode = "private"; };
-      type = types.addCheck (types.attrsOf unitOption) checkMacvlan;
+      type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionMACVLAN;
       description = ''
         Each attribute in this set specifies an option in the
         <literal>[MACVLAN]</literal> section of the unit.  See
@@ -513,7 +829,7 @@ let
     vxlanConfig = mkOption {
       default = {};
       example = { Id = "4"; };
-      type = types.addCheck (types.attrsOf unitOption) checkVxlan;
+      type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionVXLAN;
       description = ''
         Each attribute in this set specifies an option in the
         <literal>[VXLAN]</literal> section of the unit.  See
@@ -525,7 +841,7 @@ let
     tunnelConfig = mkOption {
       default = {};
       example = { Remote = "192.168.1.1"; };
-      type = types.addCheck (types.attrsOf unitOption) checkTunnel;
+      type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionTunnel;
       description = ''
         Each attribute in this set specifies an option in the
         <literal>[Tunnel]</literal> section of the unit.  See
@@ -537,7 +853,7 @@ let
     peerConfig = mkOption {
       default = {};
       example = { Name = "veth2"; };
-      type = types.addCheck (types.attrsOf unitOption) checkPeer;
+      type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionPeer;
       description = ''
         Each attribute in this set specifies an option in the
         <literal>[Peer]</literal> section of the unit.  See
@@ -549,7 +865,7 @@ let
     tunConfig = mkOption {
       default = {};
       example = { User = "openvpn"; };
-      type = types.addCheck (types.attrsOf unitOption) checkTun;
+      type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionTun;
       description = ''
         Each attribute in this set specifies an option in the
         <literal>[Tun]</literal> section of the unit.  See
@@ -561,7 +877,7 @@ let
     tapConfig = mkOption {
       default = {};
       example = { User = "openvpn"; };
-      type = types.addCheck (types.attrsOf unitOption) checkTap;
+      type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionTap;
       description = ''
         Each attribute in this set specifies an option in the
         <literal>[Tap]</literal> section of the unit.  See
@@ -570,10 +886,50 @@ let
       '';
     };
 
+    wireguardConfig = mkOption {
+      default = {};
+      example = {
+        PrivateKeyFile = "/etc/wireguard/secret.key";
+        ListenPort = 51820;
+        FwMark = 42;
+      };
+      type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionWireGuard;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[WireGuard]</literal> section of the unit. See
+        <citerefentry><refentrytitle>systemd.netdev</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+        Use <literal>PrivateKeyFile</literal> instead of
+        <literal>PrivateKey</literal>: the nix store is
+        world-readable.
+      '';
+    };
+
+    wireguardPeers = mkOption {
+      default = [];
+      example = [ { wireguardPeerConfig={
+        Endpoint = "192.168.1.1:51820";
+        PublicKey = "27s0OvaBBdHoJYkH9osZpjpgSOVNw+RaKfboT/Sfq0g=";
+        PresharedKeyFile = "/etc/wireguard/psk.key";
+        AllowedIPs = [ "10.0.0.1/32" ];
+        PersistentKeepalive = 15;
+      };}];
+      type = with types; listOf (submodule wireguardPeerOptions);
+      description = ''
+        Each item in this array specifies an option in the
+        <literal>[WireGuardPeer]</literal> section of the unit. See
+        <citerefentry><refentrytitle>systemd.netdev</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+        Use <literal>PresharedKeyFile</literal> instead of
+        <literal>PresharedKey</literal>: the nix store is
+        world-readable.
+      '';
+    };
+
     bondConfig = mkOption {
       default = {};
       example = { Mode = "802.3ad"; };
-      type = types.addCheck (types.attrsOf unitOption) checkBond;
+      type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionBond;
       description = ''
         Each attribute in this set specifies an option in the
         <literal>[Bond]</literal> section of the unit.  See
@@ -585,7 +941,7 @@ let
     xfrmConfig = mkOption {
       default = {};
       example = { InterfaceId = 1; };
-      type = types.addCheck (types.attrsOf unitOption) checkXfrm;
+      type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionXfrm;
       description = ''
         Each attribute in this set specifies an option in the
         <literal>[Xfrm]</literal> section of the unit.  See
@@ -594,6 +950,21 @@ let
       '';
     };
 
+    vrfConfig = mkOption {
+      default = {};
+      example = { Table = 2342; };
+      type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionVRF;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[VRF]</literal> section of the unit. See
+        <citerefentry><refentrytitle>systemd.netdev</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+        A detailed explanation about how VRFs work can be found in the
+        <link xlink:href="https://www.kernel.org/doc/Documentation/networking/vrf.txt">kernel
+        docs</link>.
+      '';
+    };
+
   };
 
   addressOptions = {
@@ -601,7 +972,7 @@ let
       addressConfig = mkOption {
         default = {};
         example = { Address = "192.168.0.100/24"; };
-        type = types.addCheck (types.attrsOf unitOption) checkAddress;
+        type = types.addCheck (types.attrsOf unitOption) check.network.sectionAddress;
         description = ''
           Each attribute in this set specifies an option in the
           <literal>[Address]</literal> section of the unit.  See
@@ -617,7 +988,7 @@ let
       routingPolicyRuleConfig = mkOption {
         default = { };
         example = { routingPolicyRuleConfig = { Table = 10; IncomingInterface = "eth1"; Family = "both"; } ;};
-        type = types.addCheck (types.attrsOf unitOption) checkRoutingPolicyRule;
+        type = types.addCheck (types.attrsOf unitOption) check.network.sectionRoutingPolicyRule;
         description = ''
           Each attribute in this set specifies an option in the
           <literal>[RoutingPolicyRule]</literal> section of the unit.  See
@@ -633,7 +1004,7 @@ let
       routeConfig = mkOption {
         default = {};
         example = { Gateway = "192.168.0.1"; };
-        type = types.addCheck (types.attrsOf unitOption) checkRoute;
+        type = types.addCheck (types.attrsOf unitOption) check.network.sectionRoute;
         description = ''
           Each attribute in this set specifies an option in the
           <literal>[Route]</literal> section of the unit.  See
@@ -644,28 +1015,12 @@ let
     };
   };
 
-  wireguardPeerOptions = {
-    options = {
-      wireguardPeerConfig = mkOption {
-        default = {};
-        example = { };
-        type = types.addCheck (types.attrsOf unitOption) checkWireGuardPeer;
-        description = ''
-          Each attribute in this set specifies an option in the
-          <literal>[WireGuardPeer]</literal> section of the unit.  See
-          <citerefentry><refentrytitle>systemd.network</refentrytitle>
-          <manvolnum>5</manvolnum></citerefentry> for details.
-        '';
-      };
-    };
-  };
-
   ipv6PrefixOptions = {
     options = {
       ipv6PrefixConfig = mkOption {
         default = {};
         example = { Prefix = "fd00::/64"; };
-        type = types.addCheck (types.attrsOf unitOption) checkIpv6Prefix;
+        type = types.addCheck (types.attrsOf unitOption) check.network.sectionIPv6Prefix;
         description = ''
           Each attribute in this set specifies an option in the
           <literal>[IPv6Prefix]</literal> section of the unit.  See
@@ -676,13 +1031,24 @@ let
     };
   };
 
-
   networkOptions = commonNetworkOptions // {
 
+    linkConfig = mkOption {
+      default = {};
+      example = { Unmanaged = true; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionLink;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[Link]</literal> section of the unit.  See
+        <citerefentry><refentrytitle>systemd.network</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
     networkConfig = mkOption {
       default = {};
       example = { Description = "My Network"; };
-      type = types.addCheck (types.attrsOf unitOption) checkNetwork;
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionNetwork;
       description = ''
         Each attribute in this set specifies an option in the
         <literal>[Network]</literal> section of the unit.  See
@@ -701,7 +1067,7 @@ let
     dhcpV4Config = mkOption {
       default = {};
       example = { UseDNS = true; UseRoutes = true; };
-      type = types.addCheck (types.attrsOf unitOption) checkDhcpV4;
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionDHCPv4;
       description = ''
         Each attribute in this set specifies an option in the
         <literal>[DHCPv4]</literal> section of the unit.  See
@@ -713,7 +1079,7 @@ let
     dhcpV6Config = mkOption {
       default = {};
       example = { UseDNS = true; UseRoutes = true; };
-      type = types.addCheck (types.attrsOf unitOption) checkDhcpV6;
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionDHCPv6;
       description = ''
         Each attribute in this set specifies an option in the
         <literal>[DHCPv6]</literal> section of the unit.  See
@@ -722,48 +1088,36 @@ let
       '';
     };
 
-    ipv6PrefixDelegationConfig = mkOption {
+    dhcpServerConfig = mkOption {
       default = {};
-      example = { EmitDNS = true; Managed = true; OtherInformation = true; };
-      type = types.addCheck (types.attrsOf unitOption) checkIpv6PrefixDelegation;
+      example = { PoolOffset = 50; EmitDNS = false; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionDHCPServer;
       description = ''
         Each attribute in this set specifies an option in the
-        <literal>[IPv6PrefixDelegation]</literal> section of the unit.  See
-        <citerefentry><refentrytitle>systemd.network</refentrytitle>
-        <manvolnum>5</manvolnum></citerefentry> for details.
-      '';
-    };
-
-    ipv6Prefixes = mkOption {
-      default = [];
-      example = { AddressAutoconfiguration = true; OnLink = true; };
-      type = with types; listOf (submodule ipv6PrefixOptions);
-      description = ''
-        A list of ipv6Prefix sections to be added to the unit.  See
+        <literal>[DHCPServer]</literal> section of the unit.  See
         <citerefentry><refentrytitle>systemd.network</refentrytitle>
         <manvolnum>5</manvolnum></citerefentry> for details.
       '';
     };
 
-    dhcpServerConfig = mkOption {
+    ipv6PrefixDelegationConfig = mkOption {
       default = {};
-      example = { PoolOffset = 50; EmitDNS = false; };
-      type = types.addCheck (types.attrsOf unitOption) checkDhcpServer;
+      example = { EmitDNS = true; Managed = true; OtherInformation = true; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionIPv6PrefixDelegation;
       description = ''
         Each attribute in this set specifies an option in the
-        <literal>[DHCPServer]</literal> section of the unit.  See
+        <literal>[IPv6PrefixDelegation]</literal> section of the unit.  See
         <citerefentry><refentrytitle>systemd.network</refentrytitle>
         <manvolnum>5</manvolnum></citerefentry> for details.
       '';
     };
 
-    linkConfig = mkOption {
-      default = {};
-      example = { Unmanaged = true; };
-      type = types.addCheck (types.attrsOf unitOption) checkNetworkLink;
+    ipv6Prefixes = mkOption {
+      default = [];
+      example = { AddressAutoconfiguration = true; OnLink = true; };
+      type = with types; listOf (submodule ipv6PrefixOptions);
       description = ''
-        Each attribute in this set specifies an option in the
-        <literal>[Link]</literal> section of the unit.  See
+        A list of ipv6Prefix sections to be added to the unit.  See
         <citerefentry><refentrytitle>systemd.network</refentrytitle>
         <manvolnum>5</manvolnum></citerefentry> for details.
       '';
@@ -958,160 +1312,162 @@ let
     };
   };
 
-  commonMatchText = def: optionalString (def.matchConfig != {}) ''
+  commonMatchText = def: optionalString (def.matchConfig != { }) ''
     [Match]
     ${attrsToSection def.matchConfig}
   '';
 
   linkToUnit = name: def:
     { inherit (def) enable;
-      text = commonMatchText def +
-        ''
+      text = commonMatchText def
+        + ''
           [Link]
           ${attrsToSection def.linkConfig}
-
-          ${def.extraConfig}
-        '';
+        ''
+        + def.extraConfig;
     };
 
   netdevToUnit = name: def:
     { inherit (def) enable;
-      text = commonMatchText def +
-        ''
+      text = commonMatchText def
+        + ''
           [NetDev]
           ${attrsToSection def.netdevConfig}
-
-          ${optionalString (def.vlanConfig != { }) ''
-            [VLAN]
-            ${attrsToSection def.vlanConfig}
-
-          ''}
-          ${optionalString (def.macvlanConfig != { }) ''
-            [MACVLAN]
-            ${attrsToSection def.macvlanConfig}
-
-          ''}
-          ${optionalString (def.vxlanConfig != { }) ''
-            [VXLAN]
-            ${attrsToSection def.vxlanConfig}
-
-          ''}
-          ${optionalString (def.tunnelConfig != { }) ''
-            [Tunnel]
-            ${attrsToSection def.tunnelConfig}
-
-          ''}
-          ${optionalString (def.peerConfig != { }) ''
-            [Peer]
-            ${attrsToSection def.peerConfig}
-
-          ''}
-          ${optionalString (def.tunConfig != { }) ''
-            [Tun]
-            ${attrsToSection def.tunConfig}
-
-          ''}
-          ${optionalString (def.tapConfig != { }) ''
-            [Tap]
-            ${attrsToSection def.tapConfig}
-
-          ''}
-          ${optionalString (def.bondConfig != { }) ''
-            [Bond]
-            ${attrsToSection def.bondConfig}
-
-          ''}
-          ${optionalString (def.xfrmConfig != { }) ''
-            [Xfrm]
-            ${attrsToSection def.xfrmConfig}
-
-          ''}
-          ${optionalString (def.vrfConfig != { }) ''
-            [VRF]
-            ${attrsToSection def.vrfConfig}
-
-          ''}
-          ${optionalString (def.wireguardConfig != { }) ''
-            [WireGuard]
-            ${attrsToSection def.wireguardConfig}
-
-          ''}
-          ${flip concatMapStrings def.wireguardPeers (x: ''
-            [WireGuardPeer]
-            ${attrsToSection x.wireguardPeerConfig}
-
-          '')}
-          ${def.extraConfig}
-        '';
+        ''
+        + optionalString (def.vlanConfig != { }) ''
+          [VLAN]
+          ${attrsToSection def.vlanConfig}
+        ''
+        + optionalString (def.macvlanConfig != { }) ''
+          [MACVLAN]
+          ${attrsToSection def.macvlanConfig}
+        ''
+        + optionalString (def.vxlanConfig != { }) ''
+          [VXLAN]
+          ${attrsToSection def.vxlanConfig}
+        ''
+        + optionalString (def.tunnelConfig != { }) ''
+          [Tunnel]
+          ${attrsToSection def.tunnelConfig}
+        ''
+        + optionalString (def.peerConfig != { }) ''
+          [Peer]
+          ${attrsToSection def.peerConfig}
+        ''
+        + optionalString (def.tunConfig != { }) ''
+          [Tun]
+          ${attrsToSection def.tunConfig}
+        ''
+        + optionalString (def.tapConfig != { }) ''
+          [Tap]
+          ${attrsToSection def.tapConfig}
+        ''
+        + optionalString (def.wireguardConfig != { }) ''
+          [WireGuard]
+          ${attrsToSection def.wireguardConfig}
+        ''
+        + flip concatMapStrings def.wireguardPeers (x: ''
+          [WireGuardPeer]
+          ${attrsToSection x.wireguardPeerConfig}
+        '')
+        + optionalString (def.bondConfig != { }) ''
+          [Bond]
+          ${attrsToSection def.bondConfig}
+        ''
+        + optionalString (def.xfrmConfig != { }) ''
+          [Xfrm]
+          ${attrsToSection def.xfrmConfig}
+        ''
+        + optionalString (def.vrfConfig != { }) ''
+          [VRF]
+          ${attrsToSection def.vrfConfig}
+        ''
+        + def.extraConfig;
     };
 
   networkToUnit = name: def:
     { inherit (def) enable;
-      text = commonMatchText def +
+      text = commonMatchText def
+        + optionalString (def.linkConfig != { }) ''
+          [Link]
+          ${attrsToSection def.linkConfig}
         ''
-          ${optionalString (def.linkConfig != { }) ''
-            [Link]
-            ${attrsToSection def.linkConfig}
-
-          ''}
-
+        + ''
           [Network]
-          ${attrsToSection def.networkConfig}
+        ''
+        + attrsToSection def.networkConfig
+        + optionalString (def.address != [ ]) ''
           ${concatStringsSep "\n" (map (s: "Address=${s}") def.address)}
+        ''
+        + optionalString (def.gateway != [ ]) ''
           ${concatStringsSep "\n" (map (s: "Gateway=${s}") def.gateway)}
+        ''
+        + optionalString (def.dns != [ ]) ''
           ${concatStringsSep "\n" (map (s: "DNS=${s}") def.dns)}
+        ''
+        + optionalString (def.ntp != [ ]) ''
           ${concatStringsSep "\n" (map (s: "NTP=${s}") def.ntp)}
+        ''
+        + optionalString (def.bridge != [ ]) ''
           ${concatStringsSep "\n" (map (s: "Bridge=${s}") def.bridge)}
+        ''
+        + optionalString (def.bond != [ ]) ''
           ${concatStringsSep "\n" (map (s: "Bond=${s}") def.bond)}
+        ''
+        + optionalString (def.vrf != [ ]) ''
           ${concatStringsSep "\n" (map (s: "VRF=${s}") def.vrf)}
+        ''
+        + optionalString (def.vlan != [ ]) ''
           ${concatStringsSep "\n" (map (s: "VLAN=${s}") def.vlan)}
+        ''
+        + optionalString (def.macvlan != [ ]) ''
           ${concatStringsSep "\n" (map (s: "MACVLAN=${s}") def.macvlan)}
+        ''
+        + optionalString (def.vxlan != [ ]) ''
           ${concatStringsSep "\n" (map (s: "VXLAN=${s}") def.vxlan)}
+        ''
+        + optionalString (def.tunnel != [ ]) ''
           ${concatStringsSep "\n" (map (s: "Tunnel=${s}") def.tunnel)}
+        ''
+        + optionalString (def.xfrm != [ ]) ''
           ${concatStringsSep "\n" (map (s: "Xfrm=${s}") def.xfrm)}
+        ''
+        + ''
 
-          ${optionalString (def.dhcpV4Config != { }) ''
-            [DHCPv4]
-            ${attrsToSection def.dhcpV4Config}
-
-          ''}
-          ${optionalString (def.dhcpV6Config != {}) ''
-            [DHCPv6]
-            ${attrsToSection def.dhcpV6Config}
-
-          ''}
-          ${optionalString (def.ipv6PrefixDelegationConfig != {}) ''
-            [IPv6PrefixDelegation]
-            ${attrsToSection def.ipv6PrefixDelegationConfig}
-
-          ''}
-          ${flip concatMapStrings def.ipv6Prefixes (x: ''
-            [IPv6Prefix]
-            ${attrsToSection x.ipv6PrefixConfig}
-
-          '')}
-          ${optionalString (def.dhcpServerConfig != { }) ''
-            [DHCPServer]
-            ${attrsToSection def.dhcpServerConfig}
-
-          ''}
-          ${flip concatMapStrings def.addresses (x: ''
-            [Address]
-            ${attrsToSection x.addressConfig}
-
-          '')}
-          ${flip concatMapStrings def.routes (x: ''
-            [Route]
-            ${attrsToSection x.routeConfig}
-
-          '')}
-          ${flip concatMapStrings def.routingPolicyRules (x: ''
-            [RoutingPolicyRule]
-            ${attrsToSection x.routingPolicyRuleConfig}
-
-          '')}
-          ${def.extraConfig}
-        '';
+        ''
+        + flip concatMapStrings def.addresses (x: ''
+          [Address]
+          ${attrsToSection x.addressConfig}
+        '')
+        + flip concatMapStrings def.routingPolicyRules (x: ''
+          [RoutingPolicyRule]
+          ${attrsToSection x.routingPolicyRuleConfig}
+        '')
+        + flip concatMapStrings def.routes (x: ''
+          [Route]
+          ${attrsToSection x.routeConfig}
+        '')
+        + optionalString (def.dhcpV4Config != { }) ''
+          [DHCPv4]
+          ${attrsToSection def.dhcpV4Config}
+        ''
+        + optionalString (def.dhcpV6Config != { }) ''
+          [DHCPv6]
+          ${attrsToSection def.dhcpV6Config}
+        ''
+        + optionalString (def.dhcpServerConfig != { }) ''
+          [DHCPServer]
+          ${attrsToSection def.dhcpServerConfig}
+        ''
+        + optionalString (def.ipv6PrefixDelegationConfig != { }) ''
+          [IPv6PrefixDelegation]
+          ${attrsToSection def.ipv6PrefixDelegationConfig}
+        ''
+        + flip concatMapStrings def.ipv6Prefixes (x: ''
+          [IPv6Prefix]
+          ${attrsToSection x.ipv6PrefixConfig}
+        '')
+        + def.extraConfig;
     };
 
   unitFiles = listToAttrs (map (name: {
@@ -1178,14 +1534,22 @@ in
       users.users.systemd-network.group = "systemd-network";
 
       systemd.additionalUpstreamSystemUnits = [
-        "systemd-networkd.service" "systemd-networkd-wait-online.service"
+        "systemd-networkd-wait-online.service"
+        "systemd-networkd.service"
+        "systemd-networkd.socket"
       ];
 
       systemd.network.units = mapAttrs' (n: v: nameValuePair "${n}.netdev" (netdevToUnit n v)) cfg.netdevs
         // mapAttrs' (n: v: nameValuePair "${n}.network" (networkToUnit n v)) cfg.networks;
 
+      # systemd-networkd is socket-activated by kernel netlink route change
+      # messages. It is important to have systemd buffer those on behalf of
+      # networkd.
+      systemd.sockets.systemd-networkd.wantedBy = [ "sockets.target" ];
+
       systemd.services.systemd-networkd = {
         wantedBy = [ "multi-user.target" ];
+        aliases = [ "dbus-org.freedesktop.network1.service" ];
         restartTriggers = map (x: x.source) (attrValues unitFiles);
         # prevent race condition with interface renaming (#39069)
         requires = [ "systemd-udev-settle.service" ];
diff --git a/nixos/modules/system/boot/plymouth.nix b/nixos/modules/system/boot/plymouth.nix
index 23fce22366d..55e5b07ed61 100644
--- a/nixos/modules/system/boot/plymouth.nix
+++ b/nixos/modules/system/boot/plymouth.nix
@@ -102,6 +102,8 @@ in
     systemd.services.plymouth-poweroff.wantedBy = [ "poweroff.target" ];
     systemd.services.plymouth-reboot.wantedBy = [ "reboot.target" ];
     systemd.services.plymouth-read-write.wantedBy = [ "sysinit.target" ];
+    systemd.services.systemd-ask-password-plymouth.wantedBy = ["multi-user.target"];
+    systemd.paths.systemd-ask-password-plymouth.wantedBy = ["multi-user.target"];
 
     boot.initrd.extraUtilsCommands = ''
       copy_bin_and_libs ${pkgs.plymouth}/bin/plymouthd
@@ -146,6 +148,7 @@ in
     # We use `mkAfter` to ensure that LUKS password prompt would be shown earlier than the splash screen.
     boot.initrd.preLVMCommands = mkAfter ''
       mkdir -p /etc/plymouth
+      mkdir -p /run/plymouth
       ln -s ${configFile} /etc/plymouth/plymouthd.conf
       ln -s $extraUtils/share/plymouth/plymouthd.defaults /etc/plymouth/plymouthd.defaults
       ln -s $extraUtils/share/plymouth/logo.png /etc/plymouth/logo.png
diff --git a/nixos/modules/system/boot/resolved.nix b/nixos/modules/system/boot/resolved.nix
index b7aaef575ac..b024f9cf5ee 100644
--- a/nixos/modules/system/boot/resolved.nix
+++ b/nixos/modules/system/boot/resolved.nix
@@ -148,6 +148,7 @@ in
 
     systemd.services.systemd-resolved = {
       wantedBy = [ "multi-user.target" ];
+      aliases = [ "dbus-org.freedesktop.resolve1.service" ];
       restartTriggers = [ config.environment.etc."systemd/resolved.conf".source ];
     };
 
diff --git a/nixos/modules/system/boot/stage-1-init.sh b/nixos/modules/system/boot/stage-1-init.sh
index 607aec87f01..0c1be71cf53 100644
--- a/nixos/modules/system/boot/stage-1-init.sh
+++ b/nixos/modules/system/boot/stage-1-init.sh
@@ -144,6 +144,14 @@ for o in $(cat /proc/cmdline); do
             set -- $(IFS==; echo $o)
             stage2Init=$2
             ;;
+        boot.persistence=*)
+            set -- $(IFS==; echo $o)
+            persistence=$2
+            ;;
+        boot.persistence.opt=*)
+            set -- $(IFS==; echo $o)
+            persistence_opt=$2
+            ;;
         boot.trace|debugtrace)
             # Show each command.
             set -x
@@ -370,12 +378,14 @@ mountFS() {
 
     mkdir -p "/mnt-root$mountPoint"
 
-    # For CIFS mounts, retry a few times before giving up.
+    # For ZFS and CIFS mounts, retry a few times before giving up.
+    # We do this for ZFS as a workaround for issue NixOS/nixpkgs#25383.
     local n=0
     while true; do
         mount "/mnt-root$mountPoint" && break
-        if [ "$fsType" != cifs -o "$n" -ge 10 ]; then fail; break; fi
+        if [ \( "$fsType" != cifs -a "$fsType" != zfs \) -o "$n" -ge 10 ]; then fail; break; fi
         echo "retrying..."
+        sleep 1
         n=$((n + 1))
     done
 
@@ -534,6 +544,14 @@ while read -u 3 mountPoint; do
       continue
     fi
 
+    if [ "$mountPoint" = / ] && [ "$device" = tmpfs ] && [ ! -z "$persistence" ]; then
+        echo persistence...
+        waitDevice "$persistence"
+        echo enabling persistence...
+        mountFS "$persistence" "$mountPoint" "$persistence_opt" "auto"
+        continue
+    fi
+
     mountFS "$device" "$mountPoint" "$options" "$fsType"
 done
 
diff --git a/nixos/modules/system/boot/stage-1.nix b/nixos/modules/system/boot/stage-1.nix
index dfd158e2d75..eee510d2c95 100644
--- a/nixos/modules/system/boot/stage-1.nix
+++ b/nixos/modules/system/boot/stage-1.nix
@@ -36,7 +36,7 @@ let
     set -euo pipefail
 
     declare -A seen
-    declare -a left
+    left=()
 
     patchelf="${pkgs.buildPackages.patchelf}/bin/patchelf"
 
@@ -48,7 +48,7 @@ let
       done
     }
 
-    add_needed $1
+    add_needed "$1"
 
     while [ ''${#left[@]} -ne 0 ]; do
       next=''${left[0]}
@@ -111,8 +111,8 @@ let
       copy_bin_and_libs ${pkgs.utillinux}/sbin/blkid
 
       # Copy dmsetup and lvm.
-      copy_bin_and_libs ${pkgs.lvm2}/sbin/dmsetup
-      copy_bin_and_libs ${pkgs.lvm2}/sbin/lvm
+      copy_bin_and_libs ${getBin pkgs.lvm2}/bin/dmsetup
+      copy_bin_and_libs ${getBin pkgs.lvm2}/bin/lvm
 
       # Add RAID mdadm tool.
       copy_bin_and_libs ${pkgs.mdadm}/sbin/mdadm
@@ -235,7 +235,7 @@ let
             --replace cdrom_id ${extraUtils}/bin/cdrom_id \
             --replace ${pkgs.coreutils}/bin/basename ${extraUtils}/bin/basename \
             --replace ${pkgs.utillinux}/bin/blkid ${extraUtils}/bin/blkid \
-            --replace ${pkgs.lvm2}/sbin ${extraUtils}/bin \
+            --replace ${getBin pkgs.lvm2}/bin ${extraUtils}/bin \
             --replace ${pkgs.mdadm}/sbin ${extraUtils}/sbin \
             --replace ${pkgs.bash}/bin/sh ${extraUtils}/bin/sh \
             --replace ${udev} ${extraUtils}
@@ -374,7 +374,8 @@ let
           ) config.boot.initrd.secrets)
          }
 
-        (cd "$tmp" && find . | cpio -H newc -o) | gzip >>"$1"
+        (cd "$tmp" && find . -print0 | sort -z | cpio -o -H newc -R +0:+0 --reproducible --null) | \
+          ${config.boot.initrd.compressor} >> "$1"
       '';
 
 in
@@ -517,8 +518,7 @@ in
     };
 
     boot.initrd.secrets = mkOption
-      { internal = true;
-        default = {};
+      { default = {};
         type = types.attrsOf (types.nullOr types.path);
         description =
           ''
@@ -560,10 +560,12 @@ in
           default = false;
           type = types.bool;
           description = ''
-            If set, this file system will be mounted in the initial
-            ramdisk.  By default, this applies to the root file system
-            and to the file system containing
-            <filename>/nix/store</filename>.
+            If set, this file system will be mounted in the initial ramdisk.
+            Note that the file system will always be mounted in the initial
+            ramdisk if its mount point is one of the following:
+            ${concatStringsSep ", " (
+              forEach utils.pathsNeededForBoot (i: "<filename>${i}</filename>")
+            )}.
           '';
         };
       });
@@ -585,7 +587,7 @@ in
       { assertion = !config.boot.loader.supportsInitrdSecrets ->
           all (source:
             builtins.isPath source ||
-            (builtins.isString source && hasPrefix source builtins.storeDir))
+            (builtins.isString source && hasPrefix builtins.storeDir source))
           (attrValues config.boot.initrd.secrets);
         message = ''
           boot.loader.initrd.secrets values must be unquoted paths when
diff --git a/nixos/modules/system/boot/stage-2-init.sh b/nixos/modules/system/boot/stage-2-init.sh
index d1de7920df9..936077b9df1 100644
--- a/nixos/modules/system/boot/stage-2-init.sh
+++ b/nixos/modules/system/boot/stage-2-init.sh
@@ -169,4 +169,4 @@ exec {logOutFd}>&- {logErrFd}>&-
 echo "starting systemd..."
 PATH=/run/current-system/systemd/lib/systemd:@fsPackagesPath@ \
     LOCALE_ARCHIVE=/run/current-system/sw/lib/locale/locale-archive \
-    exec systemd
+    exec @systemdExecutable@
diff --git a/nixos/modules/system/boot/stage-2.nix b/nixos/modules/system/boot/stage-2.nix
index 6b0b4722730..dd6d83ee009 100644
--- a/nixos/modules/system/boot/stage-2.nix
+++ b/nixos/modules/system/boot/stage-2.nix
@@ -10,6 +10,7 @@ let
     src = ./stage-2-init.sh;
     shellDebug = "${pkgs.bashInteractive}/bin/bash";
     shell = "${pkgs.bash}/bin/bash";
+    inherit (config.boot) systemdExecutable;
     isExecutable = true;
     inherit (config.nix) readOnlyStore;
     inherit useHostResolvConf;
@@ -72,6 +73,15 @@ in
         '';
       };
 
+      systemdExecutable = mkOption {
+        default = "systemd";
+        type = types.str;
+        description = ''
+          The program to execute to start systemd. Typically
+          <literal>systemd</literal>, which will find systemd in the
+          PATH.
+        '';
+      };
     };
 
   };
diff --git a/nixos/modules/system/boot/systemd-nspawn.nix b/nixos/modules/system/boot/systemd-nspawn.nix
index 06ea5ee49f7..b450d77429b 100644
--- a/nixos/modules/system/boot/systemd-nspawn.nix
+++ b/nixos/modules/system/boot/systemd-nspawn.nix
@@ -113,9 +113,9 @@ in {
   config =
     let
       units = mapAttrs' (n: v: let nspawnFile = "${n}.nspawn"; in nameValuePair nspawnFile (instanceToUnit nspawnFile v)) cfg;
-    in 
+    in
       mkMerge [
-        (mkIf (cfg != {}) { 
+        (mkIf (cfg != {}) {
           environment.etc."systemd/nspawn".source = mkIf (cfg != {}) (generateUnits' false "nspawn" units [] []);
         })
         {
@@ -123,7 +123,7 @@ in {
 
           # Workaround for https://github.com/NixOS/nixpkgs/pull/67232#issuecomment-531315437 and https://github.com/systemd/systemd/issues/13622
           # Once systemd fixes this upstream, we can re-enable -U
-          systemd.services."systemd-nspawn@".serviceConfig.ExecStart = [ 
+          systemd.services."systemd-nspawn@".serviceConfig.ExecStart = [
             ""  # deliberately empty. signals systemd to override the ExecStart
             # Only difference between upstream is that we do not pass the -U flag
             "${config.systemd.package}/bin/systemd-nspawn --quiet --keep-unit --boot --link-journal=try-guest --network-veth --settings=override --machine=%i"
diff --git a/nixos/modules/system/boot/systemd-unit-options.nix b/nixos/modules/system/boot/systemd-unit-options.nix
index bee21f1a8f3..ac6fed440a2 100644
--- a/nixos/modules/system/boot/systemd-unit-options.nix
+++ b/nixos/modules/system/boot/systemd-unit-options.nix
@@ -233,6 +233,7 @@ in rec {
 
     path = mkOption {
       default = [];
+      type = with types; listOf (oneOf [ package str ]);
       apply = ps: "${makeBinPath ps}:${makeSearchPathOutput "bin" "sbin" ps}";
       description = ''
         Packages added to the service's <envar>PATH</envar>
@@ -378,6 +379,16 @@ in rec {
       '';
     };
 
+    listenDatagrams = mkOption {
+      default = [];
+      type = types.listOf types.str;
+      example = [ "0.0.0.0:993" "/run/my-socket" ];
+      description = ''
+        For each item in this list, a <literal>ListenDatagram</literal>
+        option in the <literal>[Socket]</literal> section will be created.
+      '';
+    };
+
     socketConfig = mkOption {
       default = {};
       example = { ListenStream = "/run/my-socket"; };
diff --git a/nixos/modules/system/boot/systemd.nix b/nixos/modules/system/boot/systemd.nix
index 36a25c4e6c3..b215392f250 100644
--- a/nixos/modules/system/boot/systemd.nix
+++ b/nixos/modules/system/boot/systemd.nix
@@ -25,7 +25,7 @@ let
       "nss-lookup.target"
       "nss-user-lookup.target"
       "time-sync.target"
-      #"cryptsetup.target"
+      "cryptsetup.target"
       "sigpwr.target"
       "timers.target"
       "paths.target"
@@ -73,7 +73,7 @@ let
       "systemd-journald.service"
       "systemd-journal-flush.service"
       "systemd-journal-catalog-update.service"
-      "systemd-journald-audit.socket"
+      ] ++ (optional (!config.boot.isContainer) "systemd-journald-audit.socket") ++ [
       "systemd-journald-dev-log.socket"
       "syslog.socket"
 
@@ -81,10 +81,6 @@ let
       "systemd-coredump.socket"
       "systemd-coredump@.service"
 
-      # SysV init compatibility.
-      "systemd-initctl.socket"
-      "systemd-initctl.service"
-
       # Kernel module loading.
       "systemd-modules-load.service"
       "kmod-static-nodes.service"
@@ -101,7 +97,7 @@ let
       "dev-hugepages.mount"
       "dev-mqueue.mount"
       "sys-fs-fuse-connections.mount"
-      "sys-kernel-config.mount"
+      ] ++ (optional (!config.boot.isContainer) "sys-kernel-config.mount") ++ [
       "sys-kernel-debug.mount"
 
       # Maintaining state across reboots.
@@ -164,7 +160,6 @@ let
       "systemd-timedated.service"
       "systemd-localed.service"
       "systemd-hostnamed.service"
-      "systemd-binfmt.service"
       "systemd-exit.service"
       "systemd-update-done.service"
     ] ++ optionals config.services.journald.enableHttpGateway [
@@ -355,6 +350,7 @@ let
           [Socket]
           ${attrsToSection def.socketConfig}
           ${concatStringsSep "\n" (map (s: "ListenStream=${s}") def.listenStreams)}
+          ${concatStringsSep "\n" (map (s: "ListenDatagram=${s}") def.listenDatagrams)}
         '';
     };
 
@@ -750,6 +746,25 @@ in
       '';
     };
 
+    systemd.tmpfiles.packages = mkOption {
+      type = types.listOf types.package;
+      default = [];
+      example = literalExample "[ pkgs.lvm2 ]";
+      apply = map getLib;
+      description = ''
+        List of packages containing <command>systemd-tmpfiles</command> rules.
+
+        All files ending in .conf found in
+        <filename><replaceable>pkg</replaceable>/lib/tmpfiles.d</filename>
+        will be included.
+        If this folder does not exist or does not contain any files an error will be returned instead.
+
+        If a <filename>lib</filename> output is available, rules are searched there and only there.
+        If there is no <filename>lib</filename> output it will fall back to <filename>out</filename>
+        and if that does not exist either, the default output will be used.
+      '';
+    };
+
     systemd.user.units = mkOption {
       description = "Definition of systemd per-user units.";
       default = {};
@@ -819,6 +834,49 @@ in
       '';
     };
 
+    systemd.watchdog.device = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = "/dev/watchdog";
+      description = ''
+        The path to a hardware watchdog device which will be managed by systemd.
+        If not specified, systemd will default to /dev/watchdog.
+      '';
+    };
+
+    systemd.watchdog.runtimeTime = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "30s";
+      description = ''
+        The amount of time which can elapse before a watchdog hardware device
+        will automatically reboot the system. Valid time units include "ms",
+        "s", "min", "h", "d", and "w".
+      '';
+    };
+
+    systemd.watchdog.rebootTime = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "10m";
+      description = ''
+        The amount of time which can elapse after a reboot has been triggered
+        before a watchdog hardware device will automatically reboot the system.
+        Valid time units include "ms", "s", "min", "h", "d", and "w".
+      '';
+    };
+
+    systemd.watchdog.kexecTime = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "10m";
+      description = ''
+        The amount of time which can elapse when kexec is being executed before
+        a watchdog hardware device will automatically reboot the system. This
+        option should only be enabled if reloadTime is also enabled. Valid
+        time units include "ms", "s", "min", "h", "d", and "w".
+      '';
+    };
   };
 
 
@@ -827,21 +885,18 @@ in
   config = {
 
     warnings = concatLists (mapAttrsToList (name: service:
-      optional (service.serviceConfig.Type or "" == "oneshot" && service.serviceConfig.Restart or "no" != "no")
-        "Service ‘${name}.service’ with ‘Type=oneshot’ must have ‘Restart=no’") cfg.services);
+      let
+        type = service.serviceConfig.Type or "";
+        restart = service.serviceConfig.Restart or "no";
+      in optional
+      (type == "oneshot" && (restart == "always" || restart == "on-success"))
+      "Service '${name}.service' with 'Type=oneshot' cannot have 'Restart=always' or 'Restart=on-success'")
+      cfg.services);
 
     system.build.units = cfg.units;
 
-    # Systemd provides various NSS modules to look up dynamic users, locally
-    # configured IP adresses and local container hostnames.
-    # On NixOS, these can only be passed to the NSS system via nscd (and its
-    # LD_LIBRARY_PATH), which is why it's usually a very good idea to have nscd
-    # enabled (also see the config.nscd.enable description).
-    # While there is already an assertion in place complaining loudly about
-    # having nssModules configured and nscd disabled, for some reason we still
-    # check for nscd being enabled before adding to nssModules.
-    system.nssModules = optional config.services.nscd.enable systemd.out;
-    system.nssDatabases = mkIf config.services.nscd.enable {
+    system.nssModules = [ systemd.out ];
+    system.nssDatabases = {
       hosts = (mkMerge [
         [ "mymachines" ]
         (mkOrder 1600 [ "myhostname" ] # 1600 to ensure it's always the last
@@ -851,6 +906,10 @@ in
         [ "mymachines" ]
         (mkAfter [ "systemd" ])
       ]);
+      group = (mkMerge [
+        [ "mymachines" ]
+        (mkAfter [ "systemd" ])
+      ]);
     };
 
     environment.systemPackages = [ systemd ];
@@ -889,6 +948,19 @@ in
           DefaultIPAccounting=yes
         ''}
         DefaultLimitCORE=infinity
+        ${optionalString (config.systemd.watchdog.device != null) ''
+          WatchdogDevice=${config.systemd.watchdog.device}
+        ''}
+        ${optionalString (config.systemd.watchdog.runtimeTime != null) ''
+          RuntimeWatchdogSec=${config.systemd.watchdog.runtimeTime}
+        ''}
+        ${optionalString (config.systemd.watchdog.rebootTime != null) ''
+          RebootWatchdogSec=${config.systemd.watchdog.rebootTime}
+        ''}
+        ${optionalString (config.systemd.watchdog.kexecTime != null) ''
+          KExecWatchdogSec=${config.systemd.watchdog.kexecTime}
+        ''}
+
         ${config.systemd.extraConfig}
       '';
 
@@ -936,24 +1008,18 @@ in
       "sysctl.d/50-coredump.conf".source = "${systemd}/example/sysctl.d/50-coredump.conf";
       "sysctl.d/50-default.conf".source = "${systemd}/example/sysctl.d/50-default.conf";
 
-      "tmpfiles.d/00-nixos.conf".text = ''
-        # This file is created automatically and should not be modified.
-        # Please change the option ‘systemd.tmpfiles.rules’ instead.
-
-        ${concatStringsSep "\n" cfg.tmpfiles.rules}
-      '';
-
-      "tmpfiles.d/home.conf".source = "${systemd}/example/tmpfiles.d/home.conf";
-      "tmpfiles.d/journal-nocow.conf".source = "${systemd}/example/tmpfiles.d/journal-nocow.conf";
-      "tmpfiles.d/portables.conf".source = "${systemd}/example/tmpfiles.d/portables.conf";
-      "tmpfiles.d/static-nodes-permissions.conf".source = "${systemd}/example/tmpfiles.d/static-nodes-permissions.conf";
-      "tmpfiles.d/systemd.conf".source = "${systemd}/example/tmpfiles.d/systemd.conf";
-      "tmpfiles.d/systemd-nologin.conf".source = "${systemd}/example/tmpfiles.d/systemd-nologin.conf";
-      "tmpfiles.d/systemd-nspawn.conf".source = "${systemd}/example/tmpfiles.d/systemd-nspawn.conf";
-      "tmpfiles.d/systemd-tmp.conf".source = "${systemd}/example/tmpfiles.d/systemd-tmp.conf";
-      "tmpfiles.d/tmp.conf".source = "${systemd}/example/tmpfiles.d/tmp.conf";
-      "tmpfiles.d/var.conf".source = "${systemd}/example/tmpfiles.d/var.conf";
-      "tmpfiles.d/x11.conf".source = "${systemd}/example/tmpfiles.d/x11.conf";
+      "tmpfiles.d".source = pkgs.symlinkJoin {
+        name = "tmpfiles.d";
+        paths = map (p: p + "/lib/tmpfiles.d") cfg.tmpfiles.packages;
+        postBuild = ''
+          for i in $(cat $pathsPath); do
+            (test -d "$i" && test $(ls "$i"/*.conf | wc -l) -ge 1) || (
+              echo "ERROR: The path '$i' from systemd.tmpfiles.packages contains no *.conf files."
+              exit 1
+            )
+          done
+        '';
+      };
 
       "systemd/system-generators" = { source = hooks "generators" cfg.generators; };
       "systemd/system-shutdown" = { source = hooks "shutdown" cfg.shutdown; };
@@ -974,6 +1040,36 @@ in
         unitConfig.X-StopOnReconfiguration = true;
       };
 
+    systemd.tmpfiles.packages = [
+      # Default tmpfiles rules provided by systemd
+      (pkgs.runCommand "systemd-default-tmpfiles" {} ''
+        mkdir -p $out/lib/tmpfiles.d
+        cd $out/lib/tmpfiles.d
+
+        ln -s "${systemd}/example/tmpfiles.d/home.conf"
+        ln -s "${systemd}/example/tmpfiles.d/journal-nocow.conf"
+        ln -s "${systemd}/example/tmpfiles.d/static-nodes-permissions.conf"
+        ln -s "${systemd}/example/tmpfiles.d/systemd.conf"
+        ln -s "${systemd}/example/tmpfiles.d/systemd-nologin.conf"
+        ln -s "${systemd}/example/tmpfiles.d/systemd-nspawn.conf"
+        ln -s "${systemd}/example/tmpfiles.d/systemd-tmp.conf"
+        ln -s "${systemd}/example/tmpfiles.d/tmp.conf"
+        ln -s "${systemd}/example/tmpfiles.d/var.conf"
+        ln -s "${systemd}/example/tmpfiles.d/x11.conf"
+      '')
+      # User-specified tmpfiles rules
+      (pkgs.writeTextFile {
+        name = "nixos-tmpfiles.d";
+        destination = "/lib/tmpfiles.d/00-nixos.conf";
+        text = ''
+          # This file is created automatically and should not be modified.
+          # Please change the option ‘systemd.tmpfiles.rules’ instead.
+
+          ${concatStringsSep "\n" cfg.tmpfiles.rules}
+        '';
+      })
+    ];
+
     systemd.units =
          mapAttrs' (n: v: nameValuePair "${n}.path"    (pathToUnit    n v)) cfg.paths
       // mapAttrs' (n: v: nameValuePair "${n}.service" (serviceToUnit n v)) cfg.services
@@ -1060,7 +1156,6 @@ in
     systemd.targets.local-fs.unitConfig.X-StopOnReconfiguration = true;
     systemd.targets.remote-fs.unitConfig.X-StopOnReconfiguration = true;
     systemd.targets.network-online.wantedBy = [ "multi-user.target" ];
-    systemd.services.systemd-binfmt.wants = [ "proc-sys-fs-binfmt_misc.mount" ];
     systemd.services.systemd-importd.environment = proxy_env;
 
     # Don't bother with certain units in containers.
diff --git a/nixos/modules/system/boot/timesyncd.nix b/nixos/modules/system/boot/timesyncd.nix
index 9e2f36ca01f..35fb5578b07 100644
--- a/nixos/modules/system/boot/timesyncd.nix
+++ b/nixos/modules/system/boot/timesyncd.nix
@@ -41,6 +41,7 @@ with lib;
 
     systemd.services.systemd-timesyncd = {
       wantedBy = [ "sysinit.target" ];
+      aliases = [ "dbus-org.freedesktop.timesync1.service" ];
       restartTriggers = [ config.environment.etc."systemd/timesyncd.conf".source ];
     };
 
diff --git a/nixos/modules/system/boot/tmp.nix b/nixos/modules/system/boot/tmp.nix
index 5bf5e2eb2ec..26eb172210e 100644
--- a/nixos/modules/system/boot/tmp.nix
+++ b/nixos/modules/system/boot/tmp.nix
@@ -36,4 +36,4 @@ with lib;
 
   };
 
-}
\ No newline at end of file
+}
diff --git a/nixos/modules/system/etc/make-etc.sh b/nixos/modules/system/etc/make-etc.sh
index 1ca4c3046f0..aabfb5e88a6 100644
--- a/nixos/modules/system/etc/make-etc.sh
+++ b/nixos/modules/system/etc/make-etc.sh
@@ -23,7 +23,7 @@ for ((i = 0; i < ${#targets_[@]}; i++)); do
         done
 
     else
-        
+
         mkdir -p $out/etc/$(dirname $target)
         if ! [ -e $out/etc/$target ]; then
             ln -s $source $out/etc/$target
@@ -34,13 +34,12 @@ for ((i = 0; i < ${#targets_[@]}; i++)); do
                 exit 1
             fi
         fi
-        
+
         if test "${modes_[$i]}" != symlink; then
             echo "${modes_[$i]}"  > $out/etc/$target.mode
             echo "${users_[$i]}"  > $out/etc/$target.uid
             echo "${groups_[$i]}" > $out/etc/$target.gid
         fi
-        
+
     fi
 done
-
diff --git a/nixos/modules/tasks/auto-upgrade.nix b/nixos/modules/tasks/auto-upgrade.nix
index e70004e643e..69385e5f2fe 100644
--- a/nixos/modules/tasks/auto-upgrade.nix
+++ b/nixos/modules/tasks/auto-upgrade.nix
@@ -2,9 +2,9 @@
 
 with lib;
 
-let cfg = config.system.autoUpgrade; in
+let cfg = config.system.autoUpgrade;
 
-{
+in {
 
   options = {
 
@@ -21,6 +21,16 @@ let cfg = config.system.autoUpgrade; in
         '';
       };
 
+      flake = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "github:kloenk/nix";
+        description = ''
+          The Flake URI of the NixOS configuration to build.
+          Disables the option <option>system.autoUpgrade.channel</option>.
+        '';
+      };
+
       channel = mkOption {
         type = types.nullOr types.str;
         default = null;
@@ -35,10 +45,20 @@ let cfg = config.system.autoUpgrade; in
 
       flags = mkOption {
         type = types.listOf types.str;
-        default = [];
-        example = [ "-I" "stuff=/home/alice/nixos-stuff" "--option" "extra-binary-caches" "http://my-cache.example.org/" ];
+        default = [ ];
+        example = [
+          "-I"
+          "stuff=/home/alice/nixos-stuff"
+          "--option"
+          "extra-binary-caches"
+          "http://my-cache.example.org/"
+        ];
         description = ''
           Any additional flags passed to <command>nixos-rebuild</command>.
+
+          If you are using flakes and use a local repo you can add
+          <command>[ "--update-input" "nixpkgs" "--commit-lock-file" ]</command>
+          to update nixpkgs.
         '';
       };
 
@@ -82,11 +102,23 @@ let cfg = config.system.autoUpgrade; in
 
   config = lib.mkIf cfg.enable {
 
-    system.autoUpgrade.flags =
-      [ "--no-build-output" ]
-      ++ (if cfg.channel == null
-          then [ "--upgrade" ]
-          else [ "-I" "nixpkgs=${cfg.channel}/nixexprs.tar.xz" ]);
+    assertions = [{
+      assertion = !((cfg.channel != null) && (cfg.flake != null));
+      message = ''
+        The options 'system.autoUpgrade.channels' and 'system.autoUpgrade.flake' cannot both be set.
+      '';
+    }];
+
+    system.autoUpgrade.flags = [ "--no-build-output" ]
+      ++ (if cfg.flake == null then
+        (if cfg.channel == null then
+          [ "--upgrade" ]
+        else [
+          "-I"
+          "nixpkgs=${cfg.channel}/nixexprs.tar.xz"
+        ])
+      else
+        [ "--flake ${cfg.flake}" ]);
 
     systemd.services.nixos-upgrade = {
       description = "NixOS Upgrade";
@@ -96,33 +128,41 @@ let cfg = config.system.autoUpgrade; in
 
       serviceConfig.Type = "oneshot";
 
-      environment = config.nix.envVars //
-        { inherit (config.environment.sessionVariables) NIX_PATH;
-          HOME = "/root";
-        } // config.networking.proxy.envVars;
+      environment = config.nix.envVars // {
+        inherit (config.environment.sessionVariables) NIX_PATH;
+        HOME = "/root";
+      } // config.networking.proxy.envVars;
 
-      path = with pkgs; [ coreutils gnutar xz.bin gzip gitMinimal config.nix.package.out ];
+      path = with pkgs; [
+        coreutils
+        gnutar
+        xz.bin
+        gzip
+        gitMinimal
+        config.nix.package.out
+      ];
 
       script = let
-          nixos-rebuild = "${config.system.build.nixos-rebuild}/bin/nixos-rebuild";
-        in
-        if cfg.allowReboot then ''
-            ${nixos-rebuild} boot ${toString cfg.flags}
-            booted="$(readlink /run/booted-system/{initrd,kernel,kernel-modules})"
-            built="$(readlink /nix/var/nix/profiles/system/{initrd,kernel,kernel-modules})"
-            if [ "$booted" = "$built" ]; then
-              ${nixos-rebuild} switch ${toString cfg.flags}
-            else
-              /run/current-system/sw/bin/shutdown -r +1
-            fi
-          '' else ''
-            ${nixos-rebuild} switch ${toString cfg.flags}
-        '';
+        nixos-rebuild =
+          "${config.system.build.nixos-rebuild}/bin/nixos-rebuild";
+      in if cfg.allowReboot then ''
+        ${nixos-rebuild} boot ${toString cfg.flags}
+        booted="$(readlink /run/booted-system/{initrd,kernel,kernel-modules})"
+        built="$(readlink /nix/var/nix/profiles/system/{initrd,kernel,kernel-modules})"
+        if [ "$booted" = "$built" ]; then
+          ${nixos-rebuild} switch ${toString cfg.flags}
+        else
+          /run/current-system/sw/bin/shutdown -r +1
+        fi
+      '' else ''
+        ${nixos-rebuild} switch ${toString cfg.flags}
+      '';
 
       startAt = cfg.dates;
     };
 
-    systemd.timers.nixos-upgrade.timerConfig.RandomizedDelaySec = cfg.randomizedDelaySec;
+    systemd.timers.nixos-upgrade.timerConfig.RandomizedDelaySec =
+      cfg.randomizedDelaySec;
 
   };
 
diff --git a/nixos/modules/tasks/bcache.nix b/nixos/modules/tasks/bcache.nix
index 8bab91c721f..41fb7664f3d 100644
--- a/nixos/modules/tasks/bcache.nix
+++ b/nixos/modules/tasks/bcache.nix
@@ -8,6 +8,6 @@
 
   boot.initrd.extraUdevRulesCommands = ''
     cp -v ${pkgs.bcache-tools}/lib/udev/rules.d/*.rules $out/
-  ''; 
+  '';
 
 }
diff --git a/nixos/modules/tasks/encrypted-devices.nix b/nixos/modules/tasks/encrypted-devices.nix
index bc0933f16fe..9c3f2d8fccb 100644
--- a/nixos/modules/tasks/encrypted-devices.nix
+++ b/nixos/modules/tasks/encrypted-devices.nix
@@ -37,7 +37,14 @@ let
         default = null;
         example = "/mnt-root/root/.swapkey";
         type = types.nullOr types.str;
-        description = "File system location of keyfile. This unlocks the drive after the root has been mounted to <literal>/mnt-root</literal>.";
+        description = ''
+          Path to a keyfile used to unlock the backing encrypted
+          device. At the time this keyfile is accessed, the
+          <literal>neededForBoot</literal> filesystems (see
+          <literal>fileSystems.&lt;name?&gt;.neededForBoot</literal>)
+          will have been mounted under <literal>/mnt-root</literal>,
+          so the keyfile path should usually start with "/mnt-root/".
+        '';
       };
     };
   };
@@ -65,12 +72,16 @@ in
     boot.initrd = {
       luks = {
         devices =
-          builtins.listToAttrs (map (dev: { name = dev.encrypted.label; value = { device = dev.encrypted.blkDev; }; }) keylessEncDevs);
+          builtins.listToAttrs (map (dev: {
+            name = dev.encrypted.label;
+            value = { device = dev.encrypted.blkDev; };
+          }) keylessEncDevs);
         forceLuksSupportInInitrd = true;
       };
       postMountCommands =
-        concatMapStrings (dev: "cryptsetup luksOpen --key-file ${dev.encrypted.keyFile} ${dev.encrypted.blkDev} ${dev.encrypted.label};\n") keyedEncDevs;
+        concatMapStrings (dev:
+          "cryptsetup luksOpen --key-file ${dev.encrypted.keyFile} ${dev.encrypted.blkDev} ${dev.encrypted.label};\n"
+        ) keyedEncDevs;
     };
   };
 }
-
diff --git a/nixos/modules/tasks/filesystems/btrfs.nix b/nixos/modules/tasks/filesystems/btrfs.nix
index f64493e1a3c..c0ff28039b1 100644
--- a/nixos/modules/tasks/filesystems/btrfs.nix
+++ b/nixos/modules/tasks/filesystems/btrfs.nix
@@ -128,7 +128,10 @@ in
             Nice = 19;
             IOSchedulingClass = "idle";
             ExecStart = "${pkgs.btrfs-progs}/bin/btrfs scrub start -B ${fs}";
-            ExecStop  = "${pkgs.btrfs-progs}/bin/btrfs scrub cancel ${fs}";
+            # if the service is stopped before scrub end, cancel it
+            ExecStop  = pkgs.writeShellScript "btrfs-scrub-maybe-cancel" ''
+              (${pkgs.btrfs-progs}/bin/btrfs scrub status ${fs} | ${pkgs.gnugrep}/bin/grep finished) || ${pkgs.btrfs-progs}/bin/btrfs scrub cancel ${fs}
+            '';
           };
         };
       in listToAttrs (map scrubService cfgScrub.fileSystems);
diff --git a/nixos/modules/tasks/filesystems/zfs.nix b/nixos/modules/tasks/filesystems/zfs.nix
index 43347161a84..9ca7c6fb343 100644
--- a/nixos/modules/tasks/filesystems/zfs.nix
+++ b/nixos/modules/tasks/filesystems/zfs.nix
@@ -191,15 +191,14 @@ in
       };
 
       requestEncryptionCredentials = mkOption {
-        type = types.bool;
+        type = types.either types.bool (types.listOf types.str);
         default = true;
+        example = [ "tank" "data" ];
         description = ''
-          Request encryption keys or passwords for all encrypted datasets on import.
-          For root pools the encryption key can be supplied via both an
-          interactive prompt (keylocation=prompt) and from a file
-          (keylocation=file://). Note that for data pools the encryption key can
-          be only loaded from a file and not via interactive prompt since the
-          import is processed in a background systemd service.
+          If true on import encryption keys or passwords for all encrypted datasets
+          are requested. To only decrypt selected datasets supply a list of dataset
+          names instead. For root pools the encryption key can be supplied via both
+          an interactive prompt (keylocation=prompt) and from a file (keylocation=file://).
         '';
       };
 
@@ -421,9 +420,13 @@ in
               fi
               poolImported "${pool}" || poolImport "${pool}"  # Try one last time, e.g. to import a degraded pool.
             fi
-            ${lib.optionalString cfgZfs.requestEncryptionCredentials ''
-              zfs load-key -a
-            ''}
+            ${if isBool cfgZfs.requestEncryptionCredentials
+              then optionalString cfgZfs.requestEncryptionCredentials ''
+                zfs load-key -a
+              ''
+              else concatMapStrings (fs: ''
+                zfs load-key ${fs}
+              '') cfgZfs.requestEncryptionCredentials}
         '') rootPools));
       };
 
@@ -433,7 +436,16 @@ in
 
       services.zfs.zed.settings = {
         ZED_EMAIL_PROG = mkDefault "${pkgs.mailutils}/bin/mail";
-        PATH = lib.makeBinPath [ packages.zfsUser pkgs.utillinux pkgs.gawk pkgs.gnused pkgs.gnugrep pkgs.coreutils pkgs.curl ];
+        PATH = lib.makeBinPath [
+          packages.zfsUser
+          pkgs.coreutils
+          pkgs.curl
+          pkgs.gawk
+          pkgs.gnugrep
+          pkgs.gnused
+          pkgs.nettools
+          pkgs.utillinux
+        ];
       };
 
       environment.etc = genAttrs
@@ -481,7 +493,11 @@ in
             description = "Import ZFS pool \"${pool}\"";
             # we need systemd-udev-settle until https://github.com/zfsonlinux/zfs/pull/4943 is merged
             requires = [ "systemd-udev-settle.service" ];
-            after = [ "systemd-udev-settle.service" "systemd-modules-load.service" ];
+            after = [
+              "systemd-udev-settle.service"
+              "systemd-modules-load.service"
+              "systemd-ask-password-console.service"
+            ];
             wantedBy = (getPoolMounts pool) ++ [ "local-fs.target" ];
             before = (getPoolMounts pool) ++ [ "local-fs.target" ];
             unitConfig = {
@@ -506,7 +522,27 @@ in
               done
               poolImported "${pool}" || poolImport "${pool}"  # Try one last time, e.g. to import a degraded pool.
               if poolImported "${pool}"; then
-                ${optionalString cfgZfs.requestEncryptionCredentials "\"${packages.zfsUser}/sbin/zfs\" load-key -r \"${pool}\""}
+                ${optionalString (if isBool cfgZfs.requestEncryptionCredentials
+                                  then cfgZfs.requestEncryptionCredentials
+                                  else cfgZfs.requestEncryptionCredentials != []) ''
+                  ${packages.zfsUser}/sbin/zfs list -rHo name,keylocation ${pool} | while IFS=$'\t' read ds kl; do
+                    (${optionalString (!isBool cfgZfs.requestEncryptionCredentials) ''
+                         if ! echo '${concatStringsSep "\n" cfgZfs.requestEncryptionCredentials}' | grep -qFx "$ds"; then
+                           continue
+                         fi
+                       ''}
+                    case "$kl" in
+                      none )
+                        ;;
+                      prompt )
+                        ${config.systemd.package}/bin/systemd-ask-password "Enter key for $ds:" | ${packages.zfsUser}/sbin/zfs load-key "$ds"
+                        ;;
+                      * )
+                        ${packages.zfsUser}/sbin/zfs load-key "$ds"
+                        ;;
+                    esac) < /dev/null # To protect while read ds kl in case anything reads stdin
+                  done
+                ''}
                 echo "Successfully imported ${pool}"
               else
                 exit 1
diff --git a/nixos/modules/tasks/lvm.nix b/nixos/modules/tasks/lvm.nix
index d56a8a2f63a..2c3cc4c5467 100644
--- a/nixos/modules/tasks/lvm.nix
+++ b/nixos/modules/tasks/lvm.nix
@@ -1,17 +1,70 @@
 { config, lib, pkgs, ... }:
 
 with lib;
+let
+  cfg = config.services.lvm;
+in {
+  options.services.lvm = {
+    package = mkOption {
+      type = types.package;
+      default = if cfg.dmeventd.enable then pkgs.lvm2_dmeventd else pkgs.lvm2;
+      internal = true;
+      defaultText = "pkgs.lvm2";
+      description = ''
+        This option allows you to override the LVM package that's used on the system
+        (udev rules, tmpfiles, systemd services).
+        Defaults to pkgs.lvm2, or pkgs.lvm2_dmeventd if dmeventd is enabled.
+      '';
+    };
+    dmeventd.enable = mkEnableOption "the LVM dmevent daemon";
+    boot.thin.enable = mkEnableOption "support for booting from ThinLVs";
+  };
 
-{
-
-  ###### implementation
+  config = mkMerge [
+    (mkIf (!config.boot.isContainer) {
+      systemd.tmpfiles.packages = [ cfg.package.out ];
+      environment.systemPackages = [ cfg.package ];
+      systemd.packages = [ cfg.package ];
 
-  config = mkIf (!config.boot.isContainer) {
+      # TODO: update once https://github.com/NixOS/nixpkgs/pull/93006 was merged
+      services.udev.packages = [ cfg.package.out ];
+    })
+    (mkIf cfg.dmeventd.enable {
+      systemd.sockets."dm-event".wantedBy = [ "sockets.target" ];
+      systemd.services."lvm2-monitor".wantedBy = [ "sysinit.target" ];
 
-    environment.systemPackages = [ pkgs.lvm2 ];
+      environment.etc."lvm/lvm.conf".text = ''
+        dmeventd/executable = "${cfg.package}/bin/dmeventd"
+      '';
+    })
+    (mkIf cfg.boot.thin.enable {
+      boot.initrd = {
+        kernelModules = [ "dm-snapshot" "dm-thin-pool" ];
 
-    services.udev.packages = [ pkgs.lvm2 ];
+        extraUtilsCommands = ''
+          copy_bin_and_libs ${pkgs.thin-provisioning-tools}/bin/pdata_tools
+          copy_bin_and_libs ${pkgs.thin-provisioning-tools}/bin/thin_check
+        '';
+      };
 
-  };
+      environment.etc."lvm/lvm.conf".text = ''
+        global/thin_check_executable = "${pkgs.thin-provisioning-tools}/bin/thin_check"
+      '';
+    })
+    (mkIf (cfg.dmeventd.enable || cfg.boot.thin.enable) {
+      boot.initrd.preLVMCommands = ''
+          mkdir -p /etc/lvm
+          cat << EOF >> /etc/lvm/lvm.conf
+          ${optionalString cfg.boot.thin.enable ''
+            global/thin_check_executable = "$(command -v thin_check)"
+          ''}
+          ${optionalString cfg.dmeventd.enable ''
+            dmeventd/executable = "$(command -v false)"
+            activation/monitoring = 0
+          ''}
+          EOF
+      '';
+    })
+  ];
 
 }
diff --git a/nixos/modules/tasks/network-interfaces-scripted.nix b/nixos/modules/tasks/network-interfaces-scripted.nix
index 9720d90217c..9ba6ccfbe71 100644
--- a/nixos/modules/tasks/network-interfaces-scripted.nix
+++ b/nixos/modules/tasks/network-interfaces-scripted.nix
@@ -54,7 +54,16 @@ let
     };
 
   normalConfig = {
-
+    systemd.network.links = let
+      createNetworkLink = i: nameValuePair "40-${i.name}" {
+        matchConfig.OriginalName = i.name;
+        linkConfig = optionalAttrs (i.macAddress != null) {
+          MACAddress = i.macAddress;
+        } // optionalAttrs (i.mtu != null) {
+          MTUBytes = toString i.mtu;
+        };
+      };
+    in listToAttrs (map createNetworkLink interfaces);
     systemd.services =
       let
 
@@ -164,7 +173,6 @@ let
           { description = "Address configuration of ${i.name}";
             wantedBy = [
               "network-setup.service"
-              "network-link-${i.name}.service"
               "network.target"
             ];
             # order before network-setup because the routes that are configured
@@ -183,6 +191,8 @@ let
                 state="/run/nixos/network/addresses/${i.name}"
                 mkdir -p $(dirname "$state")
 
+                ip link set "${i.name}" up
+
                 ${flip concatMapStrings ips (ip:
                   let
                     cidr = "${ip.address}/${toString ip.prefixLength}";
@@ -222,57 +232,29 @@ let
               '';
             preStop = ''
               state="/run/nixos/network/routes/${i.name}"
-              while read cidr; do
-                echo -n "deleting route $cidr... "
-                ip route del "$cidr" dev "${i.name}" >/dev/null 2>&1 && echo "done" || echo "failed"
-              done < "$state"
-              rm -f "$state"
+              if [ -e "$state" ]; then
+                while read cidr; do
+                  echo -n "deleting route $cidr... "
+                  ip route del "$cidr" dev "${i.name}" >/dev/null 2>&1 && echo "done" || echo "failed"
+                done < "$state"
+                rm -f "$state"
+              fi
 
               state="/run/nixos/network/addresses/${i.name}"
-              while read cidr; do
-                echo -n "deleting address $cidr... "
-                ip addr del "$cidr" dev "${i.name}" >/dev/null 2>&1 && echo "done" || echo "failed"
-              done < "$state"
-              rm -f "$state"
+              if [ -e "$state" ]; then
+                while read cidr; do
+                  echo -n "deleting address $cidr... "
+                  ip addr del "$cidr" dev "${i.name}" >/dev/null 2>&1 && echo "done" || echo "failed"
+                done < "$state"
+                rm -f "$state"
+              fi
             '';
           };
 
-        createNetworkLink = i:
-        let
-          deviceDependency = if (config.boot.isContainer || i.name == "lo")
-            then []
-            else [ (subsystemDevice i.name) ];
-        in
-        nameValuePair "network-link-${i.name}"
-        { description = "Link configuration of ${i.name}";
-          wantedBy = [ "network-interfaces.target" ];
-          before = [ "network-interfaces.target" ];
-          bindsTo = deviceDependency;
-          after = [ "network-pre.target" ] ++ deviceDependency;
-          path = [ pkgs.iproute ];
-          serviceConfig = {
-            Type = "oneshot";
-            RemainAfterExit = true;
-          };
-          script =
-            ''
-              echo "Configuring link..."
-            '' + optionalString (i.macAddress != null) ''
-              echo "setting MAC address to ${i.macAddress}..."
-              ip link set "${i.name}" address "${i.macAddress}"
-            '' + optionalString (i.mtu != null) ''
-              echo "setting MTU to ${toString i.mtu}..."
-              ip link set "${i.name}" mtu "${toString i.mtu}"
-            '' + ''
-              echo -n "bringing up interface... "
-              ip link set "${i.name}" up && echo "done" || (echo "failed"; exit 1)
-            '';
-        };
-
         createTunDevice = i: nameValuePair "${i.name}-netdev"
           { description = "Virtual Network Interface ${i.name}";
-            bindsTo = [ "dev-net-tun.device" ];
-            after = [ "dev-net-tun.device" "network-pre.target" ];
+            bindsTo = optional (!config.boot.isContainer) "dev-net-tun.device";
+            after = optional (!config.boot.isContainer) "dev-net-tun.device" ++ [ "network-pre.target" ];
             wantedBy = [ "network-setup.service" (subsystemDevice i.name) ];
             partOf = [ "network-setup.service" ];
             before = [ "network-setup.service" ];
@@ -298,7 +280,7 @@ let
             bindsTo = deps ++ optional v.rstp "mstpd.service";
             partOf = [ "network-setup.service" ] ++ optional v.rstp "mstpd.service";
             after = [ "network-pre.target" ] ++ deps ++ optional v.rstp "mstpd.service"
-              ++ concatMap (i: [ "network-addresses-${i}.service" "network-link-${i}.service" ]) v.interfaces;
+              ++ map (i: "network-addresses-${i}.service") v.interfaces;
             before = [ "network-setup.service" ];
             serviceConfig.Type = "oneshot";
             serviceConfig.RemainAfterExit = true;
@@ -327,7 +309,7 @@ let
                   # if `libvirtd.service` is not running, do not use `virsh` which would try activate it via 'libvirtd.socket' and thus start it out-of-order.
                   # `libvirtd.service` will set up bridge interfaces when it will start normally.
                   #
-                  if ${pkgs.systemd}/bin/systemctl --quiet is-active 'libvirtd.service'; then
+                  if /run/current-system/systemd/bin/systemctl --quiet is-active 'libvirtd.service'; then
                     for uri in qemu:///system lxc:///; do
                       for dom in $(${pkgs.libvirt}/bin/virsh -c $uri list --name); do
                         ${pkgs.libvirt}/bin/virsh -c $uri dumpxml "$dom" | \
@@ -375,7 +357,7 @@ let
         createVswitchDevice = n: v: nameValuePair "${n}-netdev"
           (let
             deps = concatLists (map deviceDependency (attrNames (filterAttrs (_: config: config.type != "internal") v.interfaces)));
-            internalConfigs = concatMap (i: ["network-link-${i}.service" "network-addresses-${i}.service"]) (attrNames (filterAttrs (_: config: config.type == "internal") v.interfaces));
+            internalConfigs = map (i: "network-addresses-${i}.service") (attrNames (filterAttrs (_: config: config.type == "internal") v.interfaces));
             ofRules = pkgs.writeText "vswitch-${n}-openFlowRules" v.openFlowRules;
           in
           { description = "Open vSwitch Interface ${n}";
@@ -427,7 +409,7 @@ let
             bindsTo = deps;
             partOf = [ "network-setup.service" ];
             after = [ "network-pre.target" ] ++ deps
-              ++ concatMap (i: [ "network-addresses-${i}.service" "network-link-${i}.service" ]) v.interfaces;
+              ++ map (i: "network-addresses-${i}.service") v.interfaces;
             before = [ "network-setup.service" ];
             serviceConfig.Type = "oneshot";
             serviceConfig.RemainAfterExit = true;
@@ -540,7 +522,6 @@ let
           });
 
       in listToAttrs (
-           map createNetworkLink interfaces ++
            map configureAddrs interfaces ++
            map createTunDevice (filter (i: i.virtual) interfaces))
          // mapAttrs' createBridgeDevice cfg.bridges
diff --git a/nixos/modules/tasks/network-interfaces.nix b/nixos/modules/tasks/network-interfaces.nix
index 44677d417ea..af98123d477 100644
--- a/nixos/modules/tasks/network-interfaces.nix
+++ b/nixos/modules/tasks/network-interfaces.nix
@@ -283,7 +283,7 @@ let
         default = false;
         type = types.bool;
         description = ''
-          Turn on proxy_arp for this device (and proxy_ndp for ipv6).
+          Turn on proxy_arp for this device.
           This is mainly useful for creating pseudo-bridges between a real
           interface and a virtual network such as VPN or a virtual machine for
           interfaces that don't support real bridging (most wlan interfaces).
@@ -376,10 +376,20 @@ in
 
     networking.hostName = mkOption {
       default = "nixos";
-      type = types.str;
+      # Only allow hostnames without the domain name part (i.e. no FQDNs, see
+      # e.g. "man 5 hostname") and require valid DNS labels (recommended
+      # syntax). Note: We also allow underscores for compatibility/legacy
+      # reasons (as undocumented feature):
+      type = types.strMatching
+        "^$|^[[:alpha:]]([[:alnum:]_-]{0,61}[[:alnum:]])?$";
       description = ''
-        The name of the machine.  Leave it empty if you want to obtain
-        it from a DHCP server (if using DHCP).
+        The name of the machine. Leave it empty if you want to obtain it from a
+        DHCP server (if using DHCP). The hostname must be a valid DNS label (see
+        RFC 1035 section 2.3.1: "Preferred name syntax") and as such must not
+        contain the domain part. This means that the hostname must start with a
+        letter, end with a letter or digit, and have as interior characters only
+        letters, digits, and hyphen. The maximum length is 63 characters.
+        Additionally it is recommended to only use lower-case characters.
       '';
     };
 
@@ -398,6 +408,9 @@ in
         (this derives it from the machine-id that systemd generates) or
 
         <literal>head -c4 /dev/urandom | od -A none -t x4</literal>
+
+        The primary use case is to ensure when using ZFS that a pool isn't imported
+        accidentally on a wrong machine.
       '';
     };
 
@@ -1055,11 +1068,11 @@ in
       optionalString hasBonds "options bonding max_bonds=0";
 
     boot.kernel.sysctl = {
+      "net.ipv4.conf.all.forwarding" = mkDefault (any (i: i.proxyARP) interfaces);
       "net.ipv6.conf.all.disable_ipv6" = mkDefault (!cfg.enableIPv6);
       "net.ipv6.conf.default.disable_ipv6" = mkDefault (!cfg.enableIPv6);
-      "net.ipv6.conf.all.forwarding" = mkDefault (any (i: i.proxyARP) interfaces);
     } // listToAttrs (flip concatMap (filter (i: i.proxyARP) interfaces)
-        (i: forEach [ "4" "6" ] (v: nameValuePair "net.ipv${v}.conf.${replaceChars ["."] ["/"] i.name}.proxy_arp" true)))
+        (i: [(nameValuePair "net.ipv4.conf.${replaceChars ["."] ["/"] i.name}.proxy_arp" true)]))
       // listToAttrs (forEach interfaces
         (i: let
           opt = i.tempAddress;
@@ -1116,7 +1129,6 @@ in
       ++ optionals config.networking.wireless.enable [
         pkgs.wirelesstools # FIXME: obsolete?
         pkgs.iw
-        pkgs.rfkill
       ]
       ++ bridgeStp;
 
diff --git a/nixos/modules/testing/test-instrumentation.nix b/nixos/modules/testing/test-instrumentation.nix
index 1baeab53b0c..30ffb12cbad 100644
--- a/nixos/modules/testing/test-instrumentation.nix
+++ b/nixos/modules/testing/test-instrumentation.nix
@@ -1,22 +1,13 @@
 # This module allows the test driver to connect to the virtual machine
 # via a root shell attached to port 514.
 
-{ config, lib, pkgs, ... }:
+{ options, config, lib, pkgs, ... }:
 
 with lib;
 with import ../../lib/qemu-flags.nix { inherit pkgs; };
 
 {
 
-  # This option is a dummy that if used in conjunction with
-  # modules/virtualisation/qemu-vm.nix gets merged with the same option defined
-  # there and only is declared here because some modules use
-  # test-instrumentation.nix but not qemu-vm.nix.
-  #
-  # One particular example are the boot tests where we want instrumentation
-  # within the images but not other stuff like setting up 9p filesystems.
-  options.virtualisation.qemu = { };
-
   config = {
 
     systemd.services.backdoor =
@@ -55,7 +46,12 @@ with import ../../lib/qemu-flags.nix { inherit pkgs; };
     systemd.services."serial-getty@hvc0".enable = false;
 
     # Only use a serial console, no TTY.
-    virtualisation.qemu.consoles = [ qemuSerialDevice ];
+    # NOTE: optionalAttrs
+    #       test-instrumentation.nix appears to be used without qemu-vm.nix, so
+    #       we avoid defining consoles if not possible.
+    # TODO: refactor such that test-instrumentation can import qemu-vm
+    #       or declare virtualisation.qemu.console option in a module that's always imported
+    virtualisation = lib.optionalAttrs (options ? virtualisation.qemu.consoles) { qemu.consoles = [ qemuSerialDevice ]; };
 
     boot.initrd.preDeviceCommands =
       ''
diff --git a/nixos/modules/virtualisation/azure-agent.nix b/nixos/modules/virtualisation/azure-agent.nix
index 036b1036f92..e85482af839 100644
--- a/nixos/modules/virtualisation/azure-agent.nix
+++ b/nixos/modules/virtualisation/azure-agent.nix
@@ -48,7 +48,7 @@ let
 
   provisionedHook = pkgs.writeScript "provisioned-hook" ''
     #!${pkgs.runtimeShell}
-    ${config.systemd.package}/bin/systemctl start provisioned.target
+    /run/current-system/systemd/bin/systemctl start provisioned.target
   '';
 
 in
diff --git a/nixos/modules/virtualisation/azure-image.nix b/nixos/modules/virtualisation/azure-image.nix
index 21fd58e5c90..60fed3222ef 100644
--- a/nixos/modules/virtualisation/azure-image.nix
+++ b/nixos/modules/virtualisation/azure-image.nix
@@ -6,7 +6,7 @@ let
 in
 {
   imports = [ ./azure-common.nix ];
-  
+
   options = {
     virtualisation.azureImage.diskSize = mkOption {
       type = with types; int;
diff --git a/nixos/modules/virtualisation/containers.nix b/nixos/modules/virtualisation/containers.nix
index 7d184575640..3a6767d84a9 100644
--- a/nixos/modules/virtualisation/containers.nix
+++ b/nixos/modules/virtualisation/containers.nix
@@ -23,6 +23,15 @@ in
     maintainers = [] ++ lib.teams.podman.members;
   };
 
+
+  imports = [
+    (
+      lib.mkRemovedOptionModule
+      [ "virtualisation" "containers" "users" ]
+      "All users with `isNormalUser = true` set now get appropriate subuid/subgid mappings."
+    )
+  ];
+
   options.virtualisation.containers = {
 
     enable =
@@ -34,6 +43,25 @@ in
         '';
       };
 
+    containersConf = mkOption {
+      default = {};
+      description = "containers.conf configuration";
+      type = types.submodule {
+        options = {
+
+          extraConfig = mkOption {
+            type = types.lines;
+            default = "";
+            description = ''
+              Extra configuration that should be put in the containers.conf
+              configuration file
+            '';
+
+          };
+        };
+      };
+    };
+
     registries = {
       search = mkOption {
         type = types.listOf types.str;
@@ -80,43 +108,20 @@ in
       '';
     };
 
-    users = mkOption {
-      default = [];
-      type = types.listOf types.str;
-      description = ''
-        List of users to set up subuid/subgid mappings for.
-        This is a requirement for running rootless containers.
-      '';
-    };
-
   };
 
   config = lib.mkIf cfg.enable {
 
+    environment.etc."containers/containers.conf".text = ''
+      [network]
+      cni_plugin_dirs = ["${pkgs.cni-plugins}/bin/"]
+
+    '' + cfg.containersConf.extraConfig;
+
     environment.etc."containers/registries.conf".source = toTOML "registries.conf" {
       registries = lib.mapAttrs (n: v: { registries = v; }) cfg.registries;
     };
 
-    users.extraUsers = builtins.listToAttrs (
-      (
-        builtins.foldl' (
-          acc: user: {
-            values = acc.values ++ [
-              {
-                name = user;
-                value = {
-                  subUidRanges = [ { startUid = acc.offset; count = 65536; } ];
-                  subGidRanges = [ { startGid = acc.offset; count = 65536; } ];
-                };
-              }
-            ];
-            offset = acc.offset + 65536;
-          }
-        )
-        { values = []; offset = 100000; } (lib.unique cfg.users)
-      ).values
-    );
-
     environment.etc."containers/policy.json".source =
       if cfg.policy != {} then pkgs.writeText "policy.json" (builtins.toJSON cfg.policy)
       else copyFile "${pkgs.skopeo.src}/default-policy.json";
diff --git a/nixos/modules/virtualisation/cri-o.nix b/nixos/modules/virtualisation/cri-o.nix
index 2af4214302d..9c818eee73b 100644
--- a/nixos/modules/virtualisation/cri-o.nix
+++ b/nixos/modules/virtualisation/cri-o.nix
@@ -5,6 +5,8 @@ with lib;
 let
   cfg = config.virtualisation.cri-o;
 
+  crioPackage = (pkgs.cri-o.override { inherit (cfg) extraPackages; });
+
   # Copy configuration files to avoid having the entire sources in the system closure
   copyFile = filePath: pkgs.runCommandNoCC (builtins.unsafeDiscardStringContext (builtins.baseNameOf filePath)) {} ''
     cp ${filePath} $out
@@ -23,55 +25,92 @@ in
     enable = mkEnableOption "Container Runtime Interface for OCI (CRI-O)";
 
     storageDriver = mkOption {
-      type = types.enum ["btrfs" "overlay" "vfs"];
+      type = types.enum [ "btrfs" "overlay" "vfs" ];
       default = "overlay";
       description = "Storage driver to be used";
     };
 
     logLevel = mkOption {
-      type = types.enum ["trace" "debug" "info" "warn" "error" "fatal"];
+      type = types.enum [ "trace" "debug" "info" "warn" "error" "fatal" ];
       default = "info";
       description = "Log level to be used";
     };
 
     pauseImage = mkOption {
-      type = types.str;
-      default = "k8s.gcr.io/pause:3.1";
-      description = "Pause image for pod sandboxes to be used";
+      type = types.nullOr types.str;
+      default = null;
+      description = "Override the default pause image for pod sandboxes";
+      example = [ "k8s.gcr.io/pause:3.2" ];
     };
 
     pauseCommand = mkOption {
-      type = types.str;
-      default = "/pause";
-      description = "Pause command to be executed";
+      type = types.nullOr types.str;
+      default = null;
+      description = "Override the default pause command";
+      example = [ "/pause" ];
+    };
+
+    runtime = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = "Override the default runtime";
+      example = [ "crun" ];
+    };
+
+    extraPackages = mkOption {
+      type = with types; listOf package;
+      default = [ ];
+      example = lib.literalExample ''
+        [
+          pkgs.gvisor
+        ]
+      '';
+      description = ''
+        Extra packages to be installed in the CRI-O wrapper.
+      '';
+    };
+
+    package = lib.mkOption {
+      type = types.package;
+      default = crioPackage;
+      internal = true;
+      description = ''
+        The final CRI-O package (including extra packages).
+      '';
     };
   };
 
   config = mkIf cfg.enable {
-    environment.systemPackages = with pkgs;
-      [ cri-o cri-tools conmon iptables runc utillinux ];
+    environment.systemPackages = [ cfg.package pkgs.cri-tools ];
 
-    environment.etc."crictl.yaml".source = copyFile "${pkgs.cri-o.src}/crictl.yaml";
+    environment.etc."crictl.yaml".source = copyFile "${pkgs.cri-o-unwrapped.src}/crictl.yaml";
 
-    environment.etc."crio/crio.conf".text = ''
+    environment.etc."crio/crio.conf.d/00-default.conf".text = ''
       [crio]
       storage_driver = "${cfg.storageDriver}"
 
       [crio.image]
-      pause_image = "${cfg.pauseImage}"
-      pause_command = "${cfg.pauseCommand}"
+      ${optionalString (cfg.pauseImage != null) ''pause_image = "${cfg.pauseImage}"''}
+      ${optionalString (cfg.pauseCommand != null) ''pause_command = "${cfg.pauseCommand}"''}
 
       [crio.network]
       plugin_dirs = ["${pkgs.cni-plugins}/bin/"]
-      network_dir = "/etc/cni/net.d/"
 
       [crio.runtime]
-      conmon = "${pkgs.conmon}/bin/conmon"
+      cgroup_manager = "systemd"
       log_level = "${cfg.logLevel}"
-      manage_network_ns_lifecycle = true
+      manage_ns_lifecycle = true
+      pinns_path = "${cfg.package}/bin/pinns"
+
+      ${optionalString (cfg.runtime != null) ''
+      default_runtime = "${cfg.runtime}"
+      [crio.runtime.runtimes]
+      [crio.runtime.runtimes.${cfg.runtime}]
+      ''}
     '';
 
-    environment.etc."cni/net.d/10-crio-bridge.conf".source = copyFile "${pkgs.cri-o.src}/contrib/cni/10-crio-bridge.conf";
+    environment.etc."cni/net.d/10-crio-bridge.conf".source = copyFile "${pkgs.cri-o-unwrapped.src}/contrib/cni/10-crio-bridge.conf";
+    environment.etc."cni/net.d/99-loopback.conf".source = copyFile "${pkgs.cri-o-unwrapped.src}/contrib/cni/99-loopback.conf";
 
     # Enable common /etc/containers configuration
     virtualisation.containers.enable = true;
@@ -81,10 +120,10 @@ in
       documentation = [ "https://github.com/cri-o/cri-o" ];
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" ];
-      path = [ pkgs.utillinux pkgs.runc pkgs.iptables ];
+      path = [ cfg.package ];
       serviceConfig = {
         Type = "notify";
-        ExecStart = "${pkgs.cri-o}/bin/crio";
+        ExecStart = "${cfg.package}/bin/crio";
         ExecReload = "/bin/kill -s HUP $MAINPID";
         TasksMax = "infinity";
         LimitNOFILE = "1048576";
diff --git a/nixos/modules/virtualisation/docker-preloader.nix b/nixos/modules/virtualisation/docker-preloader.nix
deleted file mode 100644
index 6ab83058dee..00000000000
--- a/nixos/modules/virtualisation/docker-preloader.nix
+++ /dev/null
@@ -1,134 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-with builtins;
-
-let
-  cfg = config.virtualisation;
-
-  sanitizeImageName = image: replaceStrings ["/"] ["-"] image.imageName;
-  hash = drv: head (split "-" (baseNameOf drv.outPath));
-  # The label of an ext4 FS is limited to 16 bytes
-  labelFromImage = image: substring 0 16 (hash image);
-
-  # The Docker image is loaded and some files from /var/lib/docker/
-  # are written into a qcow image.
-  preload = image: pkgs.vmTools.runInLinuxVM (
-    pkgs.runCommand "docker-preload-image-${sanitizeImageName image}" {
-      buildInputs = with pkgs; [ docker e2fsprogs utillinux curl kmod ];
-      preVM = pkgs.vmTools.createEmptyImage {
-        size = cfg.dockerPreloader.qcowSize;
-        fullName = "docker-deamon-image.qcow2";
-      };
-    }
-    ''
-      mkfs.ext4 /dev/vda
-      e2label /dev/vda ${labelFromImage image}
-      mkdir -p /var/lib/docker
-      mount -t ext4 /dev/vda /var/lib/docker
-
-      modprobe overlay
-
-      # from https://github.com/tianon/cgroupfs-mount/blob/master/cgroupfs-mount
-      mount -t tmpfs -o uid=0,gid=0,mode=0755 cgroup /sys/fs/cgroup
-      cd /sys/fs/cgroup
-      for sys in $(awk '!/^#/ { if ($4 == 1) print $1 }' /proc/cgroups); do
-        mkdir -p $sys
-        if ! mountpoint -q $sys; then
-          if ! mount -n -t cgroup -o $sys cgroup $sys; then
-            rmdir $sys || true
-          fi
-        fi
-      done
-
-      dockerd -H tcp://127.0.0.1:5555 -H unix:///var/run/docker.sock &
-
-      until $(curl --output /dev/null --silent --connect-timeout 2 http://127.0.0.1:5555); do
-        printf '.'
-        sleep 1
-      done
-
-      docker load -i ${image}
-
-      kill %1
-      find /var/lib/docker/ -maxdepth 1 -mindepth 1 -not -name "image" -not -name "overlay2" | xargs rm -rf
-    '');
-
-  preloadedImages = map preload cfg.dockerPreloader.images;
-
-in
-
-{
-  options.virtualisation.dockerPreloader = {
-    images = mkOption {
-      default = [ ];
-      type = types.listOf types.package;
-      description =
-      ''
-        A list of Docker images to preload (in the /var/lib/docker directory).
-      '';
-    };
-    qcowSize = mkOption {
-      default = 1024;
-      type = types.int;
-      description =
-      ''
-        The size (MB) of qcow files.
-      '';
-    };
-  };
-
-  config = mkIf (cfg.dockerPreloader.images != []) {
-    assertions = [{
-      # If docker.storageDriver is null, Docker choose the storage
-      # driver. So, in this case, we cannot be sure overlay2 is used.
-      assertion = cfg.docker.storageDriver == "overlay2"
-        || cfg.docker.storageDriver == "overlay"
-        || cfg.docker.storageDriver == null;
-      message = "The Docker image Preloader only works with overlay2 storage driver!";
-    }];
-
-    virtualisation.qemu.options =
-      map (path: "-drive if=virtio,file=${path}/disk-image.qcow2,readonly,media=cdrom,format=qcow2")
-      preloadedImages;
-
-
-    # All attached QCOW files are mounted and their contents are linked
-    # to /var/lib/docker/ in order to make image available.
-    systemd.services.docker-preloader = {
-      description = "Preloaded Docker images";
-      wantedBy = ["docker.service"];
-      after = ["network.target"];
-      path = with pkgs; [ mount rsync jq ];
-      script = ''
-        mkdir -p /var/lib/docker/overlay2/l /var/lib/docker/image/overlay2
-        echo '{}' > /tmp/repositories.json
-
-        for i in ${concatStringsSep " " (map labelFromImage cfg.dockerPreloader.images)}; do
-          mkdir -p /mnt/docker-images/$i
-
-          # The ext4 label is limited to 16 bytes
-          mount /dev/disk/by-label/$(echo $i | cut -c1-16) -o ro,noload /mnt/docker-images/$i
-
-          find /mnt/docker-images/$i/overlay2/ -maxdepth 1 -mindepth 1 -not -name l\
-             -exec ln -s '{}' /var/lib/docker/overlay2/ \;
-          cp -P /mnt/docker-images/$i/overlay2/l/* /var/lib/docker/overlay2/l/
-
-          rsync -a /mnt/docker-images/$i/image/ /var/lib/docker/image/
-
-          # Accumulate image definitions
-          cp /tmp/repositories.json /tmp/repositories.json.tmp
-          jq -s '.[0] * .[1]' \
-            /tmp/repositories.json.tmp \
-            /mnt/docker-images/$i/image/overlay2/repositories.json \
-            > /tmp/repositories.json
-        done
-
-        mv /tmp/repositories.json /var/lib/docker/image/overlay2/repositories.json
-      '';
-      serviceConfig = {
-        Type = "oneshot";
-      };
-    };
-  };
-}
diff --git a/nixos/modules/virtualisation/docker.nix b/nixos/modules/virtualisation/docker.nix
index 7d196a46276..d87ada35a0a 100644
--- a/nixos/modules/virtualisation/docker.nix
+++ b/nixos/modules/virtualisation/docker.nix
@@ -149,6 +149,7 @@ in
   ###### implementation
 
   config = mkIf cfg.enable (mkMerge [{
+      boot.kernelModules = [ "bridge" "veth" ];
       environment.systemPackages = [ cfg.package ]
         ++ optional cfg.enableNvidia pkgs.nvidia-docker;
       users.groups.docker.gid = config.ids.gids.docker;
diff --git a/nixos/modules/virtualisation/hyperv-image.nix b/nixos/modules/virtualisation/hyperv-image.nix
index be2f12b7d01..fabc9113dfc 100644
--- a/nixos/modules/virtualisation/hyperv-image.nix
+++ b/nixos/modules/virtualisation/hyperv-image.nix
@@ -37,6 +37,7 @@ in {
       name = cfg.vmDerivationName;
       postVM = ''
         ${pkgs.vmTools.qemu}/bin/qemu-img convert -f raw -o subformat=dynamic -O vhdx $diskImage $out/${cfg.vmFileName}
+        rm $diskImage
       '';
       format = "raw";
       diskSize = cfg.baseImageSize;
diff --git a/nixos/modules/virtualisation/kvmgt.nix b/nixos/modules/virtualisation/kvmgt.nix
index 0902d2dc2cb..e08ad344628 100644
--- a/nixos/modules/virtualisation/kvmgt.nix
+++ b/nixos/modules/virtualisation/kvmgt.nix
@@ -9,8 +9,8 @@ let
 
   vgpuOptions = {
     uuid = mkOption {
-      type = types.str;
-      description = "UUID of VGPU device. You can generate one with <package>libossp_uuid</package>.";
+      type = with types; listOf str;
+      description = "UUID(s) of VGPU device. You can generate one with <package>libossp_uuid</package>.";
     };
   };
 
@@ -36,7 +36,7 @@ in {
           and find info about device via <command>cat /sys/bus/pci/devices/*/mdev_supported_types/i915-GVTg_V5_4/description</command>
         '';
         example = {
-          i915-GVTg_V5_8.uuid = "a297db4a-f4c2-11e6-90f6-d3b88d6c9525";
+          i915-GVTg_V5_8.uuid = [ "a297db4a-f4c2-11e6-90f6-d3b88d6c9525" ];
         };
       };
     };
@@ -51,31 +51,35 @@ in {
     boot.kernelModules = [ "kvmgt" ];
     boot.kernelParams = [ "i915.enable_gvt=1" ];
 
-    systemd.paths = mapAttrs' (name: value:
-      nameValuePair "kvmgt-${name}" {
-        description = "KVMGT VGPU ${name} path";
-        wantedBy = [ "multi-user.target" ];
-        pathConfig = {
-          PathExists = "/sys/bus/pci/devices/${cfg.device}/mdev_supported_types/${name}/create";
-        };
-      }
-    ) cfg.vgpus;
-
     services.udev.extraRules = ''
       SUBSYSTEM=="vfio", OWNER="root", GROUP="kvm"
     '';
 
-    systemd.services = mapAttrs' (name: value:
-      nameValuePair "kvmgt-${name}" {
-        description = "KVMGT VGPU ${name}";
-        serviceConfig = {
-          Type = "oneshot";
-          RemainAfterExit = true;
-          ExecStart = "${pkgs.runtimeShell} -c 'echo ${value.uuid} > /sys/bus/pci/devices/${cfg.device}/mdev_supported_types/${name}/create'";
-          ExecStop = "${pkgs.runtimeShell} -c 'echo 1 > /sys/bus/pci/devices/${cfg.device}/${value.uuid}/remove'";
-        };
-      }
-    ) cfg.vgpus;
+    systemd = let
+      vgpus = listToAttrs (flatten (mapAttrsToList
+        (mdev: opt: map (id: nameValuePair "kvmgt-${id}" { inherit mdev; uuid = id; }) opt.uuid)
+        cfg.vgpus));
+    in {
+      paths = mapAttrs (_: opt:
+        {
+          description = "KVMGT VGPU ${opt.uuid} path";
+          wantedBy = [ "multi-user.target" ];
+          pathConfig = {
+            PathExists = "/sys/bus/pci/devices/${cfg.device}/mdev_supported_types/${opt.mdev}/create";
+          };
+        }) vgpus;
+
+      services = mapAttrs (_: opt:
+        {
+          description = "KVMGT VGPU ${opt.uuid}";
+          serviceConfig = {
+            Type = "oneshot";
+            RemainAfterExit = true;
+            ExecStart = "${pkgs.runtimeShell} -c 'echo ${opt.uuid} > /sys/bus/pci/devices/${cfg.device}/mdev_supported_types/${opt.mdev}/create'";
+            ExecStop = "${pkgs.runtimeShell} -c 'echo 1 > /sys/bus/pci/devices/${cfg.device}/${opt.uuid}/remove'";
+          };
+        }) vgpus;
+    };
   };
 
   meta.maintainers = with maintainers; [ gnidorah ];
diff --git a/nixos/modules/virtualisation/libvirtd.nix b/nixos/modules/virtualisation/libvirtd.nix
index 4f22099443f..1d6a9457dde 100644
--- a/nixos/modules/virtualisation/libvirtd.nix
+++ b/nixos/modules/virtualisation/libvirtd.nix
@@ -7,10 +7,8 @@ let
   cfg = config.virtualisation.libvirtd;
   vswitch = config.virtualisation.vswitch;
   configFile = pkgs.writeText "libvirtd.conf" ''
-    unix_sock_group = "libvirtd"
-    unix_sock_rw_perms = "0770"
-    auth_unix_ro = "none"
-    auth_unix_rw = "none"
+    auth_unix_ro = "polkit"
+    auth_unix_rw = "polkit"
     ${cfg.extraConfig}
   '';
   qemuConfigFile = pkgs.writeText "qemu.conf" ''
@@ -116,7 +114,7 @@ in {
         Specifies the action to be done to / on the guests when the host boots.
         The "start" option starts all guests that were running prior to shutdown
         regardless of their autostart settings. The "ignore" option will not
-        start the formally running guest on boot. However, any guest marked as
+        start the formerly running guest on boot. However, any guest marked as
         autostart will still be automatically started by libvirtd.
       '';
     };
@@ -267,7 +265,16 @@ in {
       restartIfChanged = false;
     };
 
-    systemd.sockets.libvirtd    .wantedBy = [ "sockets.target" ];
-    systemd.sockets.libvirtd-tcp.wantedBy = [ "sockets.target" ];
+    # https://libvirt.org/daemons.html#monolithic-systemd-integration
+    systemd.sockets.libvirtd.wantedBy = [ "sockets.target" ];
+
+    security.polkit.extraConfig = ''
+      polkit.addRule(function(action, subject) {
+        if (action.id == "org.libvirt.unix.manage" &&
+          subject.isInGroup("libvirtd")) {
+          return polkit.Result.YES;
+        }
+      });
+    '';
   };
 }
diff --git a/nixos/modules/virtualisation/lxd.nix b/nixos/modules/virtualisation/lxd.nix
index 53b89a9f55b..3958fc2c1d7 100644
--- a/nixos/modules/virtualisation/lxd.nix
+++ b/nixos/modules/virtualisation/lxd.nix
@@ -15,7 +15,6 @@ in
   ###### interface
 
   options = {
-
     virtualisation.lxd = {
       enable = mkOption {
         type = types.bool;
@@ -25,12 +24,18 @@ in
           containers. Users in the "lxd" group can interact with
           the daemon (e.g. to start or stop containers) using the
           <command>lxc</command> command line tool, among others.
+
+          Most of the time, you'll also want to start lxcfs, so
+          that containers can "see" the limits:
+          <code>
+            virtualisation.lxc.lxcfs.enable = true;
+          </code>
         '';
       };
 
       package = mkOption {
         type = types.package;
-        default = pkgs.lxd;
+        default = pkgs.lxd.override { nftablesSupport = config.networking.nftables.enable; };
         defaultText = "pkgs.lxd";
         description = ''
           The LXD package to use.
@@ -65,6 +70,7 @@ in
           with nixos.
         '';
       };
+
       recommendedSysctlSettings = mkOption {
         type = types.bool;
         default = false;
@@ -83,7 +89,6 @@ in
   ###### implementation
 
   config = mkIf cfg.enable {
-
     environment.systemPackages = [ cfg.package ];
 
     security.apparmor = {
@@ -115,6 +120,12 @@ in
         LimitNOFILE = "1048576";
         LimitNPROC = "infinity";
         TasksMax = "infinity";
+
+        # By default, `lxd` loads configuration files from hard-coded
+        # `/usr/share/lxc/config` - since this is a no-go for us, we have to
+        # explicitly tell it where the actual configuration files are
+        Environment = mkIf (config.virtualisation.lxc.lxcfs.enable)
+          "LXD_LXC_TEMPLATE_CONFIG=${pkgs.lxcfs}/share/lxc/config";
       };
     };
 
diff --git a/nixos/modules/virtualisation/podman.nix b/nixos/modules/virtualisation/podman.nix
index 652850bf500..e0e2f04e24c 100644
--- a/nixos/modules/virtualisation/podman.nix
+++ b/nixos/modules/virtualisation/podman.nix
@@ -28,6 +28,10 @@ let
 
 in
 {
+  imports = [
+    (lib.mkRenamedOptionModule [ "virtualisation" "podman" "libpod" ] [ "virtualisation" "containers" "containersConf" ])
+  ];
+
   meta = {
     maintainers = lib.teams.podman.members;
   };
@@ -67,25 +71,6 @@ in
       '';
     };
 
-    libpod = mkOption {
-      default = {};
-      description = "Libpod configuration";
-      type = types.submodule {
-        options = {
-
-          extraConfig = mkOption {
-            type = types.lines;
-            default = "";
-            description = ''
-              Extra configuration that should be put in the libpod.conf
-              configuration file
-            '';
-
-          };
-        };
-      };
-    };
-
     package = lib.mkOption {
       type = types.package;
       default = podmanPackage;
@@ -103,11 +88,6 @@ in
     environment.systemPackages = [ cfg.package ]
       ++ lib.optional cfg.dockerCompat dockerCompat;
 
-    environment.etc."containers/libpod.conf".text = ''
-      cni_plugin_dir = ["${pkgs.cni-plugins}/bin/"]
-
-    '' + cfg.libpod.extraConfig;
-
     environment.etc."cni/net.d/87-podman-bridge.conflist".source = copyFile "${pkgs.podman-unwrapped.src}/cni/87-podman-bridge.conflist";
 
     # Enable common /etc/containers configuration
diff --git a/nixos/modules/virtualisation/qemu-vm.nix b/nixos/modules/virtualisation/qemu-vm.nix
index ac86330c098..42e43f5ee02 100644
--- a/nixos/modules/virtualisation/qemu-vm.nix
+++ b/nixos/modules/virtualisation/qemu-vm.nix
@@ -16,11 +16,6 @@ let
 
   qemu = config.system.build.qemu or pkgs.qemu_test;
 
-  vmName =
-    if config.networking.hostName == ""
-    then "noname"
-    else config.networking.hostName;
-
   cfg = config.virtualisation;
 
   consoles = lib.concatMapStringsSep " " (c: "console=${c}") cfg.qemu.consoles;
@@ -46,6 +41,13 @@ let
         description = "Extra options passed to device flag.";
       };
 
+      name = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description =
+          "A name for the drive. Must be unique in the drives list. Not passed to qemu.";
+      };
+
     };
 
   };
@@ -74,6 +76,32 @@ let
 
   drivesCmdLine = drives: concatStringsSep " " (imap1 driveCmdline drives);
 
+
+  # Creates a device name from a 1-based a numerical index, e.g.
+  # * `driveDeviceName 1` -> `/dev/vda`
+  # * `driveDeviceName 2` -> `/dev/vdb`
+  driveDeviceName = idx:
+    let letter = elemAt lowerChars (idx - 1);
+    in if cfg.qemu.diskInterface == "scsi" then
+      "/dev/sd${letter}"
+    else
+      "/dev/vd${letter}";
+
+  lookupDriveDeviceName = driveName: driveList:
+    (findSingle (drive: drive.name == driveName)
+      (throw "Drive ${driveName} not found")
+      (throw "Multiple drives named ${driveName}") driveList).device;
+
+  addDeviceNames =
+    imap1 (idx: drive: drive // { device = driveDeviceName idx; });
+
+  efiPrefix =
+    if (pkgs.stdenv.isi686 || pkgs.stdenv.isx86_64) then "${pkgs.OVMF.fd}/FV/OVMF"
+    else if pkgs.stdenv.isAarch64 then "${pkgs.OVMF.fd}/FV/AAVMF"
+    else throw "No EFI firmware available for platform";
+  efiFirmware = "${efiPrefix}_CODE.fd";
+  efiVarsDefault = "${efiPrefix}_VARS.fd";
+
   # Shell script to start the VM.
   startVM =
     ''
@@ -99,10 +127,14 @@ let
         # A writable boot disk can be booted from automatically.
         ${qemu}/bin/qemu-img create -f qcow2 -b ${bootDisk}/disk.img $TMPDIR/disk.img || exit 1
 
+        NIX_EFI_VARS=$(readlink -f ''${NIX_EFI_VARS:-${cfg.efiVars}})
+
         ${if cfg.useEFIBoot then ''
-          # VM needs a writable flash BIOS.
-          cp ${bootDisk}/bios.bin $TMPDIR || exit 1
-          chmod 0644 $TMPDIR/bios.bin || exit 1
+          # VM needs writable EFI vars
+          if ! test -e "$NIX_EFI_VARS"; then
+            cp ${bootDisk}/efi-vars.fd "$NIX_EFI_VARS" || exit 1
+            chmod 0644 "$NIX_EFI_VARS" || exit 1
+          fi
         '' else ''
         ''}
       '' else ''
@@ -119,7 +151,7 @@ let
 
       # Start QEMU.
       exec ${qemuBinary qemu} \
-          -name ${vmName} \
+          -name ${config.system.name} \
           -m ${toString config.virtualisation.memorySize} \
           -smp ${toString config.virtualisation.cores} \
           -device virtio-rng-pci \
@@ -139,6 +171,8 @@ let
 
   # Generate a hard disk image containing a /boot partition and GRUB
   # in the MBR.  Used when the `useBootLoader' option is set.
+  # Uses `runInLinuxVM` to create the image in a throwaway VM.
+  # See note [Disk layout with `useBootLoader`].
   # FIXME: use nixos/lib/make-disk-image.nix.
   bootDisk =
     pkgs.vmTools.runInLinuxVM (
@@ -147,21 +181,22 @@ let
             ''
               mkdir $out
               diskImage=$out/disk.img
-              bootFlash=$out/bios.bin
-              ${qemu}/bin/qemu-img create -f qcow2 $diskImage "40M"
+              ${qemu}/bin/qemu-img create -f qcow2 $diskImage "60M"
               ${if cfg.useEFIBoot then ''
-                cp ${pkgs.OVMF-CSM.fd}/FV/OVMF.fd $bootFlash
-                chmod 0644 $bootFlash
+                efiVars=$out/efi-vars.fd
+                cp ${efiVarsDefault} $efiVars
+                chmod 0644 $efiVars
               '' else ''
               ''}
             '';
           buildInputs = [ pkgs.utillinux ];
-          QEMU_OPTS = if cfg.useEFIBoot
-                      then "-pflash $out/bios.bin -nographic -serial pty"
-                      else "-nographic -serial pty";
+          QEMU_OPTS = "-nographic -serial stdio -monitor none"
+                      + lib.optionalString cfg.useEFIBoot (
+                        " -drive if=pflash,format=raw,unit=0,readonly=on,file=${efiFirmware}"
+                      + " -drive if=pflash,format=raw,unit=1,file=$efiVars");
         }
         ''
-          # Create a /boot EFI partition with 40M and arbitrary but fixed GUIDs for reproducibility
+          # Create a /boot EFI partition with 60M and arbitrary but fixed GUIDs for reproducibility
           ${pkgs.gptfdisk}/bin/sgdisk \
             --set-alignment=1 --new=1:34:2047 --change-name=1:BIOSBootPartition --typecode=1:ef02 \
             --set-alignment=512 --largest-new=2 --change-name=2:EFISystem --typecode=2:ef00 \
@@ -172,6 +207,19 @@ let
             --partition-guid=2:970C694F-AFD0-4B99-B750-CDB7A329AB6F \
             --hybrid 2 \
             --recompute-chs /dev/vda
+
+          ${optionalString (config.boot.loader.grub.device != "/dev/vda")
+            # In this throwaway VM, we only have the /dev/vda disk, but the
+            # actual VM described by `config` (used by `switch-to-configuration`
+            # below) may set `boot.loader.grub.device` to a different device
+            # that's nonexistent in the throwaway VM.
+            # Create a symlink for that device, so that the `grub-install`
+            # by `switch-to-configuration` will hit /dev/vda anyway.
+            ''
+              ln -s /dev/vda ${config.boot.loader.grub.device}
+            ''
+          }
+
           ${pkgs.dosfstools}/bin/mkfs.fat -F16 /dev/vda2
           export MTOOLS_SKIP_CHECK=1
           ${pkgs.mtools}/bin/mlabel -i /dev/vda2 ::boot
@@ -185,6 +233,10 @@ let
           mkdir /boot
           mount /dev/vda2 /boot
 
+          ${optionalString config.boot.loader.efi.canTouchEfiVariables ''
+            mount -t efivarfs efivarfs /sys/firmware/efi/efivars
+          ''}
+
           # This is needed for GRUB 0.97, which doesn't know about virtio devices.
           mkdir /boot/grub
           echo '(hd0) /dev/vda' > /boot/grub/device.map
@@ -212,7 +264,6 @@ in
 {
   imports = [
     ../profiles/qemu-guest.nix
-   ./docker-preloader.nix
   ];
 
   options = {
@@ -237,7 +288,7 @@ in
 
     virtualisation.diskImage =
       mkOption {
-        default = "./${vmName}.qcow2";
+        default = "./${config.system.name}.qcow2";
         description =
           ''
             Path to the disk image containing the root filesystem.
@@ -396,6 +447,7 @@ in
         mkOption {
           type = types.listOf (types.submodule driveOpts);
           description = "Drives passed to qemu.";
+          apply = addDeviceNames;
         };
 
       diskInterface =
@@ -441,11 +493,53 @@ in
           '';
       };
 
+    virtualisation.efiVars =
+      mkOption {
+        default = "./${config.system.name}-efi-vars.fd";
+        description =
+          ''
+            Path to nvram image containing UEFI variables.  The will be created
+            on startup if it does not exist.
+          '';
+      };
+
+    virtualisation.bios =
+      mkOption {
+        default = null;
+        type = types.nullOr types.package;
+        description =
+          ''
+            An alternate BIOS (such as <package>qboot</package>) with which to start the VM.
+            Should contain a file named <literal>bios.bin</literal>.
+            If <literal>null</literal>, QEMU's builtin SeaBIOS will be used.
+          '';
+      };
+
   };
 
   config = {
 
-    boot.loader.grub.device = mkVMOverride cfg.bootDevice;
+    # Note [Disk layout with `useBootLoader`]
+    #
+    # If `useBootLoader = true`, we configure 2 drives:
+    # `/dev/?da` for the root disk, and `/dev/?db` for the boot disk
+    # which has the `/boot` partition and the boot loader.
+    # Concretely:
+    #
+    # * The second drive's image `disk.img` is created in `bootDisk = ...`
+    #   using a throwaway VM. Note that there the disk is always `/dev/vda`,
+    #   even though in the final VM it will be at `/dev/*b`.
+    # * The disks are attached in `virtualisation.qemu.drives`.
+    #   Their order makes them appear as devices `a`, `b`, etc.
+    # * `fileSystems."/boot"` is adjusted to be on device `b`.
+
+    # If `useBootLoader`, GRUB goes to the second disk, see
+    # note [Disk layout with `useBootLoader`].
+    boot.loader.grub.device = mkVMOverride (
+      if cfg.useBootLoader
+        then driveDeviceName 2 # second disk
+        else cfg.bootDevice
+    );
 
     boot.initrd.extraUtilsCommands =
       ''
@@ -500,8 +594,7 @@ in
       optional cfg.writableStore "overlay"
       ++ optional (cfg.qemu.diskInterface == "scsi") "sym53c8xx";
 
-    virtualisation.bootDevice =
-      mkDefault (if cfg.qemu.diskInterface == "scsi" then "/dev/sda" else "/dev/vda");
+    virtualisation.bootDevice = mkDefault (driveDeviceName 1);
 
     virtualisation.pathsInNixDB = [ config.system.build.toplevel ];
 
@@ -519,7 +612,11 @@ in
         ''-append "$(cat ${config.system.build.toplevel}/kernel-params) init=${config.system.build.toplevel}/init regInfo=${regInfo}/registration ${consoles} $QEMU_KERNEL_PARAMS"''
       ])
       (mkIf cfg.useEFIBoot [
-        "-pflash $TMPDIR/bios.bin"
+        "-drive if=pflash,format=raw,unit=0,readonly,file=${efiFirmware}"
+        "-drive if=pflash,format=raw,unit=1,file=$NIX_EFI_VARS"
+      ])
+      (mkIf (cfg.bios != null) [
+        "-bios ${cfg.bios}/bios.bin"
       ])
       (mkIf (!cfg.graphics) [
         "-nographic"
@@ -527,25 +624,22 @@ in
     ];
 
     virtualisation.qemu.drives = mkMerge [
+      [{
+        name = "root";
+        file = "$NIX_DISK_IMAGE";
+        driveExtraOpts.cache = "writeback";
+        driveExtraOpts.werror = "report";
+      }]
       (mkIf cfg.useBootLoader [
+        # The order of this list determines the device names, see
+        # note [Disk layout with `useBootLoader`].
         {
-          file = "$NIX_DISK_IMAGE";
-          driveExtraOpts.cache = "writeback";
-          driveExtraOpts.werror = "report";
-        }
-        {
+          name = "boot";
           file = "$TMPDIR/disk.img";
           driveExtraOpts.media = "disk";
           deviceExtraOpts.bootindex = "1";
         }
       ])
-      (mkIf (!cfg.useBootLoader) [
-        {
-          file = "$NIX_DISK_IMAGE";
-          driveExtraOpts.cache = "writeback";
-          driveExtraOpts.werror = "report";
-        }
-      ])
       (imap0 (idx: _: {
         file = "$(pwd)/empty${toString idx}.qcow2";
         driveExtraOpts.werror = "report";
@@ -576,7 +670,7 @@ in
         "/tmp/xchg" =
           { device = "xchg";
             fsType = "9p";
-            options = [ "trans=virtio" "version=9p2000.L" "cache=loose" ];
+            options = [ "trans=virtio" "version=9p2000.L" ];
             neededForBoot = true;
           };
         "/tmp/shared" =
@@ -593,9 +687,9 @@ in
           };
       } // optionalAttrs cfg.useBootLoader
       { "/boot" =
-          { device = "/dev/vdb2";
+          # see note [Disk layout with `useBootLoader`]
+          { device = "${lookupDriveDeviceName "boot" cfg.qemu.drives}2"; # 2 for e.g. `vdb2`, as created in `bootDisk`
             fsType = "vfat";
-            options = [ "ro" ];
             noCheck = true; # fsck fails on a r/o filesystem
           };
       });
@@ -612,7 +706,7 @@ in
       ''
         mkdir -p $out/bin
         ln -s ${config.system.build.toplevel} $out/system
-        ln -s ${pkgs.writeScript "run-nixos-vm" startVM} $out/bin/run-${vmName}-vm
+        ln -s ${pkgs.writeScript "run-nixos-vm" startVM} $out/bin/run-${config.system.name}-vm
       '';
 
     # When building a regular system configuration, override whatever
diff --git a/nixos/modules/virtualisation/railcar.nix b/nixos/modules/virtualisation/railcar.nix
index 12da1c75fc3..3f188fc68e5 100644
--- a/nixos/modules/virtualisation/railcar.nix
+++ b/nixos/modules/virtualisation/railcar.nix
@@ -29,9 +29,9 @@ let
         default = "none";
         description = ''
           The type of the filesystem to be mounted.
-          Linux: filesystem types supported by the kernel as listed in 
-          `/proc/filesystems` (e.g., "minix", "ext2", "ext3", "jfs", "xfs", 
-          "reiserfs", "msdos", "proc", "nfs", "iso9660"). For bind mounts 
+          Linux: filesystem types supported by the kernel as listed in
+          `/proc/filesystems` (e.g., "minix", "ext2", "ext3", "jfs", "xfs",
+          "reiserfs", "msdos", "proc", "nfs", "iso9660"). For bind mounts
           (when options include either bind or rbind), the type is a dummy,
           often "none" (not listed in /proc/filesystems).
         '';
@@ -45,9 +45,9 @@ let
         default = [ "bind" ];
         description = ''
           Mount options of the filesystem to be used.
-        
-          Support optoions are listed in the mount(8) man page. Note that 
-          both filesystem-independent and filesystem-specific options 
+
+          Support options are listed in the mount(8) man page. Note that
+          both filesystem-independent and filesystem-specific options
           are listed.
         '';
       };
diff --git a/nixos/modules/virtualisation/rkt.nix b/nixos/modules/virtualisation/rkt.nix
deleted file mode 100644
index fd662b52df5..00000000000
--- a/nixos/modules/virtualisation/rkt.nix
+++ /dev/null
@@ -1,64 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-  cfg = config.virtualisation.rkt;
-in
-{
-  options.virtualisation.rkt = {
-    enable = mkEnableOption "rkt metadata service";
-
-    gc = {
-      automatic = mkOption {
-        default = true;
-        type = types.bool;
-        description = "Automatically run the garbage collector at a specific time.";
-      };
-
-      dates = mkOption {
-        default = "03:15";
-        type = types.str;
-        description = ''
-          Specification (in the format described by
-          <citerefentry><refentrytitle>systemd.time</refentrytitle>
-          <manvolnum>7</manvolnum></citerefentry>) of the time at
-          which the garbage collector will run.
-        '';
-      };
-
-      options = mkOption {
-        default = "--grace-period=24h";
-        type = types.str;
-        description = ''
-          Options given to <filename>rkt gc</filename> when the
-          garbage collector is run automatically.
-        '';
-      };
-    };
-  };
-
-  config = mkIf cfg.enable {
-    environment.systemPackages = [ pkgs.rkt ];
-
-    systemd.services.rkt = {
-      description = "rkt metadata service";
-      wantedBy = [ "multi-user.target" ];
-      after = [ "network.target" ];
-      serviceConfig = {
-        ExecStart = "${pkgs.rkt}/bin/rkt metadata-service";
-      };
-    };
-
-    systemd.services.rkt-gc = {
-      description = "rkt garbage collection";
-      startAt = optionalString cfg.gc.automatic cfg.gc.dates;
-      serviceConfig = {
-        Type = "oneshot";
-        ExecStart = "${pkgs.rkt}/bin/rkt gc ${cfg.gc.options}";
-      };
-    };
-
-    users.groups.rkt = {};
-  };
-}
diff --git a/nixos/modules/virtualisation/virtualbox-guest.nix b/nixos/modules/virtualisation/virtualbox-guest.nix
index 834b994e92d..486951983d3 100644
--- a/nixos/modules/virtualisation/virtualbox-guest.nix
+++ b/nixos/modules/virtualisation/virtualbox-guest.nix
@@ -68,7 +68,7 @@ in
         SUBSYSTEM=="misc", KERNEL=="vboxguest", TAG+="systemd"
       '';
   } (mkIf cfg.x11 {
-    services.xserver.videoDrivers = mkOverride 50 [ "virtualbox" "modesetting" ];
+    services.xserver.videoDrivers = mkOverride 50 [ "vmware" "virtualbox" "modesetting" ];
 
     services.xserver.config =
       ''
diff --git a/nixos/modules/virtualisation/virtualbox-image.nix b/nixos/modules/virtualisation/virtualbox-image.nix
index 788b4d9d976..fa580e8b42d 100644
--- a/nixos/modules/virtualisation/virtualbox-image.nix
+++ b/nixos/modules/virtualisation/virtualbox-image.nix
@@ -58,6 +58,35 @@ in {
           Run <literal>VBoxManage modifyvm --help</literal> to see more options.
         '';
      };
+      extraDisk = mkOption {
+        description = ''
+          Optional extra disk/hdd configuration.
+          The disk will be an 'ext4' partition on a separate VMDK file.
+        '';
+        default = null;
+        example = {
+          label = "storage";
+          mountPoint = "/home/demo/storage";
+          size = 100 * 1024;
+        };
+        type = types.nullOr (types.submodule {
+          options = {
+            size = mkOption {
+              type = types.int;
+              description = "Size in MiB";
+            };
+            label = mkOption {
+              type = types.str;
+              default = "vm-extra-storage";
+              description = "Label for the disk partition";
+            };
+            mountPoint = mkOption {
+              type = types.str;
+              description = "Path where to mount this disk.";
+            };
+          };
+        });
+      };
     };
   };
 
@@ -72,6 +101,7 @@ in {
         audiocontroller = "ac97";
         audio = "alsa";
         audioout = "on";
+        graphicscontroller = "vmsvga";
         rtcuseutc = "on";
         usb = "on";
         usbehci = "on";
@@ -95,6 +125,20 @@ in {
           echo "creating VirtualBox pass-through disk wrapper (no copying involved)..."
           VBoxManage internalcommands createrawvmdk -filename disk.vmdk -rawdisk $diskImage
 
+          ${optionalString (cfg.extraDisk != null) ''
+            echo "creating extra disk: data-disk.raw"
+            dataDiskImage=data-disk.raw
+            truncate -s ${toString cfg.extraDisk.size}M $dataDiskImage
+
+            parted --script $dataDiskImage -- \
+              mklabel msdos \
+              mkpart primary ext4 1MiB -1
+            eval $(partx $dataDiskImage -o START,SECTORS --nr 1 --pairs)
+            mkfs.ext4 -F -L ${cfg.extraDisk.label} $dataDiskImage -E offset=$(sectorsToBytes $START) $(sectorsToKilobytes $SECTORS)K
+            echo "creating extra disk: data-disk.vmdk"
+            VBoxManage internalcommands createrawvmdk -filename data-disk.vmdk -rawdisk $dataDiskImage
+          ''}
+
           echo "creating VirtualBox VM..."
           vmName="${cfg.vmName}";
           VBoxManage createvm --name "$vmName" --register \
@@ -105,6 +149,10 @@ in {
           VBoxManage storagectl "$vmName" --name SATA --add sata --portcount 4 --bootable on --hostiocache on
           VBoxManage storageattach "$vmName" --storagectl SATA --port 0 --device 0 --type hdd \
             --medium disk.vmdk
+          ${optionalString (cfg.extraDisk != null) ''
+            VBoxManage storageattach "$vmName" --storagectl SATA --port 1 --device 0 --type hdd \
+            --medium data-disk.vmdk
+          ''}
 
           echo "exporting VirtualBox VM..."
           mkdir -p $out
@@ -118,11 +166,19 @@ in {
         '';
     };
 
-    fileSystems."/" = {
-      device = "/dev/disk/by-label/nixos";
-      autoResize = true;
-      fsType = "ext4";
-    };
+    fileSystems = {
+      "/" = {
+        device = "/dev/disk/by-label/nixos";
+        autoResize = true;
+        fsType = "ext4";
+      };
+    } // (lib.optionalAttrs (cfg.extraDisk != null) {
+      ${cfg.extraDisk.mountPoint} = {
+        device = "/dev/disk/by-label/" + cfg.extraDisk.label;
+        autoResize = true;
+        fsType = "ext4";
+      };
+    });
 
     boot.growPartition = true;
     boot.loader.grub.device = "/dev/sda";
diff --git a/nixos/modules/virtualisation/vmware-image.nix b/nixos/modules/virtualisation/vmware-image.nix
new file mode 100644
index 00000000000..9da9e145f7a
--- /dev/null
+++ b/nixos/modules/virtualisation/vmware-image.nix
@@ -0,0 +1,90 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  boolToStr = value: if value then "on" else "off";
+  cfg = config.vmware;
+
+  subformats = [
+    "monolithicSparse"
+    "monolithicFlat"
+    "twoGbMaxExtentSparse"
+    "twoGbMaxExtentFlat"
+    "streamOptimized"
+  ];
+
+in {
+  options = {
+    vmware = {
+      baseImageSize = mkOption {
+        type = types.int;
+        default = 2048;
+        description = ''
+          The size of the VMWare base image in MiB.
+        '';
+      };
+      vmDerivationName = mkOption {
+        type = types.str;
+        default = "nixos-vmware-${config.system.nixos.label}-${pkgs.stdenv.hostPlatform.system}";
+        description = ''
+          The name of the derivation for the VMWare appliance.
+        '';
+      };
+      vmFileName = mkOption {
+        type = types.str;
+        default = "nixos-${config.system.nixos.label}-${pkgs.stdenv.hostPlatform.system}.vmdk";
+        description = ''
+          The file name of the VMWare appliance.
+        '';
+      };
+      vmSubformat = mkOption {
+        type = types.enum subformats;
+        default = "monolithicSparse";
+        description = "Specifies which VMDK subformat to use.";
+      };
+      vmCompat6 = mkOption {
+        type = types.bool;
+        default = false;
+        example = true;
+        description = "Create a VMDK version 6 image (instead of version 4).";
+      };
+    };
+  };
+
+  config = {
+    system.build.vmwareImage = import ../../lib/make-disk-image.nix {
+      name = cfg.vmDerivationName;
+      postVM = ''
+        ${pkgs.vmTools.qemu}/bin/qemu-img convert -f raw -o compat6=${boolToStr cfg.vmCompat6},subformat=${cfg.vmSubformat} -O vmdk $diskImage $out/${cfg.vmFileName}
+        rm $diskImage
+      '';
+      format = "raw";
+      diskSize = cfg.baseImageSize;
+      partitionTableType = "efi";
+      inherit config lib pkgs;
+    };
+
+    fileSystems."/" = {
+      device = "/dev/disk/by-label/nixos";
+      autoResize = true;
+      fsType = "ext4";
+    };
+
+    fileSystems."/boot" = {
+      device = "/dev/disk/by-label/ESP";
+      fsType = "vfat";
+    };
+
+    boot.growPartition = true;
+
+    boot.loader.grub = {
+      version = 2;
+      device = "nodev";
+      efiSupport = true;
+      efiInstallAsRemovable = true;
+    };
+
+    virtualisation.vmware.guest.enable = true;
+  };
+}
diff --git a/nixos/release.nix b/nixos/release.nix
index cf16986b213..1f5c1581269 100644
--- a/nixos/release.nix
+++ b/nixos/release.nix
@@ -310,6 +310,11 @@ in rec {
         services.xserver.desktopManager.gnome3.enable = true;
       });
 
+    pantheon = makeClosure ({ ... }:
+      { services.xserver.enable = true;
+        services.xserver.desktopManager.pantheon.enable = true;
+      });
+
     # Linux/Apache/PostgreSQL/PHP stack.
     lapp = makeClosure ({ pkgs, ... }:
       { services.httpd.enable = true;
diff --git a/nixos/tests/acme.nix b/nixos/tests/acme.nix
index fc41dc1eb5f..a8188473721 100644
--- a/nixos/tests/acme.nix
+++ b/nixos/tests/acme.nix
@@ -48,10 +48,9 @@ in import ./make-test-python.nix ({ lib, ... }: {
       security.acme.certs."standalone.test" = {
         webroot = "/var/lib/acme/acme-challenges";
       };
-      systemd.targets."acme-finished-standalone.test" = {};
-      systemd.services."acme-standalone.test" = {
-        wants = [ "acme-finished-standalone.test.target" ];
-        before = [ "acme-finished-standalone.test.target" ];
+      systemd.targets."acme-finished-standalone.test" = {
+        after = [ "acme-standalone.test.service" ];
+        wantedBy = [ "acme-standalone.test.service" ];
       };
       services.nginx.enable = true;
       services.nginx.virtualHosts."standalone.test" = {
@@ -68,11 +67,9 @@ in import ./make-test-python.nix ({ lib, ... }: {
 
       # A target remains active. Use this to probe the fact that
       # a service fired eventhough it is not RemainAfterExit
-      systemd.targets."acme-finished-a.example.test" = {};
-      systemd.services."acme-a.example.test" = {
-        wants = [ "acme-finished-a.example.test.target" ];
-        before = [ "acme-finished-a.example.test.target" ];
-        after = [ "nginx.service" ];
+      systemd.targets."acme-finished-a.example.test" = {
+        after = [ "acme-a.example.test.service" ];
+        wantedBy = [ "acme-a.example.test.service" ];
       };
 
       services.nginx.enable = true;
@@ -89,11 +86,9 @@ in import ./make-test-python.nix ({ lib, ... }: {
       security.acme.server = "https://acme.test/dir";
 
       specialisation.second-cert.configuration = {pkgs, ...}: {
-        systemd.targets."acme-finished-b.example.test" = {};
-        systemd.services."acme-b.example.test" = {
-          wants = [ "acme-finished-b.example.test.target" ];
-          before = [ "acme-finished-b.example.test.target" ];
-          after = [ "nginx.service" ];
+        systemd.targets."acme-finished-b.example.test" = {
+          after = [ "acme-b.example.test.service" ];
+          wantedBy = [ "acme-b.example.test.service" ];
         };
         services.nginx.virtualHosts."b.example.test" = {
           enableACME = true;
@@ -104,6 +99,7 @@ in import ./make-test-python.nix ({ lib, ... }: {
           '';
         };
       };
+
       specialisation.dns-01.configuration = {pkgs, config, nodes, lib, ...}: {
         security.acme.certs."example.test" = {
           domain = "*.example.test";
@@ -115,10 +111,12 @@ in import ./make-test-python.nix ({ lib, ... }: {
           user = config.services.nginx.user;
           group = config.services.nginx.group;
         };
-        systemd.targets."acme-finished-example.test" = {};
+        systemd.targets."acme-finished-example.test" = {
+          after = [ "acme-example.test.service" ];
+          wantedBy = [ "acme-example.test.service" ];
+        };
         systemd.services."acme-example.test" = {
-          wants = [ "acme-finished-example.test.target" ];
-          before = [ "acme-finished-example.test.target" "nginx.service" ];
+          before = [ "nginx.service" ];
           wantedBy = [ "nginx.service" ];
         };
         services.nginx.virtualHosts."c.example.test" = {
@@ -132,6 +130,26 @@ in import ./make-test-python.nix ({ lib, ... }: {
           '';
         };
       };
+
+      # When nginx depends on a service that is slow to start up, requesting used to fail
+      # certificates fail.  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";
+        };
+        systemd.targets."acme-finished-d.example.com" = {
+          after = [ "acme-d.example.com.service" ];
+          wantedBy = [ "acme-d.example.com.service" ];
+        };
+        services.nginx.virtualHosts."d.example.com" = {
+          forceSSL = true;
+          enableACME = true;
+          locations."/".proxyPass = "http://localhost:8000";
+        };
+      };
     };
 
     client = {nodes, lib, ...}: {
@@ -207,5 +225,15 @@ in import ./make-test-python.nix ({ lib, ... }: {
           client.succeed(
               "curl --cacert /tmp/ca.crt https://c.example.test/ | grep -qF 'hello world'"
           )
+
+      with subtest("Can request certificate of nginx when startup is delayed"):
+          webserver.succeed(
+              "${switchToNewServer}"
+          )
+          webserver.succeed(
+              "/run/current-system/specialisation/slow-startup/bin/switch-to-configuration test"
+          )
+          webserver.wait_for_unit("acme-finished-d.example.com.target")
+          client.succeed("curl --cacert /tmp/ca.crt https://d.example.com/")
     '';
 })
diff --git a/nixos/tests/agda.nix b/nixos/tests/agda.nix
new file mode 100644
index 00000000000..e158999e57d
--- /dev/null
+++ b/nixos/tests/agda.nix
@@ -0,0 +1,41 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+let
+  hello-world = pkgs.writeText "hello-world" ''
+    open import IO
+
+    main = run(putStrLn "Hello World!")
+  '';
+in
+{
+  name = "agda";
+  meta = with pkgs.stdenv.lib.maintainers; {
+    maintainers = [ alexarice turion ];
+  };
+
+  machine = { pkgs, ... }: {
+    environment.systemPackages = [
+      (pkgs.agda.withPackages {
+        pkgs = p: [ p.standard-library ];
+      })
+    ];
+    virtualisation.memorySize = 2000; # Agda uses a lot of memory
+  };
+
+  testScript = ''
+    # Minimal script that typechecks
+    machine.succeed("touch TestEmpty.agda")
+    machine.succeed("agda TestEmpty.agda")
+
+    # Minimal script that actually uses the standard library
+    machine.succeed('echo "import IO" > TestIO.agda')
+    machine.succeed("agda -l standard-library -i . TestIO.agda")
+
+    # # Hello world
+    machine.succeed(
+        "cp ${hello-world} HelloWorld.agda"
+    )
+    machine.succeed("agda -l standard-library -i . -c HelloWorld.agda")
+  '';
+}
+)
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 5a0c9d1afae..7b8e1b2b56d 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -23,14 +23,19 @@ in
 {
   _3proxy = handleTest ./3proxy.nix {};
   acme = handleTest ./acme.nix {};
+  agda = handleTest ./agda.nix {};
   atd = handleTest ./atd.nix {};
   avahi = handleTest ./avahi.nix {};
   babeld = handleTest ./babeld.nix {};
+  bazarr = handleTest ./bazarr.nix {};
   bcachefs = handleTestOn ["x86_64-linux"] ./bcachefs.nix {}; # linux-4.18.2018.10.12 is unsupported on aarch64
   beanstalkd = handleTest ./beanstalkd.nix {};
   bees = handleTest ./bees.nix {};
   bind = handleTest ./bind.nix {};
+  bitcoind = handleTest ./bitcoind.nix {};
   bittorrent = handleTest ./bittorrent.nix {};
+  bitwarden = handleTest ./bitwarden.nix {};
+  blockbook-frontend = handleTest ./blockbook-frontend.nix {};
   buildkite-agents = handleTest ./buildkite-agents.nix {};
   boot = handleTestOn ["x86_64-linux"] ./boot.nix {}; # syslinux is unsupported on aarch64
   boot-stage1 = handleTest ./boot-stage1.nix {};
@@ -61,38 +66,43 @@ in
   containers-macvlans = handleTest ./containers-macvlans.nix {};
   containers-physical_interfaces = handleTest ./containers-physical_interfaces.nix {};
   containers-portforward = handleTest ./containers-portforward.nix {};
+  containers-reloadable = handleTest ./containers-reloadable.nix {};
   containers-restart_networking = handleTest ./containers-restart_networking.nix {};
   containers-tmpfs = handleTest ./containers-tmpfs.nix {};
+  convos = handleTest ./convos.nix {};
   corerad = handleTest ./corerad.nix {};
   couchdb = handleTest ./couchdb.nix {};
+  cri-o = handleTestOn ["x86_64-linux"] ./cri-o.nix {};
   deluge = handleTest ./deluge.nix {};
   dhparams = handleTest ./dhparams.nix {};
   dnscrypt-proxy2 = handleTestOn ["x86_64-linux"] ./dnscrypt-proxy2.nix {};
+  dnscrypt-wrapper = handleTestOn ["x86_64-linux"] ./dnscrypt-wrapper {};
   doas = handleTest ./doas.nix {};
   docker = handleTestOn ["x86_64-linux"] ./docker.nix {};
   oci-containers = handleTestOn ["x86_64-linux"] ./oci-containers.nix {};
   docker-edge = handleTestOn ["x86_64-linux"] ./docker-edge.nix {};
-  docker-preloader = handleTestOn ["x86_64-linux"] ./docker-preloader.nix {};
   docker-registry = handleTest ./docker-registry.nix {};
   docker-tools = handleTestOn ["x86_64-linux"] ./docker-tools.nix {};
   docker-tools-overlay = handleTestOn ["x86_64-linux"] ./docker-tools-overlay.nix {};
   documize = handleTest ./documize.nix {};
   dokuwiki = handleTest ./dokuwiki.nix {};
   dovecot = handleTest ./dovecot.nix {};
-  # ec2-config doesn't work in a sandbox as the simulated ec2 instance needs network access
-  #ec2-config = (handleTestOn ["x86_64-linux"] ./ec2.nix {}).boot-ec2-config or {};
+  ec2-config = (handleTestOn ["x86_64-linux"] ./ec2.nix {}).boot-ec2-config or {};
   ec2-nixops = (handleTestOn ["x86_64-linux"] ./ec2.nix {}).boot-ec2-nixops or {};
   ecryptfs = handleTest ./ecryptfs.nix {};
   ejabberd = handleTest ./xmpp/ejabberd.nix {};
   elk = handleTestOn ["x86_64-linux"] ./elk.nix {};
+  engelsystem = handleTest ./engelsystem.nix {};
   enlightenment = handleTest ./enlightenment.nix {};
   env = handleTest ./env.nix {};
+  ergo = handleTest ./ergo.nix {};
   etcd = handleTestOn ["x86_64-linux"] ./etcd.nix {};
   etcd-cluster = handleTestOn ["x86_64-linux"] ./etcd-cluster.nix {};
   fancontrol = handleTest ./fancontrol.nix {};
   ferm = handleTest ./ferm.nix {};
   firefox = handleTest ./firefox.nix {};
   firefox-esr = handleTest ./firefox.nix { esr = true; };
+  firejail = handleTest ./firejail.nix {};
   firewall = handleTest ./firewall.nix {};
   fish = handleTest ./fish.nix {};
   flannel = handleTestOn ["x86_64-linux"] ./flannel.nix {};
@@ -114,10 +124,12 @@ in
   installed-tests = pkgs.recurseIntoAttrs (handleTest ./installed-tests {});
   gocd-agent = handleTest ./gocd-agent.nix {};
   gocd-server = handleTest ./gocd-server.nix {};
+  go-neb = handleTest ./go-neb.nix {};
   google-oslogin = handleTest ./google-oslogin {};
   grafana = handleTest ./grafana.nix {};
   graphite = handleTest ./graphite.nix {};
   graylog = handleTest ./graylog.nix {};
+  grub = handleTest ./grub.nix {};
   gvisor = handleTest ./gvisor.nix {};
   hadoop.hdfs = handleTestOn [ "x86_64-linux" ] ./hadoop/hdfs.nix {};
   hadoop.yarn = handleTestOn [ "x86_64-linux" ] ./hadoop/yarn.nix {};
@@ -131,6 +143,7 @@ in
   hitch = handleTest ./hitch {};
   hocker-fetchdocker = handleTest ./hocker-fetchdocker {};
   home-assistant = handleTest ./home-assistant.nix {};
+  hostname = handleTest ./hostname.nix {};
   hound = handleTest ./hound.nix {};
   hydra = handleTest ./hydra {};
   hydra-db-migration = handleTest ./hydra/db-migration.nix {};
@@ -141,6 +154,7 @@ in
   incron = handleTest ./incron.nix {};
   influxdb = handleTest ./influxdb.nix {};
   initrd-network-ssh = handleTest ./initrd-network-ssh {};
+  initrd-network-openvpn = handleTest ./initrd-network-openvpn {};
   initrdNetwork = handleTest ./initrd-network.nix {};
   installer = handleTest ./installer.nix {};
   iodine = handleTest ./iodine.nix {};
@@ -150,6 +164,7 @@ in
   jellyfin = handleTest ./jellyfin.nix {};
   jenkins = handleTest ./jenkins.nix {};
   jirafeau = handleTest ./jirafeau.nix {};
+  jitsi-meet = handleTest ./jitsi-meet.nix {};
   k3s = handleTest ./k3s.nix {};
   kafka = handleTest ./kafka.nix {};
   keepalived = handleTest ./keepalived.nix {};
@@ -172,6 +187,8 @@ in
   limesurvey = handleTest ./limesurvey.nix {};
   login = handleTest ./login.nix {};
   loki = handleTest ./loki.nix {};
+  lxd = handleTest ./lxd.nix {};
+  lxd-nftables = handleTest ./lxd-nftables.nix {};
   #logstash = handleTest ./logstash.nix {};
   lorri = handleTest ./lorri/default.nix {};
   magnetico = handleTest ./magnetico.nix {};
@@ -179,12 +196,10 @@ in
   mailcatcher = handleTest ./mailcatcher.nix {};
   mariadb-galera-mariabackup = handleTest ./mysql/mariadb-galera-mariabackup.nix {};
   mariadb-galera-rsync = handleTest ./mysql/mariadb-galera-rsync.nix {};
-  mathics = handleTest ./mathics.nix {};
   matomo = handleTest ./matomo.nix {};
   matrix-synapse = handleTest ./matrix-synapse.nix {};
   mediawiki = handleTest ./mediawiki.nix {};
   memcached = handleTest ./memcached.nix {};
-  mesos = handleTest ./mesos.nix {};
   metabase = handleTest ./metabase.nix {};
   miniflux = handleTest ./miniflux.nix {};
   minio = handleTest ./minio.nix {};
@@ -208,6 +223,7 @@ in
   nat.firewall = handleTest ./nat.nix { withFirewall = true; };
   nat.firewall-conntrack = handleTest ./nat.nix { withFirewall = true; withConntrackHelpers = true; };
   nat.standalone = handleTest ./nat.nix { withFirewall = false; };
+  ncdns = handleTest ./ncdns.nix {};
   ndppd = handleTest ./ndppd.nix {};
   neo4j = handleTest ./neo4j.nix {};
   specialisation = handleTest ./specialisation.nix {};
@@ -225,7 +241,9 @@ in
   nginx = handleTest ./nginx.nix {};
   nginx-etag = handleTest ./nginx-etag.nix {};
   nginx-pubhtml = handleTest ./nginx-pubhtml.nix {};
+  nginx-sandbox = handleTestOn ["x86_64-linux"] ./nginx-sandbox.nix {};
   nginx-sso = handleTest ./nginx-sso.nix {};
+  nginx-variants = handleTest ./nginx-variants.nix {};
   nix-ssh-serve = handleTest ./nix-ssh-serve.nix {};
   nixos-generate-config = handleTest ./nixos-generate-config.nix {};
   novacomd = handleTestOn ["x86_64-linux"] ./novacomd.nix {};
@@ -250,9 +268,12 @@ in
   pgjwt = handleTest ./pgjwt.nix {};
   pgmanage = handleTest ./pgmanage.nix {};
   php = handleTest ./php {};
+  pinnwand = handleTest ./pinnwand.nix {};
   plasma5 = handleTest ./plasma5.nix {};
   plotinus = handleTest ./plotinus.nix {};
-  podman = handleTest ./podman.nix {};
+  podman = handleTestOn ["x86_64-linux"] ./podman.nix {};
+  postfix = handleTest ./postfix.nix {};
+  postfix-raise-smtpd-tls-security-level = handleTest ./postfix-raise-smtpd-tls-security-level.nix {};
   postgis = handleTest ./postgis.nix {};
   postgresql = handleTest ./postgresql.nix {};
   postgresql-wal-receiver = handleTest ./postgresql-wal-receiver.nix {};
@@ -260,11 +281,14 @@ in
   pppd = handleTest ./pppd.nix {};
   predictable-interface-names = handleTest ./predictable-interface-names.nix {};
   printing = handleTest ./printing.nix {};
+  privacyidea = handleTest ./privacyidea.nix {};
   prometheus = handleTest ./prometheus.nix {};
   prometheus-exporters = handleTest ./prometheus-exporters.nix {};
   prosody = handleTest ./xmpp/prosody.nix {};
   prosodyMysql = handleTest ./xmpp/prosody-mysql.nix {};
   proxy = handleTest ./proxy.nix {};
+  pt2-clone = handleTest ./pt2-clone.nix {};
+  qboot = handleTestOn ["x86_64-linux" "i686-linux"] ./qboot.nix {};
   quagga = handleTest ./quagga.nix {};
   quorum = handleTest ./quorum.nix {};
   rabbitmq = handleTest ./rabbitmq.nix {};
@@ -283,25 +307,31 @@ in
   sanoid = handleTest ./sanoid.nix {};
   sddm = handleTest ./sddm.nix {};
   service-runner = handleTest ./service-runner.nix {};
+  shattered-pixel-dungeon = handleTest ./shattered-pixel-dungeon.nix {};
   shiori = handleTest ./shiori.nix {};
   signal-desktop = handleTest ./signal-desktop.nix {};
   simple = handleTest ./simple.nix {};
   slurm = handleTest ./slurm.nix {};
   smokeping = handleTest ./smokeping.nix {};
+  snapcast = handleTest ./snapcast.nix {};
   snapper = handleTest ./snapper.nix {};
+  sogo = handleTest ./sogo.nix {};
   solr = handleTest ./solr.nix {};
   spacecookie = handleTest ./spacecookie.nix {};
   spike = handleTest ./spike.nix {};
   sonarr = handleTest ./sonarr.nix {};
+  sslh = handleTest ./sslh.nix {};
   strongswan-swanctl = handleTest ./strongswan-swanctl.nix {};
   sudo = handleTest ./sudo.nix {};
   switchTest = handleTest ./switch-test.nix {};
   sympa = handleTest ./sympa.nix {};
+  syncthing = handleTest ./syncthing.nix {};
   syncthing-init = handleTest ./syncthing-init.nix {};
   syncthing-relay = handleTest ./syncthing-relay.nix {};
   systemd = handleTest ./systemd.nix {};
   systemd-analyze = handleTest ./systemd-analyze.nix {};
-  systemd-boot = handleTestOn ["x86_64-linux"] ./systemd-boot.nix {};
+  systemd-binfmt = handleTestOn ["x86_64-linux"] ./systemd-binfmt.nix {};
+  systemd-boot = handleTest ./systemd-boot.nix {};
   systemd-confinement = handleTest ./systemd-confinement.nix {};
   systemd-timesyncd = handleTest ./systemd-timesyncd.nix {};
   systemd-networkd-vrf = handleTest ./systemd-networkd-vrf.nix {};
@@ -331,10 +361,8 @@ in
   vault = handleTest ./vault.nix {};
   victoriametrics = handleTest ./victoriametrics.nix {};
   virtualbox = handleTestOn ["x86_64-linux"] ./virtualbox.nix {};
-  wg-quick = handleTest ./wireguard/wg-quick.nix {};
+  wasabibackend = handleTest ./wasabibackend.nix {};
   wireguard = handleTest ./wireguard {};
-  wireguard-generated = handleTest ./wireguard/generated.nix {};
-  wireguard-namespaces = handleTest ./wireguard/namespaces.nix {};
   wordpress = handleTest ./wordpress.nix {};
   xandikos = handleTest ./xandikos.nix {};
   xautolock = handleTest ./xautolock.nix {};
@@ -345,6 +373,8 @@ in
   yabar = handleTest ./yabar.nix {};
   yggdrasil = handleTest ./yggdrasil.nix {};
   zfs = handleTest ./zfs.nix {};
-  zsh-history = handleTest ./zsh-history.nix {};
+  zigbee2mqtt = handleTest ./zigbee2mqtt.nix {};
+  zoneminder = handleTest ./zoneminder.nix {};
   zookeeper = handleTest ./zookeeper.nix {};
+  zsh-history = handleTest ./zsh-history.nix {};
 }
diff --git a/nixos/tests/bazarr.nix b/nixos/tests/bazarr.nix
new file mode 100644
index 00000000000..b8cd8ef38b4
--- /dev/null
+++ b/nixos/tests/bazarr.nix
@@ -0,0 +1,26 @@
+import ./make-test-python.nix ({ lib, ... }:
+
+with lib;
+
+let
+  port = 42069;
+in
+{
+  name = "bazarr";
+  meta.maintainers = with maintainers; [ xwvvvvwx ];
+
+  nodes.machine =
+    { pkgs, ... }:
+    {
+      services.bazarr = {
+        enable = true;
+        listenPort = port;
+      };
+    };
+
+  testScript = ''
+    machine.wait_for_unit("bazarr.service")
+    machine.wait_for_open_port("${toString port}")
+    machine.succeed("curl --fail http://localhost:${toString port}/")
+  '';
+})
diff --git a/nixos/tests/bcachefs.nix b/nixos/tests/bcachefs.nix
index 0541e580322..3f116d7df92 100644
--- a/nixos/tests/bcachefs.nix
+++ b/nixos/tests/bcachefs.nix
@@ -13,7 +13,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     machine.succeed("modprobe bcachefs")
     machine.succeed("bcachefs version")
     machine.succeed("ls /dev")
-    
+
     machine.succeed(
         "mkdir /tmp/mnt",
         "udevadm settle",
diff --git a/nixos/tests/bitcoind.nix b/nixos/tests/bitcoind.nix
new file mode 100644
index 00000000000..09f3e4a6ec0
--- /dev/null
+++ b/nixos/tests/bitcoind.nix
@@ -0,0 +1,46 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "bitcoind";
+  meta = with pkgs.stdenv.lib; {
+    maintainers = with maintainers; [ _1000101 ];
+  };
+
+  machine = { ... }: {
+    services.bitcoind."mainnet" = {
+      enable = true;
+      rpc = {
+        port = 8332;
+        users.rpc.passwordHMAC = "acc2374e5f9ba9e62a5204d3686616cf$53abdba5e67a9005be6a27ca03a93ce09e58854bc2b871523a0d239a72968033";
+        users.rpc2.passwordHMAC = "1495e4a3ad108187576c68f7f9b5ddc5$accce0881c74aa01bb8960ff3bdbd39f607fd33178147679e055a4ac35f53225";
+      };
+    };
+    services.bitcoind."testnet" = {
+      enable = true;
+      configFile = "/test.blank";
+      testnet = true;
+      rpc = {
+        port = 18332;
+      };
+      extraCmdlineOptions = [ "-rpcuser=rpc" "-rpcpassword=rpc" "-rpcauth=rpc2:1495e4a3ad108187576c68f7f9b5ddc5$accce0881c74aa01bb8960ff3bdbd39f607fd33178147679e055a4ac35f53225" ];
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    machine.wait_for_unit("bitcoind-mainnet.service")
+    machine.wait_for_unit("bitcoind-testnet.service")
+
+    machine.wait_until_succeeds(
+        'curl --user rpc:rpc --data-binary \'{"jsonrpc": "1.0", "id":"curltest", "method": "getblockchaininfo", "params": [] }\' -H \'content-type: text/plain;\' localhost:8332 |  grep \'"chain":"main"\' '
+    )
+    machine.wait_until_succeeds(
+        'curl --user rpc2:rpc2 --data-binary \'{"jsonrpc": "1.0", "id":"curltest", "method": "getblockchaininfo", "params": [] }\' -H \'content-type: text/plain;\' localhost:8332 |  grep \'"chain":"main"\' '
+    )
+    machine.wait_until_succeeds(
+        'curl --user rpc:rpc --data-binary \'{"jsonrpc": "1.0", "id":"curltest", "method": "getblockchaininfo", "params": [] }\' -H \'content-type: text/plain;\' localhost:18332 |  grep \'"chain":"test"\' '
+    )
+    machine.wait_until_succeeds(
+        'curl --user rpc2:rpc2 --data-binary \'{"jsonrpc": "1.0", "id":"curltest", "method": "getblockchaininfo", "params": [] }\' -H \'content-type: text/plain;\' localhost:18332 |  grep \'"chain":"test"\' '
+    )
+  '';
+})
diff --git a/nixos/tests/bittorrent.nix b/nixos/tests/bittorrent.nix
index 0a97d5556a2..c195b60cd56 100644
--- a/nixos/tests/bittorrent.nix
+++ b/nixos/tests/bittorrent.nix
@@ -19,6 +19,7 @@ let
   externalClient2Address = "80.100.100.2";
   externalTrackerAddress = "80.100.100.3";
 
+  download-dir = "/var/lib/transmission/Downloads";
   transmissionConfig = { ... }: {
     environment.systemPackages = [ pkgs.transmission ];
     services.transmission = {
@@ -26,6 +27,7 @@ let
       settings = {
         dht-enabled = false;
         message-level = 3;
+        inherit download-dir;
       };
     };
   };
@@ -117,12 +119,12 @@ in
       router.wait_for_unit("miniupnpd")
 
       # Create the torrent.
-      tracker.succeed("mkdir /tmp/data")
+      tracker.succeed("mkdir ${download-dir}/data")
       tracker.succeed(
-          "cp ${file} /tmp/data/test.tar.bz2"
+          "cp ${file} ${download-dir}/data/test.tar.bz2"
       )
       tracker.succeed(
-          "transmission-create /tmp/data/test.tar.bz2 --private --tracker http://${externalTrackerAddress}:6969/announce --outfile /tmp/test.torrent"
+          "transmission-create ${download-dir}/data/test.tar.bz2 --private --tracker http://${externalTrackerAddress}:6969/announce --outfile /tmp/test.torrent"
       )
       tracker.succeed("chmod 644 /tmp/test.torrent")
 
@@ -133,18 +135,16 @@ in
 
       # Start the initial seeder.
       tracker.succeed(
-          "transmission-remote --add /tmp/test.torrent --no-portmap --no-dht --download-dir /tmp/data"
+          "transmission-remote --add /tmp/test.torrent --no-portmap --no-dht --download-dir ${download-dir}/data"
       )
 
       # Now we should be able to download from the client behind the NAT.
       tracker.wait_for_unit("httpd")
       client1.wait_for_unit("network-online.target")
+      client1.succeed("transmission-remote --add http://${externalTrackerAddress}/test.torrent >&2 &")
+      client1.wait_for_file("${download-dir}/test.tar.bz2")
       client1.succeed(
-          "transmission-remote --add http://${externalTrackerAddress}/test.torrent --download-dir /tmp >&2 &"
-      )
-      client1.wait_for_file("/tmp/test.tar.bz2")
-      client1.succeed(
-          "cmp /tmp/test.tar.bz2 ${file}"
+          "cmp ${download-dir}/test.tar.bz2 ${file}"
       )
 
       # Bring down the initial seeder.
@@ -154,11 +154,11 @@ in
       # the first client created a NAT hole in the router.
       client2.wait_for_unit("network-online.target")
       client2.succeed(
-          "transmission-remote --add http://${externalTrackerAddress}/test.torrent --no-portmap --no-dht --download-dir /tmp >&2 &"
+          "transmission-remote --add http://${externalTrackerAddress}/test.torrent --no-portmap --no-dht >&2 &"
       )
-      client2.wait_for_file("/tmp/test.tar.bz2")
+      client2.wait_for_file("${download-dir}/test.tar.bz2")
       client2.succeed(
-          "cmp /tmp/test.tar.bz2 ${file}"
+          "cmp ${download-dir}/test.tar.bz2 ${file}"
       )
     '';
 })
diff --git a/nixos/tests/bitwarden.nix b/nixos/tests/bitwarden.nix
new file mode 100644
index 00000000000..a47c77cec21
--- /dev/null
+++ b/nixos/tests/bitwarden.nix
@@ -0,0 +1,188 @@
+{ system ? builtins.currentSystem
+, config ? { }
+, pkgs ? import ../.. { inherit system config; }
+}:
+
+# These tests will:
+#  * Set up a bitwarden-rs server
+#  * Have Firefox use the web vault to create an account, log in, and save a password to the valut
+#  * Have the bw cli log in and read that password from the vault
+#
+# Note that Firefox must be on the same machine as the server for WebCrypto APIs to be available (or HTTPS must be configured)
+#
+# The same tests should work without modification on the official bitwarden server, if we ever package that.
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+let
+  backends = [ "sqlite" "mysql" "postgresql" ];
+
+  dbPassword = "please_dont_hack";
+
+  userEmail = "meow@example.com";
+  userPassword = "also_super_secret_ZJWpBKZi668QGt"; # Must be complex to avoid interstitial warning on the signup page
+
+  storedPassword = "seeeecret";
+
+  makeBitwardenTest = backend: makeTest {
+    name = "bitwarden_rs-${backend}";
+    meta = {
+      maintainers = with pkgs.stdenv.lib.maintainers; [ jjjollyjim ];
+    };
+
+    nodes = {
+      server = { pkgs, ... }:
+        let backendConfig = {
+          mysql = {
+            services.mysql = {
+              enable = true;
+              initialScript = pkgs.writeText "mysql-init.sql" ''
+                CREATE DATABASE bitwarden;
+                CREATE USER 'bitwardenuser'@'localhost' IDENTIFIED BY '${dbPassword}';
+                GRANT ALL ON `bitwarden`.* TO 'bitwardenuser'@'localhost';
+                FLUSH PRIVILEGES;
+              '';
+              package = pkgs.mysql;
+            };
+
+            services.bitwarden_rs.config.databaseUrl = "mysql://bitwardenuser:${dbPassword}@localhost/bitwarden";
+
+            systemd.services.bitwarden_rs.after = [ "mysql.service" ];
+          };
+
+          postgresql = {
+            services.postgresql = {
+              enable = true;
+              initialScript = pkgs.writeText "postgresql-init.sql" ''
+                CREATE DATABASE bitwarden;
+                CREATE USER bitwardenuser WITH PASSWORD '${dbPassword}';
+                GRANT ALL PRIVILEGES ON DATABASE bitwarden TO bitwardenuser;
+              '';
+            };
+
+            services.bitwarden_rs.config.databaseUrl = "postgresql://bitwardenuser:${dbPassword}@localhost/bitwarden";
+
+            systemd.services.bitwarden_rs.after = [ "postgresql.service" ];
+          };
+
+          sqlite = { };
+        };
+        in
+        mkMerge [
+          backendConfig.${backend}
+          {
+            services.bitwarden_rs = {
+              enable = true;
+              dbBackend = backend;
+              config.rocketPort = 80;
+            };
+
+            networking.firewall.allowedTCPPorts = [ 80 ];
+
+            environment.systemPackages =
+              let
+                testRunner = pkgs.writers.writePython3Bin "test-runner"
+                  {
+                    libraries = [ pkgs.python3Packages.selenium ];
+                  } ''
+                  from selenium.webdriver import Firefox
+                  from selenium.webdriver.firefox.options import Options
+                  from selenium.webdriver.support.ui import WebDriverWait
+                  from selenium.webdriver.support import expected_conditions as EC
+
+                  options = Options()
+                  options.add_argument('--headless')
+                  driver = Firefox(options=options)
+
+                  driver.implicitly_wait(20)
+                  driver.get('http://localhost/#/register')
+
+                  wait = WebDriverWait(driver, 10)
+
+                  wait.until(EC.title_contains("Create Account"))
+
+                  driver.find_element_by_css_selector('input#email').send_keys(
+                    '${userEmail}'
+                  )
+                  driver.find_element_by_css_selector('input#name').send_keys(
+                    'A Cat'
+                  )
+                  driver.find_element_by_css_selector('input#masterPassword').send_keys(
+                    '${userPassword}'
+                  )
+                  driver.find_element_by_css_selector('input#masterPasswordRetype').send_keys(
+                    '${userPassword}'
+                  )
+
+                  driver.find_element_by_xpath("//button[contains(., 'Submit')]").click()
+
+                  wait.until_not(EC.title_contains("Create Account"))
+
+                  driver.find_element_by_css_selector('input#masterPassword').send_keys(
+                    '${userPassword}'
+                  )
+                  driver.find_element_by_xpath("//button[contains(., 'Log In')]").click()
+
+                  wait.until(EC.title_contains("My Vault"))
+
+                  driver.find_element_by_xpath("//button[contains(., 'Add Item')]").click()
+
+                  driver.find_element_by_css_selector('input#name').send_keys(
+                    'secrets'
+                  )
+                  driver.find_element_by_css_selector('input#loginPassword').send_keys(
+                    '${storedPassword}'
+                  )
+
+                  driver.find_element_by_xpath("//button[contains(., 'Save')]").click()
+                '';
+              in
+              [ pkgs.firefox-unwrapped pkgs.geckodriver testRunner ];
+
+            virtualisation.memorySize = 768;
+          }
+        ];
+
+      client = { pkgs, ... }:
+        {
+          environment.systemPackages = [ pkgs.bitwarden-cli ];
+        };
+    };
+
+    testScript = ''
+      start_all()
+      server.wait_for_unit("bitwarden_rs.service")
+      server.wait_for_open_port(80)
+
+      with subtest("configure the cli"):
+          client.succeed("bw --nointeraction config server http://server")
+
+      with subtest("can't login to nonexistant account"):
+          client.fail(
+              "bw --nointeraction --raw login ${userEmail} ${userPassword}"
+          )
+
+      with subtest("use the web interface to sign up, log in, and save a password"):
+          server.succeed("PYTHONUNBUFFERED=1 test-runner | systemd-cat -t test-runner")
+
+      with subtest("log in with the cli"):
+          key = client.succeed(
+              "bw --nointeraction --raw login ${userEmail} ${userPassword}"
+          ).strip()
+
+      with subtest("sync with the cli"):
+          client.succeed(f"bw --nointeraction --raw --session {key} sync -f")
+
+      with subtest("get the password with the cli"):
+          password = client.succeed(
+              f"bw --nointeraction --raw --session {key} list items | ${pkgs.jq}/bin/jq -r .[].login.password"
+          )
+          assert password.strip() == "${storedPassword}"
+    '';
+  };
+in
+builtins.listToAttrs (
+  map
+    (backend: { name = backend; value = makeBitwardenTest backend; })
+    backends
+)
diff --git a/nixos/tests/blockbook-frontend.nix b/nixos/tests/blockbook-frontend.nix
new file mode 100644
index 00000000000..742a02999e7
--- /dev/null
+++ b/nixos/tests/blockbook-frontend.nix
@@ -0,0 +1,28 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "blockbook-frontend";
+  meta = with pkgs.stdenv.lib; {
+    maintainers = with maintainers; [ _1000101 ];
+  };
+
+  machine = { ... }: {
+    services.blockbook-frontend."test" = {
+      enable = true;
+    };
+    services.bitcoind.mainnet = {
+      enable = true;
+      rpc = {
+        port = 8030;
+        users.rpc.passwordHMAC = "acc2374e5f9ba9e62a5204d3686616cf$53abdba5e67a9005be6a27ca03a93ce09e58854bc2b871523a0d239a72968033";
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+    machine.wait_for_unit("blockbook-frontend-test.service")
+
+    machine.wait_for_open_port(9030)
+
+    machine.succeed("curl -sSfL http://localhost:9030 | grep 'Blockbook'")
+  '';
+})
diff --git a/nixos/tests/borgbackup.nix b/nixos/tests/borgbackup.nix
index d97471e293e..bf37eb8607b 100644
--- a/nixos/tests/borgbackup.nix
+++ b/nixos/tests/borgbackup.nix
@@ -43,7 +43,7 @@ in {
   nodes = {
     client = { ... }: {
       services.borgbackup.jobs = {
-        
+
         local = {
           paths = dataDir;
           repo = localRepo;
diff --git a/nixos/tests/ceph-multi-node.nix b/nixos/tests/ceph-multi-node.nix
index 22fe5cada48..e26c6d5d670 100644
--- a/nixos/tests/ceph-multi-node.nix
+++ b/nixos/tests/ceph-multi-node.nix
@@ -183,15 +183,15 @@ let
     monA.wait_until_succeeds("ceph -s | grep 'HEALTH_OK'")
 
     monA.succeed(
-        "ceph osd pool create multi-node-test 128 128",
+        "ceph osd pool create multi-node-test 32 32",
         "ceph osd pool ls | grep 'multi-node-test'",
         "ceph osd pool rename multi-node-test multi-node-other-test",
         "ceph osd pool ls | grep 'multi-node-other-test'",
     )
-    monA.wait_until_succeeds("ceph -s | grep '1 pools, 128 pgs'")
+    monA.wait_until_succeeds("ceph -s | grep '2 pools, 33 pgs'")
     monA.succeed("ceph osd pool set multi-node-other-test size 2")
     monA.wait_until_succeeds("ceph -s | grep 'HEALTH_OK'")
-    monA.wait_until_succeeds("ceph -s | grep '128 active+clean'")
+    monA.wait_until_succeeds("ceph -s | grep '33 active+clean'")
     monA.fail(
         "ceph osd pool ls | grep 'multi-node-test'",
         "ceph osd pool delete multi-node-other-test multi-node-other-test --yes-i-really-really-mean-it",
diff --git a/nixos/tests/ceph-single-node.nix b/nixos/tests/ceph-single-node.nix
index 01c4b413845..98528f6317b 100644
--- a/nixos/tests/ceph-single-node.nix
+++ b/nixos/tests/ceph-single-node.nix
@@ -143,12 +143,12 @@ let
     monA.wait_until_succeeds("ceph -s | grep 'HEALTH_OK'")
 
     monA.succeed(
-        "ceph osd pool create single-node-test 128 128",
+        "ceph osd pool create single-node-test 32 32",
         "ceph osd pool ls | grep 'single-node-test'",
         "ceph osd pool rename single-node-test single-node-other-test",
         "ceph osd pool ls | grep 'single-node-other-test'",
     )
-    monA.wait_until_succeeds("ceph -s | grep '1 pools, 128 pgs'")
+    monA.wait_until_succeeds("ceph -s | grep '2 pools, 33 pgs'")
     monA.succeed(
         "ceph osd getcrushmap -o crush",
         "crushtool -d crush -o decrushed",
@@ -158,7 +158,7 @@ let
         "ceph osd pool set single-node-other-test size 2",
     )
     monA.wait_until_succeeds("ceph -s | grep 'HEALTH_OK'")
-    monA.wait_until_succeeds("ceph -s | grep '128 active+clean'")
+    monA.wait_until_succeeds("ceph -s | grep '33 active+clean'")
     monA.fail(
         "ceph osd pool ls | grep 'multi-node-test'",
         "ceph osd pool delete single-node-other-test single-node-other-test --yes-i-really-really-mean-it",
diff --git a/nixos/tests/common/auto.nix b/nixos/tests/common/auto.nix
index 2c21a8d5167..da6b14e9f16 100644
--- a/nixos/tests/common/auto.nix
+++ b/nixos/tests/common/auto.nix
@@ -41,8 +41,8 @@ in
 
   config = mkIf cfg.enable {
 
-    services.xserver.displayManager.lightdm = {
-      enable = true;
+    services.xserver.displayManager = {
+      lightdm.enable = true;
       autoLogin = {
         enable = true;
         user = cfg.user;
diff --git a/nixos/tests/common/ec2.nix b/nixos/tests/common/ec2.nix
index ba087bb6009..502fe96231f 100644
--- a/nixos/tests/common/ec2.nix
+++ b/nixos/tests/common/ec2.nix
@@ -20,30 +20,44 @@ with pkgs.lib;
     in makeTest {
       name = "ec2-" + name;
       nodes = {};
-      testScript =
-        ''
-          my $imageDir = ($ENV{'TMPDIR'} // "/tmp") . "/vm-state-machine";
-          mkdir $imageDir, 0700;
-          my $diskImage = "$imageDir/machine.qcow2";
-          system("qemu-img create -f qcow2 -o backing_file=${image} $diskImage") == 0 or die;
-          system("qemu-img resize $diskImage 10G") == 0 or die;
+      testScript = ''
+        import os
+        import subprocess
 
-          # Note: we use net=169.0.0.0/8 rather than
-          # net=169.254.0.0/16 to prevent dhcpcd from getting horribly
-          # confused. (It would get a DHCP lease in the 169.254.*
-          # range, which it would then configure and prompty delete
-          # again when it deletes link-local addresses.) Ideally we'd
-          # turn off the DHCP server, but qemu does not have an option
-          # to do that.
-          my $startCommand = "qemu-kvm -m 1024";
-          $startCommand .= " -device virtio-net-pci,netdev=vlan0";
-          $startCommand .= " -netdev 'user,id=vlan0,net=169.0.0.0/8,guestfwd=tcp:169.254.169.254:80-cmd:${pkgs.micro-httpd}/bin/micro_httpd ${metaData}'";
-          $startCommand .= " -drive file=$diskImage,if=virtio,werror=report";
-          $startCommand .= " \$QEMU_OPTS";
+        image_dir = os.path.join(
+            os.environ.get("TMPDIR", tempfile.gettempdir()), "tmp", "vm-state-machine"
+        )
+        os.makedirs(image_dir, mode=0o700, exist_ok=True)
+        disk_image = os.path.join(image_dir, "machine.qcow2")
+        subprocess.check_call(
+            [
+                "qemu-img",
+                "create",
+                "-f",
+                "qcow2",
+                "-o",
+                "backing_file=${image}",
+                disk_image,
+            ]
+        )
+        subprocess.check_call(["qemu-img", "resize", disk_image, "10G"])
 
-          my $machine = createMachine({ startCommand => $startCommand });
+        # Note: we use net=169.0.0.0/8 rather than
+        # net=169.254.0.0/16 to prevent dhcpcd from getting horribly
+        # confused. (It would get a DHCP lease in the 169.254.*
+        # range, which it would then configure and prompty delete
+        # again when it deletes link-local addresses.) Ideally we'd
+        # turn off the DHCP server, but qemu does not have an option
+        # to do that.
+        start_command = (
+            "qemu-kvm -m 1024"
+            + " -device virtio-net-pci,netdev=vlan0"
+            + " -netdev 'user,id=vlan0,net=169.0.0.0/8,guestfwd=tcp:169.254.169.254:80-cmd:${pkgs.micro-httpd}/bin/micro_httpd ${metaData}'"
+            + f" -drive file={disk_image},if=virtio,werror=report"
+            + " $QEMU_OPTS"
+        )
 
-          ${script}
-        '';
+        machine = create_machine({"startCommand": start_command})
+      '' + script;
     };
 }
diff --git a/nixos/tests/consul.nix b/nixos/tests/consul.nix
index 6600dae4770..ee85f1d0b91 100644
--- a/nixos/tests/consul.nix
+++ b/nixos/tests/consul.nix
@@ -55,30 +55,33 @@ let
 
   server = index: { pkgs, ... }:
     let
-      ip = builtins.elemAt allConsensusServerHosts index;
+      numConsensusServers = builtins.length allConsensusServerHosts;
+      thisConsensusServerHost = builtins.elemAt allConsensusServerHosts index;
+      ip = thisConsensusServerHost; # since we already use IPs to identify servers
     in
       {
         networking.interfaces.eth1.ipv4.addresses = pkgs.lib.mkOverride 0 [
-          { address = builtins.elemAt allConsensusServerHosts index; prefixLength = 16; }
+          { address = ip; prefixLength = 16; }
         ];
         networking.firewall = firewallSettings;
 
         services.consul =
-          let
-            thisConsensusServerHost = builtins.elemAt allConsensusServerHosts index;
-          in
           assert builtins.elem thisConsensusServerHost allConsensusServerHosts;
           {
             enable = true;
             inherit webUi;
             extraConfig = defaultExtraConfig // {
               server = true;
-              bootstrap_expect = builtins.length allConsensusServerHosts;
+              bootstrap_expect = numConsensusServers;
+              # Tell Consul that we never intend to drop below this many servers.
+              # Ensures to not permanently lose consensus after temporary loss.
+              # See https://github.com/hashicorp/consul/issues/8118#issuecomment-645330040
+              autopilot.min_quorum = numConsensusServers;
               retry_join =
                 # If there's only 1 node in the network, we allow self-join;
                 # otherwise, the node must not try to join itself, and join only the other servers.
                 # See https://github.com/hashicorp/consul/issues/2868
-                if builtins.length allConsensusServerHosts == 1
+                if numConsensusServers == 1
                   then allConsensusServerHosts
                   else builtins.filter (h: h != thisConsensusServerHost) allConsensusServerHosts;
               bind_addr = ip;
@@ -104,40 +107,123 @@ in {
     for m in machines:
         m.wait_for_unit("consul.service")
 
-    for m in machines:
-        m.wait_until_succeeds("[ $(consul members | grep -o alive | wc -l) == 5 ]")
+
+    def wait_for_healthy_servers():
+        # See https://github.com/hashicorp/consul/issues/8118#issuecomment-645330040
+        # for why the `Voter` column of `list-peers` has that info.
+        # TODO: The `grep true` relies on the fact that currently in
+        #       the output like
+        #           # consul operator raft list-peers
+        #           Node     ID   Address           State     Voter  RaftProtocol
+        #           server3  ...  192.168.1.3:8300  leader    true   3
+        #           server2  ...  192.168.1.2:8300  follower  true   3
+        #           server1  ...  192.168.1.1:8300  follower  false  3
+        #       `Voter`is the only boolean column.
+        #       Change this to the more reliable way to be defined by
+        #       https://github.com/hashicorp/consul/issues/8118
+        #       once that ticket is closed.
+        for m in machines:
+            m.wait_until_succeeds(
+                "[ $(consul operator raft list-peers | grep true | wc -l) == 3 ]"
+            )
+
+
+    def wait_for_all_machines_alive():
+        """
+        Note that Serf-"alive" does not mean "Raft"-healthy;
+        see `wait_for_healthy_servers()` for that instead.
+        """
+        for m in machines:
+            m.wait_until_succeeds("[ $(consul members | grep -o alive | wc -l) == 5 ]")
+
+
+    wait_for_healthy_servers()
+    # Also wait for clients to be alive.
+    wait_for_all_machines_alive()
 
     client1.succeed("consul kv put testkey 42")
     client2.succeed("[ $(consul kv get testkey) == 42 ]")
 
-    # Test that the cluster can tolearate failures of any single server:
-    for server in servers:
-        server.crash()
 
-        # For each client, wait until they have connection again
-        # using `kv get -recurse` before issuing commands.
-        client1.wait_until_succeeds("consul kv get -recurse")
-        client2.wait_until_succeeds("consul kv get -recurse")
+    def rolling_reboot_test(proper_rolling_procedure=True):
+        """
+        Tests that the cluster can tolearate failures of any single server,
+        following the recommended rolling upgrade procedure from
+        https://www.consul.io/docs/upgrading#standard-upgrades.
 
-        # Do some consul actions while one server is down.
-        client1.succeed("consul kv put testkey 43")
-        client2.succeed("[ $(consul kv get testkey) == 43 ]")
-        client2.succeed("consul kv delete testkey")
+        Optionally, `proper_rolling_procedure=False` can be given
+        to wait only for each server to be back `Healthy`, not `Stable`
+        in the Raft consensus, see Consul setting `ServerStabilizationTime` and
+        https://github.com/hashicorp/consul/issues/8118#issuecomment-645330040.
+        """
+
+        for server in servers:
+            server.crash()
+
+            # For each client, wait until they have connection again
+            # using `kv get -recurse` before issuing commands.
+            client1.wait_until_succeeds("consul kv get -recurse")
+            client2.wait_until_succeeds("consul kv get -recurse")
 
-        # Restart crashed machine.
-        server.start()
+            # Do some consul actions while one server is down.
+            client1.succeed("consul kv put testkey 43")
+            client2.succeed("[ $(consul kv get testkey) == 43 ]")
+            client2.succeed("consul kv delete testkey")
+
+            # Restart crashed machine.
+            server.start()
+
+            if proper_rolling_procedure:
+                # Wait for recovery.
+                wait_for_healthy_servers()
+            else:
+                # NOT proper rolling upgrade procedure, see above.
+                wait_for_all_machines_alive()
+
+            # Wait for client connections.
+            client1.wait_until_succeeds("consul kv get -recurse")
+            client2.wait_until_succeeds("consul kv get -recurse")
+
+            # Do some consul actions with server back up.
+            client1.succeed("consul kv put testkey 44")
+            client2.succeed("[ $(consul kv get testkey) == 44 ]")
+            client2.succeed("consul kv delete testkey")
+
+
+    def all_servers_crash_simultaneously_test():
+        """
+        Tests that the cluster will eventually come back after all
+        servers crash simultaneously.
+        """
+
+        for server in servers:
+            server.crash()
+
+        for server in servers:
+            server.start()
 
         # Wait for recovery.
-        for m in machines:
-            m.wait_until_succeeds("[ $(consul members | grep -o alive | wc -l) == 5 ]")
+        wait_for_healthy_servers()
 
         # Wait for client connections.
         client1.wait_until_succeeds("consul kv get -recurse")
         client2.wait_until_succeeds("consul kv get -recurse")
 
-        # Do some consul actions with server back up.
+        # Do some consul actions with servers back up.
         client1.succeed("consul kv put testkey 44")
         client2.succeed("[ $(consul kv get testkey) == 44 ]")
         client2.succeed("consul kv delete testkey")
+
+
+    # Run the tests.
+
+    print("rolling_reboot_test()")
+    rolling_reboot_test()
+
+    print("all_servers_crash_simultaneously_test()")
+    all_servers_crash_simultaneously_test()
+
+    print("rolling_reboot_test(proper_rolling_procedure=False)")
+    rolling_reboot_test(proper_rolling_procedure=False)
   '';
 })
diff --git a/nixos/tests/containers-portforward.nix b/nixos/tests/containers-portforward.nix
index fc90e151bd9..1e2c2c6c374 100644
--- a/nixos/tests/containers-portforward.nix
+++ b/nixos/tests/containers-portforward.nix
@@ -5,7 +5,7 @@ let
   hostPort = 10080;
   containerIp = "192.168.0.100";
   containerPort = 80;
-in 
+in
 
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "containers-portforward";
diff --git a/nixos/tests/containers-reloadable.nix b/nixos/tests/containers-reloadable.nix
index 35aff91e85b..2d81f163938 100644
--- a/nixos/tests/containers-reloadable.nix
+++ b/nixos/tests/containers-reloadable.nix
@@ -9,13 +9,13 @@ let
       };
     };
 
-    # prevent make-test.nix to change IP
+    # prevent make-test-python.nix to change IP
     networking.interfaces = {
       eth1.ipv4.addresses = lib.mkOverride 0 [ ];
     };
   };
 in {
-  name = "cotnainers-reloadable";
+  name = "containers-reloadable";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ danbst ];
   };
diff --git a/nixos/tests/convos.nix b/nixos/tests/convos.nix
new file mode 100644
index 00000000000..b4ff1188fd8
--- /dev/null
+++ b/nixos/tests/convos.nix
@@ -0,0 +1,30 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }:
+
+with lib;
+let
+  port = 3333;
+in
+{
+  name = "convos";
+  meta = with pkgs.stdenv.lib.maintainers; {
+    maintainers = [ sgo ];
+  };
+
+  nodes = {
+    machine =
+      { pkgs, ... }:
+      {
+        services.convos = {
+          enable = true;
+          listenPort = port;
+        };
+      };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("convos")
+    machine.wait_for_open_port("${toString port}")
+    machine.succeed("journalctl -u convos | grep -q 'Listening at.*${toString port}'")
+    machine.succeed("curl http://localhost:${toString port}/")
+  '';
+})
diff --git a/nixos/tests/corerad.nix b/nixos/tests/corerad.nix
index 741fa448f68..37a1e90477a 100644
--- a/nixos/tests/corerad.nix
+++ b/nixos/tests/corerad.nix
@@ -1,9 +1,9 @@
 import ./make-test-python.nix (
   {
     nodes = {
-      router = {config, pkgs, ...}: { 
+      router = {config, pkgs, ...}: {
         config = {
-          # This machines simulates a router with IPv6 forwarding and a static IPv6 address.
+          # This machine simulates a router with IPv6 forwarding and a static IPv6 address.
           boot.kernel.sysctl = {
             "net.ipv6.conf.all.forwarding" = true;
           };
@@ -14,13 +14,25 @@ import ./make-test-python.nix (
             enable = true;
             # Serve router advertisements to the client machine with prefix information matching
             # any IPv6 /64 prefixes configured on this interface.
-            configFile = pkgs.writeText "corerad.toml" ''
-              [[interfaces]]
-              name = "eth1"
-              advertise = true
-                [[interfaces.prefix]]
-                prefix = "::/64"
-            '';
+            #
+            # This configuration is identical to the example in the CoreRAD NixOS module.
+            settings = {
+              interfaces = [
+                {
+                  name = "eth0";
+                  monitor = true;
+                }
+                {
+                  name = "eth1";
+                  advertise = true;
+                  prefix = [{ prefix = "::/64"; }];
+                }
+              ];
+              debug = {
+                address = "localhost:9430";
+                prometheus = true;
+              };
+            };
           };
         };
       };
@@ -66,5 +78,12 @@ import ./make-test-python.nix (
           assert (
               "/64 scope global temporary" in addrs
           ), "SLAAC temporary address was not configured on client after router advertisement"
+
+      with subtest("Verify HTTP debug server is configured"):
+          out = router.succeed("curl localhost:9430/metrics")
+
+          assert (
+              "corerad_build_info" in out
+          ), "Build info metric was not found in Prometheus output"
     '';
   })
diff --git a/nixos/tests/cri-o.nix b/nixos/tests/cri-o.nix
new file mode 100644
index 00000000000..f13f1bdacb6
--- /dev/null
+++ b/nixos/tests/cri-o.nix
@@ -0,0 +1,19 @@
+# This test runs CRI-O and verifies via critest
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "cri-o";
+  maintainers = with pkgs.stdenv.lib.maintainers; teams.podman.members;
+
+  nodes = {
+    crio = {
+      virtualisation.cri-o.enable = true;
+    };
+  };
+
+  testScript = ''
+    start_all()
+    crio.wait_for_unit("crio.service")
+    crio.succeed(
+        "critest --ginkgo.focus='Runtime info' --runtime-endpoint unix:///var/run/crio/crio.sock"
+    )
+  '';
+})
diff --git a/nixos/tests/dnscrypt-wrapper/default.nix b/nixos/tests/dnscrypt-wrapper/default.nix
new file mode 100644
index 00000000000..1dc925f4de7
--- /dev/null
+++ b/nixos/tests/dnscrypt-wrapper/default.nix
@@ -0,0 +1,71 @@
+import ../make-test-python.nix ({ pkgs, ... }: {
+  name = "dnscrypt-wrapper";
+  meta = with pkgs.stdenv.lib.maintainers; {
+    maintainers = [ rnhmjoj ];
+  };
+
+  nodes = {
+    server = { lib, ... }:
+      { services.dnscrypt-wrapper = with builtins;
+          { enable = true;
+            address = "192.168.1.1";
+            keys.expiration = 5; # days
+            keys.checkInterval = 2;  # min
+            # The keypair was generated by the command:
+            # dnscrypt-wrapper --gen-provider-keypair \
+            #  --provider-name=2.dnscrypt-cert.server \
+            #  --ext-address=192.168.1.1:5353
+            providerKey.public = toFile "public.key" (readFile ./public.key);
+            providerKey.secret = toFile "secret.key" (readFile ./secret.key);
+          };
+        services.tinydns.enable = true;
+        services.tinydns.data = ''
+          ..:192.168.1.1:a
+          +it.works:1.2.3.4
+        '';
+        networking.firewall.allowedUDPPorts = [ 5353 ];
+        networking.firewall.allowedTCPPorts = [ 5353 ];
+        networking.interfaces.eth1.ipv4.addresses = lib.mkForce
+          [ { address = "192.168.1.1"; prefixLength = 24; } ];
+      };
+
+    client = { lib, ... }:
+      { services.dnscrypt-proxy2.enable = true;
+        services.dnscrypt-proxy2.settings = {
+          server_names = [ "server" ];
+          static.server.stamp = "sdns://AQAAAAAAAAAAEDE5Mi4xNjguMS4xOjUzNTMgFEHYOv0SCKSuqR5CDYa7-58cCBuXO2_5uTSVU9wNQF0WMi5kbnNjcnlwdC1jZXJ0LnNlcnZlcg";
+        };
+        networking.nameservers = [ "127.0.0.1" ];
+        networking.interfaces.eth1.ipv4.addresses = lib.mkForce
+          [ { address = "192.168.1.2"; prefixLength = 24; } ];
+      };
+
+  };
+
+  testScript = ''
+    start_all()
+
+    with subtest("The server can generate the ephemeral keypair"):
+        server.wait_for_unit("dnscrypt-wrapper")
+        server.wait_for_file("/var/lib/dnscrypt-wrapper/2.dnscrypt-cert.server.key")
+        server.wait_for_file("/var/lib/dnscrypt-wrapper/2.dnscrypt-cert.server.crt")
+
+    with subtest("The client can connect to the server"):
+        server.wait_for_unit("tinydns")
+        client.wait_for_unit("dnscrypt-proxy2")
+        assert "1.2.3.4" in client.succeed(
+            "host it.works"
+        ), "The IP address of 'it.works' does not match 1.2.3.4"
+
+    with subtest("The server rotates the ephemeral keys"):
+        # advance time by a little less than 5 days
+        server.succeed("date -s \"$(date --date '4 days 6 hours')\"")
+        client.succeed("date -s \"$(date --date '4 days 6 hours')\"")
+        server.wait_for_file("/var/lib/dnscrypt-wrapper/oldkeys")
+
+    with subtest("The client can still connect to the server"):
+        server.wait_for_unit("dnscrypt-wrapper")
+        client.succeed("host it.works")
+  '';
+})
+
diff --git a/nixos/tests/dnscrypt-wrapper/public.key b/nixos/tests/dnscrypt-wrapper/public.key
new file mode 100644
index 00000000000..80232b97f52
--- /dev/null
+++ b/nixos/tests/dnscrypt-wrapper/public.key
@@ -0,0 +1 @@
+AØ:ý¤®©B
†»ûŸ—;où¹4•SÜ
@]
\ No newline at end of file
diff --git a/nixos/tests/dnscrypt-wrapper/secret.key b/nixos/tests/dnscrypt-wrapper/secret.key
new file mode 100644
index 00000000000..01fbf8e08b7
--- /dev/null
+++ b/nixos/tests/dnscrypt-wrapper/secret.key
@@ -0,0 +1 @@
+G½>Æ©» ì>Ðà¥(Ò²‡¼J•«º=Ÿ„ÝÁlìAØ:ý¤®©B
†»ûŸ—;où¹4•SÜ
@]
\ No newline at end of file
diff --git a/nixos/tests/docker-preloader.nix b/nixos/tests/docker-preloader.nix
deleted file mode 100644
index eeedec9a392..00000000000
--- a/nixos/tests/docker-preloader.nix
+++ /dev/null
@@ -1,27 +0,0 @@
-import ./make-test.nix ({ pkgs, ...} : {
-  name = "docker-preloader";
-  meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ lewo ];
-  };
-
-  nodes = {
-    docker =
-      { pkgs, ... }:
-        {
-          virtualisation.docker.enable = true;
-          virtualisation.dockerPreloader.images = [ pkgs.dockerTools.examples.nix pkgs.dockerTools.examples.bash ];
-
-          services.openssh.enable = true;
-          services.openssh.permitRootLogin = "yes";
-          services.openssh.extraConfig = "PermitEmptyPasswords yes";
-          users.extraUsers.root.password = "";
-        };
-  };    
-  testScript = ''
-    startAll;
-    
-    $docker->waitForUnit("sockets.target");
-    $docker->succeed("docker run nix nix-store --version");
-    $docker->succeed("docker run bash bash --version");
-  '';
-})
diff --git a/nixos/tests/docker-tools.nix b/nixos/tests/docker-tools.nix
index 51b472fcf9c..2543801ae8b 100644
--- a/nixos/tests/docker-tools.nix
+++ b/nixos/tests/docker-tools.nix
@@ -30,8 +30,45 @@ import ./make-test-python.nix ({ pkgs, ... }: {
         )
 
     docker.succeed("docker run --rm ${examples.bash.imageName} bash --version")
+    # Check imageTag attribute matches image
+    docker.succeed("docker images --format '{{.Tag}}' | grep -F '${examples.bash.imageTag}'")
     docker.succeed("docker rmi ${examples.bash.imageName}")
 
+    # The remaining combinations
+    with subtest("Ensure imageTag attribute matches image"):
+        docker.succeed(
+            "docker load --input='${examples.bashNoTag}'"
+        )
+        docker.succeed(
+            "docker images --format '{{.Tag}}' | grep -F '${examples.bashNoTag.imageTag}'"
+        )
+        docker.succeed("docker rmi ${examples.bashNoTag.imageName}:${examples.bashNoTag.imageTag}")
+
+        docker.succeed(
+            "docker load --input='${examples.bashNoTagLayered}'"
+        )
+        docker.succeed(
+            "docker images --format '{{.Tag}}' | grep -F '${examples.bashNoTagLayered.imageTag}'"
+        )
+        docker.succeed("docker rmi ${examples.bashNoTagLayered.imageName}:${examples.bashNoTagLayered.imageTag}")
+
+        docker.succeed(
+            "${examples.bashNoTagStreamLayered} | docker load"
+        )
+        docker.succeed(
+            "docker images --format '{{.Tag}}' | grep -F '${examples.bashNoTagStreamLayered.imageTag}'"
+        )
+        docker.succeed(
+            "docker rmi ${examples.bashNoTagStreamLayered.imageName}:${examples.bashNoTagStreamLayered.imageTag}"
+        )
+
+        docker.succeed(
+            "docker load --input='${examples.nixLayered}'"
+        )
+        docker.succeed("docker images --format '{{.Tag}}' | grep -F '${examples.nixLayered.imageTag}'")
+        docker.succeed("docker rmi ${examples.nixLayered.imageName}")
+
+
     with subtest(
         "Check if the nix store is correctly initialized by listing "
         "dependencies of the installed Nix binary"
@@ -42,6 +79,30 @@ import ./make-test-python.nix ({ pkgs, ... }: {
             "docker rmi ${examples.nix.imageName}",
         )
 
+    with subtest(
+        "Ensure (layered) nix store has correct permissions "
+        "and that the container starts when its process does not have uid 0"
+    ):
+        docker.succeed(
+            "docker load --input='${examples.bashLayeredWithUser}'",
+            "docker run -u somebody --rm ${examples.bashLayeredWithUser.imageName} ${pkgs.bash}/bin/bash -c 'test 555 == $(stat --format=%a /nix) && test 555 == $(stat --format=%a /nix/store)'",
+            "docker rmi ${examples.bashLayeredWithUser.imageName}",
+        )
+
+    with subtest("The nix binary symlinks are intact"):
+        docker.succeed(
+            "docker load --input='${examples.nix}'",
+            "docker run --rm ${examples.nix.imageName} ${pkgs.bash}/bin/bash -c 'test nix == $(readlink ${pkgs.nix}/bin/nix-daemon)'",
+            "docker rmi ${examples.nix.imageName}",
+        )
+
+    with subtest("The nix binary symlinks are intact when the image is layered"):
+        docker.succeed(
+            "docker load --input='${examples.nixLayered}'",
+            "docker run --rm ${examples.nixLayered.imageName} ${pkgs.bash}/bin/bash -c 'test nix == $(readlink ${pkgs.nix}/bin/nix-daemon)'",
+            "docker rmi ${examples.nixLayered.imageName}",
+        )
+
     with subtest("The pullImage tool works"):
         docker.succeed(
             "docker load --input='${examples.nixFromDockerHub}'",
@@ -76,13 +137,22 @@ import ./make-test-python.nix ({ pkgs, ... }: {
 
     with subtest("Ensure Docker images can use an unstable date"):
         docker.succeed(
-            "docker load --input='${examples.bash}'"
+            "docker load --input='${examples.unstableDate}'"
         )
         assert unix_time_second1 not in docker.succeed(
             "docker inspect ${examples.unstableDate.imageName} "
             + "| ${pkgs.jq}/bin/jq -r .[].Created"
         )
 
+    with subtest("Ensure Layered Docker images can use an unstable date"):
+        docker.succeed(
+            "docker load --input='${examples.unstableDateLayered}'"
+        )
+        assert unix_time_second1 not in docker.succeed(
+            "docker inspect ${examples.unstableDateLayered.imageName} "
+            + "| ${pkgs.jq}/bin/jq -r .[].Created"
+        )
+
     with subtest("Ensure Layered Docker images work"):
         docker.succeed(
             "docker load --input='${examples.layered-image}'",
@@ -124,6 +194,16 @@ import ./make-test-python.nix ({ pkgs, ... }: {
                 f"docker run --rm  ${examples.layersOrder.imageName} cat /tmp/layer{index}"
             )
 
+    with subtest("Ensure environment variables are correctly inherited"):
+        docker.succeed(
+            "docker load --input='${examples.environmentVariables}'"
+        )
+        out = docker.succeed("docker run --rm ${examples.environmentVariables.imageName} env")
+        env = out.splitlines()
+        assert "FROM_PARENT=true" in env, "envvars from the parent should be preserved"
+        assert "FROM_CHILD=true" in env, "envvars from the child should be preserved"
+        assert "LAST_LAYER=child" in env, "envvars from the child should take priority"
+
     with subtest("Ensure image with only 2 layers can be loaded"):
         docker.succeed(
             "docker load --input='${examples.two-layered-image}'"
@@ -154,5 +234,12 @@ import ./make-test-python.nix ({ pkgs, ... }: {
         # This check may be loosened to allow an *empty* store rather than *no* store.
         docker.succeed("docker run --rm no-store-paths ls /")
         docker.fail("docker run --rm no-store-paths ls /nix/store")
+
+    with subtest("Ensure buildLayeredImage does not change store path contents."):
+        docker.succeed(
+            "docker load --input='${pkgs.dockerTools.examples.filesInStore}'",
+            "docker run --rm file-in-store nix-store --verify --check-contents",
+            "docker run --rm file-in-store |& grep 'some data'",
+        )
   '';
 })
diff --git a/nixos/tests/docker.nix b/nixos/tests/docker.nix
index 8fda7c1395e..a4a61468f33 100644
--- a/nixos/tests/docker.nix
+++ b/nixos/tests/docker.nix
@@ -43,7 +43,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     docker.fail("sudo -u noprivs docker ps")
     docker.succeed("docker stop sleeping")
 
-    # Must match version twice to ensure client and server versions are correct
-    docker.succeed('[ $(docker version | grep ${pkgs.docker.version} | wc -l) = "2" ]')
+    # Must match version 4 times to ensure client and server git commits and versions are correct
+    docker.succeed('[ $(docker version | grep ${pkgs.docker.version} | wc -l) = "4" ]')
   '';
 })
diff --git a/nixos/tests/dokuwiki.nix b/nixos/tests/dokuwiki.nix
index 05271919eff..58069366ca3 100644
--- a/nixos/tests/dokuwiki.nix
+++ b/nixos/tests/dokuwiki.nix
@@ -32,24 +32,17 @@ let
 
 in {
   name = "dokuwiki";
-  meta.maintainers = with pkgs.lib.maintainers; [ "1000101" ];
-
+  meta = with pkgs.stdenv.lib; {
+    maintainers = with maintainers; [ _1000101 ];
+  };
   machine = { ... }: {
     services.dokuwiki."site1.local" = {
       aclUse = false;
       superUser = "admin";
-      nginx = {
-        forceSSL = false;
-        enableACME = false;
-      };
     };
     services.dokuwiki."site2.local" = {
-      aclUse = true;
+      usersFile = "/var/lib/dokuwiki/site2.local/users.auth.php";
       superUser = "admin";
-      nginx = {
-        forceSSL = false;
-        enableACME = false;
-      };
       templates = [ template-bootstrap3 ];
       plugins = [ plugin-icalevents ];
     };
@@ -69,6 +62,15 @@ in {
     machine.wait_for_open_port(80)
 
     machine.succeed("curl -sSfL http://site1.local/ | grep 'DokuWiki'")
+    machine.fail("curl -sSfL 'http://site1.local/doku.php?do=login' | grep 'Login'")
+
     machine.succeed("curl -sSfL http://site2.local/ | grep 'DokuWiki'")
+    machine.succeed("curl -sSfL 'http://site2.local/doku.php?do=login' | grep 'Login'")
+
+    machine.succeed(
+        "echo 'admin:$2y$10$ijdBQMzSVV20SrKtCna8gue36vnsbVm2wItAXvdm876sshI4uwy6S:Admin:admin@example.test:user' >> /var/lib/dokuwiki/site2.local/users.auth.php",
+        "curl -sSfL -d 'u=admin&p=password' --cookie-jar cjar 'http://site2.local/doku.php?do=login'",
+        "curl -sSfL --cookie cjar --cookie-jar cjar 'http://site2.local/doku.php?do=login' | grep 'Logged in as: <bdi>Admin</bdi>'",
+    )
   '';
 })
diff --git a/nixos/tests/ec2.nix b/nixos/tests/ec2.nix
index 5a59d65e602..df067248016 100644
--- a/nixos/tests/ec2.nix
+++ b/nixos/tests/ec2.nix
@@ -3,58 +3,58 @@
   pkgs ? import ../.. { inherit system config; }
 }:
 
-with import ../lib/testing.nix { inherit system pkgs; };
+with import ../lib/testing-python.nix { inherit system pkgs; };
 with pkgs.lib;
 
 with import common/ec2.nix { inherit makeTest pkgs; };
 
 let
-  imageCfg =
-    (import ../lib/eval-config.nix {
-      inherit system;
-      modules = [
-        ../maintainers/scripts/ec2/amazon-image.nix
-        ../modules/testing/test-instrumentation.nix
-        ../modules/profiles/qemu-guest.nix
-        { ec2.hvm = true;
-
-          # Hack to make the partition resizing work in QEMU.
-          boot.initrd.postDeviceCommands = mkBefore
-            ''
-              ln -s vda /dev/xvda
-              ln -s vda1 /dev/xvda1
-            '';
-
-          # Needed by nixos-rebuild due to the lack of network
-          # access. Determined by trial and error.
-          system.extraDependencies =
-            with pkgs; (
-              [
-                # Needed for a nixos-rebuild.
-                busybox
-                stdenv
-                stdenvNoCC
-                mkinitcpio-nfs-utils
-                unionfs-fuse
-                cloud-utils
-                desktop-file-utils
-                texinfo
-                libxslt.bin
-                xorg.lndir
-
-                # These are used in the configure-from-userdata tests
-                # for EC2. Httpd and valgrind are requested by the
-                # configuration.
-                apacheHttpd apacheHttpd.doc apacheHttpd.man valgrind.doc
-              ]
-            );
-        }
-      ];
-    }).config;
+  imageCfg = (import ../lib/eval-config.nix {
+    inherit system;
+    modules = [
+      ../maintainers/scripts/ec2/amazon-image.nix
+      ../modules/testing/test-instrumentation.nix
+      ../modules/profiles/qemu-guest.nix
+      {
+        ec2.hvm = true;
+
+        # Hack to make the partition resizing work in QEMU.
+        boot.initrd.postDeviceCommands = mkBefore ''
+          ln -s vda /dev/xvda
+          ln -s vda1 /dev/xvda1
+        '';
+
+        # Needed by nixos-rebuild due to the lack of network
+        # access. Determined by trial and error.
+        system.extraDependencies = with pkgs; ( [
+          # Needed for a nixos-rebuild.
+          busybox
+          cloud-utils
+          desktop-file-utils
+          libxslt.bin
+          mkinitcpio-nfs-utils
+          stdenv
+          stdenvNoCC
+          texinfo
+          unionfs-fuse
+          xorg.lndir
+
+          # These are used in the configure-from-userdata tests
+          # for EC2. Httpd and valgrind are requested by the
+          # configuration.
+          apacheHttpd
+          apacheHttpd.doc
+          apacheHttpd.man
+          valgrind.doc
+        ]);
+      }
+    ];
+  }).config;
   image = "${imageCfg.system.build.amazonImage}/${imageCfg.amazonImage.name}.vhd";
 
   sshKeys = import ./ssh-keys.nix pkgs;
   snakeOilPrivateKey = sshKeys.snakeOilPrivateKey.text;
+  snakeOilPrivateKeyFile = pkgs.writeText "private-key" snakeOilPrivateKey;
   snakeOilPublicKey = sshKeys.snakeOilPublicKey;
 
 in {
@@ -68,43 +68,47 @@ in {
       SSH_HOST_ED25519_KEY:${replaceStrings ["\n"] ["|"] snakeOilPrivateKey}
     '';
     script = ''
-      $machine->start;
-      $machine->waitForFile("/etc/ec2-metadata/user-data");
-      $machine->waitForUnit("sshd.service");
+      machine.start()
+      machine.wait_for_file("/etc/ec2-metadata/user-data")
+      machine.wait_for_unit("sshd.service")
 
-      $machine->succeed("grep unknown /etc/ec2-metadata/ami-manifest-path");
+      machine.succeed("grep unknown /etc/ec2-metadata/ami-manifest-path")
 
       # We have no keys configured on the client side yet, so this should fail
-      $machine->fail("ssh -o BatchMode=yes localhost exit");
+      machine.fail("ssh -o BatchMode=yes localhost exit")
 
       # Let's install our client private key
-      $machine->succeed("mkdir -p ~/.ssh");
+      machine.succeed("mkdir -p ~/.ssh")
 
-      $machine->succeed("echo '${snakeOilPrivateKey}' > ~/.ssh/id_ed25519");
-      $machine->succeed("chmod 600 ~/.ssh/id_ed25519");
+      machine.copy_from_host_via_shell(
+          "${snakeOilPrivateKeyFile}", "~/.ssh/id_ed25519"
+      )
+      machine.succeed("chmod 600 ~/.ssh/id_ed25519")
 
       # We haven't configured the host key yet, so this should still fail
-      $machine->fail("ssh -o BatchMode=yes localhost exit");
+      machine.fail("ssh -o BatchMode=yes localhost exit")
 
       # Add the host key; ssh should finally succeed
-      $machine->succeed("echo localhost,127.0.0.1 ${snakeOilPublicKey} > ~/.ssh/known_hosts");
-      $machine->succeed("ssh -o BatchMode=yes localhost exit");
+      machine.succeed(
+          "echo localhost,127.0.0.1 ${snakeOilPublicKey} > ~/.ssh/known_hosts"
+      )
+      machine.succeed("ssh -o BatchMode=yes localhost exit")
 
       # Test whether the root disk was resized.
-      my $blocks = $machine->succeed("stat -c %b -f /");
-      my $bsize = $machine->succeed("stat -c %S -f /");
-      my $size = $blocks * $bsize;
-      die "wrong free space $size" if $size < 9.7 * 1024 * 1024 * 1024 || $size > 10 * 1024 * 1024 * 1024;
+      blocks, block_size = map(int, machine.succeed("stat -c %b:%S -f /").split(":"))
+      GB = 1024 ** 3
+      assert 9.7 * GB <= blocks * block_size <= 10 * GB
 
       # Just to make sure resizing is idempotent.
-      $machine->shutdown;
-      $machine->start;
-      $machine->waitForFile("/etc/ec2-metadata/user-data");
+      machine.shutdown()
+      machine.start()
+      machine.wait_for_file("/etc/ec2-metadata/user-data")
     '';
   };
 
   boot-ec2-config = makeEc2Test {
     name         = "config-userdata";
+    meta.broken = true; # amazon-init wants to download from the internet while building the system
     inherit image;
     sshPublicKey = snakeOilPublicKey;
 
@@ -133,17 +137,17 @@ in {
       }
     '';
     script = ''
-      $machine->start;
+      machine.start()
 
       # amazon-init must succeed. if it fails, make the test fail
-      # immediately instead of timing out in waitForFile.
-      $machine->waitForUnit('amazon-init.service');
+      # immediately instead of timing out in wait_for_file.
+      machine.wait_for_unit("amazon-init.service")
 
-      $machine->waitForFile("/etc/testFile");
-      $machine->succeed("cat /etc/testFile | grep -q 'whoa'");
+      machine.wait_for_file("/etc/testFile")
+      assert "whoa" in machine.succeed("cat /etc/testFile")
 
-      $machine->waitForUnit("httpd.service");
-      $machine->succeed("curl http://localhost | grep Valgrind");
+      machine.wait_for_unit("httpd.service")
+      assert "Valgrind" in machine.succeed("curl http://localhost")
     '';
   };
 }
diff --git a/nixos/tests/engelsystem.nix b/nixos/tests/engelsystem.nix
new file mode 100644
index 00000000000..39c10718093
--- /dev/null
+++ b/nixos/tests/engelsystem.nix
@@ -0,0 +1,41 @@
+import ./make-test-python.nix (
+  { pkgs, lib, ... }:
+  {
+    name = "engelsystem";
+    meta = with pkgs.stdenv.lib.maintainers; {
+      maintainers = [ talyz ];
+    };
+
+    nodes.engelsystem =
+      { ... }:
+      {
+        services.engelsystem = {
+          enable = true;
+          domain = "engelsystem";
+          createDatabase = true;
+        };
+        networking.firewall.allowedTCPPorts = [ 80 443 ];
+        environment.systemPackages = with pkgs; [
+          xmlstarlet
+          libxml2
+        ];
+      };
+
+    testScript = ''
+      engelsystem.start()
+      engelsystem.wait_for_unit("phpfpm-engelsystem.service")
+      engelsystem.wait_until_succeeds("curl engelsystem/login -sS -f")
+      engelsystem.succeed(
+          "curl engelsystem/login -sS -f -c cookie | xmllint -html -xmlout - >login"
+      )
+      engelsystem.succeed(
+          "xml sel -T -t -m \"html/head/meta[@name='csrf-token']\" -v @content login >token"
+      )
+      engelsystem.succeed(
+          "curl engelsystem/login -sS -f -b cookie -F 'login=admin' -F 'password=asdfasdf' -F '_token=<token' -L | xmllint -html -xmlout - >news"
+      )
+      engelsystem.succeed(
+          "test 'News - Engelsystem' = \"$(xml sel -T -t -c html/head/title news)\""
+      )
+    '';
+  })
diff --git a/nixos/tests/enlightenment.nix b/nixos/tests/enlightenment.nix
index 5fa8d765dd1..0132b98b1cb 100644
--- a/nixos/tests/enlightenment.nix
+++ b/nixos/tests/enlightenment.nix
@@ -41,28 +41,24 @@ import ./make-test-python.nix ({ pkgs, ...} :
 
     with subtest("First time wizard"):
         machine.wait_for_text("Default")  # Language
-        machine.succeed("xdotool mousemove 512 185 click 1")  # Default Language
         machine.screenshot("wizard1")
         machine.succeed("xdotool mousemove 512 740 click 1")  # Next
-
-        machine.wait_for_text("English")  # Keyboard (default)
         machine.screenshot("wizard2")
-        machine.succeed("xdotool mousemove 512 740 click 1")  # Next
 
-        machine.wait_for_text("Standard")  # Profile (default)
+        machine.wait_for_text("English")  # Keyboard (default)
         machine.screenshot("wizard3")
         machine.succeed("xdotool mousemove 512 740 click 1")  # Next
 
-        machine.wait_for_text("Title")  # Sizing (default)
+        machine.wait_for_text("Standard")  # Profile (default)
         machine.screenshot("wizard4")
         machine.succeed("xdotool mousemove 512 740 click 1")  # Next
 
-        machine.wait_for_text("clicked")  # Windows Phocus
-        machine.succeed("xdotool mousemove 512 370 click 1")  # Click
+        machine.wait_for_text("Title")  # Sizing (default)
         machine.screenshot("wizard5")
         machine.succeed("xdotool mousemove 512 740 click 1")  # Next
 
-        machine.wait_for_text("bindings")  # Mouse Modifiers (default)
+        machine.wait_for_text("clicked")  # Windows Focus
+        machine.succeed("xdotool mousemove 512 370 click 1")  # Click
         machine.screenshot("wizard6")
         machine.succeed("xdotool mousemove 512 740 click 1")  # Next
 
@@ -74,7 +70,7 @@ import ./make-test-python.nix ({ pkgs, ...} :
         machine.screenshot("wizard8")
         machine.succeed("xdotool mousemove 512 740 click 1")  # Next
 
-        machine.wait_for_text("Compositing")  # Compositing (default)
+        machine.wait_for_text("OpenGL")  # Compositing (default)
         machine.screenshot("wizard9")
         machine.succeed("xdotool mousemove 512 740 click 1")  # Next
 
diff --git a/nixos/tests/ergo.nix b/nixos/tests/ergo.nix
new file mode 100644
index 00000000000..8cdbbf62a95
--- /dev/null
+++ b/nixos/tests/ergo.nix
@@ -0,0 +1,18 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "ergo";
+  meta = with pkgs.stdenv.lib.maintainers; {
+    maintainers = [ mmahut ];
+  };
+
+  nodes = {
+    machine = { ... }: {
+      services.ergo.enable = true;
+      services.ergo.api.keyHash = "324dcf027dd4a30a932c441f365a25e86b173defa4b8e58948253471b81b72cf";
+    };
+  };
+
+  testScript = ''
+    start_all()
+    machine.wait_for_unit("ergo.service")
+  '';
+})
diff --git a/nixos/tests/firejail.nix b/nixos/tests/firejail.nix
new file mode 100644
index 00000000000..a723cb01664
--- /dev/null
+++ b/nixos/tests/firejail.nix
@@ -0,0 +1,82 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "firejail";
+  meta = with pkgs.stdenv.lib.maintainers; {
+    maintainers = [ sgo ];
+  };
+
+  nodes.machine = { ... }: {
+    imports = [ ./common/user-account.nix ];
+
+    programs.firejail = {
+      enable = true;
+      wrappedBinaries = {
+        bash-jailed  = "${pkgs.bash}/bin/bash";
+      };
+    };
+
+    systemd.services.setupFirejailTest = {
+      wantedBy = [ "multi-user.target" ];
+      before = [ "multi-user.target" ];
+
+      environment = {
+        HOME = "/home/alice";
+      };
+
+      unitConfig = {
+        type = "oneshot";
+        RemainAfterExit = true;
+        user = "alice";
+      };
+
+      script = ''
+        cd $HOME
+
+        mkdir .password-store && echo s3cret > .password-store/secret
+        mkdir my-secrets && echo s3cret > my-secrets/secret
+
+        echo publ1c > public
+
+        mkdir -p .config/firejail
+        echo 'blacklist ''${HOME}/my-secrets' > .config/firejail/globals.local
+      '';
+    };
+  };
+
+  testScript = ''
+    start_all()
+    machine.wait_for_unit("multi-user.target")
+
+    # Test path acl with wrapper
+    machine.succeed("sudo -u alice bash-jailed -c 'cat ~/public' | grep -q publ1c")
+    machine.fail(
+        "sudo -u alice bash-jailed -c 'cat ~/.password-store/secret' | grep -q s3cret"
+    )
+    machine.fail("sudo -u alice bash-jailed -c 'cat ~/my-secrets/secret' | grep -q s3cret")
+
+
+    # Test path acl with firejail executable
+    machine.succeed("sudo -u alice firejail -- bash -c 'cat ~/public' | grep -q publ1c")
+    machine.fail(
+        "sudo -u alice firejail -- bash -c 'cat ~/.password-store/secret' | grep -q s3cret"
+    )
+    machine.fail(
+        "sudo -u alice firejail -- bash -c 'cat ~/my-secrets/secret' | grep -q s3cret"
+    )
+
+    # Disabling profiles
+    machine.succeed(
+        "sudo -u alice bash -c 'firejail --noprofile -- cat ~/.password-store/secret' | grep -q s3cret"
+    )
+
+    # CVE-2020-17367
+    machine.fail(
+        "sudo -u alice firejail --private-tmp id --output=/tmp/vuln1 && cat /tmp/vuln1"
+    )
+
+    # CVE-2020-17368
+    machine.fail(
+        "sudo -u alice firejail --private-tmp --output=/tmp/foo 'bash -c $(id>/tmp/vuln2;echo id)' && cat /tmp/vuln2"
+    )
+  '';
+})
+
diff --git a/nixos/tests/gnome3-xorg.nix b/nixos/tests/gnome3-xorg.nix
index b59badcd5de..0d05c12384f 100644
--- a/nixos/tests/gnome3-xorg.nix
+++ b/nixos/tests/gnome3-xorg.nix
@@ -12,8 +12,9 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : {
 
       services.xserver.enable = true;
 
-      services.xserver.displayManager.gdm = {
-        enable = true;
+      services.xserver.displayManager = {
+        gdm.enable = true;
+        gdm.debug = true;
         autoLogin = {
           enable = true;
           user = user.name;
@@ -21,6 +22,7 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : {
       };
 
       services.xserver.desktopManager.gnome3.enable = true;
+      services.xserver.desktopManager.gnome3.debug = true;
       services.xserver.displayManager.defaultSession = "gnome-xorg";
 
       virtualisation.memorySize = 1024;
diff --git a/nixos/tests/gnome3.nix b/nixos/tests/gnome3.nix
index 17e72c5f651..7e301be49d1 100644
--- a/nixos/tests/gnome3.nix
+++ b/nixos/tests/gnome3.nix
@@ -11,8 +11,9 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : {
 
       services.xserver.enable = true;
 
-      services.xserver.displayManager.gdm = {
-        enable = true;
+      services.xserver.displayManager = {
+        gdm.enable = true;
+        gdm.debug = true;
         autoLogin = {
           enable = true;
           user = "alice";
@@ -20,6 +21,14 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : {
       };
 
       services.xserver.desktopManager.gnome3.enable = true;
+      services.xserver.desktopManager.gnome3.debug = true;
+
+      environment.systemPackages = [
+        (pkgs.makeAutostartItem {
+          name = "org.gnome.Terminal";
+          package = pkgs.gnome3.gnome-terminal;
+        })
+      ];
 
       virtualisation.memorySize = 1024;
     };
@@ -63,9 +72,6 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : {
           )
 
       with subtest("Open Gnome Terminal"):
-          machine.succeed(
-              "${gnomeTerminalCommand}"
-          )
           # correct output should be (true, '"gnome-terminal-server"')
           machine.wait_until_succeeds(
               "${wmClass} | grep -q 'gnome-terminal-server'"
diff --git a/nixos/tests/go-neb.nix b/nixos/tests/go-neb.nix
new file mode 100644
index 00000000000..d9e5db0b4a5
--- /dev/null
+++ b/nixos/tests/go-neb.nix
@@ -0,0 +1,44 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+{
+  name = "go-neb";
+  meta = with pkgs.stdenv.lib.maintainers; {
+    maintainers = [ hexa maralorn ];
+  };
+
+  nodes = {
+    server = {
+      services.go-neb = {
+        enable = true;
+        baseUrl = "http://localhost";
+        config = {
+          clients = [ {
+            UserId = "@test:localhost";
+            AccessToken = "changeme";
+            HomeServerUrl = "http://localhost";
+            Sync = false;
+            AutoJoinRooms = false;
+            DisplayName = "neverbeseen";
+          } ];
+          services = [ {
+            ID = "wikipedia_service";
+            Type = "wikipedia";
+            UserID = "@test:localhost";
+            Config = { };
+          } ];
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+    server.wait_for_unit("go-neb.service")
+    server.wait_until_succeeds(
+        "curl -L http://localhost:4050/services/hooks/d2lraXBlZGlhX3NlcnZpY2U"
+    )
+    server.wait_until_succeeds(
+        "journalctl -eu go-neb -o cat | grep -q service_id=wikipedia_service"
+    )
+  '';
+
+})
diff --git a/nixos/tests/graphite.nix b/nixos/tests/graphite.nix
index 71776a94cbd..137be2d89c8 100644
--- a/nixos/tests/graphite.nix
+++ b/nixos/tests/graphite.nix
@@ -1,11 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ... } :
 {
   name = "graphite";
-  meta = {
-    # Fails on dependency `python-2.7-Twisted`'s test suite
-    # complaining `ImportError: No module named zope.interface`.
-    broken = true;
-  };
   nodes = {
     one =
       { ... }: {
@@ -21,7 +16,7 @@ import ./make-test-python.nix ({ pkgs, ... } :
           api = {
             enable = true;
             port = 8082;
-            finders = [ pkgs.python3Packages.influxgraph ];
+            finders = [ ];
           };
           carbon.enableCache = true;
           seyren.enable = false;  # Implicitely requires openssl-1.0.2u which is marked insecure
@@ -41,10 +36,14 @@ import ./make-test-python.nix ({ pkgs, ... } :
     # even if they're still in preStart (which takes quite long for graphiteWeb).
     # Wait for ports to open so we're sure the services are up and listening.
     one.wait_for_open_port(8080)
+    one.wait_for_open_port(8082)
     one.wait_for_open_port(2003)
     one.succeed('echo "foo 1 `date +%s`" | nc -N localhost 2003')
     one.wait_until_succeeds(
         "curl 'http://localhost:8080/metrics/find/?query=foo&format=treejson' --silent | grep foo >&2"
     )
+    one.wait_until_succeeds(
+        "curl 'http://localhost:8082/metrics/find/?query=foo&format=treejson' --silent | grep foo >&2"
+    )
   '';
 })
diff --git a/nixos/tests/grub.nix b/nixos/tests/grub.nix
new file mode 100644
index 00000000000..84bfc90955b
--- /dev/null
+++ b/nixos/tests/grub.nix
@@ -0,0 +1,60 @@
+import ./make-test-python.nix ({ lib, ... }: {
+  name = "grub";
+
+  meta = with lib.maintainers; {
+    maintainers = [ rnhmjoj ];
+  };
+
+  machine = { ... }: {
+    virtualisation.useBootLoader = true;
+
+    boot.loader.timeout = null;
+    boot.loader.grub = {
+      enable = true;
+      users.alice.password = "supersecret";
+
+      # OCR is not accurate enough
+      extraConfig = "serial; terminal_output serial";
+    };
+  };
+
+  testScript = ''
+    def grub_login_as(user, password):
+        """
+        Enters user and password to log into GRUB
+        """
+        machine.wait_for_console_text("Enter username:")
+        machine.send_chars(user + "\n")
+        machine.wait_for_console_text("Enter password:")
+        machine.send_chars(password + "\n")
+
+
+    def grub_select_all_configurations():
+        """
+        Selects "All configurations" from the GRUB menu
+        to trigger a login request.
+        """
+        machine.send_monitor_command("sendkey down")
+        machine.send_monitor_command("sendkey ret")
+
+
+    machine.start()
+
+    # wait for grub screen
+    machine.wait_for_console_text("GNU GRUB")
+
+    grub_select_all_configurations()
+    with subtest("Invalid credentials are rejected"):
+        grub_login_as("wronguser", "wrongsecret")
+        machine.wait_for_console_text("error: access denied.")
+
+    grub_select_all_configurations()
+    with subtest("Valid credentials are accepted"):
+        grub_login_as("alice", "supersecret")
+        machine.send_chars("\n")  # press enter to boot
+        machine.wait_for_console_text("Linux version")
+
+    with subtest("Machine boots correctly"):
+        machine.wait_for_unit("multi-user.target")
+  '';
+})
diff --git a/nixos/tests/haproxy.nix b/nixos/tests/haproxy.nix
index 79f34b07faf..ffb77c052a2 100644
--- a/nixos/tests/haproxy.nix
+++ b/nixos/tests/haproxy.nix
@@ -43,5 +43,13 @@ import ./make-test-python.nix ({ pkgs, ...}: {
     assert "haproxy_process_pool_allocated_bytes" in machine.succeed(
         "curl -k http://localhost:80/metrics"
     )
+
+    with subtest("reload"):
+        machine.succeed("systemctl reload haproxy")
+        # wait some time to ensure the following request hits the reloaded haproxy
+        machine.sleep(5)
+        assert "We are all good!" in machine.succeed(
+            "curl -k http://localhost:80/index.txt"
+        )
   '';
 })
diff --git a/nixos/tests/hardened.nix b/nixos/tests/hardened.nix
index 5ed0dfcf9ab..8d845de70e2 100644
--- a/nixos/tests/hardened.nix
+++ b/nixos/tests/hardened.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, latestKernel ? false, ... } : {
+import ./make-test-python.nix ({ pkgs, latestKernel ? false, ... } : {
   name = "hardened";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ joachifm ];
@@ -47,84 +47,88 @@ import ./make-test.nix ({ pkgs, latestKernel ? false, ... } : {
       };
     in
     ''
-      $machine->waitForUnit("multi-user.target");
+      machine.wait_for_unit("multi-user.target")
+
+
+      with subtest("AppArmor profiles are loaded"):
+          machine.succeed("systemctl status apparmor.service")
 
-      subtest "apparmor-loaded", sub {
-          $machine->succeed("systemctl status apparmor.service");
-      };
 
       # AppArmor securityfs
-      subtest "apparmor-securityfs", sub {
-          $machine->succeed("mountpoint -q /sys/kernel/security");
-          $machine->succeed("cat /sys/kernel/security/apparmor/profiles");
-      };
+      with subtest("AppArmor securityfs is mounted"):
+          machine.succeed("mountpoint -q /sys/kernel/security")
+          machine.succeed("cat /sys/kernel/security/apparmor/profiles")
+
 
       # Test loading out-of-tree modules
-      subtest "extra-module-packages", sub {
-          $machine->succeed("grep -Fq wireguard /proc/modules");
-      };
+      with subtest("Out-of-tree modules can be loaded"):
+          machine.succeed("grep -Fq wireguard /proc/modules")
+
 
       # Test hidepid
-      subtest "hidepid", sub {
-          $machine->succeed("grep -Fq hidepid=2 /proc/mounts");
+      with subtest("hidepid=2 option is applied and works"):
+          machine.succeed("grep -Fq hidepid=2 /proc/mounts")
           # cannot use pgrep -u here, it segfaults when access to process info is denied
-          $machine->succeed("[ `su - sybil -c 'ps --no-headers --user root | wc -l'` = 0 ]");
-          $machine->succeed("[ `su - alice -c 'ps --no-headers --user root | wc -l'` != 0 ]");
-      };
+          machine.succeed("[ `su - sybil -c 'ps --no-headers --user root | wc -l'` = 0 ]")
+          machine.succeed("[ `su - alice -c 'ps --no-headers --user root | wc -l'` != 0 ]")
+
 
       # Test kernel module hardening
-      subtest "lock-modules", sub {
+      with subtest("No more kernel modules can be loaded"):
           # note: this better a be module we normally wouldn't load ...
-          $machine->fail("modprobe dccp");
-      };
+          machine.fail("modprobe dccp")
+
 
       # Test userns
-      subtest "userns", sub {
-          $machine->succeed("unshare --user true");
-          $machine->fail("su -l alice -c 'unshare --user true'");
-      };
+      with subtest("User namespaces are restricted"):
+          machine.succeed("unshare --user true")
+          machine.fail("su -l alice -c 'unshare --user true'")
+
 
       # Test dmesg restriction
-      subtest "dmesg", sub {
-          $machine->fail("su -l alice -c dmesg");
-      };
+      with subtest("Regular users cannot access dmesg"):
+          machine.fail("su -l alice -c dmesg")
+
 
       # Test access to kcore
-      subtest "kcore", sub {
-          $machine->fail("cat /proc/kcore");
-      };
+      with subtest("Kcore is inaccessible as root"):
+          machine.fail("cat /proc/kcore")
+
 
       # Test deferred mount
-      subtest "mount", sub {
-        $machine->fail("mountpoint -q /efi"); # was deferred
-        $machine->execute("mkdir -p /efi");
-        $machine->succeed("mount /dev/disk/by-label/EFISYS /efi");
-        $machine->succeed("mountpoint -q /efi"); # now mounted
-      };
+      with subtest("Deferred mounts work"):
+          machine.fail("mountpoint -q /efi")  # was deferred
+          machine.execute("mkdir -p /efi")
+          machine.succeed("mount /dev/disk/by-label/EFISYS /efi")
+          machine.succeed("mountpoint -q /efi")  # now mounted
+
 
       # Test Nix dæmon usage
-      subtest "nix-daemon", sub {
-        $machine->fail("su -l nobody -s /bin/sh -c 'nix ping-store'");
-        $machine->succeed("su -l alice -c 'nix ping-store'") =~ "OK";
-      };
+      with subtest("nix-daemon cannot be used by all users"):
+          machine.fail("su -l nobody -s /bin/sh -c 'nix ping-store'")
+          machine.succeed("su -l alice -c 'nix ping-store'")
+
 
       # Test kernel image protection
-      subtest "kernelimage", sub {
-        $machine->fail("systemctl hibernate");
-        $machine->fail("systemctl kexec");
-      };
+      with subtest("The kernel image is protected"):
+          machine.fail("systemctl hibernate")
+          machine.fail("systemctl kexec")
 
-      # Test hardened memory allocator
-      sub runMallocTestProg {
-          my ($progName, $errorText) = @_;
-          my $text = "fatal allocator error: " . $errorText;
-          $machine->fail("${hardened-malloc-tests}/bin/" . $progName) =~ $text;
-      };
 
-      subtest "hardenedmalloc", sub {
-        runMallocTestProg("double_free_large", "invalid free");
-        runMallocTestProg("unaligned_free_small", "invalid unaligned free");
-        runMallocTestProg("write_after_free_small", "detected write after free");
-      };
+      # Test hardened memory allocator
+      def runMallocTestProg(prog_name, error_text):
+          text = "fatal allocator error: " + error_text
+          if not text in machine.fail(
+              "${hardened-malloc-tests}/bin/"
+              + prog_name
+              + " 2>&1"
+          ):
+              raise Exception("Hardened malloc does not work for {}".format(error_text))
+
+
+      with subtest("The hardened memory allocator works"):
+          runMallocTestProg("double_free_large", "invalid free")
+          runMallocTestProg("unaligned_free_small", "invalid unaligned free")
+          runMallocTestProg("write_after_free_small", "detected write after free")
     '';
 })
diff --git a/nixos/tests/hocker-fetchdocker/default.nix b/nixos/tests/hocker-fetchdocker/default.nix
index 4f30f01e403..978dbf310b1 100644
--- a/nixos/tests/hocker-fetchdocker/default.nix
+++ b/nixos/tests/hocker-fetchdocker/default.nix
@@ -1,15 +1,16 @@
-import ../make-test.nix ({ pkgs, ...} : {
+import ../make-test-python.nix ({ pkgs, ...} : {
   name = "test-hocker-fetchdocker";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ ixmatus ];
+    broken = true; # tries to download from registry-1.docker.io - how did this ever work?
   };
 
   machine = import ./machine.nix;
 
   testScript = ''
-    startAll;
+    start_all()
 
-    $machine->waitForUnit("sockets.target");
-    $machine->waitUntilSucceeds("docker run registry-1.docker.io/v2/library/hello-world:latest");
+    machine.wait_for_unit("sockets.target")
+    machine.wait_until_succeeds("docker run registry-1.docker.io/v2/library/hello-world:latest")
   '';
 })
diff --git a/nixos/tests/home-assistant.nix b/nixos/tests/home-assistant.nix
index 80dca43f1f3..a93a28d877a 100644
--- a/nixos/tests/home-assistant.nix
+++ b/nixos/tests/home-assistant.nix
@@ -2,69 +2,66 @@ import ./make-test-python.nix ({ pkgs, ... }:
 
 let
   configDir = "/var/lib/foobar";
-  apiPassword = "some_secret";
-  mqttPassword = "another_secret";
-  hassCli = "hass-cli --server http://hass:8123 --password '${apiPassword}'";
+  mqttUsername = "homeassistant";
+  mqttPassword = "secret";
 in {
   name = "home-assistant";
   meta = with pkgs.stdenv.lib; {
     maintainers = with maintainers; [ dotlambda ];
   };
 
-  nodes = {
-    hass =
-      { pkgs, ... }:
-      {
-        environment.systemPackages = with pkgs; [
-          mosquitto home-assistant-cli
-        ];
-        services.home-assistant = {
-          inherit configDir;
-          enable = true;
-          package = pkgs.home-assistant.override {
-            extraPackages = ps: with ps; [ hbmqtt ];
-          };
-          config = {
-            homeassistant = {
-              name = "Home";
-              time_zone = "UTC";
-              latitude = "0.0";
-              longitude = "0.0";
-              elevation = 0;
-              auth_providers = [
-                {
-                  type = "legacy_api_password";
-                  api_password = apiPassword;
-                }
-              ];
-            };
-            frontend = { };
-            mqtt = { # Use hbmqtt as broker
-              password = mqttPassword;
-            };
-            binary_sensor = [
-              {
-                platform = "mqtt";
-                state_topic = "home-assistant/test";
-                payload_on = "let_there_be_light";
-                payload_off = "off";
-              }
-            ];
-          };
-          lovelaceConfig = {
-            title = "My Awesome Home";
-            views = [ {
-              title = "Example";
-              cards = [ {
-                type = "markdown";
-                title = "Lovelace";
-                content = "Welcome to your **Lovelace UI**.";
-              } ];
-            } ];
-          };
-          lovelaceConfigWritable = true;
+  nodes.hass = { pkgs, ... }: {
+    environment.systemPackages = with pkgs; [ mosquitto ];
+    services.mosquitto = {
+      enable = true;
+      users = {
+        "${mqttUsername}" = {
+          acl = [ "pattern readwrite #" ];
+          password = mqttPassword;
         };
       };
+    };
+    services.home-assistant = {
+      inherit configDir;
+      enable = true;
+      config = {
+        homeassistant = {
+          name = "Home";
+          time_zone = "UTC";
+          latitude = "0.0";
+          longitude = "0.0";
+          elevation = 0;
+        };
+        frontend = {};
+        mqtt = {
+          broker = "127.0.0.1";
+          username = mqttUsername;
+          password = mqttPassword;
+        };
+        binary_sensor = [{
+          platform = "mqtt";
+          state_topic = "home-assistant/test";
+          payload_on = "let_there_be_light";
+          payload_off = "off";
+        }];
+        logger = {
+          default = "info";
+          logs."homeassistant.components.mqtt" = "debug";
+        };
+      };
+      lovelaceConfig = {
+        title = "My Awesome Home";
+        views = [{
+          title = "Example";
+          cards = [{
+            type = "markdown";
+            title = "Lovelace";
+            content = "Welcome to your **Lovelace UI**.";
+          }];
+        }];
+      };
+      lovelaceConfigWritable = true;
+    };
   };
 
   testScript = ''
@@ -76,29 +73,14 @@ in {
         hass.succeed("test -f ${configDir}/ui-lovelace.yaml")
     with subtest("Check that Home Assistant's web interface and API can be reached"):
         hass.wait_for_open_port(8123)
-        hass.succeed("curl --fail http://localhost:8123/states")
-        assert "API running" in hass.succeed(
-            "curl --fail -H 'x-ha-access: ${apiPassword}' http://localhost:8123/api/"
-        )
+        hass.succeed("curl --fail http://localhost:8123/lovelace")
     with subtest("Toggle a binary sensor using MQTT"):
-        assert '"state": "off"' in hass.succeed(
-            "curl http://localhost:8123/api/states/binary_sensor.mqtt_binary_sensor -H 'x-ha-access: ${apiPassword}'"
-        )
+        # wait for broker to become available
         hass.wait_until_succeeds(
-            "mosquitto_pub -V mqttv311 -t home-assistant/test -u homeassistant -P '${mqttPassword}' -m let_there_be_light"
-        )
-        assert '"state": "on"' in hass.succeed(
-            "curl http://localhost:8123/api/states/binary_sensor.mqtt_binary_sensor -H 'x-ha-access: ${apiPassword}'"
-        )
-    with subtest("Toggle a binary sensor using hass-cli"):
-        assert '"state": "on"' in hass.succeed(
-            "${hassCli} --output json state get binary_sensor.mqtt_binary_sensor"
+            "mosquitto_sub -V mqttv311 -t home-assistant/test -u ${mqttUsername} -P '${mqttPassword}' -W 1 -t '*'"
         )
         hass.succeed(
-            "${hassCli} state edit binary_sensor.mqtt_binary_sensor --json='{\"state\": \"off\"}'"
-        )
-        assert '"state": "off"' in hass.succeed(
-            "curl http://localhost:8123/api/states/binary_sensor.mqtt_binary_sensor -H 'x-ha-access: ${apiPassword}'"
+            "mosquitto_pub -V mqttv311 -t home-assistant/test -u ${mqttUsername} -P '${mqttPassword}' -m let_there_be_light"
         )
     with subtest("Print log to ease debugging"):
         output_log = hass.succeed("cat ${configDir}/home-assistant.log")
@@ -107,5 +89,9 @@ in {
 
     with subtest("Check that no errors were logged"):
         assert "ERROR" not in output_log
+
+    # example line: 2020-06-20 10:01:32 DEBUG (MainThread) [homeassistant.components.mqtt] Received message on home-assistant/test: b'let_there_be_light'
+    with subtest("Check we received the mosquitto message"):
+        assert "let_there_be_light" in output_log
   '';
 })
diff --git a/nixos/tests/hostname.nix b/nixos/tests/hostname.nix
new file mode 100644
index 00000000000..3b87303d73e
--- /dev/null
+++ b/nixos/tests/hostname.nix
@@ -0,0 +1,66 @@
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+let
+  makeHostNameTest = hostName: domain:
+    let
+      fqdn = hostName + (optionalString (domain != null) ".${domain}");
+    in
+      makeTest {
+        name = "hostname-${fqdn}";
+        meta = with pkgs.stdenv.lib.maintainers; {
+          maintainers = [ primeos blitz ];
+        };
+
+        machine = { lib, ... }: {
+          networking.hostName = hostName;
+          networking.domain = domain;
+
+          environment.systemPackages = with pkgs; [
+            inetutils
+          ];
+        };
+
+        testScript = ''
+          start_all()
+
+          machine = ${hostName}
+
+          machine.wait_for_unit("network-online.target")
+
+          # The FQDN, domain name, and hostname detection should work as expected:
+          assert "${fqdn}" == machine.succeed("hostname --fqdn").strip()
+          assert "${optionalString (domain != null) domain}" == machine.succeed("dnsdomainname").strip()
+          assert (
+              "${hostName}"
+              == machine.succeed(
+                  'hostnamectl status | grep "Static hostname" | cut -d: -f2'
+              ).strip()
+          )
+
+          # 127.0.0.1 and ::1 should resolve back to "localhost":
+          assert (
+              "localhost" == machine.succeed("getent hosts 127.0.0.1 | awk '{print $2}'").strip()
+          )
+          assert "localhost" == machine.succeed("getent hosts ::1 | awk '{print $2}'").strip()
+
+          # 127.0.0.2 should resolve back to the FQDN and hostname:
+          fqdn_and_host_name = "${optionalString (domain != null) "${hostName}.${domain} "}${hostName}"
+          assert (
+              fqdn_and_host_name
+              == machine.succeed("getent hosts 127.0.0.2 | awk '{print $2,$3}'").strip()
+          )
+        '';
+      };
+
+in
+{
+  noExplicitDomain = makeHostNameTest "ahost" null;
+
+  explicitDomain = makeHostNameTest "ahost" "adomain";
+}
diff --git a/nixos/tests/hydra/common.nix b/nixos/tests/hydra/common.nix
index f612717dc96..312c52e889a 100644
--- a/nixos/tests/hydra/common.nix
+++ b/nixos/tests/hydra/common.nix
@@ -37,6 +37,7 @@
     };
     services.postfix.enable = true;
     nix = {
+      distributedBuilds = true;
       buildMachines = [{
         hostName = "localhost";
         systems = [ system ];
diff --git a/nixos/tests/hydra/db-migration.nix b/nixos/tests/hydra/db-migration.nix
index cf74acfd67a..ca65e2e66aa 100644
--- a/nixos/tests/hydra/db-migration.nix
+++ b/nixos/tests/hydra/db-migration.nix
@@ -61,7 +61,7 @@ with pkgs.lib;
           'curl -L -s http://localhost:3000/build/1 -H "Accept: application/json" |  jq .buildstatus | xargs test 0 -eq'
       )
 
-      out = original.succeed("su -l postgres -c 'psql -d hydra <<< \"\\d+ jobs\" -A'")
+      out = original.succeed("su -l postgres -c 'psql -d hydra <<< \"\\d+ builds\" -A'")
       assert "jobset_id" not in out
 
       original.succeed(
@@ -69,7 +69,7 @@ with pkgs.lib;
       )
       original.wait_for_unit("hydra-init.service")
 
-      out = original.succeed("su -l postgres -c 'psql -d hydra <<< \"\\d+ jobs\" -A'")
+      out = original.succeed("su -l postgres -c 'psql -d hydra <<< \"\\d+ builds\" -A'")
       assert "jobset_id|integer|||" in out
 
       original.succeed("hydra-backfill-ids")
@@ -79,7 +79,7 @@ with pkgs.lib;
       )
       original.wait_for_unit("hydra-init.service")
 
-      out = original.succeed("su -l postgres -c 'psql -d hydra <<< \"\\d+ jobs\" -A'")
+      out = original.succeed("su -l postgres -c 'psql -d hydra <<< \"\\d+ builds\" -A'")
       assert "jobset_id|integer||not null|" in out
 
       original.wait_until_succeeds(
diff --git a/nixos/tests/ihatemoney.nix b/nixos/tests/ihatemoney.nix
index 7df0ea0b691..0451a450580 100644
--- a/nixos/tests/ihatemoney.nix
+++ b/nixos/tests/ihatemoney.nix
@@ -1,5 +1,11 @@
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
 let
-  f = backend: import ./make-test-python.nix ({ pkgs, ... }: {
+  inherit (import ../lib/testing-python.nix { inherit system pkgs; }) makeTest;
+  f = backend: makeTest {
     name = "ihatemoney-${backend}";
     machine = { lib, ... }: {
       services.ihatemoney = {
@@ -24,9 +30,10 @@ let
     testScript = ''
       machine.wait_for_open_port(8000)
       machine.wait_for_unit("uwsgi.service")
+      machine.wait_until_succeeds("curl http://localhost:8000")
 
       assert '"yay"' in machine.succeed(
-          "curl -X POST http://localhost:8000/api/projects -d 'name=yay&id=yay&password=yay&contact_email=yay\@example.com'"
+          "curl -X POST http://localhost:8000/api/projects -d 'name=yay&id=yay&password=yay&contact_email=yay@example.com'"
       )
       owner, timestamp = machine.succeed(
           "stat --printf %U:%G___%Y /var/lib/ihatemoney/secret_key"
@@ -48,7 +55,7 @@ let
 
       assert "ihatemoney" in machine.succeed("curl http://localhost:8000")
     '';
-  });
+  };
 in {
   ihatemoney-sqlite = f "sqlite";
   ihatemoney-postgresql = f "postgresql";
diff --git a/nixos/tests/initrd-network-openvpn/default.nix b/nixos/tests/initrd-network-openvpn/default.nix
new file mode 100644
index 00000000000..bb4c41e6d70
--- /dev/null
+++ b/nixos/tests/initrd-network-openvpn/default.nix
@@ -0,0 +1,145 @@
+import ../make-test-python.nix ({ lib, ...}:
+
+{
+  name = "initrd-network-openvpn";
+
+  nodes =
+    let
+
+      # Inlining of the shared secret for the
+      # OpenVPN server and client
+      secretblock = ''
+        secret [inline]
+        <secret>
+        ${lib.readFile ./shared.key}
+        </secret>
+        '';
+
+    in
+    {
+
+      # Minimal test case to check a successful boot, even with invalid config
+      minimalboot =
+        { ... }:
+        {
+          boot.initrd.network = {
+            enable = true;
+            openvpn = {
+              enable = true;
+              configuration = "/dev/null";
+            };
+          };
+        };
+
+      # initrd VPN client
+      ovpnclient =
+        { ... }:
+        {
+          virtualisation.useBootLoader = true;
+          virtualisation.vlans = [ 1 ];
+
+          boot.initrd = {
+            # This command does not fork to keep the VM in the state where
+            # only the initramfs is loaded
+            preLVMCommands =
+            ''
+              /bin/nc -p 1234 -lke /bin/echo TESTVALUE
+            '';
+
+            network = {
+              enable = true;
+
+              # Work around udhcpc only getting a lease on eth0
+              postCommands = ''
+                /bin/ip addr add 192.168.1.2/24 dev eth1
+              '';
+
+              # Example configuration for OpenVPN
+              # This is the main reason for this test
+              openvpn = {
+                enable = true;
+                configuration = "${./initrd.ovpn}";
+              };
+            };
+          };
+        };
+
+      # VPN server and gateway for ovpnclient between vlan 1 and 2
+      ovpnserver =
+        { ... }:
+        {
+          virtualisation.vlans = [ 1 2 ];
+
+          # Enable NAT and forward port 12345 to port 1234
+          networking.nat = {
+            enable = true;
+            internalInterfaces = [ "tun0" ];
+            externalInterface = "eth2";
+            forwardPorts = [ { destination = "10.8.0.2:1234";
+                               sourcePort = 12345; } ];
+          };
+
+          # Trust tun0 and allow the VPN Server to be reached
+          networking.firewall = {
+            trustedInterfaces = [ "tun0" ];
+            allowedUDPPorts = [ 1194 ];
+          };
+
+          # Minimal OpenVPN server configuration
+          services.openvpn.servers.testserver =
+          {
+            config = ''
+              dev tun0
+              ifconfig 10.8.0.1 10.8.0.2
+              ${secretblock}
+            '';
+          };
+        };
+
+      # Client that resides in the "external" VLAN
+      testclient =
+        { ... }:
+        {
+          virtualisation.vlans = [ 2 ];
+        };
+  };
+
+
+  testScript =
+    ''
+      # Minimal test case, checks whether enabling (with invalid config) harms
+      # the boot process
+      with subtest("Check for successful boot with broken openvpn config"):
+          minimalboot.start()
+          # If we get to multi-user.target, we booted successfully
+          minimalboot.wait_for_unit("multi-user.target")
+          minimalboot.shutdown()
+
+      # Elaborated test case where the ovpnclient (where this module is used)
+      # can be reached by testclient only over ovpnserver.
+      # This is an indirect test for success.
+      with subtest("Check for connection from initrd VPN client, config as file"):
+          ovpnserver.start()
+          testclient.start()
+          ovpnclient.start()
+
+          # Wait until the OpenVPN Server is available
+          ovpnserver.wait_for_unit("openvpn-testserver.service")
+          ovpnserver.succeed("ping -c 1 10.8.0.1")
+
+          # Wait for the client to connect
+          ovpnserver.wait_until_succeeds("ping -c 1 10.8.0.2")
+
+          # Wait until the testclient has network
+          testclient.wait_for_unit("network.target")
+
+          # Check that ovpnclient is reachable over vlan 1
+          ovpnserver.succeed("nc -w 2 192.168.1.2 1234 | grep -q TESTVALUE")
+
+          # Check that ovpnclient is reachable over tun0
+          ovpnserver.succeed("nc -w 2 10.8.0.2 1234 | grep -q TESTVALUE")
+
+          # Check that ovpnclient is reachable from testclient over the gateway
+          testclient.succeed("nc -w 2 192.168.2.3 12345 | grep -q TESTVALUE")
+    '';
+})
diff --git a/nixos/tests/initrd-network-openvpn/initrd.ovpn b/nixos/tests/initrd-network-openvpn/initrd.ovpn
new file mode 100644
index 00000000000..5926a48af00
--- /dev/null
+++ b/nixos/tests/initrd-network-openvpn/initrd.ovpn
@@ -0,0 +1,29 @@
+remote 192.168.1.3
+dev tun
+ifconfig 10.8.0.2 10.8.0.1
+# Only force VLAN 2 through the VPN
+route 192.168.2.0 255.255.255.0 10.8.0.1
+secret [inline]
+<secret>
+#
+# 2048 bit OpenVPN static key
+#
+-----BEGIN OpenVPN Static key V1-----
+553aabe853acdfe51cd6fcfea93dcbb0
+c8797deadd1187606b1ea8f2315eb5e6
+67c0d7e830f50df45686063b189d6c6b
+aab8bb3430cc78f7bb1f78628d5c3742
+0cef4f53a5acab2894905f4499f95d8e
+e69b7b6748b17016f89e19e91481a9fd
+bf8c10651f41a1d4fdf5f438925a6733
+13cec8f04701eb47b8f7ffc48bc3d7af
+65f07bce766015b87c3db4d668c655ff
+be5a69522a8e60ccb217f8521681b45d
+27c0b70bdfbfbb426c7646d80adf7482
+3ddac58b25cb1c1bb100de974478b4c6
+8b45a94261a2405e99810cb2b3abd49f
+21b3198ada87ff3c4e656a008e540a8d
+e7811584363597599cce2040a68ac00e
+f2125540e0f7f4adc37cb3f0d922eeb7
+-----END OpenVPN Static key V1-----
+</secret>
\ No newline at end of file
diff --git a/nixos/tests/initrd-network-openvpn/shared.key b/nixos/tests/initrd-network-openvpn/shared.key
new file mode 100644
index 00000000000..248a91a3e3d
--- /dev/null
+++ b/nixos/tests/initrd-network-openvpn/shared.key
@@ -0,0 +1,21 @@
+#
+# 2048 bit OpenVPN static key
+#
+-----BEGIN OpenVPN Static key V1-----
+553aabe853acdfe51cd6fcfea93dcbb0
+c8797deadd1187606b1ea8f2315eb5e6
+67c0d7e830f50df45686063b189d6c6b
+aab8bb3430cc78f7bb1f78628d5c3742
+0cef4f53a5acab2894905f4499f95d8e
+e69b7b6748b17016f89e19e91481a9fd
+bf8c10651f41a1d4fdf5f438925a6733
+13cec8f04701eb47b8f7ffc48bc3d7af
+65f07bce766015b87c3db4d668c655ff
+be5a69522a8e60ccb217f8521681b45d
+27c0b70bdfbfbb426c7646d80adf7482
+3ddac58b25cb1c1bb100de974478b4c6
+8b45a94261a2405e99810cb2b3abd49f
+21b3198ada87ff3c4e656a008e540a8d
+e7811584363597599cce2040a68ac00e
+f2125540e0f7f4adc37cb3f0d922eeb7
+-----END OpenVPN Static key V1-----
diff --git a/nixos/tests/installed-tests/default.nix b/nixos/tests/installed-tests/default.nix
index b6bdfea2277..889a00d4b56 100644
--- a/nixos/tests/installed-tests/default.nix
+++ b/nixos/tests/installed-tests/default.nix
@@ -50,6 +50,12 @@ let
 
             environment.systemPackages = with pkgs; [ gnome-desktop-testing ];
 
+            # The installed tests need to be added to the test VM’s closure.
+            # Otherwise, their dependencies might not actually be registered
+            # as valid paths in the VM’s Nix store database,
+            # and `nix-store --query` commands run as part of the tests
+            # (for example when building Flatpak runtimes) will fail.
+            environment.variables.TESTED_PACKAGE_INSTALLED_TESTS = "${tested.installedTests}/share";
           };
 
           testScript =
diff --git a/nixos/tests/installed-tests/flatpak.nix b/nixos/tests/installed-tests/flatpak.nix
index 091c9932662..8aeeaca90f6 100644
--- a/nixos/tests/installed-tests/flatpak.nix
+++ b/nixos/tests/installed-tests/flatpak.nix
@@ -5,14 +5,11 @@ makeInstalledTest {
   withX11 = true;
 
   testConfig = {
-    services.xserver.desktopManager.gnome3.enable = true; # TODO: figure out minimal environment where the tests work
-    # common/x11.nix enables the auto display manager (lightdm)
-    services.xserver.displayManager.gdm.enable = false;
-    services.gnome3.core-utilities.enable = false;
+    xdg.portal.enable = true;
     services.flatpak.enable = true;
-    environment.systemPackages = with pkgs; [ gnupg ostree python2 ];
+    environment.systemPackages = with pkgs; [ gnupg ostree python3 ];
     virtualisation.memorySize = 2047;
-    virtualisation.diskSize = 1024;
+    virtualisation.diskSize = 3072;
   };
 
   testRunnerFlags = "--timeout 3600";
diff --git a/nixos/tests/installed-tests/ibus.nix b/nixos/tests/installed-tests/ibus.nix
index af54b612b50..a4bc2a7d7de 100644
--- a/nixos/tests/installed-tests/ibus.nix
+++ b/nixos/tests/installed-tests/ibus.nix
@@ -5,16 +5,12 @@ makeInstalledTest {
 
   testConfig = {
     i18n.inputMethod.enabled = "ibus";
+    systemd.user.services.ibus-daemon = {
+      serviceConfig.ExecStart = "${pkgs.ibus}/bin/ibus-daemon --xim --verbose";
+      wantedBy = [ "graphical-session.target" ];
+      partOf = [ "graphical-session.target" ];
+    };
   };
 
-  preTestScript = ''
-    # ibus has ibus-desktop-testing-runner but it tries to manage desktop session so we just spawn ibus-daemon ourselves
-    machine.succeed("ibus-daemon --daemonize --verbose")
-  '';
-
   withX11 = true;
-
-  # TODO: ibus-daemon is currently crashing or something
-  # maybe make ibus systemd service that auto-restarts?
-  meta.broken = true;
 }
diff --git a/nixos/tests/installed-tests/ostree.nix b/nixos/tests/installed-tests/ostree.nix
index eef7cace54c..90e09ad4ddf 100644
--- a/nixos/tests/installed-tests/ostree.nix
+++ b/nixos/tests/installed-tests/ostree.nix
@@ -3,21 +3,10 @@
 makeInstalledTest {
   tested = pkgs.ostree;
 
-  # TODO: Wrap/patch the tests directly in the package
   testConfig = {
     environment.systemPackages = with pkgs; [
-      (python3.withPackages (p: with p; [ pyyaml ]))
       gnupg
       ostree
     ];
-
-    # for GJS tests
-    environment.variables.GI_TYPELIB_PATH = lib.makeSearchPath "lib/girepository-1.0" (with pkgs; [
-      gtk3
-      pango.out
-      ostree
-      gdk-pixbuf
-      atk
-    ]);
   };
 }
diff --git a/nixos/tests/installer.nix b/nixos/tests/installer.nix
index eef9abebf9f..50c6af485da 100644
--- a/nixos/tests/installer.nix
+++ b/nixos/tests/installer.nix
@@ -64,7 +64,7 @@ let
   # a test script fragment `createPartitions', which must create
   # partitions and filesystems.
   testScriptFun = { bootLoader, createPartitions, grubVersion, grubDevice, grubUseEfi
-                  , grubIdentifier, preBootCommands, extraConfig
+                  , grubIdentifier, preBootCommands, postBootCommands, extraConfig
                   , testSpecialisationConfig
                   }:
     let iface = if grubVersion == 1 then "ide" else "virtio";
@@ -216,6 +216,7 @@ let
       machine = create_machine_named("boot-after-rebuild-switch")
       ${preBootCommands}
       machine.wait_for_unit("network.target")
+      ${postBootCommands}
       machine.shutdown()
 
       # Tests for validating clone configuration entries in grub menu
@@ -238,6 +239,7 @@ let
       with subtest("Set grub to boot the second configuration"):
           machine.succeed("grub-reboot 1")
 
+      ${postBootCommands}
       machine.shutdown()
 
       # Reboot Machine
@@ -252,12 +254,13 @@ let
       with subtest("We should find a file named /etc/gitconfig"):
           machine.succeed("test -e /etc/gitconfig")
 
+      ${postBootCommands}
       machine.shutdown()
     '';
 
 
   makeInstallerTest = name:
-    { createPartitions, preBootCommands ? "", extraConfig ? ""
+    { createPartitions, preBootCommands ? "", postBootCommands ? "", extraConfig ? ""
     , extraInstallerConfig ? {}
     , bootLoader ? "grub" # either "grub" or "systemd-boot"
     , grubVersion ? 2, grubDevice ? "/dev/vda", grubIdentifier ? "uuid", grubUseEfi ? false
@@ -335,7 +338,7 @@ let
       };
 
       testScript = testScriptFun {
-        inherit bootLoader createPartitions preBootCommands
+        inherit bootLoader createPartitions preBootCommands postBootCommands
                 grubVersion grubDevice grubIdentifier grubUseEfi extraConfig
                 testSpecialisationConfig;
       };
@@ -552,16 +555,26 @@ in {
           + " mkpart primary 2048M -1s"  # PV2
           + " set 2 lvm on",
           "udevadm settle",
+          "sleep 1",
           "pvcreate /dev/vda1 /dev/vda2",
+          "sleep 1",
           "vgcreate MyVolGroup /dev/vda1 /dev/vda2",
+          "sleep 1",
           "lvcreate --size 1G --name swap MyVolGroup",
-          "lvcreate --size 2G --name nixos MyVolGroup",
+          "sleep 1",
+          "lvcreate --size 3G --name nixos MyVolGroup",
+          "sleep 1",
           "mkswap -f /dev/MyVolGroup/swap -L swap",
           "swapon -L swap",
           "mkfs.xfs -L nixos /dev/MyVolGroup/nixos",
           "mount LABEL=nixos /mnt",
       )
     '';
+    postBootCommands = ''
+      assert "loaded active" in machine.succeed(
+          "systemctl list-units 'lvm2-pvscan@*' -ql --no-legend | tee /dev/stderr"
+      )
+    '';
   };
 
   # Boot off an encrypted root partition with the default LUKS header format
@@ -786,7 +799,7 @@ in {
           "btrfs subvol create /mnt/badpath/boot",
           "btrfs subvol create /mnt/nixos",
           "btrfs subvol set-default "
-          + "$(btrfs subvol list /mnt | grep 'nixos' | awk '{print \$2}') /mnt",
+          + "$(btrfs subvol list /mnt | grep 'nixos' | awk '{print $2}') /mnt",
           "umount /mnt",
           "mount -o defaults LABEL=root /mnt",
           "mkdir -p /mnt/badpath/boot",  # Help ensure the detection mechanism
diff --git a/nixos/tests/ipfs.nix b/nixos/tests/ipfs.nix
index 4d721aec0c7..9c0ff5306e0 100644
--- a/nixos/tests/ipfs.nix
+++ b/nixos/tests/ipfs.nix
@@ -7,19 +7,33 @@ import ./make-test-python.nix ({ pkgs, ...} : {
   nodes.machine = { ... }: {
     services.ipfs = {
       enable = true;
+      # Also will add a unix domain socket socket API address, see module.
+      startWhenNeeded = true;
       apiAddress = "/ip4/127.0.0.1/tcp/2324";
     };
   };
 
   testScript = ''
     start_all()
-    machine.wait_for_unit("ipfs")
 
-    machine.wait_until_succeeds("ipfs --api /ip4/127.0.0.1/tcp/2324 id")
+    # IPv4 activation
+
+    machine.succeed("ipfs --api /ip4/127.0.0.1/tcp/2324 id")
     ipfs_hash = machine.succeed(
         "echo fnord | ipfs --api /ip4/127.0.0.1/tcp/2324 add | awk '{ print $2 }'"
     )
 
     machine.succeed(f"ipfs cat /ipfs/{ipfs_hash.strip()} | grep fnord")
+
+    # Unix domain socket activation
+
+    machine.stop_job("ipfs")
+
+    ipfs_hash = machine.succeed(
+        "echo fnord2 | ipfs --api /unix/run/ipfs.sock add | awk '{ print $2 }'"
+    )
+    machine.succeed(
+        f"ipfs --api /unix/run/ipfs.sock cat /ipfs/{ipfs_hash.strip()} | grep fnord2"
+    )
   '';
 })
diff --git a/nixos/tests/jitsi-meet.nix b/nixos/tests/jitsi-meet.nix
new file mode 100644
index 00000000000..42762dfdad8
--- /dev/null
+++ b/nixos/tests/jitsi-meet.nix
@@ -0,0 +1,55 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "jitsi-meet";
+  meta = with pkgs.stdenv.lib; {
+    maintainers = teams.jitsi.members;
+  };
+
+  nodes = {
+    client = { nodes, pkgs, ... }: {
+    };
+    server = { config, pkgs, ... }: {
+      services.jitsi-meet = {
+        enable = true;
+        hostName = "server";
+      };
+      services.jitsi-videobridge.openFirewall = true;
+
+      networking.firewall.allowedTCPPorts = [ 80 443 ];
+
+      services.nginx.virtualHosts.server = {
+        enableACME = true;
+        forceSSL = true;
+      };
+
+      security.acme.email = "me@example.org";
+      security.acme.acceptTerms = true;
+      security.acme.server = "https://example.com"; # self-signed only
+    };
+  };
+
+  testScript = ''
+    server.wait_for_unit("jitsi-videobridge2.service")
+    server.wait_for_unit("jicofo.service")
+    server.wait_for_unit("nginx.service")
+    server.wait_for_unit("prosody.service")
+
+    server.wait_until_succeeds(
+        "journalctl -b -u jitsi-videobridge2 -o cat | grep -q 'Performed a successful health check'"
+    )
+    server.wait_until_succeeds(
+        "journalctl -b -u jicofo -o cat | grep -q 'connected .JID: focus@auth.server'"
+    )
+    server.wait_until_succeeds(
+        "journalctl -b -u prosody -o cat | grep -q 'Authenticated as focus@auth.server'"
+    )
+    server.wait_until_succeeds(
+        "journalctl -b -u prosody -o cat | grep -q 'focus.server:component: External component successfully authenticated'"
+    )
+    server.wait_until_succeeds(
+        "journalctl -b -u prosody -o cat | grep -q 'Authenticated as jvb@auth.server'"
+    )
+
+    client.wait_for_unit("network.target")
+    assert "<title>Jitsi Meet</title>" in client.succeed("curl -sSfkL http://server/")
+  '';
+})
diff --git a/nixos/tests/kubernetes/base.nix b/nixos/tests/kubernetes/base.nix
index adb73650689..8cfac10b6dc 100644
--- a/nixos/tests/kubernetes/base.nix
+++ b/nixos/tests/kubernetes/base.nix
@@ -3,13 +3,12 @@
   pkgs ? import ../../.. { inherit system config; }
 }:
 
-with import ../../lib/testing.nix { inherit system pkgs; };
+with import ../../lib/testing-python.nix { inherit system pkgs; };
 with pkgs.lib;
 
 let
   mkKubernetesBaseTest =
     { name, domain ? "my.zyx", test, machines
-    , pkgs ? import <nixpkgs> { inherit system; }
     , extraConfiguration ? null }:
     let
       masterName = head (filter (machineName: any (role: role == "master") machines.${machineName}.roles) (attrNames machines));
@@ -75,10 +74,8 @@ let
       ) machines;
 
       testScript = ''
-        startAll;
-
-        ${test}
-      '';
+        start_all()
+      '' + test;
     };
 
   mkKubernetesMultiNodeTest = attrs: mkKubernetesBaseTest ({
diff --git a/nixos/tests/kubernetes/dns.nix b/nixos/tests/kubernetes/dns.nix
index 638942e1540..890499a0fb8 100644
--- a/nixos/tests/kubernetes/dns.nix
+++ b/nixos/tests/kubernetes/dns.nix
@@ -75,51 +75,75 @@ let
   singleNodeTest = {
     test = ''
       # prepare machine1 for test
-      $machine1->waitUntilSucceeds("kubectl get node machine1.${domain} | grep -w Ready");
-      $machine1->waitUntilSucceeds("docker load < ${redisImage}");
-      $machine1->waitUntilSucceeds("kubectl create -f ${redisPod}");
-      $machine1->waitUntilSucceeds("kubectl create -f ${redisService}");
-      $machine1->waitUntilSucceeds("docker load < ${probeImage}");
-      $machine1->waitUntilSucceeds("kubectl create -f ${probePod}");
+      machine1.wait_until_succeeds("kubectl get node machine1.${domain} | grep -w Ready")
+      machine1.wait_until_succeeds(
+          "docker load < ${redisImage}"
+      )
+      machine1.wait_until_succeeds(
+          "kubectl create -f ${redisPod}"
+      )
+      machine1.wait_until_succeeds(
+          "kubectl create -f ${redisService}"
+      )
+      machine1.wait_until_succeeds(
+          "docker load < ${probeImage}"
+      )
+      machine1.wait_until_succeeds(
+          "kubectl create -f ${probePod}"
+      )
 
       # check if pods are running
-      $machine1->waitUntilSucceeds("kubectl get pod redis | grep Running");
-      $machine1->waitUntilSucceeds("kubectl get pod probe | grep Running");
-      $machine1->waitUntilSucceeds("kubectl get pods -n kube-system | grep 'coredns.*1/1'");
+      machine1.wait_until_succeeds("kubectl get pod redis | grep Running")
+      machine1.wait_until_succeeds("kubectl get pod probe | grep Running")
+      machine1.wait_until_succeeds("kubectl get pods -n kube-system | grep 'coredns.*1/1'")
 
       # check dns on host (dnsmasq)
-      $machine1->succeed("host redis.default.svc.cluster.local");
+      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 -ti probe -- /bin/host redis.default.svc.cluster.local")
     '';
   };
 
   multiNodeTest = {
     test = ''
       # Node token exchange
-      $machine1->waitUntilSucceeds("cp -f /var/lib/cfssl/apitoken.secret /tmp/shared/apitoken.secret");
-      $machine2->waitUntilSucceeds("cat /tmp/shared/apitoken.secret | nixos-kubernetes-node-join");
+      machine1.wait_until_succeeds(
+          "cp -f /var/lib/cfssl/apitoken.secret /tmp/shared/apitoken.secret"
+      )
+      machine2.wait_until_succeeds(
+          "cat /tmp/shared/apitoken.secret | nixos-kubernetes-node-join"
+      )
 
       # prepare machines for test
-      $machine1->waitUntilSucceeds("kubectl get node machine2.${domain} | grep -w Ready");
-      $machine2->waitUntilSucceeds("docker load < ${redisImage}");
-      $machine1->waitUntilSucceeds("kubectl create -f ${redisPod}");
-      $machine1->waitUntilSucceeds("kubectl create -f ${redisService}");
-      $machine2->waitUntilSucceeds("docker load < ${probeImage}");
-      $machine1->waitUntilSucceeds("kubectl create -f ${probePod}");
+      machine1.wait_until_succeeds("kubectl get node machine2.${domain} | grep -w Ready")
+      machine2.wait_until_succeeds(
+          "docker load < ${redisImage}"
+      )
+      machine1.wait_until_succeeds(
+          "kubectl create -f ${redisPod}"
+      )
+      machine1.wait_until_succeeds(
+          "kubectl create -f ${redisService}"
+      )
+      machine2.wait_until_succeeds(
+          "docker load < ${probeImage}"
+      )
+      machine1.wait_until_succeeds(
+          "kubectl create -f ${probePod}"
+      )
 
       # check if pods are running
-      $machine1->waitUntilSucceeds("kubectl get pod redis | grep Running");
-      $machine1->waitUntilSucceeds("kubectl get pod probe | grep Running");
-      $machine1->waitUntilSucceeds("kubectl get pods -n kube-system | grep 'coredns.*1/1'");
+      machine1.wait_until_succeeds("kubectl get pod redis | grep Running")
+      machine1.wait_until_succeeds("kubectl get pod probe | grep Running")
+      machine1.wait_until_succeeds("kubectl get pods -n kube-system | grep 'coredns.*1/1'")
 
       # check dns on hosts (dnsmasq)
-      $machine1->succeed("host redis.default.svc.cluster.local");
-      $machine2->succeed("host redis.default.svc.cluster.local");
+      machine1.succeed("host redis.default.svc.cluster.local")
+      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 -ti probe -- /bin/host redis.default.svc.cluster.local")
     '';
   };
 in {
diff --git a/nixos/tests/kubernetes/rbac.nix b/nixos/tests/kubernetes/rbac.nix
index 3ce7adcd0d7..c922da515d9 100644
--- a/nixos/tests/kubernetes/rbac.nix
+++ b/nixos/tests/kubernetes/rbac.nix
@@ -94,43 +94,67 @@ let
 
   singlenode = base // {
     test = ''
-      $machine1->waitUntilSucceeds("kubectl get node machine1.my.zyx | grep -w Ready");
-
-      $machine1->waitUntilSucceeds("docker load < ${kubectlImage}");
-
-      $machine1->waitUntilSucceeds("kubectl apply -f ${roServiceAccount}");
-      $machine1->waitUntilSucceeds("kubectl apply -f ${roRole}");
-      $machine1->waitUntilSucceeds("kubectl apply -f ${roRoleBinding}");
-      $machine1->waitUntilSucceeds("kubectl create -f ${kubectlPod}");
-
-      $machine1->waitUntilSucceeds("kubectl get pod kubectl | grep Running");
-
-      $machine1->waitUntilSucceeds("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 get node machine1.my.zyx | grep -w Ready")
+
+      machine1.wait_until_succeeds(
+          "docker load < ${kubectlImage}"
+      )
+
+      machine1.wait_until_succeeds(
+          "kubectl apply -f ${roServiceAccount}"
+      )
+      machine1.wait_until_succeeds(
+          "kubectl apply -f ${roRole}"
+      )
+      machine1.wait_until_succeeds(
+          "kubectl apply -f ${roRoleBinding}"
+      )
+      machine1.wait_until_succeeds(
+          "kubectl create -f ${kubectlPod}"
+      )
+
+      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")
     '';
   };
 
   multinode = base // {
     test = ''
       # Node token exchange
-      $machine1->waitUntilSucceeds("cp -f /var/lib/cfssl/apitoken.secret /tmp/shared/apitoken.secret");
-      $machine2->waitUntilSucceeds("cat /tmp/shared/apitoken.secret | nixos-kubernetes-node-join");
-
-      $machine1->waitUntilSucceeds("kubectl get node machine2.my.zyx | grep -w Ready");
-
-      $machine2->waitUntilSucceeds("docker load < ${kubectlImage}");
-
-      $machine1->waitUntilSucceeds("kubectl apply -f ${roServiceAccount}");
-      $machine1->waitUntilSucceeds("kubectl apply -f ${roRole}");
-      $machine1->waitUntilSucceeds("kubectl apply -f ${roRoleBinding}");
-      $machine1->waitUntilSucceeds("kubectl create -f ${kubectlPod}");
-
-      $machine1->waitUntilSucceeds("kubectl get pod kubectl | grep Running");
-
-      $machine1->waitUntilSucceeds("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(
+          "cp -f /var/lib/cfssl/apitoken.secret /tmp/shared/apitoken.secret"
+      )
+      machine2.wait_until_succeeds(
+          "cat /tmp/shared/apitoken.secret | nixos-kubernetes-node-join"
+      )
+
+      machine1.wait_until_succeeds("kubectl get node machine2.my.zyx | grep -w Ready")
+
+      machine2.wait_until_succeeds(
+          "docker load < ${kubectlImage}"
+      )
+
+      machine1.wait_until_succeeds(
+          "kubectl apply -f ${roServiceAccount}"
+      )
+      machine1.wait_until_succeeds(
+          "kubectl apply -f ${roRole}"
+      )
+      machine1.wait_until_succeeds(
+          "kubectl apply -f ${roRoleBinding}"
+      )
+      machine1.wait_until_succeeds(
+          "kubectl create -f ${kubectlPod}"
+      )
+
+      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")
     '';
   };
 
diff --git a/nixos/tests/leaps.nix b/nixos/tests/leaps.nix
index 65b475d734e..ac0c602d445 100644
--- a/nixos/tests/leaps.nix
+++ b/nixos/tests/leaps.nix
@@ -7,7 +7,7 @@ import ./make-test-python.nix ({ pkgs,  ... }:
   };
 
   nodes =
-    { 
+    {
       client = { };
 
       server =
diff --git a/nixos/tests/lorri/default.nix b/nixos/tests/lorri/default.nix
index 198171082d8..c33c7503993 100644
--- a/nixos/tests/lorri/default.nix
+++ b/nixos/tests/lorri/default.nix
@@ -18,7 +18,7 @@ import ../make-test-python.nix {
     machine.wait_until_succeeds("grep --fixed-strings 'ready' lorri.stdout")
 
     # Ping the daemon
-    machine.succeed("lorri internal__ping shell.nix")
+    machine.succeed("lorri internal ping shell.nix")
 
     # Wait for the daemon to finish the build
     machine.wait_until_succeeds("grep --fixed-strings 'Completed' lorri.stdout")
diff --git a/nixos/tests/lxd-nftables.nix b/nixos/tests/lxd-nftables.nix
new file mode 100644
index 00000000000..25517914db8
--- /dev/null
+++ b/nixos/tests/lxd-nftables.nix
@@ -0,0 +1,50 @@
+# This test makes sure that lxd stops implicitly depending on iptables when
+# user enabled nftables.
+#
+# It has been extracted from `lxd.nix` for clarity, and because switching from
+# iptables to nftables requires a full reboot, which is a bit hard inside NixOS
+# tests.
+
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "lxd-nftables";
+  meta = with pkgs.stdenv.lib.maintainers; {
+    maintainers = [ patryk27 ];
+  };
+
+  machine = { lib, ... }: {
+    virtualisation = {
+      lxd.enable = true;
+    };
+
+    networking = {
+      firewall.enable = false;
+      nftables.enable = true;
+      nftables.ruleset = ''
+        table inet filter {
+          chain incoming {
+            type filter hook input priority 0;
+            policy accept;
+          }
+
+          chain forward {
+            type filter hook forward priority 0;
+            policy accept;
+          }
+
+          chain output {
+            type filter hook output priority 0;
+            policy accept;
+          }
+        }
+      '';
+    };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("network.target")
+
+    with subtest("When nftables are enabled, lxd doesn't depend on iptables anymore"):
+        machine.succeed("lsmod | grep nf_tables")
+        machine.fail("lsmod | grep ip_tables")
+  '';
+})
diff --git a/nixos/tests/lxd.nix b/nixos/tests/lxd.nix
new file mode 100644
index 00000000000..db2d44dff55
--- /dev/null
+++ b/nixos/tests/lxd.nix
@@ -0,0 +1,135 @@
+import ./make-test-python.nix ({ pkgs, ...} :
+
+let
+  # Since we don't have access to the internet during the tests, we have to
+  # pre-fetch lxd containers beforehand.
+  #
+  # I've chosen to import Alpine Linux, because its image is turbo-tiny and,
+  # generally, sufficient for our tests.
+
+  alpine-meta = pkgs.fetchurl {
+    url = "https://uk.images.linuxcontainers.org/images/alpine/3.11/i386/default/20200608_13:00/lxd.tar.xz";
+    sha256 = "1hkvaj3rr333zmx1759njy435lps33gl4ks8zfm7m4nqvipm26a0";
+  };
+
+  alpine-rootfs = pkgs.fetchurl {
+    url = "https://uk.images.linuxcontainers.org/images/alpine/3.11/i386/default/20200608_13:00/rootfs.tar.xz";
+    sha256 = "1v82zdra4j5xwsff09qlp7h5vbsg54s0j7rdg4rynichfid3r347";
+  };
+
+  lxd-config = pkgs.writeText "config.yaml" ''
+    storage_pools:
+      - name: default
+        driver: dir
+        config:
+          source: /var/lxd-pool
+
+    networks:
+      - name: lxdbr0
+        type: bridge
+        config:
+          ipv4.address: auto
+          ipv6.address: none
+
+    profiles:
+      - name: default
+        devices:
+          eth0:
+            name: eth0
+            network: lxdbr0
+            type: nic
+          root:
+            path: /
+            pool: default
+            type: disk
+  '';
+
+in {
+  name = "lxd";
+  meta = with pkgs.stdenv.lib.maintainers; {
+    maintainers = [ patryk27 ];
+  };
+
+  machine = { lib, ... }: {
+    virtualisation = {
+      # Since we're testing `limits.cpu`, we've gotta have a known number of
+      # cores to lay on
+      cores = 2;
+
+      # Ditto, for `limits.memory`
+      memorySize = 512;
+
+      lxc.lxcfs.enable = true;
+      lxd.enable = true;
+    };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("sockets.target")
+    machine.wait_for_unit("lxd.service")
+
+    # It takes additional second for lxd to settle
+    machine.sleep(1)
+
+    # lxd expects the pool's directory to already exist
+    machine.succeed("mkdir /var/lxd-pool")
+
+    machine.succeed(
+        "cat ${lxd-config} | lxd init --preseed"
+    )
+
+    machine.succeed(
+        "lxc image import ${alpine-meta} ${alpine-rootfs} --alias alpine"
+    )
+
+    with subtest("Containers can be launched and destroyed"):
+        machine.succeed("lxc launch alpine test")
+        machine.succeed("lxc exec test true")
+        machine.succeed("lxc delete -f test")
+
+    with subtest("Containers are being mounted with lxcfs inside"):
+        machine.succeed("lxc launch alpine test")
+
+        ## ---------- ##
+        ## limits.cpu ##
+
+        machine.succeed("lxc config set test limits.cpu 1")
+
+        # Since Alpine doesn't have `nproc` pre-installed, we've gotta resort
+        # to the primal methods
+        assert (
+            "1"
+            == machine.succeed("lxc exec test grep -- -c ^processor /proc/cpuinfo").strip()
+        )
+
+        machine.succeed("lxc config set test limits.cpu 2")
+
+        assert (
+            "2"
+            == machine.succeed("lxc exec test grep -- -c ^processor /proc/cpuinfo").strip()
+        )
+
+        ## ------------- ##
+        ## limits.memory ##
+
+        machine.succeed("lxc config set test limits.memory 64MB")
+
+        assert (
+            "MemTotal:          62500 kB"
+            == machine.succeed("lxc exec test grep -- MemTotal /proc/meminfo").strip()
+        )
+
+        machine.succeed("lxc config set test limits.memory 128MB")
+
+        assert (
+            "MemTotal:         125000 kB"
+            == machine.succeed("lxc exec test grep -- MemTotal /proc/meminfo").strip()
+        )
+
+        machine.succeed("lxc delete -f test")
+
+    with subtest("Unless explicitly changed, lxd leans on iptables"):
+        machine.succeed("lsmod | grep ip_tables")
+        machine.fail("lsmod | grep nf_tables")
+  '';
+})
diff --git a/nixos/tests/mathics.nix b/nixos/tests/mathics.nix
deleted file mode 100644
index fcbeeb18a72..00000000000
--- a/nixos/tests/mathics.nix
+++ /dev/null
@@ -1,20 +0,0 @@
-import ./make-test.nix ({ pkgs, ... }: {
-  name = "mathics";
-  meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ benley ];
-  };
-
-  nodes = {
-    machine = { ... }: {
-      services.mathics.enable = true;
-      services.mathics.port = 8888;
-    };
-  };
-
-  testScript = ''
-    startAll;
-    $machine->waitForUnit("mathics.service");
-    $machine->waitForOpenPort(8888);
-    $machine->succeed("curl http://localhost:8888/");
-  '';
-})
diff --git a/nixos/tests/matrix-synapse.nix b/nixos/tests/matrix-synapse.nix
index f3623aa3c09..9ca80872176 100644
--- a/nixos/tests/matrix-synapse.nix
+++ b/nixos/tests/matrix-synapse.nix
@@ -29,8 +29,8 @@ import ./make-test-python.nix ({ pkgs, ... } : let
 in {
 
   name = "matrix-synapse";
-  meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ corngood ];
+  meta = with pkgs.stdenv.lib; {
+    maintainers = teams.matrix.members;
   };
 
   nodes = {
diff --git a/nixos/tests/mesos.nix b/nixos/tests/mesos.nix
deleted file mode 100644
index 2e6dc0eda06..00000000000
--- a/nixos/tests/mesos.nix
+++ /dev/null
@@ -1,92 +0,0 @@
-import ./make-test.nix ({ pkgs, ...} : rec {
-  name = "mesos";
-  meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ offline kamilchm cstrahan ];
-  };
-
-  nodes = {
-    master = { ... }: {
-      networking.firewall.enable = false;
-      services.zookeeper.enable = true;
-      services.mesos.master = {
-          enable = true;
-          zk = "zk://master:2181/mesos";
-      };
-    };
-
-    slave = { ... }: {
-      networking.firewall.enable = false;
-      networking.nat.enable = true;
-      virtualisation.docker.enable = true;
-      services.mesos = {
-        slave = {
-          enable = true;
-          master = "master:5050";
-          dockerRegistry = registry;
-          executorEnvironmentVariables = {
-            PATH = "/run/current-system/sw/bin";
-          };
-        };
-      };
-    };
-  };
-
-  simpleDocker = pkgs.dockerTools.buildImage {
-    name = "echo";
-    tag = "latest";
-    contents = [ pkgs.stdenv.shellPackage pkgs.coreutils ];
-    config = {
-      Env = [
-        # When shell=true, mesos invokes "sh -c '<cmd>'", so make sure "sh" is
-        # on the PATH.
-        "PATH=${pkgs.stdenv.shellPackage}/bin:${pkgs.coreutils}/bin"
-      ];
-      Entrypoint = [ "echo" ];
-    };
-  };
-
-  registry = pkgs.runCommand "registry" { } ''
-    mkdir -p $out
-    cp ${simpleDocker} $out/echo:latest.tar
-  '';
-
-  testFramework = pkgs.pythonPackages.buildPythonPackage {
-    name = "mesos-tests";
-    propagatedBuildInputs = [ pkgs.mesos ];
-    catchConflicts = false;
-    src = ./mesos_test.py;
-    phases = [ "installPhase" "fixupPhase" ];
-    installPhase = ''
-      install -Dvm 0755 $src $out/bin/mesos_test.py
-
-      echo "done" > test.result
-      tar czf $out/test.tar.gz test.result
-    '';
-  };
-
-  testScript =
-    ''
-      startAll;
-      $master->waitForUnit("zookeeper.service");
-      $master->waitForUnit("mesos-master.service");
-      $slave->waitForUnit("docker.service");
-      $slave->waitForUnit("mesos-slave.service");
-      $master->waitForOpenPort(2181);
-      $master->waitForOpenPort(5050);
-      $slave->waitForOpenPort(5051);
-
-      # is slave registered?
-      $master->waitUntilSucceeds("curl -s --fail http://master:5050/master/slaves".
-                                 " | grep -q \"\\\"hostname\\\":\\\"slave\\\"\"");
-
-      # try to run docker image
-      $master->succeed("${pkgs.mesos}/bin/mesos-execute --master=master:5050".
-                       " --resources=\"cpus:0.1;mem:32\" --name=simple-docker".
-                       " --containerizer=mesos --docker_image=echo:latest".
-                       " --shell=true --command=\"echo done\" | grep -q TASK_FINISHED");
-
-      # simple command with .tar.gz uri
-      $master->succeed("${testFramework}/bin/mesos_test.py master ".
-                       "${testFramework}/test.tar.gz");
-    '';
-})
diff --git a/nixos/tests/mesos_test.py b/nixos/tests/mesos_test.py
deleted file mode 100644
index be8bb32e49a..00000000000
--- a/nixos/tests/mesos_test.py
+++ /dev/null
@@ -1,72 +0,0 @@
-#!/usr/bin/env python
-import uuid
-import time
-import subprocess
-import os
-
-import sys
-
-from mesos.interface import Scheduler
-from mesos.native import MesosSchedulerDriver
-from mesos.interface import mesos_pb2
-
-def log(msg):
-    process = subprocess.Popen("systemd-cat", stdin=subprocess.PIPE)
-    (out,err) = process.communicate(msg)
-
-class NixosTestScheduler(Scheduler):
-    def __init__(self):
-        self.master_ip = sys.argv[1]
-        self.download_uri = sys.argv[2]
-
-    def resourceOffers(self, driver, offers):
-        log("XXX got resource offer")
-
-        offer = offers[0]
-        task = self.new_task(offer)
-        uri = task.command.uris.add()
-        uri.value = self.download_uri
-        task.command.value = "cat test.result"
-        driver.launchTasks(offer.id, [task])
-
-    def statusUpdate(self, driver, update):
-        log("XXX status update")
-        if update.state == mesos_pb2.TASK_FAILED:
-            log("XXX test task failed with message: " + update.message)
-            driver.stop()
-            sys.exit(1)
-        elif update.state == mesos_pb2.TASK_FINISHED:
-            driver.stop()
-            sys.exit(0)
-
-    def new_task(self, offer):
-        task = mesos_pb2.TaskInfo()
-        id = uuid.uuid4()
-        task.task_id.value = str(id)
-        task.slave_id.value = offer.slave_id.value
-        task.name = "task {}".format(str(id))
-
-        cpus = task.resources.add()
-        cpus.name = "cpus"
-        cpus.type = mesos_pb2.Value.SCALAR
-        cpus.scalar.value = 0.1
-
-        mem = task.resources.add()
-        mem.name = "mem"
-        mem.type = mesos_pb2.Value.SCALAR
-        mem.scalar.value = 32
-
-        return task
-
-if __name__ == '__main__':
-    log("XXX framework started")
-
-    framework = mesos_pb2.FrameworkInfo()
-    framework.user = "root"
-    framework.name = "nixos-test-framework"
-    driver = MesosSchedulerDriver(
-        NixosTestScheduler(),
-        framework,
-        sys.argv[1] + ":5050"
-    )
-    driver.run()
diff --git a/nixos/tests/misc.nix b/nixos/tests/misc.nix
index 17260ce6406..ae150553273 100644
--- a/nixos/tests/misc.nix
+++ b/nixos/tests/misc.nix
@@ -20,12 +20,24 @@ import ./make-test-python.nix ({ pkgs, ...} : rec {
         { fsType = "tmpfs";
           options = [ "mode=1777" "noauto" ];
         };
+        # Tests https://discourse.nixos.org/t/how-to-make-a-derivations-executables-have-the-s-permission/8555
+        "/user-mount/point" = {
+          device = "/user-mount/source";
+          fsType = "none";
+          options = [ "bind" "rw" "user" "noauto" ];
+        };
+        "/user-mount/denied-point" = {
+          device = "/user-mount/denied-source";
+          fsType = "none";
+          options = [ "bind" "rw" "noauto" ];
+        };
       };
       systemd.automounts = singleton
         { wantedBy = [ "multi-user.target" ];
           where = "/tmp2";
         };
       users.users.sybil = { isNormalUser = true; group = "wheel"; };
+      users.users.alice = { isNormalUser = true; };
       security.sudo = { enable = true; wheelNeedsPassword = false; };
       boot.kernel.sysctl."vm.swappiness" = 1;
       boot.kernelParams = [ "vsyscall=emulate" ];
@@ -112,6 +124,26 @@ import ./make-test-python.nix ({ pkgs, ...} : rec {
           machine.succeed("touch /tmp2/x")
           machine.succeed("grep '/tmp2 tmpfs' /proc/mounts")
 
+      with subtest(
+          "Whether mounting by a user is possible with the `user` option in fstab (#95444)"
+      ):
+          machine.succeed("mkdir -p /user-mount/source")
+          machine.succeed("touch /user-mount/source/file")
+          machine.succeed("chmod -R a+Xr /user-mount/source")
+          machine.succeed("mkdir /user-mount/point")
+          machine.succeed("chown alice:users /user-mount/point")
+          machine.succeed("su - alice -c 'mount /user-mount/point'")
+          machine.succeed("su - alice -c 'ls /user-mount/point/file'")
+      with subtest(
+          "Whether mounting by a user is denied without the `user` option in  fstab"
+      ):
+          machine.succeed("mkdir -p /user-mount/denied-source")
+          machine.succeed("touch /user-mount/denied-source/file")
+          machine.succeed("chmod -R a+Xr /user-mount/denied-source")
+          machine.succeed("mkdir /user-mount/denied-point")
+          machine.succeed("chown alice:users /user-mount/denied-point")
+          machine.fail("su - alice -c 'mount /user-mount/denied-point'")
+
       with subtest("shell-vars"):
           machine.succeed('[ -n "$NIX_PATH" ]')
 
diff --git a/nixos/tests/molly-brown.nix b/nixos/tests/molly-brown.nix
new file mode 100644
index 00000000000..09ce42726ca
--- /dev/null
+++ b/nixos/tests/molly-brown.nix
@@ -0,0 +1,71 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+  let testString = "NixOS Gemini test successful";
+  in {
+
+    name = "molly-brown";
+    meta = with pkgs.stdenv.lib.maintainers; { maintainers = [ ehmry ]; };
+
+    nodes = {
+
+      geminiServer = { config, pkgs, ... }:
+        let
+          inherit (config.networking) hostName;
+          cfg = config.services.molly-brown;
+        in {
+
+          environment.systemPackages = [
+            (pkgs.writeScriptBin "test-gemini" ''
+              #!${pkgs.python3}/bin/python
+
+              import socket
+              import ssl
+              import tempfile
+              import textwrap
+              import urllib.parse
+
+              url = "gemini://geminiServer/init.gmi"
+              parsed_url = urllib.parse.urlparse(url)
+
+              s = socket.create_connection((parsed_url.netloc, 1965))
+              context = ssl.SSLContext()
+              context.check_hostname = False
+              context.verify_mode = ssl.CERT_NONE
+              s = context.wrap_socket(s, server_hostname=parsed_url.netloc)
+              s.sendall((url + "\r\n").encode("UTF-8"))
+              fp = s.makefile("rb")
+              print(fp.readline().strip())
+              print(fp.readline().strip())
+              print(fp.readline().strip())
+            '')
+          ];
+
+          networking.firewall.allowedTCPPorts = [ cfg.settings.Port ];
+
+          services.molly-brown = {
+            enable = true;
+            docBase = "/tmp/docs";
+            certPath = "/tmp/cert.pem";
+            keyPath = "/tmp/key.pem";
+          };
+
+          systemd.services.molly-brown.preStart = ''
+            ${pkgs.openssl}/bin/openssl genrsa -out "/tmp/key.pem"
+            ${pkgs.openssl}/bin/openssl req -new \
+              -subj "/CN=${config.networking.hostName}" \
+              -key "/tmp/key.pem" -out /tmp/request.pem
+            ${pkgs.openssl}/bin/openssl x509 -req -days 3650 \
+              -in /tmp/request.pem -signkey "/tmp/key.pem" -out "/tmp/cert.pem"
+
+            mkdir -p "${cfg.settings.DocBase}"
+            echo "${testString}" > "${cfg.settings.DocBase}/test.gmi"
+          '';
+        };
+    };
+    testScript = ''
+      geminiServer.wait_for_unit("molly-brown")
+      geminiServer.wait_for_open_port(1965)
+      geminiServer.succeed("test-gemini")
+    '';
+
+  })
diff --git a/nixos/tests/mongodb.nix b/nixos/tests/mongodb.nix
index a637ec4bfc0..1a712388301 100644
--- a/nixos/tests/mongodb.nix
+++ b/nixos/tests/mongodb.nix
@@ -15,7 +15,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
       node.wait_for_open_port(27017)
 
       assert "hello" in node.succeed(
-          "mongo ${testQuery}"
+          "${pkg}/bin/mongo ${testQuery}"
       )
 
       node.execute(
@@ -36,6 +36,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
           mongodb-3_4
           mongodb-3_6
           mongodb-4_0
+          mongodb-4_2
         ];
       };
     };
@@ -44,8 +45,9 @@ import ./make-test-python.nix ({ pkgs, ... }:
       node.start()
     ''
       + runMongoDBTest pkgs.mongodb-3_4
-      + runMongoDBTest pkgs.mongodb-3_6 
+      + runMongoDBTest pkgs.mongodb-3_6
       + runMongoDBTest pkgs.mongodb-4_0
+      + runMongoDBTest pkgs.mongodb-4_2
       + ''
         node.shutdown()
       '';
diff --git a/nixos/tests/mysql/mariadb-galera-mariabackup.nix b/nixos/tests/mysql/mariadb-galera-mariabackup.nix
index 73abf6c555f..cae55878060 100644
--- a/nixos/tests/mysql/mariadb-galera-mariabackup.nix
+++ b/nixos/tests/mysql/mariadb-galera-mariabackup.nix
@@ -55,9 +55,9 @@ in {
           };
           galera = {
             wsrep_on = "ON";
-            wsrep_debug = "OFF";
+            wsrep_debug = "NONE";
             wsrep_retry_autocommit = "3";
-            wsrep_provider = "${pkgs.mariadb-galera_25}/lib/galera/libgalera_smm.so";
+            wsrep_provider = "${pkgs.mariadb-galera}/lib/galera/libgalera_smm.so";
             wsrep_cluster_address = "gcomm://";
             wsrep_cluster_name = "galera";
             wsrep_node_address = "192.168.1.1";
@@ -102,9 +102,9 @@ in {
           };
           galera = {
             wsrep_on = "ON";
-            wsrep_debug = "OFF";
+            wsrep_debug = "NONE";
             wsrep_retry_autocommit = "3";
-            wsrep_provider = "${pkgs.mariadb-galera_25}/lib/galera/libgalera_smm.so";
+            wsrep_provider = "${pkgs.mariadb-galera}/lib/galera/libgalera_smm.so";
             wsrep_cluster_address = "gcomm://galera_01,galera_02,galera_03";
             wsrep_cluster_name = "galera";
             wsrep_node_address = "192.168.1.2";
@@ -149,9 +149,9 @@ in {
           };
           galera = {
             wsrep_on = "ON";
-            wsrep_debug = "OFF";
+            wsrep_debug = "NONE";
             wsrep_retry_autocommit = "3";
-            wsrep_provider = "${pkgs.mariadb-galera_25}/lib/galera/libgalera_smm.so";
+            wsrep_provider = "${pkgs.mariadb-galera}/lib/galera/libgalera_smm.so";
             wsrep_cluster_address = "gcomm://galera_01,galera_02,galera_03";
             wsrep_cluster_name = "galera";
             wsrep_node_address = "192.168.1.3";
@@ -184,17 +184,17 @@ in {
     galera_03.wait_for_unit("mysql")
     galera_03.wait_for_open_port(3306)
     galera_02.succeed(
-        "sudo -u testuser mysql -u root -e 'use testdb; select test_id from db1;' -N | grep 37"
+        "sudo -u testuser mysql -u testuser -e 'use testdb; select test_id from db1;' -N | grep 37"
     )
     galera_02.succeed(
-        "sudo -u testuser mysql -u root -e 'use testdb; create table db2 (test_id INT, PRIMARY KEY (test_id)) ENGINE = InnoDB;'"
+        "sudo -u testuser mysql -u testuser -e 'use testdb; create table db2 (test_id INT, PRIMARY KEY (test_id)) ENGINE = InnoDB;'"
     )
     galera_02.succeed("systemctl stop mysql")
     galera_01.succeed(
         "sudo -u testuser mysql -u testuser -e 'use testdb; insert into db2 values (38);'"
     )
     galera_03.succeed(
-        "sudo -u testuser mysql -u root -e 'use testdb; create table db3 (test_id INT, PRIMARY KEY (test_id)) ENGINE = InnoDB;'"
+        "sudo -u testuser mysql -u testuser -e 'use testdb; create table db3 (test_id INT, PRIMARY KEY (test_id)) ENGINE = InnoDB;'"
     )
     galera_01.succeed(
         "sudo -u testuser mysql -u testuser -e 'use testdb; insert into db3 values (39);'"
@@ -202,22 +202,22 @@ in {
     galera_02.succeed("systemctl start mysql")
     galera_02.wait_for_open_port(3306)
     galera_02.succeed(
-        "sudo -u testuser mysql -u root -e 'show status' -N | grep 'wsrep_cluster_size.*3'"
+        "sudo -u testuser mysql -u testuser -e 'show status' -N | grep 'wsrep_cluster_size.*3'"
     )
     galera_03.succeed(
-        "sudo -u testuser mysql -u root -e 'show status' -N | grep 'wsrep_local_state_comment.*Synced'"
+        "sudo -u testuser mysql -u testuser -e 'show status' -N | grep 'wsrep_local_state_comment.*Synced'"
     )
     galera_01.succeed(
-        "sudo -u testuser mysql -u root -e 'use testdb; select test_id from db3;' -N | grep 39"
+        "sudo -u testuser mysql -u testuser -e 'use testdb; select test_id from db3;' -N | grep 39"
     )
     galera_02.succeed(
-        "sudo -u testuser mysql -u root -e 'use testdb; select test_id from db2;' -N | grep 38"
+        "sudo -u testuser mysql -u testuser -e 'use testdb; select test_id from db2;' -N | grep 38"
     )
     galera_03.succeed(
-        "sudo -u testuser mysql -u root -e 'use testdb; select test_id from db1;' -N | grep 37"
+        "sudo -u testuser mysql -u testuser -e 'use testdb; select test_id from db1;' -N | grep 37"
     )
     galera_01.succeed("sudo -u testuser mysql -u testuser -e 'use testdb; drop table db3;'")
-    galera_02.succeed("sudo -u testuser mysql -u root -e 'use testdb; drop table db2;'")
-    galera_03.succeed("sudo -u testuser mysql -u root -e 'use testdb; drop table db1;'")
+    galera_02.succeed("sudo -u testuser mysql -u testuser -e 'use testdb; drop table db2;'")
+    galera_03.succeed("sudo -u testuser mysql -u testuser -e 'use testdb; drop table db1;'")
   '';
 })
diff --git a/nixos/tests/mysql/mariadb-galera-rsync.nix b/nixos/tests/mysql/mariadb-galera-rsync.nix
index cacae4569b5..4318efae8a9 100644
--- a/nixos/tests/mysql/mariadb-galera-rsync.nix
+++ b/nixos/tests/mysql/mariadb-galera-rsync.nix
@@ -51,9 +51,9 @@ in {
           };
           galera = {
             wsrep_on = "ON";
-            wsrep_debug = "OFF";
+            wsrep_debug = "NONE";
             wsrep_retry_autocommit = "3";
-            wsrep_provider = "${pkgs.mariadb-galera_25}/lib/galera/libgalera_smm.so";
+            wsrep_provider = "${pkgs.mariadb-galera}/lib/galera/libgalera_smm.so";
             wsrep_cluster_address = "gcomm://";
             wsrep_cluster_name = "galera-rsync";
             wsrep_node_address = "192.168.2.1";
@@ -97,9 +97,9 @@ in {
           };
           galera = {
             wsrep_on = "ON";
-            wsrep_debug = "OFF";
+            wsrep_debug = "NONE";
             wsrep_retry_autocommit = "3";
-            wsrep_provider = "${pkgs.mariadb-galera_25}/lib/galera/libgalera_smm.so";
+            wsrep_provider = "${pkgs.mariadb-galera}/lib/galera/libgalera_smm.so";
             wsrep_cluster_address = "gcomm://galera_04,galera_05,galera_06";
             wsrep_cluster_name = "galera-rsync";
             wsrep_node_address = "192.168.2.2";
@@ -143,9 +143,9 @@ in {
           };
           galera = {
             wsrep_on = "ON";
-            wsrep_debug = "OFF";
+            wsrep_debug = "NONE";
             wsrep_retry_autocommit = "3";
-            wsrep_provider = "${pkgs.mariadb-galera_25}/lib/galera/libgalera_smm.so";
+            wsrep_provider = "${pkgs.mariadb-galera}/lib/galera/libgalera_smm.so";
             wsrep_cluster_address = "gcomm://galera_04,galera_05,galera_06";
             wsrep_cluster_name = "galera-rsync";
             wsrep_node_address = "192.168.2.3";
@@ -177,17 +177,17 @@ in {
     galera_06.wait_for_unit("mysql")
     galera_06.wait_for_open_port(3306)
     galera_05.succeed(
-        "sudo -u testuser mysql -u root -e 'use testdb; select test_id from db1;' -N | grep 41"
+        "sudo -u testuser mysql -u testuser -e 'use testdb; select test_id from db1;' -N | grep 41"
     )
     galera_05.succeed(
-        "sudo -u testuser mysql -u root -e 'use testdb; create table db2 (test_id INT, PRIMARY KEY (test_id)) ENGINE = InnoDB;'"
+        "sudo -u testuser mysql -u testuser -e 'use testdb; create table db2 (test_id INT, PRIMARY KEY (test_id)) ENGINE = InnoDB;'"
     )
     galera_05.succeed("systemctl stop mysql")
     galera_04.succeed(
         "sudo -u testuser mysql -u testuser -e 'use testdb; insert into db2 values (42);'"
     )
     galera_06.succeed(
-        "sudo -u testuser mysql -u root -e 'use testdb; create table db3 (test_id INT, PRIMARY KEY (test_id)) ENGINE = InnoDB;'"
+        "sudo -u testuser mysql -u testuser -e 'use testdb; create table db3 (test_id INT, PRIMARY KEY (test_id)) ENGINE = InnoDB;'"
     )
     galera_04.succeed(
         "sudo -u testuser mysql -u testuser -e 'use testdb; insert into db3 values (43);'"
@@ -195,22 +195,22 @@ in {
     galera_05.succeed("systemctl start mysql")
     galera_05.wait_for_open_port(3306)
     galera_05.succeed(
-        "sudo -u testuser mysql -u root -e 'show status' -N | grep 'wsrep_cluster_size.*3'"
+        "sudo -u testuser mysql -u testuser -e 'show status' -N | grep 'wsrep_cluster_size.*3'"
     )
     galera_06.succeed(
-        "sudo -u testuser mysql -u root -e 'show status' -N | grep 'wsrep_local_state_comment.*Synced'"
+        "sudo -u testuser mysql -u testuser -e 'show status' -N | grep 'wsrep_local_state_comment.*Synced'"
     )
     galera_04.succeed(
-        "sudo -u testuser mysql -u root -e 'use testdb; select test_id from db3;' -N | grep 43"
+        "sudo -u testuser mysql -u testuser -e 'use testdb; select test_id from db3;' -N | grep 43"
     )
     galera_05.succeed(
-        "sudo -u testuser mysql -u root -e 'use testdb; select test_id from db2;' -N | grep 42"
+        "sudo -u testuser mysql -u testuser -e 'use testdb; select test_id from db2;' -N | grep 42"
     )
     galera_06.succeed(
-        "sudo -u testuser mysql -u root -e 'use testdb; select test_id from db1;' -N | grep 41"
+        "sudo -u testuser mysql -u testuser -e 'use testdb; select test_id from db1;' -N | grep 41"
     )
     galera_04.succeed("sudo -u testuser mysql -u testuser -e 'use testdb; drop table db3;'")
-    galera_05.succeed("sudo -u testuser mysql -u root -e 'use testdb; drop table db2;'")
-    galera_06.succeed("sudo -u testuser mysql -u root -e 'use testdb; drop table db1;'")
+    galera_05.succeed("sudo -u testuser mysql -u testuser -e 'use testdb; drop table db2;'")
+    galera_06.succeed("sudo -u testuser mysql -u testuser -e 'use testdb; drop table db1;'")
   '';
 })
diff --git a/nixos/tests/mysql/mysql-replication.nix b/nixos/tests/mysql/mysql-replication.nix
index 81038dccd94..b5e00325019 100644
--- a/nixos/tests/mysql/mysql-replication.nix
+++ b/nixos/tests/mysql/mysql-replication.nix
@@ -59,7 +59,7 @@ in
     master.wait_for_open_port(3306)
     # Wait for testdb to be fully populated (5 rows).
     master.wait_until_succeeds(
-        "mysql -u root -D testdb -N -B -e 'select count(id) from tests' | grep -q 5"
+        "sudo -u mysql mysql -u mysql -D testdb -N -B -e 'select count(id) from tests' | grep -q 5"
     )
 
     slave1.start()
@@ -71,19 +71,21 @@ in
 
     # wait for replications to finish
     slave1.wait_until_succeeds(
-        "mysql -u root -D testdb -N -B -e 'select count(id) from tests' | grep -q 5"
+        "sudo -u mysql mysql -u mysql -D testdb -N -B -e 'select count(id) from tests' | grep -q 5"
     )
     slave2.wait_until_succeeds(
-        "mysql -u root -D testdb -N -B -e 'select count(id) from tests' | grep -q 5"
+        "sudo -u mysql mysql -u mysql -D testdb -N -B -e 'select count(id) from tests' | grep -q 5"
     )
 
     slave2.succeed("systemctl stop mysql")
-    master.succeed("echo 'insert into testdb.tests values (123, 456);' | mysql -u root -N")
+    master.succeed(
+        "echo 'insert into testdb.tests values (123, 456);' | sudo -u mysql mysql -u mysql -N"
+    )
     slave2.succeed("systemctl start mysql")
     slave2.wait_for_unit("mysql")
     slave2.wait_for_open_port(3306)
     slave2.wait_until_succeeds(
-        "echo 'select * from testdb.tests where Id = 123;' | mysql -u root -N | grep 456"
+        "echo 'select * from testdb.tests where Id = 123;' | sudo -u mysql mysql -u mysql -N | grep 456"
     )
   '';
 })
diff --git a/nixos/tests/mysql/mysql.nix b/nixos/tests/mysql/mysql.nix
index d236ce94632..5437a286043 100644
--- a/nixos/tests/mysql/mysql.nix
+++ b/nixos/tests/mysql/mysql.nix
@@ -5,20 +5,34 @@ import ./../make-test-python.nix ({ pkgs, ...} : {
   };
 
   nodes = {
-    mysql =
+    mysql57 =
       { pkgs, ... }:
 
       {
+        users.users.testuser = { };
+        users.users.testuser2 = { };
         services.mysql.enable = true;
         services.mysql.initialDatabases = [
-          { name = "testdb"; schema = ./testdb.sql; }
-          { name = "empty_testdb"; }
+          { name = "testdb3"; schema = ./testdb.sql; }
         ];
         # note that using pkgs.writeText here is generally not a good idea,
         # as it will store the password in world-readable /nix/store ;)
         services.mysql.initialScript = pkgs.writeText "mysql-init.sql" ''
-          CREATE USER 'passworduser'@'localhost' IDENTIFIED BY 'password123';
+          CREATE USER 'testuser3'@'localhost' IDENTIFIED BY 'secure';
+          GRANT ALL PRIVILEGES ON testdb3.* TO 'testuser3'@'localhost';
         '';
+        services.mysql.ensureDatabases = [ "testdb" "testdb2" ];
+        services.mysql.ensureUsers = [{
+          name = "testuser";
+          ensurePermissions = {
+            "testdb.*" = "ALL PRIVILEGES";
+          };
+        } {
+          name = "testuser2";
+          ensurePermissions = {
+            "testdb2.*" = "ALL PRIVILEGES";
+          };
+        }];
         services.mysql.package = pkgs.mysql57;
       };
 
@@ -30,16 +44,30 @@ import ./../make-test-python.nix ({ pkgs, ...} : {
         # Kernel panic - not syncing: Out of memory: compulsory panic_on_oom is enabled
         virtualisation.memorySize = 1024;
 
+        users.users.testuser = { };
+        users.users.testuser2 = { };
         services.mysql.enable = true;
         services.mysql.initialDatabases = [
-          { name = "testdb"; schema = ./testdb.sql; }
-          { name = "empty_testdb"; }
+          { name = "testdb3"; schema = ./testdb.sql; }
         ];
         # note that using pkgs.writeText here is generally not a good idea,
         # as it will store the password in world-readable /nix/store ;)
         services.mysql.initialScript = pkgs.writeText "mysql-init.sql" ''
-          CREATE USER 'passworduser'@'localhost' IDENTIFIED BY 'password123';
+          CREATE USER 'testuser3'@'localhost' IDENTIFIED BY 'secure';
+          GRANT ALL PRIVILEGES ON testdb3.* TO 'testuser3'@'localhost';
         '';
+        services.mysql.ensureDatabases = [ "testdb" "testdb2" ];
+        services.mysql.ensureUsers = [{
+          name = "testuser";
+          ensurePermissions = {
+            "testdb.*" = "ALL PRIVILEGES";
+          };
+        } {
+          name = "testuser2";
+          ensurePermissions = {
+            "testdb2.*" = "ALL PRIVILEGES";
+          };
+        }];
         services.mysql.package = pkgs.mysql80;
       };
 
@@ -81,17 +109,49 @@ import ./../make-test-python.nix ({ pkgs, ...} : {
   testScript = ''
     start_all()
 
-    mysql.wait_for_unit("mysql")
-    mysql.succeed("echo 'use empty_testdb;' | mysql -u root")
-    mysql.succeed("echo 'use testdb; select * from tests;' | mysql -u root -N | grep 4")
-    # ';' acts as no-op, just check whether login succeeds with the user created from the initialScript
-    mysql.succeed("echo ';' | mysql -u passworduser --password=password123")
+    mysql57.wait_for_unit("mysql")
+    mysql57.succeed(
+        "echo 'use testdb; create table tests (test_id INT, PRIMARY KEY (test_id));' | sudo -u testuser mysql -u testuser"
+    )
+    mysql57.succeed(
+        "echo 'use testdb; insert into tests values (41);' | sudo -u testuser mysql -u testuser"
+    )
+    # Ensure testuser2 is not able to insert into testdb as mysql testuser2
+    mysql57.fail(
+        "echo 'use testdb; insert into tests values (22);' | sudo -u testuser2 mysql -u testuser2"
+    )
+    # Ensure testuser2 is not able to authenticate as mysql testuser
+    mysql57.fail(
+        "echo 'use testdb; insert into tests values (22);' | sudo -u testuser2 mysql -u testuser"
+    )
+    mysql57.succeed(
+        "echo 'use testdb; select test_id from tests;' | sudo -u testuser mysql -u testuser -N | grep 41"
+    )
+    mysql57.succeed(
+        "echo 'use testdb3; select * from tests;' | mysql -u testuser3 --password=secure -N | grep 4"
+    )
 
     mysql80.wait_for_unit("mysql")
-    mysql80.succeed("echo 'use empty_testdb;' | mysql -u root")
-    mysql80.succeed("echo 'use testdb; select * from tests;' | mysql -u root -N | grep 4")
-    # ';' acts as no-op, just check whether login succeeds with the user created from the initialScript
-    mysql80.succeed("echo ';' | mysql -u passworduser --password=password123")
+    mysql80.succeed(
+        "echo 'use testdb; create table tests (test_id INT, PRIMARY KEY (test_id));' | sudo -u testuser mysql -u testuser"
+    )
+    mysql80.succeed(
+        "echo 'use testdb; insert into tests values (41);' | sudo -u testuser mysql -u testuser"
+    )
+    # Ensure testuser2 is not able to insert into testdb as mysql testuser2
+    mysql80.fail(
+        "echo 'use testdb; insert into tests values (22);' | sudo -u testuser2 mysql -u testuser2"
+    )
+    # Ensure testuser2 is not able to authenticate as mysql testuser
+    mysql80.fail(
+        "echo 'use testdb; insert into tests values (22);' | sudo -u testuser2 mysql -u testuser"
+    )
+    mysql80.succeed(
+        "echo 'use testdb; select test_id from tests;' | sudo -u testuser mysql -u testuser -N | grep 41"
+    )
+    mysql80.succeed(
+        "echo 'use testdb3; select * from tests;' | mysql -u testuser3 --password=secure -N | grep 4"
+    )
 
     mariadb.wait_for_unit("mysql")
     mariadb.succeed(
@@ -112,32 +172,32 @@ import ./../make-test-python.nix ({ pkgs, ...} : {
         "echo 'use testdb; select test_id from tests;' | sudo -u testuser mysql -u testuser -N | grep 42"
     )
 
-    # Check if TokuDB plugin works
+    # Check if RocksDB plugin works
     mariadb.succeed(
-        "echo 'use testdb; create table tokudb (test_id INT, PRIMARY KEY (test_id)) ENGINE = TokuDB;' | sudo -u testuser mysql -u testuser"
+        "echo 'use testdb; create table rocksdb (test_id INT, PRIMARY KEY (test_id)) ENGINE = RocksDB;' | sudo -u testuser mysql -u testuser"
     )
     mariadb.succeed(
-        "echo 'use testdb; insert into tokudb values (25);' | sudo -u testuser mysql -u testuser"
+        "echo 'use testdb; insert into rocksdb values (28);' | sudo -u testuser mysql -u testuser"
     )
     mariadb.succeed(
-        "echo 'use testdb; select test_id from tokudb;' | sudo -u testuser mysql -u testuser -N | grep 25"
+        "echo 'use testdb; select test_id from rocksdb;' | sudo -u testuser mysql -u testuser -N | grep 28"
     )
     mariadb.succeed(
-        "echo 'use testdb; drop table tokudb;' | sudo -u testuser mysql -u testuser"
+        "echo 'use testdb; drop table rocksdb;' | sudo -u testuser mysql -u testuser"
     )
-
-    # Check if RocksDB plugin works
+  '' + pkgs.stdenv.lib.optionalString pkgs.stdenv.isx86_64 ''
+    # Check if TokuDB plugin works
     mariadb.succeed(
-        "echo 'use testdb; create table rocksdb (test_id INT, PRIMARY KEY (test_id)) ENGINE = RocksDB;' | sudo -u testuser mysql -u testuser"
+        "echo 'use testdb; create table tokudb (test_id INT, PRIMARY KEY (test_id)) ENGINE = TokuDB;' | sudo -u testuser mysql -u testuser"
     )
     mariadb.succeed(
-        "echo 'use testdb; insert into rocksdb values (28);' | sudo -u testuser mysql -u testuser"
+        "echo 'use testdb; insert into tokudb values (25);' | sudo -u testuser mysql -u testuser"
     )
     mariadb.succeed(
-        "echo 'use testdb; select test_id from rocksdb;' | sudo -u testuser mysql -u testuser -N | grep 28"
+        "echo 'use testdb; select test_id from tokudb;' | sudo -u testuser mysql -u testuser -N | grep 25"
     )
     mariadb.succeed(
-        "echo 'use testdb; drop table rocksdb;' | sudo -u testuser mysql -u testuser"
+        "echo 'use testdb; drop table tokudb;' | sudo -u testuser mysql -u testuser"
     )
   '';
 })
diff --git a/nixos/tests/ncdns.nix b/nixos/tests/ncdns.nix
new file mode 100644
index 00000000000..507e20fe7cc
--- /dev/null
+++ b/nixos/tests/ncdns.nix
@@ -0,0 +1,77 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+let
+  fakeReply = pkgs.writeText "namecoin-reply.json" ''
+  { "error": null,
+    "id": 1,
+    "result": {
+      "address": "T31q8ucJ4dI1xzhxQ5QispfECld5c7Xw",
+      "expired": false,
+      "expires_in": 2248,
+      "height": 438155,
+      "name": "d/test",
+      "txid": "db61c0b2540ba0c1a2c8cc92af703a37002e7566ecea4dbf8727c7191421edfb",
+      "value": "{\"ip\": \"1.2.3.4\", \"email\": \"root@test.bit\",\"info\": \"Fake record\"}",
+      "vout": 0
+    }
+  }
+  '';
+in
+
+{
+  name = "ncdns";
+
+  nodes.server = { ... }: {
+    networking.nameservers = [ "127.0.0.1" ];
+
+    services.namecoind.rpc = {
+      address = "127.0.0.1";
+      user = "namecoin";
+      password = "secret";
+      port = 8332;
+    };
+
+    # Fake namecoin RPC server because we can't
+    # run a full node in a test.
+    systemd.services.namecoind = {
+      wantedBy = [ "multi-user.target" ];
+      script = ''
+        while true; do
+          echo -e "HTTP/1.1 200 OK\n\n $(<${fakeReply})\n" \
+            | ${pkgs.netcat}/bin/nc -N -l 127.0.0.1 8332
+        done
+      '';
+    };
+
+    services.ncdns = {
+      enable = true;
+      dnssec.enable = true;
+    };
+
+    services.pdns-recursor = {
+      enable = true;
+      dns.allowFrom = [ "127.0.0.0/8" ];
+      settings.loglevel = 8;
+      resolveNamecoin = true;
+    };
+
+    environment.systemPackages = [ pkgs.dnsutils ];
+
+  };
+
+  testScript = ''
+    with subtest("DNSSEC keys have been generated"):
+        server.wait_for_unit("ncdns")
+        server.wait_for_file("/var/lib/ncdns/bit.key")
+        server.wait_for_file("/var/lib/ncdns/bit-zone.key")
+
+    with subtest("DNSKEY bit record is present"):
+        server.wait_for_unit("pdns-recursor")
+        server.wait_for_open_port("53")
+        server.succeed("host -t DNSKEY bit")
+
+    with subtest("can resolve a .bit name"):
+        server.wait_for_unit("namecoind")
+        server.wait_for_open_port("8332")
+        assert "1.2.3.4" in server.succeed("host -t A test.bit")
+  '';
+})
diff --git a/nixos/tests/networking.nix b/nixos/tests/networking.nix
index 3d8ab761a44..83d4f6465b6 100644
--- a/nixos/tests/networking.nix
+++ b/nixos/tests/networking.nix
@@ -8,7 +8,9 @@ with import ../lib/testing-python.nix { inherit system pkgs; };
 with pkgs.lib;
 
 let
-  router = { config, pkgs, ... }:
+  qemu-flags = import ../lib/qemu-flags.nix { inherit pkgs; };
+
+  router = { config, pkgs, lib, ... }:
     with pkgs.lib;
     let
       vlanIfs = range 1 (length config.virtualisation.vlans);
@@ -30,17 +32,20 @@ let
       services.dhcpd4 = {
         enable = true;
         interfaces = map (n: "eth${toString n}") vlanIfs;
-        extraConfig = ''
-          authoritative;
-        '' + flip concatMapStrings vlanIfs (n: ''
+        extraConfig = flip concatMapStrings vlanIfs (n: ''
           subnet 192.168.${toString n}.0 netmask 255.255.255.0 {
             option routers 192.168.${toString n}.1;
-            # XXX: technically it's _not guaranteed_ that IP addresses will be
-            # issued from the first item in range onwards! We assume that in
-            # our tests however.
             range 192.168.${toString n}.2 192.168.${toString n}.254;
           }
-        '');
+        '')
+        ;
+        machines = flip map vlanIfs (vlan:
+          {
+            hostName = "client${toString vlan}";
+            ethernetAddress = qemu-flags.qemuNicMac vlan 1;
+            ipAddress = "192.168.${toString vlan}.2";
+          }
+        );
       };
       services.radvd = {
         enable = true;
diff --git a/nixos/tests/nextcloud/basic.nix b/nixos/tests/nextcloud/basic.nix
index 92ac5c46e8f..72fb020dca7 100644
--- a/nixos/tests/nextcloud/basic.nix
+++ b/nixos/tests/nextcloud/basic.nix
@@ -9,14 +9,30 @@ in {
 
   nodes = {
     # The only thing the client needs to do is download a file.
-    client = { ... }: {};
+    client = { ... }: {
+      services.davfs2.enable = true;
+      system.activationScripts.davfs2-secrets = ''
+        echo "http://nextcloud/remote.php/webdav/ ${adminuser} ${adminpass}" > /tmp/davfs2-secrets
+        chmod 600 /tmp/davfs2-secrets
+      '';
+      fileSystems = pkgs.lib.mkVMOverride {
+        "/mnt/dav" = {
+          device = "http://nextcloud/remote.php/webdav/";
+          fsType = "davfs";
+          options = let
+            davfs2Conf = (pkgs.writeText "davfs2.conf" "secrets /tmp/davfs2-secrets");
+          in [ "conf=${davfs2Conf}" "x-systemd.automount" "noauto"];
+        };
+      };
+    };
 
-    nextcloud = { config, pkgs, ... }: {
+    nextcloud = { config, pkgs, ... }: let
+      cfg = config;
+    in {
       networking.firewall.allowedTCPPorts = [ 80 ];
 
       services.nextcloud = {
         enable = true;
-        nginx.enable = true;
         hostName = "nextcloud";
         config = {
           # Don't inherit adminuser since "root" is supposed to be the default
@@ -27,6 +43,8 @@ in {
           startAt = "20:00";
         };
       };
+
+      environment.systemPackages = [ cfg.services.nextcloud.occ ];
     };
   };
 
@@ -52,6 +70,8 @@ in {
   in ''
     start_all()
     nextcloud.wait_for_unit("multi-user.target")
+    # This is just to ensure the nextcloud-occ program is working
+    nextcloud.succeed("nextcloud-occ status")
     nextcloud.succeed("curl -sSf http://nextcloud/login")
     nextcloud.succeed(
         "${withRcloneEnv} ${copySharedFile}"
@@ -60,5 +80,6 @@ in {
     client.succeed(
         "${withRcloneEnv} ${diffSharedFile}"
     )
+    assert "hi" in client.succeed("cat /mnt/dav/test-shared-file")
   '';
 })
diff --git a/nixos/tests/nextcloud/with-mysql-and-memcached.nix b/nixos/tests/nextcloud/with-mysql-and-memcached.nix
index 8db630be893..bec3815a3e1 100644
--- a/nixos/tests/nextcloud/with-mysql-and-memcached.nix
+++ b/nixos/tests/nextcloud/with-mysql-and-memcached.nix
@@ -17,7 +17,6 @@ in {
       services.nextcloud = {
         enable = true;
         hostName = "nextcloud";
-        nginx.enable = true;
         https = true;
         caching = {
           apcu = true;
diff --git a/nixos/tests/nextcloud/with-postgresql-and-redis.nix b/nixos/tests/nextcloud/with-postgresql-and-redis.nix
index 95219cac9be..40a208115c3 100644
--- a/nixos/tests/nextcloud/with-postgresql-and-redis.nix
+++ b/nixos/tests/nextcloud/with-postgresql-and-redis.nix
@@ -17,7 +17,6 @@ in {
       services.nextcloud = {
         enable = true;
         hostName = "nextcloud";
-        nginx.enable = true;
         caching = {
           apcu = false;
           redis = true;
diff --git a/nixos/tests/nfs/kerberos.nix b/nixos/tests/nfs/kerberos.nix
index 1f2d0d453ea..078f0b7814c 100644
--- a/nixos/tests/nfs/kerberos.nix
+++ b/nixos/tests/nfs/kerberos.nix
@@ -3,7 +3,7 @@ import ../make-test-python.nix ({ pkgs, lib, ... }:
 with lib;
 
 let
-  krb5 = 
+  krb5 =
     { enable = true;
       domain_realm."nfs.test"   = "NFS.TEST";
       libdefaults.default_realm = "NFS.TEST";
@@ -31,7 +31,7 @@ in
 
 {
   name = "nfsv4-with-kerberos";
- 
+
   nodes = {
     client = { lib, ... }:
       { inherit krb5 users;
diff --git a/nixos/tests/nginx-pubhtml.nix b/nixos/tests/nginx-pubhtml.nix
index 432913cb42d..6e1e605628e 100644
--- a/nixos/tests/nginx-pubhtml.nix
+++ b/nixos/tests/nginx-pubhtml.nix
@@ -2,6 +2,7 @@ import ./make-test-python.nix {
   name = "nginx-pubhtml";
 
   machine = { pkgs, ... }: {
+    systemd.services.nginx.serviceConfig.ProtectHome = "read-only";
     services.nginx.enable = true;
     services.nginx.virtualHosts.localhost = {
       locations."~ ^/\\~([a-z0-9_]+)(/.*)?$".alias = "/home/$1/public_html$2";
diff --git a/nixos/tests/nginx-sandbox.nix b/nixos/tests/nginx-sandbox.nix
new file mode 100644
index 00000000000..bc9d3ba8add
--- /dev/null
+++ b/nixos/tests/nginx-sandbox.nix
@@ -0,0 +1,66 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "nginx-sandbox";
+  meta = with pkgs.stdenv.lib.maintainers; {
+    maintainers = [ izorkin ];
+  };
+
+  # This test checks the creation and reading of a file in sandbox mode. Used simple lua script.
+
+  machine = { pkgs, ... }: {
+    nixpkgs.overlays = [
+      (self: super: {
+        nginx-lua = super.nginx.override {
+          modules = [
+            pkgs.nginxModules.lua
+          ];
+        };
+      })
+    ];
+    services.nginx.enable = true;
+    services.nginx.package = pkgs.nginx-lua;
+    services.nginx.enableSandbox = true;
+    services.nginx.virtualHosts.localhost = {
+      extraConfig = ''
+        location /test1-write {
+          content_by_lua_block {
+            local create = os.execute('${pkgs.coreutils}/bin/mkdir /tmp/test1-read')
+            local create = os.execute('${pkgs.coreutils}/bin/touch /tmp/test1-read/foo.txt')
+            local echo = os.execute('${pkgs.coreutils}/bin/echo worked > /tmp/test1-read/foo.txt')
+          }
+        }
+        location /test1-read {
+          root /tmp;
+        }
+        location /test2-write {
+          content_by_lua_block {
+            local create = os.execute('${pkgs.coreutils}/bin/mkdir /var/web/test2-read')
+            local create = os.execute('${pkgs.coreutils}/bin/touch /var/web/test2-read/bar.txt')
+            local echo = os.execute('${pkgs.coreutils}/bin/echo error-worked > /var/web/test2-read/bar.txt')
+          }
+        }
+        location /test2-read {
+          root /var/web;
+        }
+      '';
+    };
+    users.users.foo.isNormalUser = true;
+  };
+
+  testScript = ''
+    machine.wait_for_unit("nginx")
+    machine.wait_for_open_port(80)
+
+    # Checking write in temporary folder
+    machine.succeed("$(curl -vvv http://localhost/test1-write)")
+    machine.succeed('test "$(curl -fvvv http://localhost/test1-read/foo.txt)" = worked')
+
+    # Checking write in protected folder. In sandbox mode for the nginx service, the folder /var/web is mounted
+    # in read-only mode.
+    machine.succeed("mkdir -p /var/web")
+    machine.succeed("chown nginx:nginx /var/web")
+    machine.succeed("$(curl -vvv http://localhost/test2-write)")
+    assert "404 Not Found" in machine.succeed(
+        "curl -vvv -s http://localhost/test2-read/bar.txt"
+    )
+  '';
+})
diff --git a/nixos/tests/nginx-variants.nix b/nixos/tests/nginx-variants.nix
new file mode 100644
index 00000000000..ca4655391bc
--- /dev/null
+++ b/nixos/tests/nginx-variants.nix
@@ -0,0 +1,33 @@
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+
+builtins.listToAttrs (
+  builtins.map
+    (nginxName:
+      {
+        name = nginxName;
+        value = makeTest {
+          name = "nginx-variant-${nginxName}";
+
+          machine = { pkgs, ... }: {
+            services.nginx = {
+              enable = true;
+              virtualHosts.localhost.locations."/".return = "200 'foo'";
+              package = pkgs."${nginxName}";
+            };
+          };
+
+          testScript = ''
+            machine.wait_for_unit("nginx")
+            machine.wait_for_open_port(80)
+            machine.succeed('test "$(curl -fvvv http://localhost/)" = foo')
+          '';
+        };
+      }
+    )
+    [ "nginxStable" "nginxUnstable" "nginxShibboleth" "openresty" "tengine" ]
+)
diff --git a/nixos/tests/openstack-image.nix b/nixos/tests/openstack-image.nix
index 97c9137fe1d..0b57dfb8e7e 100644
--- a/nixos/tests/openstack-image.nix
+++ b/nixos/tests/openstack-image.nix
@@ -3,30 +3,30 @@
   pkgs ? import ../.. { inherit system config; }
 }:
 
-with import ../lib/testing.nix { inherit system pkgs; };
+with import ../lib/testing-python.nix { inherit system pkgs; };
 with pkgs.lib;
 
 with import common/ec2.nix { inherit makeTest pkgs; };
 
 let
-  image =
-    (import ../lib/eval-config.nix {
-      inherit system;
-      modules = [
-        ../maintainers/scripts/openstack/openstack-image.nix
-        ../modules/testing/test-instrumentation.nix
-        ../modules/profiles/qemu-guest.nix
-        {
-          # Needed by nixos-rebuild due to lack of network access.
-          system.extraDependencies = with pkgs; [
-            stdenv
-          ];
-        }
-      ];
-    }).config.system.build.openstackImage + "/nixos.qcow2";
+  image = (import ../lib/eval-config.nix {
+    inherit system;
+    modules = [
+      ../maintainers/scripts/openstack/openstack-image.nix
+      ../modules/testing/test-instrumentation.nix
+      ../modules/profiles/qemu-guest.nix
+      {
+        # Needed by nixos-rebuild due to lack of network access.
+        system.extraDependencies = with pkgs; [
+          stdenv
+        ];
+      }
+    ];
+  }).config.system.build.openstackImage + "/nixos.qcow2";
 
   sshKeys = import ./ssh-keys.nix pkgs;
   snakeOilPrivateKey = sshKeys.snakeOilPrivateKey.text;
+  snakeOilPrivateKeyFile = pkgs.writeText "private-key" snakeOilPrivateKey;
   snakeOilPublicKey = sshKeys.snakeOilPublicKey;
 
 in {
@@ -39,32 +39,36 @@ in {
       SSH_HOST_ED25519_KEY:${replaceStrings ["\n"] ["|"] snakeOilPrivateKey}
     '';
     script = ''
-      $machine->start;
-      $machine->waitForFile("/etc/ec2-metadata/user-data");
-      $machine->waitForUnit("sshd.service");
+      machine.start()
+      machine.wait_for_file("/etc/ec2-metadata/user-data")
+      machine.wait_for_unit("sshd.service")
 
-      $machine->succeed("grep unknown /etc/ec2-metadata/ami-manifest-path");
+      machine.succeed("grep unknown /etc/ec2-metadata/ami-manifest-path")
 
       # We have no keys configured on the client side yet, so this should fail
-      $machine->fail("ssh -o BatchMode=yes localhost exit");
+      machine.fail("ssh -o BatchMode=yes localhost exit")
 
       # Let's install our client private key
-      $machine->succeed("mkdir -p ~/.ssh");
+      machine.succeed("mkdir -p ~/.ssh")
 
-      $machine->succeed("echo '${snakeOilPrivateKey}' > ~/.ssh/id_ed25519");
-      $machine->succeed("chmod 600 ~/.ssh/id_ed25519");
+      machine.copy_from_host_via_shell(
+          "${snakeOilPrivateKeyFile}", "~/.ssh/id_ed25519"
+      )
+      machine.succeed("chmod 600 ~/.ssh/id_ed25519")
 
       # We haven't configured the host key yet, so this should still fail
-      $machine->fail("ssh -o BatchMode=yes localhost exit");
+      machine.fail("ssh -o BatchMode=yes localhost exit")
 
       # Add the host key; ssh should finally succeed
-      $machine->succeed("echo localhost,127.0.0.1 ${snakeOilPublicKey} > ~/.ssh/known_hosts");
-      $machine->succeed("ssh -o BatchMode=yes localhost exit");
+      machine.succeed(
+          "echo localhost,127.0.0.1 ${snakeOilPublicKey} > ~/.ssh/known_hosts"
+      )
+      machine.succeed("ssh -o BatchMode=yes localhost exit")
 
       # Just to make sure resizing is idempotent.
-      $machine->shutdown;
-      $machine->start;
-      $machine->waitForFile("/etc/ec2-metadata/user-data");
+      machine.shutdown()
+      machine.start()
+      machine.wait_for_file("/etc/ec2-metadata/user-data")
     '';
   };
 
@@ -86,9 +90,9 @@ in {
       }
     '';
     script = ''
-      $machine->start;
-      $machine->waitForFile("/etc/testFile");
-      $machine->succeed("cat /etc/testFile | grep -q 'whoa'");
+      machine.start()
+      machine.wait_for_file("/etc/testFile")
+      assert "whoa" in machine.succeed("cat /etc/testFile")
     '';
   };
 }
diff --git a/nixos/tests/os-prober.nix b/nixos/tests/os-prober.nix
index 6a38f5ca531..be0235a4175 100644
--- a/nixos/tests/os-prober.nix
+++ b/nixos/tests/os-prober.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({pkgs, lib, ...}:
+import ./make-test-python.nix ({pkgs, lib, ...}:
 let
   # A filesystem image with a (presumably) bootable debian
   debianImage = pkgs.vmTools.diskImageFuns.debian9i386 {
@@ -34,9 +34,6 @@ let
     '';
   };
 
-  # options to add the disk to the test vm
-  QEMU_OPTS = "-drive index=2,file=${debianImage}/disk-image.qcow2,read-only,if=virtio";
-
   # a part of the configuration of the test vm
   simpleConfig = {
     boot.loader.grub = {
@@ -71,7 +68,7 @@ in {
   machine = { config, pkgs, ... }: (simpleConfig // {
       imports = [ ../modules/profiles/installation-device.nix
                   ../modules/profiles/base.nix ];
-      virtualisation.memorySize = 1024;
+      virtualisation.memorySize = 1300;
       # The test cannot access the network, so any packages
       # nixos-rebuild needs must be included in the VM.
       system.extraDependencies = with pkgs;
@@ -99,22 +96,28 @@ in {
 
   testScript = ''
     # hack to add the secondary disk
-    $machine->{startCommand} = "QEMU_OPTS=\"\$QEMU_OPTS \"${lib.escapeShellArg QEMU_OPTS} ".$machine->{startCommand};
+    os.environ[
+        "QEMU_OPTS"
+    ] = "-drive index=2,file=${debianImage}/disk-image.qcow2,read-only,if=virtio"
 
-    $machine->start;
-    $machine->succeed("udevadm settle");
-    $machine->waitForUnit("multi-user.target");
+    machine.start()
+    machine.succeed("udevadm settle")
+    machine.wait_for_unit("multi-user.target")
+    print(machine.succeed("lsblk"))
 
     # check that os-prober works standalone
-    $machine->succeed("${pkgs.os-prober}/bin/os-prober | grep /dev/vdb1");
+    machine.succeed(
+        "${pkgs.os-prober}/bin/os-prober | grep /dev/vdb1"
+    )
 
     # rebuild and test that debian is available in the grub menu
-    $machine->succeed("nixos-generate-config");
-    $machine->copyFileFromHost(
+    machine.succeed("nixos-generate-config")
+    machine.copy_from_host(
         "${configFile}",
-        "/etc/nixos/configuration.nix");
-    $machine->succeed("nixos-rebuild boot >&2");
+        "/etc/nixos/configuration.nix",
+    )
+    machine.succeed("nixos-rebuild boot >&2")
 
-    $machine->succeed("egrep 'menuentry.*debian' /boot/grub/grub.cfg");
+    machine.succeed("egrep 'menuentry.*debian' /boot/grub/grub.cfg")
   '';
 })
diff --git a/nixos/tests/pinnwand.nix b/nixos/tests/pinnwand.nix
new file mode 100644
index 00000000000..2204e74b2c2
--- /dev/null
+++ b/nixos/tests/pinnwand.nix
@@ -0,0 +1,86 @@
+import ./make-test-python.nix ({ pkgs, ...}:
+let
+  pythonEnv = pkgs.python3.withPackages (py: with py; [ appdirs toml ]);
+
+  port = 8000;
+  baseUrl = "http://server:${toString port}";
+
+  configureSteck = pkgs.writeScript "configure.py" ''
+    #!${pythonEnv.interpreter}
+    import appdirs
+    import toml
+    import os
+
+    CONFIG = {
+      "base": "${baseUrl}/",
+      "confirm": False,
+      "magic": True,
+      "ignore": True
+    }
+
+    os.makedirs(appdirs.user_config_dir('steck'))
+    with open(os.path.join(appdirs.user_config_dir('steck'), 'steck.toml'), "w") as fd:
+        toml.dump(CONFIG, fd)
+    '';
+in
+{
+  name = "pinnwand";
+  meta = with pkgs.stdenv.lib.maintainers; {
+    maintainers =[ hexa ];
+  };
+
+  nodes = {
+    server = { config, ... }:
+    {
+      networking.firewall.allowedTCPPorts = [
+        port
+      ];
+
+      services.pinnwand = {
+        enable = true;
+        port = port;
+      };
+    };
+
+    client = { pkgs, ... }:
+    {
+      environment.systemPackages = [ pkgs.steck ];
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    server.wait_for_unit("pinnwand.service")
+    client.wait_for_unit("network.target")
+
+    # create steck.toml config file
+    client.succeed("${configureSteck}")
+
+    # wait until the server running pinnwand is reachable
+    client.wait_until_succeeds("ping -c1 server")
+
+    # make sure pinnwand is listening
+    server.wait_until_succeeds("ss -lnp | grep ${toString port}")
+
+    # send the contents of /etc/machine-id
+    response = client.succeed("steck paste /etc/machine-id")
+
+    # parse the steck response
+    raw_url = None
+    removal_link = None
+    for line in response.split("\n"):
+        if line.startswith("View link:"):
+            raw_url = f"${baseUrl}/raw/{line.split('/')[-1]}"
+        if line.startswith("Removal link:"):
+            removal_link = line.split(":", 1)[1]
+
+    # check whether paste matches what we sent
+    client.succeed(f"curl {raw_url} > /tmp/machine-id")
+    client.succeed("diff /tmp/machine-id /etc/machine-id")
+
+    # remove paste and check that it's not available any more
+    client.succeed(f"curl {removal_link}")
+    client.fail(f"curl --fail {raw_url}")
+  '';
+})
diff --git a/nixos/tests/plasma5.nix b/nixos/tests/plasma5.nix
index 2eccfdf47f5..5a603f8cbfb 100644
--- a/nixos/tests/plasma5.nix
+++ b/nixos/tests/plasma5.nix
@@ -14,7 +14,7 @@ import ./make-test-python.nix ({ pkgs, ...} :
     services.xserver.displayManager.sddm.enable = true;
     services.xserver.displayManager.defaultSession = "plasma5";
     services.xserver.desktopManager.plasma5.enable = true;
-    services.xserver.displayManager.sddm.autoLogin = {
+    services.xserver.displayManager.autoLogin = {
       enable = true;
       user = "alice";
     };
diff --git a/nixos/tests/podman.nix b/nixos/tests/podman.nix
index 283db71d9a4..cd8c2b4308c 100644
--- a/nixos/tests/podman.nix
+++ b/nixos/tests/podman.nix
@@ -12,9 +12,6 @@ import ./make-test-python.nix (
         { pkgs, ... }:
         {
           virtualisation.podman.enable = true;
-          virtualisation.containers.users = [
-            "alice"
-          ];
 
           users.users.alice = {
             isNormalUser = true;
@@ -38,23 +35,45 @@ import ./make-test-python.nix (
       start_all()
 
 
-      with subtest("Run container as root"):
+      with subtest("Run container as root with runc"):
           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"
+              "podman run --runtime=runc -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
           )
           podman.succeed("podman ps | grep sleeping")
           podman.succeed("podman stop sleeping")
+          podman.succeed("podman rm sleeping")
 
-      with subtest("Run container rootless"):
+      with subtest("Run container as root with crun"):
+          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"
+          )
+          podman.succeed("podman ps | grep sleeping")
+          podman.succeed("podman stop sleeping")
+          podman.succeed("podman rm sleeping")
+
+      with subtest("Run container rootless with runc"):
+          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"
+              )
+          )
+          podman.succeed(su_cmd("podman ps | grep sleeping"))
+          podman.succeed(su_cmd("podman stop sleeping"))
+          podman.succeed(su_cmd("podman rm sleeping"))
+
+      with subtest("Run container rootless with crun"):
           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"
+                  "podman run --runtime=crun -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
               )
           )
           podman.succeed(su_cmd("podman ps | grep sleeping"))
           podman.succeed(su_cmd("podman stop sleeping"))
+          podman.succeed(su_cmd("podman rm sleeping"))
     '';
   }
 )
diff --git a/nixos/tests/postfix-raise-smtpd-tls-security-level.nix b/nixos/tests/postfix-raise-smtpd-tls-security-level.nix
new file mode 100644
index 00000000000..b3c2156122d
--- /dev/null
+++ b/nixos/tests/postfix-raise-smtpd-tls-security-level.nix
@@ -0,0 +1,44 @@
+let
+  certs = import ./common/acme/server/snakeoil-certs.nix;
+in
+import ./make-test-python.nix {
+  name = "postfix";
+
+  machine = { pkgs, ... }: {
+    imports = [ common/user-account.nix ];
+    services.postfix = {
+      enable = true;
+      enableSubmissions = true;
+      submissionsOptions = {
+        smtpd_tls_security_level = "none";
+      };
+    };
+
+    environment.systemPackages = let
+      checkConfig = pkgs.writeScriptBin "check-config" ''
+        #!${pkgs.python3.interpreter}
+        import sys
+
+        state = 1
+        success = False
+
+        with open("/etc/postfix/master.cf") as masterCf:
+          for line in masterCf:
+            if state == 1 and line.startswith("submissions"):
+              state = 2
+            elif state == 2 and line.startswith(" ") and "smtpd_tls_security_level=encrypt" in line:
+              success = True
+            elif state == 2 and not line.startswith(" "):
+              state == 3
+        if not success:
+          sys.exit(1)
+      '';
+
+    in [ checkConfig ];
+  };
+
+  testScript = ''
+    machine.wait_for_unit("postfix.service")
+    machine.succeed("check-config")
+  '';
+}
diff --git a/nixos/tests/postfix.nix b/nixos/tests/postfix.nix
new file mode 100644
index 00000000000..b0674ca3a0d
--- /dev/null
+++ b/nixos/tests/postfix.nix
@@ -0,0 +1,76 @@
+let
+  certs = import ./common/acme/server/snakeoil-certs.nix;
+in
+import ./make-test-python.nix {
+  name = "postfix";
+
+  machine = { pkgs, ... }: {
+    imports = [ common/user-account.nix ];
+    services.postfix = {
+      enable = true;
+      enableSubmission = true;
+      enableSubmissions = true;
+      sslCACert = certs.ca.cert;
+      sslCert = certs."acme.test".cert;
+      sslKey = certs."acme.test".key;
+      submissionsOptions = {
+          smtpd_sasl_auth_enable = "yes";
+          smtpd_client_restrictions = "permit";
+          milter_macro_daemon_name = "ORIGINATING";
+      };
+    };
+
+    security.pki.certificateFiles = [
+      certs.ca.cert
+    ];
+
+    networking.extraHosts = ''
+      127.0.0.1 acme.test
+    '';
+
+    environment.systemPackages = let
+      sendTestMail = pkgs.writeScriptBin "send-testmail" ''
+        #!${pkgs.python3.interpreter}
+        import smtplib
+
+        with smtplib.SMTP('acme.test') as smtp:
+          smtp.sendmail('root@localhost', 'alice@localhost', 'Subject: Test\n\nTest data.')
+          smtp.quit()
+      '';
+
+      sendTestMailStarttls = pkgs.writeScriptBin "send-testmail-starttls" ''
+        #!${pkgs.python3.interpreter}
+        import smtplib
+        import ssl
+
+        ctx = ssl.create_default_context()
+
+        with smtplib.SMTP('acme.test') as smtp:
+          smtp.ehlo()
+          smtp.starttls(context=ctx)
+          smtp.ehlo()
+          smtp.sendmail('root@localhost', 'alice@localhost', 'Subject: Test STARTTLS\n\nTest data.')
+          smtp.quit()
+      '';
+
+      sendTestMailSmtps = pkgs.writeScriptBin "send-testmail-smtps" ''
+        #!${pkgs.python3.interpreter}
+        import smtplib
+        import ssl
+
+        ctx = ssl.create_default_context()
+
+        with smtplib.SMTP_SSL(host='acme.test', context=ctx) as smtp:
+          smtp.sendmail('root@localhost', 'alice@localhost', 'Subject: Test SMTPS\n\nTest data.')
+          smtp.quit()
+      '';
+    in [ sendTestMail sendTestMailStarttls sendTestMailSmtps ];
+  };
+
+  testScript = ''
+    machine.wait_for_unit("postfix.service")
+    machine.succeed("send-testmail")
+    machine.succeed("send-testmail-starttls")
+    machine.succeed("send-testmail-smtps")
+  '';
+}
diff --git a/nixos/tests/postgresql-wal-receiver.nix b/nixos/tests/postgresql-wal-receiver.nix
index 372dd9d8c1c..c50746aa838 100644
--- a/nixos/tests/postgresql-wal-receiver.nix
+++ b/nixos/tests/postgresql-wal-receiver.nix
@@ -28,6 +28,10 @@ let
     meta.maintainers = with maintainers; [ pacien ];
 
     machine = { ... }: {
+      # Needed because this test uses a non-default 'services.postgresql.dataDir'.
+      systemd.tmpfiles.rules = [
+        "d /var/db/postgresql 0700 postgres postgres"
+      ];
       services.postgresql = {
         package = postgresqlPackage;
         enable = true;
diff --git a/nixos/tests/privacyidea.nix b/nixos/tests/privacyidea.nix
new file mode 100644
index 00000000000..45c7cd37c24
--- /dev/null
+++ b/nixos/tests/privacyidea.nix
@@ -0,0 +1,36 @@
+# Miscellaneous small tests that don't warrant their own VM run.
+
+import ./make-test-python.nix ({ pkgs, ...} : rec {
+  name = "privacyidea";
+  meta = with pkgs.stdenv.lib.maintainers; {
+    maintainers = [ fpletz ];
+  };
+
+  machine = { ... }: {
+    virtualisation.cores = 2;
+    virtualisation.memorySize = 512;
+
+    services.privacyidea = {
+      enable = true;
+      secretKey = "testing";
+      pepper = "testing";
+      adminPasswordFile = pkgs.writeText "admin-password" "testing";
+      adminEmail = "root@localhost";
+    };
+    services.nginx = {
+      enable = true;
+      virtualHosts."_".locations."/".extraConfig = ''
+        uwsgi_pass unix:/run/privacyidea/socket;
+      '';
+    };
+  };
+
+  testScript = ''
+    machine.start()
+    machine.wait_for_unit("multi-user.target")
+    machine.succeed("curl --fail http://localhost | grep privacyIDEA")
+    machine.succeed(
+        "curl --fail http://localhost/auth -F username=admin -F password=testing | grep token"
+    )
+  '';
+})
diff --git a/nixos/tests/prometheus-exporters.nix b/nixos/tests/prometheus-exporters.nix
index 4fc3668cfaf..b912e3425e0 100644
--- a/nixos/tests/prometheus-exporters.nix
+++ b/nixos/tests/prometheus-exporters.nix
@@ -56,6 +56,21 @@ let
  */
 
   exporterTests = {
+     apcupsd = {
+      exporterConfig = {
+        enable = true;
+      };
+      metricProvider = {
+        services.apcupsd.enable = true;
+      };
+      exporterTest = ''
+        wait_for_unit("apcupsd.service")
+        wait_for_open_port(3551)
+        wait_for_unit("prometheus-apcupsd-exporter.service")
+        wait_for_open_port(9162)
+        succeed("curl -sSf http://localhost:9162/metrics | grep -q 'apcupsd_info'")
+      '';
+    };
 
     bind = {
       exporterConfig = {
@@ -202,6 +217,69 @@ let
       '';
     };
 
+    keylight = {
+      # A hardware device is required to properly test this exporter, so just
+      # perform a couple of basic sanity checks that the exporter is running
+      # and requires a target, but cannot reach a specified target.
+      exporterConfig = {
+        enable = true;
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-keylight-exporter.service")
+        wait_for_open_port(9288)
+        succeed(
+            "curl -sS --write-out '%{http_code}' -o /dev/null http://localhost:9288/metrics | grep -q '400'"
+        )
+        succeed(
+            "curl -sS --write-out '%{http_code}' -o /dev/null http://localhost:9288/metrics?target=nosuchdevice | grep -q '500'"
+        )
+      '';
+    };
+
+    lnd = {
+      exporterConfig = {
+        enable = true;
+        lndTlsPath = "/var/lib/lnd/tls.cert";
+        lndMacaroonDir = "/var/lib/lnd";
+      };
+      metricProvider = {
+        systemd.services.prometheus-lnd-exporter.serviceConfig.DynamicUser = false;
+        services.bitcoind.enable = true;
+        services.bitcoind.extraConfig = ''
+          rpcauth=bitcoinrpc:e8fe33f797e698ac258c16c8d7aadfbe$872bdb8f4d787367c26bcfd75e6c23c4f19d44a69f5d1ad329e5adf3f82710f7
+          bitcoind.zmqpubrawblock=tcp://127.0.0.1:28332
+          bitcoind.zmqpubrawtx=tcp://127.0.0.1:28333
+        '';
+        systemd.services.lnd = {
+          serviceConfig.ExecStart = ''
+          ${pkgs.lnd}/bin/lnd \
+            --datadir=/var/lib/lnd \
+            --tlscertpath=/var/lib/lnd/tls.cert \
+            --tlskeypath=/var/lib/lnd/tls.key \
+            --logdir=/var/log/lnd \
+            --bitcoin.active \
+            --bitcoin.mainnet \
+            --bitcoin.node=bitcoind \
+            --bitcoind.rpcuser=bitcoinrpc \
+            --bitcoind.rpcpass=hunter2 \
+            --bitcoind.zmqpubrawblock=tcp://127.0.0.1:28332 \
+            --bitcoind.zmqpubrawtx=tcp://127.0.0.1:28333 \
+            --readonlymacaroonpath=/var/lib/lnd/readonly.macaroon
+          '';
+          serviceConfig.StateDirectory = "lnd";
+          wantedBy = [ "multi-user.target" ];
+          after = [ "network.target" ];
+        };
+      };
+      exporterTest = ''
+        wait_for_unit("lnd.service")
+        wait_for_open_port(10009)
+        wait_for_unit("prometheus-lnd-exporter.service")
+        wait_for_open_port(9092)
+        succeed("curl -sSf localhost:9092/metrics | grep -q '^promhttp_metric_handler'")
+      '';
+    };
+
     mail = {
       exporterConfig = {
         enable = true;
@@ -285,6 +363,31 @@ let
       '';
     };
 
+    modemmanager = {
+      exporterConfig = {
+        enable = true;
+        refreshRate = "10s";
+      };
+      metricProvider = {
+        # ModemManager is installed when NetworkManager is enabled. Ensure it is
+        # started and is wanted by NM and the exporter to start everything up
+        # in the right order.
+        networking.networkmanager.enable = true;
+        systemd.services.ModemManager = {
+          enable = true;
+          wantedBy = [ "NetworkManager.service" "prometheus-modemmanager-exporter.service" ];
+        };
+      };
+      exporterTest = ''
+        wait_for_unit("ModemManager.service")
+        wait_for_unit("prometheus-modemmanager-exporter.service")
+        wait_for_open_port(9539)
+        succeed(
+            "curl -sSf http://localhost:9539/metrics | grep -q 'modemmanager_info'"
+        )
+      '';
+    };
+
     nextcloud = {
       exporterConfig = {
         enable = true;
@@ -397,6 +500,20 @@ let
       '';
     };
 
+    redis = {
+      exporterConfig = {
+        enable = true;
+      };
+      metricProvider.services.redis.enable = true;
+      exporterTest = ''
+        wait_for_unit("redis.service")
+        wait_for_unit("prometheus-redis-exporter.service")
+        wait_for_open_port(6379)
+        wait_for_open_port(9121)
+        wait_until_succeeds("curl -sSf localhost:9121/metrics | grep -q 'redis_up 1'")
+      '';
+    };
+
     rspamd = {
       exporterConfig = {
         enable = true;
diff --git a/nixos/tests/prometheus.nix b/nixos/tests/prometheus.nix
index bce489168f9..af2aa66a552 100644
--- a/nixos/tests/prometheus.nix
+++ b/nixos/tests/prometheus.nix
@@ -158,7 +158,10 @@ in import ./make-test-python.nix {
 
     s3 = { pkgs, ... } : {
       # Minio requires at least 1GiB of free disk space to run.
-      virtualisation.diskSize = 2 * 1024;
+      virtualisation = {
+        diskSize = 2 * 1024;
+        memorySize = 1024;
+      };
       networking.firewall.allowedTCPPorts = [ minioPort ];
 
       services.minio = {
@@ -235,7 +238,7 @@ in import ./make-test-python.nix {
     # Test if the Thanos bucket command is able to retrieve blocks from the S3 bucket
     # and check if the blocks have the correct labels:
     store.succeed(
-        "thanos bucket ls "
+        "thanos tools bucket ls "
         + "--objstore.config-file=${nodes.store.config.services.thanos.store.objstore.config-file} "
         + "--output=json | "
         + "jq .thanos.labels.some_label | "
diff --git a/nixos/tests/pt2-clone.nix b/nixos/tests/pt2-clone.nix
new file mode 100644
index 00000000000..b502172e2ee
--- /dev/null
+++ b/nixos/tests/pt2-clone.nix
@@ -0,0 +1,35 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "pt2-clone";
+  meta = with pkgs.stdenv.lib.maintainers; {
+    maintainers = [ fgaz ];
+  };
+
+  machine = { config, pkgs, ... }: {
+    imports = [
+      ./common/x11.nix
+    ];
+
+    services.xserver.enable = true;
+    sound.enable = true;
+    environment.systemPackages = [ pkgs.pt2-clone ];
+  };
+
+  enableOCR = true;
+
+  testScript =
+    ''
+      machine.wait_for_x()
+      # Add a dummy sound card, or the program won't start
+      machine.execute("modprobe snd-dummy")
+
+      machine.execute("pt2-clone &")
+
+      machine.wait_for_window(r"ProTracker")
+      machine.sleep(5)
+      # One of the few words that actually get recognized
+      if "LENGTH" not in machine.get_screen_text():
+          raise Exception("Program did not start successfully")
+      machine.screenshot("screen")
+    '';
+})
+
diff --git a/nixos/tests/qboot.nix b/nixos/tests/qboot.nix
new file mode 100644
index 00000000000..12aef6decfa
--- /dev/null
+++ b/nixos/tests/qboot.nix
@@ -0,0 +1,13 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "qboot";
+
+  machine = { ... }: {
+    virtualisation.bios = pkgs.qboot;
+  };
+
+  testScript =
+    ''
+      start_all()
+      machine.wait_for_unit("multi-user.target")
+    '';
+})
diff --git a/nixos/tests/radicale.nix b/nixos/tests/radicale.nix
index c81e78a8f99..1d3679c82a2 100644
--- a/nixos/tests/radicale.nix
+++ b/nixos/tests/radicale.nix
@@ -14,9 +14,6 @@ let
 
         [storage]
         filesystem_folder = /tmp/collections
-
-        [logging]
-        debug = True
       '';
     };
     # WARNING: DON'T DO THIS IN PRODUCTION!
@@ -49,13 +46,18 @@ in
         services.radicale.extraArgs = [
           "--export-storage" "/tmp/collections-new"
         ];
+        system.stateVersion = "17.03";
       };
       radicale2_verify = lib.recursiveUpdate radicale2 {
-        services.radicale.extraArgs = [ "--verify-storage" ];
+        services.radicale.extraArgs = [ "--debug" "--verify-storage" ];
+        system.stateVersion = "17.09";
       };
       radicale2 = lib.recursiveUpdate (common args) {
         system.stateVersion = "17.09";
       };
+      radicale3 = lib.recursiveUpdate (common args) {
+        system.stateVersion = "20.09";
+      };
     };
 
     # This tests whether the web interface is accessible to an authenticated user
@@ -117,6 +119,22 @@ in
               retcode == 0 and "VCALENDAR" in output
           ), "Could not read calendar from Radicale 2"
 
-      radicale.succeed("curl --fail http://${user}:${password}@localhost:${port}/.web/")
+          radicale.succeed("curl --fail http://${user}:${password}@localhost:${port}/.web/")
+
+      with subtest("Check Radicale 3 functionality"):
+          radicale.succeed(
+              "${switchToConfig "radicale3"} >&2"
+          )
+          radicale.wait_for_unit("radicale.service")
+          radicale.wait_for_open_port(${port})
+
+          (retcode, output) = radicale.execute(
+              "curl --fail http://${user}:${password}@localhost:${port}/someuser/calendar.ics/"
+          )
+          assert (
+              retcode == 0 and "VCALENDAR" in output
+          ), "Could not read calendar from Radicale 3"
+
+          radicale.succeed("curl --fail http://${user}:${password}@localhost:${port}/.web/")
     '';
 })
diff --git a/nixos/tests/restic.nix b/nixos/tests/restic.nix
index 67bb7f1933d..dad5bdfff27 100644
--- a/nixos/tests/restic.nix
+++ b/nixos/tests/restic.nix
@@ -4,33 +4,50 @@ import ./make-test-python.nix (
     let
       password = "some_password";
       repository = "/tmp/restic-backup";
-      passwordFile = pkgs.writeText "password" "correcthorsebatterystaple";
+      rcloneRepository = "rclone:local:/tmp/restic-rclone-backup";
+
+      passwordFile = "${pkgs.writeText "password" "correcthorsebatterystaple"}";
+      initialize = true;
+      paths = [ "/opt" ];
+      pruneOpts = [
+        "--keep-daily 2"
+        "--keep-weekly 1"
+        "--keep-monthly 1"
+        "--keep-yearly 99"
+      ];
     in
       {
         name = "restic";
 
         meta = with pkgs.stdenv.lib.maintainers; {
-          maintainers = [ bbigras ];
+          maintainers = [ bbigras i077 ];
         };
 
         nodes = {
           server =
-            { ... }:
+            { pkgs, ... }:
               {
                 services.restic.backups = {
                   remotebackup = {
-                    inherit repository;
-                    passwordFile = "${passwordFile}";
-                    initialize = true;
-                    paths = [ "/opt" ];
-                    pruneOpts = [
-                      "--keep-daily 2"
-                      "--keep-weekly 1"
-                      "--keep-monthly 1"
-                      "--keep-yearly 99"
-                    ];
+                    inherit repository passwordFile initialize paths pruneOpts;
+                  };
+                  rclonebackup = {
+                    repository = rcloneRepository;
+                    rcloneConfig = {
+                      type = "local";
+                      one_file_system = true;
+                    };
+
+                    # This gets overridden by rcloneConfig.type
+                    rcloneConfigFile = pkgs.writeText "rclone.conf" ''
+                      [local]
+                      type=ftp
+                    '';
+                    inherit passwordFile initialize paths pruneOpts;
                   };
                 };
+
+                environment.sessionVariables.RCLONE_CONFIG_LOCAL_TYPE = "local";
               };
         };
 
@@ -38,25 +55,35 @@ import ./make-test-python.nix (
           server.start()
           server.wait_for_unit("dbus.socket")
           server.fail(
-              "${pkgs.restic}/bin/restic -r ${repository} -p ${passwordFile} snapshots"
+              "${pkgs.restic}/bin/restic -r ${repository} -p ${passwordFile} snapshots",
+              "${pkgs.restic}/bin/restic -r ${rcloneRepository} -p ${passwordFile} snapshots",
           )
           server.succeed(
               "mkdir -p /opt",
               "touch /opt/some_file",
+              "mkdir -p /tmp/restic-rclone-backup",
               "timedatectl set-time '2016-12-13 13:45'",
               "systemctl start restic-backups-remotebackup.service",
+              "systemctl start restic-backups-rclonebackup.service",
               '${pkgs.restic}/bin/restic -r ${repository} -p ${passwordFile} snapshots -c | grep -e "^1 snapshot"',
+              '${pkgs.restic}/bin/restic -r ${rcloneRepository} -p ${passwordFile} snapshots -c | grep -e "^1 snapshot"',
               "timedatectl set-time '2017-12-13 13:45'",
               "systemctl start restic-backups-remotebackup.service",
+              "systemctl start restic-backups-rclonebackup.service",
               "timedatectl set-time '2018-12-13 13:45'",
               "systemctl start restic-backups-remotebackup.service",
+              "systemctl start restic-backups-rclonebackup.service",
               "timedatectl set-time '2018-12-14 13:45'",
               "systemctl start restic-backups-remotebackup.service",
+              "systemctl start restic-backups-rclonebackup.service",
               "timedatectl set-time '2018-12-15 13:45'",
               "systemctl start restic-backups-remotebackup.service",
+              "systemctl start restic-backups-rclonebackup.service",
               "timedatectl set-time '2018-12-16 13:45'",
               "systemctl start restic-backups-remotebackup.service",
+              "systemctl start restic-backups-rclonebackup.service",
               '${pkgs.restic}/bin/restic -r ${repository} -p ${passwordFile} snapshots -c | grep -e "^4 snapshot"',
+              '${pkgs.restic}/bin/restic -r ${rcloneRepository} -p ${passwordFile} snapshots -c | grep -e "^4 snapshot"',
           )
         '';
       }
diff --git a/nixos/tests/sddm.nix b/nixos/tests/sddm.nix
index a145705250f..f9b961163c3 100644
--- a/nixos/tests/sddm.nix
+++ b/nixos/tests/sddm.nix
@@ -44,8 +44,8 @@ let
       machine = { ... }: {
         imports = [ ./common/user-account.nix ];
         services.xserver.enable = true;
-        services.xserver.displayManager.sddm = {
-          enable = true;
+        services.xserver.displayManager = {
+          sddm.enable = true;
           autoLogin = {
             enable = true;
             user = "alice";
diff --git a/nixos/tests/shattered-pixel-dungeon.nix b/nixos/tests/shattered-pixel-dungeon.nix
new file mode 100644
index 00000000000..cf6ee8db80b
--- /dev/null
+++ b/nixos/tests/shattered-pixel-dungeon.nix
@@ -0,0 +1,29 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "shattered-pixel-dungeon";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ fgaz ];
+  };
+
+  machine = { config, pkgs, ... }: {
+    imports = [
+      ./common/x11.nix
+    ];
+
+    services.xserver.enable = true;
+    environment.systemPackages = [ pkgs.shattered-pixel-dungeon ];
+  };
+
+  enableOCR = true;
+
+  testScript =
+    ''
+      machine.wait_for_x()
+      machine.execute("shattered-pixel-dungeon &")
+      machine.wait_for_window(r"Shattered Pixel Dungeon")
+      machine.sleep(5)
+      if "Enter" not in machine.get_screen_text():
+          raise Exception("Program did not start successfully")
+      machine.screenshot("screen")
+    '';
+})
+
diff --git a/nixos/tests/slurm.nix b/nixos/tests/slurm.nix
index d0e62d15437..a54c5d9db48 100644
--- a/nixos/tests/slurm.nix
+++ b/nixos/tests/slurm.nix
@@ -1,16 +1,52 @@
-import ./make-test-python.nix ({ lib, ... }:
+import ./make-test-python.nix ({ lib, pkgs, ... }:
 let
-    mungekey = "mungeverryweakkeybuteasytointegratoinatest";
-
     slurmconfig = {
-      controlMachine = "control";
-      nodeName = [ "node[1-3] CPUs=1 State=UNKNOWN" ];
-      partitionName = [ "debug Nodes=node[1-3] Default=YES MaxTime=INFINITE State=UP" ];
-      extraConfig = ''
-        AccountingStorageHost=dbd
-        AccountingStorageType=accounting_storage/slurmdbd
-      '';
+      services.slurm = {
+        controlMachine = "control";
+        nodeName = [ "node[1-3] CPUs=1 State=UNKNOWN" ];
+        partitionName = [ "debug Nodes=node[1-3] Default=YES MaxTime=INFINITE State=UP" ];
+        extraConfig = ''
+          AccountingStorageHost=dbd
+          AccountingStorageType=accounting_storage/slurmdbd
+        '';
+      };
+      environment.systemPackages = [ mpitest ];
+      networking.firewall.enable = false;
+      systemd.tmpfiles.rules = [
+        "f /etc/munge/munge.key 0400 munge munge - mungeverryweakkeybuteasytointegratoinatest"
+      ];
     };
+
+    mpitest = let
+      mpitestC = pkgs.writeText "mpitest.c" ''
+        #include <stdio.h>
+        #include <stdlib.h>
+        #include <mpi.h>
+
+        int
+        main (int argc, char *argv[])
+        {
+          int rank, size, length;
+          char name[512];
+
+          MPI_Init (&argc, &argv);
+          MPI_Comm_rank (MPI_COMM_WORLD, &rank);
+          MPI_Comm_size (MPI_COMM_WORLD, &size);
+          MPI_Get_processor_name (name, &length);
+
+          if ( rank == 0 ) printf("size=%d\n", size);
+
+          printf ("%s: hello world from process %d of %d\n", name, rank, size);
+
+          MPI_Finalize ();
+
+          return EXIT_SUCCESS;
+        }
+      '';
+    in pkgs.runCommandNoCC "mpitest" {} ''
+      mkdir -p $out/bin
+      ${pkgs.openmpi}/bin/mpicc ${mpitestC} -o $out/bin/mpitest
+    '';
 in {
   name = "slurm";
 
@@ -21,37 +57,40 @@ in {
     computeNode =
       { ...}:
       {
+        imports = [ slurmconfig ];
         # TODO slurmd port and slurmctld port should be configurations and
         # automatically allowed by the  firewall.
-        networking.firewall.enable = false;
         services.slurm = {
           client.enable = true;
-        } // slurmconfig;
+        };
       };
     in {
 
     control =
       { ...}:
       {
-        networking.firewall.enable = false;
+        imports = [ slurmconfig ];
         services.slurm = {
           server.enable = true;
-        } // slurmconfig;
+        };
       };
 
     submit =
       { ...}:
       {
-        networking.firewall.enable = false;
+        imports = [ slurmconfig ];
         services.slurm = {
           enableStools = true;
-        } // slurmconfig;
+        };
       };
 
     dbd =
       { pkgs, ... } :
       {
         networking.firewall.enable = false;
+        systemd.tmpfiles.rules = [
+          "f /etc/munge/munge.key 0400 munge munge - mungeverryweakkeybuteasytointegratoinatest"
+        ];
         services.slurm.dbdserver = {
           enable = true;
           storagePass = "password123";
@@ -87,24 +126,7 @@ in {
   ''
   start_all()
 
-  # Set up authentification across the cluster
-  for node in [submit, control, dbd, node1, node2, node3]:
-
-      node.wait_for_unit("default.target")
-
-      node.succeed("mkdir /etc/munge")
-      node.succeed(
-          "echo '${mungekey}' > /etc/munge/munge.key"
-      )
-      node.succeed("chmod 0400 /etc/munge/munge.key")
-      node.succeed("chown munge:munge /etc/munge/munge.key")
-      node.succeed("systemctl restart munged")
-
-      node.wait_for_unit("munged")
-
-
-  # Restart the services since they have probably failed due to the munge init
-  # failure
+  # Make sure DBD is up after DB initialzation
   with subtest("can_start_slurmdbd"):
       dbd.succeed("systemctl restart slurmdbd")
       dbd.wait_for_unit("slurmdbd.service")
@@ -137,5 +159,8 @@ in {
           # find the srun job from above in the database
           control.succeed("sleep 5")
           control.succeed("sacct | grep hostname")
+
+  with subtest("run_PMIx_mpitest"):
+      submit.succeed("srun -N 3 --mpi=pmix mpitest | grep size=3")
   '';
 })
diff --git a/nixos/tests/snapcast.nix b/nixos/tests/snapcast.nix
new file mode 100644
index 00000000000..92534f10281
--- /dev/null
+++ b/nixos/tests/snapcast.nix
@@ -0,0 +1,58 @@
+import ./make-test-python.nix ({ pkgs, ...} :
+
+let
+  port = 10004;
+  tcpPort = 10005;
+  httpPort = 10080;
+in {
+  name = "snapcast";
+  meta = with pkgs.stdenv.lib.maintainers; {
+    maintainers = [ hexa ];
+  };
+
+  nodes = {
+    server = {
+      services.snapserver = {
+        enable = true;
+        port = port;
+        tcp.port = tcpPort;
+        http.port = httpPort;
+        streams = {
+          mpd = {
+            type = "pipe";
+            location = "/run/snapserver/mpd";
+          };
+          bluetooth = {
+            type = "pipe";
+            location = "/run/snapserver/bluetooth";
+          };
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    import json
+
+    get_rpc_version = {"id": "1", "jsonrpc": "2.0", "method": "Server.GetRPCVersion"}
+
+    start_all()
+
+    server.wait_for_unit("snapserver.service")
+    server.wait_until_succeeds("ss -ntl | grep -q ${toString port}")
+    server.wait_until_succeeds("ss -ntl | grep -q ${toString tcpPort}")
+    server.wait_until_succeeds("ss -ntl | grep -q ${toString httpPort}")
+
+    with subtest("check that pipes are created"):
+        server.succeed("test -p /run/snapserver/mpd")
+        server.succeed("test -p /run/snapserver/bluetooth")
+
+    with subtest("test tcp json-rpc"):
+        server.succeed(f"echo '{json.dumps(get_rpc_version)}' | nc -w 1 localhost ${toString tcpPort}")
+
+    with subtest("test http json-rpc"):
+        server.succeed(
+            "curl --fail http://localhost:${toString httpPort}/jsonrpc -d '{json.dumps(get_rpc_version)}'"
+        )
+  '';
+})
diff --git a/nixos/tests/sogo.nix b/nixos/tests/sogo.nix
new file mode 100644
index 00000000000..016331a9eed
--- /dev/null
+++ b/nixos/tests/sogo.nix
@@ -0,0 +1,58 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "sogo";
+  meta = with pkgs.stdenv.lib.maintainers; {
+    maintainers = [ ajs124 das_j ];
+  };
+
+  nodes = {
+    sogo = { config, pkgs, ... }: {
+      services.nginx.enable = true;
+
+      services.mysql = {
+        enable = true;
+        package = pkgs.mysql;
+        ensureDatabases = [ "sogo" ];
+        ensureUsers = [{
+          name = "sogo";
+          ensurePermissions = {
+            "sogo.*" = "ALL PRIVILEGES";
+          };
+        }];
+      };
+
+      services.sogo = {
+        enable = true;
+        timezone = "Europe/Berlin";
+        extraConfig = ''
+          WOWorkersCount = 1;
+
+          SOGoUserSources = (
+            {
+              type = sql;
+              userPasswordAlgorithm = md5;
+              viewURL = "mysql://sogo@%2Frun%2Fmysqld%2Fmysqld.sock/sogo/sogo_users";
+              canAuthenticate = YES;
+              id = users;
+            }
+          );
+
+          SOGoProfileURL = "mysql://sogo@%2Frun%2Fmysqld%2Fmysqld.sock/sogo/sogo_user_profile";
+          OCSFolderInfoURL = "mysql://sogo@%2Frun%2Fmysqld%2Fmysqld.sock/sogo/sogo_folder_info";
+          OCSSessionsFolderURL = "mysql://sogo@%2Frun%2Fmysqld%2Fmysqld.sock/sogo/sogo_sessions_folder";
+          OCSEMailAlarmsFolderURL = "mysql://sogo@%2Frun%2Fmysqld%2Fmysqld.sock/sogo/sogo_alarms_folder";
+          OCSStoreURL = "mysql://sogo@%2Frun%2Fmysqld%2Fmysqld.sock/sogo/sogo_store";
+          OCSAclURL = "mysql://sogo@%2Frun%2Fmysqld%2Fmysqld.sock/sogo/sogo_acl";
+          OCSCacheFolderURL = "mysql://sogo@%2Frun%2Fmysqld%2Fmysqld.sock/sogo/sogo_cache_folder";
+        '';
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+    sogo.wait_for_unit("multi-user.target")
+    sogo.wait_for_open_port(20000)
+    sogo.wait_for_open_port(80)
+    sogo.succeed("curl -sSfL http://sogo/SOGo")
+  '';
+})
diff --git a/nixos/tests/sslh.nix b/nixos/tests/sslh.nix
new file mode 100644
index 00000000000..2a800aa52d0
--- /dev/null
+++ b/nixos/tests/sslh.nix
@@ -0,0 +1,83 @@
+import ./make-test-python.nix {
+  name = "sslh";
+
+  nodes = {
+    server = { pkgs, lib, ... }: {
+      networking.firewall.allowedTCPPorts = [ 443 ];
+      networking.interfaces.eth1.ipv6.addresses = [
+        {
+          address = "fe00:aa:bb:cc::2";
+          prefixLength = 64;
+        }
+      ];
+      # sslh is really slow when reverse dns does not work
+      networking.hosts = {
+        "fe00:aa:bb:cc::2" = [ "server" ];
+        "fe00:aa:bb:cc::1" = [ "client" ];
+      };
+      services.sslh = {
+        enable = true;
+        transparent = true;
+        appendConfig = ''
+          protocols:
+          (
+            { name: "ssh"; service: "ssh"; host: "localhost"; port: "22"; probe: "builtin"; },
+            { name: "http"; host: "localhost"; port: "80"; probe: "builtin"; },
+          );
+        '';
+      };
+      services.openssh.enable = true;
+      users.users.root.openssh.authorizedKeys.keyFiles = [ ./initrd-network-ssh/id_ed25519.pub ];
+      services.nginx = {
+        enable = true;
+        virtualHosts."localhost" = {
+          addSSL = false;
+          default = true;
+          root = pkgs.runCommand "testdir" {} ''
+            mkdir "$out"
+            echo hello world > "$out/index.html"
+          '';
+        };
+      };
+    };
+    client = { ... }: {
+      networking.interfaces.eth1.ipv6.addresses = [
+        {
+          address = "fe00:aa:bb:cc::1";
+          prefixLength = 64;
+        }
+      ];
+      networking.hosts."fe00:aa:bb:cc::2" = [ "server" ];
+      environment.etc.sshKey = {
+        source = ./initrd-network-ssh/id_ed25519; # dont use this anywhere else
+        mode = "0600";
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    server.wait_for_unit("sslh.service")
+    server.wait_for_unit("nginx.service")
+    server.wait_for_unit("sshd.service")
+    server.wait_for_open_port(80)
+    server.wait_for_open_port(443)
+    server.wait_for_open_port(22)
+
+    for arg in ["-6", "-4"]:
+        client.wait_until_succeeds(f"ping {arg} -c1 server")
+
+        # check that ssh through sslh works
+        client.succeed(
+            f"ssh {arg} -p 443 -i /etc/sshKey -o StrictHostKeyChecking=accept-new server 'echo $SSH_CONNECTION > /tmp/foo{arg}'"
+        )
+
+        # check that 1/ the above ssh command had an effect 2/ transparent proxying really works
+        ip = "fe00:aa:bb:cc::1" if arg == "-6" else "192.168.1."
+        server.succeed(f"grep '{ip}' /tmp/foo{arg}")
+
+        # check that http through sslh works
+        assert client.succeed(f"curl {arg} http://server:443").strip() == "hello world"
+  '';
+}
diff --git a/nixos/tests/sudo.nix b/nixos/tests/sudo.nix
index 5bbec3d5726..8c38f1b47ef 100644
--- a/nixos/tests/sudo.nix
+++ b/nixos/tests/sudo.nix
@@ -74,7 +74,7 @@ in
         with subtest("test5 user should not be able to run commands under root"):
             machine.fail("sudo -u test5 sudo -n -u root true")
 
-        with subtest("test5 user should be able to keep his environment"):
+        with subtest("test5 user should be able to keep their environment"):
             machine.succeed("sudo -u test5 sudo -n -E -u test1 true")
 
         with subtest("users in group 'barfoo' should not be able to keep their environment"):
diff --git a/nixos/tests/syncthing-init.nix b/nixos/tests/syncthing-init.nix
index 9c8e0a3d087..0a01da52b68 100644
--- a/nixos/tests/syncthing-init.nix
+++ b/nixos/tests/syncthing-init.nix
@@ -24,9 +24,8 @@ in {
   testScript = ''
     machine.wait_for_unit("syncthing-init.service")
     config = machine.succeed("cat /var/lib/syncthing/.config/syncthing/config.xml")
-   
+
     assert "testFolder" in config
     assert "${testId}" in config
   '';
 })
-
diff --git a/nixos/tests/syncthing.nix b/nixos/tests/syncthing.nix
new file mode 100644
index 00000000000..9e2a8e01e3f
--- /dev/null
+++ b/nixos/tests/syncthing.nix
@@ -0,0 +1,65 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }: {
+  name = "syncthing";
+  meta.maintainers = with pkgs.stdenv.lib.maintainers; [ chkno ];
+
+  nodes = rec {
+    a = {
+      environment.systemPackages = with pkgs; [ curl libxml2 syncthing ];
+      services.syncthing = {
+        enable = true;
+        openDefaultPorts = true;
+      };
+    };
+    b = a;
+  };
+
+  testScript = ''
+    import json
+    import shlex
+
+    confdir = "/var/lib/syncthing/.config/syncthing"
+
+
+    def addPeer(host, name, deviceID):
+        APIKey = host.succeed(
+            "xmllint --xpath 'string(configuration/gui/apikey)' %s/config.xml" % confdir
+        ).strip()
+        oldConf = host.succeed(
+            "curl -Ss -H 'X-API-Key: %s' 127.0.0.1:8384/rest/system/config" % APIKey
+        )
+        conf = json.loads(oldConf)
+        conf["devices"].append({"deviceID": deviceID, "id": name})
+        conf["folders"].append(
+            {
+                "devices": [{"deviceID": deviceID}],
+                "id": "foo",
+                "path": "/var/lib/syncthing/foo",
+                "rescanIntervalS": 1,
+            }
+        )
+        newConf = json.dumps(conf)
+        host.succeed(
+            "curl -Ss -H 'X-API-Key: %s' 127.0.0.1:8384/rest/system/config -d %s"
+            % (APIKey, shlex.quote(newConf))
+        )
+
+
+    start_all()
+    a.wait_for_unit("syncthing.service")
+    b.wait_for_unit("syncthing.service")
+    a.wait_for_open_port(22000)
+    b.wait_for_open_port(22000)
+
+    aDeviceID = a.succeed("syncthing -home=%s -device-id" % confdir).strip()
+    bDeviceID = b.succeed("syncthing -home=%s -device-id" % confdir).strip()
+    addPeer(a, "b", bDeviceID)
+    addPeer(b, "a", aDeviceID)
+
+    a.wait_for_file("/var/lib/syncthing/foo")
+    b.wait_for_file("/var/lib/syncthing/foo")
+    a.succeed("echo a2b > /var/lib/syncthing/foo/a2b")
+    b.succeed("echo b2a > /var/lib/syncthing/foo/b2a")
+    a.wait_for_file("/var/lib/syncthing/foo/b2a")
+    b.wait_for_file("/var/lib/syncthing/foo/a2b")
+  '';
+})
diff --git a/nixos/tests/systemd-binfmt.nix b/nixos/tests/systemd-binfmt.nix
new file mode 100644
index 00000000000..2a676f3da98
--- /dev/null
+++ b/nixos/tests/systemd-binfmt.nix
@@ -0,0 +1,24 @@
+# Teach the kernel how to run armv7l and aarch64-linux binaries,
+# and run GNU Hello for these architectures.
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "systemd-binfmt";
+  machine = {
+    boot.binfmt.emulatedSystems = [
+      "armv7l-linux"
+      "aarch64-linux"
+    ];
+  };
+
+  testScript = let
+    helloArmv7l = pkgs.pkgsCross.armv7l-hf-multiplatform.hello;
+    helloAarch64 = pkgs.pkgsCross.aarch64-multiplatform.hello;
+  in ''
+    machine.start()
+    assert "world" in machine.succeed(
+        "${helloArmv7l}/bin/hello"
+    )
+    assert "world" in machine.succeed(
+        "${helloAarch64}/bin/hello"
+    )
+  '';
+})
diff --git a/nixos/tests/systemd-boot.nix b/nixos/tests/systemd-boot.nix
index e911c393361..7a663dd9b42 100644
--- a/nixos/tests/systemd-boot.nix
+++ b/nixos/tests/systemd-boot.nix
@@ -6,26 +6,85 @@
 with import ../lib/testing-python.nix { inherit system pkgs; };
 with pkgs.lib;
 
-makeTest {
-  name = "systemd-boot";
-  meta.maintainers = with pkgs.stdenv.lib.maintainers; [ danielfullmer ];
-
-  machine = { pkgs, lib, ... }: {
+let
+  common = {
     virtualisation.useBootLoader = true;
     virtualisation.useEFIBoot = true;
     boot.loader.systemd-boot.enable = true;
+    boot.loader.efi.canTouchEfiVariables = true;
+    environment.systemPackages = [ pkgs.efibootmgr ];
   };
+in
+{
+  basic = makeTest {
+    name = "systemd-boot";
+    meta.maintainers = with pkgs.stdenv.lib.maintainers; [ danielfullmer ];
+
+    machine = common;
+
+    testScript = ''
+      machine.start()
+      machine.wait_for_unit("multi-user.target")
+
+      machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf")
+
+      # Ensure we actually booted using systemd-boot
+      # Magic number is the vendor UUID used by systemd-boot.
+      machine.succeed(
+          "test -e /sys/firmware/efi/efivars/LoaderEntrySelected-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"
+      )
+
+      # "bootctl install" should have created an EFI entry
+      machine.succeed('efibootmgr | grep "Linux Boot Manager"')
+    '';
+  };
+
+  # Boot without having created an EFI entry--instead using default "/EFI/BOOT/BOOTX64.EFI"
+  fallback = makeTest {
+    name = "systemd-boot-fallback";
+    meta.maintainers = with pkgs.stdenv.lib.maintainers; [ danielfullmer ];
+
+    machine = { pkgs, lib, ... }: {
+      imports = [ common ];
+      boot.loader.efi.canTouchEfiVariables = mkForce false;
+    };
 
-  testScript = ''
-    machine.start()
-    machine.wait_for_unit("multi-user.target")
+    testScript = ''
+      machine.start()
+      machine.wait_for_unit("multi-user.target")
 
-    machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf")
+      machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf")
 
-    # Ensure we actually booted using systemd-boot.
-    # Magic number is the vendor UUID used by systemd-boot.
-    machine.succeed(
-        "test -e /sys/firmware/efi/efivars/LoaderEntrySelected-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"
-    )
-  '';
+      # Ensure we actually booted using systemd-boot
+      # Magic number is the vendor UUID used by systemd-boot.
+      machine.succeed(
+          "test -e /sys/firmware/efi/efivars/LoaderEntrySelected-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"
+      )
+
+      # "bootctl install" should _not_ have created an EFI entry
+      machine.fail('efibootmgr | grep "Linux Boot Manager"')
+    '';
+  };
+
+  update = makeTest {
+    name = "systemd-boot-update";
+    meta.maintainers = with pkgs.stdenv.lib.maintainers; [ danielfullmer ];
+
+    machine = common;
+
+    testScript = ''
+      machine.succeed("mount -o remount,rw /boot")
+
+      # Replace version inside sd-boot with something older. See magic[] string in systemd src/boot/efi/boot.c
+      machine.succeed(
+          """
+        find /boot -iname '*.efi' -print0 | \
+        xargs -0 -I '{}' sed -i 's/#### LoaderInfo: systemd-boot .* ####/#### LoaderInfo: systemd-boot 001 ####/' '{}'
+      """
+      )
+
+      output = machine.succeed("/run/current-system/bin/switch-to-configuration boot")
+      assert "updating systemd-boot from 001 to " in output
+    '';
+  };
 }
diff --git a/nixos/tests/systemd-networkd-vrf.nix b/nixos/tests/systemd-networkd-vrf.nix
index af7813a2e60..bd4751f8e43 100644
--- a/nixos/tests/systemd-networkd-vrf.nix
+++ b/nixos/tests/systemd-networkd-vrf.nix
@@ -159,6 +159,8 @@ in {
     node2.wait_for_unit("network.target")
     node3.wait_for_unit("network.target")
 
+    # NOTE: please keep in mind that the trailing whitespaces in the following strings
+    # are intentional as the output is compared against the raw `iproute2`-output.
     client_ipv4_table = """
     192.168.1.2 dev vrf1 proto static metric 100 
     192.168.2.3 dev vrf2 proto static metric 100
@@ -194,18 +196,16 @@ in {
         client.succeed("ping -c5 192.168.1.2")
         client.succeed("ping -c5 192.168.2.3")
 
-    # Test whether SSH through a VRF IP is possible.
-    # (Note: this seems to be an issue on Linux 5.x, so I decided to add this to
-    # ensure that we catch this when updating the default kernel).
-    # with subtest("tcp traffic through vrf works"):
-    #     node1.wait_for_open_port(22)
-    #     client.succeed(
-    #         "cat ${snakeOilPrivateKey} > privkey.snakeoil"
-    #     )
-    #     client.succeed("chmod 600 privkey.snakeoil")
-    #     client.succeed(
-    #         "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil root@192.168.1.2 true"
-    #     )
+    # Test whether TCP through a VRF IP is possible.
+    with subtest("tcp traffic through vrf works"):
+        node1.wait_for_open_port(22)
+        client.succeed(
+            "cat ${snakeOilPrivateKey} > privkey.snakeoil"
+        )
+        client.succeed("chmod 600 privkey.snakeoil")
+        client.succeed(
+            "ulimit -l 2048; ip vrf exec vrf1 ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil root@192.168.1.2 true"
+        )
 
     # Only configured routes through the VRF from the main routing table should
     # work. Additional IPs are only reachable when binding to the vrf interface.
diff --git a/nixos/tests/systemd.nix b/nixos/tests/systemd.nix
index ca2e36a443e..9d21f9158f3 100644
--- a/nixos/tests/systemd.nix
+++ b/nixos/tests/systemd.nix
@@ -4,7 +4,10 @@ import ./make-test-python.nix ({ pkgs, ... }: {
   machine = { lib, ... }: {
     imports = [ common/user-account.nix common/x11.nix ];
 
-    virtualisation.emptyDiskImages = [ 512 ];
+    virtualisation.emptyDiskImages = [ 512 512 ];
+    virtualisation.memorySize = 1024;
+
+    environment.systemPackages = [ pkgs.cryptsetup ];
 
     fileSystems = lib.mkVMOverride {
       "/test-x-initrd-mount" = {
@@ -50,6 +53,13 @@ import ./make-test-python.nix ({ pkgs, ... }: {
         fi
       '';
     };
+
+    systemd.watchdog = {
+      device = "/dev/watchdog";
+      runtimeTime = "30s";
+      rebootTime = "10min";
+      kexecTime = "5min";
+    };
   };
 
   testScript = ''
@@ -97,6 +107,11 @@ import ./make-test-python.nix ({ pkgs, ... }: {
             re.search(r"^Filesystem state: *clean$", extinfo, re.MULTILINE) is not None
         ), ("File system was not cleanly unmounted: " + extinfo)
 
+    # Regression test for https://github.com/NixOS/nixpkgs/pull/91232
+    with subtest("setting transient hostnames works"):
+        machine.succeed("hostnamectl set-hostname --transient machine-transient")
+        machine.fail("hostnamectl set-hostname machine-all")
+
     with subtest("systemd-shutdown works"):
         machine.shutdown()
         machine.wait_for_unit("multi-user.target")
@@ -117,5 +132,40 @@ import ./make-test-python.nix ({ pkgs, ... }: {
         retcode, output = machine.execute("systemctl status testservice1.service")
         assert retcode in [0, 3]  # https://bugs.freedesktop.org/show_bug.cgi?id=77507
         assert "CPU:" in output
+
+    # Test systemd is configured to manage a watchdog
+    with subtest("systemd manages hardware watchdog"):
+        machine.wait_for_unit("multi-user.target")
+
+        # It seems that the device's path doesn't appear in 'systemctl show' so
+        # check it separately.
+        assert "WatchdogDevice=/dev/watchdog" in machine.succeed(
+            "cat /etc/systemd/system.conf"
+        )
+
+        output = machine.succeed("systemctl show | grep Watchdog")
+        assert "RuntimeWatchdogUSec=30s" in output
+        assert "RebootWatchdogUSec=10m" in output
+        assert "KExecWatchdogUSec=5m" in output
+
+    # Test systemd cryptsetup support
+    with subtest("systemd successfully reads /etc/crypttab and unlocks volumes"):
+        # create a luks volume and put a filesystem on it
+        machine.succeed(
+            "echo -n supersecret | cryptsetup luksFormat -q /dev/vdc -",
+            "echo -n supersecret | cryptsetup luksOpen --key-file - /dev/vdc foo",
+            "mkfs.ext3 /dev/mapper/foo",
+        )
+
+        # create a keyfile and /etc/crypttab
+        machine.succeed("echo -n supersecret > /var/lib/luks-keyfile")
+        machine.succeed("chmod 600 /var/lib/luks-keyfile")
+        machine.succeed("echo 'luks1 /dev/vdc /var/lib/luks-keyfile luks' > /etc/crypttab")
+
+        # after a reboot, systemd should unlock the volume and we should be able to mount it
+        machine.shutdown()
+        machine.succeed("systemctl status systemd-cryptsetup@luks1.service")
+        machine.succeed("mkdir -p /tmp/luks1")
+        machine.succeed("mount /dev/mapper/luks1 /tmp/luks1")
   '';
 })
diff --git a/nixos/tests/taskserver.nix b/nixos/tests/taskserver.nix
index ab9b589f859..f34782c7059 100644
--- a/nixos/tests/taskserver.nix
+++ b/nixos/tests/taskserver.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ... }: let
+import ./make-test-python.nix ({ pkgs, ... }: let
   snakeOil = pkgs.runCommand "snakeoil-certs" {
     outputs = [ "out" "cacert" "cert" "key" "crl" ];
     buildInputs = [ pkgs.gnutls.bin ];
@@ -105,186 +105,178 @@ in {
     newServerSystem = nodes.newServer.config.system.build.toplevel;
     switchToNewServer = "${newServerSystem}/bin/switch-to-configuration test";
   in ''
-    sub su ($$) {
-      my ($user, $cmd) = @_;
-      my $esc = $cmd =~ s/'/'\\${"'"}'/gr;
-      return "su - $user -c '$esc'";
-    }
-
-    sub setupClientsFor ($$;$) {
-      my ($org, $user, $extraInit) = @_;
-
-      for my $client ($client1, $client2) {
-        $client->nest("initialize client for user $user", sub {
-          $client->succeed(
-            (su $user, "rm -rf /home/$user/.task"),
-            (su $user, "task rc.confirmation=no config confirmation no")
-          );
-
-          my $exportinfo = $server->succeed(
-            "nixos-taskserver user export $org $user"
-          );
-
-          $exportinfo =~ s/'/'\\'''/g;
-
-          $client->nest("importing taskwarrior configuration", sub {
-            my $cmd = su $user, "eval '$exportinfo' >&2";
-            my ($status, $out) = $client->execute_($cmd);
-            if ($status != 0) {
-              $client->log("output: $out");
-              die "command `$cmd' did not succeed (exit code $status)\n";
-            }
-          });
-
-          eval { &$extraInit($client, $org, $user) };
-
-          $client->succeed(su $user,
-            "task config taskd.server server:${portStr} >&2"
-          );
-
-          $client->succeed(su $user, "task sync init >&2");
-        });
-      }
-    }
-
-    sub restartServer {
-      $server->succeed("systemctl restart taskserver.service");
-      $server->waitForOpenPort(${portStr});
-    }
-
-    sub readdImperativeUser {
-      $server->nest("(re-)add imperative user bar", sub {
-        $server->execute("nixos-taskserver org remove imperativeOrg");
-        $server->succeed(
-          "nixos-taskserver org add imperativeOrg",
-          "nixos-taskserver user add imperativeOrg bar"
-        );
-        setupClientsFor "imperativeOrg", "bar";
-      });
-    }
-
-    sub testSync ($) {
-      my $user = $_[0];
-      subtest "sync for user $user", sub {
-        $client1->succeed(su $user, "task add foo >&2");
-        $client1->succeed(su $user, "task sync >&2");
-        $client2->fail(su $user, "task list >&2");
-        $client2->succeed(su $user, "task sync >&2");
-        $client2->succeed(su $user, "task list >&2");
-      };
-    }
-
-    sub checkClientCert ($) {
-      my $user = $_[0];
-      my $cmd = "gnutls-cli".
-        " --x509cafile=/home/$user/.task/keys/ca.cert".
-        " --x509keyfile=/home/$user/.task/keys/private.key".
-        " --x509certfile=/home/$user/.task/keys/public.cert".
-        " --port=${portStr} server < /dev/null";
-      return su $user, $cmd;
-    }
+    from shlex import quote
+
+
+    def su(user, cmd):
+        return f"su - {user} -c {quote(cmd)}"
+
+
+    def no_extra_init(client, org, user):
+        pass
+
+
+    def setup_clients_for(org, user, extra_init=no_extra_init):
+        for client in [client1, client2]:
+            with client.nested(f"initialize client for user {user}"):
+                client.succeed(
+                    su(user, f"rm -rf /home/{user}/.task"),
+                    su(user, "task rc.confirmation=no config confirmation no"),
+                )
+
+                exportinfo = server.succeed(f"nixos-taskserver user export {org} {user}")
+
+                with client.nested("importing taskwarrior configuration"):
+                    client.succeed(su(user, f"eval {quote(exportinfo)} >&2"))
+
+                extra_init(client, org, user)
+
+                client.succeed(su(user, "task config taskd.server server:${portStr} >&2"))
+
+                client.succeed(su(user, "task sync init >&2"))
+
+
+    def restart_server():
+        server.systemctl("restart taskserver.service")
+        server.wait_for_open_port(${portStr})
+
+
+    def re_add_imperative_user():
+        with server.nested("(re-)add imperative user bar"):
+            server.execute("nixos-taskserver org remove imperativeOrg")
+            server.succeed(
+                "nixos-taskserver org add imperativeOrg",
+                "nixos-taskserver user add imperativeOrg bar",
+            )
+            setup_clients_for("imperativeOrg", "bar")
+
+
+    def test_sync(user):
+        with subtest(f"sync for user {user}"):
+            client1.succeed(su(user, "task add foo >&2"))
+            client1.succeed(su(user, "task sync >&2"))
+            client2.fail(su(user, "task list >&2"))
+            client2.succeed(su(user, "task sync >&2"))
+            client2.succeed(su(user, "task list >&2"))
+
+
+    def check_client_cert(user):
+        # debug level 3 is a workaround for gnutls issue https://gitlab.com/gnutls/gnutls/-/issues/1040
+        cmd = (
+            f"gnutls-cli -d 3"
+            f" --x509cafile=/home/{user}/.task/keys/ca.cert"
+            f" --x509keyfile=/home/{user}/.task/keys/private.key"
+            f" --x509certfile=/home/{user}/.task/keys/public.cert"
+            f" --port=${portStr} server < /dev/null"
+        )
+        return su(user, cmd)
+
 
     # Explicitly start the VMs so that we don't accidentally start newServer
-    $server->start;
-    $client1->start;
-    $client2->start;
+    server.start()
+    client1.start()
+    client2.start()
 
-    $server->waitForUnit("taskserver.service");
+    server.wait_for_unit("taskserver.service")
 
-    $server->succeed(
-      "nixos-taskserver user list testOrganisation | grep -qxF alice",
-      "nixos-taskserver user list testOrganisation | grep -qxF foo",
-      "nixos-taskserver user list anotherOrganisation | grep -qxF bob"
-    );
+    server.succeed(
+        "nixos-taskserver user list testOrganisation | grep -qxF alice",
+        "nixos-taskserver user list testOrganisation | grep -qxF foo",
+        "nixos-taskserver user list anotherOrganisation | grep -qxF bob",
+    )
 
-    $server->waitForOpenPort(${portStr});
+    server.wait_for_open_port(${portStr})
 
-    $client1->waitForUnit("multi-user.target");
-    $client2->waitForUnit("multi-user.target");
+    client1.wait_for_unit("multi-user.target")
+    client2.wait_for_unit("multi-user.target")
 
-    setupClientsFor "testOrganisation", "alice";
-    setupClientsFor "testOrganisation", "foo";
-    setupClientsFor "anotherOrganisation", "bob";
+    setup_clients_for("testOrganisation", "alice")
+    setup_clients_for("testOrganisation", "foo")
+    setup_clients_for("anotherOrganisation", "bob")
 
-    testSync $_ for ("alice", "bob", "foo");
+    for user in ["alice", "bob", "foo"]:
+        test_sync(user)
 
-    $server->fail("nixos-taskserver user add imperativeOrg bar");
-    readdImperativeUser;
+    server.fail("nixos-taskserver user add imperativeOrg bar")
+    re_add_imperative_user()
 
-    testSync "bar";
+    test_sync("bar")
 
-    subtest "checking certificate revocation of user bar", sub {
-      $client1->succeed(checkClientCert "bar");
+    with subtest("checking certificate revocation of user bar"):
+        client1.succeed(check_client_cert("bar"))
 
-      $server->succeed("nixos-taskserver user remove imperativeOrg bar");
-      restartServer;
+        server.succeed("nixos-taskserver user remove imperativeOrg bar")
+        restart_server()
 
-      $client1->fail(checkClientCert "bar");
+        client1.fail(check_client_cert("bar"))
 
-      $client1->succeed(su "bar", "task add destroy everything >&2");
-      $client1->fail(su "bar", "task sync >&2");
-    };
+        client1.succeed(su("bar", "task add destroy everything >&2"))
+        client1.fail(su("bar", "task sync >&2"))
 
-    readdImperativeUser;
+    re_add_imperative_user()
 
-    subtest "checking certificate revocation of org imperativeOrg", sub {
-      $client1->succeed(checkClientCert "bar");
+    with subtest("checking certificate revocation of org imperativeOrg"):
+        client1.succeed(check_client_cert("bar"))
 
-      $server->succeed("nixos-taskserver org remove imperativeOrg");
-      restartServer;
+        server.succeed("nixos-taskserver org remove imperativeOrg")
+        restart_server()
 
-      $client1->fail(checkClientCert "bar");
+        client1.fail(check_client_cert("bar"))
 
-      $client1->succeed(su "bar", "task add destroy even more >&2");
-      $client1->fail(su "bar", "task sync >&2");
-    };
+        client1.succeed(su("bar", "task add destroy even more >&2"))
+        client1.fail(su("bar", "task sync >&2"))
 
-    readdImperativeUser;
+    re_add_imperative_user()
 
-    subtest "check whether declarative config overrides user bar", sub {
-      restartServer;
-      testSync "bar";
-    };
+    with subtest("check whether declarative config overrides user bar"):
+        restart_server()
+        test_sync("bar")
 
-    subtest "check manual configuration", sub {
-      # Remove the keys from automatic CA creation, to make sure the new
-      # generation doesn't use keys from before.
-      $server->succeed('rm -rf ${cfg.dataDir}/keys/* >&2');
-
-      $server->succeed('${switchToNewServer} >&2');
-      $server->waitForUnit("taskserver.service");
-      $server->waitForOpenPort(${portStr});
-
-      $server->succeed(
-        "nixos-taskserver org add manualOrg",
-        "nixos-taskserver user add manualOrg alice"
-      );
-
-      setupClientsFor "manualOrg", "alice", sub {
-        my ($client, $org, $user) = @_;
-        my $cfgpath = "/home/$user/.task";
-
-        $client->copyFileFromHost("${snakeOil.cacert}", "$cfgpath/ca.cert");
-        for my $file ('alice.key', 'alice.cert') {
-          $client->copyFileFromHost("${snakeOil}/$file", "$cfgpath/$file");
-        }
-
-        for my $file ("$user.key", "$user.cert") {
-          $client->copyFileFromHost(
-            "${snakeOil}/$file", "$cfgpath/$file"
-          );
-        }
-        $client->copyFileFromHost(
-          "${snakeOil.cacert}", "$cfgpath/ca.cert"
-        );
-        $client->succeed(
-          (su "alice", "task config taskd.ca $cfgpath/ca.cert"),
-          (su "alice", "task config taskd.key $cfgpath/$user.key"),
-          (su $user, "task config taskd.certificate $cfgpath/$user.cert")
-        );
-      };
 
-      testSync "alice";
-    };
+    def init_manual_config(client, org, user):
+        cfgpath = f"/home/{user}/.task"
+
+        client.copy_from_host(
+            "${snakeOil.cacert}",
+            f"{cfgpath}/ca.cert",
+        )
+        for file in ["alice.key", "alice.cert"]:
+            client.copy_from_host(
+                f"${snakeOil}/{file}",
+                f"{cfgpath}/{file}",
+            )
+
+        for file in [f"{user}.key", f"{user}.cert"]:
+            client.copy_from_host(
+                f"${snakeOil}/{file}",
+                f"{cfgpath}/{file}",
+            )
+
+        client.succeed(
+            su("alice", f"task config taskd.ca {cfgpath}/ca.cert"),
+            su("alice", f"task config taskd.key {cfgpath}/{user}.key"),
+            su(user, f"task config taskd.certificate {cfgpath}/{user}.cert"),
+        )
+
+
+    with subtest("check manual configuration"):
+        # Remove the keys from automatic CA creation, to make sure the new
+        # generation doesn't use keys from before.
+        server.succeed("rm -rf ${cfg.dataDir}/keys/* >&2")
+
+        server.succeed(
+            "${switchToNewServer} >&2"
+        )
+        server.wait_for_unit("taskserver.service")
+        server.wait_for_open_port(${portStr})
+
+        server.succeed(
+            "nixos-taskserver org add manualOrg",
+            "nixos-taskserver user add manualOrg alice",
+        )
+
+        setup_clients_for("manualOrg", "alice", init_manual_config)
+
+        test_sync("alice")
   '';
 })
diff --git a/nixos/tests/teeworlds.nix b/nixos/tests/teeworlds.nix
new file mode 100644
index 00000000000..edf58896878
--- /dev/null
+++ b/nixos/tests/teeworlds.nix
@@ -0,0 +1,55 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+let
+  client =
+    { pkgs, ... }:
+
+    { imports = [ ./common/x11.nix ];
+      environment.systemPackages = [ pkgs.teeworlds ];
+    };
+
+in {
+  name = "teeworlds";
+  meta = with pkgs.stdenv.lib.maintainers; {
+    maintainers = [ hax404 ];
+  };
+
+  nodes =
+    { server =
+      { services.teeworlds = {
+          enable = true;
+          openPorts = true;
+        };
+      };
+
+      client1 = client;
+      client2 = client;
+    };
+
+    testScript =
+    ''
+      start_all()
+
+      server.wait_for_unit("teeworlds.service")
+      server.wait_until_succeeds("ss --numeric --udp --listening | grep -q 8303")
+
+      client1.wait_for_x()
+      client2.wait_for_x()
+
+      client1.execute("teeworlds 'player_name Alice;connect server'&")
+      server.wait_until_succeeds(
+          'journalctl -u teeworlds -e | grep --extended-regexp -q "team_join player=\'[0-9]:Alice"'
+      )
+
+      client2.execute("teeworlds 'player_name Bob;connect server'&")
+      server.wait_until_succeeds(
+          'journalctl -u teeworlds -e | grep --extended-regexp -q "team_join player=\'[0-9]:Bob"'
+      )
+
+      server.sleep(10)  # wait for a while to get a nice screenshot
+
+      client1.screenshot("screen_client1")
+      client2.screenshot("screen_client2")
+    '';
+
+})
diff --git a/nixos/tests/tiddlywiki.nix b/nixos/tests/tiddlywiki.nix
index cf45578b0f9..822711b8939 100644
--- a/nixos/tests/tiddlywiki.nix
+++ b/nixos/tests/tiddlywiki.nix
@@ -44,7 +44,7 @@ import ./make-test-python.nix ({ ... }: {
           configured.succeed(
               "curl --fail -o /dev/null 127.0.0.1:3000 --user somelogin:somesecret"
           )
-      
+
       with subtest("restart preserves changes"):
           # given running wiki
           default.wait_for_unit("tiddlywiki.service")
diff --git a/nixos/tests/transmission.nix b/nixos/tests/transmission.nix
index f4f2186be1f..37c0352dcfb 100644
--- a/nixos/tests/transmission.nix
+++ b/nixos/tests/transmission.nix
@@ -9,6 +9,8 @@ import ./make-test-python.nix ({ pkgs, ...} : {
 
     networking.firewall.allowedTCPPorts = [ 9091 ];
 
+    security.apparmor.enable = true;
+
     services.transmission.enable = true;
   };
 
diff --git a/nixos/tests/trezord.nix b/nixos/tests/trezord.nix
index 8d908a52249..b7b3dd31942 100644
--- a/nixos/tests/trezord.nix
+++ b/nixos/tests/trezord.nix
@@ -1,9 +1,8 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "trezord";
-  meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ mmahut "1000101" ];
+  meta = with pkgs.stdenv.lib; {
+    maintainers = with maintainers; [ mmahut _1000101 ];
   };
-
   nodes = {
     machine = { ... }: {
       services.trezord.enable = true;
diff --git a/nixos/tests/trickster.nix b/nixos/tests/trickster.nix
index e2ca00980d5..713ac8f0b2f 100644
--- a/nixos/tests/trickster.nix
+++ b/nixos/tests/trickster.nix
@@ -1,7 +1,7 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "trickster";
-  meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ "1000101" ];
+  meta = with pkgs.stdenv.lib; {
+    maintainers = with maintainers; [ _1000101 ];
   };
 
   nodes = {
@@ -34,4 +34,4 @@ import ./make-test-python.nix ({ pkgs, ... }: {
         "curl -L http://localhost:9090/metrics | grep 'promhttp_metric_handler_requests_total{code=\"500\"} 0'"
     )
   '';
-})
\ No newline at end of file
+})
diff --git a/nixos/tests/virtualbox.nix b/nixos/tests/virtualbox.nix
index aec8da6a2af..af76e6f9844 100644
--- a/nixos/tests/virtualbox.nix
+++ b/nixos/tests/virtualbox.nix
@@ -198,7 +198,6 @@ let
       systemd.services."vboxtestlog-${name}@" = {
         description = "VirtualBox Test Machine Log For ${name}";
         serviceConfig.StandardInput = "socket";
-        serviceConfig.StandardOutput = "syslog";
         serviceConfig.SyslogIdentifier = "GUEST-${name}";
         serviceConfig.ExecStart = "${pkgs.coreutils}/bin/cat";
       };
diff --git a/nixos/tests/wasabibackend.nix b/nixos/tests/wasabibackend.nix
new file mode 100644
index 00000000000..d169ad15272
--- /dev/null
+++ b/nixos/tests/wasabibackend.nix
@@ -0,0 +1,38 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "wasabibackend";
+  meta = with pkgs.stdenv.lib.maintainers; {
+    maintainers = [ mmahut ];
+  };
+
+  nodes = {
+    machine = { ... }: {
+      services.wasabibackend = {
+        enable = true;
+        network = "testnet";
+        rpc = {
+          user = "alice";
+          port = 18332;
+        };
+      };
+      services.bitcoind = {
+        enable = true;
+        testnet = true;
+        rpc.users = {
+          alice.passwordHMAC = "e7096bc21da60b29ecdbfcdb2c3acc62$f948e61cb587c399358ed99c6ed245a41460b4bf75125d8330c9f6fcc13d7ae7";
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+    machine.wait_for_unit("wasabibackend.service")
+    machine.wait_until_succeeds(
+        "grep 'Wasabi Backend started' /var/lib/wasabibackend/.walletwasabi/backend/Logs.txt"
+    )
+    machine.sleep(5)
+    machine.succeed(
+        "grep 'Config is successfully initialized' /var/lib/wasabibackend/.walletwasabi/backend/Logs.txt"
+    )
+  '';
+})
diff --git a/nixos/tests/web-servers/unit-php.nix b/nixos/tests/web-servers/unit-php.nix
index c6327a1f825..2a0a5bdaa5d 100644
--- a/nixos/tests/web-servers/unit-php.nix
+++ b/nixos/tests/web-servers/unit-php.nix
@@ -23,7 +23,10 @@ in {
               "user": "testuser",
               "group": "testgroup",
               "root": "${testdir}/www",
-              "index": "info.php"
+              "index": "info.php",
+              "options": {
+                "file": "${pkgs.unit.usedPhp74}/lib/php.ini"
+              }
             }
           }
         }
@@ -42,6 +45,13 @@ in {
   };
   testScript = ''
     machine.wait_for_unit("unit.service")
-    assert "PHP Version ${pkgs.php74.version}" in machine.succeed("curl -vvv -s http://127.0.0.1:9074/")
+
+    # Check so we get an evaluated PHP back
+    response = machine.succeed("curl -vvv -s http://127.0.0.1:9074/")
+    assert "PHP Version ${pkgs.unit.usedPhp74.version}" in response, "PHP version not detected"
+
+    # Check so we have database and some other extensions loaded
+    for ext in ["json", "opcache", "pdo_mysql", "pdo_pgsql", "pdo_sqlite"]:
+        assert ext in response, f"Missing {ext} extension"
   '';
 })
diff --git a/nixos/tests/wireguard/basic.nix b/nixos/tests/wireguard/basic.nix
new file mode 100644
index 00000000000..25d706ae2e5
--- /dev/null
+++ b/nixos/tests/wireguard/basic.nix
@@ -0,0 +1,74 @@
+{ kernelPackages ? null }:
+import ../make-test-python.nix ({ pkgs, lib, ...} :
+  let
+    wg-snakeoil-keys = import ./snakeoil-keys.nix;
+    peer = (import ./make-peer.nix) { inherit lib; };
+  in
+  {
+    name = "wireguard";
+    meta = with pkgs.stdenv.lib.maintainers; {
+      maintainers = [ ma27 ];
+    };
+
+    nodes = {
+      peer0 = peer {
+        ip4 = "192.168.0.1";
+        ip6 = "fd00::1";
+        extraConfig = {
+          boot = lib.mkIf (kernelPackages != null) { inherit kernelPackages; };
+          networking.firewall.allowedUDPPorts = [ 23542 ];
+          networking.wireguard.interfaces.wg0 = {
+            ips = [ "10.23.42.1/32" "fc00::1/128" ];
+            listenPort = 23542;
+
+            inherit (wg-snakeoil-keys.peer0) privateKey;
+
+            peers = lib.singleton {
+              allowedIPs = [ "10.23.42.2/32" "fc00::2/128" ];
+
+              inherit (wg-snakeoil-keys.peer1) publicKey;
+            };
+          };
+        };
+      };
+
+      peer1 = peer {
+        ip4 = "192.168.0.2";
+        ip6 = "fd00::2";
+        extraConfig = {
+          boot = lib.mkIf (kernelPackages != null) { inherit kernelPackages; };
+          networking.wireguard.interfaces.wg0 = {
+            ips = [ "10.23.42.2/32" "fc00::2/128" ];
+            listenPort = 23542;
+            allowedIPsAsRoutes = false;
+
+            inherit (wg-snakeoil-keys.peer1) privateKey;
+
+            peers = lib.singleton {
+              allowedIPs = [ "0.0.0.0/0" "::/0" ];
+              endpoint = "192.168.0.1:23542";
+              persistentKeepalive = 25;
+
+              inherit (wg-snakeoil-keys.peer0) publicKey;
+            };
+
+            postSetup = let inherit (pkgs) iproute; in ''
+              ${iproute}/bin/ip route replace 10.23.42.1/32 dev wg0
+              ${iproute}/bin/ip route replace fc00::1/128 dev wg0
+            '';
+          };
+        };
+      };
+    };
+
+    testScript = ''
+      start_all()
+
+      peer0.wait_for_unit("wireguard-wg0.service")
+      peer1.wait_for_unit("wireguard-wg0.service")
+
+      peer1.succeed("ping -c5 fc00::1")
+      peer1.succeed("ping -c5 10.23.42.1")
+    '';
+  }
+)
diff --git a/nixos/tests/wireguard/default.nix b/nixos/tests/wireguard/default.nix
index e3bc31c600f..dedb321ff2e 100644
--- a/nixos/tests/wireguard/default.nix
+++ b/nixos/tests/wireguard/default.nix
@@ -1,71 +1,27 @@
-import ../make-test-python.nix ({ pkgs, lib, ...} :
-  let
-    wg-snakeoil-keys = import ./snakeoil-keys.nix;
-    peer = (import ./make-peer.nix) { inherit lib; };
-  in
-  {
-    name = "wireguard";
-    meta = with pkgs.stdenv.lib.maintainers; {
-      maintainers = [ ma27 ];
-    };
-
-    nodes = {
-      peer0 = peer {
-        ip4 = "192.168.0.1";
-        ip6 = "fd00::1";
-        extraConfig = {
-          networking.firewall.allowedUDPPorts = [ 23542 ];
-          networking.wireguard.interfaces.wg0 = {
-            ips = [ "10.23.42.1/32" "fc00::1/128" ];
-            listenPort = 23542;
-
-            inherit (wg-snakeoil-keys.peer0) privateKey;
-
-            peers = lib.singleton {
-              allowedIPs = [ "10.23.42.2/32" "fc00::2/128" ];
-
-              inherit (wg-snakeoil-keys.peer1) publicKey;
-            };
-          };
-        };
-      };
-
-      peer1 = peer {
-        ip4 = "192.168.0.2";
-        ip6 = "fd00::2";
-        extraConfig = {
-          networking.wireguard.interfaces.wg0 = {
-            ips = [ "10.23.42.2/32" "fc00::2/128" ];
-            listenPort = 23542;
-            allowedIPsAsRoutes = false;
-
-            inherit (wg-snakeoil-keys.peer1) privateKey;
-
-            peers = lib.singleton {
-              allowedIPs = [ "0.0.0.0/0" "::/0" ];
-              endpoint = "192.168.0.1:23542";
-              persistentKeepalive = 25;
-
-              inherit (wg-snakeoil-keys.peer0) publicKey;
-            };
-
-            postSetup = let inherit (pkgs) iproute; in ''
-              ${iproute}/bin/ip route replace 10.23.42.1/32 dev wg0
-              ${iproute}/bin/ip route replace fc00::1/128 dev wg0
-            '';
-          };
-        };
-      };
-    };
-
-    testScript = ''
-      start_all()
-
-      peer0.wait_for_unit("wireguard-wg0.service")
-      peer1.wait_for_unit("wireguard-wg0.service")
-
-      peer1.succeed("ping -c5 fc00::1")
-      peer1.succeed("ping -c5 10.23.42.1")
-    '';
-  }
+{ system ? builtins.currentSystem
+, config ? { }
+, pkgs ? import ../../.. { inherit system config; }
+, kernelVersionsToTest ? [ "5.4" "latest" ]
+}:
+
+with pkgs.lib;
+
+let
+  tests = let callTest = p: flip (import p) { inherit system pkgs; }; in {
+    basic = callTest ./basic.nix;
+    namespaces = callTest ./namespaces.nix;
+    wg-quick = callTest ./wg-quick.nix;
+    generated = callTest ./generated.nix;
+  };
+in
+
+listToAttrs (
+  flip concatMap kernelVersionsToTest (version:
+    let
+      v' = replaceStrings [ "." ] [ "_" ] version;
+    in
+    flip mapAttrsToList tests (name: test:
+      nameValuePair "wireguard-${name}-linux-${v'}" (test { kernelPackages = pkgs."linuxPackages_${v'}"; })
+    )
+  )
 )
diff --git a/nixos/tests/wireguard/generated.nix b/nixos/tests/wireguard/generated.nix
index a29afd2d466..cdf15483265 100644
--- a/nixos/tests/wireguard/generated.nix
+++ b/nixos/tests/wireguard/generated.nix
@@ -1,4 +1,5 @@
-import ../make-test-python.nix ({ pkgs, ...} : {
+{ kernelPackages ? null }:
+import ../make-test-python.nix ({ pkgs, lib, ... } : {
   name = "wireguard-generated";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ ma27 grahamc ];
@@ -6,6 +7,7 @@ import ../make-test-python.nix ({ pkgs, ...} : {
 
   nodes = {
     peer1 = {
+      boot = lib.mkIf (kernelPackages != null) { inherit kernelPackages; };
       networking.firewall.allowedUDPPorts = [ 12345 ];
       networking.wireguard.interfaces.wg0 = {
         ips = [ "10.10.10.1/24" ];
@@ -17,6 +19,7 @@ import ../make-test-python.nix ({ pkgs, ...} : {
     };
 
     peer2 = {
+      boot = lib.mkIf (kernelPackages != null) { inherit kernelPackages; };
       networking.firewall.allowedUDPPorts = [ 12345 ];
       networking.wireguard.interfaces.wg0 = {
         ips = [ "10.10.10.2/24" ];
diff --git a/nixos/tests/wireguard/namespaces.nix b/nixos/tests/wireguard/namespaces.nix
index c8a4e3bb52a..c47175ceafc 100644
--- a/nixos/tests/wireguard/namespaces.nix
+++ b/nixos/tests/wireguard/namespaces.nix
@@ -1,3 +1,5 @@
+{ kernelPackages ? null }:
+
 let
   listenPort = 12345;
   socketNamespace = "foo";
@@ -13,7 +15,7 @@ let
 
 in
 
-import ../make-test-python.nix ({ pkgs, ...} : {
+import ../make-test-python.nix ({ pkgs, lib, ... } : {
   name = "wireguard-with-namespaces";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ asymmetric ];
@@ -23,6 +25,7 @@ import ../make-test-python.nix ({ pkgs, ...} : {
     # interface should be created in the socketNamespace
     # and not moved from there
     peer0 = pkgs.lib.attrsets.recursiveUpdate node {
+      boot = lib.mkIf (kernelPackages != null) { inherit kernelPackages; };
       networking.wireguard.interfaces.wg0 = {
         preSetup = ''
           ip netns add ${socketNamespace}
@@ -33,6 +36,7 @@ import ../make-test-python.nix ({ pkgs, ...} : {
     # interface should be created in the init namespace
     # and moved to the interfaceNamespace
     peer1 = pkgs.lib.attrsets.recursiveUpdate node {
+      boot = lib.mkIf (kernelPackages != null) { inherit kernelPackages; };
       networking.wireguard.interfaces.wg0 = {
         preSetup = ''
           ip netns add ${interfaceNamespace}
@@ -43,6 +47,7 @@ import ../make-test-python.nix ({ pkgs, ...} : {
     # interface should be created in the socketNamespace
     # and moved to the interfaceNamespace
     peer2 = pkgs.lib.attrsets.recursiveUpdate node {
+      boot = lib.mkIf (kernelPackages != null) { inherit kernelPackages; };
       networking.wireguard.interfaces.wg0 = {
         preSetup = ''
           ip netns add ${socketNamespace}
@@ -54,6 +59,7 @@ import ../make-test-python.nix ({ pkgs, ...} : {
     # interface should be created in the socketNamespace
     # and moved to the init namespace
     peer3 = pkgs.lib.attrsets.recursiveUpdate node {
+      boot = lib.mkIf (kernelPackages != null) { inherit kernelPackages; };
       networking.wireguard.interfaces.wg0 = {
         preSetup = ''
           ip netns add ${socketNamespace}
diff --git a/nixos/tests/wireguard/wg-quick.nix b/nixos/tests/wireguard/wg-quick.nix
index 7354dd01a34..5472d21cd1e 100644
--- a/nixos/tests/wireguard/wg-quick.nix
+++ b/nixos/tests/wireguard/wg-quick.nix
@@ -1,3 +1,5 @@
+{ kernelPackages ? null }:
+
 import ../make-test-python.nix ({ pkgs, lib, ... }:
   let
     wg-snakeoil-keys = import ./snakeoil-keys.nix;
@@ -14,6 +16,7 @@ import ../make-test-python.nix ({ pkgs, lib, ... }:
         ip4 = "192.168.0.1";
         ip6 = "fd00::1";
         extraConfig = {
+          boot = lib.mkIf (kernelPackages != null) { inherit kernelPackages; };
           networking.firewall.allowedUDPPorts = [ 23542 ];
           networking.wg-quick.interfaces.wg0 = {
             address = [ "10.23.42.1/32" "fc00::1/128" ];
@@ -34,6 +37,7 @@ import ../make-test-python.nix ({ pkgs, lib, ... }:
         ip4 = "192.168.0.2";
         ip6 = "fd00::2";
         extraConfig = {
+          boot = lib.mkIf (kernelPackages != null) { inherit kernelPackages; };
           networking.wg-quick.interfaces.wg0 = {
             address = [ "10.23.42.2/32" "fc00::2/128" ];
             inherit (wg-snakeoil-keys.peer1) privateKey;
diff --git a/nixos/tests/xandikos.nix b/nixos/tests/xandikos.nix
index 0fded20ff1a..48c770a3d16 100644
--- a/nixos/tests/xandikos.nix
+++ b/nixos/tests/xandikos.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix (
     {
       name = "xandikos";
 
-      meta.maintainers = [ lib.maintainers."0x4A6F" ];
+      meta.maintainers = with lib.maintainers; [ _0x4A6F ];
 
       nodes = {
         xandikos_client = {};
@@ -17,7 +17,7 @@ import ./make-test-python.nix (
           services.xandikos.enable = true;
           services.xandikos.address = "localhost";
           services.xandikos.port = 8080;
-          services.xandikos.routePrefix = "/xandikos/";
+          services.xandikos.routePrefix = "/xandikos-prefix/";
           services.xandikos.extraOptions = [
             "--defaults"
           ];
@@ -28,7 +28,7 @@ import ./make-test-python.nix (
               serverName = "xandikos.local";
               basicAuth.xandikos = "snakeOilPassword";
               locations."/xandikos/" = {
-                proxyPass = "http://localhost:8080/";
+                proxyPass = "http://localhost:8080/xandikos-prefix/";
               };
             };
           };
diff --git a/nixos/tests/xfce.nix b/nixos/tests/xfce.nix
index 99065669661..99e30342e59 100644
--- a/nixos/tests/xfce.nix
+++ b/nixos/tests/xfce.nix
@@ -11,8 +11,8 @@ import ./make-test-python.nix ({ pkgs, ...} : {
 
       services.xserver.enable = true;
 
-      services.xserver.displayManager.lightdm = {
-        enable = true;
+      services.xserver.displayManager = {
+        lightdm.enable = true;
         autoLogin = {
           enable = true;
           user = "alice";
diff --git a/nixos/tests/yggdrasil.nix b/nixos/tests/yggdrasil.nix
index 468fcf67127..1d7541308b4 100644
--- a/nixos/tests/yggdrasil.nix
+++ b/nixos/tests/yggdrasil.nix
@@ -7,6 +7,7 @@ let
     SigningPrivateKey = "fe3add8da35316c05f6d90d3ca79bd2801e6ccab6d37e5339fef4152589398abe2c43349083bc1e998e4ec4535b4c6a8f44ca9a5a8e07336561267253b2be5f4";
   };
   bobIp6 = "201:ebbd:bde9:f138:c302:4afa:1fb6:a19a";
+  bobPrefix = "301:ebbd:bde9:f138";
   bobConfig = {
     InterfacePeers = {
       eth1 = [ "tcp://192.168.1.200:12345" ];
@@ -18,6 +19,7 @@ let
     SigningPublicKey = "de111da0ec781e45bf6c63ecb45a78c24d7d4655abfaeea83b26c36eb5c0fd5b";
     SigningPrivateKey = "2a6c21550f3fca0331df50668ffab66b6dce8237bcd5728e571e8033b363e247de111da0ec781e45bf6c63ecb45a78c24d7d4655abfaeea83b26c36eb5c0fd5b";
   };
+  danIp6 = bobPrefix + "::2";
 
 in import ./make-test-python.nix ({ pkgs, ...} : {
   name = "yggdrasil";
@@ -69,6 +71,41 @@ in import ./make-test-python.nix ({ pkgs, ...} : {
                          text = builtins.toJSON bobConfig;
                        });
         };
+
+        boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = 1;
+
+        networking = {
+          bridges.br0.interfaces = [ ];
+          interfaces.br0 = {
+            ipv6.addresses = [{
+              address = bobPrefix + "::1";
+              prefixLength = 64;
+            }];
+          };
+        };
+
+        # dan is a node inside a container running on bob's host.
+        containers.dan = {
+          autoStart = true;
+          privateNetwork = true;
+          hostBridge = "br0";
+          config = { config, pkgs, ... }: {
+            networking.interfaces.eth0.ipv6 = {
+              addresses = [{
+                address = bobPrefix + "::2";
+                prefixLength = 64;
+              }];
+              routes = [{
+                address = "200::";
+                prefixLength = 7;
+                via = bobPrefix + "::1";
+              }];
+            };
+            services.httpd.enable = true;
+            services.httpd.adminAddr = "foo@example.org";
+            networking.firewall.allowedTCPPorts = [ 80 ];
+          };
+        };
       };
 
     # Carol only does local peering.  Carol's yggdrasil config is all Nix.
@@ -85,6 +122,7 @@ in import ./make-test-python.nix ({ pkgs, ...} : {
             MulticastInterfaces = [ "eth1" ];
             LinkLocalTCPPort = 43210;
           };
+          persistentKeys = true;
         };
       };
     };
@@ -99,7 +137,7 @@ in import ./make-test-python.nix ({ pkgs, ...} : {
 
       bob.start()
       carol.start()
-      bob.wait_for_unit("yggdrasil.service")
+      bob.wait_for_unit("default.target")
       carol.wait_for_unit("yggdrasil.service")
 
       ip_addr_show = "ip -o -6 addr show dev ygg0 scope global"
@@ -116,10 +154,13 @@ in import ./make-test-python.nix ({ pkgs, ...} : {
 
       carol.succeed("ping -c 1 ${aliceIp6}")
       carol.succeed("ping -c 1 ${bobIp6}")
+      carol.succeed("ping -c 1 ${bobPrefix}::1")
+      carol.succeed("ping -c 8 ${danIp6}")
 
       carol.fail("journalctl -u dhcpcd | grep ygg0")
 
       alice.wait_for_unit("httpd.service")
       carol.succeed("curl --fail -g http://[${aliceIp6}]")
+      carol.succeed("curl --fail -g http://[${danIp6}]")
     '';
 })
diff --git a/nixos/tests/zfs.nix b/nixos/tests/zfs.nix
index 7ba60ee9806..87e6c900c98 100644
--- a/nixos/tests/zfs.nix
+++ b/nixos/tests/zfs.nix
@@ -46,6 +46,17 @@ let
             "zpool destroy rpool",
             "udevadm settle",
         )
+
+        machine.succeed(
+            'echo password | zpool create -o altroot="/tmp/mnt" '
+            + "-O encryption=aes-256-gcm -O keyformat=passphrase rpool /dev/vdb1",
+            "zfs create -o mountpoint=legacy rpool/root",
+            "mount -t zfs rpool/root /tmp/mnt",
+            "udevadm settle",
+            "umount /tmp/mnt",
+            "zpool destroy rpool",
+            "udevadm settle",
+        )
       '' + extraTest;
 
     };
@@ -57,18 +68,6 @@ in {
 
   unstable = makeZfsTest "unstable" {
     enableUnstable = true;
-    extraTest = ''
-      machine.succeed(
-          'echo password | zpool create -o altroot="/tmp/mnt" '
-          + "-O encryption=aes-256-gcm -O keyformat=passphrase rpool /dev/vdb1",
-          "zfs create -o mountpoint=legacy rpool/root",
-          "mount -t zfs rpool/root /tmp/mnt",
-          "udevadm settle",
-          "umount /tmp/mnt",
-          "zpool destroy rpool",
-          "udevadm settle",
-      )
-    '';
   };
 
   installer = (import ./installer.nix { }).zfsroot;
diff --git a/nixos/tests/zigbee2mqtt.nix b/nixos/tests/zigbee2mqtt.nix
new file mode 100644
index 00000000000..b7bb21f9227
--- /dev/null
+++ b/nixos/tests/zigbee2mqtt.nix
@@ -0,0 +1,19 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+  {
+    machine = { pkgs, ... }:
+      {
+        services.zigbee2mqtt = {
+          enable = true;
+        };
+      };
+
+    testScript = ''
+      machine.wait_for_unit("zigbee2mqtt.service")
+      machine.wait_until_fails("systemctl status zigbee2mqtt.service")
+      machine.succeed(
+          "journalctl -eu zigbee2mqtt | grep \"Error: Error while opening serialport 'Error: Error: No such file or directory, cannot open /dev/ttyACM0'\""
+      )
+    '';
+  }
+)
diff --git a/nixos/tests/zoneminder.nix b/nixos/tests/zoneminder.nix
new file mode 100644
index 00000000000..a4e1a05ec0e
--- /dev/null
+++ b/nixos/tests/zoneminder.nix
@@ -0,0 +1,23 @@
+import ./make-test-python.nix ({ lib, ...}:
+
+{
+  name = "zoneminder";
+  meta.maintainers = with lib.maintainers; [ danielfullmer ];
+
+  machine = { ... }:
+  {
+    services.zoneminder = {
+      enable = true;
+      database.createLocally = true;
+      database.username = "zoneminder";
+    };
+    time.timeZone = "America/New_York";
+  };
+
+  testScript = ''
+    machine.wait_for_unit("zoneminder.service")
+    machine.wait_for_unit("nginx.service")
+    machine.wait_for_open_port(8095)
+    machine.succeed("curl --fail http://localhost:8095/")
+  '';
+})